diff options
661 files changed, 15311 insertions, 15289 deletions
diff --git a/.config/docker_example.env b/.config/docker_example.env new file mode 100644 index 0000000000..411d93659b --- /dev/null +++ b/.config/docker_example.env @@ -0,0 +1,5 @@ +# db settings +POSTGRES_PASSWORD="example-misskey-pass" +POSTGRES_USER="example-misskey-user" +POSTGRES_DB="misskey" + diff --git a/.config/example.yml b/.config/example.yml index 70c096baa1..48b1a0fd1c 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -1,8 +1,16 @@ +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# Misskey configuration +#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +# ┌─────┐ +#───┘ URL └───────────────────────────────────────────────────── + # Final accessible URL seen by a user. url: https://example.tld/ +# ┌───────────────────────┐ +#───┘ Port and TLS settings └─────────────────────────────────── -### Port and TLS settings ###################################### # # Misskey supports two deployment options for public. # @@ -30,28 +38,51 @@ url: https://example.tld/ # You need to set Certificate in 'https' section. # To use option 1, uncomment below line. -# port: 3000 # A port that your Misskey server should listen. +#port: 3000 # A port that your Misskey server should listen. # To use option 2, uncomment below lines. -# port: 443 -# -# https: -# # path for certification -# key: /etc/letsencrypt/live/example.tld/privkey.pem -# cert: /etc/letsencrypt/live/example.tld/fullchain.pem +#port: 443 -################################################################ +#https: +# # path for certification +# key: /etc/letsencrypt/live/example.tld/privkey.pem +# cert: /etc/letsencrypt/live/example.tld/fullchain.pem +# ┌──────────────────────────┐ +#───┘ PostgreSQL configuration └──────────────────────────────── -mongodb: +db: host: localhost - port: 27017 + port: 5432 + + # Database name db: misskey + + # Auth user: example-misskey-user pass: example-misskey-pass +# ┌─────────────────────┐ +#───┘ Redis configuration └───────────────────────────────────── + +redis: + host: localhost + port: 6379 + #pass: example-pass + +# ┌─────────────────────────────┐ +#───┘ Elasticsearch configuration └───────────────────────────── + +#elasticsearch: +# host: localhost +# port: 9200 +# pass: null + +# ┌────────────────────────────────────┐ +#───┘ File storage (Drive) configuration └────────────────────── + drive: - storage: 'db' + storage: 'fs' # OR @@ -88,25 +119,27 @@ drive: # accessKey: XXX # secretKey: YYY -# If enabled: -# The first account created is automatically marked as Admin. -autoAdmin: true +# ┌───────────────┐ +#───┘ ID generation └─────────────────────────────────────────── -# -# Below settings are optional -# +# You can select the ID generation method. +# You don't usually need to change this setting, but you can +# change it according to your preferences. -# Redis -#redis: -# host: localhost -# port: 6379 -# pass: example-pass +# Available methods: +# aid ... Short, Millisecond accuracy +# meid ... Similar to ObjectID, Millisecond accuracy +# ulid ... Millisecond accuracy +# objectid ... This is left for backward compatibility -# Elasticsearch -#elasticsearch: -# host: localhost -# port: 9200 -# pass: null +id: 'aid' + +# ┌─────────────────────┐ +#───┘ Other configuration └───────────────────────────────────── + +# If enabled: +# The first account created is automatically marked as Admin. +autoAdmin: true # Whether disable HSTS #disableHsts: true diff --git a/.config/mongo_initdb_example.js b/.config/mongo_initdb_example.js deleted file mode 100644 index b7e7321f35..0000000000 --- a/.config/mongo_initdb_example.js +++ /dev/null @@ -1,13 +0,0 @@ -var user = { - user: 'example-misskey-user', - pwd: 'example-misskey-pass', - roles: [ - { - role: 'readWrite', - db: 'misskey' - } - ] -}; - -db.createUser(user); - diff --git a/.dockerignore b/.dockerignore index a25d4e5718..324c4bce58 100755..100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,8 +5,8 @@ .vscode Dockerfile build/ +db/ docker-compose.yml +elasticsearch/ node_modules/ -mongo/ redis/ -elasticsearch/ diff --git a/.gitignore b/.gitignore index 6dd78fc970..650d4f6128 100644 --- a/.gitignore +++ b/.gitignore @@ -8,14 +8,14 @@ built /data /.cache-loader +/db +/elasticsearch npm-debug.log *.pem run.bat api-docs.json *.log /redis -/mongo -/elasticsearch *.code-workspace yarn.lock .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index fc621a66ca..e0aa4aad98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,32 @@ If you encounter any problems with updating, please try the following: 1. `npm run clean` or `npm run cleanall` 2. Retry update (Don't forget `npm i`) +11.0.0 (daybreak) +----------------- +* **データベースがMongoDBからPostgreSQLに変更されました** +* **Redisが必須に** +* アカウントを完全に削除できるように +* 投稿フォームで添付ファイルの閲覧注意を確認/設定できるように +* ミュート/ブロック時にそのユーザーの投稿のウォッチをすべて解除するように +* フォロー申請数が実際より1すくなくなる問題を修正 +* リストからアカウント削除したユーザーを削除できない問題を修正 +* リストTLでフォローしていないユーザーの非公開投稿が流れる問題を修正 +* リストTLでダイレクト投稿が流れない問題を修正 +* ミュートしているユーザーの投稿がタイムラインに流れてくることがある問題を修正 + +### APIの破壊的変更 +* v10時点で deprecated だったパラメータなどを削除 +* ユーザーリストの title が name に +* リバーシの対局の`settings`プロパティがなくなり、その中にあったプロパティがすべて上の階層に + * 例えば`game.settings.map`は`game.map`になる + +### 既知の問題 +* アプリが作成できない + * 依存ライブラリの問題と思わるため、対応が難しい + +### Migration +coming soon... + 10.100.0 ---------- * ユーザーリストでフォローボタンを表示するように diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1ad1f8041..6825225e0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,3 +75,95 @@ src ... Source code test ... Test code ``` + +## Notes +### placeholder +SQLをクエリビルダで組み立てる際、使用するプレースホルダは重複してはならない +例えば +``` ts +query.andWhere(new Brackets(qb => { + for (const type of ps.fileType) { + qb.orWhere(`:type = ANY(note.attachedFileTypes)`, { type: type }); + } +})); +``` +と書くと、ループ中で`type`というプレースホルダが複数回使われてしまいおかしくなる +だから次のようにする必要がある +```ts +query.andWhere(new Brackets(qb => { + for (const type of ps.fileType) { + const i = ps.fileType.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } +})); +``` + +### `null` in SQL +SQLを発行する際、パラメータが`null`になる可能性のある場合はSQL文を出し分けなければならない +例えば +``` ts +query.where('file.folderId = :folderId', { folderId: ps.folderId }); +``` +という処理で、`ps.folderId`が`null`だと結果的に`file.folderId = null`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない +だから次のようにする必要がある +``` ts +if (ps.folderId) { + query.where('file.folderId = :folderId', { folderId: ps.folderId }); +} else { + query.where('file.folderId IS NULL'); +} +``` + +### `[]` in SQL +SQLを発行する際、`IN`のパラメータが`[]`(空の配列)になる可能性のある場合はSQL文を出し分けなければならない +例えば +``` ts +const users = await Users.find({ + id: In(userIds) +}); +``` +という処理で、`userIds`が`[]`だと結果的に`user.id IN ()`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない +だから次のようにする必要がある +``` ts +const users = userIds.length > 0 ? await Users.find({ + id: In(userIds) +}) : []; +``` + +### 配列のインデックス in SQL +SQLでは配列のインデックスは**1始まり**。 +`[a, b, c]`の `a`にアクセスしたいなら`[0]`ではなく`[1]`と書く + +### `undefined`にご用心 +MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。 +MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください + +### 簡素な`undefined`チェック +データベースからレコードを取得するときに、プログラムの流れ的に(ほぼ)絶対`undefined`にはならない場合でも、`undefined`チェックしないとTypeScriptに怒られます。 +でもいちいち複数行を費やして、発生するはずのない`undefined`をチェックするのも面倒なので、`ensure`というユーティリティ関数を用意しています。 +例えば、 +``` ts +const user = await Users.findOne(userId); +// この時点で user の型は User | undefined +if (user == null) { + throw 'missing user'; +} +// この時点で user の型は User +``` +という処理を`ensure`を使うと +``` ts +const user = await Users.findOne(userId).then(ensure); +// この時点で user の型は User +``` +という風に書けます。 +もちろん`ensure`内部でエラーを握りつぶすようなことはしておらず、万が一`undefined`だった場合はPromiseがRejectされ後続の処理は実行されません。 +``` ts +const user = await Users.findOne(userId).then(ensure); +// 万が一 Users.findOne の結果が undefined だったら、ensure でエラーが発生するので +// この行に到達することは無い +// なので、.then(ensure) は +// if (user == null) { +// throw 'missing user'; +// } +// の糖衣構文のような扱いです +``` diff --git a/Dockerfile b/Dockerfile index ad04fb33dc..ec7d8a6a27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,8 +23,9 @@ RUN apk add --no-cache \ zlib-dev RUN npm i -g yarn -COPY . ./ +COPY package.json ./ RUN yarn install +COPY . ./ RUN yarn build FROM base AS runner @@ -1,4 +1,4 @@ -<a href="https://ai.misskey.xyz/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a> +<a href="https://xn--931a.moe/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a> [](https://misskey.xyz/) ================================================================ @@ -101,30 +101,33 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). ---------------------------------------------------------------- <!-- PATREON_START --> <table><tr> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5888816/36da0f7c15954df0ab13f9abdf227f66/1?token-time=2145916800&token-hash=HGkZJ7s4bSaQVoOJ5q30mTWHTxDLiw1LuyaogKPLy24%3D" alt="Hiroshi Seki" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/1?token-time=2145916800&token-hash=WeuDzzz24cRXJogyIkU-mxARqkdyms-rcZKbO-GpGjw%3D" alt="weep" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5888816/36da0f7c15954df0ab13f9abdf227f66/1.jpeg?token-time=2145916800&token-hash=at8QpJXJ8C0zINY_NmoMKv-MhXVoUK-YzTgaJPJzJYU%3D" alt="Hiroshi Seki" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3.png?token-time=2145916800&token-hash=oH_i7gJjNT7Ot6j9JiVwy7ZJIBqACVnzLqlz4YrDAZA%3D" alt="weep" width="100"></td> <td><img src="https://c8.patreon.com/2/200/12059069" alt="naga_rus" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4?token-time=2145916800&token-hash=vZdDTTF-ahiKBjjgppS2ev4rkD8H7TTKkXXoxsucs6Y%3D" alt="Melilot" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1?token-time=2145916800&token-hash=ubqJzjhBQUo8Nw6h_8jMDlJ5ocIO46EflpiRkp2jIw4%3D" alt="osapon" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13099460/43cecdbaa63a40d79bf50a96b9910b9d/1.jpe?token-time=2145916800&token-hash=bqwLTk0Wo0hUJJ8J5y7ii05bLzz-_CDA7Bo0Mp4RFU0%3D" alt="ne_moni" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4.jpe?token-time=2145916800&token-hash=zEyJqVM7u9d8Ri-65fJYSJcWF1jBH1nJ5a3taRzrTmw%3D" alt="Melilot" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1.jpg?token-time=2145916800&token-hash=mPLM9CA-riFHx-myr3bLZJuH2xBRHA9se5VbHhLIOuA%3D" alt="osapon" width="100"></td> <td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td> </tr><tr> <td><a href="https://www.patreon.com/rane_hs">Hiroshi Seki</a></td> <td><a href="https://www.patreon.com/weepjp">weep</a></td> <td><a href="https://www.patreon.com/user?u=12059069">naga_rus</a></td> +<td><a href="https://www.patreon.com/user?u=13099460">ne_moni</a></td> <td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td> <td><a href="https://www.patreon.com/osapon">osapon</a></td> <td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td> </tr></table> <table><tr> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=1FlxS9MEgmNGH_RHUVHbO5hIXB5I1z0lvA33CTvYvjA%3D" alt="gutfuckllc" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1?token-time=2145916800&token-hash=0xgcpqvFDqRcV_YIEhcPNVH7gs9sLg_BBnTJXCkN4ao%3D" alt="mydarkstar" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1.png?token-time=2145916800&token-hash=FMV7cPKBD1TU2WTbl1jg6AcdKSvTb2BSFcDhgc-EO8w%3D" alt="gutfuckllc" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/11357794/923ce94cd8c44ba788ee931907881839/1.png?token-time=2145916800&token-hash=9nEQje_eMvUjq9a7L3uBqW-MQbS-rRMaMgd7UYVoFNM%3D" alt="mydarkstar" width="100"></td> <td><img src="https://c8.patreon.com/2/200/12718187" alt="Peter G." width="100"></td> <td><img src="https://c8.patreon.com/2/200/18833336" alt="itiradi" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1?token-time=2145916800&token-hash=2PsbFNw0tnubZzgSXD01R6hIgncfiElG7H7HX2Y3dyo%3D" alt="nemu" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13039004/509d0c412eb14ae08d6a812a3054f7d6/1.jpe?token-time=2145916800&token-hash=UQRWf01TwHDV4Cls1K0YAOAjM29ssif7hLVq0ESQ0hs%3D" alt="nemu" width="100"></td> <td><img src="https://c8.patreon.com/2/200/17866454" alt="sikyosyounin" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3?token-time=2145916800&token-hash=9JtETp0X8gI280Ne1E8bxn6j4Lw5o2k4mJkICx97V_k%3D" alt="YUKIMOCHI" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5881381/6235ca5d3fb04c8e95ef5b4ff2abcc18/3.png?token-time=2145916800&token-hash=KjfQL8nf3AIf6WqzLshBYAyX44piAqOAZiYXgZS_H6A%3D" alt="YUKIMOCHI" width="100"></td> <td><img src="https://c8.patreon.com/2/200/17463605" alt="Sampot" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1?token-time=2145916800&token-hash=95p8VdGX45E8BitZR_eOcDlqCjumjzNLBPQJrJdeCpI%3D" alt="takimura" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13737140/1adf7835017d479280d90fe8d30aade2/1.png?token-time=2145916800&token-hash=0pdle8h5pDZrww0BDOjdz6zO-HudeGTh36a3qi1biVU%3D" alt="Satsuki Yanagi" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17880724/311738c8a48f4a6b9443c2445a75adde/1.jpe?token-time=2145916800&token-hash=CPxGQhKIlEaa6WUcgbyHixyKEhakiw9RFdOhsIJBQ_o%3D" alt="takimura" width="100"></td> </tr><tr> <td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td> <td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td> @@ -134,18 +137,19 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). <td><a href="https://www.patreon.com/user?u=17866454">sikyosyounin</a></td> <td><a href="https://www.patreon.com/yukimochi">YUKIMOCHI</a></td> <td><a href="https://www.patreon.com/user?u=17463605">Sampot</a></td> +<td><a href="https://www.patreon.com/user?u=13737140">Satsuki Yanagi</a></td> <td><a href="https://www.patreon.com/takimura">takimura</a></td> </tr></table> <table><tr> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17195955/be45e5e14c3e48b2bee0456c84e19df4/4?token-time=2145916800&token-hash=SbdZeN5SmsuT9stD6v0jN1z0hftg0FmRiCTxysU0Ihw%3D" alt="Damillora" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/935a10339daa4ede8e555903a0707060/1?token-time=2145916800&token-hash=3CrpqH-XtKs_NoIlSsTyVs8wCzP1WFCsG2xwps1IJq0%3D" alt="Atsuko Tominaga" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3?token-time=2145916800&token-hash=-iJszBqgYBhsM5qMdA1knf9wvprhEfESzKfR2oh7mIA%3D" alt="natalie" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=5T8XcaAf9Zyzfg3QubR06s_kJZkArVEM2dwObrBVAU4%3D" alt="Hiratake" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1?token-time=2145916800&token-hash=D6QK3fPyqiYKJfOzc-QqaSSairUrWdjld-ewp2waj6s%3D" alt="Hekovic" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=Ksk_2l3gjPDbnzMUOCSW1E-hdPJsNs2tSR4_RAakRK8%3D" alt="dansup" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=CXe9AqlZy9AsYfiWd3OBYVOzvODoN47Litz0Tu4BFpU%3D" alt="Gargron" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1?token-time=2145916800&token-hash=xhR1n6NAAyEb-IUXLD6_dshkFa3mefU5ZZuk1L8qKTs%3D" alt="Nokotaro Takeda" width="100"></td> -<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1?token-time=2145916800&token-hash=uR-48MQ0A4j0irQSrCAQZJ-sJUSs_Fkihlg3-l59b7c%3D" alt="Takashi Shibuya" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/17195955/be45e5e14c3e48b2bee0456c84e19df4/4.jpe?token-time=2145916800&token-hash=UslrPVM-8TXOe8AapuNiaFYjcIJgPNcU-fKpGbfGJNI%3D" alt="Damillora" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/16900731/935a10339daa4ede8e555903a0707060/1.png?token-time=2145916800&token-hash=c1XAS1qGBPxVdCvnICxtAUmx41mVkMG87h7cIRF9YYE%3D" alt="Atsuko Tominaga" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/3.png?token-time=2145916800&token-hash=FTm3WVom4dJ9NwWMU4OpCL_8Yc13WiwEbKrDPyTZTPs%3D" alt="natalie" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1.jpe?token-time=2145916800&token-hash=EWxXhVbZYH7KB4IDT3joc8TbIg8zPO40x1r5IDn3R7c%3D" alt="Hiratake" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/18072312/98e894d960314fa7bc236a72a39488fe/1.jpe?token-time=2145916800&token-hash=qA8j97lIZNc-74AuZ0p4F3ms6sKPeKjtNt2vEuwpsyo%3D" alt="Hekovic" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1.jpeg?token-time=2145916800&token-hash=L55UhJ0rcuNAH3w_ryeeGN4hC6taoOixyAhraEi0bzw%3D" alt="dansup" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1.jpeg?token-time=2145916800&token-hash=d8jBQLMOHD87KtXs5C9fk1o58DMF73pQ-dYH3uZJPBE%3D" alt="Gargron" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5731881/4b6038e6cda34c04b83a5fcce3806a93/1.png?token-time=2145916800&token-hash=hBayGfOmQH3kRMdNnDe4oCZD_9fsJWSt29xXR3KRMVk%3D" alt="Nokotaro Takeda" width="100"></td> +<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12531784/93a45137841849329ba692da92ac7c60/1.jpeg?token-time=2145916800&token-hash=vGe7wXGqmA8Q7m-kDNb6fyGdwk-Dxk4F-ut8ZZu51RM%3D" alt="Takashi Shibuya" width="100"></td> </tr><tr> <td><a href="https://www.patreon.com/damillora">Damillora</a></td> <td><a href="https://www.patreon.com/user?u=16900731">Atsuko Tominaga</a></td> @@ -157,8 +161,11 @@ Please see the [Contribution Guide](./CONTRIBUTING.md). <td><a href="https://www.patreon.com/takenoko">Nokotaro Takeda</a></td> <td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td> </tr></table> +<table><tr> +</tr><tr> +</tr></table> -**Last updated:** Sat, 06 Apr 2019 03:35:05 UTC +**Last updated:** Sun, 14 Apr 2019 08:13:12 UTC <!-- PATREON_END --> :four_leaf_clover: Copyright diff --git a/cli/mark-admin.js b/cli/mark-admin.js deleted file mode 100644 index e10035fde9..0000000000 --- a/cli/mark-admin.js +++ /dev/null @@ -1,23 +0,0 @@ -const mongo = require('mongodb'); -const User = require('../built/models/user').default; - -const args = process.argv.slice(2); - -const user = args[0]; - -const q = user.startsWith('@') ? { - username: user.split('@')[1], - host: user.split('@')[2] || null -} : { _id: new mongo.ObjectID(user) }; - -console.log(`Mark as admin ${user}...`); - -User.update(q, { - $set: { - isAdmin: true - } -}).then(() => { - console.log(`Done ${user}`); -}, e => { - console.error(e); -}); diff --git a/cli/migration/2.0.0.js b/cli/migration/2.0.0.js deleted file mode 100644 index f7298972e5..0000000000 --- a/cli/migration/2.0.0.js +++ /dev/null @@ -1,57 +0,0 @@ -// for Node.js interpret - -const chalk = require('chalk'); -const sequential = require('promise-sequential'); - -const { default: User } = require('../../built/models/user'); -const { default: DriveFile } = require('../../built/models/drive-file'); - -async function main() { - const promiseGens = []; - - const count = await DriveFile.count({}); - - let prev; - - for (let i = 0; i < count; i++) { - promiseGens.push(() => { - const promise = new Promise(async (res, rej) => { - const file = await DriveFile.findOne(prev ? { - _id: { $gt: prev._id } - } : {}, { - sort: { - _id: 1 - } - }); - - prev = file; - - const user = await User.findOne({ _id: file.metadata.userId }); - - DriveFile.update({ - _id: file._id - }, { - $set: { - 'metadata._user': { - host: user.host - } - } - }).then(() => { - res([i, file]); - }).catch(rej); - }); - - promise.then(([i, file]) => { - console.log(chalk`{gray ${i}} {green done: {bold ${file._id}} ${file.filename}}`); - }); - - return promise; - }); - } - - return await sequential(promiseGens); -} - -main().then(() => { - console.log('ALL DONE'); -}).catch(console.error); diff --git a/cli/migration/2.4.0.js b/cli/migration/2.4.0.js deleted file mode 100644 index aa37849aa1..0000000000 --- a/cli/migration/2.4.0.js +++ /dev/null @@ -1,71 +0,0 @@ -// for Node.js interpret - -const chalk = require('chalk'); -const sequential = require('promise-sequential'); - -const { default: User } = require('../../built/models/user'); -const { default: DriveFile } = require('../../built/models/drive-file'); - -async function main() { - const promiseGens = []; - - const count = await User.count({}); - - let prev; - - for (let i = 0; i < count; i++) { - promiseGens.push(() => { - const promise = new Promise(async (res, rej) => { - const user = await User.findOne(prev ? { - _id: { $gt: prev._id } - } : {}, { - sort: { - _id: 1 - } - }); - - prev = user; - - const set = {}; - - if (user.avatarId != null) { - const file = await DriveFile.findOne({ _id: user.avatarId }); - - if (file && file.metadata.properties.avgColor) { - set.avatarColor = file.metadata.properties.avgColor; - } - } - - if (user.bannerId != null) { - const file = await DriveFile.findOne({ _id: user.bannerId }); - - if (file && file.metadata.properties.avgColor) { - set.bannerColor = file.metadata.properties.avgColor; - } - } - - if (Object.keys(set).length === 0) return res([i, user]); - - User.update({ - _id: user._id - }, { - $set: set - }).then(() => { - res([i, user]); - }).catch(rej); - }); - - promise.then(([i, user]) => { - console.log(chalk`{gray ${i}} {green done: {bold ${user._id}} @${user.username}}`); - }); - - return promise; - }); - } - - return await sequential(promiseGens); -} - -main().then(() => { - console.log('ALL DONE'); -}).catch(console.error); diff --git a/cli/migration/5.0.0.js b/cli/migration/5.0.0.js deleted file mode 100644 index bef103fe4a..0000000000 --- a/cli/migration/5.0.0.js +++ /dev/null @@ -1,9 +0,0 @@ -const { default: DriveFile } = require('../../built/models/drive-file'); - -DriveFile.update({}, { - $rename: { - 'metadata.isMetaOnly': 'metadata.withoutChunks' - } -}, { - multi: true -}); diff --git a/cli/migration/7.0.0.js b/cli/migration/7.0.0.js deleted file mode 100644 index fa5e363db8..0000000000 --- a/cli/migration/7.0.0.js +++ /dev/null @@ -1,134 +0,0 @@ -const { default: Stats } = require('../../built/models/stats'); -const { default: User } = require('../../built/models/user'); -const { default: Note } = require('../../built/models/note'); -const { default: DriveFile } = require('../../built/models/drive-file'); - -const now = new Date(); -const y = now.getFullYear(); -const m = now.getMonth(); -const d = now.getDate(); -const today = new Date(y, m, d); - -async function main() { - const localUsersCount = await User.count({ - host: null - }); - - const remoteUsersCount = await User.count({ - host: { $ne: null } - }); - - const localNotesCount = await Note.count({ - '_user.host': null - }); - - const remoteNotesCount = await Note.count({ - '_user.host': { $ne: null } - }); - - const localDriveFilesCount = await DriveFile.count({ - 'metadata._user.host': null - }); - - const remoteDriveFilesCount = await DriveFile.count({ - 'metadata._user.host': { $ne: null } - }); - - const localDriveFilesSize = await DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': null, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(aggregates => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - const remoteDriveFilesSize = await DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': { $ne: null }, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(aggregates => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - await Stats.insert({ - date: today, - users: { - local: { - total: localUsersCount, - diff: 0 - }, - remote: { - total: remoteUsersCount, - diff: 0 - } - }, - notes: { - local: { - total: localNotesCount, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }, - remote: { - total: remoteNotesCount, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - } - }, - drive: { - local: { - totalCount: localDriveFilesCount, - totalSize: localDriveFilesSize, - diffCount: 0, - diffSize: 0 - }, - remote: { - totalCount: remoteDriveFilesCount, - totalSize: remoteDriveFilesSize, - diffCount: 0, - diffSize: 0 - } - } - }); - - console.log('done'); -} - -main(); diff --git a/cli/migration/8.0.0.js b/cli/migration/8.0.0.js deleted file mode 100644 index fd6cb24525..0000000000 --- a/cli/migration/8.0.0.js +++ /dev/null @@ -1,144 +0,0 @@ -const { default: Stats } = require('../../built/models/stats'); -const { default: User } = require('../../built/models/user'); -const { default: Note } = require('../../built/models/note'); -const { default: DriveFile } = require('../../built/models/drive-file'); - -const now = new Date(); -const y = now.getFullYear(); -const m = now.getMonth(); -const d = now.getDate(); -const h = now.getHours(); -const date = new Date(y, m, d, h); - -async function main() { - await Stats.update({}, { - $set: { - span: 'day' - } - }, { - multi: true - }); - - const localUsersCount = await User.count({ - host: null - }); - - const remoteUsersCount = await User.count({ - host: { $ne: null } - }); - - const localNotesCount = await Note.count({ - '_user.host': null - }); - - const remoteNotesCount = await Note.count({ - '_user.host': { $ne: null } - }); - - const localDriveFilesCount = await DriveFile.count({ - 'metadata._user.host': null - }); - - const remoteDriveFilesCount = await DriveFile.count({ - 'metadata._user.host': { $ne: null } - }); - - const localDriveFilesSize = await DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': null, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(aggregates => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - const remoteDriveFilesSize = await DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': { $ne: null }, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(aggregates => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - await Stats.insert({ - date: date, - span: 'hour', - users: { - local: { - total: localUsersCount, - diff: 0 - }, - remote: { - total: remoteUsersCount, - diff: 0 - } - }, - notes: { - local: { - total: localNotesCount, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }, - remote: { - total: remoteNotesCount, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - } - }, - drive: { - local: { - totalCount: localDriveFilesCount, - totalSize: localDriveFilesSize, - diffCount: 0, - diffSize: 0 - }, - remote: { - totalCount: remoteDriveFilesCount, - totalSize: remoteDriveFilesSize, - diffCount: 0, - diffSize: 0 - } - } - }); - - console.log('done'); -} - -main(); diff --git a/docker-compose.yml b/docker-compose.yml index 7ff8f6a268..12c7f514cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,8 @@ services: build: . restart: always links: - - mongo -# - redis + - db + - redis # - es ports: - "127.0.0.1:3000:3000" @@ -14,26 +14,23 @@ services: - internal_network - external_network -# redis: -# restart: always -# image: redis:4.0-alpine -# networks: -# - internal_network -### Uncomment to enable Redis persistance -## volumes: -## - ./redis:/data + redis: + restart: always + image: redis:4.0-alpine + networks: + - internal_network + volumes: + - ./redis:/data - mongo: + db: restart: always - image: mongo:4.1 + image: postgres:11.2-alpine networks: - internal_network - environment: - MONGO_INITDB_DATABASE: "misskey" + env_file: + - .config/docker.env volumes: - - ./.config/mongo_initdb.js:/docker-entrypoint-initdb.d/mongo_initdb.js:ro -### Uncomment to enable MongoDB persistance -# - ./mongo:/data + - ./db:/var/lib/postgresql/data # es: # restart: always @@ -42,9 +39,8 @@ services: # - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # networks: # - internal_network -#### Uncomment to enable ES persistence -## volumes: -## - ./elasticsearch:/usr/share/elasticsearch/data +# volumes: +# - ./elasticsearch:/usr/share/elasticsearch/data networks: internal_network: diff --git a/docs/backup.fr.md b/docs/backup.fr.md deleted file mode 100644 index 19e99068ce..0000000000 --- a/docs/backup.fr.md +++ /dev/null @@ -1,22 +0,0 @@ -Comment faire une sauvegarde de votre Misskey ? -========================== - -Assurez-vous d'avoir installé **mongodb-tools**. - ---- - -Dans votre terminal : -``` shell -$ mongodump --archive=db-backup -u <VotreNomdUtilisateur> -p <VotreMotDePasse> -``` - -Pour plus de détails, merci de consulter [la documentation de mongodump](https://docs.mongodb.com/manual/reference/program/mongodump/). - -Restauration -------- - -``` shell -$ mongorestore --archive=db-backup -``` - -Pour plus de détails, merci de consulter [la documentation de mongorestore](https://docs.mongodb.com/manual/reference/program/mongorestore/). diff --git a/docs/backup.md b/docs/backup.md deleted file mode 100644 index a69af0255b..0000000000 --- a/docs/backup.md +++ /dev/null @@ -1,22 +0,0 @@ -How to backup your Misskey -========================== - -Make sure **mongodb-tools** installed. - ---- - -In your shell: -``` shell -$ mongodump --archive=db-backup -u <YourUserName> -p <YourPassword> -``` - -For details, please see [mongodump docs](https://docs.mongodb.com/manual/reference/program/mongodump/). - -Restore -------- - -``` shell -$ mongorestore --archive=db-backup -``` - -For details, please see [mongorestore docs](https://docs.mongodb.com/manual/reference/program/mongorestore/). diff --git a/docs/docker.en.md b/docs/docker.en.md index f0fcdb66d5..1b607f9eae 100644 --- a/docs/docker.en.md +++ b/docs/docker.en.md @@ -11,13 +11,41 @@ This guide describes how to install and setup Misskey with Docker. ---------------------------------------------------------------- 1. `git clone -b master git://github.com/syuilo/misskey.git` Clone Misskey repository's master branch. 2. `cd misskey` Move to misskey directory. -3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) tag. +3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) tag. *2.* Configure Misskey ---------------------------------------------------------------- -1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. -2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` Copy the `.config/mongo_initdb_example.js` and rename it to `mongo_initdb.js`. -3. Edit `default.yml` and `mongo_initdb.js`. + +Create configuration files with following: + +```bash +cd .config +cp example.yml default.yml +cp docker_example.env docker.env +``` + +### `default.yml` + +Edit this file the same as non-Docker environment. +However hostname of Postgresql, Redis and Elasticsearch are not `localhost`, they are set in `docker-compose.yml`. +The following is default hostname: + +| Service | Hostname | +|---------------|----------| +| Postgresql | `db` | +| Redis | `redis` | +| Elasticsearch | `es` | + +### `docker.env` + +Configure Postgresql in this file. +The minimum required settings are: + +| name | Description | +|---------------------|---------------| +| `POSTGRES_PASSWORD` | Password | +| `POSTGRES_USER` | Username | +| `POSTGRES_DB` | Database name | *3.* Configure Docker ---------------------------------------------------------------- @@ -39,7 +67,7 @@ Just `docker-compose up -d`. GLHF! ### How to update your Misskey server to the latest version 1. `git fetch` 2. `git stash` -3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` +3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` 4. `git stash pop` 5. `docker-compose build` 6. Check [ChangeLog](../CHANGELOG.md) for migration information diff --git a/docs/docker.fr.md b/docs/docker.fr.md index 8f7e9f4294..e89a8f1b15 100644 --- a/docs/docker.fr.md +++ b/docs/docker.fr.md @@ -12,7 +12,7 @@ Ce guide explique comment installer et configurer Misskey avec Docker. ---------------------------------------------------------------- 1. `git clone -b master git://github.com/syuilo/misskey.git` Clone le dépôt de Misskey sur la branche master. 2. `cd misskey` Naviguez dans le dossier du dépôt. -3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout sur le tag de la [dernière version](https://github.com/syuilo/misskey/releases/latest). +3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` Checkout sur le tag de la [dernière version](https://github.com/syuilo/misskey/releases/latest). *2.* Configuration de Misskey ---------------------------------------------------------------- @@ -40,7 +40,7 @@ Utilisez la commande `docker-compose up -d`. GLHF! ### How to update your Misskey server to the latest version 1. `git fetch` 2. `git stash` -3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` +3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` 4. `git stash pop` 5. `docker-compose build` 6. Consultez le [ChangeLog](../CHANGELOG.md) pour avoir les éventuelles informations de migration diff --git a/docs/docker.ja.md b/docs/docker.ja.md index 0baf285728..ecc75fef2e 100644 --- a/docs/docker.ja.md +++ b/docs/docker.ja.md @@ -11,13 +11,41 @@ Dockerを使ったMisskey構築方法 ---------------------------------------------------------------- 1. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン 2. `cd misskey` misskeyディレクトリに移動 -3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認 +3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認 -*2.* 設定ファイルを作成する +*2.* 設定ファイルの作成と編集 ---------------------------------------------------------------- -1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする -2. `cp .config/mongo_initdb_example.js .config/mongo_initdb.js` `.config/mongo_initdb_example.js`をコピーし名前を`mongo_initdb.js`にする -3. `default.yml`と`mongo_initdb.js`を編集する + +下記コマンドで設定ファイルを作成してください。 + +```bash +cd .config +cp example.yml default.yml +cp docker_example.env docker.env +``` + +### `default.yml`の編集 + +非Docker環境と同じ様に編集してください。 +ただし、Postgresql、RedisとElasticsearchのホストは`localhost`ではなく、`docker-compose.yml`で設定されたサービス名になっています。 +標準設定では次の通りです。 + +| サービス | ホスト名 | +|---------------|---------| +| Postgresql |`db` | +| Redis |`redis` | +| Elasticsearch |`es` | + +### `docker.env`の編集 + +このファイルはPostgresqlの設定を記述します。 +最低限記述する必要がある設定は次の通りです。 + +| 設定 | 内容 | +|---------------------|--------------| +| `POSTGRES_PASSWORD` | パスワード | +| `POSTGRES_USER` | ユーザー名 | +| `POSTGRES_DB` | データベース名 | *3.* Dockerの設定 ---------------------------------------------------------------- @@ -39,7 +67,7 @@ Dockerを使ったMisskey構築方法 ### Misskeyを最新バージョンにアップデートする方法: 1. `git fetch` 2. `git stash` -3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` +3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` 4. `git stash pop` 5. `docker-compose build` 6. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する diff --git a/docs/setup.en.md b/docs/setup.en.md index 1125081445..45e3e2c68b 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -22,37 +22,28 @@ adduser --disabled-password --disabled-login misskey Please install and setup these softwares: #### Dependencies :package: -* **[Node.js](https://nodejs.org/en/)** >= 10.0.0 -* **[MongoDB](https://www.mongodb.com/)** >= 3.6 +* **[Node.js](https://nodejs.org/en/)** >= 11.7.0 +* **[PostgreSQL](https://www.postgresql.org/)** >= 10 +* **[Redis](https://redis.io/)** ##### Optional -* [Redis](https://redis.io/) - * Redis is optional, but we strongly recommended to install it * [Elasticsearch](https://www.elastic.co/) - required to enable the search feature * [FFmpeg](https://www.ffmpeg.org/) -*3.* Setup MongoDB ----------------------------------------------------------------- -As root: -1. `mongo` Go to the mongo shell -2. `use misskey` Use the misskey database -3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Create the misskey user. -4. `exit` You're done! - -*4.* Install Misskey +*3.* Install Misskey ---------------------------------------------------------------- 1. `su - misskey` Connect to misskey user. 2. `git clone -b master git://github.com/syuilo/misskey.git` Clone the misskey repo from master branch. 3. `cd misskey` Navigate to misskey directory -4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) +4. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) 5. `npm install` Install misskey dependencies. -*5.* Configure Misskey +*4.* Configure Misskey ---------------------------------------------------------------- 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. 2. Edit `default.yml` -*6.* Build Misskey +*5.* Build Misskey ---------------------------------------------------------------- Build misskey with the following: @@ -68,6 +59,12 @@ If you're still encountering errors about some modules, use node-gyp: 3. `node-gyp build` 4. `NODE_ENV=production npm run build` +*6.* Init DB +---------------------------------------------------------------- +``` shell +npm run init +``` + *7.* That is it. ---------------------------------------------------------------- Well done! Now, you have an environment that run to Misskey. @@ -107,7 +104,7 @@ You can check if the service is running with `systemctl status misskey`. ### How to update your Misskey server to the latest version 1. `git fetch` -2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` +2. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` 3. `npm install` 4. `NODE_ENV=production npm run build` 5. Check [ChangeLog](../CHANGELOG.md) for migration information diff --git a/docs/setup.fr.md b/docs/setup.fr.md index 959ec3392f..e6d36aeff8 100644 --- a/docs/setup.fr.md +++ b/docs/setup.fr.md @@ -22,37 +22,28 @@ adduser --disabled-password --disabled-login misskey Installez les paquets suivants : #### Dépendences :package: -* **[Node.js](https://nodejs.org/en/)** >= 10.0.0 -* **[MongoDB](https://www.mongodb.com/)** >= 3.6 +* **[Node.js](https://nodejs.org/en/)** >= 11.7.0 +* **[PostgreSQL](https://www.postgresql.org/)** >= 10 +* **[Redis](https://redis.io/)** ##### Optionnels -* [Redis](https://redis.io/) - * Redis est optionnel mais nous vous recommandons vivement de l'installer * [Elasticsearch](https://www.elastic.co/) - requis pour pouvoir activer la fonctionnalité de recherche * [FFmpeg](https://www.ffmpeg.org/) -*3.* Paramètrage de MongoDB ----------------------------------------------------------------- -En root : -1. `mongo` Ouvrez le shell mongo -2. `use misskey` Utilisez la base de données misskey -3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` Créez l'utilisateur misskey. -4. `exit` Vous avez terminé ! - -*4.* Installation de Misskey +*3.* Installation de Misskey ---------------------------------------------------------------- 1. `su - misskey` Basculez vers l'utilisateur misskey. 2. `git clone -b master git://github.com/syuilo/misskey.git` Clonez la branche master du dépôt misskey. 3. `cd misskey` Accédez au dossier misskey. -4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout sur le tag de la [version la plus récente](https://github.com/syuilo/misskey/releases/latest) +4. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` Checkout sur le tag de la [version la plus récente](https://github.com/syuilo/misskey/releases/latest) 5. `npm install` Installez les dépendances de misskey. -*5.* Création du fichier de configuration +*4.* Création du fichier de configuration ---------------------------------------------------------------- 1. `cp .config/example.yml .config/default.yml` Copiez le fichier `.config/example.yml` et renommez-le`default.yml`. 2. Editez le fichier `default.yml` -*6.* Construction de Misskey +*5.* Construction de Misskey ---------------------------------------------------------------- Construisez Misskey comme ceci : @@ -68,7 +59,7 @@ Si vous rencontrez des erreurs concernant certains modules, utilisez node-gyp: 3. `node-gyp build` 4. `NODE_ENV=production npm run build` -*7.* C'est tout. +*6.* C'est tout. ---------------------------------------------------------------- Excellent ! Maintenant, vous avez un environnement prêt pour lancer Misskey @@ -107,7 +98,7 @@ Vous pouvez vérifier si le service a démarré en utilisant la commande `system ### Méthode de mise à jour vers la plus récente version de Misskey 1. `git fetch` -2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` +2. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` 3. `npm install` 4. `NODE_ENV=production npm run build` 5. Consultez [ChangeLog](../CHANGELOG.md) pour les information de migration. diff --git a/docs/setup.ja.md b/docs/setup.ja.md index 8a21e104d6..1b1730b69e 100644 --- a/docs/setup.ja.md +++ b/docs/setup.ja.md @@ -22,44 +22,29 @@ adduser --disabled-password --disabled-login misskey これらのソフトウェアをインストール・設定してください: #### 依存関係 :package: -* **[Node.js](https://nodejs.org/en/)** (10.0.0以上) -* **[MongoDB](https://www.mongodb.com/)** (3.6以上) +* **[Node.js](https://nodejs.org/en/)** (11.7.0以上) +* **[PostgreSQL](https://www.postgresql.org/)** (10以上) +* **[Redis](https://redis.io/)** ##### オプション -* [Redis](https://redis.io/) - * Redisはオプションですが、インストールすることを強く推奨します。 - * インストールしなくていいのは、あなたのインスタンスが自分専用のときだけとお考えください。 - * 具体的には、Redisをインストールしないと、次の事が出来なくなります: - * Misskeyプロセスを複数起動しての負荷分散 - * レートリミット - * ジョブキュー - * Twitter連携 * [Elasticsearch](https://www.elastic.co/) * 検索機能を有効にするためにはインストールが必要です。 * [FFmpeg](https://www.ffmpeg.org/) -*3.* MongoDBの設定 ----------------------------------------------------------------- -ルートで: -1. `mongo` mongoシェルを起動 -2. `use misskey` misskeyデータベースを使用 -3. `db.createUser( { user: "misskey", pwd: "<password>", roles: [ { role: "readWrite", db: "misskey" } ] } )` misskeyユーザーを作成 -4. `exit` mongoシェルを終了 - -*4.* Misskeyのインストール +*3.* Misskeyのインストール ---------------------------------------------------------------- 1. `su - misskey` misskeyユーザーを使用 2. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン 3. `cd misskey` misskeyディレクトリに移動 -4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認 +4. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認 5. `npm install` Misskeyの依存パッケージをインストール -*5.* 設定ファイルを作成する +*4.* 設定ファイルを作成する ---------------------------------------------------------------- 1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。 2. `default.yml` を編集する。 -*6.* Misskeyのビルド +*5.* Misskeyのビルド ---------------------------------------------------------------- 次のコマンドでMisskeyをビルドしてください: @@ -74,6 +59,12 @@ Debianをお使いであれば、`build-essential`パッケージをインスト 3. `node-gyp build` 4. `NODE_ENV=production npm run build` +*6.* データベースを初期化 +---------------------------------------------------------------- +``` shell +npm run init +``` + *7.* 以上です! ---------------------------------------------------------------- お疲れ様でした。これでMisskeyを動かす準備は整いました。 @@ -113,7 +104,7 @@ CentOSで1024以下のポートを使用してMisskeyを使用する場合は`Ex ### Misskeyを最新バージョンにアップデートする方法: 1. `git fetch` -2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` +2. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` 3. `npm install` 4. `NODE_ENV=production npm run build` 5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する diff --git a/gulpfile.ts b/gulpfile.ts index b2956c2403..2242843db1 100644 --- a/gulpfile.ts +++ b/gulpfile.ts @@ -120,7 +120,7 @@ gulp.task('copy:client', () => ]) .pipe(isProduction ? (imagemin as any)() : gutil.noop()) .pipe(rename(path => { - path.dirname = path.dirname.replace('assets', '.'); + path.dirname = path.dirname!.replace('assets', '.'); })) .pipe(gulp.dest('./built/client/assets/')) ); @@ -1 +1 @@ -require('./built'); +require('./built').default(); diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 34148c97d9..eaba7edc7d 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -20,7 +20,7 @@ common: outro: "Podívejte se na unikátní vlastnosti Misskey vlastníma očima! Pokud si myslíte, že tato instance není pro vás, zkuste jiné instance, neboť Misskey je decentralizovaná sociální síť, takže můžete snadno najít své přátele. Hodně štěstí a zábavy!" adblock: detected: "Prosím vypněte svůj blokovač reklam" - warning: "<strong>Misskey nepoužívá reklamy</strong>, některé vlastnosti však mohou být nedostupné nebo mohou způsobovat chyby, pokud máte povolený blokovač reklam." + warning: "Některé vlastnosti mohou být nedostupné nebo mohou způsobovat chyby, pokud máte povolený blokovač reklam. <strong>Misskey nepoužívá reklamy</strong>." application-authorization: "Autorizované aplikace" close: "Zavřít" do-not-copy-paste: "Prosím nezadávejte ani nevkládejte sem kód. Váš účet může být kompromitován." @@ -227,6 +227,7 @@ common: cancel: "Zrušit" update-available-title: "Aktualizace k dispozici" update-available: "Je k dispozici nová verze Misskey ({newer},vaše verze je {current}). Pro aplikování nové verze znovunačtěte stránku." + my-token-regenerated: "Váš token byl regenerován, proto budete odhlášen/a." verified-user: "Ověřené účty" hide-password: "Skrýt heslo" show-password: "Zobrazit heslo" @@ -281,12 +282,6 @@ common: auth/views/form.vue: share-access: "Chcete dovolit aplikaci <i>{name}</i> přístup k vašemu účtu?" permission-ask: "Tato aplikace vyžaduje následující oprávnění:" - account-read: "Zobrazit informace účtu" - note-write: "Odeslat." - following-write: "Sledovat a přestat sledovat" - drive-read: "Přečíst váš Disk" - notification-read: "Sledovat oznámení." - notification-write: "Zpravovat notifikace." cancel: "Zrušit" accept: "Povolit přístup" auth/views/index.vue: @@ -302,6 +297,7 @@ common/views/pages/explore.vue: popular-tags: "Populární tagy" federated: "Z fedivesmíru" explore: "Prozkoumat {host}" + users-info: "Aktuálně je zde registrováno {users} uživatelů" common/views/components/url-preview.vue: enable-player: "Otevřít v přehrávači" disable-player: "Zavřít přehrávač" @@ -354,19 +350,23 @@ common/views/components/connect-failed.troubleshooter.vue: no-network-desc: "Ujistěte se že jste připojeni k Internetu." no-internet: "Nejste připojeni k internetu" no-internet-desc: "Jste připojen k síti, ale zdá se že stále chybí připojení k Internetu. Prosím zkontrolujte Vaše připojení k Internetu." + no-server: "Nelze se připojit k serveru Misskey" common/views/components/media-banner.vue: click-to-show: "Klikněte pro zobrazení" common/views/components/theme.vue: - light-theme: "Šablona pro použití ve světlém vzhledu" - dark-theme: "Šablona pro použití v tmavém vzhledu" + light-theme: "Motiv pro použití ve světlém vzhledu" + dark-theme: "Motiv pro použití v tmavém vzhledu" light-themes: "Světlý vzhled" dark-themes: "Tmavý vzhled" - install-a-theme: "Nainstalovat šablonu" - theme-code: "Kód šablony" + install-a-theme: "Nainstalovat motiv" + theme-code: "Kód motivu" install: "Nainstalovat" installed: "\"{}\" byl nainstalován" create-a-theme: "Vytvořit motiv" + save-created-theme: "Uložit motiv" base-theme: "Základní vzhled" + base-theme-light: "Světlý" + base-theme-dark: "Tmavý" find-more-theme: "Najít další vzhledy" theme-name: "Jméno vzhledu" preview-created-theme: "Náhled" @@ -435,6 +435,7 @@ common/views/components/user-menu.vue: unblock: "Odblokovat" push-to-list: "Přidat do seznamu" select-list: "Vyberte seznam" + report-abuse: "Nahlásit spam" report-abuse-reported: "Problém byl nahlášen administrátorovi. Děkujeme za Vaší kooperaci." silence: "Ztlumit" suspend: "Zmrazit" @@ -793,8 +794,6 @@ desktop/views/components/settings.tags.vue: title: "Tagy" add: "Přidat" save: "Uložit" -desktop/views/components/taskmanager.vue: - title: "Správce úloh" desktop/views/components/timeline.vue: home: "Domů" local: "Lokální" @@ -839,7 +838,7 @@ admin/views/index.vue: emoji: "Emoji" moderators: "Moderátoři" users: "Uživatelé" - federation: "Federovaná" + federation: "Z fedivesmíru" announcements: "Oznámení" hashtags: "Hashtagy" queue: "Fronta úloh" @@ -851,7 +850,7 @@ admin/views/dashboard.vue: drive: "Disk" instances: "Instance" this-instance: "Tato instance" - federated: "Federovaná" + federated: "Z fedivesmíru" admin/views/abuse.vue: details: "Popis" remove-report: "Odstranit" @@ -1006,7 +1005,7 @@ admin/views/federation.vue: status: "Status" latest-request-received-at: "Poslední požadavek přijat" block: "Blokován" - instances: "Federovaná" + instances: "Z fedivesmíru" states: all: "Všechny" blocked: "Blokován" @@ -1184,10 +1183,3 @@ deck/deck.user-column.vue: dev/views/new-app.vue: app-name-desc: "Jméno vaší aplikace" app-desc: "Stručný popis nebo představení vaší aplikace." - account-read: "Zobrazit informace účtu" - note-write: "Odeslat." - reaction-write: "Přidat nebo odebrat reakce." - following-write: "Sledovat a přestat sledovat" - drive-read: "Přečíst váš Disk" - notification-read: "Sledovat oznámení." - notification-write: "Zpravovat notifikace." diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 68b0b587a6..0e83327af6 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -138,13 +138,6 @@ common: auth/views/form.vue: share-access: "Erlaubst Du <i>{name}</i> auf deinen Account zuzugreifen?" permission-ask: "Diese Applikation benötigt folgende Berechtigungen:" - account-read: "Accountinformationen anzeigen." - account-write: "Accountinformationen bearbeiten." - note-write: "Senden." - like-write: "Auf Beiträge reagieren." - following-write: "Folgen oder entfolgen." - notification-read: "Siehe deine Benachrichtigungen." - notification-write: "Benachrichtigungen verwalten." cancel: "Abbrechen" accept: "Zugriff erlauben." auth/views/index.vue: @@ -478,7 +471,6 @@ desktop/views/components/post-form.vue: posting: "Posting" attach-media-from-local: "Medien von deinem PC hinzufügen" attach-media-from-drive: "Medien von deinem Speicher hinzufügen" - attach-cancel: "Hinzufügen abbrechen" create-poll: "Eine Abstimmung erstellen" text-remain: "{} Zeichen verbleibend" visibility: "Sichtbarkeit" @@ -524,8 +516,6 @@ desktop/views/components/sub-note-content.vue: desktop/views/components/settings.tags.vue: add: "Hinzufügen" save: "Speichern" -desktop/views/components/taskmanager.vue: - title: "Taskmanager" desktop/views/components/timeline.vue: home: "Home" local: "Lokal" @@ -664,10 +654,3 @@ deck: rename: "Umbenennen" deck/deck.user-column.vue: activity: "Aktivität" -dev/views/new-app.vue: - account-read: "Accountinformationen anzeigen." - account-write: "Accountinformationen bearbeiten." - note-write: "Senden." - following-write: "Folgen oder entfolgen." - notification-read: "Siehe deine Benachrichtigungen." - notification-write: "Benachrichtigungen verwalten." diff --git a/locales/en-US.yml b/locales/en-US.yml index 2b11de112c..85bc713999 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -69,6 +69,11 @@ common: following: "Following" followers: "Followers" favorites: "Favorites" + permissions: + 'read:account': "View account information" + 'write:account': "Update your account information" + 'read:drive': "Browse the Drive" + 'write:drive': "Work with the Drive" empty-timeline-info: follow-users-to-make-your-timeline: "Following users will show their posts in your timeline." explore: "Find users" @@ -282,15 +287,6 @@ common: auth/views/form.vue: share-access: "Would you allow <i>{name}</i> to access your account?" permission-ask: "This application requires the following permissions:" - account-read: "View account information." - account-write: "Modify account information." - note-write: "Post." - like-write: "Express yourself about this post." - following-write: "Follow and unfollow." - drive-read: "Read your drive." - drive-write: "Upload/delete files in your drive." - notification-read: "Read your notifications." - notification-write: "Manage your notifications." cancel: "Cancel" accept: "Allow access." auth/views/index.vue: @@ -877,7 +873,6 @@ desktop/views/components/post-form.vue: posting: "Posting" attach-media-from-local: "Attach media from your device" attach-media-from-drive: "Attach media from your Drive" - attach-cancel: "Cancel attachment" insert-a-kao: "v('ω')v" create-poll: "Create a poll" text-remain: "{} characters remaining" @@ -970,6 +965,10 @@ common/views/components/password-settings.vue: not-match: "The new passwords do not match" changed: "Password changed" failed: "Failed to change password" +common/views/components/post-form-attaches.vue: + attach-cancel: "Remove Attachment" + mark-as-sensitive: "Mark as 'sensitive'" + unmark-as-sensitive: "Unmark as 'sensitive'" desktop/views/components/sub-note-content.vue: private: "This post is private" deleted: "This post has been deleted" @@ -980,8 +979,6 @@ desktop/views/components/settings.tags.vue: query: "Query (optional)" add: "Add" save: "Save" -desktop/views/components/taskmanager.vue: - title: "Task Manager" desktop/views/components/timeline.vue: home: "Home" local: "Local" @@ -1115,11 +1112,6 @@ admin/views/instance.vue: save: "Save" saved: "Saved" user-recommendation-config: "Recommended users" - enable-external-user-recommendation: "Enable external user recommendations" - external-user-recommendation-engine: "Engine" - external-user-recommendation-engine-desc: "Example : https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" - external-user-recommendation-timeout: "Timeout" - external-user-recommendation-timeout-desc: "Number of milliseconds (ex. 300,000)" email-config: "Email server settings" email-config-info: "Used to confirm email and password reset etc." enable-email: "Enable email delivery" @@ -1632,12 +1624,3 @@ dev/views/new-app.vue: authority: "Permissions" authority-desc: "Only the functions requested here can be accessed via the API." authority-warning: "You can change it even after creating the application, but if you give different permissions, all user keys associated at that time will be invalidated." - account-read: "View account information." - account-write: "Modify account information." - note-write: "Post." - reaction-write: "Add or remove reactions." - following-write: "Follow and unfollow." - drive-read: "Read the drive." - drive-write: "Upload/delete files in the drive." - notification-read: "Read your notifications." - notification-write: "Manage your notifications." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 237a10f85d..9ce261326c 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -169,15 +169,6 @@ common: you: "Tú" auth/views/form.vue: permission-ask: "La aplicación requiere los siguientes permisos:" - account-read: "Viendo información de la cuenta:" - account-write: "Modificar información de la cuenta:" - note-write: "Publicación." - like-write: "Para reaccionar a las publicaciones." - following-write: "Seguir o dejar de seguir" - drive-read: "Leer tu unidad." - drive-write: "Cargar o borrar archivos de tu unidad." - notification-read: "Leer tus notificaciones." - notification-write: "Administrar tus notificaciones." cancel: "Cancelar" accept: "Garantizar acceso." auth/views/index.vue: @@ -631,7 +622,6 @@ desktop/views/components/post-form.vue: posting: "Publicando" attach-media-from-local: "Agregar medios de tu dispositivo" attach-media-from-drive: "Adjunta multimedia desde tu Disco" - attach-cancel: "Quitar el archivo adjunto" create-poll: "Crea una encuesta" text-remain: "quedan {} caracteres" recent-tags: "Reciente" @@ -697,6 +687,9 @@ common/views/components/mute-and-block.vue: mute: "Silenciar" block: "Bloquear" save: "Guardar" +common/views/components/post-form-attaches.vue: + mark-as-sensitive: "Marcar como 'sensible'" + unmark-as-sensitive: "Desmarcar como 'sensible'" desktop/views/components/sub-note-content.vue: private: "Esta publicación es privada" deleted: "Esta publicación ha sido removida" @@ -708,7 +701,6 @@ desktop/views/components/settings.tags.vue: desktop/views/components/timeline.vue: home: "Inicio" local: "Local" - hybrid: "Social" global: "Global" list: "Listas" hashtag: "Hashtags" @@ -768,7 +760,6 @@ admin/views/instance.vue: recaptcha-secret-key: "clave secreta reCAPTCHA" invite: "Invitar" save: "Guardar" - external-user-recommendation-timeout: "Tiempo de espera" email: "Correo electrónico" smtp-host: "Host SMTP" smtp-port: "Puerto SMTP" @@ -898,7 +889,6 @@ mobile/views/pages/user-lists.vue: mobile/views/pages/home.vue: home: "Inicio" local: "Local" - hybrid: "Social" global: "Global" mobile/views/pages/widgets.vue: dashboard: "Panel de control" @@ -921,7 +911,6 @@ mobile/views/pages/user/home.photos.vue: deck: home: "Inicio" local: "Local" - hybrid: "Social" hashtag: "Etiquetas" global: "Global" notifications: "Notificaciones" @@ -929,12 +918,3 @@ deck: rename: "Renombrar" deck/deck.user-column.vue: activity: "Actividad" -dev/views/new-app.vue: - account-read: "Viendo información de la cuenta:" - account-write: "Modificar información de la cuenta:" - note-write: "Publicación." - following-write: "Seguir o dejar de seguir" - drive-read: "Leer tu unidad." - drive-write: "Cargar o borrar archivos de tu unidad." - notification-read: "Leer tus notificaciones." - notification-write: "Administrar tus notificaciones." diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index d53c522bc1..ab7588b68d 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -67,6 +67,11 @@ common: following: "Suit" followers: "Abonné·e·s" favorites: "Mettre cette note en favoris" + permissions: + 'read:account': "Afficher les informations du compte" + 'write:account': "Mettre à jour les informations de votre compte" + 'read:drive': "Parcourir le Drive" + 'write:drive': "Écrire sur le Drive" empty-timeline-info: follow-users-to-make-your-timeline: "Les utilisateurs suivants afficheront leurs publications sur votre fil." explore: "Trouver des utilisateurs" @@ -121,6 +126,7 @@ common: notification: "Notifications" apps: "Applications" tags: "Hashtags" + mute-and-block: "Silencer / Bloquer" blocking: "En cours blocage" security: "Sécurité" signin: "Historique des connexions" @@ -134,6 +140,16 @@ common: remember-note-visibility: "Se souvenir du mode de visibilité de la publication" web-search-engine: "Moteur de recherche Web" web-search-engine-desc: "Exemple : https://www.google.com/?#q={{query}}" + keep-cw: "Maintenir l'avertissement de contenu" + i-like-sushi: "Je préfère les sushis plutôt que le pudding" + show-reversi-board-labels: "Afficher les étiquettes des lignes et colonnes dans Reversi" + use-avatar-reversi-stones: "Utiliser l’avatar comme pion dans Reversi" + disable-animated-mfm: "Désactiver les textes animés dans les publications" + disable-showing-animated-images: "Désactiver l'animation des images" + suggest-recent-hashtags: "Afficher les hashtags populaires dans le champs de saisie" + always-show-nsfw: "Toujours afficher les contenus sensibles" + always-mark-nsfw: "Toujours marquer les notes ayant des médias comme sensibles" + show-full-acct: "Afficher l’adresse complète de l’utilisateur" show-via: "Afficher via" reduce-motion: "Réduire les animations dans l’interface utilisateur" this-setting-is-this-device-only: "Uniquement sur cet appareil" @@ -143,25 +159,69 @@ common: line-width-normal: "Normale" line-width-thick: "Épaisse" font-size: "Taille du texte" + font-size-x-small: "Très petit" + font-size-small: "Petite" font-size-medium: "Normale" + font-size-large: "Grande" font-size-x-large: "Large" + deck-column-align: "Alignement des colonnes du Deck" deck-column-align-center: "Centrer" deck-column-align-left: "À gauche" deck-column-align-flexible: "Flexible" deck-column-width: "Largeur des colonnes du Deck" + deck-column-width-narrow: "Étroite" + deck-column-width-narrower: "Légèrement étroite" deck-column-width-normal: "Normale" + deck-column-width-wider: "Légèrement large" + deck-column-width-wide: "Large" + use-shadow: "Utiliser les ombres dans l'interface utilisateur" + rounded-corners: "Coins arrondis de l'interface utilisateur" + circle-icons: "Utiliser des icônes circulaires" + contrasted-acct: "Ajouter du contraste au nom de l’utilisateur" + wallpaper: "Image du fond d'écran" + choose-wallpaper: "Sélectionner un fond d'écran" + delete-wallpaper: "Supprimer le fond d'écran" + post-form-on-timeline: "Afficher le champs de saisie en haut du fil" + show-clock-on-header: "Afficher l'horloge sur le coté supérieur droit" timeline: "Fil d’actualité" + show-my-renotes: "Afficher mes republications dans le fil" + remain-deleted-note: "Continuer à afficher les notes supprimées" + sound: "Son" + enable-sounds: "Activer les sons" + enable-sounds-desc: "Jouer un son lorsque vous recevez un message/publication. Ce paramètre est sauvegardé dans le navigateur." + volume: "Volume" + test: "Test" + update: "Mise à jour de Misskey" + version: "Version actuelle :" + latest-version: "Dernière version :" + update-checking: "Recherche de mises à jour" + do-update: "Rechercher des mises à jour" + update-settings: "Paramètres avancés" + no-updates: "Aucune mise à jour disponible" + no-updates-desc: "Votre Misskey est à jour." + update-available: "Nouvelle version disponible !" + update-available-desc: "Les mises à jour seront appliquées une fois la page est rechargée." + advanced-settings: "Paramètres avancés" + debug-mode: "Activer le mode débogage" + debug-mode-desc: "Ce paramètre est stocké dans le navigateur." + navbar-position: "Position de la barre de navigation" navbar-position-top: "en haut" navbar-position-left: "À gauche" navbar-position-right: "à droite" + i-am-under-limited-internet: "J'ai un accès Internet limité" + post-style: "Style d'affichage des notes" post-style-standard: "Standard" post-style-smart: "Intelligent" notification-position: "Afficher les notifications" notification-position-bottom: "en bas" notification-position-top: "en haut" + disable-via-mobile: "Enlever la mention publié via 'mobile'" + load-raw-images: "Afficher les photos jointes dans leur qualité originale" + load-remote-media: "Afficher les médias depuis le serveur distant" search: "Recherche" delete: "Supprimer" loading: "Chargement en cours …" + ok: "Confirmer" cancel: "Quitter" update-available-title: "Mise à jour disponible" update-available: "Une nouvelle version de Misskey est disponible ({newer}, version actuelle: {current}). Veuillez recharger la page pour appliquer la mise à jour." @@ -176,7 +236,7 @@ common: view-on-remote: " Consulter le profil complet" renoted-by: "Renoté par {user}" no-notes: "Sans aucune note" - turn-on-darkmode: "Basculer vers le mode nuit" + turn-on-darkmode: "Mode nuit" turn-off-darkmode: "Mode jour" error: title: "Une erreur est survenue" @@ -220,15 +280,6 @@ common: auth/views/form.vue: share-access: "Désirez-vous autoriser <i>{name}</i> à avoir accès à votre compte ?" permission-ask: "Cette application nécessite les autorisations suivantes :" - account-read: "Afficher les informations du compte." - account-write: "Modifications des informations du compte." - note-write: "Publier." - like-write: "Réagir aux publications." - following-write: "Suivre des comptes et se désabonner." - drive-read: "Lire votre Drive" - drive-write: "Téléverser/supprimer des fichiers dans votre Drive." - notification-read: "Lire vos notifications." - notification-write: "Gérer vos notifications." cancel: "Annuler" accept: "Autoriser l’accès" auth/views/index.vue: @@ -250,6 +301,8 @@ common/views/pages/explore.vue: federated: "Du Fédiverse" explore: "Explorer {host}" users-info: "Actuellement, {users} utilisateurs se sont inscrit ici" +common/views/components/url-preview.vue: + disable-player: "Fermer le lecteur" common/views/components/user-list.vue: no-users: "Il n'y a aucun utilisateur" common/views/components/games/reversi/reversi.vue: @@ -424,19 +477,32 @@ common/views/components/user-menu.vue: common/views/components/poll.vue: vote-to: "Voter pour '{}'" vote-count: "{} votes" + total-votes: "{} Total des votes" vote: "Vote" show-result: "Montrer les résultats" voted: "Voté" + closed: "Terminé" + remaining-days: "{d} jours, {h} heures restantes" + remaining-hours: "{h} heures et {m} minutes restantes" + remaining-minutes: "{m} minutes et {s} secondes restantes" + remaining-seconds: "{s} secondes restantes" common/views/components/poll-editor.vue: no-only-one-choice: "Vous devez saisir au moins deux choix." choice-n: "Choix {}" remove: "Supprimer ce choix" add: "+ Ajouter un choix" destroy: "Annuler ce sondage" + expiration: "Valide jusqu'à" + infinite: "Illimité" + at: "Choisir une date et une durée" + no-more: "Vous ne pouvez pas en ajouter davantage" + deadline-date: "Date d’échéance" + deadline-time: "Durée" interval: "Durée" unit: "Unité" second: "secondes" minute: "Minutes" + hour: "Heures" day: "D" common/views/components/reaction-picker.vue: choose-reaction: "Choisissez votre réaction" @@ -568,6 +634,7 @@ common/views/components/profile-editor.vue: email-not-verified: "Adresse de courriel n’est pas confirmée. Veuillez vérifier votre boite de réception." export: "Exporter" import: "Importer" + export-and-import: "Exportation et importation" export-targets: all-notes: "Toutes les notes publiées" following-list: "Liste des abonnements" @@ -793,7 +860,6 @@ desktop/views/components/post-form.vue: posting: "Publication …" attach-media-from-local: "Joindre un média depuis votre appareil" attach-media-from-drive: "Joindre un média depuis votre Drive" - attach-cancel: "Annuler le fichier attaché" insert-a-kao: "v('ω')v" create-poll: "Créer un sondage" text-remain: "{} caractères restants" @@ -886,6 +952,9 @@ common/views/components/password-settings.vue: not-match: "Les nouveaux mots de passe ne sont pas identiques" changed: "Mot de passe modifié avec succès" failed: "Échec lors de la modification du mot de passe" +common/views/components/post-form-attaches.vue: + mark-as-sensitive: "Marquer comme sensible" + unmark-as-sensitive: "Ne pas marquer comme sensible" desktop/views/components/sub-note-content.vue: private: "cette publication est privée" deleted: "cette publication a été supprimée" @@ -896,8 +965,6 @@ desktop/views/components/settings.tags.vue: query: "Requête (optionnelle)" add: "Ajouter" save: "Enregistrer" -desktop/views/components/taskmanager.vue: - title: "Gestionnaire de tâches" desktop/views/components/timeline.vue: home: "Accueil" local: "Local" @@ -958,6 +1025,7 @@ admin/views/index.vue: hashtags: "Hashtags" abuse: "Abus" queue: "File d’attente" + logs: "Journaux" back-to-misskey: "Retour vers Misskey" admin/views/dashboard.vue: dashboard: "Tableau de bord" @@ -1026,11 +1094,6 @@ admin/views/instance.vue: save: "Sauvegarder" saved: "Enregistré" user-recommendation-config: "Utilisateurs" - enable-external-user-recommendation: "Activer la recommandation des utilisateurs distants" - external-user-recommendation-engine: "Moteur" - external-user-recommendation-engine-desc: "Exemple : https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" - external-user-recommendation-timeout: "Délai d’expiration" - external-user-recommendation-timeout-desc: "En millisecondes (par exemple : 300000)" email-config: "Paramètres du serveur de messagerie" email-config-info: "Utilisé pour confirmer votre adresse de courrier électronique et la réinitialisation de votre mot de passe." enable-email: "Activation de la distribution du courrier" @@ -1199,6 +1262,7 @@ admin/views/federation.vue: marked-as-closed: "Marquées comme fermées" lookup: "Recherche" instances: "Fédérées" + instance-not-registered: "L’instance n’a pas encore été découverte" sort: "Trier par" sorts: caughtAtAsc: "Date d’inscription (Ascendant)" @@ -1206,6 +1270,7 @@ admin/views/federation.vue: lastCommunicatedAtAsc: "La date et l'heure des interactions plus anciennes" lastCommunicatedAtDesc: "La date et l'heure des nouvelles interactions" notesDesc: "Description des notes" + usersAsc: "Peu d'abonnés" followingAsc: "Les moins suivies" followingDesc: "Ayant le plus d'abonné·e·s" followersAsc: "Ayant le moins d'abonné·e·s" @@ -1530,12 +1595,3 @@ dev/views/new-app.vue: authority: "Autorisations " authority-desc: "Sont accessibles via l’API, uniquement les fonctionnalités demandées ici." authority-warning: "Vous pouvez le changer même après avoir créé l'application, mais si vous attribuez une nouvelle permission, toutes les clés utilisateur associées seront dès lors invalides." - account-read: "Afficher les informations du compte" - account-write: "Modifications des informations du compte" - note-write: "Publications." - reaction-write: "Ajout et suppression de réactions." - following-write: "S’abonner et se désabonner." - drive-read: "Lecture du Drive." - drive-write: "Téléversement/suppression des fichiers de votre Lecteur." - notification-read: "Lire vos notifications." - notification-write: "Gestion de vos notifications." diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index d4457b6594..cc6fe2b086 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -73,6 +73,12 @@ common: followers: "フォロワー" favorites: "お気に入り" + permissions: + 'read:account': "アカウントの情報を見る" + 'write:account': "アカウントの情報を変更する" + 'read:drive': "ドライブを見る" + 'write:drive': "ドライブを操作する" + empty-timeline-info: follow-users-to-make-your-timeline: "ユーザーをフォローすると投稿がタイムラインに表示されます。" explore: "ユーザーを探索する" @@ -299,15 +305,6 @@ common: auth/views/form.vue: share-access: "<i>{name}</i>があなたのアカウントにアクセスすることを許可しますか?" permission-ask: "このアプリは次の権限を要求しています:" - account-read: "アカウントの情報を見る。" - account-write: "アカウントの情報を操作する。" - note-write: "投稿する。" - like-write: "いいねしたりいいね解除する。" - following-write: "フォローしたりフォロー解除する。" - drive-read: "ドライブを見る。" - drive-write: "ドライブを操作する。" - notification-read: "通知を見る。" - notification-write: "通知を操作する。" cancel: "キャンセル" accept: "アクセスを許可" @@ -967,7 +964,6 @@ desktop/views/components/post-form.vue: posting: "投稿中" attach-media-from-local: "PCからメディアを添付" attach-media-from-drive: "ドライブからメディアを添付" - attach-cancel: "添付取り消し" insert-a-kao: "v('ω')v" create-poll: "アンケートを作成" text-remain: "残り{}文字" @@ -1073,6 +1069,11 @@ common/views/components/password-settings.vue: changed: "パスワードを変更しました" failed: "パスワード変更に失敗しました" +common/views/components/post-form-attaches.vue: + attach-cancel: "添付取り消し" + mark-as-sensitive: "閲覧注意に設定" + unmark-as-sensitive: "閲覧注意を解除" + desktop/views/components/sub-note-content.vue: private: "この投稿は非公開です" deleted: "この投稿は削除されました" @@ -1085,9 +1086,6 @@ desktop/views/components/settings.tags.vue: add: "追加" save: "保存" -desktop/views/components/taskmanager.vue: - title: "タスクマネージャ" - desktop/views/components/timeline.vue: home: "ホーム" local: "ローカル" @@ -1238,11 +1236,6 @@ admin/views/instance.vue: save: "保存" saved: "保存しました" user-recommendation-config: "おすすめユーザー" - enable-external-user-recommendation: "外部ユーザーレコメンデーションを有効にする" - external-user-recommendation-engine: "エンジン" - external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" - external-user-recommendation-timeout: "タイムアウト" - external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)" email-config: "メールサーバーの設定" email-config-info: "メールアドレス確認やパスワードリセットの際に使われます。" enable-email: "メール配信を有効にする" @@ -1823,12 +1816,3 @@ dev/views/new-app.vue: authority: "権限" authority-desc: "ここで要求した機能だけがAPIからアクセスできます。" authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。" - account-read: "アカウントの情報を見る。" - account-write: "アカウントの情報を操作する。" - note-write: "投稿する。" - reaction-write: "リアクションしたりリアクションをキャンセルする。" - following-write: "フォローしたりフォロー解除する。" - drive-read: "ドライブを見る。" - drive-write: "ドライブを操作する。" - notification-read: "通知を見る。" - notification-write: "通知を操作する。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 3e0c3d0286..9f8e581ef4 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -166,15 +166,6 @@ common: auth/views/form.vue: share-access: "あんたのアカウントに<i>{name}</i>がアクセスしようとしてるで?ええか?" permission-ask: "このアプリは次の権限を要求してんで:" - account-read: "アカウントの情報を見させてもらうで。" - account-write: "アカウントの情報を操作させてもらうで。" - note-write: "投稿させてもらうで。" - like-write: "いいねしたりいいね解除させてもらうで。" - following-write: "フォローしたりフォロー解除させてもらうで。" - drive-read: "ドライブを見させてもらうで。" - drive-write: "ドライブを操作させてもらうで。" - notification-read: "通知を見させてもらうで。" - notification-write: "通知を操作させてもらうで。" cancel: "やめとくわ" accept: "アクセスを許可や!" auth/views/index.vue: @@ -688,7 +679,6 @@ desktop/views/components/post-form.vue: posting: "投稿中" attach-media-from-local: "PCからメディア持ってくる" attach-media-from-drive: "ドライブからメディア持ってくる" - attach-cancel: "くっつけるのやめよか" create-poll: "アンケートを作成" text-remain: "残り{}文字" recent-tags: "最近のタグ" @@ -779,6 +769,9 @@ common/views/components/password-settings.vue: enter-new-password-again: "もっぺん入れてや" not-match: "パスワードがおうとらん" changed: "パスワード変えたわ" +common/views/components/post-form-attaches.vue: + mark-as-sensitive: "見たらあかん感じにしとく" + unmark-as-sensitive: "やっぱ見せたるわ" desktop/views/components/sub-note-content.vue: private: "この投稿は見せられへんわ" deleted: "この投稿なんか無くなってもうたわ" @@ -787,12 +780,9 @@ desktop/views/components/sub-note-content.vue: desktop/views/components/settings.tags.vue: add: "増やす" save: "保存" -desktop/views/components/taskmanager.vue: - title: "タスクマネージャ" desktop/views/components/timeline.vue: home: "ホーム" local: "ローカル" - hybrid: "ソーシャル" global: "グローバル" mentions: "あんた宛て" messages: "ダイレクト投稿" @@ -908,11 +898,6 @@ admin/views/instance.vue: save: "保存" saved: "保存したで!" user-recommendation-config: "このユーザーええで" - enable-external-user-recommendation: "外部ユーザーレコメンデーションを使えるようにする" - external-user-recommendation-engine: "エンジン" - external-user-recommendation-engine-desc: "例: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" - external-user-recommendation-timeout: "タイムアウト" - external-user-recommendation-timeout-desc: "ミリ秒単位 (例: 300000)" email-config: "メールサーバーの設定" email-config-info: "メールアドレス確認やパスワードリセットの際に使うで。" enable-email: "メール配信を有効にする" @@ -1174,7 +1159,6 @@ mobile/views/pages/following.vue: mobile/views/pages/home.vue: home: "ホーム" local: "ローカル" - hybrid: "ソーシャル" global: "グローバル" mentions: "あんた宛て" messages: "ダイレクト投稿" @@ -1229,7 +1213,6 @@ deck: widgets: "ウィジェット" home: "ホーム" local: "ローカル" - hybrid: "ソーシャル" hashtag: "ハッシュタグ" global: "グローバル" mentions: "あんた宛て" @@ -1298,12 +1281,3 @@ dev/views/new-app.vue: authority: "権限" authority-desc: "ここにチェックした機能しかAPIからアクセスできひんから気ぃつけてな" authority-warning: "アプリ作った後でも変えれるけど、新しいやつ追加したらそん時関連付いてるユーザーキーは全部ほかされるで。" - account-read: "アカウントの情報見せて" - account-write: "アカウントの情報いじらせて" - note-write: "投稿させて" - reaction-write: "リアクションしたりそれをキャンセルさせて" - following-write: "フォローとかフォロー解除させて" - drive-read: "ドライブ見せて" - drive-write: "ドライブいじらせて" - notification-read: "通知見せて" - notification-write: "通知いじらせて" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index d746586088..83e971adf4 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -69,6 +69,11 @@ common: following: "팔로우 중" followers: "팔로워" favorites: "즐겨찾기" + permissions: + 'read:account': "계정 정보 보기" + 'write:account': "계정 정보 변경" + 'read:drive': "드라이브 보기" + 'write:drive': "드라이브 수정" empty-timeline-info: follow-users-to-make-your-timeline: "사용자를 팔로우하면 글이 타임라인에 표시됩니다." explore: "사용자 탐색" @@ -282,15 +287,6 @@ common: auth/views/form.vue: share-access: "<i>{name}</i>가 당신의 계정에 엑세스하도록 허용하시겠습니까?" permission-ask: "이 앱은 다음의 권한을 요청합니다:" - account-read: "계정 정보 보기." - account-write: "계정 정보의 수정." - note-write: "게시하기." - like-write: "좋아요 하거나 좋아요 해제하기." - following-write: "팔로우하거나 팔로우를 취소하기." - drive-read: "드라이브 보기." - drive-write: "드라이브의 수정." - notification-read: "알림 읽기." - notification-write: "알림 수정하기." cancel: "취소" accept: "접근 권한 허용" auth/views/index.vue: @@ -877,7 +873,6 @@ desktop/views/components/post-form.vue: posting: "게시중" attach-media-from-local: "PC에서 미디어 첨부" attach-media-from-drive: "드라이브에서 미디어 첨부" - attach-cancel: "첨부 취소" insert-a-kao: "v('ω')v" create-poll: "투표 만들기" text-remain: "{}문자 남음" @@ -970,6 +965,9 @@ common/views/components/password-settings.vue: not-match: "새 비밀번호가 일치하지 않습니다" changed: "비밀번호를 변경하였습니다" failed: "비밀번호 변경을 실패하였습니다." +common/views/components/post-form-attaches.vue: + mark-as-sensitive: "열람주의로 설정" + unmark-as-sensitive: "열람주의 해제" desktop/views/components/sub-note-content.vue: private: "이 글은 비공개입니다" deleted: "이 글은 삭제되었습니다" @@ -980,8 +978,6 @@ desktop/views/components/settings.tags.vue: query: "쿼리 (생략 가능)" add: "추가" save: "저장" -desktop/views/components/taskmanager.vue: - title: "작업 관리자" desktop/views/components/timeline.vue: home: "홈" local: "로컬" @@ -1115,11 +1111,6 @@ admin/views/instance.vue: save: "저장" saved: "저장하였습니다" user-recommendation-config: "추천 사용자" - enable-external-user-recommendation: "외부 사용자 추천 활성화" - external-user-recommendation-engine: "엔진" - external-user-recommendation-engine-desc: "예: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" - external-user-recommendation-timeout: "타임 아웃" - external-user-recommendation-timeout-desc: "밀리초 (예: 300000)" email-config: "메일 서버 설정" email-config-info: "메일 주소 확인 혹은 비밀번호 재설정에 사용 됩니다." enable-email: "메일 발신 활성화" @@ -1632,12 +1623,3 @@ dev/views/new-app.vue: authority: "권한" authority-desc: "이곳에서 요청한 권한에 한정하여 API로 액세스할 수 있습니다." authority-warning: "앱을 생성한 뒤에도 변경할 수 있지만, 새로운 권한을 설정하는 경우 그 시점부터 예전에 발급받았던 유저 키는 모두 무효화됩니다." - account-read: "계정 정보 보기." - account-write: "계정 정보 편집." - note-write: "글 쓰기." - reaction-write: "리액션을 하거나 리액션을 취소할 수 있음." - following-write: "팔로우하거나 팔로우 해제하기." - drive-read: "드라이브 보기." - drive-write: "드라이브를 조작." - notification-read: "알림 보기." - notification-write: "알림 조작." diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index ee798cd8d8..772d41f93b 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -334,7 +334,6 @@ desktop/views/components/post-form.vue: posting: "Bezig met plaatsen" attach-media-from-local: "Media bijvoegen van je computer" attach-media-from-drive: "Media bijvoegen uit je Drive" - attach-cancel: "Bijlage annuleren" create-poll: "Peiling creëren" text-remain: "{} resterende tekens" desktop/views/components/post-form-window.vue: @@ -384,8 +383,6 @@ desktop/views/components/sub-note-content.vue: poll: "Peilingen" desktop/views/components/settings.tags.vue: add: "Toevoegen" -desktop/views/components/taskmanager.vue: - title: "Taakbeheer" desktop/views/components/timeline.vue: home: "Startpagina" local: "Lokaal" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 16199f6cc9..61d7a10e14 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -23,6 +23,7 @@ common: enter-password: "Wprowadź Hasło" 2fa: "Uwierzytelnienie dwuetapowe" customize-home: "Dostosuj stronę główną" + featured-notes: "Wyróżnienia" dark-mode: "Tryb ciemny" signin: "Zaloguj się" signup: "Rejestracja" @@ -61,6 +62,8 @@ common: following: "Śledzisz" followers: "Śledzący" favorites: "Moje ulubione" + permissions: + 'read:drive': "Wyświetl dysk" empty-timeline-info: explore: "Poznaj" weekday-short: @@ -201,15 +204,6 @@ common: you: "Ty" auth/views/form.vue: permission-ask: "Ta aplikacja wymaga następujących uprawnień:" - account-read: "Wyświetlanie informacji o koncie:" - account-write: "Modyfikowanie informacji o koncie:" - note-write: "Publikacja." - like-write: "Reagowanie na wpisy." - following-write: "Śledzenie i cofanie śledzenia." - drive-read: "Odczytywanie Twojego dysku." - drive-write: "Wysyłanie i usuwanie plików na Twoim dysku." - notification-read: "Odczytywanie Twoich powiadomień." - notification-write: "Zarządzanie Twoimi powiadomieniami." cancel: "Anuluj" accept: "Przyznaj dostęp." auth/views/index.vue: @@ -715,7 +709,6 @@ desktop/views/components/post-form.vue: posting: "Wysyłanie" attach-media-from-local: "Załącz zawartość multimedialną z komputera" attach-media-from-drive: "Załącz zawartość multimedialną z dysku" - attach-cancel: "Usuń załącznik" insert-a-kao: "v('ω')v" create-poll: "Utwórz ankietę" text-remain: "pozostałe znaki: {}" @@ -795,6 +788,9 @@ common/views/components/password-settings.vue: enter-current-password: "Wprowadź obecne hasło" enter-new-password: "Wprowadź nowe hasło" enter-new-password-again: "Wprowadź ponownie nowe hasło" +common/views/components/post-form-attaches.vue: + mark-as-sensitive: "Oznacz jako zawartość wrażliwą" + unmark-as-sensitive: "Cofnij oznaczenie jako zawartość wrażliwą" desktop/views/components/sub-note-content.vue: private: "ten wpis jest prywatny" deleted: "ten wpis został usunięty" @@ -805,12 +801,9 @@ desktop/views/components/settings.tags.vue: query: "Zapytanie (opcjonalne)" add: "Dodaj" save: "Zapisz" -desktop/views/components/taskmanager.vue: - title: "Menedżer zadań" desktop/views/components/timeline.vue: home: "Strona główna" local: "Lokalne" - hybrid: "Społeczność" global: "Globalne" mentions: "Wspomnienia" messages: "Bezpośrednie wpisy" @@ -882,8 +875,6 @@ admin/views/instance.vue: save: "Zapisz" saved: "Zapisano" user-recommendation-config: "Polecani użytkownicy" - external-user-recommendation-engine: "Silnik" - external-user-recommendation-engine-desc: "Np: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" email: "Adres e-mail" admin/views/charts.vue: notes: "Wpisy" @@ -1119,7 +1110,6 @@ mobile/views/pages/signup.vue: mobile/views/pages/home.vue: home: "Strona główna" local: "Lokalne" - hybrid: "Społeczność" global: "Globalne" mentions: "Wspomnienia" messages: "Bezpośrednie wpisy" @@ -1169,7 +1159,6 @@ deck: widgets: "Widżety" home: "Strona główna" local: "Lokalne" - hybrid: "Społeczność" hashtag: "Hashtag" global: "Globalne" mentions: "Wspomnienia" @@ -1224,11 +1213,3 @@ dev/views/apps.vue: dev/views/new-app.vue: app-name: "Nazwa Aplikacji" authority: "Uprawnienia" - account-read: "Wyświetlanie informacji o koncie:" - account-write: "Modyfikowanie informacji o koncie:" - note-write: "Publikacja." - following-write: "Śledzenie i cofanie śledzenia." - drive-read: "Odczytywanie Twojego dysku." - drive-write: "Wysyłanie i usuwanie plików na Twoim dysku." - notification-read: "Odczytywanie Twoich powiadomień." - notification-write: "Zarządzanie Twoimi powiadomieniami." diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 83db77ad90..7732f1c946 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -119,9 +119,6 @@ common: you: "Você" auth/views/form.vue: permission-ask: "Este aplicativo precisa das seguintes permissões:" - account-read: "Ver informações da conta." - account-write: "Modificar informações da conta." - note-write: "Publicar" cancel: "Cancelar" accept: "Permitir acesso" auth/views/index.vue: @@ -292,7 +289,3 @@ docs: description: "Descrição" dev/views/index.vue: manage-apps: "Gerenciar aplicativos" -dev/views/new-app.vue: - account-read: "Ver informações da conta." - account-write: "Modificar informações da conta." - note-write: "Publicar" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index bd25cbf6fa..195c6d5ef8 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -69,6 +69,11 @@ common: following: "正在关注" followers: "关注者" favorites: "最爱" + permissions: + 'read:account': "查看账户信息" + 'write:account': "更改我的帐户信息" + 'read:drive': "查看网盘" + 'write:drive': "管理网盘文件" empty-timeline-info: follow-users-to-make-your-timeline: "关注其他用户时,帖子将显示在时间线中。" explore: "查找用户" @@ -282,15 +287,6 @@ common: auth/views/form.vue: share-access: "您要允许<i>{name}</i>来访问您的账户吗?" permission-ask: "这个应用程序需要以下权限:" - account-read: "查看账户信息" - account-write: "修改账户信息" - note-write: "投稿。" - like-write: "点赞或取消赞。" - following-write: "关注或取消关注。" - drive-read: "查看您的网盘" - drive-write: "管理网盘文件。" - notification-read: "查看通知。" - notification-write: "管理通知。" cancel: "取消" accept: "允许访问。" auth/views/index.vue: @@ -877,7 +873,6 @@ desktop/views/components/post-form.vue: posting: "发送中" attach-media-from-local: "从设备中添加媒体文件" attach-media-from-drive: "从网盘中添加媒体文件" - attach-cancel: "删除附件" insert-a-kao: "v('ω')v" create-poll: "创建一个投票" text-remain: "还剩{}字" @@ -970,6 +965,9 @@ common/views/components/password-settings.vue: not-match: "新密码不匹配" changed: "密码已更改" failed: "更改密码失败" +common/views/components/post-form-attaches.vue: + mark-as-sensitive: "标记为“敏感”" + unmark-as-sensitive: "取消标记为“敏感”" desktop/views/components/sub-note-content.vue: private: "这个帖子是私密的" deleted: "帖子已删除" @@ -980,8 +978,6 @@ desktop/views/components/settings.tags.vue: query: "查询 (可选)" add: "添加" save: "保存" -desktop/views/components/taskmanager.vue: - title: "任务管理器" desktop/views/components/timeline.vue: home: "首页" local: "本地" @@ -1115,11 +1111,6 @@ admin/views/instance.vue: save: "保存" saved: "保存完毕" user-recommendation-config: "推荐用户" - enable-external-user-recommendation: "启用外部用户推荐" - external-user-recommendation-engine: "引擎" - external-user-recommendation-engine-desc: "例如: https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}" - external-user-recommendation-timeout: "超时" - external-user-recommendation-timeout-desc: "单位为毫秒 (例如:300000)" email-config: "电子邮件服务器设置" email-config-info: "用于确认电子邮件和密码重置等。" enable-email: "启用电子邮件送递" @@ -1503,7 +1494,7 @@ mobile/views/pages/following.vue: mobile/views/pages/home.vue: home: "首页" local: "Local" - hybrid: "Social" + hybrid: "社交" global: "Global" mentions: "Mentions" messages: "直接发布" @@ -1559,7 +1550,7 @@ deck: widgets: "小部件" home: "首页" local: "Local" - hybrid: "Social" + hybrid: "社交" hashtag: "标签" global: "Global" mentions: "Mentions" @@ -1632,12 +1623,3 @@ dev/views/new-app.vue: authority: "权限" authority-desc: "只能通过API访问此处请求的功能。" authority-warning: "您可以在创建应用程序后对其进行更改,但如果您授予不同的权限,则当时关联的所有用户密钥都将失效。" - account-read: "查看账户信息" - account-write: "修改账户信息" - note-write: "投稿。" - reaction-write: "添加或删除回应。" - following-write: "关注和不关注" - drive-read: "查看网盘" - drive-write: "管理网盘文件。" - notification-read: "阅读您的通知" - notification-write: "管理通知" diff --git a/package.json b/package.json index 79ac69e8cd..5ec8c524df 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "misskey", "author": "syuilo <i@syuilo.com>", - "version": "10.100.0", - "codename": "nighthike", + "version": "11.0.0", + "codename": "daybreak", "repository": { "type": "git", "url": "https://github.com/syuilo/misskey.git" @@ -11,7 +11,8 @@ "private": true, "scripts": { "start": "node ./index.js", - "debug": "DEBUG=misskey:* node ./index.js", + "init": "node ./built/init.js", + "migrate": "node ./built/migrate.js", "build": "webpack && gulp build", "webpack": "webpack", "watch": "webpack --watch", @@ -32,14 +33,13 @@ "@prezzemolo/rap": "0.1.2", "@prezzemolo/zip": "0.0.3", "@types/bcryptjs": "2.4.2", - "@types/bull": "3.5.8", - "@types/chai-http": "3.0.5", + "@types/bull": "3.5.11", "@types/dateformat": "3.0.0", "@types/deep-equal": "1.0.1", "@types/double-ended-queue": "2.1.0", - "@types/elasticsearch": "5.0.30", - "@types/file-type": "10.6.0", - "@types/gulp": "4.0.5", + "@types/elasticsearch": "5.0.32", + "@types/file-type": "10.9.1", + "@types/gulp": "4.0.6", "@types/gulp-mocha": "0.0.32", "@types/gulp-rename": "0.0.33", "@types/gulp-replace": "0.0.31", @@ -47,66 +47,66 @@ "@types/gulp-util": "3.0.34", "@types/is-root": "1.0.0", "@types/is-url": "1.2.28", - "@types/js-yaml": "3.12.0", + "@types/js-yaml": "3.12.1", "@types/jsdom": "12.2.3", "@types/katex": "0.10.1", "@types/koa": "2.0.48", "@types/koa-bodyparser": "5.0.2", - "@types/koa-compress": "2.0.8", + "@types/koa-compress": "2.0.9", "@types/koa-cors": "0.0.0", "@types/koa-favicon": "2.0.19", "@types/koa-logger": "3.1.1", "@types/koa-mount": "3.0.1", "@types/koa-multer": "1.0.0", "@types/koa-router": "7.0.40", - "@types/koa-send": "4.1.1", + "@types/koa-send": "4.1.2", "@types/koa-views": "2.0.3", "@types/koa__cors": "2.2.3", + "@types/lolex": "3.1.1", "@types/minio": "7.0.1", - "@types/mkdirp": "0.5.2", - "@types/mocha": "5.2.5", - "@types/mongodb": "3.1.20", - "@types/node": "11.10.4", - "@types/nodemailer": "4.6.6", + "@types/mocha": "5.2.6", + "@types/mongodb": "3.1.22", + "@types/monk": "6.0.0", + "@types/node": "11.13.4", + "@types/nodemailer": "4.6.7", "@types/nprogress": "0.0.29", "@types/oauth": "0.9.1", "@types/parse5": "5.0.0", "@types/parsimmon": "1.10.0", "@types/portscanner": "2.1.0", "@types/pug": "2.0.4", - "@types/qrcode": "1.3.0", + "@types/qrcode": "1.3.2", "@types/ratelimiter": "2.1.28", - "@types/redis": "2.8.10", + "@types/redis": "2.8.12", "@types/rename": "1.0.1", "@types/request": "2.48.1", "@types/request-promise-native": "1.0.15", "@types/request-stats": "3.0.0", "@types/rimraf": "2.0.2", - "@types/seedrandom": "2.4.27", - "@types/sharp": "0.21.2", + "@types/sharp": "0.22.1", "@types/showdown": "1.9.2", "@types/speakeasy": "2.0.4", "@types/systeminformation": "3.23.1", "@types/tinycolor2": "1.4.1", - "@types/tmp": "0.0.33", + "@types/tmp": "0.1.0", "@types/uuid": "3.4.4", "@types/web-push": "3.3.0", - "@types/webpack": "4.4.24", + "@types/webpack": "4.4.27", "@types/webpack-stream": "3.2.10", "@types/websocket": "0.0.40", "@types/ws": "6.0.1", "animejs": "3.0.1", - "apexcharts": "3.6.2", + "apexcharts": "3.6.7", "autobind-decorator": "2.4.0", "autosize": "4.0.2", "autwh": "0.1.0", "bcryptjs": "2.4.3", "bootstrap-vue": "2.0.0-rc.13", "bull": "3.7.0", - "cafy": "15.1.0", + "cafy": "15.1.1", "chai": "4.2.0", - "chai-http": "4.2.1", "chalk": "2.4.2", + "cli-highlight": "2.1.0", "commander": "2.20.0", "content-disposition": "0.5.3", "crc-32": "1.2.0", @@ -114,17 +114,15 @@ "cssnano": "4.1.10", "dateformat": "3.0.3", "deep-equal": "1.0.1", - "deepcopy": "0.6.3", "diskusage": "1.0.0", "double-ended-queue": "2.1.0-0", "elasticsearch": "15.4.1", "emojilib": "2.4.0", - "escape-regexp": "0.0.1", - "eslint": "5.15.1", + "eslint": "5.16.0", "eslint-plugin-vue": "5.2.2", "eventemitter3": "3.1.0", "feed": "2.0.4", - "file-type": "10.10.0", + "file-type": "10.11.0", "fuckadblock": "3.2.1", "gulp": "4.0.0", "gulp-cssnano": "2.1.3", @@ -135,19 +133,19 @@ "gulp-sourcemaps": "2.6.5", "gulp-stylus": "2.7.0", "gulp-tslint": "8.1.4", - "gulp-typescript": "5.0.0", + "gulp-typescript": "5.0.1", "gulp-uglify": "3.0.2", "gulp-util": "3.0.8", "hard-source-webpack-plugin": "0.13.1", - "html-minifier": "3.5.21", + "html-minifier": "4.0.0", "http-signature": "1.2.0", - "insert-text-at-cursor": "0.1.2", + "insert-text-at-cursor": "0.2.0", "is-root": "2.0.0", - "is-svg": "4.0.0", - "js-yaml": "3.13.0", + "is-svg": "4.1.0", + "js-yaml": "3.13.1", "jsdom": "14.0.0", "json5": "2.1.0", - "json5-loader": "1.0.1", + "json5-loader": "2.0.0", "katex": "0.10.1", "koa": "2.7.0", "koa-bodyparser": "4.2.1", @@ -163,23 +161,25 @@ "koa-views": "6.2.0", "langmap": "0.0.16", "loader-utils": "1.2.3", + "lolex": "3.1.0", "lookup-dns-cache": "2.1.0", "minio": "7.0.5", - "mkdirp": "0.5.1", - "mocha": "5.2.0", + "mocha": "6.1.3", "moji": "0.5.1", "moment": "2.24.0", - "mongodb": "3.2.2", + "mongodb": "3.2.3", "monk": "6.0.6", "ms": "2.1.1", "nan": "2.12.1", "nested-property": "0.0.7", - "nodemailer": "5.1.1", + "node-fetch": "2.3.0", + "nodemailer": "6.1.0", "nprogress": "0.2.0", "object-assign-deep": "0.4.0", "os-utils": "0.0.14", "parse5": "5.1.0", "parsimmon": "1.12.0", + "pg": "7.9.0", "portscanner": "2.2.0", "postcss-loader": "3.0.0", "prismjs": "1.16.0", @@ -195,14 +195,15 @@ "recaptcha-promise": "0.1.3", "reconnecting-websocket": "4.1.10", "redis": "2.8.0", + "reflect-metadata": "0.1.13", "rename": "1.0.4", "request": "2.88.0", "request-promise-native": "1.0.7", "request-stats": "3.0.0", + "require-all": "3.0.0", "rimraf": "2.6.3", "rndstr": "1.0.0", "s-age": "1.1.2", - "seedrandom": "2.4.4", "sharp": "0.22.0", "showdown": "1.9.0", "showdown-highlightjs-extension": "0.1.2", @@ -212,19 +213,21 @@ "stylus": "0.54.5", "stylus-loader": "3.0.2", "summaly": "2.2.0", - "systeminformation": "4.0.16", + "systeminformation": "4.1.4", "syuilo-password-strength": "0.0.1", "terser-webpack-plugin": "1.2.3", "textarea-caret": "3.1.0", "tinycolor2": "1.4.1", - "tmp": "0.0.33", + "tmp": "0.1.0", "ts-loader": "5.3.3", - "ts-node": "8.0.3", - "tslint": "5.13.1", + "ts-node": "7.0.1", + "tslint": "5.15.0", "tslint-sonarts": "1.9.0", + "typeorm": "0.2.16-rc.1", "typescript": "3.3.3333", "typescript-eslint-parser": "22.0.0", "uglify-es": "3.3.9", + "ulid": "2.3.0", "url-loader": "1.1.2", "uuid": "3.3.2", "v-animate-css": "0.0.3", @@ -232,7 +235,7 @@ "video-thumbnail-generator": "1.1.3", "vue": "2.6.10", "vue-color": "2.7.0", - "vue-content-loading": "1.5.3", + "vue-content-loading": "1.6.0", "vue-cropperjs": "3.0.0", "vue-i18n": "8.10.0", "vue-js-modal": "1.3.28", @@ -240,7 +243,7 @@ "vue-loader": "15.7.0", "vue-marquee-text-component": "1.1.1", "vue-prism-component": "1.1.1", - "vue-router": "3.0.2", + "vue-router": "3.0.4", "vue-sequential-entrance": "1.1.3", "vue-style-loader": "4.1.2", "vue-svg-inline-loader": "1.2.15", @@ -250,9 +253,8 @@ "vuex": "3.1.0", "vuex-persistedstate": "2.5.4", "web-push": "3.3.3", - "webfinger.js": "2.7.0", - "webpack": "4.28.4", - "webpack-cli": "3.2.3", + "webpack": "4.30.0", + "webpack-cli": "3.3.0", "websocket": "1.0.28", "ws": "6.2.1", "xev": "2.0.1" diff --git a/src/@types/deepcopy.d.ts b/src/@types/deepcopy.d.ts deleted file mode 100644 index f276b7e678..0000000000 --- a/src/@types/deepcopy.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare module 'deepcopy' { - type DeepcopyCustomizerValueType = 'Object'; - - type DeepcopyCustomizer<T> = ( - value: T, - valueType: DeepcopyCustomizerValueType) => T; - - interface IDeepcopyOptions<T> { - customizer: DeepcopyCustomizer<T>; - } - - function deepcopy<T>( - value: T, - options?: IDeepcopyOptions<T> | DeepcopyCustomizer<T>): T; - - namespace deepcopy {} // Hack - - export = deepcopy; -} diff --git a/src/@types/escape-regexp.d.ts b/src/@types/escape-regexp.d.ts deleted file mode 100644 index d68e6048a1..0000000000 --- a/src/@types/escape-regexp.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare module 'escape-regexp' { - function escapeRegExp(str: string): string; - - namespace escapeRegExp {} // Hack - - export = escapeRegExp; -} diff --git a/src/@types/webfinger.js.d.ts b/src/@types/webfinger.js.d.ts deleted file mode 100644 index 3556c45770..0000000000 --- a/src/@types/webfinger.js.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -declare module 'webfinger.js' { - interface IWebFingerConstructorConfig { - tls_only?: boolean; - webfist_fallback?: boolean; - uri_fallback?: boolean; - request_timeout?: number; - } - - type JRDProperties = { [type: string]: string }; - - interface IJRDLink { - rel: string; - type?: string; - href?: string; - template?: string; - titles?: { [lang: string]: string }; - properties?: JRDProperties; - } - - interface IJRD { - subject?: string; - expires?: Date; - aliases?: string[]; - properties?: JRDProperties; - links?: IJRDLink[]; - } - - interface IIDXLinks { - 'avatar': IJRDLink[]; - 'remotestorage': IJRDLink[]; - 'blog': IJRDLink[]; - 'vcard': IJRDLink[]; - 'updates': IJRDLink[]; - 'share': IJRDLink[]; - 'profile': IJRDLink[]; - 'webfist': IJRDLink[]; - 'camlistore': IJRDLink[]; - [type: string]: IJRDLink[]; - } - - interface IIDXProperties { - 'name': string; - [type: string]: string; - } - - interface IIDX { - links: IIDXLinks; - properties: IIDXProperties; - } - - interface ILookupCallbackResult { - object: IJRD; - json: string; - idx: IIDX; - } - - type LookupCallback = (err: Error | string, result?: ILookupCallbackResult) => void; - - export class WebFinger { - constructor(config?: IWebFingerConstructorConfig); - - public lookup(address: string, cb: LookupCallback): NodeJS.Timeout; - public lookupLink(address: string, rel: string, cb: IJRDLink): void; - } -} diff --git a/src/argv.ts b/src/argv.ts index b5540441cc..562852d17b 100644 --- a/src/argv.ts +++ b/src/argv.ts @@ -15,5 +15,8 @@ program .parse(process.argv); if (process.env.MK_ONLY_QUEUE) program.onlyQueue = true; +if (process.env.NODE_ENV === 'test') program.disableClustering = true; +if (process.env.NODE_ENV === 'test') program.quiet = true; +if (process.env.NODE_ENV === 'test') program.noDaemons = true; export { program }; diff --git a/src/boot/index.ts b/src/boot/index.ts new file mode 100644 index 0000000000..2c86d8ed8c --- /dev/null +++ b/src/boot/index.ts @@ -0,0 +1,77 @@ +import * as cluster from 'cluster'; +import chalk from 'chalk'; +import Xev from 'xev'; + +import Logger from '../services/logger'; +import { program } from '../argv'; + +// for typeorm +import 'reflect-metadata'; +import { masterMain } from './master'; +import { workerMain } from './worker'; + +const logger = new Logger('core', 'cyan'); +const clusterLogger = logger.createSubLogger('cluster', 'orange', false); +const ev = new Xev(); + +/** + * Init process + */ +export default async function() { + process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`; + + if (cluster.isMaster || program.disableClustering) { + await masterMain(); + + if (cluster.isMaster) { + ev.mount(); + } + } + + if (cluster.isWorker || program.disableClustering) { + await workerMain(); + } + + // ユニットテスト時にMisskeyが子プロセスで起動された時のため + // それ以外のときは process.send は使えないので弾く + if (process.send) { + process.send('ok'); + } +} + +//#region Events + +// Listen new workers +cluster.on('fork', worker => { + clusterLogger.debug(`Process forked: [${worker.id}]`); +}); + +// Listen online workers +cluster.on('online', worker => { + clusterLogger.debug(`Process is now online: [${worker.id}]`); +}); + +// Listen for dying workers +cluster.on('exit', worker => { + // Replace the dead worker, + // we're not sentimental + clusterLogger.error(chalk.red(`[${worker.id}] died :(`)); + cluster.fork(); +}); + +// Display detail of unhandled promise rejection +if (!program.quiet) { + process.on('unhandledRejection', console.dir); +} + +// Display detail of uncaught exception +process.on('uncaughtException', err => { + logger.error(err); +}); + +// Dying away... +process.on('exit', code => { + logger.info(`The process is going to exit with code ${code}`); +}); + +//#endregion diff --git a/src/boot/master.ts b/src/boot/master.ts new file mode 100644 index 0000000000..4d360c7265 --- /dev/null +++ b/src/boot/master.ts @@ -0,0 +1,176 @@ +import * as os from 'os'; +import * as cluster from 'cluster'; +import chalk from 'chalk'; +import * as portscanner from 'portscanner'; +import * as isRoot from 'is-root'; + +import Logger from '../services/logger'; +import loadConfig from '../config/load'; +import { Config } from '../config/types'; +import { lessThan } from '../prelude/array'; +import * as pkg from '../../package.json'; +import { program } from '../argv'; +import { showMachineInfo } from '../misc/show-machine-info'; +import { initDb } from '../db/postgre'; + +const logger = new Logger('core', 'cyan'); +const bootLogger = logger.createSubLogger('boot', 'magenta', false); + +function greet() { + if (!program.quiet) { + //#region Misskey logo + const v = `v${pkg.version}`; + console.log(' _____ _ _ '); + console.log(' | |_|___ ___| |_ ___ _ _ '); + console.log(' | | | | |_ -|_ -| \'_| -_| | |'); + console.log(' |_|_|_|_|___|___|_,_|___|_ |'); + console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length))); + //#endregion + + console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.'); + console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo')); + + console.log(''); + console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`); + } + + bootLogger.info('Welcome to Misskey!'); + bootLogger.info(`Misskey v${pkg.version}`, null, true); +} + +/** + * Init master process + */ +export async function masterMain() { + greet(); + + let config!: Config; + + try { + // initialize app + config = await init(); + + if (config.port == null) { + bootLogger.error('The port is not configured. Please configure port.', null, true); + process.exit(1); + } + + if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) { + bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true); + process.exit(1); + } + + if (!await isPortAvailable(config.port)) { + bootLogger.error(`Port ${config.port} is already in use`, null, true); + process.exit(1); + } + } catch (e) { + bootLogger.error('Fatal error occurred during initialization', null, true); + process.exit(1); + } + + bootLogger.succ('Misskey initialized'); + + if (!program.disableClustering) { + await spawnWorkers(config.clusterLimit); + } + + if (!program.noDaemons) { + require('../daemons/server-stats').default(); + require('../daemons/notes-stats').default(); + require('../daemons/queue-stats').default(); + } + + bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); +} + +const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10)); +const requiredNodejsVersion = [11, 7, 0]; +const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion); + +function isWellKnownPort(port: number): boolean { + return port < 1024; +} + +async function isPortAvailable(port: number): Promise<boolean> { + return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed'; +} + +function showEnvironment(): void { + const env = process.env.NODE_ENV; + const logger = bootLogger.createSubLogger('env'); + logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); + + if (env !== 'production') { + logger.warn('The environment is not in production mode.'); + logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); + } + + logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`); +} + +/** + * Init app + */ +async function init(): Promise<Config> { + showEnvironment(); + + const nodejsLogger = bootLogger.createSubLogger('nodejs'); + + nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`); + + if (!satisfyNodejsVersion) { + nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true); + process.exit(1); + } + + await showMachineInfo(bootLogger); + + const configLogger = bootLogger.createSubLogger('config'); + let config; + + try { + config = loadConfig(); + } catch (exception) { + if (typeof exception === 'string') { + configLogger.error(exception); + process.exit(1); + } + if (exception.code === 'ENOENT') { + configLogger.error('Configuration file not found', null, true); + process.exit(1); + } + throw exception; + } + + configLogger.succ('Loaded'); + + // Try to connect to DB + try { + bootLogger.info('Connecting database...'); + await initDb(); + } catch (e) { + bootLogger.error('Cannot connect to database', null, true); + bootLogger.error(e); + process.exit(1); + } + + return config; +} + +async function spawnWorkers(limit: number = Infinity) { + const workers = Math.min(limit, os.cpus().length); + bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); + await Promise.all([...Array(workers)].map(spawnWorker)); + bootLogger.succ('All workers started'); +} + +function spawnWorker(): Promise<void> { + return new Promise(res => { + const worker = cluster.fork(); + worker.on('message', message => { + if (message !== 'ready') return; + res(); + }); + }); +} diff --git a/src/boot/worker.ts b/src/boot/worker.ts new file mode 100644 index 0000000000..362fa3f26b --- /dev/null +++ b/src/boot/worker.ts @@ -0,0 +1,20 @@ +import * as cluster from 'cluster'; +import { initDb } from '../db/postgre'; + +/** + * Init worker process + */ +export async function workerMain() { + await initDb(); + + // start server + await require('../server').default(); + + // start job queue + require('../queue').default(); + + if (cluster.isWorker) { + // Send a 'ready' message to parent process + process.send!('ready'); + } +} diff --git a/src/client/app/admin/views/drive.vue b/src/client/app/admin/views/drive.vue index 7812aadaaf..e4565b78fe 100644 --- a/src/client/app/admin/views/drive.vue +++ b/src/client/app/admin/views/drive.vue @@ -38,7 +38,7 @@ <div class="kidvdlkg" v-for="file in files"> <div @click="file._open = !file._open"> <div> - <div class="thumbnail" :style="thumbnail(file)"></div> + <x-file-thumbnail class="thumbnail" :file="file" fit="contain" @click="showFileMenu(file)"/> </div> <div> <header> @@ -48,7 +48,7 @@ <div> <div> <span style="margin-right:16px;">{{ file.type }}</span> - <span>{{ file.datasize | bytes }}</span> + <span>{{ file.size | bytes }}</span> </div> <div><mk-time :time="file.createdAt" mode="detail"/></div> </div> @@ -75,10 +75,15 @@ import Vue from 'vue'; import i18n from '../../i18n'; import { faCloud, faTerminal, faSearch } from '@fortawesome/free-solid-svg-icons'; import { faTrashAlt, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; +import XFileThumbnail from '../../common/views/components/drive-file-thumbnail.vue'; export default Vue.extend({ i18n: i18n('admin/views/drive.vue'), + components: { + XFileThumbnail + }, + data() { return { file: null, @@ -151,13 +156,6 @@ export default Vue.extend({ }); }, - thumbnail(file: any): any { - return { - 'background-color': file.properties.avgColor && file.properties.avgColor.length == 3 ? `rgb(${file.properties.avgColor.join(',')})` : 'transparent', - 'background-image': `url(${file.thumbnailUrl})` - }; - }, - async del(file: any) { const process = async () => { await this.$root.api('drive/files/delete', { fileId: file.id }); @@ -179,9 +177,9 @@ export default Vue.extend({ this.$root.api('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive + }).then(() => { + file.isSensitive = !file.isSensitive; }); - - file.isSensitive = !file.isSensitive; }, async show() { diff --git a/src/client/app/admin/views/hashtags.vue b/src/client/app/admin/views/hashtags.vue index b3190c29c4..e1cc4b494d 100644 --- a/src/client/app/admin/views/hashtags.vue +++ b/src/client/app/admin/views/hashtags.vue @@ -3,7 +3,7 @@ <ui-card> <template #title>{{ $t('hided-tags') }}</template> <section> - <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hidedTags"></textarea> + <textarea class="jdnqwkzlnxcfftthoybjxrebyolvoucw" v-model="hiddenTags"></textarea> <ui-button @click="save">{{ $t('save') }}</ui-button> </section> </ui-card> @@ -18,18 +18,18 @@ export default Vue.extend({ i18n: i18n('admin/views/hashtags.vue'), data() { return { - hidedTags: '', + hiddenTags: '', }; }, created() { this.$root.getMeta().then(meta => { - this.hidedTags = meta.hidedTags.join('\n'); + this.hiddenTags = meta.hiddenTags.join('\n'); }); }, methods: { save() { this.$root.api('admin/update-meta', { - hidedTags: this.hidedTags.split('\n') + hiddenTags: this.hiddenTags.split('\n') }).then(() => { //this.$root.os.apis.dialog({ text: `Saved` }); }).catch(e => { diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 2d2a07784b..bc2a5fba85 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -78,12 +78,6 @@ <ui-input v-model="summalyProxy">URL</ui-input> </section> <section> - <header><fa :icon="faUserPlus"/> {{ $t('user-recommendation-config') }}</header> - <ui-switch v-model="enableExternalUserRecommendation">{{ $t('enable-external-user-recommendation') }}</ui-switch> - <ui-input v-model="externalUserRecommendationEngine" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-engine') }}<template #desc>{{ $t('external-user-recommendation-engine-desc') }}</template></ui-input> - <ui-input v-model="externalUserRecommendationTimeout" type="number" :disabled="!enableExternalUserRecommendation">{{ $t('external-user-recommendation-timeout') }}<template #suffix>ms</template><template #desc>{{ $t('external-user-recommendation-timeout-desc') }}</template></ui-input> - </section> - <section> <ui-button @click="updateMeta">{{ $t('save') }}</ui-button> </section> </ui-card> @@ -184,9 +178,6 @@ export default Vue.extend({ discordClientSecret: null, proxyAccount: null, inviteCode: null, - enableExternalUserRecommendation: false, - externalUserRecommendationEngine: null, - externalUserRecommendationTimeout: null, summalyProxy: null, enableEmail: false, email: null, @@ -205,8 +196,8 @@ export default Vue.extend({ created() { this.$root.getMeta().then(meta => { - this.maintainerName = meta.maintainer.name; - this.maintainerEmail = meta.maintainer.email; + this.maintainerName = meta.maintainerName; + this.maintainerEmail = meta.maintainerEmail; this.disableRegistration = meta.disableRegistration; this.disableLocalTimeline = meta.disableLocalTimeline; this.disableGlobalTimeline = meta.disableGlobalTimeline; @@ -236,9 +227,6 @@ export default Vue.extend({ this.enableDiscordIntegration = meta.enableDiscordIntegration; this.discordClientId = meta.discordClientId; this.discordClientSecret = meta.discordClientSecret; - this.enableExternalUserRecommendation = meta.enableExternalUserRecommendation; - this.externalUserRecommendationEngine = meta.externalUserRecommendationEngine; - this.externalUserRecommendationTimeout = meta.externalUserRecommendationTimeout; this.summalyProxy = meta.summalyProxy; this.enableEmail = meta.enableEmail; this.email = meta.email; @@ -299,9 +287,6 @@ export default Vue.extend({ enableDiscordIntegration: this.enableDiscordIntegration, discordClientId: this.discordClientId, discordClientSecret: this.discordClientSecret, - enableExternalUserRecommendation: this.enableExternalUserRecommendation, - externalUserRecommendationEngine: this.externalUserRecommendationEngine, - externalUserRecommendationTimeout: parseInt(this.externalUserRecommendationTimeout, 10), summalyProxy: this.summalyProxy, enableEmail: this.enableEmail, email: this.email, diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue index 4a2d957ed7..5c2cfdb396 100644 --- a/src/client/app/admin/views/logs.vue +++ b/src/client/app/admin/views/logs.vue @@ -19,7 +19,7 @@ </ui-horizon-group> <div class="nqjzuvev"> - <code v-for="log in logs" :key="log._id" :class="log.level"> + <code v-for="log in logs" :key="log.id" :class="log.level"> <details> <summary><mk-time :time="log.createdAt"/> [{{ log.domain.join('.') }}] {{ log.message }}</summary> <vue-json-pretty v-if="log.data" :data="log.data"></vue-json-pretty> diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue index ff485cec86..0f46b564a9 100644 --- a/src/client/app/admin/views/users.vue +++ b/src/client/app/admin/views/users.vue @@ -165,7 +165,7 @@ export default Vue.extend({ /** 処理対象ユーザーの情報を更新する */ async refreshUser() { - this.$root.api('admin/show-user', { userId: this.user._id }).then(info => { + this.$root.api('admin/show-user', { userId: this.user.id }).then(info => { this.user = info; }); }, @@ -173,7 +173,7 @@ export default Vue.extend({ async resetPassword() { if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return; - this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => { + this.$root.api('admin/reset-password', { userId: this.user.id }).then(res => { this.$root.dialog({ type: 'success', text: this.$t('password-updated', { password: res.password }) @@ -187,7 +187,7 @@ export default Vue.extend({ this.verifying = true; const process = async () => { - await this.$root.api('admin/verify-user', { userId: this.user._id }); + await this.$root.api('admin/verify-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', text: this.$t('verified') @@ -212,7 +212,7 @@ export default Vue.extend({ this.unverifying = true; const process = async () => { - await this.$root.api('admin/unverify-user', { userId: this.user._id }); + await this.$root.api('admin/unverify-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', text: this.$t('unverified') @@ -233,7 +233,7 @@ export default Vue.extend({ async silenceUser() { const process = async () => { - await this.$root.api('admin/silence-user', { userId: this.user._id }); + await this.$root.api('admin/silence-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', splash: true @@ -252,7 +252,7 @@ export default Vue.extend({ async unsilenceUser() { const process = async () => { - await this.$root.api('admin/unsilence-user', { userId: this.user._id }); + await this.$root.api('admin/unsilence-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', splash: true @@ -275,7 +275,7 @@ export default Vue.extend({ this.suspending = true; const process = async () => { - await this.$root.api('admin/suspend-user', { userId: this.user._id }); + await this.$root.api('admin/suspend-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', text: this.$t('suspended') @@ -300,7 +300,7 @@ export default Vue.extend({ this.unsuspending = true; const process = async () => { - await this.$root.api('admin/unsuspend-user', { userId: this.user._id }); + await this.$root.api('admin/unsuspend-user', { userId: this.user.id }); this.$root.dialog({ type: 'success', text: this.$t('unsuspended') @@ -320,7 +320,7 @@ export default Vue.extend({ }, async updateRemoteUser() { - this.$root.api('admin/update-remote-user', { userId: this.user._id }).then(res => { + this.$root.api('admin/update-remote-user', { userId: this.user.id }).then(res => { this.$root.dialog({ type: 'success', text: this.$t('remote-user-updated') diff --git a/src/client/app/auth/views/form.vue b/src/client/app/auth/views/form.vue index 105af375b6..b640a40560 100644 --- a/src/client/app/auth/views/form.vue +++ b/src/client/app/auth/views/form.vue @@ -14,15 +14,7 @@ <h2>{{ $t('permission-ask') }}</h2> <ul> <template v-for="p in app.permission"> - <li v-if="p == 'account-read'">{{ $t('account-read') }}</li> - <li v-if="p == 'account-write'">{{ $t('account-write') }}</li> - <li v-if="p == 'note-write'">{{ $t('note-write') }}</li> - <li v-if="p == 'like-write'">{{ $t('like-write') }}</li> - <li v-if="p == 'following-write'">{{ $t('following-write') }}</li> - <li v-if="p == 'drive-read'">{{ $t('drive-read') }}</li> - <li v-if="p == 'drive-write'">{{ $t('drive-write') }}</li> - <li v-if="p == 'notification-read'">{{ $t('notification-read') }}</li> - <li v-if="p == 'notification-write'">{{ $t('notification-write') }}</li> + <li :key="p">{{ $t(`@.permissions.${p}`) }}</li> </template> </ul> </section> diff --git a/src/client/app/common/define-widget.ts b/src/client/app/common/define-widget.ts index 1efdbb1880..632ddf2ed6 100644 --- a/src/client/app/common/define-widget.ts +++ b/src/client/app/common/define-widget.ts @@ -45,15 +45,9 @@ export default function <T extends object>(data: { this.$watch('props', () => { this.mergeProps(); }); - - this.bakeProps(); }, methods: { - bakeProps() { - this.bakedOldProps = JSON.stringify(this.props); - }, - mergeProps() { if (data.props) { const defaultProps = data.props(); @@ -65,17 +59,10 @@ export default function <T extends object>(data: { }, save() { - if (this.bakedOldProps == JSON.stringify(this.props)) return; - - this.bakeProps(); - if (this.platform == 'deck') { this.$store.commit('device/updateDeckColumn', this.column); } else { - this.$root.api('i/update_widget', { - id: this.id, - data: this.props - }); + this.$store.commit('device/updateWidget', this.widget); } } } diff --git a/src/client/app/common/scripts/note-mixin.ts b/src/client/app/common/scripts/note-mixin.ts index 5707d1bb41..67bbe8c0ae 100644 --- a/src/client/app/common/scripts/note-mixin.ts +++ b/src/client/app/common/scripts/note-mixin.ts @@ -70,8 +70,8 @@ export default (opts: Opts = {}) => ({ }, reactionsCount(): number { - return this.appearNote.reactionCounts - ? sum(Object.values(this.appearNote.reactionCounts)) + return this.appearNote.reactions + ? sum(Object.values(this.appearNote.reactions)) : 0; }, diff --git a/src/client/app/common/scripts/note-subscriber.ts b/src/client/app/common/scripts/note-subscriber.ts index c2b4dd6df9..02d810ded9 100644 --- a/src/client/app/common/scripts/note-subscriber.ts +++ b/src/client/app/common/scripts/note-subscriber.ts @@ -87,16 +87,16 @@ export default prop => ({ case 'reacted': { const reaction = body.reaction; - if (this.$_ns_target.reactionCounts == null) { - Vue.set(this.$_ns_target, 'reactionCounts', {}); + if (this.$_ns_target.reactions == null) { + Vue.set(this.$_ns_target, 'reactions', {}); } - if (this.$_ns_target.reactionCounts[reaction] == null) { - Vue.set(this.$_ns_target.reactionCounts, reaction, 0); + if (this.$_ns_target.reactions[reaction] == null) { + Vue.set(this.$_ns_target.reactions, reaction, 0); } // Increment the count - this.$_ns_target.reactionCounts[reaction]++; + this.$_ns_target.reactions[reaction]++; if (body.userId == this.$store.state.i.id) { Vue.set(this.$_ns_target, 'myReaction', reaction); @@ -107,16 +107,16 @@ export default prop => ({ case 'unreacted': { const reaction = body.reaction; - if (this.$_ns_target.reactionCounts == null) { + if (this.$_ns_target.reactions == null) { return; } - if (this.$_ns_target.reactionCounts[reaction] == null) { + if (this.$_ns_target.reactions[reaction] == null) { return; } // Decrement the count - if (this.$_ns_target.reactionCounts[reaction] > 0) this.$_ns_target.reactionCounts[reaction]--; + if (this.$_ns_target.reactions[reaction] > 0) this.$_ns_target.reactions[reaction]--; if (body.userId == this.$store.state.i.id) { Vue.set(this.$_ns_target, 'myReaction', null); @@ -125,9 +125,11 @@ export default prop => ({ } case 'pollVoted': { - if (body.userId == this.$store.state.i.id) return; const choice = body.choice; - this.$_ns_target.poll.choices.find(c => c.id === choice).votes++; + this.$_ns_target.poll.choices[choice].votes++; + if (body.userId == this.$store.state.i.id) { + Vue.set(this.$_ns_target.poll.choices[choice], 'isVoted', true); + } break; } diff --git a/src/client/app/common/views/components/avatar.vue b/src/client/app/common/views/components/avatar.vue index dce594e702..04f3ed9f78 100644 --- a/src/client/app/common/views/components/avatar.vue +++ b/src/client/app/common/views/components/avatar.vue @@ -55,11 +55,7 @@ export default Vue.extend({ }, icon(): any { return { - backgroundColor: this.lightmode - ? `rgb(${this.user.avatarColor.slice(0, 3).join(',')})` - : this.user.avatarColor && this.user.avatarColor.length == 3 - ? `rgb(${this.user.avatarColor.join(',')})` - : null, + backgroundColor: this.user.avatarColor, backgroundImage: this.lightmode ? null : `url(${this.url})`, borderRadius: this.$store.state.settings.circleIcons ? '100%' : null }; @@ -67,7 +63,7 @@ export default Vue.extend({ }, mounted() { if (this.user.avatarColor) { - this.$el.style.color = `rgb(${this.user.avatarColor.slice(0, 3).join(',')})`; + this.$el.style.color = this.user.avatarColor; } }, methods: { diff --git a/src/client/app/common/views/components/drive-file-thumbnail.vue b/src/client/app/common/views/components/drive-file-thumbnail.vue index faa727f3b6..1a3ef37193 100644 --- a/src/client/app/common/views/components/drive-file-thumbnail.vue +++ b/src/client/app/common/views/components/drive-file-thumbnail.vue @@ -105,15 +105,11 @@ export default Vue.extend({ }, isThumbnailAvailable(): boolean { return this.file.thumbnailUrl - ? this.file.thumbnailUrl.endsWith('?thumbnail') - ? (this.is === 'image' || this.is === 'video') - : true + ? (this.is === 'image' || this.is === 'video') : false; }, background(): string { - return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 - ? `rgb(${this.file.properties.avgColor.join(',')})` - : 'transparent'; + return this.file.properties.avgColor || 'transparent'; } }, mounted() { @@ -122,10 +118,10 @@ export default Vue.extend({ }, methods: { onThumbnailLoaded() { - if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) { + if (this.file.properties.avgColor) { anime({ targets: this.$refs.thumbnail, - backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`, + backgroundColor: this.file.properties.avgColor.replace('255)', '0)'), duration: 100, easing: 'linear' }); diff --git a/src/client/app/common/views/components/games/reversi/reversi.game.vue b/src/client/app/common/views/components/games/reversi/reversi.game.vue index c6fc36db33..bd0401f785 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.game.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.game.vue @@ -24,11 +24,11 @@ <div class="board"> <div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> - <span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span> + <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> </div> <div class="flex"> <div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> - <div v-for="i in game.settings.map.length">{{ i }}</div> + <div v-for="i in game.map.length">{{ i }}</div> </div> <div class="cells" :style="cellsStyle"> <div v-for="(stone, i) in o.board" @@ -46,11 +46,11 @@ </div> </div> <div class="labels-y" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> - <div v-for="i in game.settings.map.length">{{ i }}</div> + <div v-for="i in game.map.length">{{ i }}</div> </div> </div> <div class="labels-x" v-if="this.$store.state.settings.games.reversi.showBoardLabels"> - <span v-for="i in game.settings.map[0].length">{{ String.fromCharCode(64 + i) }}</span> + <span v-for="i in game.map[0].length">{{ String.fromCharCode(64 + i) }}</span> </div> </div> @@ -71,9 +71,9 @@ </div> <div class="info"> - <p v-if="game.settings.isLlotheo">{{ $t('is-llotheo') }}</p> - <p v-if="game.settings.loopedBoard">{{ $t('looped-map') }}</p> - <p v-if="game.settings.canPutEverywhere">{{ $t('can-put-everywhere') }}</p> + <p v-if="game.isLlotheo">{{ $t('is-llotheo') }}</p> + <p v-if="game.loopedBoard">{{ $t('looped-map') }}</p> + <p v-if="game.canPutEverywhere">{{ $t('can-put-everywhere') }}</p> </div> </div> </template> @@ -160,8 +160,8 @@ export default Vue.extend({ cellsStyle(): any { return { - 'grid-template-rows': `repeat(${this.game.settings.map.length}, 1fr)`, - 'grid-template-columns': `repeat(${this.game.settings.map[0].length}, 1fr)` + 'grid-template-rows': `repeat(${this.game.map.length}, 1fr)`, + 'grid-template-columns': `repeat(${this.game.map[0].length}, 1fr)` }; } }, @@ -169,10 +169,10 @@ export default Vue.extend({ watch: { logPos(v) { if (!this.game.isEnded) return; - this.o = new Reversi(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard + this.o = new Reversi(this.game.map, { + isLlotheo: this.game.isLlotheo, + canPutEverywhere: this.game.canPutEverywhere, + loopedBoard: this.game.loopedBoard }); for (const log of this.logs.slice(0, v)) { this.o.put(log.color, log.pos); @@ -184,10 +184,10 @@ export default Vue.extend({ created() { this.game = this.initGame; - this.o = new Reversi(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard + this.o = new Reversi(this.game.map, { + isLlotheo: this.game.isLlotheo, + canPutEverywhere: this.game.canPutEverywhere, + loopedBoard: this.game.loopedBoard }); for (const log of this.game.logs) { @@ -286,10 +286,10 @@ export default Vue.extend({ onRescue(game) { this.game = game; - this.o = new Reversi(this.game.settings.map, { - isLlotheo: this.game.settings.isLlotheo, - canPutEverywhere: this.game.settings.canPutEverywhere, - loopedBoard: this.game.settings.loopedBoard + this.o = new Reversi(this.game.map, { + isLlotheo: this.game.isLlotheo, + canPutEverywhere: this.game.canPutEverywhere, + loopedBoard: this.game.loopedBoard }); for (const log of this.game.logs) { diff --git a/src/client/app/common/views/components/games/reversi/reversi.room.vue b/src/client/app/common/views/components/games/reversi/reversi.room.vue index d5d148790c..9ee1a78b86 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.room.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.room.vue @@ -17,9 +17,9 @@ </header> <div> - <div class="random" v-if="game.settings.map == null"><fa icon="dice"/></div> - <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.settings.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.settings.map[0].length }, 1fr)` }"> - <div v-for="(x, i) in game.settings.map.join('')" + <div class="random" v-if="game.map == null"><fa icon="dice"/></div> + <div class="board" v-else :style="{ 'grid-template-rows': `repeat(${ game.map.length }, 1fr)`, 'grid-template-columns': `repeat(${ game.map[0].length }, 1fr)` }"> + <div v-for="(x, i) in game.map.join('')" :data-none="x == ' '" @click="onPixelClick(i, x)"> <fa v-if="x == 'b'" :icon="fasCircle"/> @@ -35,9 +35,9 @@ </header> <div> - <form-radio v-model="game.settings.bw" value="random" @change="updateSettings">{{ $t('random') }}</form-radio> - <form-radio v-model="game.settings.bw" :value="1" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> - <form-radio v-model="game.settings.bw" :value="2" @change="updateSettings">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> + <form-radio v-model="game.bw" value="random" @change="updateSettings('bw')">{{ $t('random') }}</form-radio> + <form-radio v-model="game.bw" :value="1" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user1"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> + <form-radio v-model="game.bw" :value="2" @change="updateSettings('bw')">{{ this.$t('black-is').split('{}')[0] }}<b><mk-user-name :user="game.user2"/></b>{{ this.$t('black-is').split('{}')[1] }}</form-radio> </div> </div> @@ -47,9 +47,9 @@ </header> <div> - <ui-switch v-model="game.settings.isLlotheo" @change="updateSettings">{{ $t('is-llotheo') }}</ui-switch> - <ui-switch v-model="game.settings.loopedBoard" @change="updateSettings">{{ $t('looped-map') }}</ui-switch> - <ui-switch v-model="game.settings.canPutEverywhere" @change="updateSettings">{{ $t('can-put-everywhere') }}</ui-switch> + <ui-switch v-model="game.isLlotheo" @change="updateSettings('isLlotheo')">{{ $t('is-llotheo') }}</ui-switch> + <ui-switch v-model="game.loopedBoard" @change="updateSettings('loopedBoard')">{{ $t('looped-map') }}</ui-switch> + <ui-switch v-model="game.canPutEverywhere" @change="updateSettings('canPutEverywhere')">{{ $t('can-put-everywhere') }}</ui-switch> </div> </div> @@ -159,8 +159,8 @@ export default Vue.extend({ this.connection.on('initForm', this.onInitForm); this.connection.on('message', this.onMessage); - if (this.game.user1Id != this.$store.state.i.id && this.game.settings.form1) this.form = this.game.settings.form1; - if (this.game.user2Id != this.$store.state.i.id && this.game.settings.form2) this.form = this.game.settings.form2; + if (this.game.user1Id != this.$store.state.i.id && this.game.form1) this.form = this.game.form1; + if (this.game.user2Id != this.$store.state.i.id && this.game.form2) this.form = this.game.form2; }, beforeDestroy() { @@ -189,18 +189,19 @@ export default Vue.extend({ this.$forceUpdate(); }, - updateSettings() { + updateSettings(key: string) { this.connection.send('updateSettings', { - settings: this.game.settings + key: key, + value: this.game[key] }); }, - onUpdateSettings(settings) { - this.game.settings = settings; - if (this.game.settings.map == null) { + onUpdateSettings({ key, value }) { + this.game[key] = value; + if (this.game.map == null) { this.mapName = null; } else { - const found = Object.values(maps).find(x => x.data.join('') == this.game.settings.map.join('')); + const found = Object.values(maps).find(x => x.data.join('') == this.game.map.join('')); this.mapName = found ? found.name : '-Custom-'; } }, @@ -224,27 +225,27 @@ export default Vue.extend({ onMapChange() { if (this.mapName == null) { - this.game.settings.map = null; + this.game.map = null; } else { - this.game.settings.map = Object.values(maps).find(x => x.name == this.mapName).data; + this.game.map = Object.values(maps).find(x => x.name == this.mapName).data; } this.$forceUpdate(); this.updateSettings(); }, onPixelClick(pos, pixel) { - const x = pos % this.game.settings.map[0].length; - const y = Math.floor(pos / this.game.settings.map[0].length); + const x = pos % this.game.map[0].length; + const y = Math.floor(pos / this.game.map[0].length); const newPixel = pixel == ' ' ? '-' : pixel == '-' ? 'b' : pixel == 'b' ? 'w' : ' '; - const line = this.game.settings.map[y].split(''); + const line = this.game.map[y].split(''); line[x] = newPixel; - this.$set(this.game.settings.map, y, line.join('')); + this.$set(this.game.map, y, line.join('')); this.$forceUpdate(); - this.updateSettings(); + this.updateSettings('map'); } } }); diff --git a/src/client/app/common/views/components/games/reversi/reversi.vue b/src/client/app/common/views/components/games/reversi/reversi.vue index b6803cd7f7..d33471a049 100644 --- a/src/client/app/common/views/components/games/reversi/reversi.vue +++ b/src/client/app/common/views/components/games/reversi/reversi.vue @@ -106,7 +106,7 @@ export default Vue.extend({ async nav(game, actualNav = true) { if (this.selfNav) { // 受け取ったゲーム情報が省略されたものなら完全な情報を取得する - if (game != null && (game.settings == null || game.settings.map == null)) { + if (game != null && game.map == null) { game = await this.$root.api('games/reversi/games/show', { gameId: game.id }); diff --git a/src/client/app/common/views/components/instance.vue b/src/client/app/common/views/components/instance.vue index 7b8d4f8e0b..497e4976f5 100644 --- a/src/client/app/common/views/components/instance.vue +++ b/src/client/app/common/views/components/instance.vue @@ -2,7 +2,7 @@ <div class="nhasjydimbopojusarffqjyktglcuxjy" v-if="meta"> <div class="banner" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"></div> - <h1>{{ meta.name }}</h1> + <h1>{{ meta.name || 'Misskey' }}</h1> <p v-html="meta.description || this.$t('@.about')"></p> <router-link to="/">{{ $t('start') }}</router-link> </div> diff --git a/src/client/app/common/views/components/media-image.vue b/src/client/app/common/views/components/media-image.vue index 3947ef5527..2559907512 100644 --- a/src/client/app/common/views/components/media-image.vue +++ b/src/client/app/common/views/components/media-image.vue @@ -52,7 +52,7 @@ export default Vue.extend({ } return { - 'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent', + 'background-color': this.image.properties.avgColor || 'transparent', 'background-image': url }; } diff --git a/src/client/app/common/views/components/mention.vue b/src/client/app/common/views/components/mention.vue index 11dddbd52a..e1f67282b6 100644 --- a/src/client/app/common/views/components/mention.vue +++ b/src/client/app/common/views/components/mention.vue @@ -33,7 +33,7 @@ export default Vue.extend({ }, computed: { canonical(): string { - return `@${this.username}@${toUnicode(this.host)}`; + return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`; }, isMe(): boolean { return this.$store.getters.isSignedIn && this.canonical.toLowerCase() === `@${this.$store.state.i.username}@${toUnicode(localHost)}`.toLowerCase(); diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index ce76c402f3..256ea760b3 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -11,7 +11,7 @@ <div class="file" v-if="message.file"> <a :href="message.file.url" target="_blank" :title="message.file.name"> <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name" - :style="{ backgroundColor: message.file.properties.avgColor && message.file.properties.avgColor.length == 3 ? `rgb(${message.file.properties.avgColor.join(',')})` : 'transparent' }"/> + :style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/> <p v-else>{{ message.file.name }}</p> </a> </div> diff --git a/src/client/app/common/views/components/poll.vue b/src/client/app/common/views/components/poll.vue index ba14ba3a44..dc3aaa34f3 100644 --- a/src/client/app/common/views/components/poll.vue +++ b/src/client/app/common/views/components/poll.vue @@ -1,7 +1,7 @@ <template> <div class="mk-poll" :data-done="closed || isVoted"> <ul> - <li v-for="choice in poll.choices" :key="choice.id" @click="vote(choice.id)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''"> + <li v-for="(choice, i) in poll.choices" :key="i" @click="vote(i)" :class="{ voted: choice.voted }" :title="!closed && !isVoted ? $t('vote-to').replace('{}', choice.text) : ''"> <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <span> <template v-if="choice.isVoted"><fa icon="check"/></template> @@ -82,12 +82,6 @@ export default Vue.extend({ noteId: this.note.id, choice: id }).then(() => { - for (const c of this.poll.choices) { - if (c.id == id) { - c.votes++; - Vue.set(c, 'isVoted', true); - } - } if (!this.showResult) this.showResult = !this.poll.multiple; }); } diff --git a/src/client/app/common/views/components/post-form-attaches.vue b/src/client/app/common/views/components/post-form-attaches.vue new file mode 100644 index 0000000000..467e430ccf --- /dev/null +++ b/src/client/app/common/views/components/post-form-attaches.vue @@ -0,0 +1,139 @@ +<template> +<div class="skeikyzd" v-show="files.length != 0"> + <x-draggable class="files" :list="files" :options="{ animation: 150 }"> + <div v-for="file in files" :key="file.id" @click="showFileMenu(file, $event)" @contextmenu.prevent="showFileMenu(file, $event)"> + <x-file-thumbnail :data-id="file.id" class="thumbnail" :file="file" fit="cover"/> + <img class="remove" @click.stop="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/> + <div class="sensitive" v-if="file.isSensitive"> + <fa class="icon" :icon="faExclamationTriangle"/> + </div> + </div> + </x-draggable> + <p class="remain">{{ 4 - files.length }}/4</p> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import i18n from '../../../i18n'; +import * as XDraggable from 'vuedraggable'; +import XMenu from '../../../common/views/components/menu.vue'; +import { faTimesCircle, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; +import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; +import XFileThumbnail from './drive-file-thumbnail.vue' + +export default Vue.extend({ + i18n: i18n('common/views/components/post-form-attaches.vue'), + + components: { + XDraggable, + XFileThumbnail + }, + + props: { + files: { + type: Object, + required: true + }, + detachMediaFn: { + type: Object, + required: false + } + }, + + data() { + return { + faExclamationTriangle + }; + }, + + methods: { + detachMedia(id) { + if (this.detachMediaFn) this.detachMediaFn(id) + else if (this.$parent.detachMedia) this.$parent.detachMedia(id) + }, + toggleSensitive(file) { + this.$root.api('drive/files/update', { + fileId: file.id, + isSensitive: !file.isSensitive + }).then(() => { + file.isSensitive = !file.isSensitive; + }); + }, + showFileMenu(file, ev: MouseEvent) { + this.$root.new(XMenu, { + items: [{ + type: 'item', + text: file.isSensitive ? this.$t('unmark-as-sensitive') : this.$t('mark-as-sensitive'), + icon: file.isSensitive ? faEyeSlash : faEye, + action: () => { this.toggleSensitive(file) } + }, { + type: 'item', + text: this.$t('attach-cancel'), + icon: faTimesCircle, + action: () => { this.detachMedia(file.id) } + }], + source: ev.currentTarget || ev.target + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +.skeikyzd + padding 4px + + > .files + display flex + flex-wrap wrap + + > div + width 64px + height 64px + margin 4px + cursor move + + &:hover > .remove + display block + + > .thumbnail + width 100% + height 100% + z-index 1 + color var(--text) + + > .remove + display none + position absolute + top -6px + right -6px + width 16px + height 16px + cursor pointer + z-index 1000 + + > .sensitive + display flex + position absolute + width 64px + height 64px + top 0 + left 0 + z-index 2 + background rgba(17, 17, 17, .7) + color #fff + + > .icon + margin auto + + > .remain + display block + position absolute + top 8px + right 8px + margin 0 + padding 0 + color var(--primaryAlpha04) + +</style> diff --git a/src/client/app/common/views/components/reactions-viewer.vue b/src/client/app/common/views/components/reactions-viewer.vue index cf7f88b2f5..46668054b8 100644 --- a/src/client/app/common/views/components/reactions-viewer.vue +++ b/src/client/app/common/views/components/reactions-viewer.vue @@ -20,7 +20,7 @@ export default Vue.extend({ }, computed: { reactions(): any { - return this.note.reactionCounts; + return this.note.reactions; }, isMe(): boolean { return this.$store.getters.isSignedIn && this.$store.state.i.id === this.note.userId; diff --git a/src/client/app/common/views/components/settings/notification.vue b/src/client/app/common/views/components/settings/notification.vue index b689544d69..2554fe6331 100644 --- a/src/client/app/common/views/components/settings/notification.vue +++ b/src/client/app/common/views/components/settings/notification.vue @@ -2,7 +2,7 @@ <ui-card> <template #title><fa :icon="['far', 'bell']"/> {{ $t('title') }}</template> <section> - <ui-switch v-model="$store.state.i.settings.autoWatch" @change="onChangeAutoWatch"> + <ui-switch v-model="$store.state.i.autoWatch" @change="onChangeAutoWatch"> {{ $t('auto-watch') }}<template #desc>{{ $t('auto-watch-desc') }}</template> </ui-switch> <section> diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue index b9837a6966..fd08f85816 100644 --- a/src/client/app/common/views/components/settings/profile.vue +++ b/src/client/app/common/views/components/settings/profile.vue @@ -158,14 +158,14 @@ export default Vue.extend({ computed: { alwaysMarkNsfw: { - get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, + get() { return this.$store.state.i.alwaysMarkNsfw; }, set(value) { this.$root.api('i/update', { alwaysMarkNsfw: value }); } }, bannerStyle(): any { if (this.$store.state.i.bannerUrl == null) return {}; return { - backgroundColor: this.$store.state.i.bannerColor && this.$store.state.i.bannerColor.length == 3 ? `rgb(${ this.$store.state.i.bannerColor.join(',') })` : null, + backgroundColor: this.$store.state.i.bannerColor, backgroundImage: `url(${ this.$store.state.i.bannerUrl })` }; }, @@ -178,10 +178,10 @@ export default Vue.extend({ this.email = this.$store.state.i.email; this.name = this.$store.state.i.name; this.username = this.$store.state.i.username; - this.location = this.$store.state.i.profile.location; + this.location = this.$store.state.i.location; this.description = this.$store.state.i.description; this.lang = this.$store.state.i.lang; - this.birthday = this.$store.state.i.profile.birthday; + this.birthday = this.$store.state.i.birthday; this.avatarId = this.$store.state.i.avatarId; this.bannerId = this.$store.state.i.bannerId; this.isCat = this.$store.state.i.isCat; diff --git a/src/client/app/common/views/components/settings/theme.vue b/src/client/app/common/views/components/settings/theme.vue index 1dff61e459..3440aacb28 100644 --- a/src/client/app/common/views/components/settings/theme.vue +++ b/src/client/app/common/views/components/settings/theme.vue @@ -130,20 +130,6 @@ import * as tinycolor from 'tinycolor2'; import * as JSON5 from 'json5'; import { faMoon, faSun } from '@fortawesome/free-regular-svg-icons'; -// 後方互換性のため -function convertOldThemedefinition(t) { - const t2 = { - id: t.meta.id, - name: t.meta.name, - author: t.meta.author, - base: t.meta.base, - vars: t.meta.vars, - props: t - }; - delete t2.props.meta; - return t2; -} - export default Vue.extend({ i18n: i18n('common/views/components/theme.vue'), components: { @@ -231,20 +217,6 @@ export default Vue.extend({ } }, - beforeCreate() { - // migrate old theme definitions - // 後方互換性のため - this.$store.commit('device/set', { - key: 'themes', value: this.$store.state.device.themes.map(t => { - if (t.id == null) { - return convertOldThemedefinition(t); - } else { - return t; - } - }) - }); - }, - methods: { install(code) { let theme; @@ -259,11 +231,6 @@ export default Vue.extend({ return; } - // 後方互換性のため - if (theme.id == null && theme.meta != null) { - theme = convertOldThemedefinition(theme); - } - if (theme.id == null) { this.$root.dialog({ type: 'error', diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue index 16e1afaa94..45c2eabd45 100644 --- a/src/client/app/common/views/components/signup.vue +++ b/src/client/app/common/views/components/signup.vue @@ -4,7 +4,7 @@ <ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required styl="fill"> <span>{{ $t('invitation-code') }}</span> <template #prefix><fa icon="id-card-alt"/></template> - <template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainer.email)"></template> + <template #desc v-html="this.$t('invitation-info').replace('{}', 'mailto:' + meta.maintainerEmail)"></template> </ui-input> <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername" styl="fill"> <span>{{ $t('username') }}</span> diff --git a/src/client/app/common/views/components/user-list-editor.vue b/src/client/app/common/views/components/user-list-editor.vue index 53c945ca0a..8d2e04d045 100644 --- a/src/client/app/common/views/components/user-list-editor.vue +++ b/src/client/app/common/views/components/user-list-editor.vue @@ -1,7 +1,7 @@ <template> <div class="cudqjmnl"> <ui-card> - <template #title><fa :icon="faList"/> {{ list.title }}</template> + <template #title><fa :icon="faList"/> {{ list.name }}</template> <section> <ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> @@ -75,7 +75,7 @@ export default Vue.extend({ this.$root.dialog({ title: this.$t('rename'), input: { - default: this.list.title + default: this.list.name } }).then(({ canceled, result: title }) => { if (canceled) return; @@ -89,7 +89,7 @@ export default Vue.extend({ del() { this.$root.dialog({ type: 'warning', - text: this.$t('delete-are-you-sure').replace('$1', this.list.title), + text: this.$t('delete-are-you-sure').replace('$1', this.list.name), showCancelButton: true }).then(({ canceled }) => { if (canceled) return; diff --git a/src/client/app/common/views/components/user-list.vue b/src/client/app/common/views/components/user-list.vue index b56cb13c3e..b8bcc35d82 100644 --- a/src/client/app/common/views/components/user-list.vue +++ b/src/client/app/common/views/components/user-list.vue @@ -51,7 +51,7 @@ export default Vue.extend({ fetchingMoreUsers: false, us: [], inited: false, - cursor: null + more: false }; }, diff --git a/src/client/app/common/views/components/user-menu.vue b/src/client/app/common/views/components/user-menu.vue index 93fd759fd9..a95f7a9225 100644 --- a/src/client/app/common/views/components/user-menu.vue +++ b/src/client/app/common/views/components/user-menu.vue @@ -73,7 +73,7 @@ export default Vue.extend({ title: t, select: { items: lists.map(list => ({ - value: list.id, text: list.title + value: list.id, text: list.name })) }, showCancelButton: true diff --git a/src/client/app/common/views/deck/deck.direct.vue b/src/client/app/common/views/deck/deck.direct.vue index 2618363b14..29db5cb7f3 100644 --- a/src/client/app/common/views/deck/deck.direct.vue +++ b/src/client/app/common/views/deck/deck.direct.vue @@ -28,12 +28,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/common/views/deck/deck.favorites-column.vue b/src/client/app/common/views/deck/deck.favorites-column.vue index 238938594f..526b998f87 100644 --- a/src/client/app/common/views/deck/deck.favorites-column.vue +++ b/src/client/app/common/views/deck/deck.favorites-column.vue @@ -37,12 +37,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/common/views/deck/deck.hashtag-tl.vue b/src/client/app/common/views/deck/deck.hashtag-tl.vue index 07d96f82c4..6f89f6a23d 100644 --- a/src/client/app/common/views/deck/deck.hashtag-tl.vue +++ b/src/client/app/common/views/deck/deck.hashtag-tl.vue @@ -28,7 +28,7 @@ export default Vue.extend({ data() { return { connection: null, - makePromise: cursor => this.$root.api('notes/search_by_tag', { + makePromise: cursor => this.$root.api('notes/search-by-tag', { limit: fetchLimit + 1, untilId: cursor ? cursor : undefined, withFiles: this.mediaOnly, @@ -41,12 +41,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/common/views/deck/deck.list-tl.vue b/src/client/app/common/views/deck/deck.list-tl.vue index d1887990f2..24080ad4ea 100644 --- a/src/client/app/common/views/deck/deck.list-tl.vue +++ b/src/client/app/common/views/deck/deck.list-tl.vue @@ -41,12 +41,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/common/views/deck/deck.mentions.vue b/src/client/app/common/views/deck/deck.mentions.vue index 1efd778226..153b4cd052 100644 --- a/src/client/app/common/views/deck/deck.mentions.vue +++ b/src/client/app/common/views/deck/deck.mentions.vue @@ -27,12 +27,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/common/views/deck/deck.notes.vue b/src/client/app/common/views/deck/deck.notes.vue index 8787a82a1c..15a78bef26 100644 --- a/src/client/app/common/views/deck/deck.notes.vue +++ b/src/client/app/common/views/deck/deck.notes.vue @@ -26,8 +26,8 @@ </template> </component> - <footer v-if="cursor != null"> - <button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <footer v-if="more"> + <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> </button> @@ -61,7 +61,7 @@ export default Vue.extend({ fetching: true, moreFetching: false, inited: false, - cursor: null + more: false }; }, @@ -119,7 +119,7 @@ export default Vue.extend({ this.notes = x; } else { this.notes = x.notes; - this.cursor = x.cursor; + this.more = x.more; } this.inited = true; this.fetching = false; @@ -129,12 +129,12 @@ export default Vue.extend({ }); }, - more() { - if (this.cursor == null || this.moreFetching) return; + fetchMore() { + if (!this.more || this.moreFetching) return; this.moreFetching = true; - this.makePromise(this.cursor).then(x => { + this.makePromise(this.notes[this.notes.length - 1].id).then(x => { this.notes = this.notes.concat(x.notes); - this.cursor = x.cursor; + this.more = x.more; this.moreFetching = false; }, e => { this.moreFetching = false; @@ -157,7 +157,7 @@ export default Vue.extend({ // オーバーフローしたら古い投稿は捨てる if (this.notes.length >= displayLimit) { this.notes = this.notes.slice(0, displayLimit); - this.cursor = this.notes[this.notes.length - 1].id + this.more = true; } } else { this.queue.push(note); @@ -181,7 +181,7 @@ export default Vue.extend({ }, onBottom() { - this.more(); + this.fetchMore(); } } }); diff --git a/src/client/app/common/views/deck/deck.notification.vue b/src/client/app/common/views/deck/deck.notification.vue index 6a116260e5..3ced7b7e23 100644 --- a/src/client/app/common/views/deck/deck.notification.vue +++ b/src/client/app/common/views/deck/deck.notification.vue @@ -62,7 +62,7 @@ </div> </div> - <div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> + <div class="notification pollVote" v-if="notification.type == 'pollVote'"> <mk-avatar class="avatar" :user="notification.user"/> <div> <header> diff --git a/src/client/app/common/views/deck/deck.search-column.vue b/src/client/app/common/views/deck/deck.search-column.vue index fb0ba5f6e4..ab19bdaab6 100644 --- a/src/client/app/common/views/deck/deck.search-column.vue +++ b/src/client/app/common/views/deck/deck.search-column.vue @@ -39,7 +39,7 @@ export default Vue.extend({ } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/common/views/deck/deck.tl-column.vue b/src/client/app/common/views/deck/deck.tl-column.vue index d53aabaea5..5ab8ccb12f 100644 --- a/src/client/app/common/views/deck/deck.tl-column.vue +++ b/src/client/app/common/views/deck/deck.tl-column.vue @@ -82,7 +82,7 @@ export default Vue.extend({ case 'local': return this.$t('@deck.local'); case 'hybrid': return this.$t('@deck.hybrid'); case 'global': return this.$t('@deck.global'); - case 'list': return this.column.list.title; + case 'list': return this.column.list.name; case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title; } } diff --git a/src/client/app/common/views/deck/deck.tl.vue b/src/client/app/common/views/deck/deck.tl.vue index 35cdfa704f..9284f06ee1 100644 --- a/src/client/app/common/views/deck/deck.tl.vue +++ b/src/client/app/common/views/deck/deck.tl.vue @@ -85,12 +85,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }); diff --git a/src/client/app/common/views/deck/deck.user-column.home.vue b/src/client/app/common/views/deck/deck.user-column.home.vue index ee24cad1c5..cf592d031f 100644 --- a/src/client/app/common/views/deck/deck.user-column.home.vue +++ b/src/client/app/common/views/deck/deck.user-column.home.vue @@ -95,12 +95,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: new Date(notes[notes.length - 1].createdAt).getTime() + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }); diff --git a/src/client/app/common/views/deck/deck.user-column.vue b/src/client/app/common/views/deck/deck.user-column.vue index 9e9f494b13..fb50d880eb 100644 --- a/src/client/app/common/views/deck/deck.user-column.vue +++ b/src/client/app/common/views/deck/deck.user-column.vue @@ -8,7 +8,7 @@ <div class="is-remote" v-if="user.host != null"> <details> <summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary> - <a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a> + <a :href="user.url" target="_blank">{{ $t('@.view-on-remote') }}</a> </details> </div> <header :style="bannerStyle"> @@ -88,7 +88,7 @@ export default Vue.extend({ if (this.user == null) return {}; if (this.user.bannerUrl == null) return {}; return { - backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundColor: this.user.bannerColor, backgroundImage: `url(${ this.user.bannerUrl })` }; }, diff --git a/src/client/app/common/views/deck/deck.vue b/src/client/app/common/views/deck/deck.vue index 8ffb3223f9..b46f2167ad 100644 --- a/src/client/app/common/views/deck/deck.vue +++ b/src/client/app/common/views/deck/deck.vue @@ -106,16 +106,6 @@ export default Vue.extend({ value: deck }); } - - // 互換性のため - if (this.$store.state.device.deck != null && this.$store.state.device.deck.layout == null) { - this.$store.commit('device/set', { - key: 'deck', - value: Object.assign({}, this.$store.state.device.deck, { - layout: this.$store.state.device.deck.columns.map(c => [c.id]) - }) - }); - } }, mounted() { @@ -199,7 +189,7 @@ export default Vue.extend({ title: this.$t('@deck.select-list'), select: { items: lists.map(list => ({ - value: list.id, text: list.title + value: list.id, text: list.name })) }, showCancelButton: true diff --git a/src/client/app/common/views/pages/explore.vue b/src/client/app/common/views/pages/explore.vue index 098bf1f4c4..67e92af445 100644 --- a/src/client/app/common/views/pages/explore.vue +++ b/src/client/app/common/views/pages/explore.vue @@ -3,7 +3,7 @@ <ui-container :show-header="false" v-if="meta && stats"> <div class="kpdsmpnk" :style="{ backgroundImage: meta.bannerUrl ? `url(${meta.bannerUrl})` : null }"> <div> - <router-link to="/explore" class="title">{{ $t('explore', { host: meta.name }) }}</router-link> + <router-link to="/explore" class="title">{{ $t('explore', { host: meta.name || 'Misskey' }) }}</router-link> <span>{{ $t('users-info', { users: num(stats.originalUsersCount) }) }}</span> </div> </div> @@ -13,8 +13,8 @@ <template #header><fa :icon="faHashtag" fixed-width/>{{ $t('popular-tags') }}</template> <div class="vxjfqztj"> - <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.tag}`" :key="'local:' + tag.tag" class="local">{{ tag.tag }}</router-link> - <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.tag}`" :key="'remote:' + tag.tag">{{ tag.tag }}</router-link> + <router-link v-for="tag in tagsLocal" :to="`/explore/tags/${tag.name}`" :key="'local:' + tag.name" class="local">{{ tag.name }}</router-link> + <router-link v-for="tag in tagsRemote" :to="`/explore/tags/${tag.name}`" :key="'remote:' + tag.name">{{ tag.name }}</router-link> </div> </ui-container> diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue index f8d12a2dca..f6a11a7b4f 100644 --- a/src/client/app/common/views/pages/follow.vue +++ b/src/client/app/common/views/pages/follow.vue @@ -57,7 +57,7 @@ export default Vue.extend({ bannerStyle(): any { if (this.user.bannerUrl == null) return {}; return { - backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundColor: this.user.bannerColor, backgroundImage: `url(${ this.user.bannerUrl })` }; } diff --git a/src/client/app/common/views/pages/followers.vue b/src/client/app/common/views/pages/followers.vue index 94d9c9b13c..1d68d71e80 100644 --- a/src/client/app/common/views/pages/followers.vue +++ b/src/client/app/common/views/pages/followers.vue @@ -9,20 +9,30 @@ import Vue from 'vue'; import parseAcct from '../../../../../misc/acct/parse'; import i18n from '../../../i18n'; +const fetchLimit = 30; + export default Vue.extend({ - i18n: i18n(''), + i18n: i18n(), data() { return { makePromise: cursor => this.$root.api('users/followers', { ...parseAcct(this.$route.params.user), - limit: 30, - cursor: cursor ? cursor : undefined - }).then(x => { - return { - users: x.users, - cursor: x.next - }; + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + }).then(followings => { + if (followings.length == fetchLimit + 1) { + followings.pop(); + return { + users: followings.map(following => following.follower), + cursor: followings[followings.length - 1].id + }; + } else { + return { + users: followings.map(following => following.follower), + more: false + }; + } }), }; }, diff --git a/src/client/app/common/views/pages/following.vue b/src/client/app/common/views/pages/following.vue index 39739fa3da..b65d335314 100644 --- a/src/client/app/common/views/pages/following.vue +++ b/src/client/app/common/views/pages/following.vue @@ -7,19 +7,32 @@ <script lang="ts"> import Vue from 'vue'; import parseAcct from '../../../../../misc/acct/parse'; +import i18n from '../../../i18n'; + +const fetchLimit = 30; export default Vue.extend({ + i18n: i18n(), + data() { return { makePromise: cursor => this.$root.api('users/following', { ...parseAcct(this.$route.params.user), - limit: 30, - cursor: cursor ? cursor : undefined - }).then(x => { - return { - users: x.users, - cursor: x.next - }; + limit: fetchLimit + 1, + untilId: cursor ? cursor : undefined, + }).then(followings => { + if (followings.length == fetchLimit + 1) { + followings.pop(); + return { + users: followings.map(following => following.followee), + cursor: followings[followings.length - 1].id + }; + } else { + return { + users: followings.map(following => following.followee), + more: false + }; + } }), }; }, diff --git a/src/client/app/common/views/pages/share.vue b/src/client/app/common/views/pages/share.vue index 760350b921..0452b25dfc 100644 --- a/src/client/app/common/views/pages/share.vue +++ b/src/client/app/common/views/pages/share.vue @@ -42,7 +42,7 @@ export default Vue.extend({ }, mounted() { this.$root.getMeta().then(meta => { - this.name = meta.name; + this.name = meta.name || 'Misskey'; }); } }); diff --git a/src/client/app/common/views/widgets/post-form.vue b/src/client/app/common/views/widgets/post-form.vue index f1826cc59f..b30168b879 100644 --- a/src/client/app/common/views/widgets/post-form.vue +++ b/src/client/app/common/views/widgets/post-form.vue @@ -21,14 +21,7 @@ <fa :icon="['far', 'laugh']"/> </button> </div> - <div class="files" v-show="files.length != 0"> - <x-draggable :list="files" :options="{ animation: 150 }"> - <div v-for="file in files" :key="file.id"> - <div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div> - <img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/> - </div> - </x-draggable> - </div> + <x-post-form-attaches class="files" :files="files" :detachMediaFn="detachMedia"/> <input ref="file" type="file" multiple="multiple" tabindex="-1" @change="onChangeFile"/> <mk-uploader ref="uploader" @uploaded="attachMedia"/> <footer> @@ -45,7 +38,7 @@ import define from '../../../common/define-widget'; import i18n from '../../../i18n'; import insertTextAtCursor from 'insert-text-at-cursor'; -import * as XDraggable from 'vuedraggable'; +import XPostFormAttaches from '../components/post-form-attaches.vue'; export default define({ name: 'post-form', @@ -56,7 +49,7 @@ export default define({ i18n: i18n('desktop/views/widgets/post-form.vue'), components: { - XDraggable + XPostFormAttaches }, data() { @@ -249,38 +242,6 @@ export default define({ & + .emoji opacity 0.7 - > .files - > div - padding 4px - - &:after - content "" - display block - clear both - - > div - float left - border solid 4px transparent - cursor move - - &:hover > .remove - display block - - > .img - width 64px - height 64px - background-size cover - background-position center center - - > .remove - display none - position absolute - top -6px - right -6px - width 16px - height 16px - cursor pointer - > input[type=file] display none diff --git a/src/client/app/common/views/widgets/server.info.vue b/src/client/app/common/views/widgets/server.info.vue index f7efb6fa2a..a97b4ec496 100644 --- a/src/client/app/common/views/widgets/server.info.vue +++ b/src/client/app/common/views/widgets/server.info.vue @@ -1,6 +1,6 @@ <template> <div class="info"> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> + <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> <p>Machine: {{ meta.machine }}</p> <p>Node: {{ meta.node }}</p> <p>Version: {{ meta.version }} </p> diff --git a/src/client/app/desktop/views/components/drive.file.vue b/src/client/app/desktop/views/components/drive.file.vue index c560e6d97e..46aae9ad2b 100644 --- a/src/client/app/desktop/views/components/drive.file.vue +++ b/src/client/app/desktop/views/components/drive.file.vue @@ -60,7 +60,7 @@ export default Vue.extend({ return this.browser.selectedFiles.some(f => f.id == this.file.id); }, title(): string { - return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.datasize)}`; + return `${this.file.name}\n${this.file.type} ${Vue.filter('bytes')(this.file.size)}`; } }, methods: { @@ -139,10 +139,10 @@ export default Vue.extend({ }, onThumbnailLoaded() { - if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) { + if (this.file.properties.avgColor) { anime({ targets: this.$refs.thumbnail, - backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`, + backgroundColor: this.file.properties.avgColor.replace('255)', '0)'), duration: 100, easing: 'linear' }); diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue index 7513f002a7..fcabb4b8eb 100644 --- a/src/client/app/desktop/views/components/drive.vue +++ b/src/client/app/desktop/views/components/drive.vue @@ -769,7 +769,6 @@ export default Vue.extend({ > .mk-uploader height 100px padding 16px - background #fff > input display none diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue index 9b36716e83..585294fc89 100644 --- a/src/client/app/desktop/views/components/note.vue +++ b/src/client/app/desktop/views/components/note.vue @@ -54,11 +54,11 @@ </button> <button v-if="!isMyNote && appearNote.myReaction == null" class="reactionButton button" @click="react()" ref="reactButton" :title="$t('add-reaction')"> <fa icon="plus"/> - <p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p> + <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> </button> <button v-if="!isMyNote && appearNote.myReaction != null" class="reactionButton reacted button" @click="undoReact(appearNote)" ref="reactButton" :title="$t('undo-reaction')"> <fa icon="minus"/> - <p class="count" v-if="Object.values(appearNote.reactionCounts).some(x => x)">{{ Object.values(appearNote.reactionCounts).reduce((a, c) => a + c, 0) }}</p> + <p class="count" v-if="Object.values(appearNote.reactions).some(x => x)">{{ Object.values(appearNote.reactions).reduce((a, c) => a + c, 0) }}</p> </button> <button @click="menu()" ref="menuButton" class="button"> <fa icon="ellipsis-h"/> diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index e4df8a4b55..b2c2d9e861 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -25,8 +25,8 @@ </template> </component> - <footer v-if="cursor != null"> - <button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <footer v-if="more"> + <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> </button> @@ -58,7 +58,7 @@ export default Vue.extend({ fetching: true, moreFetching: false, inited: false, - cursor: null + more: false }; }, @@ -112,7 +112,7 @@ export default Vue.extend({ this.notes = x; } else { this.notes = x.notes; - this.cursor = x.cursor; + this.more = x.more; } this.inited = true; this.fetching = false; @@ -122,12 +122,12 @@ export default Vue.extend({ }); }, - more() { - if (this.cursor == null || this.moreFetching) return; + fetchMore() { + if (!this.more || this.moreFetching) return; this.moreFetching = true; - this.makePromise(this.cursor).then(x => { + this.makePromise(this.notes[this.notes.length - 1].id).then(x => { this.notes = this.notes.concat(x.notes); - this.cursor = x.cursor; + this.more = x.more; this.moreFetching = false; }, e => { this.moreFetching = false; @@ -157,7 +157,7 @@ export default Vue.extend({ // オーバーフローしたら古い投稿は捨てる if (this.notes.length >= displayLimit) { this.notes = this.notes.slice(0, displayLimit); - this.cursor = this.notes[this.notes.length - 1].id + this.more = true; } } else { this.queue.push(note); @@ -183,7 +183,7 @@ export default Vue.extend({ if (this.$store.state.settings.fetchOnScroll !== false) { const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.more(); + if (current > document.body.offsetHeight - 8) this.fetchMore(); } } } diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index 24b6fc3eba..0bf0132926 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -110,7 +110,7 @@ </div> </template> - <template v-if="notification.type == 'poll_vote'"> + <template v-if="notification.type == 'pollVote'"> <mk-avatar class="avatar" :user="notification.user"/> <div class="text"> <p><fa icon="chart-pie"/><a :href="notification.user | userPage" v-user-preview="notification.user.id"> diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index 6ba4d47087..fe39a17e67 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -27,15 +27,7 @@ <button class="emoji" @click="emoji" ref="emoji"> <fa :icon="['far', 'laugh']"/> </button> - <div class="files" :class="{ with: poll }" v-show="files.length != 0"> - <x-draggable :list="files" :options="{ animation: 150 }"> - <div v-for="file in files" :key="file.id"> - <div class="img" :style="{ backgroundImage: `url(${file.thumbnailUrl})` }" :title="file.name"></div> - <img class="remove" @click="detachMedia(file.id)" src="/assets/desktop/remove.png" :title="$t('attach-cancel')" alt=""/> - </div> - </x-draggable> - <p class="remain">{{ 4 - files.length }}/4</p> - </div> + <x-post-form-attaches class="files" :class="{ with: poll }" :files="files"/> <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> </div> </div> @@ -65,7 +57,6 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import insertTextAtCursor from 'insert-text-at-cursor'; -import * as XDraggable from 'vuedraggable'; import getFace from '../../../common/scripts/get-face'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import { parse } from '../../../../../mfm/parse'; @@ -74,13 +65,14 @@ import { erase, unique } from '../../../../../prelude/array'; import { length } from 'stringz'; import { toASCII } from 'punycode'; import extractMentions from '../../../../../misc/extract-mentions'; +import XPostFormAttaches from '../../../common/views/components/post-form-attaches.vue'; export default Vue.extend({ i18n: i18n('desktop/views/components/post-form.vue'), components: { - XDraggable, - MkVisibilityChooser + MkVisibilityChooser, + XPostFormAttaches }, props: { @@ -513,7 +505,7 @@ export default Vue.extend({ kao() { this.text += getFace(); - } + }, } }); </script> @@ -618,46 +610,6 @@ export default Vue.extend({ border-bottom solid 1px var(--primaryAlpha01) !important border-radius 0 - > .remain - display block - position absolute - top 8px - right 8px - margin 0 - padding 0 - color var(--primaryAlpha04) - - > div - padding 4px - - &:after - content "" - display block - clear both - - > div - float left - border solid 4px transparent - cursor move - - &:hover > .remove - display block - - > .img - width 64px - height 64px - background-size cover - background-position center center - - > .remove - display none - position absolute - top -6px - right -6px - width 16px - height 16px - cursor pointer - > .mk-poll-editor background var(--desktopPostFormTextareaBg) border solid 1px var(--primaryAlpha01) diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue index 6fc75d73fc..0cbe5ff05a 100644 --- a/src/client/app/desktop/views/components/user-list-timeline.vue +++ b/src/client/app/desktop/views/components/user-list-timeline.vue @@ -30,12 +30,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/desktop/views/components/user-list-window.vue b/src/client/app/desktop/views/components/user-list-window.vue index afece9fe86..6764579b20 100644 --- a/src/client/app/desktop/views/components/user-list-window.vue +++ b/src/client/app/desktop/views/components/user-list-window.vue @@ -1,6 +1,6 @@ <template> <mk-window ref="window" width="450px" height="500px" @closed="destroyDom"> - <template #header><fa icon="list"/> {{ list.title }}</template> + <template #header><fa icon="list"/> {{ list.name }}</template> <x-editor :list="list"/> </mk-window> diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue index 4f0af4a278..7afcd6aa3b 100644 --- a/src/client/app/desktop/views/components/user-lists-window.vue +++ b/src/client/app/desktop/views/components/user-lists-window.vue @@ -4,7 +4,7 @@ <div class="xkxvokkjlptzyewouewmceqcxhpgzprp"> <button class="ui" @click="add">{{ $t('create-list') }}</button> - <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a> + <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.name }}</a> </div> </mk-window> </template> diff --git a/src/client/app/desktop/views/home/favorites.vue b/src/client/app/desktop/views/home/favorites.vue index 4a4fc9ad8f..951de97498 100644 --- a/src/client/app/desktop/views/home/favorites.vue +++ b/src/client/app/desktop/views/home/favorites.vue @@ -6,7 +6,7 @@ </template> </sequential-entrance> <div class="more" v-if="existMore"> - <ui-button inline @click="more">{{ $t('@.load-more') }}</ui-button> + <ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> </div> </div> </template> @@ -48,7 +48,7 @@ export default Vue.extend({ Progress.done(); }); }, - more() { + fetchMore() { this.moreFetching = true; this.$root.api('i/favorites', { limit: 11, diff --git a/src/client/app/desktop/views/home/home.vue b/src/client/app/desktop/views/home/home.vue index fb7af5a9ad..d0b2fc10bc 100644 --- a/src/client/app/desktop/views/home/home.vue +++ b/src/client/app/desktop/views/home/home.vue @@ -101,7 +101,7 @@ export default Vue.extend({ computed: { home(): any[] { if (this.$store.getters.isSignedIn) { - return this.$store.state.settings.home || []; + return this.$store.state.device.home || []; } else { return [{ name: 'instance', @@ -182,12 +182,8 @@ export default Vue.extend({ } //#endregion - if (this.$store.state.settings.home == null) { - this.$root.api('i/update_home', { - home: _defaultDesktopHomeWidgets - }).then(() => { - this.$store.commit('settings/setHome', _defaultDesktopHomeWidgets); - }); + if (this.$store.state.device.home == null) { + this.$store.commit('device/setHome', _defaultDesktopHomeWidgets); } } }, @@ -226,7 +222,7 @@ export default Vue.extend({ }, addWidget() { - this.$store.dispatch('settings/addHomeWidget', { + this.$store.commit('device/addHomeWidget', { name: this.widgetAdderSelected, id: uuid(), place: 'left', @@ -237,12 +233,9 @@ export default Vue.extend({ saveHome() { const left = this.widgets.left; const right = this.widgets.right; - this.$store.commit('settings/setHome', left.concat(right)); + this.$store.commit('device/setHome', left.concat(right)); for (const w of left) w.place = 'left'; for (const w of right) w.place = 'right'; - this.$root.api('i/update_home', { - home: this.home - }); }, done() { diff --git a/src/client/app/desktop/views/home/search.vue b/src/client/app/desktop/views/home/search.vue index 1632e730d3..84153d18c4 100644 --- a/src/client/app/desktop/views/home/search.vue +++ b/src/client/app/desktop/views/home/search.vue @@ -35,7 +35,7 @@ export default Vue.extend({ } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/desktop/views/home/tag.vue b/src/client/app/desktop/views/home/tag.vue index 4f9bc66e7b..92f5a47528 100644 --- a/src/client/app/desktop/views/home/tag.vue +++ b/src/client/app/desktop/views/home/tag.vue @@ -21,7 +21,7 @@ export default Vue.extend({ i18n: i18n('desktop/views/pages/tag.vue'), data() { return { - makePromise: cursor => this.$root.api('notes/search_by_tag', { + makePromise: cursor => this.$root.api('notes/search-by-tag', { limit: limit + 1, offset: cursor ? cursor : undefined, tag: this.$route.params.tag @@ -35,7 +35,7 @@ export default Vue.extend({ } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/desktop/views/home/timeline.core.vue b/src/client/app/desktop/views/home/timeline.core.vue index e306ac873c..12806365d4 100644 --- a/src/client/app/desktop/views/home/timeline.core.vue +++ b/src/client/app/desktop/views/home/timeline.core.vue @@ -58,7 +58,7 @@ export default Vue.extend({ }; if (this.src == 'tag') { - this.endpoint = 'notes/search_by_tag'; + this.endpoint = 'notes/search-by-tag'; this.query = { query: this.tagTl.query }; @@ -113,12 +113,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }); diff --git a/src/client/app/desktop/views/home/timeline.vue b/src/client/app/desktop/views/home/timeline.vue index 0b8ced4795..f257f1fa97 100644 --- a/src/client/app/desktop/views/home/timeline.vue +++ b/src/client/app/desktop/views/home/timeline.vue @@ -9,7 +9,7 @@ <span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span> <span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span> <span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span> - <span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.title }}</span> + <span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</span> <div class="buttons"> <button :data-active="src == 'mentions'" @click="src = 'mentions'" :title="$t('mentions')"><fa icon="at"/><i class="indicator" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></button> <button :data-active="src == 'messages'" @click="src = 'messages'" :title="$t('messages')"><fa :icon="['far', 'envelope']"/><i class="indicator" v-if="$store.state.i.hasUnreadSpecifiedNotes"><fa icon="circle"/></i></button> @@ -143,7 +143,7 @@ export default Vue.extend({ menu = menu.concat(lists.map(list => ({ icon: 'list', - text: list.title, + text: list.name, action: () => { this.list = list; this.src = 'list'; diff --git a/src/client/app/desktop/views/home/user/index.vue b/src/client/app/desktop/views/home/user/index.vue index 17a34a30cc..338cd1c59d 100644 --- a/src/client/app/desktop/views/home/user/index.vue +++ b/src/client/app/desktop/views/home/user/index.vue @@ -4,7 +4,7 @@ <fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }} </div> <div class="is-remote" v-if="user.host != null" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> - <fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a> + <fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" target="_blank">{{ $t('@.view-on-remote') }}</a> </div> <div class="main"> <x-header class="header" :user="user"/> diff --git a/src/client/app/desktop/views/home/user/user.header.vue b/src/client/app/desktop/views/home/user/user.header.vue index 85dcd3ddae..e21757ccf9 100644 --- a/src/client/app/desktop/views/home/user/user.header.vue +++ b/src/client/app/desktop/views/home/user/user.header.vue @@ -36,8 +36,8 @@ </dl> </div> <div class="info"> - <span class="location" v-if="user.host === null && user.profile.location"><fa icon="map-marker"/> {{ user.profile.location }}</span> - <span class="birthday" v-if="user.host === null && user.profile.birthday"><fa icon="birthday-cake"/> {{ user.profile.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> + <span class="location" v-if="user.host === null && user.location"><fa icon="map-marker"/> {{ user.location }}</span> + <span class="birthday" v-if="user.host === null && user.birthday"><fa icon="birthday-cake"/> {{ user.birthday.replace('-', $t('year')).replace('-', $t('month')) + $t('day') }} ({{ $t('years-old', { age }) }})</span> </div> <div class="status"> <router-link :to="user | userPage()" class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</router-link> @@ -65,13 +65,13 @@ export default Vue.extend({ style(): any { if (this.user.bannerUrl == null) return {}; return { - backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundColor: this.user.bannerColor, backgroundImage: `url(${ this.user.bannerUrl })` }; }, age(): number { - return age(this.user.profile.birthday); + return age(this.user.birthday); } }, mounted() { diff --git a/src/client/app/desktop/views/home/user/user.timeline.vue b/src/client/app/desktop/views/home/user/user.timeline.vue index 4bdf4b6cdc..eafac4c7fd 100644 --- a/src/client/app/desktop/views/home/user/user.timeline.vue +++ b/src/client/app/desktop/views/home/user/user.timeline.vue @@ -42,12 +42,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: new Date(notes[notes.length - 1].createdAt).getTime() + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue index ddffeae408..5a5cd9c8e6 100644 --- a/src/client/app/desktop/views/pages/welcome.vue +++ b/src/client/app/desktop/views/pages/welcome.vue @@ -13,8 +13,8 @@ <div class="body"> <div class="main block"> <div> - <h1 v-if="name != 'Misskey'">{{ name }}</h1> - <h1 v-else><img svg-inline src="../../../../assets/title.svg" :alt="name"></h1> + <h1 v-if="name != null">{{ name }}</h1> + <h1 v-else><img svg-inline src="../../../../assets/title.svg" alt="Misskey"></h1> <div class="info"> <span><b>{{ host }}</b> - <span v-html="$t('powered-by-misskey')"></span></span> @@ -87,7 +87,7 @@ <div> <div v-if="meta" class="body"> <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> + <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> </div> </div> </div> @@ -162,7 +162,7 @@ export default Vue.extend({ banner: null, copyright, host: toUnicode(host), - name: 'Misskey', + name: null, description: '', announcements: [], photos: [] diff --git a/src/client/app/dev/views/new-app.vue b/src/client/app/dev/views/new-app.vue index d8c128904a..00f2ed60d9 100644 --- a/src/client/app/dev/views/new-app.vue +++ b/src/client/app/dev/views/new-app.vue @@ -15,15 +15,21 @@ <b-form-group :description="$t('description')"> <b-alert show variant="warning"><fa icon="exclamation-triangle"/> {{ $t('authority-warning') }}</b-alert> <b-form-checkbox-group v-model="permission" stacked> - <b-form-checkbox value="account-read">{{ $t('account-read') }}</b-form-checkbox> - <b-form-checkbox value="account-write">{{ $t('account-write') }}</b-form-checkbox> - <b-form-checkbox value="note-write">{{ $t('note-write') }}</b-form-checkbox> - <b-form-checkbox value="reaction-write">{{ $t('reaction-write') }}</b-form-checkbox> - <b-form-checkbox value="following-write">{{ $t('following-write') }}</b-form-checkbox> - <b-form-checkbox value="drive-read">{{ $t('drive-read') }}</b-form-checkbox> - <b-form-checkbox value="drive-write">{{ $t('drive-write') }}</b-form-checkbox> - <b-form-checkbox value="notification-read">{{ $t('notification-read') }}</b-form-checkbox> - <b-form-checkbox value="notification-write">{{ $t('notification-write') }}</b-form-checkbox> + <b-form-checkbox value="read:account">{{ $t('read:account') }}</b-form-checkbox> + <b-form-checkbox value="write:account">{{ $t('write:account') }}</b-form-checkbox> + <b-form-checkbox value="write:notes">{{ $t('write:notes') }}</b-form-checkbox> + <b-form-checkbox value="read:reactions">{{ $t('read:reactions') }}</b-form-checkbox> + <b-form-checkbox value="write:reactions">{{ $t('write:reactions') }}</b-form-checkbox> + <b-form-checkbox value="read:following">{{ $t('read:following') }}</b-form-checkbox> + <b-form-checkbox value="write:following">{{ $t('write:following') }}</b-form-checkbox> + <b-form-checkbox value="read:mutes">{{ $t('read:mutes') }}</b-form-checkbox> + <b-form-checkbox value="write:mutes">{{ $t('write:mutes') }}</b-form-checkbox> + <b-form-checkbox value="read:blocks">{{ $t('read:blocks') }}</b-form-checkbox> + <b-form-checkbox value="write:blocks">{{ $t('write:blocks') }}</b-form-checkbox> + <b-form-checkbox value="read:drive">{{ $t('read:drive') }}</b-form-checkbox> + <b-form-checkbox value="write:drive">{{ $t('write:drive') }}</b-form-checkbox> + <b-form-checkbox value="read:notifications">{{ $t('read:notifications') }}</b-form-checkbox> + <b-form-checkbox value="write:notifications">{{ $t('write:notifications') }}</b-form-checkbox> </b-form-checkbox-group> </b-form-group> </b-card> diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts index 9e191bf43c..a3525ed319 100644 --- a/src/client/app/mios.ts +++ b/src/client/app/mios.ts @@ -28,7 +28,7 @@ export default class MiOS extends EventEmitter { }; public get instanceName() { - return this.meta ? this.meta.data.name : 'Misskey'; + return this.meta ? (this.meta.data.name || 'Misskey') : 'Misskey'; } private isMetaFetching = false; @@ -278,21 +278,6 @@ export default class MiOS extends EventEmitter { }); }); - main.on('homeUpdated', x => { - this.store.commit('settings/setHome', x); - }); - - main.on('mobileHomeUpdated', x => { - this.store.commit('settings/setMobileHome', x); - }); - - main.on('widgetUpdated', x => { - this.store.commit('settings/updateWidget', { - id: x.id, - data: x.data - }); - }); - // トークンが再生成されたとき // このままではMisskeyが利用できないので強制的にサインアウトさせる main.on('myTokenRegenerated', () => { diff --git a/src/client/app/mobile/views/components/drive.file-detail.vue b/src/client/app/mobile/views/components/drive.file-detail.vue index 92f5c1fd19..98124354ed 100644 --- a/src/client/app/mobile/views/components/drive.file-detail.vue +++ b/src/client/app/mobile/views/components/drive.file-detail.vue @@ -22,7 +22,7 @@ <div> <span class="type"><mk-file-type-icon :type="file.type"/> {{ file.type }}</span> <span class="separator"></span> - <span class="data-size">{{ file.datasize | bytes }}</span> + <span class="data-size">{{ file.size | bytes }}</span> <span class="separator"></span> <span class="created-at" @click="showCreatedAt"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> <template v-if="file.isSensitive"> @@ -85,8 +85,8 @@ export default Vue.extend({ }, style(): any { - return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? { - 'background-color': `rgb(${ this.file.properties.avgColor.join(',') })` + return this.file.properties.avgColor ? { + 'background-color': this.file.properties.avgColor } : {}; }, diff --git a/src/client/app/mobile/views/components/drive.file.vue b/src/client/app/mobile/views/components/drive.file.vue index feca266ede..ed95537f9c 100644 --- a/src/client/app/mobile/views/components/drive.file.vue +++ b/src/client/app/mobile/views/components/drive.file.vue @@ -10,7 +10,7 @@ <footer> <span class="type"><mk-file-type-icon :type="file.type"/>{{ file.type }}</span> <span class="separator"></span> - <span class="data-size">{{ file.datasize | bytes }}</span> + <span class="data-size">{{ file.size | bytes }}</span> <span class="separator"></span> <span class="created-at"><fa :icon="['far', 'clock']"/><mk-time :time="file.createdAt"/></span> <template v-if="file.isSensitive"> diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 16a1682c2a..5b9652e1e2 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -21,8 +21,8 @@ </template> </component> - <footer v-if="cursor != null"> - <button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <footer v-if="more"> + <button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> <template v-if="!moreFetching">{{ $t('@.load-more') }}</template> <template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template> </button> @@ -53,7 +53,7 @@ export default Vue.extend({ fetching: true, moreFetching: false, inited: false, - cursor: null + more: false }; }, @@ -113,7 +113,7 @@ export default Vue.extend({ this.notes = x; } else { this.notes = x.notes; - this.cursor = x.cursor; + this.more = x.more; } this.inited = true; this.fetching = false; @@ -123,12 +123,12 @@ export default Vue.extend({ }); }, - more() { - if (this.cursor == null || this.moreFetching) return; + fetchMore() { + if (!this.more || this.moreFetching) return; this.moreFetching = true; - this.makePromise(this.cursor).then(x => { + this.makePromise(this.notes[this.notes.length - 1].id).then(x => { this.notes = this.notes.concat(x.notes); - this.cursor = x.cursor; + this.more = x.more; this.moreFetching = false; }, e => { this.moreFetching = false; @@ -151,7 +151,7 @@ export default Vue.extend({ // オーバーフローしたら古い投稿は捨てる if (this.notes.length >= displayLimit) { this.notes = this.notes.slice(0, displayLimit); - this.cursor = this.notes[this.notes.length - 1].id + this.more = true; } } else { this.queue.push(note); @@ -182,7 +182,7 @@ export default Vue.extend({ if (this.$el.offsetHeight == 0) return; const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.more(); + if (current > document.body.offsetHeight - 8) this.fetchMore(); } } } diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue index 1b8eceaa6c..8422c73420 100644 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ b/src/client/app/mobile/views/components/notification-preview.vue @@ -54,7 +54,7 @@ </div> </template> - <template v-if="notification.type == 'poll_vote'"> + <template v-if="notification.type == 'pollVote'"> <mk-avatar class="avatar" :user="notification.user"/> <div class="text"> <p><fa icon="chart-pie"/><mk-user-name :user="notification.user"/></p> diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index 5308d96533..1128a76000 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -54,7 +54,7 @@ </div> </div> - <div class="notification poll_vote" v-if="notification.type == 'poll_vote'"> + <div class="notification pollVote" v-if="notification.type == 'pollVote'"> <mk-avatar class="avatar" :user="notification.user"/> <div> <header> diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 9c1072b4a3..6b26cdf890 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -21,13 +21,7 @@ </div> <input v-show="useCw" ref="cw" v-model="cw" :placeholder="$t('annotations')" v-autocomplete="{ model: 'cw' }"> <textarea v-model="text" ref="text" :disabled="posting" :placeholder="placeholder" v-autocomplete="{ model: 'text' }"></textarea> - <div class="attaches" v-show="files.length != 0"> - <x-draggable class="files" :list="files" :options="{ animation: 150 }"> - <div class="file" v-for="file in files" :key="file.id"> - <div class="img" :style="`background-image: url(${file.thumbnailUrl})`" @click="detachMedia(file)"></div> - </div> - </x-draggable> - </div> + <x-post-form-attaches class="attaches" :files="files"/> <mk-poll-editor v-if="poll" ref="poll" @destroyed="poll = false" @updated="onPollUpdate()"/> <mk-uploader ref="uploader" @uploaded="attachMedia" @change="onChangeUploadings"/> <footer> @@ -57,7 +51,6 @@ import Vue from 'vue'; import i18n from '../../../i18n'; import insertTextAtCursor from 'insert-text-at-cursor'; -import * as XDraggable from 'vuedraggable'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import getFace from '../../../common/scripts/get-face'; import { parse } from '../../../../../mfm/parse'; @@ -66,11 +59,12 @@ import { erase, unique } from '../../../../../prelude/array'; import { length } from 'stringz'; import { toASCII } from 'punycode'; import extractMentions from '../../../../../misc/extract-mentions'; +import XPostFormAttaches from '../../../common/views/components/post-form-attaches.vue'; export default Vue.extend({ i18n: i18n('mobile/views/components/post-form.vue'), components: { - XDraggable + XPostFormAttaches }, props: { @@ -264,8 +258,8 @@ export default Vue.extend({ this.$emit('change-attached-files', this.files); }, - detachMedia(file) { - this.files = this.files.filter(x => x.id != file.id); + detachMedia(id) { + this.files = this.files.filter(x => x.id != id); this.$emit('change-attached-files', this.files); }, @@ -481,32 +475,6 @@ export default Vue.extend({ min-width 100% min-height 80px - > .attaches - - > .files - display block - margin 0 - padding 4px - list-style none - - &:after - content "" - display block - clear both - - > .file - display block - float left - margin 0 - padding 0 - border solid 4px transparent - - > .img - width 64px - height 64px - background-size cover - background-position center center - > .mk-uploader margin 8px 0 0 0 padding 8px diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue index e67d7931f7..c3f09c9b15 100644 --- a/src/client/app/mobile/views/components/user-list-timeline.vue +++ b/src/client/app/mobile/views/components/user-list-timeline.vue @@ -27,12 +27,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue index 3ba4011c6c..8ae9794b75 100644 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ b/src/client/app/mobile/views/components/user-timeline.vue @@ -27,12 +27,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: new Date(notes[notes.length - 1].createdAt).getTime() + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/mobile/views/pages/favorites.vue b/src/client/app/mobile/views/pages/favorites.vue index 196bd4b93c..6f175a0053 100644 --- a/src/client/app/mobile/views/pages/favorites.vue +++ b/src/client/app/mobile/views/pages/favorites.vue @@ -8,7 +8,7 @@ <mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/> </template> </sequential-entrance> - <ui-button v-if="existMore" @click="more">{{ $t('@.load-more') }}</ui-button> + <ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button> </main> </mk-ui> </template> @@ -53,7 +53,7 @@ export default Vue.extend({ Progress.done(); }); }, - more() { + fetchMore() { this.moreFetching = true; this.$root.api('i/favorites', { limit: 11, diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index 4f9f5119ab..809158dd29 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -59,7 +59,7 @@ export default Vue.extend({ }; if (this.src == 'tag') { - this.endpoint = 'notes/search_by_tag'; + this.endpoint = 'notes/search-by-tag'; this.query = { query: this.tagTl.query }; @@ -114,12 +114,12 @@ export default Vue.extend({ notes.pop(); return { notes: notes, - cursor: notes[notes.length - 1].id + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }); diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 59fae2340b..cf15670f3f 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -9,7 +9,7 @@ <span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span> <span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span> <span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span> - <span v-if="src == 'list'"><fa icon="list"/>{{ list.title }}</span> + <span v-if="src == 'list'"><fa icon="list"/>{{ list.name }}</span> <span v-if="src == 'tag'"><fa icon="hashtag"/>{{ tagTl.title }}</span> </span> <span style="margin-left:8px"> diff --git a/src/client/app/mobile/views/pages/search.vue b/src/client/app/mobile/views/pages/search.vue index 9e4be82041..f4b2512809 100644 --- a/src/client/app/mobile/views/pages/search.vue +++ b/src/client/app/mobile/views/pages/search.vue @@ -21,19 +21,19 @@ export default Vue.extend({ return { makePromise: cursor => this.$root.api('notes/search', { limit: limit + 1, - offset: cursor ? cursor : undefined, + untilId: cursor ? cursor : undefined, query: this.q }).then(notes => { if (notes.length == limit + 1) { notes.pop(); return { notes: notes, - cursor: cursor ? cursor + limit : limit + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/mobile/views/pages/tag.vue b/src/client/app/mobile/views/pages/tag.vue index 318e63a473..f41cf1f18c 100644 --- a/src/client/app/mobile/views/pages/tag.vue +++ b/src/client/app/mobile/views/pages/tag.vue @@ -19,21 +19,21 @@ export default Vue.extend({ i18n: i18n('mobile/views/pages/tag.vue'), data() { return { - makePromise: cursor => this.$root.api('notes/search_by_tag', { + makePromise: cursor => this.$root.api('notes/search-by-tag', { limit: limit + 1, - offset: cursor ? cursor : undefined, + untilId: cursor ? cursor : undefined, tag: this.$route.params.tag }).then(notes => { if (notes.length == limit + 1) { notes.pop(); return { notes: notes, - cursor: cursor ? cursor + limit : limit + more: true }; } else { return { notes: notes, - cursor: null + more: false }; } }) diff --git a/src/client/app/mobile/views/pages/user-list.vue b/src/client/app/mobile/views/pages/user-list.vue index 874bae5d18..68fd0358c4 100644 --- a/src/client/app/mobile/views/pages/user-list.vue +++ b/src/client/app/mobile/views/pages/user-list.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <template #header v-if="!fetching"><fa icon="list"/>{{ list.title }}</template> + <template #header v-if="!fetching"><fa icon="list"/>{{ list.name }}</template> <main v-if="!fetching"> <x-editor :list="list"/> diff --git a/src/client/app/mobile/views/pages/user-lists.vue b/src/client/app/mobile/views/pages/user-lists.vue index fd129339fd..49006f41f6 100644 --- a/src/client/app/mobile/views/pages/user-lists.vue +++ b/src/client/app/mobile/views/pages/user-lists.vue @@ -5,7 +5,7 @@ <main> <ul> - <li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.title }}</router-link></li> + <li v-for="list in lists" :key="list.id"><router-link :to="`/i/lists/${list.id}`">{{ list.name }}</router-link></li> </ul> </main> </mk-ui> diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue index 9c36f1295b..c4f47514d8 100644 --- a/src/client/app/mobile/views/pages/user/home.photos.vue +++ b/src/client/app/mobile/views/pages/user/home.photos.vue @@ -4,7 +4,7 @@ <div class="stream" v-if="!fetching && images.length > 0"> <a v-for="(image, i) in images" :key="i" class="img" - :style="`background-image: url(${thumbnail(image.media)})`" + :style="`background-image: url(${thumbnail(image.file)})`" :href="image.note | notePage" ></a> </div> @@ -40,11 +40,11 @@ export default Vue.extend({ untilDate: new Date().getTime() + 1000 * 86400 * 365 }).then(notes => { for (const note of notes) { - for (const media of note.media) { + for (const file of note.files) { if (this.images.length < 9) { this.images.push({ note, - media + file }); } } diff --git a/src/client/app/mobile/views/pages/user/index.vue b/src/client/app/mobile/views/pages/user/index.vue index fe5ef057e7..cc53ae0cfe 100644 --- a/src/client/app/mobile/views/pages/user/index.vue +++ b/src/client/app/mobile/views/pages/user/index.vue @@ -5,7 +5,7 @@ </template> <div class="wwtwuxyh" v-if="!fetching"> <div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</p></div> - <div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div> + <div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div> <header> <div class="banner" :style="style"></div> <div class="body"> @@ -36,11 +36,11 @@ </dl> </div> <div class="info"> - <p class="location" v-if="user.host === null && user.profile.location"> - <fa icon="map-marker"/>{{ user.profile.location }} + <p class="location" v-if="user.host === null && user.location"> + <fa icon="map-marker"/>{{ user.location }} </p> - <p class="birthday" v-if="user.host === null && user.profile.birthday"> - <fa icon="birthday-cake"/>{{ user.profile.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }}) + <p class="birthday" v-if="user.host === null && user.birthday"> + <fa icon="birthday-cake"/>{{ user.birthday.replace('-', '年').replace('-', '月') + '日' }} ({{ $t('years-old', { age }) }}) </p> </div> <div class="status"> @@ -104,7 +104,7 @@ export default Vue.extend({ }, computed: { age(): number { - return age(this.user.profile.birthday); + return age(this.user.birthday); }, avator(): string { return this.$store.state.device.disableShowingAnimatedImages @@ -114,7 +114,7 @@ export default Vue.extend({ style(): any { if (this.user.bannerUrl == null) return {}; return { - backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null, + backgroundColor: this.user.bannerColor, backgroundImage: `url(${ this.user.bannerUrl })` }; } diff --git a/src/client/app/mobile/views/pages/welcome.vue b/src/client/app/mobile/views/pages/welcome.vue index 1a2b0b6c12..dd71a918db 100644 --- a/src/client/app/mobile/views/pages/welcome.vue +++ b/src/client/app/mobile/views/pages/welcome.vue @@ -3,10 +3,10 @@ <div class="banner" :style="{ backgroundImage: banner ? `url(${banner})` : null }"></div> <div> - <img svg-inline src="../../../../assets/title.svg" :alt="name"> + <img svg-inline src="../../../../assets/title.svg" alt="Misskey"> <p class="host">{{ host }}</p> <div class="about"> - <h2>{{ name }}</h2> + <h2>{{ name || 'Misskey' }}</h2> <p v-html="description || this.$t('@.about')"></p> <router-link class="signup" to="/signup">{{ $t('@.signup') }}</router-link> </div> @@ -62,7 +62,7 @@ </article> <div class="info" v-if="meta"> <p>Version: <b>{{ meta.version }}</b></p> - <p>Maintainer: <b><a :href="'mailto:' + meta.maintainer.email" target="_blank">{{ meta.maintainer.name }}</a></b></p> + <p>Maintainer: <b><a :href="'mailto:' + meta.maintainerEmail" target="_blank">{{ meta.maintainerName }}</a></b></p> </div> <footer> <small>{{ copyright }}</small> @@ -87,7 +87,7 @@ export default Vue.extend({ stats: null, banner: null, host: toUnicode(host), - name: 'Misskey', + name: null, description: '', photos: [], announcements: [] diff --git a/src/client/app/mobile/views/pages/widgets.vue b/src/client/app/mobile/views/pages/widgets.vue index 96dcb977fa..2647f96de2 100644 --- a/src/client/app/mobile/views/pages/widgets.vue +++ b/src/client/app/mobile/views/pages/widgets.vue @@ -119,7 +119,7 @@ export default Vue.extend({ }, addWidget() { - this.$store.dispatch('settings/addMobileHomeWidget', { + this.$store.commit('settings/addMobileHomeWidget', { name: this.widgetAdderSelected, id: uuid(), data: {} @@ -127,14 +127,11 @@ export default Vue.extend({ }, removeWidget(widget) { - this.$store.dispatch('settings/removeMobileHomeWidget', widget); + this.$store.commit('settings/removeMobileHomeWidget', widget); }, saveHome() { this.$store.commit('settings/setMobileHome', this.widgets); - this.$root.api('i/update_mobile_home', { - home: this.widgets - }); } } }); diff --git a/src/client/app/store.ts b/src/client/app/store.ts index e49934fc16..c82981ad24 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -7,8 +7,6 @@ import { erase } from '../../prelude/array'; import getNoteSummary from '../../misc/get-note-summary'; const defaultSettings = { - home: null, - mobileHome: [], keepCw: false, tagTimelines: [], fetchOnScroll: true, @@ -41,6 +39,8 @@ const defaultSettings = { }; const defaultDeviceSettings = { + home: null, + mobileHome: [], deck: null, deckMode: false, deckColumnAlign: 'center', @@ -120,7 +120,7 @@ export default (os: MiOS) => new Vuex.Store({ actions: { login(ctx, i) { ctx.commit('updateI', i); - ctx.dispatch('settings/merge', i.clientSettings); + ctx.dispatch('settings/merge', i.clientData); }, logout(ctx) { @@ -134,8 +134,8 @@ export default (os: MiOS) => new Vuex.Store({ ctx.commit('updateIKeyValue', { key, value }); } - if (me.clientSettings) { - ctx.dispatch('settings/merge', me.clientSettings); + if (me.clientData) { + ctx.dispatch('settings/merge', me.clientData); } }, }, @@ -162,6 +162,48 @@ export default (os: MiOS) => new Vuex.Store({ state.visibility = visibility; }, + setHome(state, data) { + state.home = data; + }, + + addHomeWidget(state, widget) { + state.home.unshift(widget); + }, + + setMobileHome(state, data) { + state.mobileHome = data; + }, + + updateWidget(state, x) { + let w; + + //#region Desktop home + if (state.home) { + w = state.home.find(w => w.id == x.id); + if (w) { + w.data = x.data; + } + } + //#endregion + + //#region Mobile home + if (state.mobileHome) { + w = state.mobileHome.find(w => w.id == x.id); + if (w) { + w.data = x.data; + } + } + //#endregion + }, + + addMobileHomeWidget(state, widget) { + state.mobileHome.unshift(widget); + }, + + removeMobileHomeWidget(state, widget) { + state.mobileHome = state.mobileHome.filter(w => w.id != widget.id); + }, + addDeckColumn(state, column) { if (column.name == undefined) column.name = null; state.deck.columns.push(column); @@ -301,48 +343,6 @@ export default (os: MiOS) => new Vuex.Store({ set(state, x: { key: string; value: any }) { nestedProperty.set(state, x.key, x.value); }, - - setHome(state, data) { - state.home = data; - }, - - addHomeWidget(state, widget) { - state.home.unshift(widget); - }, - - setMobileHome(state, data) { - state.mobileHome = data; - }, - - updateWidget(state, x) { - let w; - - //#region Desktop home - if (state.home) { - w = state.home.find(w => w.id == x.id); - if (w) { - w.data = x.data; - } - } - //#endregion - - //#region Mobile home - if (state.mobileHome) { - w = state.mobileHome.find(w => w.id == x.id); - if (w) { - w.data = x.data; - } - } - //#endregion - }, - - addMobileHomeWidget(state, widget) { - state.mobileHome.unshift(widget); - }, - - removeMobileHomeWidget(state, widget) { - state.mobileHome = state.mobileHome.filter(w => w.id != widget.id); - }, }, actions: { @@ -363,30 +363,6 @@ export default (os: MiOS) => new Vuex.Store({ }); } }, - - addHomeWidget(ctx, widget) { - ctx.commit('addHomeWidget', widget); - - os.api('i/update_home', { - home: ctx.state.home - }); - }, - - addMobileHomeWidget(ctx, widget) { - ctx.commit('addMobileHomeWidget', widget); - - os.api('i/update_mobile_home', { - home: ctx.state.mobileHome - }); - }, - - removeMobileHomeWidget(ctx, widget) { - ctx.commit('removeMobileHomeWidget', widget); - - os.api('i/update_mobile_home', { - home: ctx.state.mobileHome.filter(w => w.id != widget.id) - }); - } } } } diff --git a/src/client/themes/light.json5 b/src/client/themes/light.json5 index 3bcabddba7..d5680f8f82 100644 --- a/src/client/themes/light.json5 +++ b/src/client/themes/light.json5 @@ -7,7 +7,7 @@ kind: 'light', vars: { - primary: '#fb4e4e', + primary: '#f18570', secondary: '#fff', text: '#666', }, diff --git a/src/config/load.ts b/src/config/load.ts index 50ae47d9e2..26b25eab4e 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -25,11 +25,11 @@ export default function load() { const mixin = {} as Mixin; - const url = validateUrl(config.url); + const url = tryCreateUrl(config.url); - config.url = normalizeUrl(config.url); + config.url = url.origin; - config.port = config.port || parseInt(process.env.PORT, 10); + config.port = config.port || parseInt(process.env.PORT || '', 10); mixin.host = url.host; mixin.hostname = url.hostname; @@ -53,14 +53,3 @@ function tryCreateUrl(url: string) { throw `url="${url}" is not a valid URL.`; } } - -function validateUrl(url: string) { - const result = tryCreateUrl(url); - if (result.pathname.replace('/', '').length) throw `url="${url}" is not a valid URL, has a pathname.`; - if (!url.includes(result.host)) throw `url="${url}" is not a valid URL, has an invalid hostname.`; - return result; -} - -function normalizeUrl(url: string) { - return url.endsWith('/') ? url.substr(0, url.length - 1) : url; -} diff --git a/src/config/types.ts b/src/config/types.ts index 5f30d410c9..d1749c52f7 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -8,7 +8,7 @@ export type Source = { port: number; https?: { [x: string]: string }; disableHsts?: boolean; - mongodb: { + db: { host: string; port: number; db: string; @@ -42,6 +42,8 @@ export type Source = { accesslog?: string; clusterLimit?: number; + + id: string; }; /** diff --git a/src/daemons/notes-stats-child.ts b/src/daemons/notes-stats-child.ts index 7f54a36bff..b60f5badfd 100644 --- a/src/daemons/notes-stats-child.ts +++ b/src/daemons/notes-stats-child.ts @@ -1,26 +1,28 @@ -import Note from '../models/note'; +import { MoreThanOrEqual, getRepository } from 'typeorm'; +import { Note } from '../models/entities/note'; +import { initDb } from '../db/postgre'; const interval = 5000; -async function tick() { - const [all, local] = await Promise.all([Note.count({ - createdAt: { - $gte: new Date(Date.now() - interval) - } - }), Note.count({ - createdAt: { - $gte: new Date(Date.now() - interval) - }, - '_user.host': null - })]); +initDb().then(() => { + const Notes = getRepository(Note); - const stats = { - all, local - }; + async function tick() { + const [all, local] = await Promise.all([Notes.count({ + createdAt: MoreThanOrEqual(new Date(Date.now() - interval)) + }), Notes.count({ + createdAt: MoreThanOrEqual(new Date(Date.now() - interval)), + userHost: null + })]); - process.send(stats); -} + const stats = { + all, local + }; -tick(); + process.send!(stats); + } -setInterval(tick, interval); + tick(); + + setInterval(tick, interval); +}); diff --git a/src/daemons/server-stats.ts b/src/daemons/server-stats.ts index b82f421779..ee62c32d7e 100644 --- a/src/daemons/server-stats.ts +++ b/src/daemons/server-stats.ts @@ -56,20 +56,12 @@ function cpuUsage() { // MEMORY(excl buffer + cache) STAT async function usedMem() { - try { - const data = await sysUtils.mem(); - return data.active; - } catch (error) { - throw error; - } + const data = await sysUtils.mem(); + return data.active; } // TOTAL MEMORY STAT async function totalMem() { - try { - const data = await sysUtils.mem(); - return data.total; - } catch (error) { - throw error; - } + const data = await sysUtils.mem(); + return data.total; } diff --git a/src/db/mongodb.ts b/src/db/mongodb.ts deleted file mode 100644 index f82ced1765..0000000000 --- a/src/db/mongodb.ts +++ /dev/null @@ -1,39 +0,0 @@ -import config from '../config'; - -const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; -const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null; - -const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; - -/** - * monk - */ -import mongo from 'monk'; - -const db = mongo(uri); - -export default db; - -/** - * MongoDB native module (officialy) - */ -import * as mongodb from 'mongodb'; - -let mdb: mongodb.Db; - -const nativeDbConn = async (): Promise<mongodb.Db> => { - if (mdb) return mdb; - - const db = await ((): Promise<mongodb.Db> => new Promise((resolve, reject) => { - mongodb.MongoClient.connect(uri, { useNewUrlParser: true }, (e: Error, client: any) => { - if (e) return reject(e); - resolve(client.db(config.mongodb.db)); - }); - }))(); - - mdb = db; - - return db; -}; - -export { nativeDbConn }; diff --git a/src/db/postgre.ts b/src/db/postgre.ts new file mode 100644 index 0000000000..641a552c09 --- /dev/null +++ b/src/db/postgre.ts @@ -0,0 +1,137 @@ +import { createConnection, Logger, getConnection } from 'typeorm'; +import config from '../config'; +import { entities as charts } from '../services/chart/entities'; +import { dbLogger } from './logger'; +import * as highlight from 'cli-highlight'; + +import { Log } from '../models/entities/log'; +import { User } from '../models/entities/user'; +import { DriveFile } from '../models/entities/drive-file'; +import { DriveFolder } from '../models/entities/drive-folder'; +import { AccessToken } from '../models/entities/access-token'; +import { App } from '../models/entities/app'; +import { PollVote } from '../models/entities/poll-vote'; +import { Note } from '../models/entities/note'; +import { NoteReaction } from '../models/entities/note-reaction'; +import { NoteWatching } from '../models/entities/note-watching'; +import { NoteUnread } from '../models/entities/note-unread'; +import { Notification } from '../models/entities/notification'; +import { Meta } from '../models/entities/meta'; +import { Following } from '../models/entities/following'; +import { Instance } from '../models/entities/instance'; +import { Muting } from '../models/entities/muting'; +import { SwSubscription } from '../models/entities/sw-subscription'; +import { Blocking } from '../models/entities/blocking'; +import { UserList } from '../models/entities/user-list'; +import { UserListJoining } from '../models/entities/user-list-joining'; +import { Hashtag } from '../models/entities/hashtag'; +import { NoteFavorite } from '../models/entities/note-favorite'; +import { AbuseUserReport } from '../models/entities/abuse-user-report'; +import { RegistrationTicket } from '../models/entities/registration-tickets'; +import { MessagingMessage } from '../models/entities/messaging-message'; +import { Signin } from '../models/entities/signin'; +import { AuthSession } from '../models/entities/auth-session'; +import { FollowRequest } from '../models/entities/follow-request'; +import { Emoji } from '../models/entities/emoji'; +import { ReversiGame } from '../models/entities/games/reversi/game'; +import { ReversiMatching } from '../models/entities/games/reversi/matching'; +import { UserNotePining } from '../models/entities/user-note-pinings'; +import { Poll } from '../models/entities/poll'; +import { UserKeypair } from '../models/entities/user-keypair'; +import { UserPublickey } from '../models/entities/user-publickey'; +import { UserProfile } from '../models/entities/user-profile'; + +const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); + +class MyCustomLogger implements Logger { + private highlight(sql: string) { + return highlight.highlight(sql, { + language: 'sql', ignoreIllegals: true, + }); + } + + public logQuery(query: string, parameters?: any[]) { + sqlLogger.info(this.highlight(query)); + } + + public logQueryError(error: string, query: string, parameters?: any[]) { + sqlLogger.error(this.highlight(query)); + } + + public logQuerySlow(time: number, query: string, parameters?: any[]) { + sqlLogger.warn(this.highlight(query)); + } + + public logSchemaBuild(message: string) { + sqlLogger.info(message); + } + + public log(message: string) { + sqlLogger.info(message); + } + + public logMigration(message: string) { + sqlLogger.info(message); + } +} + +export function initDb(justBorrow = false, sync = false, log = false) { + const enableLogging = log || !['production', 'test'].includes(process.env.NODE_ENV || ''); + + try { + const conn = getConnection(); + return Promise.resolve(conn); + } catch (e) {} + + return createConnection({ + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + synchronize: process.env.NODE_ENV === 'test' || sync, + dropSchema: process.env.NODE_ENV === 'test' && !justBorrow, + logging: enableLogging, + logger: enableLogging ? new MyCustomLogger() : undefined, + entities: [ + Meta, + Instance, + App, + AuthSession, + AccessToken, + User, + UserProfile, + UserKeypair, + UserPublickey, + UserList, + UserListJoining, + UserNotePining, + Following, + FollowRequest, + Muting, + Blocking, + Note, + NoteFavorite, + NoteReaction, + NoteWatching, + NoteUnread, + Log, + DriveFile, + DriveFolder, + Poll, + PollVote, + Notification, + Emoji, + Hashtag, + SwSubscription, + AbuseUserReport, + RegistrationTicket, + MessagingMessage, + Signin, + ReversiGame, + ReversiMatching, + ...charts as any + ] + }); +} diff --git a/src/db/redis.ts b/src/db/redis.ts index cebf2a10af..6518cb0059 100644 --- a/src/db/redis.ts +++ b/src/db/redis.ts @@ -1,7 +1,7 @@ import * as redis from 'redis'; import config from '../config'; -export default config.redis ? redis.createClient( +export default redis.createClient( config.redis.port, config.redis.host, { @@ -9,4 +9,4 @@ export default config.redis ? redis.createClient( prefix: config.redis.prefix, db: config.redis.db || 0 } -) : null; +); diff --git a/src/docs/reversi-bot.ja-JP.md b/src/docs/reversi-bot.ja-JP.md index a389ead571..b1f759ade8 100644 --- a/src/docs/reversi-bot.ja-JP.md +++ b/src/docs/reversi-bot.ja-JP.md @@ -42,9 +42,9 @@ Misskeyのリバーシ機能に対応したBotの開発方法をここに記し ``` pos = x + (y * mapWidth) ``` -`mapWidth`は、ゲーム情報の`settings.map`から、次のようにして計算できます: +`mapWidth`は、ゲーム情報の`map`から、次のようにして計算できます: ``` -mapWidth = settings.map[0].length +mapWidth = map[0].length ``` ### Pos から X,Y座標 に変換する @@ -54,7 +54,7 @@ y = Math.floor(pos / mapWidth) ``` ## マップ情報 -マップ情報は、ゲーム情報の`settings.map`に入っています。 +マップ情報は、ゲーム情報の`map`に入っています。 文字列の配列になっており、ひとつひとつの文字がマス情報を表しています。 それをもとにマップのデザインを知る事が出来ます: * `(スペース)` ... マス無し diff --git a/src/games/reversi/core.ts b/src/games/reversi/core.ts index bb27d6f803..cf8986263b 100644 --- a/src/games/reversi/core.ts +++ b/src/games/reversi/core.ts @@ -37,7 +37,7 @@ export type Undo = { /** * ターン */ - turn: Color; + turn: Color | null; }; /** @@ -47,12 +47,12 @@ export default class Reversi { public map: MapPixel[]; public mapWidth: number; public mapHeight: number; - public board: Color[]; - public turn: Color = BLACK; + public board: (Color | null | undefined)[]; + public turn: Color | null = BLACK; public opts: Options; public prevPos = -1; - public prevColor: Color = null; + public prevColor: Color | null = null; private logs: Undo[] = []; @@ -145,12 +145,12 @@ export default class Reversi { // ターン計算 this.turn = this.canPutSomewhere(!this.prevColor) ? !this.prevColor : - this.canPutSomewhere(this.prevColor) ? this.prevColor : + this.canPutSomewhere(this.prevColor!) ? this.prevColor : null; } public undo() { - const undo = this.logs.pop(); + const undo = this.logs.pop()!; this.prevColor = undo.color; this.prevPos = undo.pos; this.board[undo.pos] = null; @@ -254,10 +254,10 @@ export default class Reversi { /** * ゲームの勝者 (null = 引き分け) */ - public get winner(): Color { + public get winner(): Color | null { return this.isEnded ? this.blackCount == this.whiteCount ? null : this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK : - undefined; + undefined as never; } } diff --git a/src/index.ts b/src/index.ts index e55ba5115d..c4a1088c2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,281 +6,8 @@ Error.stackTraceLimit = Infinity; require('events').EventEmitter.defaultMaxListeners = 128; -import * as os from 'os'; -import * as cluster from 'cluster'; -import chalk from 'chalk'; -import * as portscanner from 'portscanner'; -import * as isRoot from 'is-root'; -import Xev from 'xev'; +import boot from './boot'; -import Logger from './services/logger'; -import serverStats from './daemons/server-stats'; -import notesStats from './daemons/notes-stats'; -import queueStats from './daemons/queue-stats'; -import loadConfig from './config/load'; -import { Config } from './config/types'; -import { lessThan } from './prelude/array'; -import * as pkg from '../package.json'; -import { program } from './argv'; -import { checkMongoDB } from './misc/check-mongodb'; -import { showMachineInfo } from './misc/show-machine-info'; - -const logger = new Logger('core', 'cyan'); -const bootLogger = logger.createSubLogger('boot', 'magenta', false); -const clusterLogger = logger.createSubLogger('cluster', 'orange'); -const ev = new Xev(); - -/** - * Init process - */ -function main() { - process.title = `Misskey (${cluster.isMaster ? 'master' : 'worker'})`; - - if (program.onlyQueue) { - queueMain(); - return; - } - - if (cluster.isMaster || program.disableClustering) { - masterMain(); - - if (cluster.isMaster) { - ev.mount(); - } - - if (program.daemons) { - serverStats(); - notesStats(); - queueStats(); - } - } - - if (cluster.isWorker || program.disableClustering) { - workerMain(); - } -} - -function greet() { - if (!program.quiet) { - //#region Misskey logo - const v = `v${pkg.version}`; - console.log(' _____ _ _ '); - console.log(' | |_|___ ___| |_ ___ _ _ '); - console.log(' | | | | |_ -|_ -| \'_| -_| | |'); - console.log(' |_|_|_|_|___|___|_,_|___|_ |'); - console.log(' ' + chalk.gray(v) + (' |___|\n'.substr(v.length))); - //#endregion - - console.log(' Misskey is maintained by @syuilo, @AyaMorisawa, @mei23, and @acid-chicken.'); - console.log(chalk.keyword('orange')(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo')); - - console.log(''); - console.log(chalk`< ${os.hostname()} {gray (PID: ${process.pid.toString()})} >`); - } - - bootLogger.info('Welcome to Misskey!'); - bootLogger.info(`Misskey v${pkg.version}`, null, true); -} - -/** - * Init master process - */ -async function masterMain() { - greet(); - - let config: Config; - - try { - // initialize app - config = await init(); - - if (config.port == null) { - bootLogger.error('The port is not configured. Please configure port.', null, true); - process.exit(1); - } - - if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) { - bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true); - process.exit(1); - } - - if (!await isPortAvailable(config.port)) { - bootLogger.error(`Port ${config.port} is already in use`, null, true); - process.exit(1); - } - } catch (e) { - bootLogger.error('Fatal error occurred during initialization', null, true); - process.exit(1); - } - - bootLogger.succ('Misskey initialized'); - - if (!program.disableClustering) { - await spawnWorkers(config.clusterLimit); - } - - bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true); -} - -/** - * Init worker process - */ -async function workerMain() { - // start server - await require('./server').default(); - - // start job queue - require('./queue').default(); - - if (cluster.isWorker) { - // Send a 'ready' message to parent process - process.send('ready'); - } -} - -async function queueMain() { - greet(); - - try { - // initialize app - await init(); - } catch (e) { - bootLogger.error('Fatal error occurred during initialization', null, true); - process.exit(1); - } - - bootLogger.succ('Misskey initialized'); - - // start processor - require('./queue').default(); - - bootLogger.succ('Queue started', null, true); -} - -const runningNodejsVersion = process.version.slice(1).split('.').map(x => parseInt(x, 10)); -const requiredNodejsVersion = [10, 0, 0]; -const satisfyNodejsVersion = !lessThan(runningNodejsVersion, requiredNodejsVersion); - -function isWellKnownPort(port: number): boolean { - return port < 1024; -} - -async function isPortAvailable(port: number): Promise<boolean> { - return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed'; -} - -function showEnvironment(): void { - const env = process.env.NODE_ENV; - const logger = bootLogger.createSubLogger('env'); - logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); - - if (env !== 'production') { - logger.warn('The environment is not in production mode.'); - logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); - } - - logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`); +export default function() { + return boot(); } - -/** - * Init app - */ -async function init(): Promise<Config> { - showEnvironment(); - - const nodejsLogger = bootLogger.createSubLogger('nodejs'); - - nodejsLogger.info(`Version ${runningNodejsVersion.join('.')}`); - - if (!satisfyNodejsVersion) { - nodejsLogger.error(`Node.js version is less than ${requiredNodejsVersion.join('.')}. Please upgrade it.`, null, true); - process.exit(1); - } - - await showMachineInfo(bootLogger); - - const configLogger = bootLogger.createSubLogger('config'); - let config; - - try { - config = loadConfig(); - } catch (exception) { - if (typeof exception === 'string') { - configLogger.error(exception); - process.exit(1); - } - if (exception.code === 'ENOENT') { - configLogger.error('Configuration file not found', null, true); - process.exit(1); - } - throw exception; - } - - configLogger.succ('Loaded'); - - // Try to connect to MongoDB - try { - await checkMongoDB(config, bootLogger); - } catch (e) { - bootLogger.error('Cannot connect to database', null, true); - process.exit(1); - } - - return config; -} - -async function spawnWorkers(limit: number = Infinity) { - const workers = Math.min(limit, os.cpus().length); - bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); - await Promise.all([...Array(workers)].map(spawnWorker)); - bootLogger.succ('All workers started'); -} - -function spawnWorker(): Promise<void> { - return new Promise(res => { - const worker = cluster.fork(); - worker.on('message', message => { - if (message !== 'ready') return; - res(); - }); - }); -} - -//#region Events - -// Listen new workers -cluster.on('fork', worker => { - clusterLogger.debug(`Process forked: [${worker.id}]`); -}); - -// Listen online workers -cluster.on('online', worker => { - clusterLogger.debug(`Process is now online: [${worker.id}]`); -}); - -// Listen for dying workers -cluster.on('exit', worker => { - // Replace the dead worker, - // we're not sentimental - clusterLogger.error(chalk.red(`[${worker.id}] died :(`)); - cluster.fork(); -}); - -// Display detail of unhandled promise rejection -if (!program.quiet) { - process.on('unhandledRejection', console.dir); -} - -// Display detail of uncaught exception -process.on('uncaughtException', err => { - logger.error(err); -}); - -// Dying away... -process.on('exit', code => { - logger.info(`The process is going to exit with code ${code}`); -}); - -//#endregion - -main(); diff --git a/src/init.ts b/src/init.ts new file mode 100644 index 0000000000..3dcfc28b78 --- /dev/null +++ b/src/init.ts @@ -0,0 +1,10 @@ +import { initDb } from './db/postgre'; + +console.log('Init database...'); + +initDb(false, true, true).then(() => { + console.log('Done :)'); +}, e => { + console.error('Failed to init database'); + console.error(e); +}); diff --git a/src/mfm/fromHtml.ts b/src/mfm/fromHtml.ts index 369f9de0ee..5fc4a16416 100644 --- a/src/mfm/fromHtml.ts +++ b/src/mfm/fromHtml.ts @@ -3,8 +3,6 @@ import { URL } from 'url'; import { urlRegex } from './prelude'; export function fromHtml(html: string): string { - if (html == null) return null; - const dom = parseFragment(html) as DefaultTreeDocumentFragment; let text = ''; diff --git a/src/mfm/parse.ts b/src/mfm/parse.ts index 9d60771708..f8464121f3 100644 --- a/src/mfm/parse.ts +++ b/src/mfm/parse.ts @@ -2,7 +2,7 @@ import { mfmLanguage } from './language'; import { MfmForest } from './prelude'; import { normalize } from './normalize'; -export function parse(source: string): MfmForest { +export function parse(source: string | null): MfmForest | null { if (source == null || source == '') { return null; } @@ -10,7 +10,7 @@ export function parse(source: string): MfmForest { return normalize(mfmLanguage.root.tryParse(source)); } -export function parsePlain(source: string): MfmForest { +export function parsePlain(source: string | null): MfmForest | null { if (source == null || source == '') { return null; } diff --git a/src/mfm/toHtml.ts b/src/mfm/toHtml.ts index c676ae6ffc..58976fc2c3 100644 --- a/src/mfm/toHtml.ts +++ b/src/mfm/toHtml.ts @@ -1,10 +1,10 @@ import { JSDOM } from 'jsdom'; import config from '../config'; -import { INote } from '../models/note'; import { intersperse } from '../prelude/array'; import { MfmForest, MfmTree } from './prelude'; +import { IMentionedRemoteUsers } from '../models/entities/note'; -export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) { +export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { if (tokens == null) { return null; } diff --git a/src/migrate.ts b/src/migrate.ts new file mode 100644 index 0000000000..18217f4085 --- /dev/null +++ b/src/migrate.ts @@ -0,0 +1,502 @@ +process.env.NODE_ENV = 'production'; + +import monk from 'monk'; +import * as mongo from 'mongodb'; +import * as fs from 'fs'; +import * as uuid from 'uuid'; +import chalk from 'chalk'; +import config from './config'; +import { initDb } from './db/postgre'; +import { User } from './models/entities/user'; +import { getRepository } from 'typeorm'; +import generateUserToken from './server/api/common/generate-native-user-token'; +import { DriveFile } from './models/entities/drive-file'; +import { DriveFolder } from './models/entities/drive-folder'; +import { InternalStorage } from './services/drive/internal-storage'; +import { createTemp } from './misc/create-temp'; +import { Note } from './models/entities/note'; +import { Following } from './models/entities/following'; +import { Poll } from './models/entities/poll'; +import { PollVote } from './models/entities/poll-vote'; +import { NoteFavorite } from './models/entities/note-favorite'; +import { NoteReaction } from './models/entities/note-reaction'; +import { UserPublickey } from './models/entities/user-publickey'; +import { UserKeypair } from './models/entities/user-keypair'; +import { extractPublic } from './crypto_key'; +import { Emoji } from './models/entities/emoji'; +import { toPuny as _toPuny } from './misc/convert-host'; +import { UserProfile } from './models/entities/user-profile'; + +function toPuny(x: string | null): string | null { + if (x == null) return null; + return _toPuny(x); +} + +const u = (config as any).mongodb.user ? encodeURIComponent((config as any).mongodb.user) : null; +const p = (config as any).mongodb.pass ? encodeURIComponent((config as any).mongodb.pass) : null; + +const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${(config as any).mongodb.host}:${(config as any).mongodb.port}/${(config as any).mongodb.db}`; + +const db = monk(uri); +let mdb: mongo.Db; + +const test = false; +const limit = 500; + +const nativeDbConn = async (): Promise<mongo.Db> => { + if (mdb) return mdb; + + const db = await ((): Promise<mongo.Db> => new Promise((resolve, reject) => { + mongo.MongoClient.connect(uri, { useNewUrlParser: true }, (e: Error, client: any) => { + if (e) return reject(e); + resolve(client.db((config as any).mongodb.db)); + }); + }))(); + + mdb = db; + + return db; +}; + +const _User = db.get<any>('users'); +const _DriveFile = db.get<any>('driveFiles.files'); +const _DriveFolder = db.get<any>('driveFolders'); +const _Note = db.get<any>('notes'); +const _Following = db.get<any>('following'); +const _PollVote = db.get<any>('pollVotes'); +const _Favorite = db.get<any>('favorites'); +const _NoteReaction = db.get<any>('noteReactions'); +const _Emoji = db.get<any>('emoji'); +const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => { + const db = await nativeDbConn(); + const bucket = new mongo.GridFSBucket(db, { + bucketName: 'driveFiles' + }); + return bucket; +}; + +async function main() { + await initDb(); + const Users = getRepository(User); + const UserProfiles = getRepository(UserProfile); + const DriveFiles = getRepository(DriveFile); + const DriveFolders = getRepository(DriveFolder); + const Notes = getRepository(Note); + const Followings = getRepository(Following); + const Polls = getRepository(Poll); + const PollVotes = getRepository(PollVote); + const NoteFavorites = getRepository(NoteFavorite); + const NoteReactions = getRepository(NoteReaction); + const UserPublickeys = getRepository(UserPublickey); + const UserKeypairs = getRepository(UserKeypair); + const Emojis = getRepository(Emoji); + + async function migrateUser(user: any) { + await Users.save({ + id: user._id.toHexString(), + createdAt: user.createdAt || new Date(), + username: user.username, + usernameLower: user.username.toLowerCase(), + host: toPuny(user.host), + token: generateUserToken(), + isAdmin: user.isAdmin || false, + name: user.name, + followersCount: user.followersCount || 0, + followingCount: user.followingCount || 0, + notesCount: user.notesCount || 0, + isBot: user.isBot || false, + isCat: user.isCat || false, + isVerified: user.isVerified || false, + inbox: user.inbox, + sharedInbox: user.sharedInbox, + uri: user.uri, + }); + await UserProfiles.save({ + userId: user._id.toHexString(), + description: user.description, + userHost: toPuny(user.host), + autoAcceptFollowed: true, + autoWatch: false, + password: user.password, + location: user.profile ? user.profile.location : null, + birthday: user.profile ? user.profile.birthday : null, + }); + if (user.publicKey) { + await UserPublickeys.save({ + userId: user._id.toHexString(), + keyId: user.publicKey.id, + keyPem: user.publicKey.publicKeyPem + }); + } + if (user.keypair) { + await UserKeypairs.save({ + userId: user._id.toHexString(), + publicKey: extractPublic(user.keypair), + privateKey: user.keypair, + }); + } + } + + async function migrateFollowing(following: any) { + await Followings.save({ + id: following._id.toHexString(), + createdAt: new Date(), + followerId: following.followerId.toHexString(), + followeeId: following.followeeId.toHexString(), + + // 非正規化 + followerHost: following._follower ? toPuny(following._follower.host) : null, + followerInbox: following._follower ? following._follower.inbox : null, + followerSharedInbox: following._follower ? following._follower.sharedInbox : null, + followeeHost: following._followee ? toPuny(following._followee.host) : null, + followeeInbox: following._followee ? following._followee.inbox : null, + followeeSharedInbox: following._followee ? following._followee.sharedInbo : null + }); + } + + async function migrateDriveFolder(folder: any) { + await DriveFolders.save({ + id: folder._id.toHexString(), + createdAt: folder.createdAt || new Date(), + name: folder.name, + parentId: folder.parentId ? folder.parentId.toHexString() : null, + }); + } + + async function migrateDriveFile(file: any) { + const user = await _User.findOne({ + _id: file.metadata.userId + }); + if (user == null) return; + if (file.metadata.storage && file.metadata.storage.key) { // when object storage + await DriveFiles.save({ + id: file._id.toHexString(), + userId: user._id.toHexString(), + userHost: toPuny(user.host), + createdAt: file.uploadDate || new Date(), + md5: file.md5, + name: file.filename, + type: file.contentType, + properties: file.metadata.properties || {}, + size: file.length, + url: file.metadata.url, + uri: file.metadata.uri, + accessKey: file.metadata.storage.key, + folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null, + storedInternal: false, + isLink: false + }); + } else if (!file.metadata.isLink) { + const [temp, clean] = await createTemp(); + await new Promise(async (res, rej) => { + const bucket = await getDriveFileBucket(); + const readable = bucket.openDownloadStream(file._id); + const dest = fs.createWriteStream(temp); + readable.pipe(dest); + readable.on('end', () => { + dest.end(); + res(); + }); + }); + + const key = uuid.v4(); + const url = InternalStorage.saveFromPath(key, temp); + await DriveFiles.save({ + id: file._id.toHexString(), + userId: user._id.toHexString(), + userHost: toPuny(user.host), + createdAt: file.uploadDate || new Date(), + md5: file.md5, + name: file.filename, + type: file.contentType, + properties: file.metadata.properties, + size: file.length, + url: url, + uri: file.metadata.uri, + accessKey: key, + folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null, + storedInternal: true, + isLink: false + }); + clean(); + } else { + await DriveFiles.save({ + id: file._id.toHexString(), + userId: user._id.toHexString(), + userHost: toPuny(user.host), + createdAt: file.uploadDate || new Date(), + md5: file.md5, + name: file.filename, + type: file.contentType, + properties: file.metadata.properties, + size: file.length, + url: file.metadata.url, + uri: file.metadata.uri, + accessKey: null, + folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null, + storedInternal: false, + isLink: true + }); + } + } + + async function migrateNote(note: any) { + await Notes.save({ + id: note._id.toHexString(), + createdAt: note.createdAt || new Date(), + text: note.text, + cw: note.cw || null, + tags: note.tags || [], + userId: note.userId.toHexString(), + viaMobile: note.viaMobile || false, + geo: note.geo, + appId: null, + visibility: note.visibility || 'public', + visibleUserIds: note.visibleUserIds ? note.visibleUserIds.map((id: any) => id.toHexString()) : [], + replyId: note.replyId ? note.replyId.toHexString() : null, + renoteId: note.renoteId ? note.renoteId.toHexString() : null, + userHost: null, + fileIds: note.fileIds ? note.fileIds.map((id: any) => id.toHexString()) : [], + localOnly: note.localOnly || false, + hasPoll: note.poll != null + }); + + if (note.poll) { + await Polls.save({ + noteId: note._id.toHexString(), + choices: note.poll.choices.map((x: any) => x.text), + expiresAt: note.poll.expiresAt, + multiple: note.poll.multiple, + votes: note.poll.choices.map((x: any) => x.votes), + noteVisibility: note.visibility, + userId: note.userId.toHexString(), + userHost: null + }); + } + } + + async function migratePollVote(vote: any) { + await PollVotes.save({ + id: vote._id.toHexString(), + createdAt: vote.createdAt, + noteId: vote.noteId.toHexString(), + userId: vote.userId.toHexString(), + choice: vote.choice + }); + } + + async function migrateNoteFavorite(favorite: any) { + await NoteFavorites.save({ + id: favorite._id.toHexString(), + createdAt: favorite.createdAt, + noteId: favorite.noteId.toHexString(), + userId: favorite.userId.toHexString(), + }); + } + + async function migrateNoteReaction(reaction: any) { + await NoteReactions.save({ + id: reaction._id.toHexString(), + createdAt: reaction.createdAt, + noteId: reaction.noteId.toHexString(), + userId: reaction.userId.toHexString(), + reaction: reaction.reaction + }); + } + + async function reMigrateUser(user: any) { + const u = await _User.findOne({ + _id: new mongo.ObjectId(user.id) + }); + const avatar = u.avatarId ? await DriveFiles.findOne(u.avatarId.toHexString()) : null; + const banner = u.bannerId ? await DriveFiles.findOne(u.bannerId.toHexString()) : null; + await Users.update(user.id, { + avatarId: avatar ? avatar.id : null, + bannerId: banner ? banner.id : null, + avatarUrl: avatar ? avatar.url : null, + bannerUrl: banner ? banner.url : null + }); + } + + async function migrateEmoji(emoji: any) { + await Emojis.save({ + id: emoji._id.toHexString(), + updatedAt: emoji.createdAt, + aliases: emoji.aliases, + url: emoji.url, + uri: emoji.uri, + host: toPuny(emoji.host), + name: emoji.name + }); + } + + let allUsersCount = await _User.count({ + deletedAt: { $exists: false } + }); + if (test && allUsersCount > limit) allUsersCount = limit; + for (let i = 0; i < allUsersCount; i++) { + const user = await _User.findOne({ + deletedAt: { $exists: false } + }, { + skip: i + }); + try { + await migrateUser(user); + console.log(`USER (${i + 1}/${allUsersCount}) ${user._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`USER (${i + 1}/${allUsersCount}) ${user._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + let allFollowingsCount = await _Following.count(); + if (test && allFollowingsCount > limit) allFollowingsCount = limit; + for (let i = 0; i < allFollowingsCount; i++) { + const following = await _Following.findOne({}, { + skip: i + }); + try { + await migrateFollowing(following); + console.log(`FOLLOWING (${i + 1}/${allFollowingsCount}) ${following._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`FOLLOWING (${i + 1}/${allFollowingsCount}) ${following._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + let allDriveFoldersCount = await _DriveFolder.count(); + if (test && allDriveFoldersCount > limit) allDriveFoldersCount = limit; + for (let i = 0; i < allDriveFoldersCount; i++) { + const folder = await _DriveFolder.findOne({}, { + skip: i + }); + try { + await migrateDriveFolder(folder); + console.log(`FOLDER (${i + 1}/${allDriveFoldersCount}) ${folder._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`FOLDER (${i + 1}/${allDriveFoldersCount}) ${folder._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + let allDriveFilesCount = await _DriveFile.count({ + 'metadata._user.host': null, + 'metadata.deletedAt': { $exists: false } + }); + if (test && allDriveFilesCount > limit) allDriveFilesCount = limit; + for (let i = 0; i < allDriveFilesCount; i++) { + const file = await _DriveFile.findOne({ + 'metadata._user.host': null, + 'metadata.deletedAt': { $exists: false } + }, { + skip: i + }); + try { + await migrateDriveFile(file); + console.log(`FILE (${i + 1}/${allDriveFilesCount}) ${file._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`FILE (${i + 1}/${allDriveFilesCount}) ${file._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + let allNotesCount = await _Note.count({ + '_user.host': null, + 'metadata.deletedAt': { $exists: false } + }); + if (test && allNotesCount > limit) allNotesCount = limit; + for (let i = 0; i < allNotesCount; i++) { + const note = await _Note.findOne({ + '_user.host': null, + 'metadata.deletedAt': { $exists: false } + }, { + skip: i + }); + try { + await migrateNote(note); + console.log(`NOTE (${i + 1}/${allNotesCount}) ${note._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`NOTE (${i + 1}/${allNotesCount}) ${note._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + let allPollVotesCount = await _PollVote.count(); + if (test && allPollVotesCount > limit) allPollVotesCount = limit; + for (let i = 0; i < allPollVotesCount; i++) { + const vote = await _PollVote.findOne({}, { + skip: i + }); + try { + await migratePollVote(vote); + console.log(`VOTE (${i + 1}/${allPollVotesCount}) ${vote._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`VOTE (${i + 1}/${allPollVotesCount}) ${vote._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + let allNoteFavoritesCount = await _Favorite.count(); + if (test && allNoteFavoritesCount > limit) allNoteFavoritesCount = limit; + for (let i = 0; i < allNoteFavoritesCount; i++) { + const favorite = await _Favorite.findOne({}, { + skip: i + }); + try { + await migrateNoteFavorite(favorite); + console.log(`FAVORITE (${i + 1}/${allNoteFavoritesCount}) ${favorite._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`FAVORITE (${i + 1}/${allNoteFavoritesCount}) ${favorite._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + let allNoteReactionsCount = await _NoteReaction.count(); + if (test && allNoteReactionsCount > limit) allNoteReactionsCount = limit; + for (let i = 0; i < allNoteReactionsCount; i++) { + const reaction = await _NoteReaction.findOne({}, { + skip: i + }); + try { + await migrateNoteReaction(reaction); + console.log(`REACTION (${i + 1}/${allNoteReactionsCount}) ${reaction._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`REACTION (${i + 1}/${allNoteReactionsCount}) ${reaction._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + let allActualUsersCount = await Users.count(); + if (test && allActualUsersCount > limit) allActualUsersCount = limit; + for (let i = 0; i < allActualUsersCount; i++) { + const [user] = await Users.find({ + take: 1, + skip: i + }); + try { + await reMigrateUser(user); + console.log(`RE:USER (${i + 1}/${allActualUsersCount}) ${user.id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`RE:USER (${i + 1}/${allActualUsersCount}) ${user.id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + const allEmojisCount = await _Emoji.count(); + for (let i = 0; i < allEmojisCount; i++) { + const emoji = await _Emoji.findOne({}, { + skip: i + }); + try { + await migrateEmoji(emoji); + console.log(`EMOJI (${i + 1}/${allEmojisCount}) ${emoji._id} ${chalk.green('DONE')}`); + } catch (e) { + console.log(`EMOJI (${i + 1}/${allEmojisCount}) ${emoji._id} ${chalk.red('ERR')}`); + console.error(e); + } + } + + console.log('DONE :)'); +} + +main(); diff --git a/src/misc/acct/render.ts b/src/misc/acct/render.ts index 67e063fcb3..094eceffe9 100644 --- a/src/misc/acct/render.ts +++ b/src/misc/acct/render.ts @@ -1,5 +1,5 @@ import Acct from './type'; export default (user: Acct) => { - return user.host === null ? user.username : `${user.username}@${user.host}`; + return user.host == null ? user.username : `${user.username}@${user.host}`; }; diff --git a/src/misc/acct/type.ts b/src/misc/acct/type.ts index c88a920c69..7f31257400 100644 --- a/src/misc/acct/type.ts +++ b/src/misc/acct/type.ts @@ -1,6 +1,6 @@ type Acct = { username: string; - host: string; + host: string | null; }; export default Acct; diff --git a/src/misc/cafy-id.ts b/src/misc/cafy-id.ts index bc8fe4ea2b..39886611e1 100644 --- a/src/misc/cafy-id.ts +++ b/src/misc/cafy-id.ts @@ -1,38 +1,13 @@ -import * as mongo from 'mongodb'; import { Context } from 'cafy'; -import isObjectId from './is-objectid'; -export const isAnId = (x: any) => mongo.ObjectID.isValid(x); -export const isNotAnId = (x: any) => !isAnId(x); -export const transform = (x: string | mongo.ObjectID): mongo.ObjectID => { - if (x === undefined) return undefined; - if (x === null) return null; - - if (isAnId(x) && !isObjectId(x)) { - return new mongo.ObjectID(x); - } else { - return x as mongo.ObjectID; - } -}; -export const transformMany = (xs: (string | mongo.ObjectID)[]): mongo.ObjectID[] => { - if (xs == null) return null; - - return xs.map(x => transform(x)); -}; - -export type ObjectId = mongo.ObjectID; - -/** - * ID - */ -export default class ID<Maybe = string> extends Context<string | Maybe> { +export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> { public readonly name = 'ID'; constructor(optional = false, nullable = false) { super(optional, nullable); this.push((v: any) => { - if (!isObjectId(v) && isNotAnId(v)) { + if (typeof v !== 'string') { return new Error('must-be-an-id'); } return true; diff --git a/src/misc/check-mongodb.ts b/src/misc/check-mongodb.ts deleted file mode 100644 index 8e03db5d42..0000000000 --- a/src/misc/check-mongodb.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { nativeDbConn } from '../db/mongodb'; -import { Config } from '../config/types'; -import Logger from '../services/logger'; -import { lessThan } from '../prelude/array'; - -const requiredMongoDBVersion = [3, 6]; - -export function checkMongoDB(config: Config, logger: Logger) { - return new Promise((res, rej) => { - const mongoDBLogger = logger.createSubLogger('db'); - const u = config.mongodb.user ? encodeURIComponent(config.mongodb.user) : null; - const p = config.mongodb.pass ? encodeURIComponent(config.mongodb.pass) : null; - const uri = `mongodb://${u && p ? `${u}:****@` : ''}${config.mongodb.host}:${config.mongodb.port}/${config.mongodb.db}`; - mongoDBLogger.info(`Connecting to ${uri} ...`); - - nativeDbConn().then(db => { - mongoDBLogger.succ('Connectivity confirmed'); - - db.admin().serverInfo().then(x => { - const version = x.version as string; - mongoDBLogger.info(`Version: ${version}`); - if (lessThan(version.split('.').map(x => parseInt(x, 10)), requiredMongoDBVersion)) { - mongoDBLogger.error(`MongoDB version is less than ${requiredMongoDBVersion.join('.')}. Please upgrade it.`); - rej('outdated version'); - } else { - res(); - } - }).catch(err => { - mongoDBLogger.error(`Failed to fetch server info: ${err.message}`); - rej(err); - }); - }).catch(err => { - mongoDBLogger.error(err.message); - rej(err); - }); - }); -} diff --git a/src/misc/convert-host.ts b/src/misc/convert-host.ts index 8f2f1c7aba..a5fb15c66f 100644 --- a/src/misc/convert-host.ts +++ b/src/misc/convert-host.ts @@ -1,27 +1,26 @@ import config from '../config'; -import { toUnicode, toASCII } from 'punycode'; +import { toASCII } from 'punycode'; import { URL } from 'url'; -export function getFullApAccount(username: string, host: string) { - return host ? `${username}@${toApHost(host)}` : `${username}@${toApHost(config.host)}`; +export function getFullApAccount(username: string, host: string | null) { + return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`; } export function isSelfHost(host: string) { if (host == null) return true; - return toApHost(config.host) === toApHost(host); + return toPuny(config.host) === toPuny(host); } export function extractDbHost(uri: string) { const url = new URL(uri); - return toDbHost(url.hostname); + return toPuny(url.hostname); } -export function toDbHost(host: string) { - if (host == null) return null; - return toUnicode(host.toLowerCase()); +export function toPuny(host: string) { + return toASCII(host.toLowerCase()); } -export function toApHost(host: string) { +export function toPunyNullable(host: string | null | undefined): string | null { if (host == null) return null; return toASCII(host.toLowerCase()); } diff --git a/src/misc/detect-mine.ts b/src/misc/detect-mine.ts index bbf49efc10..70d58ffe21 100644 --- a/src/misc/detect-mine.ts +++ b/src/misc/detect-mine.ts @@ -3,7 +3,7 @@ import fileType from 'file-type'; import checkSvg from '../misc/check-svg'; export async function detectMine(path: string) { - return new Promise<[string, string]>((res, rej) => { + return new Promise<[string, string | null]>((res, rej) => { const readable = fs.createReadStream(path); readable .on('error', rej) diff --git a/src/misc/donwload-url.ts b/src/misc/donwload-url.ts index 0dd4e4ef5d..167e01fdd1 100644 --- a/src/misc/donwload-url.ts +++ b/src/misc/donwload-url.ts @@ -1,5 +1,4 @@ import * as fs from 'fs'; -import * as URL from 'url'; import * as request from 'request'; import config from '../config'; import chalk from 'chalk'; @@ -26,7 +25,7 @@ export async function downloadUrl(url: string, path: string) { rej(error); }); - const requestUrl = URL.parse(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url; + const requestUrl = new URL(url).pathname.match(/[^\u0021-\u00ff]/) ? encodeURI(url) : url; const req = request({ url: requestUrl, diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts index 3584a819bf..cf1fc474ce 100644 --- a/src/misc/fetch-meta.ts +++ b/src/misc/fetch-meta.ts @@ -1,32 +1,14 @@ -import Meta, { IMeta } from '../models/meta'; +import { Meta } from '../models/entities/meta'; +import { Metas } from '../models'; +import { genId } from './gen-id'; -const defaultMeta: any = { - name: 'Misskey', - maintainer: {}, - langs: [], - cacheRemoteFiles: true, - localDriveCapacityMb: 256, - remoteDriveCapacityMb: 8, - hidedTags: [], - stats: { - originalNotesCount: 0, - originalUsersCount: 0 - }, - maxNoteTextLength: 1000, - enableEmojiReaction: true, - enableTwitterIntegration: false, - enableGithubIntegration: false, - enableDiscordIntegration: false, - enableExternalUserRecommendation: false, - externalUserRecommendationEngine: 'https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-misskey-api.cgi?{{host}}+{{user}}+{{limit}}+{{offset}}', - externalUserRecommendationTimeout: 300000, - mascotImageUrl: '/assets/ai.png', - errorImageUrl: 'https://ai.misskey.xyz/aiart/yubitun.png', - enableServiceWorker: false -}; - -export default async function(): Promise<IMeta> { - const meta = await Meta.findOne({}); - - return Object.assign({}, defaultMeta, meta); +export default async function(): Promise<Meta> { + const meta = await Metas.findOne(); + if (meta) { + return meta; + } else { + return Metas.save({ + id: genId(), + } as Meta); + } } diff --git a/src/misc/fetch-proxy-account.ts b/src/misc/fetch-proxy-account.ts new file mode 100644 index 0000000000..17b021e91e --- /dev/null +++ b/src/misc/fetch-proxy-account.ts @@ -0,0 +1,9 @@ +import fetchMeta from './fetch-meta'; +import { ILocalUser } from '../models/entities/user'; +import { Users } from '../models'; +import { ensure } from '../prelude/ensure'; + +export async function fetchProxyAccount(): Promise<ILocalUser> { + const meta = await fetchMeta(); + return await Users.findOne({ username: meta.proxyAccount!, host: null }).then(ensure) as ILocalUser; +} diff --git a/src/misc/gen-id.ts b/src/misc/gen-id.ts new file mode 100644 index 0000000000..99cb70b3fb --- /dev/null +++ b/src/misc/gen-id.ts @@ -0,0 +1,19 @@ +import { ulid } from 'ulid'; +import { genAid } from './id/aid'; +import { genMeid } from './id/meid'; +import { genObjectId } from './id/object-id'; +import config from '../config'; + +const metohd = config.id.toLowerCase(); + +export function genId(date?: Date): string { + if (!date || (date > new Date())) date = new Date(); + + switch (metohd) { + case 'aid': return genAid(date); + case 'meid': return genMeid(date); + case 'ulid': return ulid(date.getTime()); + case 'objectid': return genObjectId(date); + default: throw new Error('unknown id generation method'); + } +} diff --git a/src/misc/get-drive-file-url.ts b/src/misc/get-drive-file-url.ts deleted file mode 100644 index f2b0f8b001..0000000000 --- a/src/misc/get-drive-file-url.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { IDriveFile } from '../models/drive-file'; -import config from '../config'; - -export default function(file: IDriveFile, thumbnail = false): string { - if (file == null) return null; - - const isImage = file.contentType && file.contentType.startsWith('image/'); - - if (file.metadata.withoutChunks) { - if (thumbnail) { - return file.metadata.thumbnailUrl || file.metadata.webpublicUrl || (isImage ? file.metadata.url : '/assets/thumbnail-not-available.png'); - } else { - return file.metadata.webpublicUrl || file.metadata.url; - } - } else { - if (thumbnail) { - return `${config.driveUrl}/${file._id}?thumbnail`; - } else { - return `${config.driveUrl}/${file._id}?web`; - } - } -} - -export function getOriginalUrl(file: IDriveFile) { - if (file.metadata && file.metadata.url) { - return file.metadata.url; - } - - const accessKey = file.metadata ? file.metadata.accessKey : null; - return `${config.driveUrl}/${file._id}${accessKey ? '?original=' + accessKey : ''}`; -} diff --git a/src/misc/get-notification-summary.ts b/src/misc/get-notification-summary.ts index 71d4973ce9..b20711c605 100644 --- a/src/misc/get-notification-summary.ts +++ b/src/misc/get-notification-summary.ts @@ -20,7 +20,7 @@ export default function(notification: any): string { return `引用されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`; case 'reaction': return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getNoteSummary(notification.note)}」`; - case 'poll_vote': + case 'pollVote': return `投票されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`; default: return `<不明な通知タイプ: ${notification.type}>`; diff --git a/src/misc/get-user-name.ts b/src/misc/get-user-name.ts index eab9f87ef0..b6b45118b0 100644 --- a/src/misc/get-user-name.ts +++ b/src/misc/get-user-name.ts @@ -1,5 +1,5 @@ -import { IUser } from '../models/user'; +import { User } from '../models/entities/user'; -export default function(user: IUser): string { +export default function(user: User): string { return user.name || user.username; } diff --git a/src/misc/get-user-summary.ts b/src/misc/get-user-summary.ts deleted file mode 100644 index 09cf5ebadc..0000000000 --- a/src/misc/get-user-summary.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { IUser, isLocalUser } from '../models/user'; -import getAcct from './acct/render'; -import getUserName from './get-user-name'; - -/** - * ユーザーを表す文字列を取得します。 - * @param user ユーザー - */ -export default function(user: IUser): string { - let string = `${getUserName(user)} (@${getAcct(user)})\n` + - `${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`; - - if (isLocalUser(user)) { - string += `場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n`; - } - - return string + `「${user.description}」`; -} diff --git a/src/misc/id/aid.ts b/src/misc/id/aid.ts new file mode 100644 index 0000000000..530d84e582 --- /dev/null +++ b/src/misc/id/aid.ts @@ -0,0 +1,23 @@ +// AID +// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ2の[ノイズ文字列] + +import * as cluster from 'cluster'; + +const TIME2000 = 946684800000; +let counter = process.pid + (cluster.isMaster ? 0 : cluster.worker.id); + +function getTime(time: number) { + time = time - TIME2000; + if (time < 0) time = 0; + + return time.toString(36).padStart(8, '0'); +} + +function getNoise() { + return counter.toString(36).padStart(2, '0').slice(-2); +} + +export function genAid(date: Date): string { + counter++; + return getTime(date.getTime()) + getNoise(); +} diff --git a/src/misc/id/meid.ts b/src/misc/id/meid.ts new file mode 100644 index 0000000000..d4dc73b4f1 --- /dev/null +++ b/src/misc/id/meid.ts @@ -0,0 +1,26 @@ +const CHARS = '0123456789abcdef'; + +function getTime(time: number) { + if (time < 0) time = 0; + if (time === 0) { + return CHARS[0]; + } + + time += 0x800000000000; + + return time.toString(16).padStart(12, CHARS[0]); +} + +function getRandom() { + let str = ''; + + for (let i = 0; i < 12; i++) { + str += CHARS[Math.floor(Math.random() * CHARS.length)]; + } + + return str; +} + +export function genMeid(date: Date): string { + return 'f' + getTime(date.getTime()) + getRandom(); +} diff --git a/src/misc/id/object-id.ts b/src/misc/id/object-id.ts new file mode 100644 index 0000000000..392ea43301 --- /dev/null +++ b/src/misc/id/object-id.ts @@ -0,0 +1,26 @@ +const CHARS = '0123456789abcdef'; + +function getTime(time: number) { + if (time < 0) time = 0; + if (time === 0) { + return CHARS[0]; + } + + time = Math.floor(time / 1000); + + return time.toString(16).padStart(8, CHARS[0]); +} + +function getRandom() { + let str = ''; + + for (let i = 0; i < 16; i++) { + str += CHARS[Math.floor(Math.random() * CHARS.length)]; + } + + return str; +} + +export function genObjectId(date: Date): string { + return getTime(date.getTime()) + getRandom(); +} diff --git a/src/misc/identifiable-error.ts b/src/misc/identifiable-error.ts index 1edd26cd18..2d7c6bd0c6 100644 --- a/src/misc/identifiable-error.ts +++ b/src/misc/identifiable-error.ts @@ -7,7 +7,7 @@ export class IdentifiableError extends Error { constructor(id: string, message?: string) { super(message); - this.message = message; + this.message = message || ''; this.id = id; } } diff --git a/src/misc/is-duplicate-key-value-error.ts b/src/misc/is-duplicate-key-value-error.ts new file mode 100644 index 0000000000..23d8ceb1b7 --- /dev/null +++ b/src/misc/is-duplicate-key-value-error.ts @@ -0,0 +1,3 @@ +export function isDuplicateKeyValueError(e: Error): boolean { + return e.message.startsWith('duplicate key value'); +} diff --git a/src/misc/is-objectid.ts b/src/misc/is-objectid.ts deleted file mode 100644 index a77c4ee2d5..0000000000 --- a/src/misc/is-objectid.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ObjectID } from 'mongodb'; - -export default function(x: any): x is ObjectID { - return x && typeof x === 'object' && (x.hasOwnProperty('toHexString') || x.hasOwnProperty('_bsontype')); -} diff --git a/src/misc/is-quote.ts b/src/misc/is-quote.ts index a99b8f6434..0a2a72f4a0 100644 --- a/src/misc/is-quote.ts +++ b/src/misc/is-quote.ts @@ -1,5 +1,5 @@ -import { INote } from '../models/note'; +import { Note } from '../models/entities/note'; -export default function(note: INote): boolean { - return note.renoteId != null && (note.text != null || note.poll != null || (note.fileIds != null && note.fileIds.length > 0)); +export default function(note: Note): boolean { + return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); } diff --git a/src/misc/nyaize.ts b/src/misc/nyaize.ts new file mode 100644 index 0000000000..918e7d63fd --- /dev/null +++ b/src/misc/nyaize.ts @@ -0,0 +1,9 @@ +export function nyaize(text: string): string { + return text + // ja-JP + .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') + // ko-KR + .replace(/[나-낳]/g, match => String.fromCharCode( + match.codePointAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0) + )); +} diff --git a/src/misc/reaction-lib.ts b/src/misc/reaction-lib.ts index 7e5a1b0bc0..008991454b 100644 --- a/src/misc/reaction-lib.ts +++ b/src/misc/reaction-lib.ts @@ -1,6 +1,6 @@ -import Emoji from '../models/emoji'; import { emojiRegex } from './emoji-regex'; import fetchMeta from './fetch-meta'; +import { Emojis } from '../models'; const basic10: Record<string, string> = { '👍': 'like', @@ -20,7 +20,7 @@ export async function getFallbackReaction(): Promise<string> { return meta.useStarForReactionFallback ? 'star' : 'like'; } -export async function toDbReaction(reaction: string, enableEmoji = true): Promise<string> { +export async function toDbReaction(reaction?: string | null, enableEmoji = true): Promise<string> { if (reaction == null) return await getFallbackReaction(); // 既存の文字列リアクションはそのまま @@ -49,7 +49,7 @@ export async function toDbReaction(reaction: string, enableEmoji = true): Promis const custom = reaction.match(/^:([\w+-]+):$/); if (custom) { - const emoji = await Emoji.findOne({ + const emoji = await Emojis.findOne({ host: null, name: custom[1], }); diff --git a/src/misc/schema.ts b/src/misc/schema.ts index e5c24dd468..7c17953d97 100644 --- a/src/misc/schema.ts +++ b/src/misc/schema.ts @@ -19,8 +19,8 @@ type MyType<T extends Schema> = { export type SchemaType<p extends Schema> = p['type'] extends 'number' ? number : p['type'] extends 'string' ? string : - p['type'] extends 'array' ? MyType<p['items']>[] : - p['type'] extends 'object' ? ObjType<p['properties']> : + p['type'] extends 'array' ? MyType<NonNullable<p['items']>>[] : + p['type'] extends 'object' ? ObjType<NonNullable<p['properties']>> : any; export function convertOpenApiSchema(schema: Schema) { diff --git a/src/misc/should-mute-this-note.ts b/src/misc/should-mute-this-note.ts index b1d29c6a28..8f606a2943 100644 --- a/src/misc/should-mute-this-note.ts +++ b/src/misc/should-mute-this-note.ts @@ -1,20 +1,13 @@ -import * as mongo from 'mongodb'; -import isObjectId from './is-objectid'; - -function toString(id: any) { - return isObjectId(id) ? (id as mongo.ObjectID).toHexString() : id; -} - export default function(note: any, mutedUserIds: string[]): boolean { - if (mutedUserIds.includes(toString(note.userId))) { + if (mutedUserIds.includes(note.userId)) { return true; } - if (note.reply != null && mutedUserIds.includes(toString(note.reply.userId))) { + if (note.reply != null && mutedUserIds.includes(note.reply.userId)) { return true; } - if (note.renote != null && mutedUserIds.includes(toString(note.renote.userId))) { + if (note.renote != null && mutedUserIds.includes(note.renote.userId)) { return true; } diff --git a/src/models/abuse-user-report.ts b/src/models/abuse-user-report.ts deleted file mode 100644 index f3900d348d..0000000000 --- a/src/models/abuse-user-report.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packUser } from './user'; - -const AbuseUserReport = db.get<IAbuseUserReport>('abuseUserReports'); -AbuseUserReport.createIndex('userId'); -AbuseUserReport.createIndex('reporterId'); -AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true }); -export default AbuseUserReport; - -export interface IAbuseUserReport { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - reporterId: mongo.ObjectID; - comment: string; -} - -export const packMany = ( - reports: (string | mongo.ObjectID | IAbuseUserReport)[] -) => { - return Promise.all(reports.map(x => pack(x))); -}; - -export const pack = ( - report: any -) => new Promise<any>(async (resolve, reject) => { - let _report: any; - - if (isObjectId(report)) { - _report = await AbuseUserReport.findOne({ - _id: report - }); - } else if (typeof report === 'string') { - _report = await AbuseUserReport.findOne({ - _id: new mongo.ObjectID(report) - }); - } else { - _report = deepcopy(report); - } - - // Rename _id to id - _report.id = _report._id; - delete _report._id; - - _report.reporter = await packUser(_report.reporterId, null, { detail: true }); - _report.user = await packUser(_report.userId, null, { detail: true }); - - resolve(_report); -}); diff --git a/src/models/access-token.ts b/src/models/access-token.ts deleted file mode 100644 index 66c5c91c0b..0000000000 --- a/src/models/access-token.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const AccessToken = db.get<IAccessToken>('accessTokens'); -AccessToken.createIndex('token'); -AccessToken.createIndex('hash'); -export default AccessToken; - -export type IAccessToken = { - _id: mongo.ObjectID; - createdAt: Date; - appId: mongo.ObjectID; - userId: mongo.ObjectID; - token: string; - hash: string; -}; diff --git a/src/models/app.ts b/src/models/app.ts deleted file mode 100644 index 45d50bccda..0000000000 --- a/src/models/app.ts +++ /dev/null @@ -1,102 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import AccessToken from './access-token'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import config from '../config'; -import { dbLogger } from '../db/logger'; - -const App = db.get<IApp>('apps'); -App.createIndex('secret'); -export default App; - -export type IApp = { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID | null; - secret: string; - name: string; - description: string; - permission: string[]; - callbackUrl: string; -}; - -/** - * Pack an app for API response - */ -export const pack = ( - app: any, - me?: any, - options?: { - detail?: boolean, - includeSecret?: boolean, - includeProfileImageIds?: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: false, - includeSecret: false, - includeProfileImageIds: false - }, options); - - let _app: any; - - const fields = opts.detail ? {} : { - name: true - }; - - // Populate the app if 'app' is ID - if (isObjectId(app)) { - _app = await App.findOne({ - _id: app - }); - } else if (typeof app === 'string') { - _app = await App.findOne({ - _id: new mongo.ObjectID(app) - }, { fields }); - } else { - _app = deepcopy(app); - } - - // Me - if (me && !isObjectId(me)) { - if (typeof me === 'string') { - me = new mongo.ObjectID(me); - } else { - me = me._id; - } - } - - // (データベースの欠損などで)アプリがデータベース上に見つからなかったとき - if (_app == null) { - dbLogger.warn(`[DAMAGED DB] (missing) pkg: app :: ${app}`); - return null; - } - - // Rename _id to id - _app.id = _app._id; - delete _app._id; - - // Visible by only owner - if (!opts.includeSecret) { - delete _app.secret; - } - - _app.iconUrl = _app.icon != null - ? `${config.driveUrl}/${_app.icon}` - : `${config.driveUrl}/app-default.jpg`; - - if (me) { - // 既に連携しているか - const exist = await AccessToken.count({ - appId: _app.id, - userId: me, - }, { - limit: 1 - }); - - _app.isAuthorized = exist === 1; - } - - resolve(_app); -}); diff --git a/src/models/auth-session.ts b/src/models/auth-session.ts deleted file mode 100644 index 428c707470..0000000000 --- a/src/models/auth-session.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packApp } from './app'; - -const AuthSession = db.get<IAuthSession>('authSessions'); -export default AuthSession; - -export interface IAuthSession { - _id: mongo.ObjectID; - createdAt: Date; - appId: mongo.ObjectID; - userId: mongo.ObjectID; - token: string; -} - -/** - * Pack an auth session for API response - * - * @param {any} session - * @param {any} me? - * @return {Promise<any>} - */ -export const pack = ( - session: any, - me?: any -) => new Promise<any>(async (resolve, reject) => { - let _session: any; - - // TODO: Populate session if it ID - _session = deepcopy(session); - - // Me - if (me && !isObjectId(me)) { - if (typeof me === 'string') { - me = new mongo.ObjectID(me); - } else { - me = me._id; - } - } - - delete _session._id; - - // Populate app - _session.app = await packApp(_session.appId, me); - - resolve(_session); -}); diff --git a/src/models/blocking.ts b/src/models/blocking.ts deleted file mode 100644 index 4bdaa741e9..0000000000 --- a/src/models/blocking.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import * as deepcopy from 'deepcopy'; -import { pack as packUser, IUser } from './user'; - -const Blocking = db.get<IBlocking>('blocking'); -Blocking.createIndex('blockerId'); -Blocking.createIndex('blockeeId'); -Blocking.createIndex(['blockerId', 'blockeeId'], { unique: true }); -export default Blocking; - -export type IBlocking = { - _id: mongo.ObjectID; - createdAt: Date; - blockeeId: mongo.ObjectID; - blockerId: mongo.ObjectID; -}; - -export const packMany = ( - blockings: (string | mongo.ObjectID | IBlocking)[], - me?: string | mongo.ObjectID | IUser -) => { - return Promise.all(blockings.map(x => pack(x, me))); -}; - -export const pack = ( - blocking: any, - me?: any -) => new Promise<any>(async (resolve, reject) => { - let _blocking: any; - - // Populate the blocking if 'blocking' is ID - if (isObjectId(blocking)) { - _blocking = await Blocking.findOne({ - _id: blocking - }); - } else if (typeof blocking === 'string') { - _blocking = await Blocking.findOne({ - _id: new mongo.ObjectID(blocking) - }); - } else { - _blocking = deepcopy(blocking); - } - - // Rename _id to id - _blocking.id = _blocking._id; - delete _blocking._id; - - // Populate blockee - _blocking.blockee = await packUser(_blocking.blockeeId, me, { - detail: true - }); - - resolve(_blocking); -}); diff --git a/src/models/drive-file-thumbnail.ts b/src/models/drive-file-thumbnail.ts deleted file mode 100644 index bdb3d010e6..0000000000 --- a/src/models/drive-file-thumbnail.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as mongo from 'mongodb'; -import monkDb, { nativeDbConn } from '../db/mongodb'; - -const DriveFileThumbnail = monkDb.get<IDriveFileThumbnail>('driveFileThumbnails.files'); -DriveFileThumbnail.createIndex('metadata.originalId', { sparse: true, unique: true }); -export default DriveFileThumbnail; - -export const DriveFileThumbnailChunk = monkDb.get('driveFileThumbnails.chunks'); - -export const getDriveFileThumbnailBucket = async (): Promise<mongo.GridFSBucket> => { - const db = await nativeDbConn(); - const bucket = new mongo.GridFSBucket(db, { - bucketName: 'driveFileThumbnails' - }); - return bucket; -}; - -export type IMetadata = { - originalId: mongo.ObjectID; -}; - -export type IDriveFileThumbnail = { - _id: mongo.ObjectID; - uploadDate: Date; - md5: string; - filename: string; - contentType: string; - metadata: IMetadata; -}; diff --git a/src/models/drive-file-webpublic.ts b/src/models/drive-file-webpublic.ts deleted file mode 100644 index d087c355d3..0000000000 --- a/src/models/drive-file-webpublic.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as mongo from 'mongodb'; -import monkDb, { nativeDbConn } from '../db/mongodb'; - -const DriveFileWebpublic = monkDb.get<IDriveFileWebpublic>('driveFileWebpublics.files'); -DriveFileWebpublic.createIndex('metadata.originalId', { sparse: true, unique: true }); -export default DriveFileWebpublic; - -export const DriveFileWebpublicChunk = monkDb.get('driveFileWebpublics.chunks'); - -export const getDriveFileWebpublicBucket = async (): Promise<mongo.GridFSBucket> => { - const db = await nativeDbConn(); - const bucket = new mongo.GridFSBucket(db, { - bucketName: 'driveFileWebpublics' - }); - return bucket; -}; - -export type IMetadata = { - originalId: mongo.ObjectID; -}; - -export type IDriveFileWebpublic = { - _id: mongo.ObjectID; - uploadDate: Date; - md5: string; - filename: string; - contentType: string; - metadata: IMetadata; -}; diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts deleted file mode 100644 index c31e9a709f..0000000000 --- a/src/models/drive-file.ts +++ /dev/null @@ -1,232 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import { pack as packFolder } from './drive-folder'; -import { pack as packUser } from './user'; -import monkDb, { nativeDbConn } from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import getDriveFileUrl, { getOriginalUrl } from '../misc/get-drive-file-url'; -import { dbLogger } from '../db/logger'; - -const DriveFile = monkDb.get<IDriveFile>('driveFiles.files'); -DriveFile.createIndex('md5'); -DriveFile.createIndex('metadata.uri'); -DriveFile.createIndex('metadata.userId'); -DriveFile.createIndex('metadata.folderId'); -DriveFile.createIndex('metadata._user.host'); -export default DriveFile; - -export const DriveFileChunk = monkDb.get('driveFiles.chunks'); - -export const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => { - const db = await nativeDbConn(); - const bucket = new mongo.GridFSBucket(db, { - bucketName: 'driveFiles' - }); - return bucket; -}; - -export type IMetadata = { - properties: any; - userId: mongo.ObjectID; - _user: any; - folderId: mongo.ObjectID; - comment: string; - - /** - * リモートインスタンスから取得した場合の元URL - */ - uri?: string; - - /** - * URL for web(生成されている場合) or original - * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ - */ - url?: string; - - /** - * URL for thumbnail (thumbnailがなければなし) - * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ - */ - thumbnailUrl?: string; - - /** - * URL for original (web用が生成されてない場合はurlがoriginalを指す) - * * オブジェクトストレージを利用している or リモートサーバーへの直リンクである 場合のみ - */ - webpublicUrl?: string; - - accessKey?: string; - - src?: string; - deletedAt?: Date; - - /** - * このファイルの中身データがMongoDB内に保存されていないか否か - * オブジェクトストレージを利用している or リモートサーバーへの直リンクである - * な場合は true になります - */ - withoutChunks?: boolean; - - storage?: string; - - /*** - * ObjectStorage の格納先の情報 - */ - storageProps?: IStorageProps; - isSensitive?: boolean; - - /** - * このファイルが添付された投稿のID一覧 - */ - attachedNoteIds?: mongo.ObjectID[]; - - /** - * 外部の(信頼されていない)URLへの直リンクか否か - */ - isRemote?: boolean; -}; - -export type IStorageProps = { - /** - * ObjectStorage key for original - */ - key: string; - - /*** - * ObjectStorage key for thumbnail (thumbnailがなければなし) - */ - thumbnailKey?: string; - - /*** - * ObjectStorage key for webpublic (webpublicがなければなし) - */ - webpublicKey?: string; - - id?: string; -}; - -export type IDriveFile = { - _id: mongo.ObjectID; - uploadDate: Date; - md5: string; - filename: string; - contentType: string; - metadata: IMetadata; - - /** - * ファイルサイズ - */ - length: number; -}; - -export function validateFileName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) && - (name.indexOf('\\') === -1) && - (name.indexOf('/') === -1) && - (name.indexOf('..') === -1) - ); -} - -export const packMany = ( - files: any[], - options?: { - detail?: boolean - self?: boolean, - withUser?: boolean, - } -) => { - return Promise.all(files.map(f => pack(f, options))); -}; - -/** - * Pack a drive file for API response - */ -export const pack = ( - file: any, - options?: { - detail?: boolean, - self?: boolean, - withUser?: boolean, - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: false, - self: false - }, options); - - let _file: any; - - // Populate the file if 'file' is ID - if (isObjectId(file)) { - _file = await DriveFile.findOne({ - _id: file - }); - } else if (typeof file === 'string') { - _file = await DriveFile.findOne({ - _id: new mongo.ObjectID(file) - }); - } else { - _file = deepcopy(file); - } - - // (データベースの欠損などで)ファイルがデータベース上に見つからなかったとき - if (_file == null) { - dbLogger.warn(`[DAMAGED DB] (missing) pkg: driveFile :: ${file}`); - return resolve(null); - } - - // rendered target - let _target: any = {}; - - _target.id = _file._id; - _target.createdAt = _file.uploadDate; - _target.name = _file.filename; - _target.type = _file.contentType; - _target.datasize = _file.length; - _target.md5 = _file.md5; - - _target = Object.assign(_target, _file.metadata); - - _target.url = getDriveFileUrl(_file); - _target.thumbnailUrl = getDriveFileUrl(_file, true); - _target.isRemote = _file.metadata.isRemote; - - if (_target.properties == null) _target.properties = {}; - - if (opts.detail) { - if (_target.folderId) { - // Populate folder - _target.folder = await packFolder(_target.folderId, { - detail: true - }); - } - - /* - if (_target.tags) { - // Populate tags - _target.tags = await _target.tags.map(async (tag: any) => - await serializeDriveTag(tag) - ); - } - */ - } - - if (opts.withUser) { - // Populate user - _target.user = await packUser(_file.metadata.userId); - } - - delete _target.withoutChunks; - delete _target.storage; - delete _target.storageProps; - delete _target.isRemote; - delete _target._user; - - if (opts.self) { - _target.url = getOriginalUrl(_file); - } - - resolve(_target); -}); diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts deleted file mode 100644 index b0f6e4273e..0000000000 --- a/src/models/drive-folder.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import DriveFile from './drive-file'; - -const DriveFolder = db.get<IDriveFolder>('driveFolders'); -DriveFolder.createIndex('userId'); -export default DriveFolder; - -export type IDriveFolder = { - _id: mongo.ObjectID; - createdAt: Date; - name: string; - userId: mongo.ObjectID; - parentId: mongo.ObjectID; -}; - -export function isValidFolderName(name: string): boolean { - return ( - (name.trim().length > 0) && - (name.length <= 200) - ); -} - -/** - * Pack a drive folder for API response - */ -export const pack = ( - folder: any, - options?: { - detail: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: false - }, options); - - let _folder: any; - - // Populate the folder if 'folder' is ID - if (isObjectId(folder)) { - _folder = await DriveFolder.findOne({ _id: folder }); - } else if (typeof folder === 'string') { - _folder = await DriveFolder.findOne({ _id: new mongo.ObjectID(folder) }); - } else { - _folder = deepcopy(folder); - } - - // Rename _id to id - _folder.id = _folder._id; - delete _folder._id; - - if (opts.detail) { - const childFoldersCount = await DriveFolder.count({ - parentId: _folder.id - }); - - const childFilesCount = await DriveFile.count({ - 'metadata.folderId': _folder.id - }); - - _folder.foldersCount = childFoldersCount; - _folder.filesCount = childFilesCount; - } - - if (opts.detail && _folder.parentId) { - // Populate parent folder - _folder.parent = await pack(_folder.parentId, { - detail: true - }); - } - - resolve(_folder); -}); diff --git a/src/models/emoji.ts b/src/models/emoji.ts deleted file mode 100644 index cbf939222e..0000000000 --- a/src/models/emoji.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Emoji = db.get<IEmoji>('emoji'); -Emoji.createIndex('name'); -Emoji.createIndex('host'); -Emoji.createIndex(['name', 'host'], { unique: true }); - -export default Emoji; - -export type IEmoji = { - _id: mongo.ObjectID; - name: string; - host: string; - url: string; - aliases?: string[]; - updatedAt?: Date; - /** AP object id */ - uri?: string; - type?: string; -}; diff --git a/src/models/entities/abuse-user-report.ts b/src/models/entities/abuse-user-report.ts new file mode 100644 index 0000000000..43ab56023a --- /dev/null +++ b/src/models/entities/abuse-user-report.ts @@ -0,0 +1,41 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'reporterId'], { unique: true }) +export class AbuseUserReport { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the AbuseUserReport.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public reporterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public reporter: User | null; + + @Column('varchar', { + length: 512, + }) + public comment: string; +} diff --git a/src/models/entities/access-token.ts b/src/models/entities/access-token.ts new file mode 100644 index 0000000000..d08930cf5a --- /dev/null +++ b/src/models/entities/access-token.ts @@ -0,0 +1,45 @@ +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn, RelationId } from 'typeorm'; +import { User } from './user'; +import { App } from './app'; +import { id } from '../id'; + +@Entity() +export class AccessToken { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AccessToken.' + }) + public createdAt: Date; + + @Index() + @Column('varchar', { + length: 128 + }) + public token: string; + + @Index() + @Column('varchar', { + length: 128 + }) + public hash: string; + + @RelationId((self: AccessToken) => self.user) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public appId: App['id']; + + @ManyToOne(type => App, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public app: App | null; +} diff --git a/src/models/entities/app.ts b/src/models/entities/app.ts new file mode 100644 index 0000000000..ea87546311 --- /dev/null +++ b/src/models/entities/app.ts @@ -0,0 +1,60 @@ +import { Entity, PrimaryColumn, Column, Index, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class App { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the App.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.' + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL', + nullable: true, + }) + public user: User | null; + + @Index() + @Column('varchar', { + length: 64, + comment: 'The secret key of the App.' + }) + public secret: string; + + @Column('varchar', { + length: 128, + comment: 'The name of the App.' + }) + public name: string; + + @Column('varchar', { + length: 512, + comment: 'The description of the App.' + }) + public description: string; + + @Column('varchar', { + length: 64, array: true, + comment: 'The permission of the App.' + }) + public permission: string[]; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The callbackUrl of the App.' + }) + public callbackUrl: string | null; +} diff --git a/src/models/entities/auth-session.ts b/src/models/entities/auth-session.ts new file mode 100644 index 0000000000..4eec27e3f6 --- /dev/null +++ b/src/models/entities/auth-session.ts @@ -0,0 +1,43 @@ +import { Entity, PrimaryColumn, Index, Column, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from './user'; +import { App } from './app'; +import { id } from '../id'; + +@Entity() +export class AuthSession { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the AuthSession.' + }) + public createdAt: Date; + + @Index() + @Column('varchar', { + length: 128 + }) + public token: string; + + @Column({ + ...id(), + nullable: true + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE', + nullable: true + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public appId: App['id']; + + @ManyToOne(type => App, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public app: App | null; +} diff --git a/src/models/entities/blocking.ts b/src/models/entities/blocking.ts new file mode 100644 index 0000000000..48487cb086 --- /dev/null +++ b/src/models/entities/blocking.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['blockerId', 'blockeeId'], { unique: true }) +export class Blocking { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Blocking.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The blockee user ID.' + }) + public blockeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public blockee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The blocker user ID.' + }) + public blockerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public blocker: User | null; +} diff --git a/src/models/entities/drive-file.ts b/src/models/entities/drive-file.ts new file mode 100644 index 0000000000..130af39ede --- /dev/null +++ b/src/models/entities/drive-file.ts @@ -0,0 +1,154 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { DriveFolder } from './drive-folder'; +import { id } from '../id'; + +@Entity() +export class DriveFile { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the DriveFile.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.' + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: 'The host of owner. It will be null if the user in local.' + }) + public userHost: string | null; + + @Index() + @Column('varchar', { + length: 32, + comment: 'The MD5 hash of the DriveFile.' + }) + public md5: string; + + @Column('varchar', { + length: 256, + comment: 'The file name of the DriveFile.' + }) + public name: string; + + @Index() + @Column('varchar', { + length: 128, + comment: 'The content type (MIME) of the DriveFile.' + }) + public type: string; + + @Column('integer', { + comment: 'The file size (bytes) of the DriveFile.' + }) + public size: number; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The comment of the DriveFile.' + }) + public comment: string | null; + + @Column('jsonb', { + default: {}, + comment: 'The any properties of the DriveFile. For example, it includes image width/height.' + }) + public properties: Record<string, any>; + + @Column('boolean') + public storedInternal: boolean; + + @Column('varchar', { + length: 512, + comment: 'The URL of the DriveFile.' + }) + public url: string; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URL of the thumbnail of the DriveFile.' + }) + public thumbnailUrl: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URL of the webpublic of the DriveFile.' + }) + public webpublicUrl: string | null; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, nullable: true, + }) + public accessKey: string | null; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, nullable: true, + }) + public thumbnailAccessKey: string | null; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, nullable: true, + }) + public webpublicAccessKey: string | null; + + @Index() + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URI of the DriveFile. it will be null when the DriveFile is local.' + }) + public uri: string | null; + + @Column('varchar', { + length: 512, nullable: true, + }) + public src: string | null; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The parent folder ID. If null, it means the DriveFile is located in root.' + }) + public folderId: DriveFolder['id'] | null; + + @ManyToOne(type => DriveFolder, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public folder: DriveFolder | null; + + @Column('boolean', { + default: false, + comment: 'Whether the DriveFile is NSFW.' + }) + public isSensitive: boolean; + + /** + * 外部の(信頼されていない)URLへの直リンクか否か + */ + @Column('boolean', { + default: false, + comment: 'Whether the DriveFile is direct link to remote server.' + }) + public isLink: boolean; +} diff --git a/src/models/entities/drive-folder.ts b/src/models/entities/drive-folder.ts new file mode 100644 index 0000000000..a80d075855 --- /dev/null +++ b/src/models/entities/drive-folder.ts @@ -0,0 +1,49 @@ +import { JoinColumn, ManyToOne, Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class DriveFolder { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the DriveFolder.' + }) + public createdAt: Date; + + @Column('varchar', { + length: 128, + comment: 'The name of the DriveFolder.' + }) + public name: string; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The owner ID.' + }) + public userId: User['id'] | null; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The parent folder ID. If null, it means the DriveFolder is located in root.' + }) + public parentId: DriveFolder['id'] | null; + + @ManyToOne(type => DriveFolder, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public parent: DriveFolder | null; +} diff --git a/src/models/entities/emoji.ts b/src/models/entities/emoji.ts new file mode 100644 index 0000000000..020636a7fb --- /dev/null +++ b/src/models/entities/emoji.ts @@ -0,0 +1,46 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +@Index(['name', 'host'], { unique: true }) +export class Emoji { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + nullable: true + }) + public updatedAt: Date | null; + + @Index() + @Column('varchar', { + length: 128 + }) + public name: string; + + @Index() + @Column('varchar', { + length: 128, nullable: true + }) + public host: string | null; + + @Column('varchar', { + length: 512, + }) + public url: string; + + @Column('varchar', { + length: 512, nullable: true + }) + public uri: string | null; + + @Column('varchar', { + length: 64, nullable: true + }) + public type: string | null; + + @Column('varchar', { + array: true, length: 128, default: '{}' + }) + public aliases: string[]; +} diff --git a/src/models/entities/follow-request.ts b/src/models/entities/follow-request.ts new file mode 100644 index 0000000000..22ec263962 --- /dev/null +++ b/src/models/entities/follow-request.ts @@ -0,0 +1,85 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['followerId', 'followeeId'], { unique: true }) +export class FollowRequest { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the FollowRequest.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The followee user ID.' + }) + public followeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public followee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The follower user ID.' + }) + public followerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public follower: User | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'id of Follow Activity.' + }) + public requestId: string | null; + + //#region Denormalized fields + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public followerHost: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]' + }) + public followerInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]' + }) + public followerSharedInbox: string | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public followeeHost: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]' + }) + public followeeInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]' + }) + public followeeSharedInbox: string | null; + //#endregion +} diff --git a/src/models/entities/following.ts b/src/models/entities/following.ts new file mode 100644 index 0000000000..ee3286a1a1 --- /dev/null +++ b/src/models/entities/following.ts @@ -0,0 +1,80 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['followerId', 'followeeId'], { unique: true }) +export class Following { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Following.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The followee user ID.' + }) + public followeeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public followee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The follower user ID.' + }) + public followerId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public follower: User | null; + + //#region Denormalized fields + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public followerHost: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]' + }) + public followerInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]' + }) + public followerSharedInbox: string | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public followeeHost: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]' + }) + public followeeInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: '[Denormalized]' + }) + public followeeSharedInbox: string | null; + //#endregion +} diff --git a/src/models/entities/games/reversi/game.ts b/src/models/entities/games/reversi/game.ts new file mode 100644 index 0000000000..9deacaf5c6 --- /dev/null +++ b/src/models/entities/games/reversi/game.ts @@ -0,0 +1,133 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from '../../user'; +import { id } from '../../../id'; + +@Entity() +export class ReversiGame { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the ReversiGame.' + }) + public createdAt: Date; + + @Column('timestamp with time zone', { + nullable: true, + comment: 'The started date of the ReversiGame.' + }) + public startedAt: Date | null; + + @Column(id()) + public user1Id: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user1: User | null; + + @Column(id()) + public user2Id: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user2: User | null; + + @Column('boolean', { + default: false, + }) + public user1Accepted: boolean; + + @Column('boolean', { + default: false, + }) + public user2Accepted: boolean; + + /** + * どちらのプレイヤーが先行(黒)か + * 1 ... user1 + * 2 ... user2 + */ + @Column('integer', { + nullable: true, + }) + public black: number | null; + + @Column('boolean', { + default: false, + }) + public isStarted: boolean; + + @Column('boolean', { + default: false, + }) + public isEnded: boolean; + + @Column({ + ...id(), + nullable: true + }) + public winnerId: User['id'] | null; + + @Column({ + ...id(), + nullable: true + }) + public surrendered: User['id'] | null; + + @Column('jsonb', { + default: [], + }) + public logs: { + at: Date; + color: boolean; + pos: number; + }[]; + + @Column('varchar', { + array: true, length: 64, + }) + public map: string[]; + + @Column('varchar', { + length: 32 + }) + public bw: string; + + @Column('boolean', { + default: false, + }) + public isLlotheo: boolean; + + @Column('boolean', { + default: false, + }) + public canPutEverywhere: boolean; + + @Column('boolean', { + default: false, + }) + public loopedBoard: boolean; + + @Column('jsonb', { + nullable: true, default: null, + }) + public form1: any | null; + + @Column('jsonb', { + nullable: true, default: null, + }) + public form2: any | null; + + /** + * ログのposを文字列としてすべて連結したもののCRC32値 + */ + @Column('varchar', { + length: 32, nullable: true + }) + public crc32: string | null; +} diff --git a/src/models/entities/games/reversi/matching.ts b/src/models/entities/games/reversi/matching.ts new file mode 100644 index 0000000000..477a29316e --- /dev/null +++ b/src/models/entities/games/reversi/matching.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from '../../user'; +import { id } from '../../../id'; + +@Entity() +export class ReversiMatching { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the ReversiMatching.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public parentId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public parent: User | null; + + @Index() + @Column(id()) + public childId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public child: User | null; +} diff --git a/src/models/entities/hashtag.ts b/src/models/entities/hashtag.ts new file mode 100644 index 0000000000..842cdaa562 --- /dev/null +++ b/src/models/entities/hashtag.ts @@ -0,0 +1,87 @@ +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class Hashtag { + @PrimaryColumn(id()) + public id: string; + + @Index({ unique: true }) + @Column('varchar', { + length: 128 + }) + public name: string; + + @Column({ + ...id(), + array: true, + }) + public mentionedUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public mentionedUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public mentionedLocalUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public mentionedLocalUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public mentionedRemoteUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public mentionedRemoteUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public attachedUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedLocalUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public attachedLocalUsersCount: number; + + @Column({ + ...id(), + array: true, + }) + public attachedRemoteUserIds: User['id'][]; + + @Index() + @Column('integer', { + default: 0 + }) + public attachedRemoteUsersCount: number; +} diff --git a/src/models/entities/instance.ts b/src/models/entities/instance.ts new file mode 100644 index 0000000000..977054263c --- /dev/null +++ b/src/models/entities/instance.ts @@ -0,0 +1,132 @@ +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class Instance { + @PrimaryColumn(id()) + public id: string; + + /** + * このインスタンスを捕捉した日時 + */ + @Index() + @Column('timestamp with time zone', { + comment: 'The caught date of the Instance.' + }) + public caughtAt: Date; + + /** + * ホスト + */ + @Index({ unique: true }) + @Column('varchar', { + length: 128, + comment: 'The host of the Instance.' + }) + public host: string; + + /** + * インスタンスのシステム (MastodonとかMisskeyとかPleromaとか) + */ + @Column('varchar', { + length: 64, nullable: true, + comment: 'The system of the Instance.' + }) + public system: string | null; + + /** + * インスタンスのユーザー数 + */ + @Column('integer', { + default: 0, + comment: 'The count of the users of the Instance.' + }) + public usersCount: number; + + /** + * インスタンスの投稿数 + */ + @Column('integer', { + default: 0, + comment: 'The count of the notes of the Instance.' + }) + public notesCount: number; + + /** + * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数 + */ + @Column('integer', { + default: 0, + }) + public followingCount: number; + + /** + * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数 + */ + @Column('integer', { + default: 0, + }) + public followersCount: number; + + /** + * ドライブ使用量 + */ + @Column('integer', { + default: 0, + }) + public driveUsage: number; + + /** + * ドライブのファイル数 + */ + @Column('integer', { + default: 0, + }) + public driveFiles: number; + + /** + * 直近のリクエスト送信日時 + */ + @Column('timestamp with time zone', { + nullable: true, + }) + public latestRequestSentAt: Date | null; + + /** + * 直近のリクエスト送信時のHTTPステータスコード + */ + @Column('integer', { + nullable: true, + }) + public latestStatus: number | null; + + /** + * 直近のリクエスト受信日時 + */ + @Column('timestamp with time zone', { + nullable: true, + }) + public latestRequestReceivedAt: Date | null; + + /** + * このインスタンスと最後にやり取りした日時 + */ + @Column('timestamp with time zone') + public lastCommunicatedAt: Date; + + /** + * このインスタンスと不通かどうか + */ + @Column('boolean', { + default: false + }) + public isNotResponding: boolean; + + /** + * このインスタンスが閉鎖済みとしてマークされているか + */ + @Column('boolean', { + default: false + }) + public isMarkedAsClosed: boolean; +} diff --git a/src/models/entities/log.ts b/src/models/entities/log.ts new file mode 100644 index 0000000000..99e1e8947e --- /dev/null +++ b/src/models/entities/log.ts @@ -0,0 +1,46 @@ +import { Entity, PrimaryColumn, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class Log { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Log.' + }) + public createdAt: Date; + + @Index() + @Column('varchar', { + length: 64, array: true, default: '{}' + }) + public domain: string[]; + + @Index() + @Column('enum', { + enum: ['error', 'warning', 'info', 'success', 'debug'] + }) + public level: string; + + @Column('varchar', { + length: 8 + }) + public worker: string; + + @Column('varchar', { + length: 128 + }) + public machine: string; + + @Column('varchar', { + length: 1024 + }) + public message: string; + + @Column('jsonb', { + default: {} + }) + public data: Record<string, any>; +} diff --git a/src/models/entities/messaging-message.ts b/src/models/entities/messaging-message.ts new file mode 100644 index 0000000000..d3c3eab3a2 --- /dev/null +++ b/src/models/entities/messaging-message.ts @@ -0,0 +1,64 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { DriveFile } from './drive-file'; +import { id } from '../id'; + +@Entity() +export class MessagingMessage { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the MessagingMessage.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The sender user ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The recipient user ID.' + }) + public recipientId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public recipient: User | null; + + @Column('varchar', { + length: 4096, nullable: true + }) + public text: string | null; + + @Column('boolean', { + default: false, + }) + public isRead: boolean; + + @Column({ + ...id(), + nullable: true, + }) + public fileId: DriveFile['id'] | null; + + @ManyToOne(type => DriveFile, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public file: DriveFile | null; +} diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts new file mode 100644 index 0000000000..f3ac23bac7 --- /dev/null +++ b/src/models/entities/meta.ts @@ -0,0 +1,264 @@ +import { Entity, Column, PrimaryColumn } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class Meta { + @PrimaryColumn(id()) + public id: string; + + @Column('varchar', { + length: 128, nullable: true + }) + public name: string | null; + + @Column('varchar', { + length: 1024, nullable: true + }) + public description: string | null; + + /** + * メンテナの名前 + */ + @Column('varchar', { + length: 128, nullable: true + }) + public maintainerName: string | null; + + /** + * メンテナの連絡先 + */ + @Column('varchar', { + length: 128, nullable: true + }) + public maintainerEmail: string | null; + + @Column('jsonb', { + default: [], + }) + public announcements: Record<string, any>[]; + + @Column('boolean', { + default: false, + }) + public disableRegistration: boolean; + + @Column('boolean', { + default: false, + }) + public disableLocalTimeline: boolean; + + @Column('boolean', { + default: false, + }) + public disableGlobalTimeline: boolean; + + @Column('boolean', { + default: true, + }) + public enableEmojiReaction: boolean; + + @Column('boolean', { + default: false, + }) + public useStarForReactionFallback: boolean; + + @Column('varchar', { + length: 64, array: true, default: '{}' + }) + public langs: string[]; + + @Column('varchar', { + length: 256, array: true, default: '{}' + }) + public hiddenTags: string[]; + + @Column('varchar', { + length: 256, array: true, default: '{}' + }) + public blockedHosts: string[]; + + @Column('varchar', { + length: 512, + nullable: true, + default: '/assets/ai.png' + }) + public mascotImageUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public bannerUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true, + default: 'https://xn--931a.moe/aiart/yubitun.png' + }) + public errorImageUrl: string | null; + + @Column('varchar', { + length: 512, + nullable: true + }) + public iconUrl: string | null; + + @Column('boolean', { + default: true, + }) + public cacheRemoteFiles: boolean; + + @Column('varchar', { + length: 128, + nullable: true + }) + public proxyAccount: string | null; + + @Column('boolean', { + default: false, + }) + public enableRecaptcha: boolean; + + @Column('varchar', { + length: 64, + nullable: true + }) + public recaptchaSiteKey: string | null; + + @Column('varchar', { + length: 64, + nullable: true + }) + public recaptchaSecretKey: string | null; + + @Column('integer', { + default: 1024, + comment: 'Drive capacity of a local user (MB)' + }) + public localDriveCapacityMb: number; + + @Column('integer', { + default: 32, + comment: 'Drive capacity of a remote user (MB)' + }) + public remoteDriveCapacityMb: number; + + @Column('integer', { + default: 500, + comment: 'Max allowed note text length in characters' + }) + public maxNoteTextLength: number; + + @Column('varchar', { + length: 128, + nullable: true + }) + public summalyProxy: string | null; + + @Column('boolean', { + default: false, + }) + public enableEmail: boolean; + + @Column('varchar', { + length: 128, + nullable: true + }) + public email: string | null; + + @Column('boolean', { + default: false, + }) + public smtpSecure: boolean; + + @Column('varchar', { + length: 128, + nullable: true + }) + public smtpHost: string | null; + + @Column('integer', { + nullable: true + }) + public smtpPort: number | null; + + @Column('varchar', { + length: 128, + nullable: true + }) + public smtpUser: string | null; + + @Column('varchar', { + length: 128, + nullable: true + }) + public smtpPass: string | null; + + @Column('boolean', { + default: false, + }) + public enableServiceWorker: boolean; + + @Column('varchar', { + length: 128, + nullable: true + }) + public swPublicKey: string | null; + + @Column('varchar', { + length: 128, + nullable: true + }) + public swPrivateKey: string | null; + + @Column('boolean', { + default: false, + }) + public enableTwitterIntegration: boolean; + + @Column('varchar', { + length: 128, + nullable: true + }) + public twitterConsumerKey: string | null; + + @Column('varchar', { + length: 128, + nullable: true + }) + public twitterConsumerSecret: string | null; + + @Column('boolean', { + default: false, + }) + public enableGithubIntegration: boolean; + + @Column('varchar', { + length: 128, + nullable: true + }) + public githubClientId: string | null; + + @Column('varchar', { + length: 128, + nullable: true + }) + public githubClientSecret: string | null; + + @Column('boolean', { + default: false, + }) + public enableDiscordIntegration: boolean; + + @Column('varchar', { + length: 128, + nullable: true + }) + public discordClientId: string | null; + + @Column('varchar', { + length: 128, + nullable: true + }) + public discordClientSecret: string | null; +} diff --git a/src/models/entities/muting.ts b/src/models/entities/muting.ts new file mode 100644 index 0000000000..0084213bcc --- /dev/null +++ b/src/models/entities/muting.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['muterId', 'muteeId'], { unique: true }) +export class Muting { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Muting.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The mutee user ID.' + }) + public muteeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public mutee: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The muter user ID.' + }) + public muterId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public muter: User | null; +} diff --git a/src/models/entities/note-favorite.ts b/src/models/entities/note-favorite.ts new file mode 100644 index 0000000000..0713c3ae56 --- /dev/null +++ b/src/models/entities/note-favorite.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { Note } from './note'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteFavorite { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the NoteFavorite.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; +} diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts new file mode 100644 index 0000000000..1ce5d841fb --- /dev/null +++ b/src/models/entities/note-reaction.ts @@ -0,0 +1,42 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteReaction { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the NoteReaction.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column('varchar', { + length: 32 + }) + public reaction: string; +} diff --git a/src/models/entities/note-unread.ts b/src/models/entities/note-unread.ts new file mode 100644 index 0000000000..2d18728256 --- /dev/null +++ b/src/models/entities/note-unread.ts @@ -0,0 +1,43 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteUnread { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column({ + ...id(), + comment: '[Denormalized]' + }) + public noteUserId: User['id']; + + /** + * ダイレクト投稿か + */ + @Column('boolean') + public isSpecified: boolean; +} diff --git a/src/models/entities/note-watching.ts b/src/models/entities/note-watching.ts new file mode 100644 index 0000000000..741a1c0c8b --- /dev/null +++ b/src/models/entities/note-watching.ts @@ -0,0 +1,52 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class NoteWatching { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the NoteWatching.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The watcher ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The target Note ID.' + }) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + //#region Denormalized fields + @Index() + @Column({ + ...id(), + comment: '[Denormalized]' + }) + public noteUserId: Note['userId']; + //#endregion +} diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts new file mode 100644 index 0000000000..969363da3b --- /dev/null +++ b/src/models/entities/note.ts @@ -0,0 +1,231 @@ +import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { App } from './app'; +import { DriveFile } from './drive-file'; +import { id } from '../id'; + +@Entity() +export class Note { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Note.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The ID of reply target.' + }) + public replyId: Note['id'] | null; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public reply: Note | null; + + @Index() + @Column({ + ...id(), + nullable: true, + comment: 'The ID of renote target.' + }) + public renoteId: Note['id'] | null; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public renote: Note | null; + + @Column({ + type: 'text', nullable: true + }) + public text: string | null; + + @Column('varchar', { + length: 256, nullable: true + }) + public name: string | null; + + @Column('varchar', { + length: 512, nullable: true + }) + public cw: string | null; + + @Column({ + ...id(), + nullable: true + }) + public appId: App['id'] | null; + + @ManyToOne(type => App, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public app: App | null; + + @Index() + @Column({ + ...id(), + comment: 'The ID of author.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('boolean', { + default: false + }) + public viaMobile: boolean; + + @Column('boolean', { + default: false + }) + public localOnly: boolean; + + @Column('integer', { + default: 0 + }) + public renoteCount: number; + + @Column('integer', { + default: 0 + }) + public repliesCount: number; + + @Column('jsonb', { + default: {} + }) + public reactions: Record<string, number>; + + /** + * public ... 公開 + * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す + * followers ... フォロワーのみ + * specified ... visibleUserIds で指定したユーザーのみ + */ + @Column('enum', { enum: ['public', 'home', 'followers', 'specified'] }) + public visibility: 'public' | 'home' | 'followers' | 'specified'; + + @Index({ unique: true }) + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URI of a note. it will be null when the note is local.' + }) + public uri: string | null; + + @Column('integer', { + default: 0, select: false + }) + public score: number; + + @Column({ + ...id(), + array: true, default: '{}' + }) + public fileIds: DriveFile['id'][]; + + @Column('varchar', { + length: 256, array: true, default: '{}' + }) + public attachedFileTypes: string[]; + + @Index() + @Column({ + ...id(), + array: true, default: '{}' + }) + public visibleUserIds: User['id'][]; + + @Index() + @Column({ + ...id(), + array: true, default: '{}' + }) + public mentions: User['id'][]; + + @Column('text', { + default: '[]' + }) + public mentionedRemoteUsers: string; + + @Column('varchar', { + length: 128, array: true, default: '{}' + }) + public emojis: string[]; + + @Index() + @Column('varchar', { + length: 128, array: true, default: '{}' + }) + public tags: string[]; + + @Column('boolean', { + default: false + }) + public hasPoll: boolean; + + @Column('jsonb', { + nullable: true, default: null + }) + public geo: any | null; + + //#region Denormalized fields + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public userHost: string | null; + + @Column({ + ...id(), + nullable: true, + comment: '[Denormalized]' + }) + public replyUserId: User['id'] | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public replyUserHost: string | null; + + @Column({ + ...id(), + nullable: true, + comment: '[Denormalized]' + }) + public renoteUserId: User['id'] | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public renoteUserHost: string | null; + //#endregion + + constructor(data: Partial<Note>) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} + +export type IMentionedRemoteUsers = { + uri: string; + username: string; + host: string; +}[]; diff --git a/src/models/entities/notification.ts b/src/models/entities/notification.ts new file mode 100644 index 0000000000..627a57bece --- /dev/null +++ b/src/models/entities/notification.ts @@ -0,0 +1,94 @@ +import { Entity, Index, JoinColumn, ManyToOne, Column, PrimaryColumn } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; +import { Note } from './note'; + +@Entity() +export class Notification { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the Notification.' + }) + public createdAt: Date; + + /** + * 通知の受信者 + */ + @Index() + @Column({ + ...id(), + comment: 'The ID of recipient user of the Notification.' + }) + public notifieeId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public notifiee: User | null; + + /** + * 通知の送信者(initiator) + */ + @Column({ + ...id(), + comment: 'The ID of sender user of the Notification.' + }) + public notifierId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public notifier: User | null; + + /** + * 通知の種類。 + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - (自分または自分がWatchしている)投稿が返信された + * renote - (自分または自分がWatchしている)投稿がRenoteされた + * quote - (自分または自分がWatchしている)投稿が引用Renoteされた + * reaction - (自分または自分がWatchしている)投稿にリアクションされた + * pollVote - (自分または自分がWatchしている)投稿の投票に投票された + */ + @Column('varchar', { + length: 32, + comment: 'The type of the Notification.' + }) + public type: string; + + /** + * 通知が読まれたかどうか + */ + @Column('boolean', { + default: false, + comment: 'Whether the Notification is read.' + }) + public isRead: boolean; + + @Column({ + ...id(), + nullable: true + }) + public noteId: Note['id'] | null; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column('varchar', { + length: 128, nullable: true + }) + public reaction: string; + + @Column('integer', { + nullable: true + }) + public choice: number; +} diff --git a/src/models/entities/poll-vote.ts b/src/models/entities/poll-vote.ts new file mode 100644 index 0000000000..709376f909 --- /dev/null +++ b/src/models/entities/poll-vote.ts @@ -0,0 +1,40 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId', 'choice'], { unique: true }) +export class PollVote { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the PollVote.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column('integer') + public choice: number; +} diff --git a/src/models/entities/poll.ts b/src/models/entities/poll.ts new file mode 100644 index 0000000000..6bb67163a2 --- /dev/null +++ b/src/models/entities/poll.ts @@ -0,0 +1,71 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; +import { id } from '../id'; +import { Note } from './note'; +import { User } from './user'; + +@Entity() +export class Poll { + @PrimaryColumn(id()) + public noteId: Note['id']; + + @OneToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; + + @Column('timestamp with time zone', { + nullable: true + }) + public expiresAt: Date | null; + + @Column('boolean') + public multiple: boolean; + + @Column('varchar', { + length: 128, array: true, default: '{}' + }) + public choices: string[]; + + @Column('integer', { + array: true, + }) + public votes: number[]; + + //#region Denormalized fields + @Column('enum', { + enum: ['public', 'home', 'followers', 'specified'], + comment: '[Denormalized]' + }) + public noteVisibility: 'public' | 'home' | 'followers' | 'specified'; + + @Index() + @Column({ + ...id(), + comment: '[Denormalized]' + }) + public userId: User['id']; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public userHost: string | null; + //#endregion + + constructor(data: Partial<Poll>) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} + +export type IPoll = { + choices: string[]; + votes?: number[]; + multiple: boolean; + expiresAt: Date | null; +}; diff --git a/src/models/entities/registration-tickets.ts b/src/models/entities/registration-tickets.ts new file mode 100644 index 0000000000..d962f78a78 --- /dev/null +++ b/src/models/entities/registration-tickets.ts @@ -0,0 +1,17 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class RegistrationTicket { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 64, + }) + public code: string; +} diff --git a/src/models/entities/signin.ts b/src/models/entities/signin.ts new file mode 100644 index 0000000000..7e047084b1 --- /dev/null +++ b/src/models/entities/signin.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class Signin { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the Signin.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + }) + public ip: string; + + @Column('jsonb') + public headers: Record<string, any>; + + @Column('boolean') + public success: boolean; +} diff --git a/src/models/entities/sw-subscription.ts b/src/models/entities/sw-subscription.ts new file mode 100644 index 0000000000..7c3f6f0a6c --- /dev/null +++ b/src/models/entities/sw-subscription.ts @@ -0,0 +1,37 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class SwSubscription { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 512, + }) + public endpoint: string; + + @Column('varchar', { + length: 256, + }) + public auth: string; + + @Column('varchar', { + length: 128, + }) + public publickey: string; +} diff --git a/src/models/entities/user-keypair.ts b/src/models/entities/user-keypair.ts new file mode 100644 index 0000000000..603321d758 --- /dev/null +++ b/src/models/entities/user-keypair.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class UserKeypair { + @PrimaryColumn(id()) + public userId: User['id']; + + @OneToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 4096, + }) + public publicKey: string; + + @Column('varchar', { + length: 4096, + }) + public privateKey: string; + + constructor(data: Partial<UserKeypair>) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/src/models/entities/user-list-joining.ts b/src/models/entities/user-list-joining.ts new file mode 100644 index 0000000000..8af4efb6a7 --- /dev/null +++ b/src/models/entities/user-list-joining.ts @@ -0,0 +1,41 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { UserList } from './user-list'; +import { id } from '../id'; + +@Entity() +export class UserListJoining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserListJoining.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The user ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column({ + ...id(), + comment: 'The list ID.' + }) + public userListId: UserList['id']; + + @ManyToOne(type => UserList, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public userList: UserList | null; +} diff --git a/src/models/entities/user-list.ts b/src/models/entities/user-list.ts new file mode 100644 index 0000000000..35a83ef8c3 --- /dev/null +++ b/src/models/entities/user-list.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class UserList { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserList.' + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + comment: 'The owner ID.' + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, + comment: 'The name of the UserList.' + }) + public name: string; +} diff --git a/src/models/entities/user-note-pinings.ts b/src/models/entities/user-note-pinings.ts new file mode 100644 index 0000000000..04a6f8f645 --- /dev/null +++ b/src/models/entities/user-note-pinings.ts @@ -0,0 +1,35 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { Note } from './note'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'noteId'], { unique: true }) +export class UserNotePining { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + comment: 'The created date of the UserNotePinings.' + }) + public createdAt: Date; + + @Index() + @Column(id()) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column(id()) + public noteId: Note['id']; + + @ManyToOne(type => Note, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public note: Note | null; +} diff --git a/src/models/entities/user-profile.ts b/src/models/entities/user-profile.ts new file mode 100644 index 0000000000..a2d7b8d2c2 --- /dev/null +++ b/src/models/entities/user-profile.ts @@ -0,0 +1,209 @@ +import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { id } from '../id'; +import { User } from './user'; + +@Entity() +export class UserProfile { + @PrimaryColumn(id()) + public userId: User['id']; + + @OneToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The location of the User.' + }) + public location: string | null; + + @Column('char', { + length: 10, nullable: true, + comment: 'The birthday (YYYY-MM-DD) of the User.' + }) + public birthday: string | null; + + @Column('varchar', { + length: 1024, nullable: true, + comment: 'The description (bio) of the User.' + }) + public description: string | null; + + @Column('jsonb', { + default: [], + }) + public fields: { + name: string; + value: string; + }[]; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'Remote URL of the user.' + }) + public url: string | null; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The email address of the User.' + }) + public email: string | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public emailVerifyCode: string | null; + + @Column('boolean', { + default: false, + }) + public emailVerified: boolean; + + @Column('varchar', { + length: 128, nullable: true, + }) + public twoFactorTempSecret: string | null; + + @Column('varchar', { + length: 128, nullable: true, + }) + public twoFactorSecret: string | null; + + @Column('boolean', { + default: false, + }) + public twoFactorEnabled: boolean; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The password hash of the User. It will be null if the origin of the user is local.' + }) + public password: string | null; + + @Column('jsonb', { + default: {}, + comment: 'The client-specific data of the User.' + }) + public clientData: Record<string, any>; + + @Column('boolean', { + default: false, + }) + public autoWatch: boolean; + + @Column('boolean', { + default: false, + }) + public autoAcceptFollowed: boolean; + + @Column('boolean', { + default: false, + }) + public alwaysMarkNsfw: boolean; + + @Column('boolean', { + default: false, + }) + public carefulBot: boolean; + + //#region Linking + @Column('boolean', { + default: false, + }) + public twitter: boolean; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public twitterAccessToken: string | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public twitterAccessTokenSecret: string | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public twitterUserId: string | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public twitterScreenName: string | null; + + @Column('boolean', { + default: false, + }) + public github: boolean; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public githubAccessToken: string | null; + + @Column('integer', { + nullable: true, default: null, + }) + public githubId: number | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public githubLogin: string | null; + + @Column('boolean', { + default: false, + }) + public discord: boolean; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public discordAccessToken: string | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public discordRefreshToken: string | null; + + @Column('integer', { + nullable: true, default: null, + }) + public discordExpiresDate: number | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public discordId: string | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public discordUsername: string | null; + + @Column('varchar', { + length: 64, nullable: true, default: null, + }) + public discordDiscriminator: string | null; + //#endregion + + //#region Denormalized fields + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: '[Denormalized]' + }) + public userHost: string | null; + //#endregion + + constructor(data: Partial<UserProfile>) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/src/models/entities/user-publickey.ts b/src/models/entities/user-publickey.ts new file mode 100644 index 0000000000..21edc3e9e2 --- /dev/null +++ b/src/models/entities/user-publickey.ts @@ -0,0 +1,34 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm'; +import { User } from './user'; +import { id } from '../id'; + +@Entity() +export class UserPublickey { + @PrimaryColumn(id()) + public userId: User['id']; + + @OneToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index({ unique: true }) + @Column('varchar', { + length: 256, + }) + public keyId: string; + + @Column('varchar', { + length: 4096, + }) + public keyPem: string; + + constructor(data: Partial<UserPublickey>) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts new file mode 100644 index 0000000000..e40c32a76f --- /dev/null +++ b/src/models/entities/user.ts @@ -0,0 +1,224 @@ +import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { DriveFile } from './drive-file'; +import { id } from '../id'; + +@Entity() +@Index(['usernameLower', 'host'], { unique: true }) +export class User { + @PrimaryColumn(id()) + public id: string; + + @Index() + @Column('timestamp with time zone', { + comment: 'The created date of the User.' + }) + public createdAt: Date; + + @Index() + @Column('timestamp with time zone', { + nullable: true, + comment: 'The updated date of the User.' + }) + public updatedAt: Date | null; + + @Column('timestamp with time zone', { + nullable: true + }) + public lastFetchedAt: Date | null; + + @Column('varchar', { + length: 128, + comment: 'The username of the User.' + }) + public username: string; + + @Index() + @Column('varchar', { + length: 128, select: false, + comment: 'The username (lowercased) of the User.' + }) + public usernameLower: string; + + @Column('varchar', { + length: 128, nullable: true, + comment: 'The name of the User.' + }) + public name: string | null; + + @Column('integer', { + default: 0, + comment: 'The count of followers.' + }) + public followersCount: number; + + @Column('integer', { + default: 0, + comment: 'The count of following.' + }) + public followingCount: number; + + @Column('integer', { + default: 0, + comment: 'The count of notes.' + }) + public notesCount: number; + + @Column({ + ...id(), + nullable: true, + comment: 'The ID of avatar DriveFile.' + }) + public avatarId: DriveFile['id'] | null; + + @OneToOne(type => DriveFile, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public avatar: DriveFile | null; + + @Column({ + ...id(), + nullable: true, + comment: 'The ID of banner DriveFile.' + }) + public bannerId: DriveFile['id'] | null; + + @OneToOne(type => DriveFile, { + onDelete: 'SET NULL' + }) + @JoinColumn() + public banner: DriveFile | null; + + @Index() + @Column('varchar', { + length: 128, array: true, default: '{}' + }) + public tags: string[]; + + @Column('varchar', { + length: 512, nullable: true, + }) + public avatarUrl: string | null; + + @Column('varchar', { + length: 512, nullable: true, + }) + public bannerUrl: string | null; + + @Column('varchar', { + length: 32, nullable: true, + }) + public avatarColor: string | null; + + @Column('varchar', { + length: 32, nullable: true, + }) + public bannerColor: string | null; + + @Column('boolean', { + default: false, + comment: 'Whether the User is suspended.' + }) + public isSuspended: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether the User is silenced.' + }) + public isSilenced: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether the User is locked.' + }) + public isLocked: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether the User is a bot.' + }) + public isBot: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether the User is a cat.' + }) + public isCat: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether the User is the admin.' + }) + public isAdmin: boolean; + + @Column('boolean', { + default: false, + comment: 'Whether the User is a moderator.' + }) + public isModerator: boolean; + + @Column('boolean', { + default: false, + }) + public isVerified: boolean; + + @Column('varchar', { + length: 128, array: true, default: '{}' + }) + public emojis: string[]; + + @Index() + @Column('varchar', { + length: 128, nullable: true, + comment: 'The host of the User. It will be null if the origin of the user is local.' + }) + public host: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The inbox URL of the User. It will be null if the origin of the user is local.' + }) + public inbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.' + }) + public sharedInbox: string | null; + + @Column('varchar', { + length: 512, nullable: true, + comment: 'The featured URL of the User. It will be null if the origin of the user is local.' + }) + public featured: string | null; + + @Index() + @Column('varchar', { + length: 512, nullable: true, + comment: 'The URI of the User. It will be null if the origin of the user is local.' + }) + public uri: string | null; + + @Index({ unique: true }) + @Column('char', { + length: 16, nullable: true, unique: true, + comment: 'The native access token of the User. It will be null if the origin of the user is local.' + }) + public token: string | null; + + constructor(data: Partial<User>) { + if (data == null) return; + + for (const [k, v] of Object.entries(data)) { + (this as any)[k] = v; + } + } +} + +export interface ILocalUser extends User { + host: null; +} + +export interface IRemoteUser extends User { + host: string; +} diff --git a/src/models/favorite.ts b/src/models/favorite.ts deleted file mode 100644 index 2008edbfaf..0000000000 --- a/src/models/favorite.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packNote } from './note'; -import { dbLogger } from '../db/logger'; - -const Favorite = db.get<IFavorite>('favorites'); -Favorite.createIndex('userId'); -Favorite.createIndex(['userId', 'noteId'], { unique: true }); -export default Favorite; - -export type IFavorite = { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - noteId: mongo.ObjectID; -}; - -export const packMany = ( - favorites: any[], - me: any -) => { - return Promise.all(favorites.map(f => pack(f, me))); -}; - -/** - * Pack a favorite for API response - */ -export const pack = ( - favorite: any, - me: any -) => new Promise<any>(async (resolve, reject) => { - let _favorite: any; - - // Populate the favorite if 'favorite' is ID - if (isObjectId(favorite)) { - _favorite = await Favorite.findOne({ - _id: favorite - }); - } else if (typeof favorite === 'string') { - _favorite = await Favorite.findOne({ - _id: new mongo.ObjectID(favorite) - }); - } else { - _favorite = deepcopy(favorite); - } - - // Rename _id to id - _favorite.id = _favorite._id; - delete _favorite._id; - - // Populate note - _favorite.note = await packNote(_favorite.noteId, me, { - detail: true - }); - - // (データベースの不具合などで)投稿が見つからなかったら - if (_favorite.note == null) { - dbLogger.warn(`[DAMAGED DB] (missing) pkg: favorite -> note :: ${_favorite.id} (note ${_favorite.noteId})`); - return resolve(null); - } - - resolve(_favorite); -}); diff --git a/src/models/follow-request.ts b/src/models/follow-request.ts deleted file mode 100644 index 4f75c63a32..0000000000 --- a/src/models/follow-request.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packUser } from './user'; - -const FollowRequest = db.get<IFollowRequest>('followRequests'); -FollowRequest.createIndex('followerId'); -FollowRequest.createIndex('followeeId'); -FollowRequest.createIndex(['followerId', 'followeeId'], { unique: true }); -export default FollowRequest; - -export type IFollowRequest = { - _id: mongo.ObjectID; - createdAt: Date; - followeeId: mongo.ObjectID; - followerId: mongo.ObjectID; - requestId?: string; // id of Follow Activity - - // 非正規化 - _followee: { - host: string; - inbox?: string; - sharedInbox?: string; - }, - _follower: { - host: string; - inbox?: string; - sharedInbox?: string; - } -}; - -/** - * Pack a request for API response - */ -export const pack = ( - request: any, - me?: any -) => new Promise<any>(async (resolve, reject) => { - let _request: any; - - // Populate the request if 'request' is ID - if (isObjectId(request)) { - _request = await FollowRequest.findOne({ - _id: request - }); - } else if (typeof request === 'string') { - _request = await FollowRequest.findOne({ - _id: new mongo.ObjectID(request) - }); - } else { - _request = deepcopy(request); - } - - // Rename _id to id - _request.id = _request._id; - delete _request._id; - - // Populate follower - _request.follower = await packUser(_request.followerId, me); - - // Populate followee - _request.followee = await packUser(_request.followeeId, me); - - resolve(_request); -}); diff --git a/src/models/following.ts b/src/models/following.ts deleted file mode 100644 index 12cc27211b..0000000000 --- a/src/models/following.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Following = db.get<IFollowing>('following'); -Following.createIndex('followerId'); -Following.createIndex('followeeId'); -Following.createIndex(['followerId', 'followeeId'], { unique: true }); -export default Following; - -export type IFollowing = { - _id: mongo.ObjectID; - createdAt: Date; - followeeId: mongo.ObjectID; - followerId: mongo.ObjectID; - - // 非正規化 - _followee: { - host: string; - inbox?: string; - sharedInbox?: string; - }, - _follower: { - host: string; - inbox?: string; - sharedInbox?: string; - } -}; diff --git a/src/models/games/reversi/game.ts b/src/models/games/reversi/game.ts deleted file mode 100644 index 57c493cff5..0000000000 --- a/src/models/games/reversi/game.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../../../db/mongodb'; -import isObjectId from '../../../misc/is-objectid'; -import { IUser, pack as packUser } from '../../user'; - -const ReversiGame = db.get<IReversiGame>('reversiGames'); -export default ReversiGame; - -export interface IReversiGame { - _id: mongo.ObjectID; - createdAt: Date; - startedAt: Date; - user1Id: mongo.ObjectID; - user2Id: mongo.ObjectID; - user1Accepted: boolean; - user2Accepted: boolean; - - /** - * どちらのプレイヤーが先行(黒)か - * 1 ... user1 - * 2 ... user2 - */ - black: number; - - isStarted: boolean; - isEnded: boolean; - winnerId: mongo.ObjectID; - surrendered: mongo.ObjectID; - logs: { - at: Date; - color: boolean; - pos: number; - }[]; - settings: { - map: string[]; - bw: string | number; - isLlotheo: boolean; - canPutEverywhere: boolean; - loopedBoard: boolean; - }; - form1: any; - form2: any; - - // ログのposを文字列としてすべて連結したもののCRC32値 - crc32: string; -} - -/** - * Pack an reversi game for API response - */ -export const pack = ( - game: any, - me?: string | mongo.ObjectID | IUser, - options?: { - detail?: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: true - }, options); - - let _game: any; - - // Populate the game if 'game' is ID - if (isObjectId(game)) { - _game = await ReversiGame.findOne({ - _id: game - }); - } else if (typeof game === 'string') { - _game = await ReversiGame.findOne({ - _id: new mongo.ObjectID(game) - }); - } else { - _game = deepcopy(game); - } - - // Me - const meId: mongo.ObjectID = me - ? isObjectId(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - // Rename _id to id - _game.id = _game._id; - delete _game._id; - - if (opts.detail === false) { - delete _game.logs; - delete _game.settings.map; - } else { - // 互換性のため - if (_game.settings.map.hasOwnProperty('size')) { - _game.settings.map = _game.settings.map.data.match(new RegExp(`.{1,${_game.settings.map.size}}`, 'g')); - } - } - - // Populate user - _game.user1 = await packUser(_game.user1Id, meId); - _game.user2 = await packUser(_game.user2Id, meId); - if (_game.winnerId) { - _game.winner = await packUser(_game.winnerId, meId); - } else { - _game.winner = null; - } - - resolve(_game); -}); diff --git a/src/models/games/reversi/matching.ts b/src/models/games/reversi/matching.ts deleted file mode 100644 index ba2ac1bc05..0000000000 --- a/src/models/games/reversi/matching.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../../../db/mongodb'; -import isObjectId from '../../../misc/is-objectid'; -import { IUser, pack as packUser } from '../../user'; - -const Matching = db.get<IMatching>('reversiMatchings'); -export default Matching; - -export interface IMatching { - _id: mongo.ObjectID; - createdAt: Date; - parentId: mongo.ObjectID; - childId: mongo.ObjectID; -} - -/** - * Pack an reversi matching for API response - */ -export const pack = ( - matching: any, - me?: string | mongo.ObjectID | IUser -) => new Promise<any>(async (resolve, reject) => { - - // Me - const meId: mongo.ObjectID = me - ? isObjectId(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - const _matching = deepcopy(matching); - - // Rename _id to id - _matching.id = _matching._id; - delete _matching._id; - - // Populate user - _matching.parent = await packUser(_matching.parentId, meId); - _matching.child = await packUser(_matching.childId, meId); - - resolve(_matching); -}); diff --git a/src/models/hashtag.ts b/src/models/hashtag.ts deleted file mode 100644 index c1de42086e..0000000000 --- a/src/models/hashtag.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Hashtag = db.get<IHashtags>('hashtags'); -Hashtag.createIndex('tag', { unique: true }); -Hashtag.createIndex('mentionedUsersCount'); -Hashtag.createIndex('mentionedLocalUsersCount'); -Hashtag.createIndex('mentionedRemoteUsersCount'); -Hashtag.createIndex('attachedUsersCount'); -Hashtag.createIndex('attachedLocalUsersCount'); -Hashtag.createIndex('attachedRemoteUsersCount'); -export default Hashtag; - -// 後方互換性のため -Hashtag.findOne({ attachedUserIds: { $exists: false }}).then(h => { - if (h != null) { - Hashtag.update({}, { - $rename: { - mentionedUserIdsCount: 'mentionedUsersCount' - }, - $set: { - mentionedLocalUserIds: [], - mentionedLocalUsersCount: 0, - attachedUserIds: [], - attachedUsersCount: 0, - attachedLocalUserIds: [], - attachedLocalUsersCount: 0, - } - }, { - multi: true - }); - } -}); -Hashtag.findOne({ attachedRemoteUserIds: { $exists: false }}).then(h => { - if (h != null) { - Hashtag.update({}, { - $set: { - mentionedRemoteUserIds: [], - mentionedRemoteUsersCount: 0, - attachedRemoteUserIds: [], - attachedRemoteUsersCount: 0, - } - }, { - multi: true - }); - } -}); - -export interface IHashtags { - tag: string; - mentionedUserIds: mongo.ObjectID[]; - mentionedUsersCount: number; - mentionedLocalUserIds: mongo.ObjectID[]; - mentionedLocalUsersCount: number; - mentionedRemoteUserIds: mongo.ObjectID[]; - mentionedRemoteUsersCount: number; - attachedUserIds: mongo.ObjectID[]; - attachedUsersCount: number; - attachedLocalUserIds: mongo.ObjectID[]; - attachedLocalUsersCount: number; - attachedRemoteUserIds: mongo.ObjectID[]; - attachedRemoteUsersCount: number; -} diff --git a/src/models/id.ts b/src/models/id.ts new file mode 100644 index 0000000000..be2cccfe3b --- /dev/null +++ b/src/models/id.ts @@ -0,0 +1,4 @@ +export const id = () => ({ + type: 'varchar' as 'varchar', + length: 32 +}); diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000000..d66e4e710a --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,74 @@ +import { getRepository, getCustomRepository } from 'typeorm'; +import { Instance } from './entities/instance'; +import { Emoji } from './entities/emoji'; +import { Poll } from './entities/poll'; +import { PollVote } from './entities/poll-vote'; +import { Meta } from './entities/meta'; +import { SwSubscription } from './entities/sw-subscription'; +import { NoteWatching } from './entities/note-watching'; +import { UserListJoining } from './entities/user-list-joining'; +import { Hashtag } from './entities/hashtag'; +import { NoteUnread } from './entities/note-unread'; +import { RegistrationTicket } from './entities/registration-tickets'; +import { UserRepository } from './repositories/user'; +import { NoteRepository } from './repositories/note'; +import { DriveFileRepository } from './repositories/drive-file'; +import { DriveFolderRepository } from './repositories/drive-folder'; +import { Log } from './entities/log'; +import { AccessToken } from './entities/access-token'; +import { UserNotePining } from './entities/user-note-pinings'; +import { SigninRepository } from './repositories/signin'; +import { MessagingMessageRepository } from './repositories/messaging-message'; +import { ReversiGameRepository } from './repositories/games/reversi/game'; +import { UserListRepository } from './repositories/user-list'; +import { FollowRequestRepository } from './repositories/follow-request'; +import { MutingRepository } from './repositories/muting'; +import { BlockingRepository } from './repositories/blocking'; +import { NoteReactionRepository } from './repositories/note-reaction'; +import { NotificationRepository } from './repositories/notification'; +import { NoteFavoriteRepository } from './repositories/note-favorite'; +import { ReversiMatchingRepository } from './repositories/games/reversi/matching'; +import { UserPublickey } from './entities/user-publickey'; +import { UserKeypair } from './entities/user-keypair'; +import { AppRepository } from './repositories/app'; +import { FollowingRepository } from './repositories/following'; +import { AbuseUserReportRepository } from './repositories/abuse-user-report'; +import { AuthSessionRepository } from './repositories/auth-session'; +import { UserProfile } from './entities/user-profile'; + +export const Apps = getCustomRepository(AppRepository); +export const Notes = getCustomRepository(NoteRepository); +export const NoteFavorites = getCustomRepository(NoteFavoriteRepository); +export const NoteWatchings = getRepository(NoteWatching); +export const NoteReactions = getCustomRepository(NoteReactionRepository); +export const NoteUnreads = getRepository(NoteUnread); +export const Polls = getRepository(Poll); +export const PollVotes = getRepository(PollVote); +export const Users = getCustomRepository(UserRepository); +export const UserProfiles = getRepository(UserProfile); +export const UserKeypairs = getRepository(UserKeypair); +export const UserPublickeys = getRepository(UserPublickey); +export const UserLists = getCustomRepository(UserListRepository); +export const UserListJoinings = getRepository(UserListJoining); +export const UserNotePinings = getRepository(UserNotePining); +export const Followings = getCustomRepository(FollowingRepository); +export const FollowRequests = getCustomRepository(FollowRequestRepository); +export const Instances = getRepository(Instance); +export const Emojis = getRepository(Emoji); +export const DriveFiles = getCustomRepository(DriveFileRepository); +export const DriveFolders = getCustomRepository(DriveFolderRepository); +export const Notifications = getCustomRepository(NotificationRepository); +export const Metas = getRepository(Meta); +export const Mutings = getCustomRepository(MutingRepository); +export const Blockings = getCustomRepository(BlockingRepository); +export const SwSubscriptions = getRepository(SwSubscription); +export const Hashtags = getRepository(Hashtag); +export const AbuseUserReports = getCustomRepository(AbuseUserReportRepository); +export const RegistrationTickets = getRepository(RegistrationTicket); +export const AuthSessions = getCustomRepository(AuthSessionRepository); +export const AccessTokens = getRepository(AccessToken); +export const Signins = getCustomRepository(SigninRepository); +export const MessagingMessages = getCustomRepository(MessagingMessageRepository); +export const ReversiGames = getCustomRepository(ReversiGameRepository); +export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository); +export const Logs = getRepository(Log); diff --git a/src/models/instance.ts b/src/models/instance.ts deleted file mode 100644 index cdce570a4b..0000000000 --- a/src/models/instance.ts +++ /dev/null @@ -1,90 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Instance = db.get<IInstance>('instances'); -Instance.createIndex('host', { unique: true }); -export default Instance; - -export interface IInstance { - _id: mongo.ObjectID; - - /** - * ホスト - */ - host: string; - - /** - * このインスタンスを捕捉した日時 - */ - caughtAt: Date; - - /** - * このインスタンスのシステム (MastodonとかMisskeyとかPleromaとか) - */ - system: string; - - /** - * このインスタンスのユーザー数 - */ - usersCount: number; - - /** - * このインスタンスから受け取った投稿数 - */ - notesCount: number; - - /** - * このインスタンスのユーザーからフォローされている、自インスタンスのユーザーの数 - */ - followingCount: number; - - /** - * このインスタンスのユーザーをフォローしている、自インスタンスのユーザーの数 - */ - followersCount: number; - - /** - * ドライブ使用量 - */ - driveUsage: number; - - /** - * ドライブのファイル数 - */ - driveFiles: number; - - /** - * 直近のリクエスト送信日時 - */ - latestRequestSentAt?: Date; - - /** - * 直近のリクエスト送信時のHTTPステータスコード - */ - latestStatus?: number; - - /** - * 直近のリクエスト受信日時 - */ - latestRequestReceivedAt?: Date; - - /** - * このインスタンスと不通かどうか - */ - isNotResponding: boolean; - - /** - * このインスタンスと最後にやり取りした日時 - */ - lastCommunicatedAt: Date; - - /** - * このインスタンスをブロックしているか - */ - isBlocked: boolean; - - /** - * このインスタンスが閉鎖済みとしてマークされているか - */ - isMarkedAsClosed: boolean; -} diff --git a/src/models/log.ts b/src/models/log.ts deleted file mode 100644 index 6f79e83c78..0000000000 --- a/src/models/log.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const Log = db.get<ILog>('logs'); -Log.createIndex('createdAt', { expireAfterSeconds: 3600 * 24 * 3 }); -Log.createIndex('level'); -Log.createIndex('domain'); -export default Log; - -export interface ILog { - _id: mongo.ObjectID; - createdAt: Date; - machine: string; - worker: string; - domain: string[]; - level: string; - message: string; - data: any; -} diff --git a/src/models/messaging-message.ts b/src/models/messaging-message.ts deleted file mode 100644 index 67abb4d111..0000000000 --- a/src/models/messaging-message.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import { pack as packUser } from './user'; -import { pack as packFile } from './drive-file'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { length } from 'stringz'; - -const MessagingMessage = db.get<IMessagingMessage>('messagingMessages'); -MessagingMessage.createIndex('userId'); -MessagingMessage.createIndex('recipientId'); -export default MessagingMessage; - -export interface IMessagingMessage { - _id: mongo.ObjectID; - createdAt: Date; - text: string; - userId: mongo.ObjectID; - recipientId: mongo.ObjectID; - isRead: boolean; - fileId: mongo.ObjectID; -} - -export function isValidText(text: string): boolean { - return length(text.trim()) <= 1000 && text.trim() != ''; -} - -/** - * Pack a messaging message for API response - */ -export const pack = ( - message: any, - me?: any, - options?: { - populateRecipient: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = options || { - populateRecipient: true - }; - - let _message: any; - - // Populate the message if 'message' is ID - if (isObjectId(message)) { - _message = await MessagingMessage.findOne({ - _id: message - }); - } else if (typeof message === 'string') { - _message = await MessagingMessage.findOne({ - _id: new mongo.ObjectID(message) - }); - } else { - _message = deepcopy(message); - } - - // Rename _id to id - _message.id = _message._id; - delete _message._id; - - // Populate user - _message.user = await packUser(_message.userId, me); - - if (_message.fileId) { - // Populate file - _message.file = await packFile(_message.fileId); - } - - if (opts.populateRecipient) { - // Populate recipient - _message.recipient = await packUser(_message.recipientId, me); - } - - resolve(_message); -}); diff --git a/src/models/meta.ts b/src/models/meta.ts deleted file mode 100644 index 5ca0f01236..0000000000 --- a/src/models/meta.ts +++ /dev/null @@ -1,257 +0,0 @@ -import db from '../db/mongodb'; -import config from '../config'; -import User from './user'; -import { transform } from '../misc/cafy-id'; - -const Meta = db.get<IMeta>('meta'); -export default Meta; - -// 後方互換性のため。 -// 過去のMisskeyではインスタンス名や紹介を設定ファイルに記述していたのでそれを移行 -if ((config as any).name) { - Meta.findOne({}).then(m => { - if (m != null && m.name == null) { - Meta.update({}, { - $set: { - name: (config as any).name - } - }); - } - }); -} -if ((config as any).description) { - Meta.findOne({}).then(m => { - if (m != null && m.description == null) { - Meta.update({}, { - $set: { - description: (config as any).description - } - }); - } - }); -} -if ((config as any).localDriveCapacityMb) { - Meta.findOne({}).then(m => { - if (m != null && m.localDriveCapacityMb == null) { - Meta.update({}, { - $set: { - localDriveCapacityMb: (config as any).localDriveCapacityMb - } - }); - } - }); -} -if ((config as any).remoteDriveCapacityMb) { - Meta.findOne({}).then(m => { - if (m != null && m.remoteDriveCapacityMb == null) { - Meta.update({}, { - $set: { - remoteDriveCapacityMb: (config as any).remoteDriveCapacityMb - } - }); - } - }); -} -if ((config as any).preventCacheRemoteFiles) { - Meta.findOne({}).then(m => { - if (m != null && m.cacheRemoteFiles == null) { - Meta.update({}, { - $set: { - cacheRemoteFiles: !(config as any).preventCacheRemoteFiles - } - }); - } - }); -} -if ((config as any).recaptcha) { - Meta.findOne({}).then(m => { - if (m != null && m.enableRecaptcha == null) { - Meta.update({}, { - $set: { - enableRecaptcha: (config as any).recaptcha != null, - recaptchaSiteKey: (config as any).recaptcha.site_key, - recaptchaSecretKey: (config as any).recaptcha.secret_key, - } - }); - } - }); -} -if ((config as any).ghost) { - Meta.findOne({}).then(async m => { - if (m != null && m.proxyAccount == null) { - const account = await User.findOne({ _id: transform((config as any).ghost) }); - Meta.update({}, { - $set: { - proxyAccount: account.username - } - }); - } - }); -} -if ((config as any).maintainer) { - Meta.findOne({}).then(m => { - if (m != null && m.maintainer == null) { - Meta.update({}, { - $set: { - maintainer: (config as any).maintainer - } - }); - } - }); -} -if ((config as any).twitter) { - Meta.findOne({}).then(m => { - if (m != null && m.enableTwitterIntegration == null) { - Meta.update({}, { - $set: { - enableTwitterIntegration: true, - twitterConsumerKey: (config as any).twitter.consumer_key, - twitterConsumerSecret: (config as any).twitter.consumer_secret - } - }); - } - }); -} -if ((config as any).github) { - Meta.findOne({}).then(m => { - if (m != null && m.enableGithubIntegration == null) { - Meta.update({}, { - $set: { - enableGithubIntegration: true, - githubClientId: (config as any).github.client_id, - githubClientSecret: (config as any).github.client_secret - } - }); - } - }); -} -if ((config as any).user_recommendation) { - Meta.findOne({}).then(m => { - if (m != null && m.enableExternalUserRecommendation == null) { - Meta.update({}, { - $set: { - enableExternalUserRecommendation: true, - externalUserRecommendationEngine: (config as any).user_recommendation.engine, - externalUserRecommendationTimeout: (config as any).user_recommendation.timeout - } - }); - } - }); -} -if ((config as any).sw) { - Meta.findOne({}).then(m => { - if (m != null && m.enableServiceWorker == null) { - Meta.update({}, { - $set: { - enableServiceWorker: true, - swPublicKey: (config as any).sw.public_key, - swPrivateKey: (config as any).sw.private_key - } - }); - } - }); -} -Meta.findOne({}).then(m => { - if (m != null && (m as any).broadcasts != null) { - Meta.update({}, { - $rename: { - broadcasts: 'announcements' - } - }); - } -}); - -export type IMeta = { - name?: string; - description?: string; - - /** - * メンテナ情報 - */ - maintainer: { - /** - * メンテナの名前 - */ - name: string; - - /** - * メンテナの連絡先 - */ - email?: string; - }; - - langs?: string[]; - - announcements?: any[]; - - stats?: { - notesCount: number; - originalNotesCount: number; - usersCount: number; - originalUsersCount: number; - }; - - disableRegistration?: boolean; - disableLocalTimeline?: boolean; - disableGlobalTimeline?: boolean; - enableEmojiReaction?: boolean; - useStarForReactionFallback?: boolean; - hidedTags?: string[]; - mascotImageUrl?: string; - bannerUrl?: string; - errorImageUrl?: string; - iconUrl?: string; - - cacheRemoteFiles?: boolean; - - proxyAccount?: string; - - enableRecaptcha?: boolean; - recaptchaSiteKey?: string; - recaptchaSecretKey?: string; - - /** - * Drive capacity of a local user (MB) - */ - localDriveCapacityMb?: number; - - /** - * Drive capacity of a remote user (MB) - */ - remoteDriveCapacityMb?: number; - - /** - * Max allowed note text length in characters - */ - maxNoteTextLength?: number; - - summalyProxy?: string; - - enableTwitterIntegration?: boolean; - twitterConsumerKey?: string; - twitterConsumerSecret?: string; - - enableGithubIntegration?: boolean; - githubClientId?: string; - githubClientSecret?: string; - - enableDiscordIntegration?: boolean; - discordClientId?: string; - discordClientSecret?: string; - - enableExternalUserRecommendation?: boolean; - externalUserRecommendationEngine?: string; - externalUserRecommendationTimeout?: number; - - enableEmail?: boolean; - email?: string; - smtpSecure?: boolean; - smtpHost?: string; - smtpPort?: number; - smtpUser?: string; - smtpPass?: string; - - enableServiceWorker?: boolean; - swPublicKey?: string; - swPrivateKey?: string; -}; diff --git a/src/models/mute.ts b/src/models/mute.ts deleted file mode 100644 index 52775e13ca..0000000000 --- a/src/models/mute.ts +++ /dev/null @@ -1,56 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import * as deepcopy from 'deepcopy'; -import { pack as packUser, IUser } from './user'; - -const Mute = db.get<IMute>('mute'); -Mute.createIndex('muterId'); -Mute.createIndex('muteeId'); -Mute.createIndex(['muterId', 'muteeId'], { unique: true }); -export default Mute; - -export interface IMute { - _id: mongo.ObjectID; - createdAt: Date; - muterId: mongo.ObjectID; - muteeId: mongo.ObjectID; -} - -export const packMany = ( - mutes: (string | mongo.ObjectID | IMute)[], - me?: string | mongo.ObjectID | IUser -) => { - return Promise.all(mutes.map(x => pack(x, me))); -}; - -export const pack = ( - mute: any, - me?: any -) => new Promise<any>(async (resolve, reject) => { - let _mute: any; - - // Populate the mute if 'mute' is ID - if (isObjectId(mute)) { - _mute = await Mute.findOne({ - _id: mute - }); - } else if (typeof mute === 'string') { - _mute = await Mute.findOne({ - _id: new mongo.ObjectID(mute) - }); - } else { - _mute = deepcopy(mute); - } - - // Rename _id to id - _mute.id = _mute._id; - delete _mute._id; - - // Populate mutee - _mute.mutee = await packUser(_mute.muteeId, me, { - detail: true - }); - - resolve(_mute); -}); diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts deleted file mode 100644 index 89b7529350..0000000000 --- a/src/models/note-reaction.ts +++ /dev/null @@ -1,51 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { pack as packUser } from './user'; - -const NoteReaction = db.get<INoteReaction>('noteReactions'); -NoteReaction.createIndex('noteId'); -NoteReaction.createIndex('userId'); -NoteReaction.createIndex(['userId', 'noteId'], { unique: true }); -export default NoteReaction; - -export interface INoteReaction { - _id: mongo.ObjectID; - createdAt: Date; - noteId: mongo.ObjectID; - userId: mongo.ObjectID; - reaction: string; -} - -/** - * Pack a reaction for API response - */ -export const pack = ( - reaction: any, - me?: any -) => new Promise<any>(async (resolve, reject) => { - let _reaction: any; - - // Populate the reaction if 'reaction' is ID - if (isObjectId(reaction)) { - _reaction = await NoteReaction.findOne({ - _id: reaction - }); - } else if (typeof reaction === 'string') { - _reaction = await NoteReaction.findOne({ - _id: new mongo.ObjectID(reaction) - }); - } else { - _reaction = deepcopy(reaction); - } - - // Rename _id to id - _reaction.id = _reaction._id; - delete _reaction._id; - - // Populate user - _reaction.user = await packUser(_reaction.userId, me); - - resolve(_reaction); -}); diff --git a/src/models/note-unread.ts b/src/models/note-unread.ts deleted file mode 100644 index dd08640d85..0000000000 --- a/src/models/note-unread.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const NoteUnread = db.get<INoteUnread>('noteUnreads'); -NoteUnread.createIndex('userId'); -NoteUnread.createIndex('noteId'); -NoteUnread.createIndex(['userId', 'noteId'], { unique: true }); -export default NoteUnread; - -export interface INoteUnread { - _id: mongo.ObjectID; - noteId: mongo.ObjectID; - userId: mongo.ObjectID; - isSpecified: boolean; - - _note: { - userId: mongo.ObjectID; - }; -} diff --git a/src/models/note-watching.ts b/src/models/note-watching.ts deleted file mode 100644 index 83aaf8ad06..0000000000 --- a/src/models/note-watching.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const NoteWatching = db.get<INoteWatching>('noteWatching'); -NoteWatching.createIndex('userId'); -NoteWatching.createIndex('noteId'); -NoteWatching.createIndex(['userId', 'noteId'], { unique: true }); -export default NoteWatching; - -export interface INoteWatching { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - noteId: mongo.ObjectID; -} diff --git a/src/models/note.ts b/src/models/note.ts deleted file mode 100644 index 8c71c1940c..0000000000 --- a/src/models/note.ts +++ /dev/null @@ -1,418 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import rap from '@prezzemolo/rap'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { length } from 'stringz'; -import { IUser, pack as packUser } from './user'; -import { pack as packApp } from './app'; -import PollVote from './poll-vote'; -import NoteReaction from './note-reaction'; -import { packMany as packFileMany, IDriveFile } from './drive-file'; -import Following from './following'; -import Emoji from './emoji'; -import { dbLogger } from '../db/logger'; -import { unique, concat } from '../prelude/array'; - -const Note = db.get<INote>('notes'); -Note.createIndex('uri', { sparse: true, unique: true }); -Note.createIndex('userId'); -Note.createIndex('mentions'); -Note.createIndex('visibleUserIds'); -Note.createIndex('replyId'); -Note.createIndex('renoteId'); -Note.createIndex('tagsLower'); -Note.createIndex('_user.host'); -Note.createIndex('_files._id'); -Note.createIndex('_files.contentType'); -Note.createIndex({ createdAt: -1 }); -Note.createIndex({ score: -1 }, { sparse: true }); -export default Note; - -export function isValidCw(text: string): boolean { - return length(text.trim()) <= 100; -} - -export type INote = { - _id: mongo.ObjectID; - createdAt: Date; - deletedAt: Date; - updatedAt?: Date; - fileIds: mongo.ObjectID[]; - replyId: mongo.ObjectID; - renoteId: mongo.ObjectID; - poll: IPoll; - name?: string; - text: string; - tags: string[]; - tagsLower: string[]; - emojis: string[]; - cw: string; - userId: mongo.ObjectID; - appId: mongo.ObjectID; - viaMobile: boolean; - localOnly: boolean; - renoteCount: number; - repliesCount: number; - reactionCounts: Record<string, number>; - mentions: mongo.ObjectID[]; - mentionedRemoteUsers: { - uri: string; - username: string; - host: string; - }[]; - - /** - * public ... 公開 - * home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す - * followers ... フォロワーのみ - * specified ... visibleUserIds で指定したユーザーのみ - */ - visibility: 'public' | 'home' | 'followers' | 'specified'; - - visibleUserIds: mongo.ObjectID[]; - - geo: { - coordinates: number[]; - altitude: number; - accuracy: number; - altitudeAccuracy: number; - heading: number; - speed: number; - }; - - uri: string; - - /** - * 人気の投稿度合いを表すスコア - */ - score: number; - - // 非正規化 - _reply?: { - userId: mongo.ObjectID; - }; - _renote?: { - userId: mongo.ObjectID; - }; - _user: { - host: string; - inbox?: string; - }; - _files?: IDriveFile[]; -}; - -export type IPoll = { - choices: IChoice[]; - multiple?: boolean; - expiresAt?: Date; -}; - -export type IChoice = { - id: number; - text: string; - votes: number; -}; - -export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => { - let hide = false; - - // visibility が private かつ投稿者のIDが自分のIDではなかったら非表示(後方互換性のため) - if (packedNote.visibility == 'private' && (meId == null || !meId.equals(packedNote.userId))) { - hide = true; - } - - // visibility が specified かつ自分が指定されていなかったら非表示 - if (packedNote.visibility == 'specified') { - if (meId == null) { - hide = true; - } else if (meId.equals(packedNote.userId)) { - hide = false; - } else { - // 指定されているかどうか - const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id)); - - if (specified) { - hide = false; - } else { - hide = true; - } - } - } - - // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 - if (packedNote.visibility == 'followers') { - if (meId == null) { - hide = true; - } else if (meId.equals(packedNote.userId)) { - hide = false; - } else if (packedNote.reply && meId.equals(packedNote.reply.userId)) { - // 自分の投稿に対するリプライ - hide = false; - } else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId.equals(id))) { - // 自分へのメンション - hide = false; - } else { - // フォロワーかどうか - const following = await Following.findOne({ - followeeId: packedNote.userId, - followerId: meId - }); - - if (following == null) { - hide = true; - } else { - hide = false; - } - } - } - - if (hide) { - packedNote.fileIds = []; - packedNote.files = []; - packedNote.text = null; - packedNote.poll = null; - packedNote.cw = null; - packedNote.tags = []; - packedNote.geo = null; - packedNote.isHidden = true; - } -}; - -export const packMany = ( - notes: (string | mongo.ObjectID | INote)[], - me?: string | mongo.ObjectID | IUser, - options?: { - detail?: boolean; - skipHide?: boolean; - } -) => { - return Promise.all(notes.map(n => pack(n, me, options))); -}; - -/** - * Pack a note for API response - * - * @param note target - * @param me? serializee - * @param options? serialize options - * @return response - */ -export const pack = async ( - note: string | mongo.ObjectID | INote, - me?: string | mongo.ObjectID | IUser, - options?: { - detail?: boolean; - skipHide?: boolean; - } -) => { - const opts = Object.assign({ - detail: true, - skipHide: false - }, options); - - // Me - const meId: mongo.ObjectID = me - ? isObjectId(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - let _note: any; - - // Populate the note if 'note' is ID - if (isObjectId(note)) { - _note = await Note.findOne({ - _id: note - }); - } else if (typeof note === 'string') { - _note = await Note.findOne({ - _id: new mongo.ObjectID(note) - }); - } else { - _note = deepcopy(note); - } - - // (データベースの欠損などで)投稿がデータベース上に見つからなかったとき - if (_note == null) { - dbLogger.warn(`[DAMAGED DB] (missing) pkg: note :: ${note}`); - return null; - } - - const id = _note._id; - - // Some counts - _note.renoteCount = _note.renoteCount || 0; - _note.repliesCount = _note.repliesCount || 0; - _note.reactionCounts = _note.reactionCounts || {}; - - // _note._userを消す前か、_note.userを解決した後でないとホストがわからない - if (_note._user) { - const host = _note._user.host; - // 互換性のため。(古いMisskeyではNoteにemojisが無い) - if (_note.emojis == null) { - _note.emojis = Emoji.find({ - host: host - }, { - fields: { _id: false } - }); - } else { - _note.emojis = unique(concat([_note.emojis, Object.keys(_note.reactionCounts).map(x => x.replace(/:/g, ''))])); - - _note.emojis = Emoji.find({ - name: { $in: _note.emojis }, - host: host - }, { - fields: { _id: false } - }); - } - } - - // Rename _id to id - _note.id = _note._id; - delete _note._id; - - delete _note.prev; - delete _note.next; - delete _note.tagsLower; - delete _note.score; - delete _note._user; - delete _note._reply; - delete _note._renote; - delete _note._files; - delete _note._replyIds; - delete _note.mentionedRemoteUsers; - - if (_note.geo) delete _note.geo.type; - - // Populate user - _note.user = packUser(_note.userId, meId); - - // Populate app - if (_note.appId) { - _note.app = packApp(_note.appId); - } - - // Populate files - _note.files = packFileMany(_note.fileIds || []); - - // 後方互換性のため - _note.mediaIds = _note.fileIds; - _note.media = _note.files; - - // When requested a detailed note data - if (opts.detail) { - if (_note.replyId) { - // Populate reply to note - _note.reply = pack(_note.replyId, meId, { - detail: false - }); - } - - if (_note.renoteId) { - // Populate renote - _note.renote = pack(_note.renoteId, meId, { - detail: _note.text == null - }); - } - - // Poll - if (meId && _note.poll) { - _note.poll = (async poll => { - if (poll.multiple) { - const votes = await PollVote.find({ - userId: meId, - noteId: id - }); - - const myChoices = (poll.choices as IChoice[]).filter(x => votes.some(y => x.id == y.choice)); - for (const myChoice of myChoices) { - (myChoice as any).isVoted = true; - } - - return poll; - } else { - poll.multiple = false; - } - - const vote = await PollVote - .findOne({ - userId: meId, - noteId: id - }); - - if (vote) { - const myChoice = (poll.choices as IChoice[]) - .filter(x => x.id == vote.choice)[0] as any; - - myChoice.isVoted = true; - } - - return poll; - })(_note.poll); - } - - if (meId) { - // Fetch my reaction - _note.myReaction = (async () => { - const reaction = await NoteReaction - .findOne({ - userId: meId, - noteId: id, - deletedAt: { $exists: false } - }); - - if (reaction) { - return reaction.reaction; - } - - return null; - })(); - } - } - - // resolve promises in _note object - _note = await rap(_note); - - //#region (データベースの欠損などで)参照しているデータがデータベース上に見つからなかったとき - if (_note.user == null) { - dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> user :: ${_note.id} (user ${_note.userId})`); - return null; - } - - if (opts.detail) { - if (_note.replyId != null && _note.reply == null) { - dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> reply :: ${_note.id} (reply ${_note.replyId})`); - return null; - } - - if (_note.renoteId != null && _note.renote == null) { - dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> renote :: ${_note.id} (renote ${_note.renoteId})`); - return null; - } - } - //#endregion - - if (_note.name) { - _note.text = `【${_note.name}】\n${_note.text}`; - } - - if (_note.user.isCat && _note.text) { - _note.text = (_note.text - // ja-JP - .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') - // ko-KR - .replace(/[나-낳]/g, (match: string) => String.fromCharCode( - match.codePointAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0) - )) - ); - } - - if (!opts.skipHide) { - await hideNote(_note, meId); - } - - return _note; -}; diff --git a/src/models/notification.ts b/src/models/notification.ts deleted file mode 100644 index 75456af57b..0000000000 --- a/src/models/notification.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { IUser, pack as packUser } from './user'; -import { pack as packNote } from './note'; -import { dbLogger } from '../db/logger'; - -const Notification = db.get<INotification>('notifications'); -Notification.createIndex('notifieeId'); -export default Notification; - -export interface INotification { - _id: mongo.ObjectID; - createdAt: Date; - - /** - * 通知の受信者 - */ - notifiee?: IUser; - - /** - * 通知の受信者 - */ - notifieeId: mongo.ObjectID; - - /** - * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー - */ - notifier?: IUser; - - /** - * イニシエータ(initiator)、Origin。通知を行う原因となったユーザー - */ - notifierId: mongo.ObjectID; - - /** - * 通知の種類。 - * follow - フォローされた - * mention - 投稿で自分が言及された - * reply - (自分または自分がWatchしている)投稿が返信された - * renote - (自分または自分がWatchしている)投稿がRenoteされた - * quote - (自分または自分がWatchしている)投稿が引用Renoteされた - * reaction - (自分または自分がWatchしている)投稿にリアクションされた - * poll_vote - (自分または自分がWatchしている)投稿の投票に投票された - */ - type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'poll_vote'; - - /** - * 通知が読まれたかどうか - */ - isRead: boolean; -} - -export const packMany = ( - notifications: any[] -) => { - return Promise.all(notifications.map(n => pack(n))); -}; - -/** - * Pack a notification for API response - */ -export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => { - let _notification: any; - - // Populate the notification if 'notification' is ID - if (isObjectId(notification)) { - _notification = await Notification.findOne({ - _id: notification - }); - } else if (typeof notification === 'string') { - _notification = await Notification.findOne({ - _id: new mongo.ObjectID(notification) - }); - } else { - _notification = deepcopy(notification); - } - - // Rename _id to id - _notification.id = _notification._id; - delete _notification._id; - - // Rename notifierId to userId - _notification.userId = _notification.notifierId; - delete _notification.notifierId; - - const me = _notification.notifieeId; - delete _notification.notifieeId; - - // Populate notifier - _notification.user = await packUser(_notification.userId, me); - - switch (_notification.type) { - case 'follow': - case 'receiveFollowRequest': - // nope - break; - case 'mention': - case 'reply': - case 'renote': - case 'quote': - case 'reaction': - case 'poll_vote': - // Populate note - _notification.note = await packNote(_notification.noteId, me); - - // (データベースの不具合などで)投稿が見つからなかったら - if (_notification.note == null) { - dbLogger.warn(`[DAMAGED DB] (missing) pkg: notification -> note :: ${_notification.id} (note ${_notification.noteId})`); - return resolve(null); - } - break; - default: - dbLogger.error(`Unknown type: ${_notification.type}`); - break; - } - - resolve(_notification); -}); diff --git a/src/models/poll-vote.ts b/src/models/poll-vote.ts deleted file mode 100644 index e6178cbc26..0000000000 --- a/src/models/poll-vote.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const PollVote = db.get<IPollVote>('pollVotes'); -PollVote.dropIndex(['userId', 'noteId'], { unique: true }).catch(() => {}); -PollVote.createIndex('userId'); -PollVote.createIndex('noteId'); -PollVote.createIndex(['userId', 'noteId', 'choice'], { unique: true }); -export default PollVote; - -export interface IPollVote { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - noteId: mongo.ObjectID; - choice: number; -} diff --git a/src/models/registration-tickets.ts b/src/models/registration-tickets.ts deleted file mode 100644 index 846acefedf..0000000000 --- a/src/models/registration-tickets.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const RegistrationTicket = db.get<IRegistrationTicket>('registrationTickets'); -RegistrationTicket.createIndex('code', { unique: true }); -export default RegistrationTicket; - -export interface IRegistrationTicket { - _id: mongo.ObjectID; - createdAt: Date; - code: string; -} diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts new file mode 100644 index 0000000000..f619d6e37f --- /dev/null +++ b/src/models/repositories/abuse-user-report.ts @@ -0,0 +1,33 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Users } from '..'; +import rap from '@prezzemolo/rap'; +import { AbuseUserReport } from '../entities/abuse-user-report'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(AbuseUserReport) +export class AbuseUserReportRepository extends Repository<AbuseUserReport> { + public packMany( + reports: any[], + ) { + return Promise.all(reports.map(x => this.pack(x))); + } + + public async pack( + src: AbuseUserReport['id'] | AbuseUserReport, + ) { + const report = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return await rap({ + id: report.id, + createdAt: report.createdAt, + reporterId: report.reporterId, + userId: report.userId, + reporter: Users.pack(report.reporter || report.reporterId, null, { + detail: true + }), + user: Users.pack(report.user || report.userId, null, { + detail: true + }), + }); + } +} diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts new file mode 100644 index 0000000000..a0c0cf68cb --- /dev/null +++ b/src/models/repositories/app.ts @@ -0,0 +1,37 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { App } from '../entities/app'; +import { AccessTokens } from '..'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(App) +export class AppRepository extends Repository<App> { + public async pack( + src: App['id'] | App, + me?: any, + options?: { + detail?: boolean, + includeSecret?: boolean, + includeProfileImageIds?: boolean + } + ) { + const opts = Object.assign({ + detail: false, + includeSecret: false, + includeProfileImageIds: false + }, options); + + const app = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: app.id, + name: app.name, + ...(opts.includeSecret ? { secret: app.secret } : {}), + ...(me ? { + isAuthorized: await AccessTokens.count({ + appId: app.id, + userId: me, + }).then(count => count > 0) + } : {}) + }; + } +} diff --git a/src/models/repositories/auth-session.ts b/src/models/repositories/auth-session.ts new file mode 100644 index 0000000000..540c5466f5 --- /dev/null +++ b/src/models/repositories/auth-session.ts @@ -0,0 +1,20 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Apps } from '..'; +import rap from '@prezzemolo/rap'; +import { AuthSession } from '../entities/auth-session'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(AuthSession) +export class AuthSessionRepository extends Repository<AuthSession> { + public async pack( + src: AuthSession['id'] | AuthSession, + me?: any + ) { + const session = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return await rap({ + id: session.id, + app: Apps.pack(session.appId, me) + }); + } +} diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts new file mode 100644 index 0000000000..e18aa591f3 --- /dev/null +++ b/src/models/repositories/blocking.ts @@ -0,0 +1,29 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Users } from '..'; +import rap from '@prezzemolo/rap'; +import { Blocking } from '../entities/blocking'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(Blocking) +export class BlockingRepository extends Repository<Blocking> { + public packMany( + blockings: any[], + me: any + ) { + return Promise.all(blockings.map(x => this.pack(x, me))); + } + + public async pack( + src: Blocking['id'] | Blocking, + me?: any + ) { + const blocking = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return await rap({ + id: blocking.id, + blockee: Users.pack(blocking.blockeeId, me, { + detail: true + }) + }); + } +} diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts new file mode 100644 index 0000000000..7dd794d031 --- /dev/null +++ b/src/models/repositories/drive-file.ts @@ -0,0 +1,111 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { DriveFile } from '../entities/drive-file'; +import { Users, DriveFolders } from '..'; +import rap from '@prezzemolo/rap'; +import { User } from '../entities/user'; +import { toPuny } from '../../misc/convert-host'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(DriveFile) +export class DriveFileRepository extends Repository<DriveFile> { + public validateFileName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) && + (name.indexOf('\\') === -1) && + (name.indexOf('/') === -1) && + (name.indexOf('..') === -1) + ); + } + + public getPublicUrl(file: DriveFile, thumbnail = false): string | null { + return thumbnail ? (file.thumbnailUrl || file.webpublicUrl || null) : (file.webpublicUrl || file.thumbnailUrl || file.url); + } + + public async clacDriveUsageOf(user: User['id'] | User): Promise<number> { + const id = typeof user === 'object' ? user.id : user; + + const { sum } = await this + .createQueryBuilder('file') + .where('file.userId = :id', { id: id }) + .select('SUM(file.size)', 'sum') + .getRawOne(); + + return parseInt(sum, 10) || 0; + } + + public async clacDriveUsageOfHost(host: string): Promise<number> { + const { sum } = await this + .createQueryBuilder('file') + .where('file.userHost = :host', { host: toPuny(host) }) + .select('SUM(file.size)', 'sum') + .getRawOne(); + + return parseInt(sum, 10) || 0; + } + + public async clacDriveUsageOfLocal(): Promise<number> { + const { sum } = await this + .createQueryBuilder('file') + .where('file.userHost IS NULL') + .select('SUM(file.size)', 'sum') + .getRawOne(); + + return parseInt(sum, 10) || 0; + } + + public async clacDriveUsageOfRemote(): Promise<number> { + const { sum } = await this + .createQueryBuilder('file') + .where('file.userHost IS NOT NULL') + .select('SUM(file.size)', 'sum') + .getRawOne(); + + return parseInt(sum, 10) || 0; + } + + public packMany( + files: any[], + options?: { + detail?: boolean + self?: boolean, + withUser?: boolean, + } + ) { + return Promise.all(files.map(f => this.pack(f, options))); + } + + public async pack( + src: DriveFile['id'] | DriveFile, + options?: { + detail?: boolean, + self?: boolean, + withUser?: boolean, + } + ) { + const opts = Object.assign({ + detail: false, + self: false + }, options); + + const file = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return await rap({ + id: file.id, + createdAt: file.createdAt, + name: file.name, + type: file.type, + md5: file.md5, + size: file.size, + isSensitive: file.isSensitive, + properties: file.properties, + url: opts.self ? file.url : this.getPublicUrl(file, false), + thumbnailUrl: this.getPublicUrl(file, true), + folderId: file.folderId, + folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { + detail: true + }) : null, + user: opts.withUser ? Users.pack(file.userId!) : null + }); + } +} diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts new file mode 100644 index 0000000000..ce88adefa4 --- /dev/null +++ b/src/models/repositories/drive-folder.ts @@ -0,0 +1,50 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { DriveFolders, DriveFiles } from '..'; +import rap from '@prezzemolo/rap'; +import { DriveFolder } from '../entities/drive-folder'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(DriveFolder) +export class DriveFolderRepository extends Repository<DriveFolder> { + public validateFolderName(name: string): boolean { + return ( + (name.trim().length > 0) && + (name.length <= 200) + ); + } + + public async pack( + src: DriveFolder['id'] | DriveFolder, + options?: { + detail: boolean + } + ): Promise<Record<string, any>> { + const opts = Object.assign({ + detail: false + }, options); + + const folder = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return await rap({ + id: folder.id, + createdAt: folder.createdAt, + name: folder.name, + parentId: folder.parentId, + + ...(opts.detail ? { + foldersCount: DriveFolders.count({ + parentId: folder.id + }), + filesCount: DriveFiles.count({ + folderId: folder.id + }), + + ...(folder.parentId ? { + parent: this.pack(folder.parentId, { + detail: true + }) + } : {}) + } : {}) + }); + } +} diff --git a/src/models/repositories/follow-request.ts b/src/models/repositories/follow-request.ts new file mode 100644 index 0000000000..451ed8e2d5 --- /dev/null +++ b/src/models/repositories/follow-request.ts @@ -0,0 +1,20 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { FollowRequest } from '../entities/follow-request'; +import { Users } from '..'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(FollowRequest) +export class FollowRequestRepository extends Repository<FollowRequest> { + public async pack( + src: FollowRequest['id'] | FollowRequest, + me?: any + ) { + const request = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: request.id, + follower: await Users.pack(request.followerId, me), + followee: await Users.pack(request.followeeId, me), + }; + } +} diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts new file mode 100644 index 0000000000..3fff57866f --- /dev/null +++ b/src/models/repositories/following.ts @@ -0,0 +1,85 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Users } from '..'; +import rap from '@prezzemolo/rap'; +import { Following } from '../entities/following'; +import { ensure } from '../../prelude/ensure'; + +type LocalFollowerFollowing = Following & { + followerHost: null; + followerInbox: null; + followerSharedInbox: null; +}; + +type RemoteFollowerFollowing = Following & { + followerHost: string; + followerInbox: string; + followerSharedInbox: string; +}; + +type LocalFolloweeFollowing = Following & { + followeeHost: null; + followeeInbox: null; + followeeSharedInbox: null; +}; + +type RemoteFolloweeFollowing = Following & { + followeeHost: string; + followeeInbox: string; + followeeSharedInbox: string; +}; + +@EntityRepository(Following) +export class FollowingRepository extends Repository<Following> { + public isLocalFollower(following: Following): following is LocalFollowerFollowing { + return following.followerHost == null; + } + + public isRemoteFollower(following: Following): following is RemoteFollowerFollowing { + return following.followerHost != null; + } + + public isLocalFollowee(following: Following): following is LocalFolloweeFollowing { + return following.followeeHost == null; + } + + public isRemoteFollowee(following: Following): following is RemoteFolloweeFollowing { + return following.followeeHost != null; + } + + public packMany( + followings: any[], + me?: any, + opts?: { + populateFollowee?: boolean; + populateFollower?: boolean; + } + ) { + return Promise.all(followings.map(x => this.pack(x, me, opts))); + } + + public async pack( + src: Following['id'] | Following, + me?: any, + opts?: { + populateFollowee?: boolean; + populateFollower?: boolean; + } + ) { + const following = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + if (opts == null) opts = {}; + + return await rap({ + id: following.id, + createdAt: following.createdAt, + followeeId: following.followeeId, + followerId: following.followerId, + followee: opts.populateFollowee ? Users.pack(following.followee || following.followeeId, me, { + detail: true + }) : null, + follower: opts.populateFollower ? Users.pack(following.follower || following.followerId, me, { + detail: true + }) : null, + }); + } +} diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts new file mode 100644 index 0000000000..c380f5251e --- /dev/null +++ b/src/models/repositories/games/reversi/game.ts @@ -0,0 +1,50 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Users } from '../../..'; +import { ReversiGame } from '../../../entities/games/reversi/game'; +import { ensure } from '../../../../prelude/ensure'; + +@EntityRepository(ReversiGame) +export class ReversiGameRepository extends Repository<ReversiGame> { + public async pack( + src: ReversiGame['id'] | ReversiGame, + me?: any, + options?: { + detail?: boolean + } + ) { + const opts = Object.assign({ + detail: true + }, options); + + const game = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + const meId = me ? typeof me === 'string' ? me : me.id : null; + + return { + id: game.id, + createdAt: game.createdAt, + startedAt: game.startedAt, + isStarted: game.isStarted, + isEnded: game.isEnded, + form1: game.form1, + form2: game.form2, + user1Accepted: game.user1Accepted, + user2Accepted: game.user2Accepted, + user1Id: game.user1Id, + user2Id: game.user2Id, + user1: await Users.pack(game.user1Id, meId), + user2: await Users.pack(game.user2Id, meId), + winnerId: game.winnerId, + winner: game.winnerId ? await Users.pack(game.winnerId, meId) : null, + surrendered: game.surrendered, + black: game.black, + bw: game.bw, + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard, + ...(opts.detail ? { + logs: game.logs, + map: game.map, + } : {}) + }; + } +} diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts new file mode 100644 index 0000000000..4d99c6ef76 --- /dev/null +++ b/src/models/repositories/games/reversi/matching.ts @@ -0,0 +1,28 @@ +import { EntityRepository, Repository } from 'typeorm'; +import rap from '@prezzemolo/rap'; +import { ReversiMatching } from '../../../entities/games/reversi/matching'; +import { Users } from '../../..'; +import { ensure } from '../../../../prelude/ensure'; + +@EntityRepository(ReversiMatching) +export class ReversiMatchingRepository extends Repository<ReversiMatching> { + public async pack( + src: ReversiMatching['id'] | ReversiMatching, + me: any + ) { + const matching = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return await rap({ + id: matching.id, + createdAt: matching.createdAt, + parentId: matching.parentId, + parent: Users.pack(matching.parentId, me, { + detail: true + }), + childId: matching.childId, + child: Users.pack(matching.childId, me, { + detail: true + }) + }); + } +} diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts new file mode 100644 index 0000000000..6659273539 --- /dev/null +++ b/src/models/repositories/messaging-message.ts @@ -0,0 +1,38 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { MessagingMessage } from '../entities/messaging-message'; +import { Users, DriveFiles } from '..'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(MessagingMessage) +export class MessagingMessageRepository extends Repository<MessagingMessage> { + public isValidText(text: string): boolean { + return text.trim().length <= 1000 && text.trim() != ''; + } + + public async pack( + src: MessagingMessage['id'] | MessagingMessage, + me?: any, + options?: { + populateRecipient: boolean + } + ) { + const opts = options || { + populateRecipient: true + }; + + const message = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: message.id, + createdAt: message.createdAt, + text: message.text, + userId: message.userId, + user: await Users.pack(message.user || message.userId, me), + recipientId: message.recipientId, + recipient: opts.populateRecipient ? await Users.pack(message.recipient || message.recipientId, me) : null, + fileId: message.fileId, + file: message.fileId ? await DriveFiles.pack(message.fileId) : null, + isRead: message.isRead + }; + } +} diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts new file mode 100644 index 0000000000..1812e2e713 --- /dev/null +++ b/src/models/repositories/muting.ts @@ -0,0 +1,29 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Users } from '..'; +import rap from '@prezzemolo/rap'; +import { Muting } from '../entities/muting'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(Muting) +export class MutingRepository extends Repository<Muting> { + public packMany( + mutings: any[], + me: any + ) { + return Promise.all(mutings.map(x => this.pack(x, me))); + } + + public async pack( + src: Muting['id'] | Muting, + me?: any + ) { + const muting = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return await rap({ + id: muting.id, + mutee: Users.pack(muting.muteeId, me, { + detail: true + }) + }); + } +} diff --git a/src/models/repositories/note-favorite.ts b/src/models/repositories/note-favorite.ts new file mode 100644 index 0000000000..f428903c13 --- /dev/null +++ b/src/models/repositories/note-favorite.ts @@ -0,0 +1,26 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { NoteFavorite } from '../entities/note-favorite'; +import { Notes } from '..'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(NoteFavorite) +export class NoteFavoriteRepository extends Repository<NoteFavorite> { + public packMany( + favorites: any[], + me: any + ) { + return Promise.all(favorites.map(x => this.pack(x, me))); + } + + public async pack( + src: NoteFavorite['id'] | NoteFavorite, + me?: any + ) { + const favorite = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: favorite.id, + note: await Notes.pack(favorite.note || favorite.noteId, me), + }; + } +} diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts new file mode 100644 index 0000000000..28191d4ab0 --- /dev/null +++ b/src/models/repositories/note-reaction.ts @@ -0,0 +1,19 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { NoteReaction } from '../entities/note-reaction'; +import { Users } from '..'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(NoteReaction) +export class NoteReactionRepository extends Repository<NoteReaction> { + public async pack( + src: NoteReaction['id'] | NoteReaction, + me?: any + ) { + const reaction = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return { + id: reaction.id, + user: await Users.pack(reaction.userId, me), + }; + } +} diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts new file mode 100644 index 0000000000..7a48d24e13 --- /dev/null +++ b/src/models/repositories/note.ts @@ -0,0 +1,215 @@ +import { EntityRepository, Repository, In } from 'typeorm'; +import { Note } from '../entities/note'; +import { User } from '../entities/user'; +import { unique, concat } from '../../prelude/array'; +import { nyaize } from '../../misc/nyaize'; +import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..'; +import rap from '@prezzemolo/rap'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(Note) +export class NoteRepository extends Repository<Note> { + public validateCw(x: string) { + return x.trim().length <= 100; + } + + private async hideNote(packedNote: any, meId: User['id'] | null) { + let hide = false; + + // visibility が specified かつ自分が指定されていなかったら非表示 + if (packedNote.visibility == 'specified') { + if (meId == null) { + hide = true; + } else if (meId === packedNote.userId) { + hide = false; + } else { + // 指定されているかどうか + const specified = packedNote.visibleUserIds.some((id: any) => meId === id); + + if (specified) { + hide = false; + } else { + hide = true; + } + } + } + + // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 + if (packedNote.visibility == 'followers') { + if (meId == null) { + hide = true; + } else if (meId === packedNote.userId) { + hide = false; + } else if (packedNote.reply && (meId === packedNote.reply.userId)) { + // 自分の投稿に対するリプライ + hide = false; + } else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId === id)) { + // 自分へのメンション + hide = false; + } else { + // フォロワーかどうか + const following = await Followings.findOne({ + followeeId: packedNote.userId, + followerId: meId + }); + + if (following == null) { + hide = true; + } else { + hide = false; + } + } + } + + if (hide) { + packedNote.visibleUserIds = null; + packedNote.fileIds = []; + packedNote.files = []; + packedNote.text = null; + packedNote.poll = null; + packedNote.cw = null; + packedNote.tags = []; + packedNote.geo = null; + packedNote.isHidden = true; + } + } + + public packMany( + notes: (Note['id'] | Note)[], + me?: User['id'] | User | null | undefined, + options?: { + detail?: boolean; + skipHide?: boolean; + } + ) { + return Promise.all(notes.map(n => this.pack(n, me, options))); + } + + public async pack( + src: Note['id'] | Note, + me?: User['id'] | User | null | undefined, + options?: { + detail?: boolean; + skipHide?: boolean; + } + ): Promise<Record<string, any>> { + const opts = Object.assign({ + detail: true, + skipHide: false + }, options); + + const meId = me ? typeof me === 'string' ? me : me.id : null; + const note = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + const host = note.userHost; + + async function populatePoll() { + const poll = await Polls.findOne({ noteId: note.id }).then(ensure); + const choices = poll.choices.map(c => ({ + text: c, + votes: poll.votes[poll.choices.indexOf(c)], + isVoted: false + })); + + if (poll.multiple) { + const votes = await PollVotes.find({ + userId: meId!, + noteId: note.id + }); + + const myChoices = votes.map(v => v.choice); + for (const myChoice of myChoices) { + choices[myChoice].isVoted = true; + } + } else { + const vote = await PollVotes.findOne({ + userId: meId!, + noteId: note.id + }); + + if (vote) { + choices[vote.choice].isVoted = true; + } + } + + return { + multiple: poll.multiple, + expiresAt: poll.expiresAt, + choices + }; + } + + async function populateMyReaction() { + const reaction = await NoteReactions.findOne({ + userId: meId!, + noteId: note.id, + }); + + if (reaction) { + return reaction.reaction; + } + + return undefined; + } + + let text = note.text; + + if (note.name) { + text = `【${note.name}】\n${note.text}`; + } + + const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)])); + + const packed = await rap({ + id: note.id, + createdAt: note.createdAt, + app: note.appId ? Apps.pack(note.appId) : undefined, + userId: note.userId, + user: Users.pack(note.user || note.userId, meId), + text: text, + cw: note.cw, + visibility: note.visibility, + localOnly: note.localOnly || undefined, + visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined, + viaMobile: note.viaMobile || undefined, + renoteCount: note.renoteCount, + repliesCount: note.repliesCount, + reactions: note.reactions, + emojis: reactionEmojis.length > 0 ? Emojis.find({ + name: In(reactionEmojis), + host: host + }) : [], + tags: note.tags, + fileIds: note.fileIds, + files: DriveFiles.packMany(note.fileIds), + replyId: note.replyId, + renoteId: note.renoteId, + uri: note.uri, + + ...(opts.detail ? { + reply: note.replyId ? this.pack(note.replyId, meId, { + detail: false + }) : undefined, + + renote: note.renoteId ? this.pack(note.renoteId, meId, { + detail: true + }) : undefined, + + poll: note.hasPoll ? populatePoll() : undefined, + + ...(meId ? { + myReaction: populateMyReaction() + } : {}) + } : {}) + }); + + if (packed.user.isCat && packed.text) { + packed.text = nyaize(packed.text); + } + + if (!opts.skipHide) { + await this.hideNote(packed, meId); + } + + return packed; + } +} diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts new file mode 100644 index 0000000000..4781d4c065 --- /dev/null +++ b/src/models/repositories/notification.ts @@ -0,0 +1,48 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Users, Notes } from '..'; +import rap from '@prezzemolo/rap'; +import { Notification } from '../entities/notification'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(Notification) +export class NotificationRepository extends Repository<Notification> { + public packMany( + notifications: any[], + ) { + return Promise.all(notifications.map(x => this.pack(x))); + } + + public async pack( + src: Notification['id'] | Notification, + ) { + const notification = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + return await rap({ + id: notification.id, + createdAt: notification.createdAt, + type: notification.type, + userId: notification.notifierId, + user: Users.pack(notification.notifier || notification.notifierId), + ...(notification.type === 'mention' ? { + note: Notes.pack(notification.note || notification.noteId!), + } : {}), + ...(notification.type === 'reply' ? { + note: Notes.pack(notification.note || notification.noteId!), + } : {}), + ...(notification.type === 'renote' ? { + note: Notes.pack(notification.note || notification.noteId!), + } : {}), + ...(notification.type === 'quote' ? { + note: Notes.pack(notification.note || notification.noteId!), + } : {}), + ...(notification.type === 'reaction' ? { + note: Notes.pack(notification.note || notification.noteId!), + reaction: notification.reaction + } : {}), + ...(notification.type === 'pollVote' ? { + note: Notes.pack(notification.note || notification.noteId!), + choice: notification.choice + } : {}) + }); + } +} diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts new file mode 100644 index 0000000000..f5b90c0e9e --- /dev/null +++ b/src/models/repositories/signin.ts @@ -0,0 +1,11 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { Signin } from '../entities/signin'; + +@EntityRepository(Signin) +export class SigninRepository extends Repository<Signin> { + public async pack( + src: any, + ) { + return src; + } +} diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts new file mode 100644 index 0000000000..e591794b8b --- /dev/null +++ b/src/models/repositories/user-list.ts @@ -0,0 +1,23 @@ +import { EntityRepository, Repository } from 'typeorm'; +import { UserList } from '../entities/user-list'; +import { ensure } from '../../prelude/ensure'; +import { UserListJoinings } from '..'; + +@EntityRepository(UserList) +export class UserListRepository extends Repository<UserList> { + public async pack( + src: UserList['id'] | UserList, + ) { + const userList = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + + const users = await UserListJoinings.find({ + userListId: userList.id + }); + + return { + id: userList.id, + name: userList.name, + userIds: users.map(x => x.userId) + }; + } +} diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts new file mode 100644 index 0000000000..7e67ead72a --- /dev/null +++ b/src/models/repositories/user.ts @@ -0,0 +1,213 @@ +import { EntityRepository, Repository, In } from 'typeorm'; +import { User, ILocalUser, IRemoteUser } from '../entities/user'; +import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..'; +import rap from '@prezzemolo/rap'; +import { ensure } from '../../prelude/ensure'; + +@EntityRepository(User) +export class UserRepository extends Repository<User> { + public async getRelation(me: User['id'], target: User['id']) { + const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ + Followings.findOne({ + followerId: me, + followeeId: target + }), + Followings.findOne({ + followerId: target, + followeeId: me + }), + FollowRequests.findOne({ + followerId: me, + followeeId: target + }), + FollowRequests.findOne({ + followerId: target, + followeeId: me + }), + Blockings.findOne({ + blockerId: me, + blockeeId: target + }), + Blockings.findOne({ + blockerId: target, + blockeeId: me + }), + Mutings.findOne({ + muterId: me, + muteeId: target + }) + ]); + + return { + id: target, + isFollowing: following1 != null, + hasPendingFollowRequestFromYou: followReq1 != null, + hasPendingFollowRequestToYou: followReq2 != null, + isFollowed: following2 != null, + isBlocking: toBlocking != null, + isBlocked: fromBlocked != null, + isMuted: mute != null + }; + } + + public packMany( + users: (User['id'] | User)[], + me?: User['id'] | User | null | undefined, + options?: { + detail?: boolean, + includeSecrets?: boolean, + includeHasUnreadNotes?: boolean + } + ) { + return Promise.all(users.map(u => this.pack(u, me, options))); + } + + public async pack( + src: User['id'] | User, + me?: User['id'] | User | null | undefined, + options?: { + detail?: boolean, + includeSecrets?: boolean, + includeHasUnreadNotes?: boolean + } + ): Promise<Record<string, any>> { + const opts = Object.assign({ + detail: false, + includeSecrets: false + }, options); + + const user = typeof src === 'object' ? src : await this.findOne(src).then(ensure); + const meId = me ? typeof me === 'string' ? me : me.id : null; + + const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null; + const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : []; + const profile = opts.detail ? await UserProfiles.findOne({ userId: user.id }).then(ensure) : null; + + return await rap({ + id: user.id, + name: user.name, + username: user.username, + host: user.host, + avatarUrl: user.avatarUrl, + avatarColor: user.avatarColor, + isAdmin: user.isAdmin || undefined, + isBot: user.isBot || undefined, + isCat: user.isCat || undefined, + isVerified: user.isVerified || undefined, + + // カスタム絵文字添付 + emojis: user.emojis.length > 0 ? Emojis.find({ + where: { + name: In(user.emojis), + host: user.host + }, + select: ['name', 'host', 'url', 'aliases'] + }) : [], + + ...(opts.includeHasUnreadNotes ? { + hasUnreadSpecifiedNotes: NoteUnreads.count({ + where: { userId: user.id, isSpecified: true }, + take: 1 + }).then(count => count > 0), + hasUnreadMentions: NoteUnreads.count({ + where: { userId: user.id }, + take: 1 + }).then(count => count > 0), + } : {}), + + ...(opts.detail ? { + url: profile!.url, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + bannerUrl: user.bannerUrl, + bannerColor: user.bannerColor, + isLocked: user.isLocked, + description: profile!.description, + location: profile!.location, + birthday: profile!.birthday, + followersCount: user.followersCount, + followingCount: user.followingCount, + notesCount: user.notesCount, + pinnedNoteIds: pins.map(pin => pin.noteId), + pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, { + detail: true + }), + } : {}), + + ...(opts.detail && meId === user.id ? { + avatarId: user.avatarId, + bannerId: user.bannerId, + autoWatch: profile!.autoWatch, + alwaysMarkNsfw: profile!.alwaysMarkNsfw, + carefulBot: profile!.carefulBot, + hasUnreadMessagingMessage: MessagingMessages.count({ + where: { + recipientId: user.id, + isRead: false + }, + take: 1 + }).then(count => count > 0), + hasUnreadNotification: Notifications.count({ + where: { + notifieeId: user.id, + isRead: false + }, + take: 1 + }).then(count => count > 0), + pendingReceivedFollowRequestsCount: FollowRequests.count({ + followeeId: user.id + }), + } : {}), + + ...(opts.includeSecrets ? { + clientData: profile!.clientData, + email: profile!.email, + emailVerified: profile!.emailVerified, + } : {}), + + ...(relation ? { + isFollowing: relation.isFollowing, + isFollowed: relation.isFollowed, + hasPendingFollowRequestFromYou: relation.hasPendingFollowRequestFromYou, + hasPendingFollowRequestToYou: relation.hasPendingFollowRequestToYou, + isBlocking: relation.isBlocking, + isBlocked: relation.isBlocked, + isMuted: relation.isMuted, + } : {}) + }); + } + + public isLocalUser(user: User): user is ILocalUser { + return user.host == null; + } + + public isRemoteUser(user: User): user is IRemoteUser { + return !this.isLocalUser(user); + } + + //#region Validators + public validateUsername(username: string, remote = false): boolean { + return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username); + } + + public validatePassword(password: string): boolean { + return typeof password == 'string' && password != ''; + } + + public isValidName(name?: string): boolean { + return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != ''); + } + + public isValidDescription(description: string): boolean { + return typeof description == 'string' && description.length < 500 && description.trim() != ''; + } + + public isValidLocation(location: string): boolean { + return typeof location == 'string' && location.length < 50 && location.trim() != ''; + } + + public isValidBirthday(birthday: string): boolean { + return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday); + } + //#endregion +} diff --git a/src/models/signin.ts b/src/models/signin.ts deleted file mode 100644 index d8b05c0e30..0000000000 --- a/src/models/signin.ts +++ /dev/null @@ -1,34 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; - -const Signin = db.get<ISignin>('signin'); -export default Signin; - -export interface ISignin { - _id: mongo.ObjectID; - createdAt: Date; - userId: mongo.ObjectID; - ip: string; - headers: any; - success: boolean; -} - -/** - * Pack a signin record for API response - * - * @param {any} record - * @return {Promise<any>} - */ -export const pack = ( - record: any -) => new Promise<any>(async (resolve, reject) => { - - const _record = deepcopy(record); - - // Rename _id to id - _record.id = _record._id; - delete _record._id; - - resolve(_record); -}); diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts deleted file mode 100644 index 743d0d2dd9..0000000000 --- a/src/models/sw-subscription.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as mongo from 'mongodb'; -import db from '../db/mongodb'; - -const SwSubscription = db.get<ISwSubscription>('swSubscriptions'); -export default SwSubscription; - -export interface ISwSubscription { - _id: mongo.ObjectID; - userId: mongo.ObjectID; - endpoint: string; - auth: string; - publickey: string; -} diff --git a/src/models/user-list.ts b/src/models/user-list.ts deleted file mode 100644 index e7dd74bdd1..0000000000 --- a/src/models/user-list.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; - -const UserList = db.get<IUserList>('userList'); -export default UserList; - -export interface IUserList { - _id: mongo.ObjectID; - createdAt: Date; - title: string; - userId: mongo.ObjectID; - userIds: mongo.ObjectID[]; -} - -export const pack = ( - userList: string | mongo.ObjectID | IUserList -) => new Promise<any>(async (resolve, reject) => { - let _userList: any; - - if (isObjectId(userList)) { - _userList = await UserList.findOne({ - _id: userList - }); - } else if (typeof userList === 'string') { - _userList = await UserList.findOne({ - _id: new mongo.ObjectID(userList) - }); - } else { - _userList = deepcopy(userList); - } - - if (!_userList) throw `invalid userList arg ${userList}`; - - // Rename _id to id - _userList.id = _userList._id; - delete _userList._id; - - resolve(_userList); -}); diff --git a/src/models/user.ts b/src/models/user.ts deleted file mode 100644 index 0c3f7b5508..0000000000 --- a/src/models/user.ts +++ /dev/null @@ -1,438 +0,0 @@ -import * as mongo from 'mongodb'; -import * as deepcopy from 'deepcopy'; -import rap from '@prezzemolo/rap'; -import db from '../db/mongodb'; -import isObjectId from '../misc/is-objectid'; -import { packMany as packNoteMany } from './note'; -import Following from './following'; -import Blocking from './blocking'; -import Mute from './mute'; -import { getFriendIds } from '../server/api/common/get-friends'; -import config from '../config'; -import FollowRequest from './follow-request'; -import fetchMeta from '../misc/fetch-meta'; -import Emoji from './emoji'; -import { dbLogger } from '../db/logger'; - -const User = db.get<IUser>('users'); - -User.createIndex('createdAt'); -User.createIndex('updatedAt'); -User.createIndex('followersCount'); -User.createIndex('tags'); -User.createIndex('isSuspended'); -User.createIndex('username'); -User.createIndex('usernameLower'); -User.createIndex('host'); -User.createIndex(['username', 'host'], { unique: true }); -User.createIndex(['usernameLower', 'host'], { unique: true }); -User.createIndex('token', { sparse: true, unique: true }); -User.createIndex('uri', { sparse: true, unique: true }); - -export default User; - -type IUserBase = { - _id: mongo.ObjectID; - createdAt: Date; - updatedAt?: Date; - deletedAt?: Date; - followersCount: number; - followingCount: number; - name?: string; - notesCount: number; - username: string; - usernameLower: string; - avatarId: mongo.ObjectID; - bannerId: mongo.ObjectID; - avatarUrl?: string; - bannerUrl?: string; - avatarColor?: any; - bannerColor?: any; - wallpaperId: mongo.ObjectID; - wallpaperUrl?: string; - data: any; - description: string; - lang?: string; - pinnedNoteIds: mongo.ObjectID[]; - emojis?: string[]; - tags?: string[]; - - isDeleted: boolean; - - /** - * 凍結されているか否か - */ - isSuspended: boolean; - - /** - * サイレンスされているか否か - */ - isSilenced: boolean; - - /** - * 鍵アカウントか否か - */ - isLocked: boolean; - - /** - * Botか否か - */ - isBot: boolean; - - /** - * Botからのフォローを承認制にするか - */ - carefulBot: boolean; - - /** - * フォローしているユーザーからのフォローリクエストを自動承認するか - */ - autoAcceptFollowed: boolean; - - /** - * このアカウントに届いているフォローリクエストの数 - */ - pendingReceivedFollowRequestsCount: number; - - host: string; -}; - -export interface ILocalUser extends IUserBase { - host: null; - keypair: string; - email: string; - emailVerified?: boolean; - emailVerifyCode?: string; - password: string; - token: string; - twitter: { - accessToken: string; - accessTokenSecret: string; - userId: string; - screenName: string; - }; - github: { - accessToken: string; - id: string; - login: string; - }; - discord: { - accessToken: string; - refreshToken: string; - expiresDate: number; - id: string; - username: string; - discriminator: string; - }; - profile: { - location: string; - birthday: string; // 'YYYY-MM-DD' - tags: string[]; - }; - fields?: { - name: string; - value: string; - }[]; - isCat: boolean; - isAdmin?: boolean; - isModerator?: boolean; - isVerified?: boolean; - twoFactorSecret: string; - twoFactorEnabled: boolean; - twoFactorTempSecret?: string; - clientSettings: any; - settings: { - autoWatch: boolean; - alwaysMarkNsfw?: boolean; - }; - hasUnreadNotification: boolean; - hasUnreadMessagingMessage: boolean; -} - -export interface IRemoteUser extends IUserBase { - inbox: string; - sharedInbox?: string; - featured?: string; - endpoints: string[]; - uri: string; - url?: string; - publicKey: { - id: string; - publicKeyPem: string; - }; - lastFetchedAt: Date; - isAdmin: false; - isModerator: false; -} - -export type IUser = ILocalUser | IRemoteUser; - -export const isLocalUser = (user: any): user is ILocalUser => - user.host === null; - -export const isRemoteUser = (user: any): user is IRemoteUser => - !isLocalUser(user); - -//#region Validators -export function validateUsername(username: string, remote = false): boolean { - return typeof username == 'string' && (remote ? /^\w([\w-]*\w)?$/ : /^\w{1,20}$/).test(username); -} - -export function validatePassword(password: string): boolean { - return typeof password == 'string' && password != ''; -} - -export function isValidName(name?: string): boolean { - return name === null || (typeof name == 'string' && name.length < 50 && name.trim() != ''); -} - -export function isValidDescription(description: string): boolean { - return typeof description == 'string' && description.length < 500 && description.trim() != ''; -} - -export function isValidLocation(location: string): boolean { - return typeof location == 'string' && location.length < 50 && location.trim() != ''; -} - -export function isValidBirthday(birthday: string): boolean { - return typeof birthday == 'string' && /^([0-9]{4})\-([0-9]{2})-([0-9]{2})$/.test(birthday); -} -//#endregion - -export async function getRelation(me: mongo.ObjectId, target: mongo.ObjectId) { - const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ - Following.findOne({ - followerId: me, - followeeId: target - }), - Following.findOne({ - followerId: target, - followeeId: me - }), - FollowRequest.findOne({ - followerId: me, - followeeId: target - }), - FollowRequest.findOne({ - followerId: target, - followeeId: me - }), - Blocking.findOne({ - blockerId: me, - blockeeId: target - }), - Blocking.findOne({ - blockerId: target, - blockeeId: me - }), - Mute.findOne({ - muterId: me, - muteeId: target - }) - ]); - - return { - id: target, - isFollowing: following1 !== null, - hasPendingFollowRequestFromYou: followReq1 !== null, - hasPendingFollowRequestToYou: followReq2 !== null, - isFollowed: following2 !== null, - isBlocking: toBlocking !== null, - isBlocked: fromBlocked !== null, - isMuted: mute !== null - }; -} - -/** - * Pack a user for API response - * - * @param user target - * @param me? serializee - * @param options? serialize options - * @return Packed user - */ -export const pack = ( - user: string | mongo.ObjectID | IUser, - me?: string | mongo.ObjectID | IUser, - options?: { - detail?: boolean, - includeSecrets?: boolean, - includeHasUnreadNotes?: boolean - } -) => new Promise<any>(async (resolve, reject) => { - const opts = Object.assign({ - detail: false, - includeSecrets: false - }, options); - - let _user: any; - - const fields = opts.detail ? {} : { - name: true, - username: true, - host: true, - avatarColor: true, - avatarUrl: true, - emojis: true, - isCat: true, - isBot: true, - isAdmin: true, - isVerified: true - }; - - // Populate the user if 'user' is ID - if (isObjectId(user)) { - _user = await User.findOne({ - _id: user - }, { fields }); - } else if (typeof user === 'string') { - _user = await User.findOne({ - _id: new mongo.ObjectID(user) - }, { fields }); - } else { - _user = deepcopy(user); - } - - // (データベースの欠損などで)ユーザーがデータベース上に見つからなかったとき - if (_user == null) { - dbLogger.warn(`user not found on database: ${user}`); - return resolve(null); - } - - // Me - const meId: mongo.ObjectID = me - ? isObjectId(me) - ? me as mongo.ObjectID - : typeof me === 'string' - ? new mongo.ObjectID(me) - : (me as IUser)._id - : null; - - // Rename _id to id - _user.id = _user._id; - delete _user._id; - - delete _user.usernameLower; - delete _user.emailVerifyCode; - - if (_user.host == null) { - // Remove private properties - delete _user.keypair; - delete _user.password; - delete _user.token; - delete _user.twoFactorTempSecret; - delete _user.two_factor_temp_secret; // 後方互換性のため - delete _user.twoFactorSecret; - if (_user.twitter) { - delete _user.twitter.accessToken; - delete _user.twitter.accessTokenSecret; - } - if (_user.github) { - delete _user.github.accessToken; - } - if (_user.discord) { - delete _user.discord.accessToken; - delete _user.discord.refreshToken; - delete _user.discord.expiresDate; - } - - // Visible via only the official client - if (!opts.includeSecrets) { - delete _user.email; - delete _user.emailVerified; - delete _user.settings; - delete _user.clientSettings; - } - - if (!opts.detail) { - delete _user.twoFactorEnabled; - } - } else { - delete _user.publicKey; - } - - if (_user.avatarUrl == null) { - _user.avatarUrl = `${config.driveUrl}/default-avatar.jpg`; - } - - if (!meId || !meId.equals(_user.id) || !opts.detail) { - delete _user.avatarId; - delete _user.bannerId; - delete _user.hasUnreadMessagingMessage; - delete _user.hasUnreadNotification; - } - - if (meId && !meId.equals(_user.id) && opts.detail) { - const relation = await getRelation(meId, _user.id); - - _user.isFollowing = relation.isFollowing; - _user.isFollowed = relation.isFollowed; - _user.hasPendingFollowRequestFromYou = relation.hasPendingFollowRequestFromYou; - _user.hasPendingFollowRequestToYou = relation.hasPendingFollowRequestToYou; - _user.isBlocking = relation.isBlocking; - _user.isBlocked = relation.isBlocked; - _user.isMuted = relation.isMuted; - } - - if (opts.detail) { - if (_user.pinnedNoteIds) { - // Populate pinned notes - _user.pinnedNotes = packNoteMany(_user.pinnedNoteIds, meId, { - detail: true - }); - } - - if (meId && !meId.equals(_user.id)) { - const myFollowingIds = await getFriendIds(meId); - - // Get following you know count - _user.followingYouKnowCount = Following.count({ - followeeId: { $in: myFollowingIds }, - followerId: _user.id - }); - - // Get followers you know count - _user.followersYouKnowCount = Following.count({ - followeeId: _user.id, - followerId: { $in: myFollowingIds } - }); - } - } - - if (!opts.includeHasUnreadNotes) { - delete _user.hasUnreadSpecifiedNotes; - delete _user.hasUnreadMentions; - } - - // カスタム絵文字添付 - if (_user.emojis) { - _user.emojis = Emoji.find({ - name: { $in: _user.emojis }, - host: _user.host - }, { - fields: { _id: false } - }); - } - - // resolve promises in _user object - _user = await rap(_user); - - resolve(_user); -}); - -/* -function img(url) { - return { - thumbnail: { - large: `${url}`, - medium: '', - small: '' - } - }; -} -*/ - -export async function fetchProxyAccount(): Promise<ILocalUser> { - const meta = await fetchMeta(); - return await User.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser; -} diff --git a/src/prelude/ensure.ts b/src/prelude/ensure.ts new file mode 100644 index 0000000000..1af281c056 --- /dev/null +++ b/src/prelude/ensure.ts @@ -0,0 +1,10 @@ +/** + * 値が null または undefined の場合はエラーを発生させ、そうでない場合は値をそのまま返します + */ +export function ensure<T>(x: T): NonNullable<T> { + if (x == null) { + throw new Error('ぬるぽ'); + } else { + return x!; + } +} diff --git a/src/prelude/maybe.ts b/src/prelude/maybe.ts index f9ac95c0b5..0b4b543ca5 100644 --- a/src/prelude/maybe.ts +++ b/src/prelude/maybe.ts @@ -1,19 +1,19 @@ -export interface Maybe<T> { - isJust(): this is Just<T>; +export interface IMaybe<T> { + isJust(): this is IJust<T>; } -export type Just<T> = Maybe<T> & { - get(): T -}; +export interface IJust<T> extends IMaybe<T> { + get(): T; +} -export function just<T>(value: T): Just<T> { +export function just<T>(value: T): IJust<T> { return { isJust: () => true, get: () => value }; } -export function nothing<T>(): Maybe<T> { +export function nothing<T>(): IMaybe<T> { return { isJust: () => false, }; diff --git a/src/queue/index.ts b/src/queue/index.ts index d8328a1d57..a010004f15 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -2,17 +2,17 @@ import * as Queue from 'bull'; import * as httpSignature from 'http-signature'; import config from '../config'; -import { ILocalUser } from '../models/user'; +import { ILocalUser } from '../models/entities/user'; import { program } from '../argv'; import processDeliver from './processors/deliver'; import processInbox from './processors/inbox'; import processDb from './processors/db'; import { queueLogger } from './logger'; -import { IDriveFile } from '../models/drive-file'; +import { DriveFile } from '../models/entities/drive-file'; function initializeQueue(name: string) { - return new Queue(name, config.redis != null ? { + return new Queue(name, { redis: { port: config.redis.port, host: config.redis.host, @@ -20,7 +20,15 @@ function initializeQueue(name: string) { db: config.redis.db || 0, }, prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue' - } : null); + }); +} + +function renderError(e: Error): any { + return { + stack: e.stack, + message: e.message, + name: e.name + }; } export const deliverQueue = initializeQueue('deliver'); @@ -34,16 +42,16 @@ deliverQueue .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`)) .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) id=${job.id} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`)) - .on('error', (error) => deliverLogger.error(`error ${error}`)) + .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`)); inboxQueue .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => inboxLogger.debug(`active id=${job.id}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) id=${job.id}`)) - .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`)) - .on('error', (error) => inboxLogger.error(`error ${error}`)) + .on('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`, { job, e: renderError(err) })) + .on('error', (job: any, err: Error) => inboxLogger.error(`error ${err}`, { job, e: renderError(err) })) .on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); export function deliver(user: ILocalUser, content: any, to: any) { @@ -83,15 +91,6 @@ export function inbox(activity: any, signature: httpSignature.IParsedSignature) }); } -export function createDeleteNotesJob(user: ILocalUser) { - return dbQueue.add('deleteNotes', { - user: user - }, { - removeOnComplete: true, - removeOnFail: true - }); -} - export function createDeleteDriveFilesJob(user: ILocalUser) { return dbQueue.add('deleteDriveFiles', { user: user @@ -146,7 +145,7 @@ export function createExportUserListsJob(user: ILocalUser) { }); } -export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_id']) { +export function createImportFollowingJob(user: ILocalUser, fileId: DriveFile['id']) { return dbQueue.add('importFollowing', { user: user, fileId: fileId @@ -156,7 +155,7 @@ export function createImportFollowingJob(user: ILocalUser, fileId: IDriveFile['_ }); } -export function createImportUserListsJob(user: ILocalUser, fileId: IDriveFile['_id']) { +export function createImportUserListsJob(user: ILocalUser, fileId: DriveFile['id']) { return dbQueue.add('importUserLists', { user: user, fileId: fileId diff --git a/src/queue/processors/db/delete-drive-files.ts b/src/queue/processors/db/delete-drive-files.ts index 3de960a25e..4e4eab86b7 100644 --- a/src/queue/processors/db/delete-drive-files.ts +++ b/src/queue/processors/db/delete-drive-files.ts @@ -1,55 +1,55 @@ import * as Bull from 'bull'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; -import User from '../../../models/user'; -import DriveFile from '../../../models/drive-file'; import deleteFile from '../../../services/drive/delete-file'; +import { Users, DriveFiles } from '../../../models'; +import { MoreThan } from 'typeorm'; const logger = queueLogger.createSubLogger('delete-drive-files'); export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> { - logger.info(`Deleting drive files of ${job.data.user._id} ...`); + logger.info(`Deleting drive files of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } let deletedCount = 0; - let ended = false; let cursor: any = null; - while (!ended) { - const files = await DriveFile.find({ - userId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + while (true) { + const files = await DriveFiles.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); if (files.length === 0) { - ended = true; job.progress(100); break; } - cursor = files[files.length - 1]._id; + cursor = files[files.length - 1].id; for (const file of files) { await deleteFile(file); deletedCount++; } - const total = await DriveFile.count({ - userId: user._id, + const total = await DriveFiles.count({ + userId: user.id, }); job.progress(deletedCount / total); } - logger.succ(`All drive files (${deletedCount}) of ${user._id} has been deleted.`); + logger.succ(`All drive files (${deletedCount}) of ${user.id} has been deleted.`); done(); } diff --git a/src/queue/processors/db/delete-notes.ts b/src/queue/processors/db/delete-notes.ts deleted file mode 100644 index 021db8062e..0000000000 --- a/src/queue/processors/db/delete-notes.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as Bull from 'bull'; -import * as mongo from 'mongodb'; - -import { queueLogger } from '../../logger'; -import Note from '../../../models/note'; -import deleteNote from '../../../services/note/delete'; -import User from '../../../models/user'; - -const logger = queueLogger.createSubLogger('delete-notes'); - -export async function deleteNotes(job: Bull.Job, done: any): Promise<void> { - logger.info(`Deleting notes of ${job.data.user._id} ...`); - - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); - - let deletedCount = 0; - let ended = false; - let cursor: any = null; - - while (!ended) { - const notes = await Note.find({ - userId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 - } - }); - - if (notes.length === 0) { - ended = true; - job.progress(100); - break; - } - - cursor = notes[notes.length - 1]._id; - - for (const note of notes) { - await deleteNote(user, note, true); - deletedCount++; - } - - const total = await Note.count({ - userId: user._id, - }); - - job.progress(deletedCount / total); - } - - logger.succ(`All notes (${deletedCount}) of ${user._id} has been deleted.`); - done(); -} diff --git a/src/queue/processors/db/export-blocking.ts b/src/queue/processors/db/export-blocking.ts index 7f32c06472..c4b8c9438d 100644 --- a/src/queue/processors/db/export-blocking.ts +++ b/src/queue/processors/db/export-blocking.ts @@ -1,23 +1,24 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); -import Blocking from '../../../models/blocking'; import { getFullApAccount } from '../../../misc/convert-host'; +import { Users, Blockings } from '../../../models'; +import { MoreThan } from 'typeorm'; const logger = queueLogger.createSubLogger('export-blocking'); export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting blocking of ${job.data.user._id} ...`); + logger.info(`Exporting blocking of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } // Create temp file const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { @@ -32,30 +33,33 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let ended = false; let cursor: any = null; - while (!ended) { - const blockings = await Blocking.find({ - blockerId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + while (true) { + const blockings = await Blockings.find({ + where: { + blockerId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); if (blockings.length === 0) { - ended = true; job.progress(100); break; } - cursor = blockings[blockings.length - 1]._id; + cursor = blockings[blockings.length - 1].id; for (const block of blockings) { - const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } }); + const u = await Users.findOne({ id: block.blockeeId }); + if (u == null) { + exportedCount++; continue; + } + const content = getFullApAccount(u.username, u.host); await new Promise((res, rej) => { stream.write(content + '\n', err => { @@ -70,8 +74,8 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { exportedCount++; } - const total = await Blocking.count({ - blockerId: user._id, + const total = await Blockings.count({ + blockerId: user.id, }); job.progress(exportedCount / total); @@ -83,7 +87,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } diff --git a/src/queue/processors/db/export-following.ts b/src/queue/processors/db/export-following.ts index 019414072a..9fab5bb21a 100644 --- a/src/queue/processors/db/export-following.ts +++ b/src/queue/processors/db/export-following.ts @@ -1,23 +1,24 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); -import Following from '../../../models/following'; import { getFullApAccount } from '../../../misc/convert-host'; +import { Users, Followings } from '../../../models'; +import { MoreThan } from 'typeorm'; const logger = queueLogger.createSubLogger('export-following'); export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting following of ${job.data.user._id} ...`); + logger.info(`Exporting following of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } // Create temp file const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { @@ -32,30 +33,33 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let ended = false; let cursor: any = null; - while (!ended) { - const followings = await Following.find({ - followerId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + while (true) { + const followings = await Followings.find({ + where: { + followerId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); if (followings.length === 0) { - ended = true; job.progress(100); break; } - cursor = followings[followings.length - 1]._id; + cursor = followings[followings.length - 1].id; for (const following of followings) { - const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } }); + const u = await Users.findOne({ id: following.followeeId }); + if (u == null) { + exportedCount++; continue; + } + const content = getFullApAccount(u.username, u.host); await new Promise((res, rej) => { stream.write(content + '\n', err => { @@ -70,8 +74,8 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { exportedCount++; } - const total = await Following.count({ - followerId: user._id, + const total = await Followings.count({ + followerId: user.id, }); job.progress(exportedCount / total); @@ -83,7 +87,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { const fileName = 'following-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } diff --git a/src/queue/processors/db/export-mute.ts b/src/queue/processors/db/export-mute.ts index 5ded7cf651..b957b48b20 100644 --- a/src/queue/processors/db/export-mute.ts +++ b/src/queue/processors/db/export-mute.ts @@ -1,23 +1,24 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); -import Mute from '../../../models/mute'; import { getFullApAccount } from '../../../misc/convert-host'; +import { Users, Mutings } from '../../../models'; +import { MoreThan } from 'typeorm'; const logger = queueLogger.createSubLogger('export-mute'); export async function exportMute(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting mute of ${job.data.user._id} ...`); + logger.info(`Exporting mute of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } // Create temp file const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { @@ -32,30 +33,33 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> { const stream = fs.createWriteStream(path, { flags: 'a' }); let exportedCount = 0; - let ended = false; let cursor: any = null; - while (!ended) { - const mutes = await Mute.find({ - muterId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + while (true) { + const mutes = await Mutings.find({ + where: { + muterId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); if (mutes.length === 0) { - ended = true; job.progress(100); break; } - cursor = mutes[mutes.length - 1]._id; + cursor = mutes[mutes.length - 1].id; for (const mute of mutes) { - const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } }); + const u = await Users.findOne({ id: mute.muteeId }); + if (u == null) { + exportedCount++; continue; + } + const content = getFullApAccount(u.username, u.host); await new Promise((res, rej) => { stream.write(content + '\n', err => { @@ -70,8 +74,8 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> { exportedCount++; } - const total = await Mute.count({ - muterId: user._id, + const total = await Mutings.count({ + muterId: user.id, }); job.progress(exportedCount / total); @@ -83,7 +87,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> { const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } diff --git a/src/queue/processors/db/export-notes.ts b/src/queue/processors/db/export-notes.ts index 8f3cdc5b99..d03a216a59 100644 --- a/src/queue/processors/db/export-notes.ts +++ b/src/queue/processors/db/export-notes.ts @@ -1,22 +1,26 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; -import Note, { INote } from '../../../models/note'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); +import { Users, Notes, Polls } from '../../../models'; +import { MoreThan } from 'typeorm'; +import { Note } from '../../../models/entities/note'; +import { Poll } from '../../../models/entities/poll'; +import { ensure } from '../../../prelude/ensure'; const logger = queueLogger.createSubLogger('export-notes'); export async function exportNotes(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting notes of ${job.data.user._id} ...`); + logger.info(`Exporting notes of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } // Create temp file const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { @@ -42,30 +46,33 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> { }); let exportedNotesCount = 0; - let ended = false; let cursor: any = null; - while (!ended) { - const notes = await Note.find({ - userId: user._id, - ...(cursor ? { _id: { $gt: cursor } } : {}) - }, { - limit: 100, - sort: { - _id: 1 + while (true) { + const notes = await Notes.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}) + }, + take: 100, + order: { + id: 1 } }); if (notes.length === 0) { - ended = true; job.progress(100); break; } - cursor = notes[notes.length - 1]._id; + cursor = notes[notes.length - 1].id; for (const note of notes) { - const content = JSON.stringify(serialize(note)); + let poll: Poll | undefined; + if (note.hasPoll) { + poll = await Polls.findOne({ noteId: note.id }).then(ensure); + } + const content = JSON.stringify(serialize(note, poll)); await new Promise((res, rej) => { stream.write(exportedNotesCount === 0 ? content : ',\n' + content, err => { if (err) { @@ -79,8 +86,8 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> { exportedNotesCount++; } - const total = await Note.count({ - userId: user._id, + const total = await Notes.count({ + userId: user.id, }); job.progress(exportedNotesCount / total); @@ -103,20 +110,20 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> { const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.json'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } -function serialize(note: INote): any { +function serialize(note: Note, poll: Poll | null = null): any { return { - id: note._id, + id: note.id, text: note.text, createdAt: note.createdAt, fileIds: note.fileIds, replyId: note.replyId, renoteId: note.renoteId, - poll: note.poll, + poll: poll, cw: note.cw, viaMobile: note.viaMobile, visibility: note.visibility, diff --git a/src/queue/processors/db/export-user-lists.ts b/src/queue/processors/db/export-user-lists.ts index dfbf152ec0..5cd978c1aa 100644 --- a/src/queue/processors/db/export-user-lists.ts +++ b/src/queue/processors/db/export-user-lists.ts @@ -1,26 +1,27 @@ import * as Bull from 'bull'; import * as tmp from 'tmp'; import * as fs from 'fs'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; import addFile from '../../../services/drive/add-file'; -import User from '../../../models/user'; import dateFormat = require('dateformat'); -import UserList from '../../../models/user-list'; import { getFullApAccount } from '../../../misc/convert-host'; +import { Users, UserLists, UserListJoinings } from '../../../models'; +import { In } from 'typeorm'; const logger = queueLogger.createSubLogger('export-user-lists'); export async function exportUserLists(job: Bull.Job, done: any): Promise<void> { - logger.info(`Exporting user lists of ${job.data.user._id} ...`); + logger.info(`Exporting user lists of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } - const lists = await UserList.find({ - userId: user._id + const lists = await UserLists.find({ + userId: user.id }); // Create temp file @@ -36,18 +37,14 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> { const stream = fs.createWriteStream(path, { flags: 'a' }); for (const list of lists) { - const users = await User.find({ - _id: { $in: list.userIds } - }, { - fields: { - username: true, - host: true - } + const joinings = await UserListJoinings.find({ userListId: list.id }); + const users = await Users.find({ + id: In(joinings.map(j => j.userId)) }); for (const u of users) { const acct = getFullApAccount(u.username, u.host); - const content = `${list.title},${acct}`; + const content = `${list.name},${acct}`; await new Promise((res, rej) => { stream.write(content + '\n', err => { if (err) { @@ -67,7 +64,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> { const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-mm-dd-HH-MM-ss') + '.csv'; const driveFile = await addFile(user, path, fileName); - logger.succ(`Exported to: ${driveFile._id}`); + logger.succ(`Exported to: ${driveFile.id}`); cleanup(); done(); } diff --git a/src/queue/processors/db/import-following.ts b/src/queue/processors/db/import-following.ts index 069afa74c4..8de3193e46 100644 --- a/src/queue/processors/db/import-following.ts +++ b/src/queue/processors/db/import-following.ts @@ -1,32 +1,33 @@ import * as Bull from 'bull'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; -import User from '../../../models/user'; import follow from '../../../services/following/create'; -import DriveFile from '../../../models/drive-file'; -import { getOriginalUrl } from '../../../misc/get-drive-file-url'; import parseAcct from '../../../misc/acct/parse'; -import resolveUser from '../../../remote/resolve-user'; +import { resolveUser } from '../../../remote/resolve-user'; import { downloadTextFile } from '../../../misc/download-text-file'; -import { isSelfHost, toDbHost } from '../../../misc/convert-host'; +import { isSelfHost, toPuny } from '../../../misc/convert-host'; +import { Users, DriveFiles } from '../../../models'; const logger = queueLogger.createSubLogger('import-following'); export async function importFollowing(job: Bull.Job, done: any): Promise<void> { - logger.info(`Importing following of ${job.data.user._id} ...`); + logger.info(`Importing following of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } - const file = await DriveFile.findOne({ - _id: new mongo.ObjectID(job.data.fileId.toString()) + const file = await DriveFiles.findOne({ + id: job.data.fileId }); + if (file == null) { + done(); + return; + } - const url = getOriginalUrl(file); - - const csv = await downloadTextFile(url); + const csv = await downloadTextFile(file.url); let linenum = 0; @@ -36,11 +37,11 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> { try { const { username, host } = parseAcct(line.trim()); - let target = isSelfHost(host) ? await User.findOne({ + let target = isSelfHost(host!) ? await Users.findOne({ host: null, usernameLower: username.toLowerCase() - }) : await User.findOne({ - host: toDbHost(host), + }) : await Users.findOne({ + host: toPuny(host!), usernameLower: username.toLowerCase() }); @@ -55,9 +56,9 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> { } // skip myself - if (target._id.equals(job.data.user._id)) continue; + if (target.id === job.data.user.id) continue; - logger.info(`Follow[${linenum}] ${target._id} ...`); + logger.info(`Follow[${linenum}] ${target.id} ...`); follow(user, target); } catch (e) { diff --git a/src/queue/processors/db/import-user-lists.ts b/src/queue/processors/db/import-user-lists.ts index 50d3c649d4..1e852be945 100644 --- a/src/queue/processors/db/import-user-lists.ts +++ b/src/queue/processors/db/import-user-lists.ts @@ -1,67 +1,68 @@ import * as Bull from 'bull'; -import * as mongo from 'mongodb'; import { queueLogger } from '../../logger'; -import User from '../../../models/user'; -import UserList from '../../../models/user-list'; -import DriveFile from '../../../models/drive-file'; -import { getOriginalUrl } from '../../../misc/get-drive-file-url'; import parseAcct from '../../../misc/acct/parse'; -import resolveUser from '../../../remote/resolve-user'; +import { resolveUser } from '../../../remote/resolve-user'; import { pushUserToUserList } from '../../../services/user-list/push'; import { downloadTextFile } from '../../../misc/download-text-file'; -import { isSelfHost, toDbHost } from '../../../misc/convert-host'; +import { isSelfHost, toPuny } from '../../../misc/convert-host'; +import { DriveFiles, Users, UserLists, UserListJoinings } from '../../../models'; +import { genId } from '../../../misc/gen-id'; const logger = queueLogger.createSubLogger('import-user-lists'); export async function importUserLists(job: Bull.Job, done: any): Promise<void> { - logger.info(`Importing user lists of ${job.data.user._id} ...`); + logger.info(`Importing user lists of ${job.data.user.id} ...`); - const user = await User.findOne({ - _id: new mongo.ObjectID(job.data.user._id.toString()) - }); + const user = await Users.findOne(job.data.user.id); + if (user == null) { + done(); + return; + } - const file = await DriveFile.findOne({ - _id: new mongo.ObjectID(job.data.fileId.toString()) + const file = await DriveFiles.findOne({ + id: job.data.fileId }); + if (file == null) { + done(); + return; + } - const url = getOriginalUrl(file); - - const csv = await downloadTextFile(url); + const csv = await downloadTextFile(file.url); for (const line of csv.trim().split('\n')) { const listName = line.split(',')[0].trim(); const { username, host } = parseAcct(line.split(',')[1].trim()); - let list = await UserList.findOne({ - userId: user._id, - title: listName + let list = await UserLists.findOne({ + userId: user.id, + name: listName }); if (list == null) { - list = await UserList.insert({ + list = await UserLists.save({ + id: genId(), createdAt: new Date(), - userId: user._id, - title: listName, + userId: user.id, + name: listName, userIds: [] }); } - let target = isSelfHost(host) ? await User.findOne({ + let target = isSelfHost(host!) ? await Users.findOne({ host: null, usernameLower: username.toLowerCase() - }) : await User.findOne({ - host: toDbHost(host), + }) : await Users.findOne({ + host: toPuny(host!), usernameLower: username.toLowerCase() }); - if (host == null && target == null) continue; - if (list.userIds.some(id => id.equals(target._id))) continue; - if (target == null) { target = await resolveUser(username, host); } + if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue; + pushUserToUserList(target, list); } diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts index 1bc9a9af7c..921cdf7ab1 100644 --- a/src/queue/processors/db/index.ts +++ b/src/queue/processors/db/index.ts @@ -1,5 +1,4 @@ import * as Bull from 'bull'; -import { deleteNotes } from './delete-notes'; import { deleteDriveFiles } from './delete-drive-files'; import { exportNotes } from './export-notes'; import { exportFollowing } from './export-following'; @@ -10,7 +9,6 @@ import { importFollowing } from './import-following'; import { importUserLists } from './import-user-lists'; const jobs = { - deleteNotes, deleteDriveFiles, exportNotes, exportFollowing, diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts index 28d3a17f6b..8837c80d87 100644 --- a/src/queue/processors/deliver.ts +++ b/src/queue/processors/deliver.ts @@ -1,13 +1,13 @@ import * as Bull from 'bull'; import request from '../../remote/activitypub/request'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; import Logger from '../../services/logger'; +import { Instances } from '../../models'; +import { instanceChart } from '../../services/chart'; const logger = new Logger('deliver'); -let latest: string = null; +let latest: string | null = null; export default async (job: Bull.Job) => { const { host } = new URL(job.data.to); @@ -21,13 +21,11 @@ export default async (job: Bull.Job) => { // Update stats registerOrFetchInstanceDoc(host).then(i => { - Instance.update({ _id: i._id }, { - $set: { - latestRequestSentAt: new Date(), - latestStatus: 200, - lastCommunicatedAt: new Date(), - isNotResponding: false - } + Instances.update(i.id, { + latestRequestSentAt: new Date(), + latestStatus: 200, + lastCommunicatedAt: new Date(), + isNotResponding: false }); instanceChart.requestSent(i.host, true); @@ -37,12 +35,10 @@ export default async (job: Bull.Job) => { } catch (res) { // Update stats registerOrFetchInstanceDoc(host).then(i => { - Instance.update({ _id: i._id }, { - $set: { - latestRequestSentAt: new Date(), - latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null, - isNotResponding: true - } + Instances.update(i.id, { + latestRequestSentAt: new Date(), + latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null, + isNotResponding: true }); instanceChart.requestSent(i.host, false); diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts index 436f3335c8..05fed0566d 100644 --- a/src/queue/processors/inbox.ts +++ b/src/queue/processors/inbox.ts @@ -1,16 +1,20 @@ import * as Bull from 'bull'; import * as httpSignature from 'http-signature'; import parseAcct from '../../misc/acct/parse'; -import User, { IRemoteUser } from '../../models/user'; +import { IRemoteUser } from '../../models/entities/user'; import perform from '../../remote/activitypub/perform'; import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person'; -import { toUnicode } from 'punycode'; import { URL } from 'url'; import { publishApLogStream } from '../../services/stream'; import Logger from '../../services/logger'; import { registerOrFetchInstanceDoc } from '../../services/register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; +import { Instances, Users, UserPublickeys } from '../../models'; +import { instanceChart } from '../../services/chart'; +import { UserPublickey } from '../../models/entities/user-publickey'; +import fetchMeta from '../../misc/fetch-meta'; +import { toPuny, toPunyNullable } from '../../misc/convert-host'; +import { validActor } from '../../remote/activitypub/type'; +import { ensure } from '../../prelude/ensure'; const logger = new Logger('inbox'); @@ -28,9 +32,13 @@ export default async (job: Bull.Job): Promise<void> => { const keyIdLower = signature.keyId.toLowerCase(); let user: IRemoteUser; + let key: UserPublickey; if (keyIdLower.startsWith('acct:')) { - const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); + const acct = parseAcct(keyIdLower.slice('acct:'.length)); + const host = toPunyNullable(acct.host); + const username = toPuny(acct.username); + if (host === null) { logger.warn(`request was made by local user: @${username}`); return; @@ -46,16 +54,21 @@ export default async (job: Bull.Job): Promise<void> => { // ブロックしてたら中断 // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく - const instance = await Instance.findOne({ host: host.toLowerCase() }); - if (instance && instance.isBlocked) { + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(host)) { logger.info(`Blocked request: ${host}`); return; } - user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser; + user = await Users.findOne({ + usernameLower: username.toLowerCase(), + host: host + }) as IRemoteUser; + + key = await UserPublickeys.findOne(user.id).then(ensure); } else { // アクティビティ内のホストの検証 - const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase()); + const host = toPuny(new URL(signature.keyId).hostname); try { ValidateActivity(activity, host); } catch (e) { @@ -65,24 +78,25 @@ export default async (job: Bull.Job): Promise<void> => { // ブロックしてたら中断 // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく - const instance = await Instance.findOne({ host: host.toLowerCase() }); - if (instance && instance.isBlocked) { - logger.warn(`Blocked request: ${host}`); + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(host)) { + logger.info(`Blocked request: ${host}`); return; } - user = await User.findOne({ - host: { $ne: null }, - 'publicKey.id': signature.keyId - }) as IRemoteUser; + key = await UserPublickeys.findOne({ + keyId: signature.keyId + }).then(ensure); + + user = await Users.findOne(key.userId) as IRemoteUser; } // Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了 if (activity.type === 'Update') { - if (activity.object && activity.object.type === 'Person') { + if (activity.object && validActor.includes(activity.object.type)) { if (user == null) { logger.warn('Update activity received, but user not registed.'); - } else if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) { + } else if (!httpSignature.verifySignature(signature, key.keyPem)) { logger.warn('Update activity received, but signature verification failed.'); } else { updatePerson(activity.actor, null, activity.object); @@ -92,15 +106,15 @@ export default async (job: Bull.Job): Promise<void> => { } // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する - if (user === null) { + if (user == null) { user = await resolvePerson(activity.actor) as IRemoteUser; } - if (user === null) { + if (user == null) { throw new Error('failed to resolve user'); } - if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) { + if (!httpSignature.verifySignature(signature, key.keyPem)) { logger.error('signature verification failed'); return; } @@ -116,12 +130,10 @@ export default async (job: Bull.Job): Promise<void> => { // Update stats registerOrFetchInstanceDoc(user.host).then(i => { - Instance.update({ _id: i._id }, { - $set: { - latestRequestReceivedAt: new Date(), - lastCommunicatedAt: new Date(), - isNotResponding: false - } + Instances.update(i.id, { + latestRequestReceivedAt: new Date(), + lastCommunicatedAt: new Date(), + isNotResponding: false }); instanceChart.requestReceived(i.host); @@ -139,7 +151,7 @@ export default async (job: Bull.Job): Promise<void> => { function ValidateActivity(activity: any, host: string) { // id (if exists) if (typeof activity.id === 'string') { - const uriHost = toUnicode(new URL(activity.id).hostname.toLowerCase()); + const uriHost = toPuny(new URL(activity.id).hostname); if (host !== uriHost) { const diag = activity.signature ? '. Has LD-Signature. Forwarded?' : ''; throw new Error(`activity.id(${activity.id}) has different host(${host})${diag}`); @@ -148,7 +160,7 @@ function ValidateActivity(activity: any, host: string) { // actor (if exists) if (typeof activity.actor === 'string') { - const uriHost = toUnicode(new URL(activity.actor).hostname.toLowerCase()); + const uriHost = toPuny(new URL(activity.actor).hostname); if (host !== uriHost) throw new Error('activity.actor has different host'); } @@ -156,13 +168,13 @@ function ValidateActivity(activity: any, host: string) { if (activity.type === 'Create' && activity.object) { // object.id (if exists) if (typeof activity.object.id === 'string') { - const uriHost = toUnicode(new URL(activity.object.id).hostname.toLowerCase()); + const uriHost = toPuny(new URL(activity.object.id).hostname); if (host !== uriHost) throw new Error('activity.object.id has different host'); } // object.attributedTo (if exists) if (typeof activity.object.attributedTo === 'string') { - const uriHost = toUnicode(new URL(activity.object.attributedTo).hostname.toLowerCase()); + const uriHost = toPuny(new URL(activity.object.attributedTo).hostname); if (host !== uriHost) throw new Error('activity.object.attributedTo has different host'); } } diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts index 07c820c28a..377b8dac42 100644 --- a/src/remote/activitypub/kernel/accept/follow.ts +++ b/src/remote/activitypub/kernel/accept/follow.ts @@ -1,21 +1,22 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import config from '../../../../config'; import accept from '../../../../services/following/requests/accept'; import { IFollow } from '../../type'; +import { Users } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id; + if (id == null) throw new Error('missing id'); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const follower = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) + const follower = await Users.findOne({ + id: id.split('/').pop() }); - if (follower === null) { + if (follower == null) { throw new Error('follower not found'); } diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts index 443c1935d6..5a27ce1d4d 100644 --- a/src/remote/activitypub/kernel/accept/index.ts +++ b/src/remote/activitypub/kernel/accept/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import acceptFollow from './follow'; import { IAccept, IFollow } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts index eb2dba5b21..a5b2687416 100644 --- a/src/remote/activitypub/kernel/add/index.ts +++ b/src/remote/activitypub/kernel/add/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IAdd } from '../../type'; import { resolveNote } from '../../models/note'; import { addPinned } from '../../../../services/i/pin'; @@ -14,7 +14,8 @@ export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => { if (activity.target === actor.featured) { const note = await resolveNote(activity.object); - await addPinned(actor, note._id); + if (note == null) throw new Error('note not found'); + await addPinned(actor, note.id); return; } diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts index 5f738da6c7..ebd5a27b92 100644 --- a/src/remote/activitypub/kernel/announce/index.ts +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import announceNote from './note'; import { IAnnounce, INote } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts index 912936bef8..f9822c5187 100644 --- a/src/remote/activitypub/kernel/announce/note.ts +++ b/src/remote/activitypub/kernel/announce/note.ts @@ -1,12 +1,12 @@ import Resolver from '../../resolver'; import post from '../../../../services/note/create'; -import { IRemoteUser, IUser } from '../../../../models/user'; +import { IRemoteUser, User } from '../../../../models/entities/user'; import { IAnnounce, INote } from '../../type'; import { fetchNote, resolveNote } from '../../models/note'; import { resolvePerson } from '../../models/person'; import { apLogger } from '../../logger'; import { extractDbHost } from '../../../../misc/convert-host'; -import Instance from '../../../../models/instance'; +import fetchMeta from '../../../../misc/fetch-meta'; const logger = apLogger; @@ -27,8 +27,8 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity: // アナウンス先をブロックしてたら中断 // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく - const instance = await Instance.findOne({ host: extractDbHost(uri) }); - if (instance && instance.isBlocked) return; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(extractDbHost(uri))) return; // 既に同じURIを持つものが登録されていないかチェック const exist = await fetchNote(uri); @@ -53,16 +53,16 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity: logger.info(`Creating the (Re)Note: ${uri}`); //#region Visibility - const visibility = getVisibility(activity.to, activity.cc, actor); + const visibility = getVisibility(activity.to || [], activity.cc || [], actor); - let visibleUsers: IUser[] = []; + let visibleUsers: User[] = []; if (visibility == 'specified') { - visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri))); + visibleUsers = await Promise.all((note.to || []).map(uri => resolvePerson(uri))); } //#endergion await post(actor, { - createdAt: new Date(activity.published), + createdAt: activity.published ? new Date(activity.published) : null, renote, visibility, visibleUsers, @@ -75,9 +75,6 @@ type visibility = 'public' | 'home' | 'followers' | 'specified'; function getVisibility(to: string[], cc: string[], actor: IRemoteUser): visibility { const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; - to = to || []; - cc = cc || []; - if (to.includes(PUBLIC)) { return 'public'; } else if (cc.includes(PUBLIC)) { diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts index a10163016c..5c247326cb 100644 --- a/src/remote/activitypub/kernel/block/index.ts +++ b/src/remote/activitypub/kernel/block/index.ts @@ -1,28 +1,27 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import { IBlock } from '../../type'; import block from '../../../../services/blocking/create'; import { apLogger } from '../../logger'; +import { Users } from '../../../../models'; +import { IRemoteUser } from '../../../../models/entities/user'; const logger = apLogger; export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); const uri = activity.id || activity; logger.info(`Block: ${uri}`); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const blockee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const blockee = await Users.findOne(id.split('/').pop()); - if (blockee === null) { + if (blockee == null) { throw new Error('blockee not found'); } diff --git a/src/remote/activitypub/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts index 9c19abbcc4..7720e8f1bd 100644 --- a/src/remote/activitypub/kernel/create/image.ts +++ b/src/remote/activitypub/kernel/create/image.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { createImage } from '../../models/image'; export default async function(actor: IRemoteUser, image: any): Promise<void> { diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts index 6e314d0b82..0326b591f8 100644 --- a/src/remote/activitypub/kernel/create/index.ts +++ b/src/remote/activitypub/kernel/create/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import createImage from './image'; import createNote from './note'; import { ICreate } from '../../type'; diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts index 0f874b9fbf..70e61bdf1b 100644 --- a/src/remote/activitypub/kernel/create/note.ts +++ b/src/remote/activitypub/kernel/create/note.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { createNote, fetchNote } from '../../models/note'; /** diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts index c9c385b1fa..fab5e7ab64 100644 --- a/src/remote/activitypub/kernel/delete/index.ts +++ b/src/remote/activitypub/kernel/delete/index.ts @@ -1,9 +1,9 @@ import Resolver from '../../resolver'; import deleteNote from './note'; -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IDelete } from '../../type'; import { apLogger } from '../../logger'; +import { Notes } from '../../../../models'; /** * 削除アクティビティを捌きます @@ -27,7 +27,7 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => { break; case 'Tombstone': - const note = await Note.findOne({ uri }); + const note = await Notes.findOne({ uri }); if (note != null) { deleteNote(actor, uri); } diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts index f67919c56b..b146e68a07 100644 --- a/src/remote/activitypub/kernel/delete/note.ts +++ b/src/remote/activitypub/kernel/delete/note.ts @@ -1,20 +1,20 @@ -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import deleteNode from '../../../../services/note/delete'; import { apLogger } from '../../logger'; +import { Notes } from '../../../../models'; const logger = apLogger; export default async function(actor: IRemoteUser, uri: string): Promise<void> { logger.info(`Deleting the Note: ${uri}`); - const note = await Note.findOne({ uri }); + const note = await Notes.findOne({ uri }); if (note == null) { throw new Error('note not found'); } - if (!note.userId.equals(actor._id)) { + if (note.userId !== actor.id) { throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); } diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts index e2db70b20d..c255067bfd 100644 --- a/src/remote/activitypub/kernel/follow.ts +++ b/src/remote/activitypub/kernel/follow.ts @@ -1,21 +1,20 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import config from '../../../config'; import follow from '../../../services/following/create'; import { IFollow } from '../type'; +import { Users } from '../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const followee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const followee = await Users.findOne(id.split('/').pop()); - if (followee === null) { + if (followee == null) { throw new Error('followee not found'); } diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts index 4f7a5c91fd..d1251817fa 100644 --- a/src/remote/activitypub/kernel/index.ts +++ b/src/remote/activitypub/kernel/index.ts @@ -1,5 +1,5 @@ import { Object } from '../type'; -import { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import create from './create'; import performDeleteActivity from './delete'; import performUpdateActivity from './update'; @@ -71,7 +71,7 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { default: apLogger.warn(`unknown activity type: ${(activity as any).type}`); - return null; + return; } }; diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts index ed35da8133..a08b453a89 100644 --- a/src/remote/activitypub/kernel/like.ts +++ b/src/remote/activitypub/kernel/like.ts @@ -1,19 +1,19 @@ -import * as mongo from 'mongodb'; -import Note from '../../../models/note'; -import { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import { ILike } from '../type'; import create from '../../../services/note/reaction/create'; +import { Notes } from '../../../models'; export default async (actor: IRemoteUser, activity: ILike) => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); // Transform: // https://misskey.ex/notes/xxxx to // xxxx - const noteId = new mongo.ObjectID(id.split('/').pop()); + const noteId = id.split('/').pop(); - const note = await Note.findOne({ _id: noteId }); - if (note === null) { + const note = await Notes.findOne(noteId); + if (note == null) { throw new Error(); } diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts index 35cd2ec0c9..d8b5a4b9b9 100644 --- a/src/remote/activitypub/kernel/reject/follow.ts +++ b/src/remote/activitypub/kernel/reject/follow.ts @@ -1,21 +1,20 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import config from '../../../../config'; import reject from '../../../../services/following/requests/reject'; import { IFollow } from '../../type'; +import { Users } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id; + if (id == null) throw new Error('missing id'); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const follower = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const follower = await Users.findOne(id.split('/').pop()); - if (follower === null) { + if (follower == null) { throw new Error('follower not found'); } diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts index c3585abbb6..8ece5cf174 100644 --- a/src/remote/activitypub/kernel/reject/index.ts +++ b/src/remote/activitypub/kernel/reject/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import rejectFollow from './follow'; import { IReject, IFollow } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts index 91b207c80d..32b8d66471 100644 --- a/src/remote/activitypub/kernel/remove/index.ts +++ b/src/remote/activitypub/kernel/remove/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IRemove } from '../../type'; import { resolveNote } from '../../models/note'; import { removePinned } from '../../../../services/i/pin'; @@ -14,7 +14,8 @@ export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => { if (activity.target === actor.featured) { const note = await resolveNote(activity.object); - await removePinned(actor, note._id); + if (note == null) throw new Error('note not found'); + await removePinned(actor, note.id); return; } diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts index 4a22ac7924..8ef70a9bef 100644 --- a/src/remote/activitypub/kernel/undo/block.ts +++ b/src/remote/activitypub/kernel/undo/block.ts @@ -1,28 +1,27 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import { IBlock } from '../../type'; import unblock from '../../../../services/blocking/delete'; import { apLogger } from '../../logger'; +import { IRemoteUser } from '../../../../models/entities/user'; +import { Users } from '../../../../models'; const logger = apLogger; export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); const uri = activity.id || activity; logger.info(`UnBlock: ${uri}`); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const blockee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const blockee = await Users.findOne(id.split('/').pop()); - if (blockee === null) { + if (blockee == null) { throw new Error('blockee not found'); } diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts index af06aa5b31..d75f055640 100644 --- a/src/remote/activitypub/kernel/undo/follow.ts +++ b/src/remote/activitypub/kernel/undo/follow.ts @@ -1,24 +1,21 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import unfollow from '../../../../services/following/delete'; import cancelRequest from '../../../../services/following/requests/cancel'; import { IFollow } from '../../type'; -import FollowRequest from '../../../../models/follow-request'; -import Following from '../../../../models/following'; +import { IRemoteUser } from '../../../../models/entities/user'; +import { Users, FollowRequests, Followings } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const followee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const followee = await Users.findOne(id.split('/').pop()); - if (followee === null) { + if (followee == null) { throw new Error('followee not found'); } @@ -26,14 +23,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); } - const req = await FollowRequest.findOne({ - followerId: actor._id, - followeeId: followee._id + const req = await FollowRequests.findOne({ + followerId: actor.id, + followeeId: followee.id }); - const following = await Following.findOne({ - followerId: actor._id, - followeeId: followee._id + const following = await Followings.findOne({ + followerId: actor.id, + followeeId: followee.id }); if (req) { diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts index 80b44fae04..5f2e58c3bf 100644 --- a/src/remote/activitypub/kernel/undo/index.ts +++ b/src/remote/activitypub/kernel/undo/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IUndo, IFollow, IBlock, ILike } from '../../type'; import unfollow from './follow'; import unblock from './block'; @@ -39,6 +39,4 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => { undoLike(actor, object as ILike); break; } - - return null; }; diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts index b324ec854c..2678828a9a 100644 --- a/src/remote/activitypub/kernel/undo/like.ts +++ b/src/remote/activitypub/kernel/undo/like.ts @@ -1,20 +1,20 @@ -import * as mongo from 'mongodb'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { ILike } from '../../type'; -import Note from '../../../../models/note'; import deleteReaction from '../../../../services/note/reaction/delete'; +import { Notes } from '../../../../models'; /** * Process Undo.Like activity */ export default async (actor: IRemoteUser, activity: ILike): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); - const noteId = new mongo.ObjectID(id.split('/').pop()); + const noteId = id.split('/').pop(); - const note = await Note.findOne({ _id: noteId }); - if (note === null) { - throw 'note not found'; + const note = await Notes.findOne(noteId); + if (note == null) { + throw new Error('note not found'); } await deleteReaction(actor, note); diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts index 49b730391a..b8dff73395 100644 --- a/src/remote/activitypub/kernel/update/index.ts +++ b/src/remote/activitypub/kernel/update/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IUpdate, IObject } from '../../type'; import { apLogger } from '../../logger'; import { updateQuestion } from '../../models/question'; diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts index 967ee65544..dba915fee9 100644 --- a/src/remote/activitypub/misc/get-note-html.ts +++ b/src/remote/activitypub/misc/get-note-html.ts @@ -1,9 +1,9 @@ -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; import { toHtml } from '../../../mfm/toHtml'; import { parse } from '../../../mfm/parse'; -export default function(note: INote) { - let html = toHtml(parse(note.text), note.mentionedRemoteUsers); +export default function(note: Note) { + let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers)); if (html == null) html = '<p>.</p>'; return html; diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts index bd97d13d27..f8b35ea21c 100644 --- a/src/remote/activitypub/models/image.ts +++ b/src/remote/activitypub/models/image.ts @@ -1,19 +1,21 @@ import uploadFromUrl from '../../../services/drive/upload-from-url'; -import { IRemoteUser } from '../../../models/user'; -import DriveFile, { IDriveFile } from '../../../models/drive-file'; +import { IRemoteUser } from '../../../models/entities/user'; import Resolver from '../resolver'; import fetchMeta from '../../../misc/fetch-meta'; import { apLogger } from '../logger'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; +import { ensure } from '../../../prelude/ensure'; const logger = apLogger; /** * Imageを作成します。 */ -export async function createImage(actor: IRemoteUser, value: any): Promise<IDriveFile> { +export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - return null; + throw new Error('actor has been suspended'); } const image = await new Resolver().resolve(value) as any; @@ -27,30 +29,18 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv const instance = await fetchMeta(); const cache = instance.cacheRemoteFiles; - let file; - try { - file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache); - } catch (e) { - // 4xxの場合は添付されてなかったことにする - if (e >= 400 && e < 500) { - logger.warn(`Ignored image: ${image.url} - ${e}`); - return null; - } - throw e; - } + let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache); - if (file.metadata.isRemote) { + if (file.isLink) { // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 // URLを更新する - if (file.metadata.url !== image.url) { - file = await DriveFile.findOneAndUpdate({ _id: file._id }, { - $set: { - 'metadata.url': image.url, - 'metadata.uri': image.url - } - }, { - returnNewDocument: true + if (file.url !== image.url) { + await DriveFiles.update({ id: file.id }, { + url: image.url, + uri: image.url }); + + file = await DriveFiles.findOne(file.id).then(ensure); } } @@ -63,7 +53,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> { +export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> { // TODO // リモートサーバーからフェッチしてきて登録 diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 6251621527..8842342342 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -1,26 +1,27 @@ -import * as mongo from 'mongodb'; import * as promiseLimit from 'promise-limit'; import config from '../../../config'; import Resolver from '../resolver'; -import Note, { INote } from '../../../models/note'; import post from '../../../services/note/create'; -import { INote as INoteActivityStreamsObject, IObject } from '../type'; import { resolvePerson, updatePerson } from './person'; import { resolveImage } from './image'; -import { IRemoteUser, IUser } from '../../../models/user'; +import { IRemoteUser, User } from '../../../models/entities/user'; import { fromHtml } from '../../../mfm/fromHtml'; -import Emoji, { IEmoji } from '../../../models/emoji'; import { ITag, extractHashtags } from './tag'; -import { toUnicode } from 'punycode'; import { unique, concat, difference } from '../../../prelude/array'; import { extractPollFromQuestion } from './question'; import vote from '../../../services/note/polls/vote'; import { apLogger } from '../logger'; -import { IDriveFile } from '../../../models/drive-file'; +import { DriveFile } from '../../../models/entities/drive-file'; import { deliverQuestionUpdate } from '../../../services/note/polls/update'; -import Instance from '../../../models/instance'; -import { extractDbHost } from '../../../misc/convert-host'; +import { extractDbHost, toPuny } from '../../../misc/convert-host'; +import { Notes, Emojis, Polls } from '../../../models'; +import { Note } from '../../../models/entities/note'; +import { IObject, INote } from '../type'; +import { Emoji } from '../../../models/entities/emoji'; +import { genId } from '../../../misc/gen-id'; +import fetchMeta from '../../../misc/fetch-meta'; +import { ensure } from '../../../prelude/ensure'; const logger = apLogger; @@ -29,17 +30,18 @@ const logger = apLogger; * * Misskeyに対象のNoteが登録されていればそれを返します。 */ -export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> { +export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { const uri = typeof value == 'string' ? value : value.id; + if (uri == null) throw new Error('missing uri'); // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - return await Note.findOne({ _id: id }); + const id = uri.split('/').pop(); + return await Notes.findOne(id).then(x => x || null); } //#region このサーバーに既に登録されていたらそれを返す - const exist = await Note.findOne({ uri }); + const exist = await Notes.findOne({ uri }); if (exist) { return exist; @@ -52,7 +54,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P /** * Noteを作成します。 */ -export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> { +export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note | null> { if (resolver == null) resolver = new Resolver(); const object: any = await resolver.resolve(value); @@ -65,21 +67,21 @@ export async function createNote(value: any, resolver?: Resolver, silent = false value: value, object: object }); - return null; + throw new Error('invalid note'); } - const note: INoteActivityStreamsObject = object; + const note: INote = object; logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await resolvePerson(note.attributedTo, null, resolver) as IRemoteUser; + const actor = await resolvePerson(note.attributedTo, resolver) as IRemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - return null; + throw new Error('actor has been suspended'); } //#region Visibility @@ -87,7 +89,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc; let visibility = 'public'; - let visibleUsers: IUser[] = []; + let visibleUsers: User[] = []; if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) { if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) { visibility = 'home'; @@ -95,9 +97,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false visibility = 'followers'; } else { visibility = 'specified'; - visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver))); + visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, resolver))); } -} + } //#endergion const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver); @@ -113,25 +115,27 @@ export async function createNote(value: any, resolver?: Resolver, silent = false note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; const files = note.attachment .map(attach => attach.sensitive = note.sensitive) - ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>))) + ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>))) .filter(image => image != null) : []; // リプライ - const reply: INote = note.inReplyTo - ? await resolveNote(note.inReplyTo, resolver).catch(e => { - // 4xxの場合はリプライしてないことにする - if (e.statusCode >= 400 && e.statusCode < 500) { - logger.warn(`Ignored inReplyTo ${note.inReplyTo} - ${e.statusCode} `); - return null; + const reply: Note | null = note.inReplyTo + ? await resolveNote(note.inReplyTo, resolver).then(x => { + if (x == null) { + logger.warn(`Specified inReplyTo, but nout found`); + throw new Error('inReplyTo not found'); + } else { + return x; } + }).catch(e => { logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`); throw e; }) : null; // 引用 - let quote: INote; + let quote: Note | undefined | null; if (note._misskey_quote && typeof note._misskey_quote == 'string') { quote = await resolveNote(note._misskey_quote).catch(e => { @@ -148,25 +152,27 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const cw = note.summary === '' ? null : note.summary; // テキストのパース - const text = note._misskey_content || fromHtml(note.content); + const text = note._misskey_content || (note.content ? fromHtml(note.content) : null); // vote - if (reply && reply.poll) { + if (reply && reply.hasPoll) { + const poll = await Polls.findOne(reply.id).then(ensure); + const tryCreateVote = async (name: string, index: number): Promise<null> => { - if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) { + if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); } else if (index >= 0) { logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); await vote(actor, reply, index); // リモートフォロワーにUpdate配信 - deliverQuestionUpdate(reply._id); + deliverQuestionUpdate(reply.id); } return null; }; if (note.name) { - return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name)); + return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); } // 後方互換性のため @@ -179,9 +185,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false } } - const emojis = await extractEmojis(note.tag, actor.host).catch(e => { + const emojis = await extractEmojis(note.tag || [], actor.host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const apEmojis = emojis.map(emoji => emoji.name); @@ -195,7 +201,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false } return await post(actor, { - createdAt: new Date(note.published), + createdAt: note.published ? new Date(note.published) : null, files, reply, renote: quote, @@ -222,13 +228,14 @@ export async function createNote(value: any, resolver?: Resolver, silent = false * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> { +export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { const uri = typeof value == 'string' ? value : value.id; + if (uri == null) throw new Error('missing uri'); // ブロックしてたら中断 // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく - const instance = await Instance.findOne({ host: extractDbHost(uri) }); - if (instance && instance.isBlocked) throw { statusCode: 451 }; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 }; //#region このサーバーに既に登録されていたらそれを返す const exist = await fetchNote(uri); @@ -241,65 +248,81 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver): // リモートサーバーからフェッチしてきて登録 // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 - return await createNote(uri, resolver); + return await createNote(uri, resolver).catch(e => { + if (e.name === 'duplicated') { + return fetchNote(uri).then(note => { + if (note == null) { + throw new Error('something happened'); + } else { + return note; + } + }); + } else { + throw e; + } + }); } -export async function extractEmojis(tags: ITag[], host_: string) { - const host = toUnicode(host_.toLowerCase()); +export async function extractEmojis(tags: ITag[], host: string): Promise<Emoji[]> { + host = toPuny(host); if (!tags) return []; - const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url); + const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url && tag.name); - return await Promise.all( - eomjiTags.map(async tag => { - const name = tag.name.replace(/^:/, '').replace(/:$/, ''); + return await Promise.all(eomjiTags.map(async tag => { + const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); - const exists = await Emoji.findOne({ - host, - name - }); + const exists = await Emojis.findOne({ + host, + name + }); - if (exists) { - if ((tag.updated != null && exists.updatedAt == null) - || (tag.id != null && exists.uri == null) - || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) { - return await Emoji.findOneAndUpdate({ - host, - name, - }, { - $set: { - uri: tag.id, - url: tag.icon.url, - updatedAt: new Date(tag.updated), - } - }); - } - return exists; + if (exists) { + if ((tag.updated != null && exists.updatedAt == null) + || (tag.id != null && exists.uri == null) + || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) + ) { + await Emojis.update({ + host, + name, + }, { + uri: tag.id, + url: tag.icon!.url, + updatedAt: new Date(tag.updated!), + }); + + return await Emojis.findOne({ + host, + name + }) as Emoji; } - logger.info(`register emoji host=${host}, name=${name}`); + return exists; + } + + logger.info(`register emoji host=${host}, name=${name}`); - return await Emoji.insert({ - host, - name, - uri: tag.id, - url: tag.icon.url, - updatedAt: tag.updated ? new Date(tag.updated) : undefined, - aliases: [] - }); - }) - ); + return await Emojis.save({ + id: genId(), + host, + name, + uri: tag.id, + url: tag.icon!.url, + updatedAt: tag.updated ? new Date(tag.updated) : undefined, + aliases: [] + } as Partial<Emoji>); + })); } async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: string[], resolver: Resolver) { const ignoreUris = ['https://www.w3.org/ns/activitystreams#Public', `${actor.uri}/followers`]; const uris = difference(unique(concat([to || [], cc || []])), ignoreUris); - const limit = promiseLimit(2); + const limit = promiseLimit<User | null>(2); const users = await Promise.all( - uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<IUser>) + uris.map(uri => limit(() => resolvePerson(uri, resolver).catch(() => null)) as Promise<User | null>) ); - return users.filter(x => x != null); + return users.filter(x => x != null) as User[]; } diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index d27c937988..c1c07c7bbf 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -1,29 +1,32 @@ -import * as mongo from 'mongodb'; import * as promiseLimit from 'promise-limit'; -import { toUnicode } from 'punycode'; import config from '../../../config'; -import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user'; import Resolver from '../resolver'; import { resolveImage } from './image'; import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'; -import { IDriveFile } from '../../../models/drive-file'; -import Meta from '../../../models/meta'; +import { DriveFile } from '../../../models/entities/drive-file'; import { fromHtml } from '../../../mfm/fromHtml'; -import usersChart from '../../../services/chart/users'; -import instanceChart from '../../../services/chart/instance'; import { URL } from 'url'; import { resolveNote, extractEmojis } from './note'; import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; -import Instance from '../../../models/instance'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; -import { IEmoji } from '../../../models/emoji'; import { ITag, extractHashtags } from './tag'; -import Following from '../../../models/following'; import { IIdentifier } from './identifier'; import { apLogger } from '../logger'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; import { updateHashtag } from '../../../services/update-hashtag'; +import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '../../../models'; +import { User, IRemoteUser } from '../../../models/entities/user'; +import { Emoji } from '../../../models/entities/emoji'; +import { UserNotePining } from '../../../models/entities/user-note-pinings'; +import { genId } from '../../../misc/gen-id'; +import { instanceChart, usersChart } from '../../../services/chart'; +import { UserPublickey } from '../../../models/entities/user-publickey'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; +import { toPuny } from '../../../misc/convert-host'; +import { UserProfile } from '../../../models/entities/user-profile'; +import { validActor } from '../../../remote/activitypub/type'; +import { getConnection } from 'typeorm'; +import { ensure } from '../../../prelude/ensure'; const logger = apLogger; /** @@ -32,13 +35,13 @@ const logger = apLogger; * @param uri Fetch target URI */ function validatePerson(x: any, uri: string) { - const expectHost = toUnicode(new URL(uri).hostname.toLowerCase()); + const expectHost = toPuny(new URL(uri).hostname); if (x == null) { return new Error('invalid person: object is null'); } - if (x.type != 'Person' && x.type != 'Service') { + if (!validActor.includes(x.type)) { return new Error(`invalid person: object is not a person or service '${x.type}'`); } @@ -50,11 +53,11 @@ function validatePerson(x: any, uri: string) { return new Error('invalid person: inbox is not a string'); } - if (!validateUsername(x.preferredUsername, true)) { + if (!Users.validateUsername(x.preferredUsername, true)) { return new Error('invalid person: invalid username'); } - if (!isValidName(x.name == '' ? null : x.name)) { + if (!Users.isValidName(x.name == '' ? null : x.name)) { return new Error('invalid person: invalid name'); } @@ -62,7 +65,7 @@ function validatePerson(x: any, uri: string) { return new Error('invalid person: id is not a string'); } - const idHost = toUnicode(new URL(x.id).hostname.toLowerCase()); + const idHost = toPuny(new URL(x.id).hostname); if (idHost !== expectHost) { return new Error('invalid person: id has different host'); } @@ -71,7 +74,7 @@ function validatePerson(x: any, uri: string) { return new Error('invalid person: publicKey.id is not a string'); } - const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase()); + const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname); if (publicKeyIdHost !== expectHost) { return new Error('invalid person: publicKey.id has different host'); } @@ -84,17 +87,17 @@ function validatePerson(x: any, uri: string) { * * Misskeyに対象のPersonが登録されていればそれを返します。 */ -export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> { - if (typeof uri !== 'string') throw 'uri is not string'; +export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> { + if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - return await User.findOne({ _id: id }); + const id = uri.split('/').pop(); + return await Users.findOne(id).then(x => x || null); } //#region このサーバーに既に登録されていたらそれを返す - const exist = await User.findOne({ uri }); + const exist = await Users.findOne({ uri }); if (exist) { return exist; @@ -107,8 +110,8 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUs /** * Personを作成します。 */ -export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> { - if (typeof uri !== 'string') throw 'uri is not string'; +export async function createPerson(uri: string, resolver?: Resolver): Promise<User> { + if (typeof uri !== 'string') throw new Error('uri is not string'); if (resolver == null) resolver = new Resolver(); @@ -124,24 +127,9 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU logger.info(`Creating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ - resolver.resolve(person.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ) - ]); + const host = toPuny(new URL(object.id).hostname); - const host = toUnicode(new URL(object.id).hostname.toLowerCase()); - - const { fields, services } = analyzeAttachments(person.attachment); + const { fields } = analyzeAttachments(person.attachment || []); const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()); @@ -150,39 +138,45 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU // Create user let user: IRemoteUser; try { - user = await User.insert({ - avatarId: null, - bannerId: null, - createdAt: Date.parse(person.published) || null, - lastFetchedAt: new Date(), - description: fromHtml(person.summary), - followersCount, - followingCount, - notesCount, - name: person.name, - isLocked: person.manuallyApprovesFollowers, - username: person.preferredUsername, - usernameLower: person.preferredUsername.toLowerCase(), - host, - publicKey: { - id: person.publicKey.id, - publicKeyPem: person.publicKey.publicKeyPem - }, - inbox: person.inbox, - sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), - featured: person.featured, - endpoints: person.endpoints, - uri: person.id, - url: person.url, - fields, - ...services, - tags, - isBot, - isCat: (person as any).isCat === true - }) as IRemoteUser; + // Start transaction + await getConnection().transaction(async transactionalEntityManager => { + user = await transactionalEntityManager.save(new User({ + id: genId(), + avatarId: null, + bannerId: null, + createdAt: new Date(), + lastFetchedAt: new Date(), + name: person.name, + isLocked: person.manuallyApprovesFollowers, + username: person.preferredUsername, + usernameLower: person.preferredUsername.toLowerCase(), + host, + inbox: person.inbox, + sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), + featured: person.featured, + uri: person.id, + tags, + isBot, + isCat: (person as any).isCat === true + })) as IRemoteUser; + + await transactionalEntityManager.save(new UserProfile({ + userId: user.id, + description: person.summary ? fromHtml(person.summary) : null, + url: person.url, + fields, + userHost: host + })); + + await transactionalEntityManager.save(new UserPublickey({ + userId: user.id, + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem + })); + }); } catch (e) { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { throw new Error('already registered'); } @@ -192,83 +186,66 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU // Register host registerOrFetchInstanceDoc(host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - usersCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'usersCount', 1); instanceChart.newUser(i.host); }); - //#region Increment users count - Meta.update({}, { - $inc: { - 'stats.usersCount': 1 - } - }, { upsert: true }); - - usersChart.update(user, true); - //#endregion + usersChart.update(user!, true); // ハッシュタグ更新 - for (const tag of tags) updateHashtag(user, tag, true, true); - for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false); + for (const tag of tags) updateHashtag(user!, tag, true, true); + for (const tag of (user!.tags || []).filter(x => !tags.includes(x))) updateHashtag(user!, tag, true, false); //#region アイコンとヘッダー画像をフェッチ - const [avatar, banner] = (await Promise.all<IDriveFile>([ + const [avatar, banner] = (await Promise.all<DriveFile | null>([ person.icon, person.image ].map(img => img == null ? Promise.resolve(null) - : resolveImage(user, img).catch(() => null) + : resolveImage(user!, img).catch(() => null) ))); - const avatarId = avatar ? avatar._id : null; - const bannerId = banner ? banner._id : null; - const avatarUrl = getDriveFileUrl(avatar, true); - const bannerUrl = getDriveFileUrl(banner, false); - const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null; - const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null; + const avatarId = avatar ? avatar.id : null; + const bannerId = banner ? banner.id : null; + const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar) : null; + const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null; + const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null; + const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null; - await User.update({ _id: user._id }, { - $set: { - avatarId, - bannerId, - avatarUrl, - bannerUrl, - avatarColor, - bannerColor - } + await Users.update(user!.id, { + avatarId, + bannerId, + avatarUrl, + bannerUrl, + avatarColor, + bannerColor }); - user.avatarId = avatarId; - user.bannerId = bannerId; - user.avatarUrl = avatarUrl; - user.bannerUrl = bannerUrl; - user.avatarColor = avatarColor; - user.bannerColor = bannerColor; + user!.avatarId = avatarId; + user!.bannerId = bannerId; + user!.avatarUrl = avatarUrl; + user!.bannerUrl = bannerUrl; + user!.avatarColor = avatarColor; + user!.bannerColor = bannerColor; //#endregion //#region カスタム絵文字取得 - const emojis = await extractEmojis(person.tag, host).catch(e => { + const emojis = await extractEmojis(person.tag || [], host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); - await User.update({ _id: user._id }, { - $set: { - emojis: emojiNames - } + await Users.update(user!.id, { + emojis: emojiNames }); //#endregion - await updateFeatured(user._id).catch(err => logger.error(err)); + await updateFeatured(user!.id).catch(err => logger.error(err)); - return user; + return user!; } /** @@ -278,8 +255,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU * @param resolver Resolver * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) */ -export async function updatePerson(uri: string, resolver?: Resolver, hint?: object): Promise<void> { - if (typeof uri !== 'string') throw 'uri is not string'; +export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: object): Promise<void> { + if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ if (uri.startsWith(config.url + '/')) { @@ -287,7 +264,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje } //#region このサーバーに既に登録されているか - const exist = await User.findOne({ uri }) as IRemoteUser; + const exist = await Users.findOne({ uri }) as IRemoteUser; if (exist == null) { return; @@ -295,10 +272,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje //#endregion // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する - await User.update({ _id: exist._id }, { - $set: { - lastFetchedAt: new Date(), - }, + await Users.update(exist.id, { + lastFetchedAt: new Date(), }); if (resolver == null) resolver = new Resolver(); @@ -315,23 +290,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje logger.info(`Updating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ - resolver.resolve(person.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ) - ]); - // アイコンとヘッダー画像をフェッチ - const [avatar, banner] = (await Promise.all<IDriveFile>([ + const [avatar, banner] = (await Promise.all<DriveFile | null>([ person.icon, person.image ].map(img => @@ -341,14 +301,14 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje ))); // カスタム絵文字取得 - const emojis = await extractEmojis(person.tag, exist.host).catch(e => { + const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); - const { fields, services } = analyzeAttachments(person.attachment); + const { fields, services } = analyzeAttachments(person.attachment || []); const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()); @@ -358,41 +318,45 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), featured: person.featured, emojis: emojiNames, - description: fromHtml(person.summary), - followersCount, - followingCount, - notesCount, + description: person.summary ? fromHtml(person.summary) : null, name: person.name, url: person.url, endpoints: person.endpoints, fields, - ...services, tags, isBot: object.type == 'Service', isCat: (person as any).isCat === true, isLocked: person.manuallyApprovesFollowers, - createdAt: Date.parse(person.published) || null, - publicKey: { - id: person.publicKey.id, - publicKeyPem: person.publicKey.publicKeyPem - }, - } as any; + } as Partial<User>; if (avatar) { - updates.avatarId = avatar._id; - updates.avatarUrl = getDriveFileUrl(avatar, true); - updates.avatarColor = avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null; + updates.avatarId = avatar.id; + updates.avatarUrl = DriveFiles.getPublicUrl(avatar); + updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null; } if (banner) { - updates.bannerId = banner._id; - updates.bannerUrl = getDriveFileUrl(banner, true); - updates.bannerColor = banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null; + updates.bannerId = banner.id; + updates.bannerUrl = DriveFiles.getPublicUrl(banner); + updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null; } // Update user - await User.update({ _id: exist._id }, { - $set: updates + await Users.update(exist.id, updates); + + await UserPublickeys.update({ userId: exist.id }, { + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem + }); + + await UserProfiles.update({ userId: exist.id }, { + twitterUserId: services.twitter.userId, + twitterScreenName: services.twitter.screenName, + githubId: services.github.id, + githubLogin: services.github.login, + discordId: services.discord.id, + discordUsername: services.discord.username, + discordDiscriminator: services.discord.discriminator, }); // ハッシュタグ更新 @@ -400,17 +364,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje for (const tag of (exist.tags || []).filter(x => !tags.includes(x))) updateHashtag(exist, tag, true, false); // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする - await Following.update({ - followerId: exist._id - }, { - $set: { - '_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined) - } + await Followings.update({ + followerId: exist.id }, { - multi: true + followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined) }); - await updateFeatured(exist._id).catch(err => logger.error(err)); + await updateFeatured(exist.id).catch(err => logger.error(err)); } /** @@ -419,8 +379,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> { - if (typeof uri !== 'string') throw 'uri is not string'; +export async function resolvePerson(uri: string, resolver?: Resolver): Promise<User> { + if (typeof uri !== 'string') throw new Error('uri is not string'); //#region このサーバーに既に登録されていたらそれを返す const exist = await fetchPerson(uri); @@ -479,22 +439,25 @@ export function analyzeAttachments(attachments: ITag[]) { }[] = []; const services: { [x: string]: any } = {}; - if (Array.isArray(attachments)) - for (const attachment of attachments.filter(isPropertyValue)) - if (isPropertyValue(attachment.identifier)) - addService(services, attachment.identifier); - else + if (Array.isArray(attachments)) { + for (const attachment of attachments.filter(isPropertyValue)) { + if (isPropertyValue(attachment.identifier!)) { + addService(services, attachment.identifier!); + } else { fields.push({ - name: attachment.name, - value: fromHtml(attachment.value) + name: attachment.name!, + value: fromHtml(attachment.value!) }); + } + } + } return { fields, services }; } -export async function updateFeatured(userId: mongo.ObjectID) { - const user = await User.findOne({ _id: userId }); - if (!isRemoteUser(user)) return; +export async function updateFeatured(userId: User['id']) { + const user = await Users.findOne(userId).then(ensure); + if (!Users.isRemoteUser(user)) return; if (!user.featured) return; logger.info(`Updating the featured: ${user.uri}`); @@ -511,15 +474,18 @@ export async function updateFeatured(userId: mongo.ObjectID) { if (!Array.isArray(items)) throw new Error(`Collection items is not an array`); // Resolve and regist Notes - const limit = promiseLimit(2); + const limit = promiseLimit<Note | null>(2); const featuredNotes = await Promise.all(items .filter(item => item.type === 'Note') .slice(0, 5) - .map(item => limit(() => resolveNote(item, resolver)) as Promise<INote>)); + .map(item => limit(() => resolveNote(item, resolver)))); - await User.update({ _id: user._id }, { - $set: { - pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id) - } - }); + for (const note of featuredNotes.filter(note => note != null)) { + UserNotePinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + noteId: note!.id + } as UserNotePining); + } } diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts index c073684349..5c126c3a56 100644 --- a/src/remote/activitypub/models/question.ts +++ b/src/remote/activitypub/models/question.ts @@ -1,8 +1,9 @@ import config from '../../../config'; -import Note, { IChoice, IPoll } from '../../../models/note'; import Resolver from '../resolver'; import { IQuestion } from '../type'; import { apLogger } from '../logger'; +import { Notes, Polls } from '../../../models'; +import { IPoll } from '../../../models/entities/poll'; export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> { const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source; @@ -10,18 +11,18 @@ export async function extractPollFromQuestion(source: string | IQuestion): Promi const expiresAt = question.endTime ? new Date(question.endTime) : null; if (multiple && !question.anyOf) { - throw 'invalid question'; + throw new Error('invalid question'); } - const choices = question[multiple ? 'anyOf' : 'oneOf'] - .map((x, i) => ({ - id: i, - text: x.name, - votes: x.replies && x.replies.totalItems || x._misskey_votes || 0, - } as IChoice)); + const choices = question[multiple ? 'anyOf' : 'oneOf']! + .map((x, i) => x.name!); + + const votes = question[multiple ? 'anyOf' : 'oneOf']! + .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); return { choices, + votes, multiple, expiresAt }; @@ -36,12 +37,14 @@ export async function updateQuestion(value: any) { const uri = typeof value == 'string' ? value : value.id; // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(config.url + '/')) throw 'uri points local'; + if (uri.startsWith(config.url + '/')) throw new Error('uri points local'); //#region このサーバーに既に登録されているか - const note = await Note.findOne({ uri }); + const note = await Notes.findOne({ uri }); + if (note == null) throw new Error('Question is not registed'); - if (note == null) throw 'Question is not registed'; + const poll = await Polls.findOne({ noteId: note.id }); + if (poll == null) throw new Error('Question is not registed'); //#endregion // resolve new Question object @@ -49,30 +52,24 @@ export async function updateQuestion(value: any) { const question = await resolver.resolve(value) as IQuestion; apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - if (question.type !== 'Question') throw 'object is not a Question'; + if (question.type !== 'Question') throw new Error('object is not a Question'); const apChoices = question.oneOf || question.anyOf; - const dbChoices = note.poll.choices; let changed = false; - for (const db of dbChoices) { - const oldCount = db.votes; - const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems; + for (const choice of poll.choices) { + const oldCount = poll.votes[poll.choices.indexOf(choice)]; + const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; if (oldCount != newCount) { changed = true; - db.votes = newCount; + poll.votes[poll.choices.indexOf(choice)] = newCount; } } - await Note.update({ - _id: note._id - }, { - $set: { - 'poll.choices': dbChoices, - updatedAt: new Date(), - } + await Polls.update({ noteId: note.id }, { + votes: poll.votes }); return changed; diff --git a/src/remote/activitypub/models/tag.ts b/src/remote/activitypub/models/tag.ts index 0a1e6e29f9..8d2008d1d9 100644 --- a/src/remote/activitypub/models/tag.ts +++ b/src/remote/activitypub/models/tag.ts @@ -14,13 +14,13 @@ export type ITag = { identifier?: IIdentifier; }; -export function extractHashtags(tags: ITag[]) { - if (!tags) return []; +export function extractHashtags(tags: ITag[] | null | undefined): string[] { + if (tags == null) return []; const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string'); return hashtags.map(tag => { - const m = tag.name.match(/^#(.+)/); + const m = tag.name ? tag.name.match(/^#(.+)/) : null; return m ? m[1] : null; - }).filter(x => x != null); + }).filter(x => x != null) as string[]; } diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts index 2e4f53adf5..425adaec96 100644 --- a/src/remote/activitypub/perform.ts +++ b/src/remote/activitypub/perform.ts @@ -1,5 +1,5 @@ import { Object } from './type'; -import { IRemoteUser } from '../../models/user'; +import { IRemoteUser } from '../../models/entities/user'; import kernel from './kernel'; export default async (actor: IRemoteUser, activity: Object): Promise<void> => { diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts index fdbdff3f12..21b4629074 100644 --- a/src/remote/activitypub/renderer/accept.ts +++ b/src/remote/activitypub/renderer/accept.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Accept', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts index 4d6fe392aa..46f937f61d 100644 --- a/src/remote/activitypub/renderer/add.ts +++ b/src/remote/activitypub/renderer/add.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (user: ILocalUser, target: any, object: any) => ({ type: 'Add', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, target, object }); diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts index f6f2f9bdcd..11e7be449b 100644 --- a/src/remote/activitypub/renderer/announce.ts +++ b/src/remote/activitypub/renderer/announce.ts @@ -1,7 +1,7 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; -export default (object: any, note: INote) => { +export default (object: any, note: Note) => { const attributedTo = `${config.url}/users/${note.userId}`; let to: string[] = []; @@ -18,7 +18,7 @@ export default (object: any, note: INote) => { } return { - id: `${config.url}/notes/${note._id}/activity`, + id: `${config.url}/notes/${note.id}/activity`, actor: `${config.url}/users/${note.userId}`, type: 'Announce', published: note.createdAt.toISOString(), diff --git a/src/remote/activitypub/renderer/block.ts b/src/remote/activitypub/renderer/block.ts index 694f3a1418..c29a9aea82 100644 --- a/src/remote/activitypub/renderer/block.ts +++ b/src/remote/activitypub/renderer/block.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser, IRemoteUser } from '../../../models/user'; +import { ILocalUser, IRemoteUser } from '../../../models/entities/user'; -export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({ +export default (blocker: ILocalUser, blockee: IRemoteUser) => ({ type: 'Block', - actor: `${config.url}/users/${blocker._id}`, + actor: `${config.url}/users/${blocker.id}`, object: blockee.uri }); diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts index 1ee1418fce..e1fc0515c8 100644 --- a/src/remote/activitypub/renderer/create.ts +++ b/src/remote/activitypub/renderer/create.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; -export default (object: any, note: INote) => { +export default (object: any, note: Note) => { const activity = { - id: `${config.url}/notes/${note._id}/activity`, + id: `${config.url}/notes/${note.id}/activity`, actor: `${config.url}/users/${note.userId}`, type: 'Create', published: note.createdAt.toISOString(), diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts index e090e1c886..a98c97e6e9 100644 --- a/src/remote/activitypub/renderer/delete.ts +++ b/src/remote/activitypub/renderer/delete.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Delete', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts index 17721e9417..4f6ea8c4ee 100644 --- a/src/remote/activitypub/renderer/document.ts +++ b/src/remote/activitypub/renderer/document.ts @@ -1,8 +1,8 @@ -import { IDriveFile } from '../../../models/drive-file'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; -export default (file: IDriveFile) => ({ +export default (file: DriveFile) => ({ type: 'Document', - mediaType: file.contentType, - url: getDriveFileUrl(file) + mediaType: file.type, + url: DriveFiles.getPublicUrl(file) }); diff --git a/src/remote/activitypub/renderer/emoji.ts b/src/remote/activitypub/renderer/emoji.ts index 1a05b4e89e..947a96df37 100644 --- a/src/remote/activitypub/renderer/emoji.ts +++ b/src/remote/activitypub/renderer/emoji.ts @@ -1,7 +1,7 @@ -import { IEmoji } from '../../../models/emoji'; import config from '../../../config'; +import { Emoji } from '../../../models/entities/emoji'; -export default (emoji: IEmoji) => ({ +export default (emoji: Emoji) => ({ id: `${config.url}/emojis/${emoji.name}`, type: 'Emoji', name: `:${emoji.name}:`, diff --git a/src/remote/activitypub/renderer/follow-user.ts b/src/remote/activitypub/renderer/follow-user.ts index 9a488d392b..6d354803e5 100644 --- a/src/remote/activitypub/renderer/follow-user.ts +++ b/src/remote/activitypub/renderer/follow-user.ts @@ -1,16 +1,13 @@ import config from '../../../config'; -import * as mongo from 'mongodb'; -import User, { isLocalUser } from '../../../models/user'; +import { Users } from '../../../models'; +import { User } from '../../../models/entities/user'; +import { ensure } from '../../../prelude/ensure'; /** * Convert (local|remote)(Follower|Followee)ID to URL * @param id Follower|Followee ID */ -export default async function renderFollowUser(id: mongo.ObjectID): Promise<any> { - - const user = await User.findOne({ - _id: id - }); - - return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri; +export default async function renderFollowUser(id: User['id']): Promise<any> { + const user = await Users.findOne(id).then(ensure); + return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri; } diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts index 98d4cdd020..400b15ec7b 100644 --- a/src/remote/activitypub/renderer/follow.ts +++ b/src/remote/activitypub/renderer/follow.ts @@ -1,11 +1,12 @@ import config from '../../../config'; -import { IUser, isLocalUser } from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { Users } from '../../../models'; -export default (follower: IUser, followee: IUser, requestId?: string) => { +export default (follower: User, followee: User, requestId?: string) => { const follow = { type: 'Follow', - actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri, - object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri + actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, + object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri } as any; if (requestId) follow.id = requestId; diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts index ec637b9521..ce98f98c62 100644 --- a/src/remote/activitypub/renderer/image.ts +++ b/src/remote/activitypub/renderer/image.ts @@ -1,8 +1,8 @@ -import { IDriveFile } from '../../../models/drive-file'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; -export default (file: IDriveFile) => ({ +export default (file: DriveFile) => ({ type: 'Image', - url: getDriveFileUrl(file), - sensitive: file.metadata.isSensitive + url: DriveFiles.getPublicUrl(file), + sensitive: file.isSensitive }); diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts index 0d5e52557c..334e5e00cd 100644 --- a/src/remote/activitypub/renderer/key.ts +++ b/src/remote/activitypub/renderer/key.ts @@ -1,10 +1,10 @@ import config from '../../../config'; -import { extractPublic } from '../../../crypto_key'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; +import { UserKeypair } from '../../../models/entities/user-keypair'; -export default (user: ILocalUser) => ({ - id: `${config.url}/users/${user._id}/publickey`, +export default (user: ILocalUser, key: UserKeypair) => ({ + id: `${config.url}/users/${user.id}/publickey`, type: 'Key', - owner: `${config.url}/users/${user._id}`, - publicKeyPem: extractPublic(user.keypair) + owner: `${config.url}/users/${user.id}`, + publicKeyPem: key.publicKey }); diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts index 523cb4f1ad..01f10ec0a9 100644 --- a/src/remote/activitypub/renderer/like.ts +++ b/src/remote/activitypub/renderer/like.ts @@ -1,10 +1,10 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; -import { INote } from '../../../models/note'; +import { ILocalUser } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; -export default (user: ILocalUser, note: INote, reaction: string) => ({ +export default (user: ILocalUser, note: Note, reaction: string) => ({ type: 'Like', - actor: `${config.url}/users/${user._id}`, - object: note.uri ? note.uri : `${config.url}/notes/${note._id}`, + actor: `${config.url}/users/${user.id}`, + object: note.uri ? note.uri : `${config.url}/notes/${note.id}`, _misskey_reaction: reaction }); diff --git a/src/remote/activitypub/renderer/mention.ts b/src/remote/activitypub/renderer/mention.ts index 8d12e6d8bf..889be5d85d 100644 --- a/src/remote/activitypub/renderer/mention.ts +++ b/src/remote/activitypub/renderer/mention.ts @@ -1,8 +1,9 @@ -import { IUser, isRemoteUser } from '../../../models/user'; import config from '../../../config'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users } from '../../../models'; -export default (mention: IUser) => ({ +export default (mention: User) => ({ type: 'Mention', - href: isRemoteUser(mention) ? mention.uri : `${config.url}/@${mention.username}`, - name: isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${mention.username}`, + href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`, + name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, }); diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 8b349526e1..c66af2667b 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -3,38 +3,37 @@ import renderHashtag from './hashtag'; import renderMention from './mention'; import renderEmoji from './emoji'; import config from '../../../config'; -import DriveFile, { IDriveFile } from '../../../models/drive-file'; -import Note, { INote } from '../../../models/note'; -import User from '../../../models/user'; import toHtml from '../misc/get-note-html'; -import Emoji, { IEmoji } from '../../../models/emoji'; +import { Note, IMentionedRemoteUsers } from '../../../models/entities/note'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles, Notes, Users, Emojis, Polls } from '../../../models'; +import { In } from 'typeorm'; +import { Emoji } from '../../../models/entities/emoji'; +import { Poll } from '../../../models/entities/poll'; +import { ensure } from '../../../prelude/ensure'; -export default async function renderNote(note: INote, dive = true): Promise<any> { - const promisedFiles: Promise<IDriveFile[]> = note.fileIds - ? DriveFile.find({ _id: { $in: note.fileIds } }) +export default async function renderNote(note: Note, dive = true): Promise<any> { + const promisedFiles: Promise<DriveFile[]> = note.fileIds.length > 0 + ? DriveFiles.find({ id: In(note.fileIds) }) : Promise.resolve([]); let inReplyTo; - let inReplyToNote: INote; + let inReplyToNote: Note | undefined; if (note.replyId) { - inReplyToNote = await Note.findOne({ - _id: note.replyId, - }); + inReplyToNote = await Notes.findOne(note.replyId); - if (inReplyToNote !== null) { - const inReplyToUser = await User.findOne({ - _id: inReplyToNote.userId, - }); + if (inReplyToNote != null) { + const inReplyToUser = await Users.findOne(inReplyToNote.userId); - if (inReplyToUser !== null) { + if (inReplyToUser != null) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; } else { if (dive) { inReplyTo = await renderNote(inReplyToNote, false); } else { - inReplyTo = `${config.url}/notes/${inReplyToNote._id}`; + inReplyTo = `${config.url}/notes/${inReplyToNote.id}`; } } } @@ -46,24 +45,18 @@ export default async function renderNote(note: INote, dive = true): Promise<any> let quote; if (note.renoteId) { - const renote = await Note.findOne({ - _id: note.renoteId, - }); + const renote = await Notes.findOne(note.renoteId); if (renote) { - quote = renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`; + quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`; } } - const user = await User.findOne({ - _id: note.userId - }); + const user = await Users.findOne(note.userId).then(ensure); - const attributedTo = `${config.url}/users/${user._id}`; + const attributedTo = `${config.url}/users/${user.id}`; - const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0 - ? note.mentionedRemoteUsers.map(x => x.uri) - : []; + const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); let to: string[] = []; let cc: string[] = []; @@ -81,10 +74,8 @@ export default async function renderNote(note: INote, dive = true): Promise<any> to = mentions; } - const mentionedUsers = note.mentions ? await User.find({ - _id: { - $in: note.mentions - } + const mentionedUsers = note.mentions.length > 0 ? await Users.find({ + id: In(note.mentions) }) : []; const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag)); @@ -93,23 +84,28 @@ export default async function renderNote(note: INote, dive = true): Promise<any> const files = await promisedFiles; let text = note.text; + let poll: Poll | undefined; + + if (note.hasPoll) { + poll = await Polls.findOne({ noteId: note.id }); + } - let question: string; - if (note.poll != null) { + let question: string | undefined; + if (poll) { if (text == null) text = ''; - const url = `${config.url}/notes/${note._id}`; + const url = `${config.url}/notes/${note.id}`; // TODO: i18n text += `\n[リモートで結果を表示](${url})`; - question = `${config.url}/questions/${note._id}`; + question = `${config.url}/questions/${note.id}`; } let apText = text; if (apText == null) apText = ''; // Provides choices as text for AP - if (note.poll != null) { - const cs = note.poll.choices.map(c => `${c.id}: ${c.text}`); + if (poll) { + const cs = poll.choices.map((c, i) => `${i}: ${c}`); apText += '\n----------------------------------------\n'; apText += cs.join('\n'); apText += '\n----------------------------------------\n'; @@ -135,31 +131,25 @@ export default async function renderNote(note: INote, dive = true): Promise<any> ...apemojis, ]; - const { - choices = [], - expiresAt = null, - multiple = false - } = note.poll || {}; - - const asPoll = note.poll ? { + const asPoll = poll ? { type: 'Question', content: toHtml(Object.assign({}, note, { text: text })), _misskey_fallback_content: content, - [expiresAt && expiresAt < new Date() ? 'closed' : 'endTime']: expiresAt, - [multiple ? 'anyOf' : 'oneOf']: choices.map(({ text, votes }) => ({ + [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, + [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ type: 'Note', name: text, replies: { type: 'Collection', - totalItems: votes + totalItems: poll!.votes[i] } })) } : {}; return { - id: `${config.url}/notes/${note._id}`, + id: `${config.url}/notes/${note.id}`, type: 'Note', attributedTo, summary, @@ -172,21 +162,21 @@ export default async function renderNote(note: INote, dive = true): Promise<any> cc, inReplyTo, attachment: files.map(renderDocument), - sensitive: files.some(file => file.metadata.isSensitive), + sensitive: files.some(file => file.isSensitive), tag, ...asPoll }; } -export async function getEmojis(names: string[]): Promise<IEmoji[]> { - if (names == null || names.length < 1) return []; +export async function getEmojis(names: string[]): Promise<Emoji[]> { + if (names == null || names.length === 0) return []; const emojis = await Promise.all( - names.map(name => Emoji.findOne({ + names.map(name => Emojis.findOne({ name, host: null })) ); - return emojis.filter(emoji => emoji != null); + return emojis.filter(emoji => emoji != null) as Emoji[]; } diff --git a/src/remote/activitypub/renderer/ordered-collection-page.ts b/src/remote/activitypub/renderer/ordered-collection-page.ts index 83af07870e..2433358646 100644 --- a/src/remote/activitypub/renderer/ordered-collection-page.ts +++ b/src/remote/activitypub/renderer/ordered-collection-page.ts @@ -7,7 +7,7 @@ * @param prev URL of prev page (optional) * @param next URL of next page (optional) */ -export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev: string, next: string) { +export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { const page = { id, partOf, diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 77e60cd61a..3fb164ef4e 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -1,21 +1,23 @@ import renderImage from './image'; import renderKey from './key'; import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; import { toHtml } from '../../../mfm/toHtml'; import { parse } from '../../../mfm/parse'; -import DriveFile from '../../../models/drive-file'; import { getEmojis } from './note'; import renderEmoji from './emoji'; import { IIdentifier } from '../models/identifier'; import renderHashtag from './hashtag'; +import { DriveFiles, UserProfiles, UserKeypairs } from '../../../models'; +import { ensure } from '../../../prelude/ensure'; -export default async (user: ILocalUser) => { - const id = `${config.url}/users/${user._id}`; +export async function renderPerson(user: ILocalUser) { + const id = `${config.url}/users/${user.id}`; - const [avatar, banner] = await Promise.all([ - DriveFile.findOne({ _id: user.avatarId }), - DriveFile.findOne({ _id: user.bannerId }) + const [avatar, banner, profile] = await Promise.all([ + user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined), + user.bannerId ? DriveFiles.findOne(user.bannerId) : Promise.resolve(undefined), + UserProfiles.findOne({ userId: user.id }).then(ensure) ]); const attachment: { @@ -26,41 +28,41 @@ export default async (user: ILocalUser) => { identifier?: IIdentifier }[] = []; - if (user.twitter) { + if (profile.twitter) { attachment.push({ type: 'PropertyValue', name: 'Twitter', - value: `<a href="https://twitter.com/intent/user?user_id=${user.twitter.userId}" rel="me nofollow noopener" target="_blank"><span>@${user.twitter.screenName}</span></a>`, + value: `<a href="https://twitter.com/intent/user?user_id=${profile.twitterUserId}" rel="me nofollow noopener" target="_blank"><span>@${profile.twitterScreenName}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:twitter', - value: `${user.twitter.userId}@${user.twitter.screenName}` + value: `${profile.twitterUserId}@${profile.twitterScreenName}` } }); } - if (user.github) { + if (profile.github) { attachment.push({ type: 'PropertyValue', name: 'GitHub', - value: `<a href="https://github.com/${user.github.login}" rel="me nofollow noopener" target="_blank"><span>@${user.github.login}</span></a>`, + value: `<a href="https://github.com/${profile.githubLogin}" rel="me nofollow noopener" target="_blank"><span>@${profile.githubLogin}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:github', - value: `${user.github.id}@${user.github.login}` + value: `${profile.githubId}@${profile.githubLogin}` } }); } - if (user.discord) { + if (profile.discord) { attachment.push({ type: 'PropertyValue', name: 'Discord', - value: `<a href="https://discordapp.com/users/${user.discord.id}" rel="me nofollow noopener" target="_blank"><span>${user.discord.username}#${user.discord.discriminator}</span></a>`, + value: `<a href="https://discordapp.com/users/${profile.discordId}" rel="me nofollow noopener" target="_blank"><span>${profile.discordUsername}#${profile.discordDiscriminator}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:discord', - value: `${user.discord.id}@${user.discord.username}#${user.discord.discriminator}` + value: `${profile.discordId}@${profile.discordUsername}#${profile.discordDiscriminator}` } }); } @@ -75,6 +77,8 @@ export default async (user: ILocalUser) => { ...hashtagTags, ]; + const keypair = await UserKeypairs.findOne(user.id).then(ensure); + return { type: user.isBot ? 'Service' : 'Person', id, @@ -88,13 +92,13 @@ export default async (user: ILocalUser) => { url: `${config.url}/@${user.username}`, preferredUsername: user.username, name: user.name, - summary: toHtml(parse(user.description)), - icon: user.avatarId && renderImage(avatar), - image: user.bannerId && renderImage(banner), + summary: toHtml(parse(profile.description)), + icon: avatar ? renderImage(avatar) : null, + image: banner ? renderImage(banner) : null, tag, manuallyApprovesFollowers: user.isLocked, - publicKey: renderKey(user), + publicKey: renderKey(user, keypair), isCat: user.isCat, attachment: attachment.length ? attachment : undefined }; -}; +} diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts index cf0bf387c8..6ade10d1bf 100644 --- a/src/remote/activitypub/renderer/question.ts +++ b/src/remote/activitypub/renderer/question.ts @@ -1,19 +1,20 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; -import { INote } from '../../../models/note'; +import { ILocalUser } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { Poll } from '../../../models/entities/poll'; -export default async function renderQuestion(user: ILocalUser, note: INote) { +export default async function renderQuestion(user: ILocalUser, note: Note, poll: Poll) { const question = { type: 'Question', - id: `${config.url}/questions/${note._id}`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/questions/${note.id}`, + actor: `${config.url}/users/${user.id}`, content: note.text || '', - [note.poll.multiple ? 'anyOf' : 'oneOf']: note.poll.choices.map(c => ({ - name: c.text, - _misskey_votes: c.votes, + [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ + name: text, + _misskey_votes: poll.votes[i], replies: { type: 'Collection', - totalItems: c.votes + totalItems: poll.votes[i] } })) }; diff --git a/src/remote/activitypub/renderer/reject.ts b/src/remote/activitypub/renderer/reject.ts index 6d7d23708a..c4e0ba0d0a 100644 --- a/src/remote/activitypub/renderer/reject.ts +++ b/src/remote/activitypub/renderer/reject.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Reject', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts index ed840be751..1b9a6b8c05 100644 --- a/src/remote/activitypub/renderer/remove.ts +++ b/src/remote/activitypub/renderer/remove.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (user: ILocalUser, target: any, object: any) => ({ type: 'Remove', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, target, object }); diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts index dbcf5732be..2ff6b61b90 100644 --- a/src/remote/activitypub/renderer/undo.ts +++ b/src/remote/activitypub/renderer/undo.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser, IUser } from '../../../models/user'; +import { ILocalUser, User } from '../../../models/entities/user'; -export default (object: any, user: ILocalUser | IUser) => ({ +export default (object: any, user: ILocalUser | User) => ({ type: 'Undo', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts index cf9acc9acb..c1d5ba29b2 100644 --- a/src/remote/activitypub/renderer/update.ts +++ b/src/remote/activitypub/renderer/update.ts @@ -1,10 +1,10 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => { const activity = { - id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`, + actor: `${config.url}/users/${user.id}`, type: 'Update', to: [ 'https://www.w3.org/ns/activitystreams#Public' ], object diff --git a/src/remote/activitypub/renderer/vote.ts b/src/remote/activitypub/renderer/vote.ts index 014b76765b..8929c03460 100644 --- a/src/remote/activitypub/renderer/vote.ts +++ b/src/remote/activitypub/renderer/vote.ts @@ -1,22 +1,23 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; -import { IRemoteUser, ILocalUser } from '../../../models/user'; -import { IPollVote } from '../../../models/poll-vote'; +import { Note } from '../../../models/entities/note'; +import { IRemoteUser, ILocalUser } from '../../../models/entities/user'; +import { PollVote } from '../../../models/entities/poll-vote'; +import { Poll } from '../../../models/entities/poll'; -export default async function renderVote(user: ILocalUser, vote: IPollVote, pollNote: INote, pollOwner: IRemoteUser): Promise<any> { +export default async function renderVote(user: ILocalUser, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> { return { - id: `${config.url}/users/${user._id}#votes/${vote._id}/activity`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`, + actor: `${config.url}/users/${user.id}`, type: 'Create', to: [pollOwner.uri], published: new Date().toISOString(), object: { - id: `${config.url}/users/${user._id}#votes/${vote._id}`, + id: `${config.url}/users/${user.id}#votes/${vote.id}`, type: 'Note', - attributedTo: `${config.url}/users/${user._id}`, + attributedTo: `${config.url}/users/${user.id}`, to: [pollOwner.uri], - inReplyTo: pollNote.uri, - name: pollNote.poll.choices.find(x => x.id === vote.choice).text + inReplyTo: note.uri, + name: poll.choices[vote.choice] } }; } diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index 08dd7a6ba9..897dd9acac 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -4,13 +4,15 @@ import { URL } from 'url'; import * as crypto from 'crypto'; import { lookup, IRunOptions } from 'lookup-dns-cache'; import * as promiseAny from 'promise-any'; -import { toUnicode } from 'punycode'; import config from '../../config'; -import { ILocalUser } from '../../models/user'; +import { ILocalUser } from '../../models/entities/user'; import { publishApLogStream } from '../../services/stream'; import { apLogger } from './logger'; -import Instance from '../../models/instance'; +import { UserKeypairs } from '../../models'; +import fetchMeta from '../../misc/fetch-meta'; +import { toPuny } from '../../misc/convert-host'; +import { ensure } from '../../prelude/ensure'; export const logger = apLogger.createSubLogger('deliver'); @@ -23,8 +25,8 @@ export default async (user: ILocalUser, url: string, object: any) => { // ブロックしてたら中断 // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく - const instance = await Instance.findOne({ host: toUnicode(host) }); - if (instance && instance.isBlocked) return; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(toPuny(host))) return; const data = JSON.stringify(object); @@ -35,7 +37,11 @@ export default async (user: ILocalUser, url: string, object: any) => { const addr = await resolveAddr(hostname); if (!addr) return; - const _ = new Promise((resolve, reject) => { + const keypair = await UserKeypairs.findOne({ + userId: user.id + }).then(ensure); + + await new Promise((resolve, reject) => { const req = request({ protocol, hostname: addr, @@ -51,7 +57,7 @@ export default async (user: ILocalUser, url: string, object: any) => { 'Digest': `SHA-256=${hash}` } }, res => { - if (res.statusCode >= 400) { + if (res.statusCode! >= 400) { logger.warn(`${url} --> ${res.statusCode}`); reject(res); } else { @@ -62,13 +68,13 @@ export default async (user: ILocalUser, url: string, object: any) => { sign(req, { authorizationHeaderName: 'Signature', - key: user.keypair, - keyId: `${config.url}/users/${user._id}/publickey`, + key: keypair.privateKey, + keyId: `${config.url}/users/${user.id}/publickey`, headers: ['date', 'host', 'digest'] }); // Signature: Signature ... => Signature: ... - let sig = req.getHeader('Signature').toString(); + let sig = req.getHeader('Signature')!.toString(); sig = sig.replace(/^Signature /, ''); req.setHeader('Signature', sig); @@ -82,8 +88,6 @@ export default async (user: ILocalUser, url: string, object: any) => { req.end(data); }); - await _; - //#region Log publishApLogStream({ direction: 'out', @@ -107,7 +111,7 @@ async function resolveAddr(domain: string) { function resolveAddrInner(domain: string, options: IRunOptions = {}): Promise<string> { return new Promise((res, rej) => { - lookup(domain, options, (error: any, address: string | string[]) => { + lookup(domain, options, (error, address) => { if (error) return rej(error); return res(Array.isArray(address) ? address[0] : address); }); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 05152993e4..e8d0be638a 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -64,7 +64,7 @@ export default class Resolver { json: true }); - if (object === null || ( + if (object == null || ( Array.isArray(object['@context']) ? !object['@context'].includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index c381e63507..95c69fb8ac 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -12,7 +12,7 @@ export interface IObject { attachment?: any[]; inReplyTo?: any; replies?: ICollection; - content: string; + content?: string; name?: string; startTime?: Date; endTime?: Date; @@ -44,16 +44,16 @@ export interface IOrderedCollection extends IObject { export interface INote extends IObject { type: 'Note' | 'Question'; - _misskey_content: string; - _misskey_quote: string; - _misskey_question: string; + _misskey_content?: string; + _misskey_quote?: string; + _misskey_question?: string; } export interface IQuestion extends IObject { type: 'Note' | 'Question'; - _misskey_content: string; - _misskey_quote: string; - _misskey_question: string; + _misskey_content?: string; + _misskey_quote?: string; + _misskey_question?: string; oneOf?: IQuestionChoice[]; anyOf?: IQuestionChoice[]; endTime?: Date; @@ -65,6 +65,8 @@ interface IQuestionChoice { _misskey_votes?: number; } +export const validActor = ['Person', 'Service']; + export interface IPerson extends IObject { type: 'Person'; name: string; @@ -127,7 +129,7 @@ export interface IRemove extends IActivity { export interface ILike extends IActivity { type: 'Like'; - _misskey_reaction: string; + _misskey_reaction?: string; } export interface IAnnounce extends IActivity { diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 400293da89..a4bfca8422 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -1,38 +1,47 @@ -import { toUnicode, toASCII } from 'punycode'; -import User, { IUser, IRemoteUser } from '../models/user'; import webFinger from './webfinger'; import config from '../config'; import { createPerson, updatePerson } from './activitypub/models/person'; import { URL } from 'url'; import { remoteLogger } from './logger'; import chalk from 'chalk'; +import { User, IRemoteUser } from '../models/entities/user'; +import { Users } from '../models'; +import { toPuny } from '../misc/convert-host'; const logger = remoteLogger.createSubLogger('resolve-user'); -export default async (username: string, _host: string, option?: any, resync?: boolean): Promise<IUser> => { +export async function resolveUser(username: string, host: string | null, option?: any, resync = false): Promise<User> { const usernameLower = username.toLowerCase(); - if (_host == null) { + if (host == null) { logger.info(`return local user: ${usernameLower}`); - return await User.findOne({ usernameLower, host: null }); + return await Users.findOne({ usernameLower, host: null }).then(u => { + if (u == null) { + throw new Error('user not found'); + } else { + return u; + } + }); } - const configHostAscii = toASCII(config.host).toLowerCase(); - const configHost = toUnicode(configHostAscii); - - const hostAscii = toASCII(_host).toLowerCase(); - const host = toUnicode(hostAscii); + host = toPuny(host); - if (configHost == host) { + if (config.host == host) { logger.info(`return local user: ${usernameLower}`); - return await User.findOne({ usernameLower, host: null }); + return await Users.findOne({ usernameLower, host: null }).then(u => { + if (u == null) { + throw new Error('user not found'); + } else { + return u; + } + }); } - const user = await User.findOne({ usernameLower, host }, option); + const user = await Users.findOne({ usernameLower, host }, option); - const acctLower = `${usernameLower}@${hostAscii}`; + const acctLower = `${usernameLower}@${host}`; - if (user === null) { + if (user == null) { const self = await resolveSelf(acctLower); logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`); @@ -50,17 +59,15 @@ export default async (username: string, _host: string, option?: any, resync?: bo // validate uri const uri = new URL(self.href); - if (uri.hostname !== hostAscii) { + if (uri.hostname !== host) { throw new Error(`Invalied uri`); } - await User.update({ + await Users.update({ usernameLower, host: host }, { - $set: { - uri: self.href - } + uri: self.href }); } else { logger.info(`uri is fine: ${acctLower}`); @@ -69,20 +76,26 @@ export default async (username: string, _host: string, option?: any, resync?: bo await updatePerson(self.href); logger.info(`return resynced remote user: ${acctLower}`); - return await User.findOne({ uri: self.href }); + return await Users.findOne({ uri: self.href }).then(u => { + if (u == null) { + throw new Error('user not found'); + } else { + return u; + } + }); } logger.info(`return existing remote user: ${acctLower}`); return user; -}; +} async function resolveSelf(acctLower: string) { logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); const finger = await webFinger(acctLower).catch(e => { - logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${e.message} (${e.status})`); - throw e; + logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`); + throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`); }); - const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); + const self = finger.links.find(link => link.rel != null && link.rel.toLowerCase() === 'self'); if (!self) { logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: self link not found`); throw new Error('self link not found'); diff --git a/src/remote/webfinger.ts b/src/remote/webfinger.ts index 1229dbaf98..800673943b 100644 --- a/src/remote/webfinger.ts +++ b/src/remote/webfinger.ts @@ -1,10 +1,11 @@ -import { WebFinger } from 'webfinger.js'; - -const webFinger = new WebFinger({ }); +import config from '../config'; +import * as request from 'request-promise-native'; +import { URL } from 'url'; +import { query as urlQuery } from '../prelude/url'; type ILink = { href: string; - rel: string; + rel?: string; }; type IWebFinger = { @@ -12,12 +13,33 @@ type IWebFinger = { subject: string; }; -export default async function resolve(query: any): Promise<IWebFinger> { - return await new Promise((res, rej) => webFinger.lookup(query, (error: Error | string, result: any) => { - if (error) { - return rej(error); - } +export default async function(query: string): Promise<IWebFinger> { + const url = genUrl(query); + + return await request({ + url, + proxy: config.proxy, + timeout: 10 * 1000, + forever: true, + headers: { + 'User-Agent': config.userAgent, + Accept: 'application/jrd+json, application/json' + }, + json: true + }); +} + +function genUrl(query: string) { + if (query.match(/^https?:\/\//)) { + const u = new URL(query); + return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query }); + } + + const m = query.match(/^([^@]+)@(.*)/); + if (m) { + const hostname = m[2]; + return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` }); + } - res(result.object); - })) as IWebFinger; + throw new Error(`Invalied query (${query})`); } diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index df5f5b141d..12fccbfa7d 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -1,15 +1,11 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import * as json from 'koa-json-body'; import * as httpSignature from 'http-signature'; import { renderActivity } from '../remote/activitypub/renderer'; -import Note from '../models/note'; -import User, { isLocalUser, ILocalUser, IUser } from '../models/user'; -import Emoji from '../models/emoji'; import renderNote from '../remote/activitypub/renderer/note'; import renderKey from '../remote/activitypub/renderer/key'; -import renderPerson from '../remote/activitypub/renderer/person'; +import { renderPerson } from '../remote/activitypub/renderer/person'; import renderEmoji from '../remote/activitypub/renderer/emoji'; import Outbox, { packActivity } from './activitypub/outbox'; import Followers from './activitypub/followers'; @@ -18,6 +14,10 @@ import Featured from './activitypub/featured'; import renderQuestion from '../remote/activitypub/renderer/question'; import { inbox as processInbox } from '../queue'; import { isSelfHost } from '../misc/convert-host'; +import { Notes, Users, Emojis, UserKeypairs, Polls } from '../models'; +import { ILocalUser, User } from '../models/entities/user'; +import { In } from 'typeorm'; +import { ensure } from '../prelude/ensure'; // Init router const router = new Router(); @@ -64,25 +64,20 @@ router.post('/users/:user/inbox', json(), inbox); router.get('/notes/:note', async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); - if (!ObjectID.isValid(ctx.params.note)) { - ctx.status = 404; - return; - } - - const note = await Note.findOne({ - _id: new ObjectID(ctx.params.note), - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true } + const note = await Notes.findOne({ + id: ctx.params.note, + visibility: In(['public', 'home']), + localOnly: false }); - if (note === null) { + if (note == null) { ctx.status = 404; return; } // リモートだったらリダイレクト - if (note._user.host != null) { - if (note.uri == null || isSelfHost(note._user.host)) { + if (note.userHost != null) { + if (note.uri == null || isSelfHost(note.userHost)) { ctx.status = 500; return; } @@ -97,19 +92,14 @@ router.get('/notes/:note', async (ctx, next) => { // note activity router.get('/notes/:note/activity', async ctx => { - if (!ObjectID.isValid(ctx.params.note)) { - ctx.status = 404; - return; - } - - const note = await Note.findOne({ - _id: new ObjectID(ctx.params.note), - '_user.host': null, - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true } + const note = await Notes.findOne({ + id: ctx.params.note, + userHost: null, + visibility: In(['public', 'home']), + localOnly: false }); - if (note === null) { + if (note == null) { ctx.status = 404; return; } @@ -121,32 +111,23 @@ router.get('/notes/:note/activity', async ctx => { // question router.get('/questions/:question', async (ctx, next) => { - if (!ObjectID.isValid(ctx.params.question)) { - ctx.status = 404; - return; - } - - const poll = await Note.findOne({ - _id: new ObjectID(ctx.params.question), - '_user.host': null, - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true }, - poll: { - $exists: true, - $ne: null - }, + const pollNote = await Notes.findOne({ + id: ctx.params.question, + userHost: null, + visibility: In(['public', 'home']), + localOnly: false, + hasPoll: true }); - if (poll === null) { + if (pollNote == null) { ctx.status = 404; return; } - const user = await User.findOne({ - _id: poll.userId - }); + const user = await Users.findOne(pollNote.userId).then(ensure); + const poll = await Polls.findOne({ noteId: pollNote.id }).then(ensure); - ctx.body = renderActivity(await renderQuestion(user as ILocalUser, poll)); + ctx.body = renderActivity(await renderQuestion(user as ILocalUser, pollNote, poll)); setResponseType(ctx); }); @@ -164,25 +145,22 @@ router.get('/users/:user/collections/featured', Featured); // publickey router.get('/users/:user/publickey', async ctx => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } - if (isLocalUser(user)) { - ctx.body = renderActivity(renderKey(user)); + const keypair = await UserKeypairs.findOne(user.id).then(ensure); + + if (Users.isLocalUser(user)) { + ctx.body = renderActivity(renderKey(user, keypair)); ctx.set('Cache-Control', 'public, max-age=180'); setResponseType(ctx); } else { @@ -191,8 +169,8 @@ router.get('/users/:user/publickey', async ctx => { }); // user -async function userInfo(ctx: Router.IRouterContext, user: IUser) { - if (user === null) { +async function userInfo(ctx: Router.IRouterContext, user: User) { + if (user == null) { ctx.status = 404; return; } @@ -205,17 +183,12 @@ async function userInfo(ctx: Router.IRouterContext, user: IUser) { router.get('/users/:user', async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } + const userId = ctx.params.user; - const userId = new ObjectID(ctx.params.user); - - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null - }); + }).then(ensure); await userInfo(ctx, user); }); @@ -223,10 +196,10 @@ router.get('/users/:user', async (ctx, next) => { router.get('/@:user', async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); - const user = await User.findOne({ + const user = await Users.findOne({ usernameLower: ctx.params.user.toLowerCase(), host: null - }); + }).then(ensure); await userInfo(ctx, user); }); @@ -234,12 +207,12 @@ router.get('/@:user', async (ctx, next) => { // emoji router.get('/emojis/:emoji', async ctx => { - const emoji = await Emoji.findOne({ + const emoji = await Emojis.findOne({ host: null, name: ctx.params.emoji }); - if (emoji === null) { + if (emoji == null) { ctx.status = 404; return; } diff --git a/src/server/activitypub/featured.ts b/src/server/activitypub/featured.ts index fc6150902b..86ec1000c7 100644 --- a/src/server/activitypub/featured.ts +++ b/src/server/activitypub/featured.ts @@ -1,41 +1,36 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import config from '../../config'; -import User from '../../models/user'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import { setResponseType } from '../activitypub'; -import Note from '../../models/note'; import renderNote from '../../remote/activitypub/renderer/note'; +import { Users, Notes, UserNotePinings } from '../../models'; +import { ensure } from '../../prelude/ensure'; export default async (ctx: Router.IRouterContext) => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; // Verify user - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } - const pinnedNoteIds = user.pinnedNoteIds || []; + const pinings = await UserNotePinings.find({ userId: user.id }); - const pinnedNotes = await Promise.all(pinnedNoteIds.filter(ObjectID.isValid).map(id => Note.findOne({ _id: id }))); + const pinnedNotes = await Promise.all(pinings.map(pining => + Notes.findOne(pining.noteId).then(ensure))); const renderedNotes = await Promise.all(pinnedNotes.map(note => renderNote(note))); const rendered = renderOrderedCollection( `${config.url}/users/${userId}/collections/featured`, - renderedNotes.length, null, null, renderedNotes + renderedNotes.length, undefined, undefined, renderedNotes ); ctx.body = renderActivity(rendered); diff --git a/src/server/activitypub/followers.ts b/src/server/activitypub/followers.ts index 002576b2e7..e48dc57f7a 100644 --- a/src/server/activitypub/followers.ts +++ b/src/server/activitypub/followers.ts @@ -1,24 +1,18 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import config from '../../config'; import $ from 'cafy'; -import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; -import Following from '../../models/following'; +import { ID } from '../../misc/cafy-id'; import * as url from '../../prelude/url'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; import renderFollowUser from '../../remote/activitypub/renderer/follow-user'; import { setResponseType } from '../activitypub'; +import { Users, Followings } from '../../models'; +import { LessThan } from 'typeorm'; export default async (ctx: Router.IRouterContext) => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; // Get 'cursor' parameter const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor); @@ -34,12 +28,12 @@ export default async (ctx: Router.IRouterContext) => { } // Verify user - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -49,22 +43,20 @@ export default async (ctx: Router.IRouterContext) => { if (page) { const query = { - followeeId: user._id + followeeId: user.id } as any; // カーソルが指定されている場合 if (cursor) { - query._id = { - $lt: transform(cursor) - }; + query.id = LessThan(cursor); } // Get followers - const followings = await Following - .find(query, { - limit: limit + 1, - sort: { _id: -1 } - }); + const followings = await Followings.find({ + where: query, + take: limit + 1, + order: { id: -1 } + }); // 「次のページ」があるかどうか const inStock = followings.length === limit + 1; @@ -77,18 +69,18 @@ export default async (ctx: Router.IRouterContext) => { cursor })}`, user.followersCount, renderedFollowers, partOf, - null, + undefined, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings[followings.length - 1]._id.toHexString() - })}` : null + cursor: followings[followings.length - 1].id + })}` : undefined ); ctx.body = renderActivity(rendered); setResponseType(ctx); } else { // index page - const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null); + const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); ctx.body = renderActivity(rendered); ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); setResponseType(ctx); diff --git a/src/server/activitypub/following.ts b/src/server/activitypub/following.ts index 0d7486f68a..4a7314e0ce 100644 --- a/src/server/activitypub/following.ts +++ b/src/server/activitypub/following.ts @@ -1,24 +1,19 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import config from '../../config'; import $ from 'cafy'; -import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; -import Following from '../../models/following'; +import { ID } from '../../misc/cafy-id'; import * as url from '../../prelude/url'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; import renderFollowUser from '../../remote/activitypub/renderer/follow-user'; import { setResponseType } from '../activitypub'; +import { Users, Followings } from '../../models'; +import { LessThan, FindConditions } from 'typeorm'; +import { Following } from '../../models/entities/following'; export default async (ctx: Router.IRouterContext) => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; // Get 'cursor' parameter const [cursor, cursorErr] = $.optional.type(ID).get(ctx.request.query.cursor); @@ -34,12 +29,12 @@ export default async (ctx: Router.IRouterContext) => { } // Verify user - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -49,22 +44,20 @@ export default async (ctx: Router.IRouterContext) => { if (page) { const query = { - followerId: user._id - } as any; + followerId: user.id + } as FindConditions<Following>; // カーソルが指定されている場合 if (cursor) { - query._id = { - $lt: transform(cursor) - }; + query.id = LessThan(cursor); } // Get followings - const followings = await Following - .find(query, { - limit: limit + 1, - sort: { _id: -1 } - }); + const followings = await Followings.find({ + where: query, + take: limit + 1, + order: { id: -1 } + }); // 「次のページ」があるかどうか const inStock = followings.length === limit + 1; @@ -77,18 +70,18 @@ export default async (ctx: Router.IRouterContext) => { cursor })}`, user.followingCount, renderedFollowees, partOf, - null, + undefined, inStock ? `${partOf}?${url.query({ page: 'true', - cursor: followings[followings.length - 1]._id.toHexString() - })}` : null + cursor: followings[followings.length - 1].id + })}` : undefined ); ctx.body = renderActivity(rendered); setResponseType(ctx); } else { // index page - const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null); + const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); ctx.body = renderActivity(rendered); ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); setResponseType(ctx); diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index ff8f884b19..118d8f00a9 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -1,28 +1,24 @@ -import { ObjectID } from 'mongodb'; import * as Router from 'koa-router'; import config from '../../config'; import $ from 'cafy'; -import ID, { transform } from '../../misc/cafy-id'; -import User from '../../models/user'; +import { ID } from '../../misc/cafy-id'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection'; import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page'; import { setResponseType } from '../activitypub'; - -import Note, { INote } from '../../models/note'; import renderNote from '../../remote/activitypub/renderer/note'; import renderCreate from '../../remote/activitypub/renderer/create'; import renderAnnounce from '../../remote/activitypub/renderer/announce'; import { countIf } from '../../prelude/array'; import * as url from '../../prelude/url'; +import { Users, Notes } from '../../models'; +import { makePaginationQuery } from '../api/common/make-pagination-query'; +import { Brackets } from 'typeorm'; +import { Note } from '../../models/entities/note'; +import { ensure } from '../../prelude/ensure'; export default async (ctx: Router.IRouterContext) => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); + const userId = ctx.params.user; // Get 'sinceId' parameter const [sinceId, sinceIdErr] = $.optional.type(ID).get(ctx.request.query.since_id); @@ -41,12 +37,12 @@ export default async (ctx: Router.IRouterContext) => { } // Verify user - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: userId, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -55,34 +51,15 @@ export default async (ctx: Router.IRouterContext) => { const partOf = `${config.url}/users/${userId}/outbox`; if (page) { - //#region Construct query - const sort = { - _id: -1 - }; - - const query = { - userId: user._id, - visibility: { $in: ['public', 'home'] }, - localOnly: { $ne: true } - } as any; - - if (sinceId) { - sort._id = 1; - query._id = { - $gt: transform(sinceId) - }; - } else if (untilId) { - query._id = { - $lt: transform(untilId) - }; - } - //#endregion + const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId) + .andWhere('note.userId = :userId', { userId: user.id }) + .andWhere(new Brackets(qb => { qb + .where(`note.visibility = 'public'`) + .orWhere(`note.visibility = 'home'`); + })) + .andWhere('note.localOnly = FALSE'); - const notes = await Note - .find(query, { - limit: limit, - sort: sort - }); + const notes = await query.take(limit).getMany(); if (sinceId) notes.reverse(); @@ -96,12 +73,12 @@ export default async (ctx: Router.IRouterContext) => { user.notesCount, activities, partOf, notes.length ? `${partOf}?${url.query({ page: 'true', - since_id: notes[0]._id.toHexString() - })}` : null, + since_id: notes[0].id + })}` : undefined, notes.length ? `${partOf}?${url.query({ page: 'true', - until_id: notes[notes.length - 1]._id.toHexString() - })}` : null + until_id: notes[notes.length - 1].id + })}` : undefined ); ctx.body = renderActivity(rendered); @@ -123,10 +100,10 @@ export default async (ctx: Router.IRouterContext) => { * Pack Create<Note> or Announce Activity * @param note Note */ -export async function packActivity(note: INote): Promise<object> { - if (note.renoteId && note.text == null && note.poll == null && (note.fileIds == null || note.fileIds.length == 0)) { - const renote = await Note.findOne(note.renoteId); - return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`, note); +export async function packActivity(note: Note): Promise<any> { + if (note.renoteId && note.text == null && !note.hasPoll && (note.fileIds == null || note.fileIds.length == 0)) { + const renote = await Notes.findOne(note.renoteId).then(ensure); + return renderAnnounce(renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`, note); } return renderCreate(await renderNote(note, false), note); diff --git a/src/server/api/api-handler.ts b/src/server/api/api-handler.ts index 827aecdf25..2de6994f32 100644 --- a/src/server/api/api-handler.ts +++ b/src/server/api/api-handler.ts @@ -15,11 +15,11 @@ export default (endpoint: IEndpoint, ctx: Koa.BaseContext) => new Promise((res) ctx.status = x; ctx.body = { error: { - message: y.message, - code: y.code, - id: y.id, - kind: y.kind, - ...(y.info ? { info: y.info } : {}) + message: y!.message, + code: y!.code, + id: y!.id, + kind: y!.kind, + ...(y!.info ? { info: y!.info } : {}) } }; } else { @@ -31,9 +31,9 @@ export default (endpoint: IEndpoint, ctx: Koa.BaseContext) => new Promise((res) // Authentication authenticate(body['i']).then(([user, app]) => { // API invoking - call(endpoint.name, user, app, body, (ctx.req as any).file).then(res => { + call(endpoint.name, user, app, body, (ctx.req as any).file).then((res: any) => { reply(res); - }).catch(e => { + }).catch((e: ApiError) => { reply(e.httpStatusCode ? e.httpStatusCode : e.kind == 'client' ? 400 : 500, e); }); }).catch(() => { diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts index 7781b87c88..ecf4a82c45 100644 --- a/src/server/api/authenticate.ts +++ b/src/server/api/authenticate.ts @@ -1,37 +1,37 @@ -import App, { IApp } from '../../models/app'; -import { default as User, IUser } from '../../models/user'; -import AccessToken from '../../models/access-token'; import isNativeToken from './common/is-native-token'; +import { User } from '../../models/entities/user'; +import { App } from '../../models/entities/app'; +import { Users, AccessTokens, Apps } from '../../models'; -export default async (token: string): Promise<[IUser, IApp]> => { +export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => { if (token == null) { return [null, null]; } if (isNativeToken(token)) { // Fetch user - const user: IUser = await User + const user = await Users .findOne({ token }); - if (user === null) { - throw 'user not found'; + if (user == null) { + throw new Error('user not found'); } return [user, null]; } else { - const accessToken = await AccessToken.findOne({ + const accessToken = await AccessTokens.findOne({ hash: token.toLowerCase() }); - if (accessToken === null) { - throw 'invalid signature'; + if (accessToken == null) { + throw new Error('invalid signature'); } - const app = await App - .findOne({ _id: accessToken.appId }); + const app = await Apps + .findOne(accessToken.appId); - const user = await User - .findOne({ _id: accessToken.userId }); + const user = await Users + .findOne(accessToken.userId); return [user, app]; } diff --git a/src/server/api/call.ts b/src/server/api/call.ts index 89a44b3c65..c79f8eef5d 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -1,10 +1,10 @@ import { performance } from 'perf_hooks'; import limiter from './limiter'; -import { IUser } from '../../models/user'; -import { IApp } from '../../models/app'; +import { User } from '../../models/entities/user'; import endpoints from './endpoints'; import { ApiError } from './error'; import { apiLogger } from './logger'; +import { App } from '../../models/entities/app'; const accessDenied = { message: 'Access denied.', @@ -12,7 +12,7 @@ const accessDenied = { id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e' }; -export default async (endpoint: string, user: IUser, app: IApp, data: any, file?: any) => { +export default async (endpoint: string, user: User | null | undefined, app: App | null | undefined, data: any, file?: any) => { const isSecure = user != null && app == null; const ep = endpoints.find(e => e.name === endpoint); @@ -39,15 +39,15 @@ export default async (endpoint: string, user: IUser, app: IApp, data: any, file? }); } - if (ep.meta.requireCredential && user.isSuspended) { + if (ep.meta.requireCredential && user!.isSuspended) { throw new ApiError(accessDenied, { reason: 'Your account has been suspended.' }); } - if (ep.meta.requireAdmin && !user.isAdmin) { + if (ep.meta.requireAdmin && !user!.isAdmin) { throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); } - if (ep.meta.requireModerator && !user.isAdmin && !user.isModerator) { + if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) { throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); } @@ -61,7 +61,7 @@ export default async (endpoint: string, user: IUser, app: IApp, data: any, file? if (ep.meta.requireCredential && ep.meta.limit) { // Rate limit - await limiter(ep, user).catch(e => { + await limiter(ep, user!).catch(e => { throw new ApiError({ message: 'Rate limit exceeded. Please try again later.', code: 'RATE_LIMIT_EXCEEDED', @@ -80,7 +80,11 @@ export default async (endpoint: string, user: IUser, app: IApp, data: any, file? apiLogger.error(`Internal error occurred in ${ep.name}`, { ep: ep.name, ps: data, - e: e + e: { + message: e.message, + code: e.name, + stack: e.stack + } }); throw new ApiError(null, { e: { diff --git a/src/server/api/common/generate-mute-query.ts b/src/server/api/common/generate-mute-query.ts new file mode 100644 index 0000000000..090c14eb83 --- /dev/null +++ b/src/server/api/common/generate-mute-query.ts @@ -0,0 +1,36 @@ +import { User } from '../../../models/entities/user'; +import { Mutings } from '../../../models'; +import { SelectQueryBuilder, Brackets } from 'typeorm'; + +export function generateMuteQuery(q: SelectQueryBuilder<any>, me: User) { + const mutingQuery = Mutings.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); + + // 投稿の作者をミュートしていない かつ + // 投稿の返信先の作者をミュートしていない かつ + // 投稿の引用元の作者をミュートしていない + q + .andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`) + .andWhere(new Brackets(qb => { qb + .where(`note.replyUserId IS NULL`) + .orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`); + })) + .andWhere(new Brackets(qb => { qb + .where(`note.renoteUserId IS NULL`) + .orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`); + })); + + q.setParameters(mutingQuery.getParameters()); +} + +export function generateMuteQueryForUsers(q: SelectQueryBuilder<any>, me: User) { + const mutingQuery = Mutings.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: me.id }); + + q + .andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`); + + q.setParameters(mutingQuery.getParameters()); +} diff --git a/src/server/api/common/generate-native-user-token.ts b/src/server/api/common/generate-native-user-token.ts index 2082b89a5a..9d44885630 100644 --- a/src/server/api/common/generate-native-user-token.ts +++ b/src/server/api/common/generate-native-user-token.ts @@ -1,3 +1,3 @@ import rndstr from 'rndstr'; -export default () => `!${rndstr('a-zA-Z0-9', 32)}`; +export default () => `0${rndstr('a-zA-Z0-9', 15)}`; diff --git a/src/server/api/common/generate-visibility-query.ts b/src/server/api/common/generate-visibility-query.ts new file mode 100644 index 0000000000..2807dc99dc --- /dev/null +++ b/src/server/api/common/generate-visibility-query.ts @@ -0,0 +1,40 @@ +import { User } from '../../../models/entities/user'; +import { Followings } from '../../../models'; +import { Brackets, SelectQueryBuilder } from 'typeorm'; + +export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: User) { + if (me == null) { + q.andWhere(new Brackets(qb => { qb + .where(`note.visibility = 'public'`) + .orWhere(`note.visibility = 'home'`); + })); + } else { + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); + + q.andWhere(new Brackets(qb => { qb + // 公開投稿である + .where(new Brackets(qb => { qb + .where(`note.visibility = 'public'`) + .orWhere(`note.visibility = 'home'`); + })) + // または 自分自身 + .orWhere('note.userId = :userId1', { userId1: me.id }) + // または 自分宛て + .orWhere(':userId2 = ANY(note.visibleUserIds)', { userId2: me.id }) + .orWhere(new Brackets(qb => { qb + // または フォロワー宛ての投稿であり、 + .where('note.visibility = \'followers\'') + .andWhere(new Brackets(qb => { qb + // 自分がフォロワーである + .where(`note.userId IN (${ followingQuery.getQuery() })`) + // または 自分の投稿へのリプライ + .orWhere('note.replyUserId = :userId3', { userId3: me.id }); + })); + })); + })); + + q.setParameters(followingQuery.getParameters()); + } +} diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts deleted file mode 100644 index 876aa399f7..0000000000 --- a/src/server/api/common/get-friends.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as mongodb from 'mongodb'; -import Following from '../../../models/following'; - -export const getFriendIds = async (me: mongodb.ObjectID, includeMe = true) => { - // Fetch relation to other users who the I follows - // SELECT followee - const followings = await Following - .find({ - followerId: me - }, { - fields: { - followeeId: true - } - }); - - // ID list of other users who the I follows - const myfollowingIds = followings.map(following => following.followeeId); - - if (includeMe) { - myfollowingIds.push(me); - } - - return myfollowingIds; -}; - -export const getFriends = async (me: mongodb.ObjectID, includeMe = true, remoteOnly = false) => { - const q: any = remoteOnly ? { - followerId: me, - '_followee.host': { $ne: null } - } : { - followerId: me - }; - // Fetch relation to other users who the I follows - const followings = await Following - .find(q); - - // ID list of other users who the I follows - const myfollowings = followings.map(following => ({ - id: following.followeeId - })); - - if (includeMe) { - myfollowings.push({ - id: me - }); - } - - return myfollowings; -}; diff --git a/src/server/api/common/get-hide-users.ts b/src/server/api/common/get-hide-users.ts deleted file mode 100644 index 3cdf806751..0000000000 --- a/src/server/api/common/get-hide-users.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as mongo from 'mongodb'; -import Mute from '../../../models/mute'; -import User, { IUser } from '../../../models/user'; -import { unique } from '../../../prelude/array'; - -export async function getHideUserIds(me: IUser) { - return await getHideUserIdsById(me ? me._id : null); -} - -export async function getHideUserIdsById(meId?: mongo.ObjectID) { - const [suspended, muted] = await Promise.all([ - User.find({ - isSuspended: true - }, { - fields: { - _id: true - } - }), - meId ? Mute.find({ - muterId: meId - }) : Promise.resolve([]) - ]); - - return unique(suspended.map(user => user._id).concat(muted.map(mute => mute.muteeId))); -} diff --git a/src/server/api/common/get-host-lower.ts b/src/server/api/common/get-host-lower.ts deleted file mode 100644 index 26ddf6c6d0..0000000000 --- a/src/server/api/common/get-host-lower.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { toUnicode } from 'punycode'; - -export default (host: string) => { - if (host == null) return null; - return toUnicode(host).toLowerCase(); -}; diff --git a/src/server/api/common/getters.ts b/src/server/api/common/getters.ts index 7a72e6489a..04716d19c6 100644 --- a/src/server/api/common/getters.ts +++ b/src/server/api/common/getters.ts @@ -1,18 +1,15 @@ -import * as mongo from 'mongodb'; -import Note from '../../../models/note'; -import User, { isRemoteUser, isLocalUser } from '../../../models/user'; import { IdentifiableError } from '../../../misc/identifiable-error'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { Notes, Users } from '../../../models'; /** * Get note for API processing */ -export async function getNote(noteId: mongo.ObjectID) { - const note = await Note.findOne({ - _id: noteId, - deletedAt: { $exists: false } - }); +export async function getNote(noteId: Note['id']) { + const note = await Notes.findOne(noteId); - if (note === null) { + if (note == null) { throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.'); } @@ -22,23 +19,10 @@ export async function getNote(noteId: mongo.ObjectID) { /** * Get user for API processing */ -export async function getUser(userId: mongo.ObjectID) { - const user = await User.findOne({ - _id: userId, - $or: [{ - isDeleted: { $exists: false } - }, { - isDeleted: false - }] - }, { - fields: { - data: false, - profile: false, - clientSettings: false - } - }); +export async function getUser(userId: User['id']) { + const user = await Users.findOne(userId); - if (user === null) { + if (user == null) { throw new IdentifiableError('15348ddd-432d-49c2-8a5a-8069753becff', 'No such user.'); } @@ -48,11 +32,11 @@ export async function getUser(userId: mongo.ObjectID) { /** * Get remote user for API processing */ -export async function getRemoteUser(userId: mongo.ObjectID) { +export async function getRemoteUser(userId: User['id']) { const user = await getUser(userId); - if (!isRemoteUser(user)) { - throw 'user is not a remote user'; + if (!Users.isRemoteUser(user)) { + throw new Error('user is not a remote user'); } return user; @@ -61,11 +45,11 @@ export async function getRemoteUser(userId: mongo.ObjectID) { /** * Get local user for API processing */ -export async function getLocalUser(userId: mongo.ObjectID) { +export async function getLocalUser(userId: User['id']) { const user = await getUser(userId); - if (!isLocalUser(user)) { - throw 'user is not a local user'; + if (!Users.isLocalUser(user)) { + throw new Error('user is not a local user'); } return user; diff --git a/src/server/api/common/is-native-token.ts b/src/server/api/common/is-native-token.ts index 6afbc99ab5..22af84aad2 100644 --- a/src/server/api/common/is-native-token.ts +++ b/src/server/api/common/is-native-token.ts @@ -1 +1 @@ -export default (token: string) => token.startsWith('!'); +export default (token: string) => token.startsWith('0'); diff --git a/src/server/api/common/make-pagination-query.ts b/src/server/api/common/make-pagination-query.ts new file mode 100644 index 0000000000..51c11e5dff --- /dev/null +++ b/src/server/api/common/make-pagination-query.ts @@ -0,0 +1,28 @@ +import { SelectQueryBuilder } from 'typeorm'; + +export function makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number) { + if (sinceId && untilId) { + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); + q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.id`, 'DESC'); + } else if (sinceId) { + q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId }); + q.orderBy(`${q.alias}.id`, 'ASC'); + } else if (untilId) { + q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId }); + q.orderBy(`${q.alias}.id`, 'DESC'); + } else if (sinceDate && untilDate) { + q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); + q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); + q.orderBy(`${q.alias}.createdAt`, 'DESC'); + } else if (sinceDate) { + q.andWhere(`${q.alias}.createdAt > :sinceDate`, { sinceDate: new Date(sinceDate) }); + q.orderBy(`${q.alias}.createdAt`, 'ASC'); + } else if (untilDate) { + q.andWhere(`${q.alias}.createdAt < :untilDate`, { untilDate: new Date(untilDate) }); + q.orderBy(`${q.alias}.createdAt`, 'DESC'); + } else { + q.orderBy(`${q.alias}.id`, 'DESC'); + } + return q; +} diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index 9f1e7e6ab4..2cb5a1f87f 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -1,77 +1,43 @@ -import * as mongo from 'mongodb'; -import isObjectId from '../../../misc/is-objectid'; -import Message from '../../../models/messaging-message'; -import { IMessagingMessage as IMessage } from '../../../models/messaging-message'; import { publishMainStream } from '../../../services/stream'; import { publishMessagingStream } from '../../../services/stream'; import { publishMessagingIndexStream } from '../../../services/stream'; -import User from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { MessagingMessage } from '../../../models/entities/messaging-message'; +import { MessagingMessages } from '../../../models'; +import { In } from 'typeorm'; /** * Mark messages as read */ -export default ( - user: string | mongo.ObjectID, - otherparty: string | mongo.ObjectID, - message: string | string[] | IMessage | IMessage[] | mongo.ObjectID | mongo.ObjectID[] -) => new Promise<any>(async (resolve, reject) => { - - const userId = isObjectId(user) - ? user - : new mongo.ObjectID(user); - - const otherpartyId = isObjectId(otherparty) - ? otherparty - : new mongo.ObjectID(otherparty); - - const ids: mongo.ObjectID[] = Array.isArray(message) - ? isObjectId(message[0]) - ? (message as mongo.ObjectID[]) - : typeof message[0] === 'string' - ? (message as string[]).map(m => new mongo.ObjectID(m)) - : (message as IMessage[]).map(m => m._id) - : isObjectId(message) - ? [(message as mongo.ObjectID)] - : typeof message === 'string' - ? [new mongo.ObjectID(message)] - : [(message as IMessage)._id]; +export default async ( + userId: User['id'], + otherpartyId: User['id'], + messageIds: MessagingMessage['id'][] +) => { + if (messageIds.length === 0) return; // Update documents - await Message.update({ - _id: { $in: ids }, + await MessagingMessages.update({ + id: In(messageIds), userId: otherpartyId, recipientId: userId, isRead: false }, { - $set: { - isRead: true - } - }, { - multi: true - }); + isRead: true + }); // Publish event - publishMessagingStream(otherpartyId, userId, 'read', ids.map(id => id.toString())); - publishMessagingIndexStream(userId, 'read', ids.map(id => id.toString())); + publishMessagingStream(otherpartyId, userId, 'read', messageIds); + publishMessagingIndexStream(userId, 'read', messageIds); // Calc count of my unread messages - const count = await Message - .count({ - recipientId: userId, - isRead: false - }, { - limit: 1 - }); + const count = await MessagingMessages.count({ + recipientId: userId, + isRead: false + }); if (count == 0) { - // Update flag - User.update({ _id: userId }, { - $set: { - hasUnreadMessagingMessage: false - } - }); - // 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行 publishMainStream(userId, 'readAllMessagingMessages'); } -}); +}; diff --git a/src/server/api/common/read-notification.ts b/src/server/api/common/read-notification.ts index 4361305119..c8d43ba286 100644 --- a/src/server/api/common/read-notification.ts +++ b/src/server/api/common/read-notification.ts @@ -1,72 +1,38 @@ -import * as mongo from 'mongodb'; -import isObjectId from '../../../misc/is-objectid'; -import { default as Notification, INotification } from '../../../models/notification'; import { publishMainStream } from '../../../services/stream'; -import Mute from '../../../models/mute'; -import User from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { Notification } from '../../../models/entities/notification'; +import { Mutings, Notifications } from '../../../models'; +import { In, Not } from 'typeorm'; /** * Mark notifications as read */ -export default ( - user: string | mongo.ObjectID, - message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[] -) => new Promise<any>(async (resolve, reject) => { - - const userId = isObjectId(user) - ? user - : new mongo.ObjectID(user); - - const ids: mongo.ObjectID[] = Array.isArray(message) - ? isObjectId(message[0]) - ? (message as mongo.ObjectID[]) - : typeof message[0] === 'string' - ? (message as string[]).map(m => new mongo.ObjectID(m)) - : (message as INotification[]).map(m => m._id) - : isObjectId(message) - ? [(message as mongo.ObjectID)] - : typeof message === 'string' - ? [new mongo.ObjectID(message)] - : [(message as INotification)._id]; - - const mute = await Mute.find({ +export async function readNotification( + userId: User['id'], + notificationIds: Notification['id'][] +) { + const mute = await Mutings.find({ muterId: userId }); const mutedUserIds = mute.map(m => m.muteeId); // Update documents - await Notification.update({ - _id: { $in: ids }, + await Notifications.update({ + id: In(notificationIds), isRead: false }, { - $set: { - isRead: true - } - }, { - multi: true - }); + isRead: true + }); // Calc count of my unread notifications - const count = await Notification - .count({ - notifieeId: userId, - notifierId: { - $nin: mutedUserIds - }, - isRead: false - }, { - limit: 1 - }); - - if (count == 0) { - // Update flag - User.update({ _id: userId }, { - $set: { - hasUnreadNotification: false - } - }); + const count = await Notifications.count({ + notifieeId: userId, + ...(mutedUserIds.length > 0 ? { notifierId: Not(In(mutedUserIds)) } : {}), + isRead: false + }); + if (count === 0) { // 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行 publishMainStream(userId, 'readAllNotifications'); } -}); +} diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts index 84cad3a935..0f4ee4ca11 100644 --- a/src/server/api/common/signin.ts +++ b/src/server/api/common/signin.ts @@ -1,7 +1,7 @@ import * as Koa from 'koa'; import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default function(ctx: Koa.BaseContext, user: ILocalUser, redirect = false) { if (redirect) { diff --git a/src/server/api/define.ts b/src/server/api/define.ts index f2fababc32..990cbf2a86 100644 --- a/src/server/api/define.ts +++ b/src/server/api/define.ts @@ -1,19 +1,19 @@ import * as fs from 'fs'; -import { ILocalUser } from '../../models/user'; -import { IApp } from '../../models/app'; +import { ILocalUser } from '../../models/entities/user'; import { IEndpointMeta } from './endpoints'; import { ApiError } from './error'; +import { App } from '../../models/entities/app'; type Params<T extends IEndpointMeta> = { - [P in keyof T['params']]: T['params'][P]['transform'] extends Function - ? ReturnType<T['params'][P]['transform']> - : ReturnType<T['params'][P]['validator']['get']>[0]; + [P in keyof T['params']]: NonNullable<T['params']>[P]['transform'] extends Function + ? ReturnType<NonNullable<T['params']>[P]['transform']> + : ReturnType<NonNullable<T['params']>[P]['validator']['get']>[0]; }; export type Response = Record<string, any> | void; -export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T>, user: ILocalUser, app: IApp, file?: any, cleanup?: Function) => Promise<Response>): (params: any, user: ILocalUser, app: IApp, file?: any) => Promise<any> { - return (params: any, user: ILocalUser, app: IApp, file?: any) => { +export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T>, user: ILocalUser, app: App, file?: any, cleanup?: Function) => Promise<Response>): (params: any, user: ILocalUser, app: App, file?: any) => Promise<any> { + return (params: any, user: ILocalUser, app: App, file?: any) => { function cleanup() { fs.unlink(file.path, () => {}); } @@ -34,11 +34,11 @@ export default function <T extends IEndpointMeta>(meta: T, cb: (params: Params<T }; } -function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, ApiError] { +function getParams<T extends IEndpointMeta>(defs: T, params: any): [Params<T>, ApiError | null] { if (defs.params == null) return [params, null]; const x: any = {}; - let err: ApiError = null; + let err: ApiError | null = null; Object.entries(defs.params).some(([k, def]) => { const [v, e] = def.validator.get(params[k]); if (e) { diff --git a/src/server/api/endpoints/admin/abuse-user-reports.ts b/src/server/api/endpoints/admin/abuse-user-reports.ts index d9fe3429ce..63d1dd795c 100644 --- a/src/server/api/endpoints/admin/abuse-user-reports.ts +++ b/src/server/api/endpoints/admin/abuse-user-reports.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Report, { packMany } from '../../../../models/abuse-user-report'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { AbuseUserReports } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { tags: ['admin'], @@ -17,37 +18,18 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, } }; export default define(meta, async (ps) => { - const sort = { - _id: -1 - }; - const query = {} as any; - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(AbuseUserReports.createQueryBuilder('report'), ps.sinceId, ps.untilId); - const reports = await Report - .find(query, { - limit: ps.limit, - sort: sort - }); + const reports = await query.take(ps.limit!).getMany(); - return await packMany(reports); + return await AbuseUserReports.packMany(reports); }); diff --git a/src/server/api/endpoints/admin/drive/files.ts b/src/server/api/endpoints/admin/drive/files.ts index 8ed417a429..7c6672e6de 100644 --- a/src/server/api/endpoints/admin/drive/files.ts +++ b/src/server/api/endpoints/admin/drive/files.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import File, { packMany } from '../../../../../models/drive-file'; import define from '../../../define'; import { fallback } from '../../../../../prelude/symbol'; +import { DriveFiles } from '../../../../../models'; export const meta = { tags: ['admin'], @@ -41,27 +41,25 @@ export const meta = { }; const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - '+createdAt': { uploadDate: -1 }, - '-createdAt': { uploadDate: 1 }, - '+size': { length: -1 }, - '-size': { length: 1 }, - [fallback]: { _id: -1 } + '+createdAt': { createdAt: -1 }, + '-createdAt': { createdAt: 1 }, + '+size': { size: -1 }, + '-size': { size: 1 }, + [fallback]: { id: -1 } }; export default define(meta, async (ps, me) => { - const q = { - 'metadata.deletedAt': { $exists: false }, - } as any; + const q = {} as any; - if (ps.origin == 'local') q['metadata._user.host'] = null; - if (ps.origin == 'remote') q['metadata._user.host'] = { $ne: null }; + if (ps.origin == 'local') q['userHost'] = null; + if (ps.origin == 'remote') q['userHost'] = { $ne: null }; - const files = await File - .find(q, { - limit: ps.limit, - sort: sort[ps.sort] || sort[fallback], - skip: ps.offset - }); + const files = await DriveFiles.find({ + where: q, + take: ps.limit!, + order: sort[ps.sort!] || sort[fallback], + skip: ps.offset + }); - return await packMany(files, { detail: true, withUser: true, self: true }); + return await DriveFiles.packMany(files, { detail: true, withUser: true, self: true }); }); diff --git a/src/server/api/endpoints/admin/drive/show-file.ts b/src/server/api/endpoints/admin/drive/show-file.ts index 405b6d44ce..a2b6c158f0 100644 --- a/src/server/api/endpoints/admin/drive/show-file.ts +++ b/src/server/api/endpoints/admin/drive/show-file.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import DriveFile from '../../../../../models/drive-file'; import { ApiError } from '../../../error'; +import { DriveFiles } from '../../../../../models'; export const meta = { tags: ['admin'], @@ -13,7 +13,6 @@ export const meta = { params: { fileId: { validator: $.type(ID), - transform: transform, }, }, @@ -27,9 +26,7 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const file = await DriveFile.findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); if (file == null) { throw new ApiError(meta.errors.noSuchFile); diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts index c126c8380f..c26e8dd04d 100644 --- a/src/server/api/endpoints/admin/emoji/add.ts +++ b/src/server/api/endpoints/admin/emoji/add.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import Emoji from '../../../../../models/emoji'; import define from '../../../define'; import { detectUrlMine } from '../../../../../misc/detect-url-mine'; +import { Emojis } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { desc: { @@ -32,7 +33,8 @@ export const meta = { export default define(meta, async (ps) => { const type = await detectUrlMine(ps.url); - const emoji = await Emoji.insert({ + const emoji = await Emojis.save({ + id: genId(), updatedAt: new Date(), name: ps.name, host: null, @@ -42,6 +44,6 @@ export default define(meta, async (ps) => { }); return { - id: emoji._id + id: emoji.id }; }); diff --git a/src/server/api/endpoints/admin/emoji/list.ts b/src/server/api/endpoints/admin/emoji/list.ts index 954f8f96c6..54686a5c5a 100644 --- a/src/server/api/endpoints/admin/emoji/list.ts +++ b/src/server/api/endpoints/admin/emoji/list.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; -import Emoji from '../../../../../models/emoji'; import define from '../../../define'; +import { Emojis } from '../../../../../models'; +import { toPunyNullable } from '../../../../../misc/convert-host'; export const meta = { desc: { @@ -21,12 +22,12 @@ export const meta = { }; export default define(meta, async (ps) => { - const emojis = await Emoji.find({ - host: ps.host + const emojis = await Emojis.find({ + host: toPunyNullable(ps.host) }); return emojis.map(e => ({ - id: e._id, + id: e.id, name: e.name, aliases: e.aliases, host: e.host, diff --git a/src/server/api/endpoints/admin/emoji/remove.ts b/src/server/api/endpoints/admin/emoji/remove.ts index 4c69dffbae..316834b884 100644 --- a/src/server/api/endpoints/admin/emoji/remove.ts +++ b/src/server/api/endpoints/admin/emoji/remove.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import Emoji from '../../../../../models/emoji'; import define from '../../../define'; -import ID from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; +import { Emojis } from '../../../../../models'; export const meta = { desc: { @@ -21,13 +21,9 @@ export const meta = { }; export default define(meta, async (ps) => { - const emoji = await Emoji.findOne({ - _id: ps.id - }); + const emoji = await Emojis.findOne(ps.id); if (emoji == null) throw new Error('emoji not found'); - await Emoji.remove({ _id: emoji._id }); - - return; + await Emojis.delete(emoji.id); }); diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts index 8b1c07be9e..48b4a4ee23 100644 --- a/src/server/api/endpoints/admin/emoji/update.ts +++ b/src/server/api/endpoints/admin/emoji/update.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import Emoji from '../../../../../models/emoji'; import define from '../../../define'; -import ID from '../../../../../misc/cafy-id'; import { detectUrlMine } from '../../../../../misc/detect-url-mine'; +import { ID } from '../../../../../misc/cafy-id'; +import { Emojis } from '../../../../../models'; export const meta = { desc: { @@ -34,23 +34,17 @@ export const meta = { }; export default define(meta, async (ps) => { - const emoji = await Emoji.findOne({ - _id: ps.id - }); + const emoji = await Emojis.findOne(ps.id); if (emoji == null) throw new Error('emoji not found'); const type = await detectUrlMine(ps.url); - await Emoji.update({ _id: emoji._id }, { - $set: { - updatedAt: new Date(), - name: ps.name, - aliases: ps.aliases, - url: ps.url, - type, - } + await Emojis.update(emoji.id, { + updatedAt: new Date(), + name: ps.name, + aliases: ps.aliases, + url: ps.url, + type, }); - - return; }); diff --git a/src/server/api/endpoints/admin/federation/remove-all-following.ts b/src/server/api/endpoints/admin/federation/remove-all-following.ts index 98afdfc2a5..25aae6db88 100644 --- a/src/server/api/endpoints/admin/federation/remove-all-following.ts +++ b/src/server/api/endpoints/admin/federation/remove-all-following.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import Following from '../../../../../models/following'; -import User from '../../../../../models/user'; import deleteFollowing from '../../../../../services/following/delete'; +import { Followings, Users } from '../../../../../models'; +import { ensure } from '../../../../../prelude/ensure'; export const meta = { tags: ['admin'], @@ -18,18 +18,16 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const followings = await Following.find({ - '_follower.host': ps.host + const followings = await Followings.find({ + followerHost: ps.host }); const pairs = await Promise.all(followings.map(f => Promise.all([ - User.findOne({ _id: f.followerId }), - User.findOne({ _id: f.followeeId }) + Users.findOne(f.followerId).then(ensure), + Users.findOne(f.followeeId).then(ensure) ]))); for (const pair of pairs) { deleteFollowing(pair[0], pair[1]); } - - return; }); diff --git a/src/server/api/endpoints/admin/federation/update-instance.ts b/src/server/api/endpoints/admin/federation/update-instance.ts index 0d127b53b3..90ab7a3ec5 100644 --- a/src/server/api/endpoints/admin/federation/update-instance.ts +++ b/src/server/api/endpoints/admin/federation/update-instance.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../../define'; -import Instance from '../../../../../models/instance'; +import { Instances } from '../../../../../models'; +import { toPuny } from '../../../../../misc/convert-host'; export const meta = { tags: ['admin'], @@ -13,10 +14,6 @@ export const meta = { validator: $.str }, - isBlocked: { - validator: $.bool - }, - isClosed: { validator: $.bool }, @@ -24,18 +21,13 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const instance = await Instance.findOne({ host: ps.host }); + const instance = await Instances.findOne({ host: toPuny(ps.host) }); if (instance == null) { throw new Error('instance not found'); } - Instance.update({ host: ps.host }, { - $set: { - isBlocked: ps.isBlocked, - isMarkedAsClosed: ps.isClosed - } + Instances.update({ host: toPuny(ps.host) }, { + isMarkedAsClosed: ps.isClosed }); - - return; }); diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts index 28aa301957..4e264feef6 100644 --- a/src/server/api/endpoints/admin/invite.ts +++ b/src/server/api/endpoints/admin/invite.ts @@ -1,6 +1,7 @@ import rndstr from 'rndstr'; -import RegistrationTicket from '../../../../models/registration-tickets'; import define from '../../define'; +import { RegistrationTickets } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; export const meta = { desc: { @@ -18,7 +19,8 @@ export const meta = { export default define(meta, async (ps) => { const code = rndstr({ length: 5, chars: '0-9' }); - await RegistrationTicket.insert({ + await RegistrationTickets.save({ + id: genId(), createdAt: new Date(), code: code }); diff --git a/src/server/api/endpoints/admin/logs.ts b/src/server/api/endpoints/admin/logs.ts index 805a42b9e0..86e99730c5 100644 --- a/src/server/api/endpoints/admin/logs.ts +++ b/src/server/api/endpoints/admin/logs.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import Log from '../../../../models/log'; +import { Logs } from '../../../../models'; +import { Brackets } from 'typeorm'; export const meta = { tags: ['admin'], @@ -27,41 +28,47 @@ export const meta = { }; export default define(meta, async (ps) => { - const sort = { - _id: -1 - }; - const query = {} as any; + const query = Logs.createQueryBuilder('log'); + + if (ps.level) query.andWhere('log.level = :level', { level: ps.level }); - if (ps.level) query.level = ps.level; if (ps.domain) { - for (const d of ps.domain.split(' ')) { - const qs: any[] = []; - let i = 0; - for (const sd of (d.startsWith('-') ? d.substr(1) : d).split('.')) { - qs.push({ - [`domain.${i}`]: d.startsWith('-') ? { $ne: sd } : sd - }); - i++; - } - if (d.startsWith('-')) { - if (query['$and'] == null) query['$and'] = []; - query['$and'].push({ - $and: qs - }); - } else { - if (query['$or'] == null) query['$or'] = []; - query['$or'].push({ - $and: qs - }); - } + const whiteDomains = ps.domain.split(' ').filter(x => !x.startsWith('-')); + const blackDomains = ps.domain.split(' ').filter(x => x.startsWith('-')).map(x => x.substr(1)); + + if (whiteDomains.length > 0) { + query.andWhere(new Brackets(qb => { + for (const whiteDomain of whiteDomains) { + let i = 0; + for (const subDomain of whiteDomain.split('.')) { + const p = `whiteSubDomain_${subDomain}_${i}`; + // SQL is 1 based, so we need '+ 1' + qb.orWhere(`log.domain[${i + 1}] = :${p}`, { [p]: subDomain }); + i++; + } + } + })); + } + + if (blackDomains.length > 0) { + query.andWhere(new Brackets(qb => { + for (const blackDomain of blackDomains) { + const subDomains = blackDomain.split('.'); + let i = 0; + for (const subDomain of subDomains) { + const p = `blackSubDomain_${subDomain}_${i}`; + // 全体で否定できないのでド・モルガンの法則で + // !(P && Q) を !P || !Q で表す + // SQL is 1 based, so we need '+ 1' + qb.orWhere(`log.domain[${i + 1}] != :${p}`, { [p]: subDomain }); + i++; + } + } + })); } } - const logs = await Log - .find(query, { - limit: ps.limit, - sort: sort - }); + const logs = await query.orderBy('log.createdAt', 'DESC').take(ps.limit!).getMany(); return logs; }); diff --git a/src/server/api/endpoints/admin/moderators/add.ts b/src/server/api/endpoints/admin/moderators/add.ts index 2271bcd1a9..a15f0a17a2 100644 --- a/src/server/api/endpoints/admin/moderators/add.ts +++ b/src/server/api/endpoints/admin/moderators/add.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import User from '../../../../../models/user'; +import { Users } from '../../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.update({ - _id: user._id - }, { - $set: { - isModerator: true - } + await Users.update(user.id, { + isModerator: true }); - - return; }); diff --git a/src/server/api/endpoints/admin/moderators/remove.ts b/src/server/api/endpoints/admin/moderators/remove.ts index 84143d3e35..209cf0814f 100644 --- a/src/server/api/endpoints/admin/moderators/remove.ts +++ b/src/server/api/endpoints/admin/moderators/remove.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import User from '../../../../../models/user'; +import { Users } from '../../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.update({ - _id: user._id - }, { - $set: { - isModerator: false - } + await Users.update(user.id, { + isModerator: false }); - - return; }); diff --git a/src/server/api/endpoints/admin/queue/jobs.ts b/src/server/api/endpoints/admin/queue/jobs.ts index c2496d7ef7..4e47775692 100644 --- a/src/server/api/endpoints/admin/queue/jobs.ts +++ b/src/server/api/endpoints/admin/queue/jobs.ts @@ -28,9 +28,9 @@ export default define(meta, async (ps) => { const queue = ps.domain === 'deliver' ? deliverQueue : ps.domain === 'inbox' ? inboxQueue : - null; + null as never; - const jobs = await queue.getJobs([ps.state], 0, ps.limit); + const jobs = await queue.getJobs([ps.state], 0, ps.limit!); return jobs.map(job => ({ id: job.id, diff --git a/src/server/api/endpoints/admin/remove-abuse-user-report.ts b/src/server/api/endpoints/admin/remove-abuse-user-report.ts index fa17e2c937..f293c00718 100644 --- a/src/server/api/endpoints/admin/remove-abuse-user-report.ts +++ b/src/server/api/endpoints/admin/remove-abuse-user-report.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import AbuseUserReport from '../../../../models/abuse-user-report'; +import { AbuseUserReports } from '../../../../models'; export const meta = { tags: ['admin'], @@ -12,23 +12,16 @@ export const meta = { params: { reportId: { validator: $.type(ID), - transform: transform }, } }; export default define(meta, async (ps) => { - const report = await AbuseUserReport.findOne({ - _id: ps.reportId - }); + const report = await AbuseUserReports.findOne(ps.reportId); if (report == null) { throw new Error('report not found'); } - await AbuseUserReport.remove({ - _id: report._id - }); - - return; + await AbuseUserReports.delete(report.id); }); diff --git a/src/server/api/endpoints/admin/reset-password.ts b/src/server/api/endpoints/admin/reset-password.ts index 73901d8358..42df668606 100644 --- a/src/server/api/endpoints/admin/reset-password.ts +++ b/src/server/api/endpoints/admin/reset-password.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; import * as bcrypt from 'bcryptjs'; import rndstr from 'rndstr'; +import { Users, UserProfiles } from '../../../../models'; export const meta = { desc: { @@ -18,7 +18,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to suspend' @@ -28,9 +27,7 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); @@ -45,12 +42,10 @@ export default define(meta, async (ps) => { // Generate hash of password const hash = bcrypt.hashSync(passwd); - await User.findOneAndUpdate({ - _id: user._id + await UserProfiles.update({ + userId: user.id }, { - $set: { - password: hash - } + password: hash }); return { diff --git a/src/server/api/endpoints/admin/show-user.ts b/src/server/api/endpoints/admin/show-user.ts index 985f71a873..452125dea0 100644 --- a/src/server/api/endpoints/admin/show-user.ts +++ b/src/server/api/endpoints/admin/show-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -16,7 +16,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to suspend' @@ -26,9 +25,7 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); diff --git a/src/server/api/endpoints/admin/show-users.ts b/src/server/api/endpoints/admin/show-users.ts index 5feb1b4fd8..97760ae797 100644 --- a/src/server/api/endpoints/admin/show-users.ts +++ b/src/server/api/endpoints/admin/show-users.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import User, { pack } from '../../../../models/user'; import define from '../../define'; -import { fallback } from '../../../../prelude/symbol'; +import { Users } from '../../../../models'; export const meta = { tags: ['admin'], @@ -55,51 +54,38 @@ export const meta = { } }; -const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - '+follower': { followersCount: -1 }, - '-follower': { followersCount: 1 }, - '+createdAt': { createdAt: -1 }, - '-createdAt': { createdAt: 1 }, - '+updatedAt': { updatedAt: -1 }, - '-updatedAt': { updatedAt: 1 }, - [fallback]: { _id: -1 } -}; - export default define(meta, async (ps, me) => { - const q = { - $and: [] - } as any; + const query = Users.createQueryBuilder('user'); + + switch (ps.state) { + case 'admin': query.where('user.isAdmin = TRUE'); break; + case 'moderator': query.where('user.isModerator = TRUE'); break; + case 'adminOrModerator': query.where('user.isAdmin = TRUE OR isModerator = TRUE'); break; + case 'verified': query.where('user.isVerified = TRUE'); break; + case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; + case 'silenced': query.where('user.isSilenced = TRUE'); break; + case 'suspended': query.where('user.isSuspended = TRUE'); break; + } - // state - q.$and.push( - ps.state == 'admin' ? { isAdmin: true } : - ps.state == 'moderator' ? { isModerator: true } : - ps.state == 'adminOrModerator' ? { - $or: [{ - isAdmin: true - }, { - isModerator: true - }] - } : - ps.state == 'verified' ? { isVerified: true } : - ps.state == 'silenced' ? { isSilenced: true } : - ps.state == 'suspended' ? { isSuspended: true } : - {} - ); + switch (ps.origin) { + case 'local': query.andWhere('user.host IS NULL'); break; + case 'remote': query.andWhere('user.host IS NOT NULL'); break; + } + + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; + case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; + default: query.orderBy('user.id', 'ASC'); break; + } - // origin - q.$and.push( - ps.origin == 'local' ? { host: null } : - ps.origin == 'remote' ? { host: { $ne: null } } : - {} - ); + query.take(ps.limit!); + query.skip(ps.offset); - const users = await User - .find(q, { - limit: ps.limit, - sort: sort[ps.sort] || sort[fallback], - skip: ps.offset - }); + const users = await query.getMany(); - return await Promise.all(users.map(user => pack(user, me, { detail: true }))); + return await Users.packMany(users, me, { detail: true }); }); diff --git a/src/server/api/endpoints/admin/silence-user.ts b/src/server/api/endpoints/admin/silence-user.ts index 2557d8de6a..83aa88012a 100644 --- a/src/server/api/endpoints/admin/silence-user.ts +++ b/src/server/api/endpoints/admin/silence-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to make silence' @@ -27,9 +26,7 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); @@ -39,13 +36,7 @@ export default define(meta, async (ps) => { throw new Error('cannot silence admin'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSilenced: true - } + await Users.update(user.id, { + isSilenced: true }); - - return; }); diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts index 0a2d309530..fa4d378708 100644 --- a/src/server/api/endpoints/admin/suspend-user.ts +++ b/src/server/api/endpoints/admin/suspend-user.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User, { IUser } from '../../../../models/user'; -import Following from '../../../../models/following'; import deleteFollowing from '../../../../services/following/delete'; +import { Users, Followings } from '../../../../models'; +import { User } from '../../../../models/entities/user'; export const meta = { desc: { @@ -19,7 +19,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to suspend' @@ -29,9 +28,7 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); @@ -45,27 +42,21 @@ export default define(meta, async (ps) => { throw new Error('cannot suspend moderator'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSuspended: true - } + await Users.update(user.id, { + isSuspended: true }); unFollowAll(user); - - return; }); -async function unFollowAll(follower: IUser) { - const followings = await Following.find({ - followerId: follower._id +async function unFollowAll(follower: User) { + const followings = await Followings.find({ + followerId: follower.id }); for (const following of followings) { - const followee = await User.findOne({ - _id: following.followeeId + const followee = await Users.findOne({ + id: following.followeeId }); if (followee == null) { diff --git a/src/server/api/endpoints/admin/unsilence-user.ts b/src/server/api/endpoints/admin/unsilence-user.ts index 01bf41aaef..f9b173366b 100644 --- a/src/server/api/endpoints/admin/unsilence-user.ts +++ b/src/server/api/endpoints/admin/unsilence-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to unsilence' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSilenced: false - } + await Users.update(user.id, { + isSilenced: false }); - - return; }); diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts index 5da35f28e6..08dae034d3 100644 --- a/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/src/server/api/endpoints/admin/unsuspend-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to unsuspend' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isSuspended: false - } + await Users.update(user.id, { + isSuspended: false }); - - return; }); diff --git a/src/server/api/endpoints/admin/unverify-user.ts b/src/server/api/endpoints/admin/unverify-user.ts index d3ca05cb39..b215dbf10d 100644 --- a/src/server/api/endpoints/admin/unverify-user.ts +++ b/src/server/api/endpoints/admin/unverify-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to unverify' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isVerified: false - } + await Users.update(user.id, { + isVerified: false }); - - return; }); diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index f8f7cb5d9a..e242ac71a1 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; -import Meta from '../../../../models/meta'; import define from '../../define'; +import { Metas } from '../../../../models'; +import { Meta } from '../../../../models/entities/meta'; export const meta = { desc: { @@ -55,7 +56,7 @@ export const meta = { } }, - hidedTags: { + hiddenTags: { validator: $.optional.nullable.arr($.str), desc: { 'ja-JP': '統計などで無視するハッシュタグ' @@ -253,27 +254,6 @@ export const meta = { } }, - enableExternalUserRecommendation: { - validator: $.optional.bool, - desc: { - 'ja-JP': '外部ユーザーレコメンデーションを有効にする' - } - }, - - externalUserRecommendationEngine: { - validator: $.optional.nullable.str, - desc: { - 'ja-JP': '外部ユーザーレコメンデーションのサードパーティエンジン' - } - }, - - externalUserRecommendationTimeout: { - validator: $.optional.nullable.num.min(0), - desc: { - 'ja-JP': '外部ユーザーレコメンデーションのタイムアウト (ミリ秒)' - } - }, - enableEmail: { validator: $.optional.bool, desc: { @@ -347,7 +327,7 @@ export const meta = { }; export default define(meta, async (ps) => { - const set = {} as any; + const set = {} as Partial<Meta>; if (ps.announcements) { set.announcements = ps.announcements; @@ -373,8 +353,8 @@ export default define(meta, async (ps) => { set.useStarForReactionFallback = ps.useStarForReactionFallback; } - if (Array.isArray(ps.hidedTags)) { - set.hidedTags = ps.hidedTags; + if (Array.isArray(ps.hiddenTags)) { + set.hiddenTags = ps.hiddenTags; } if (ps.mascotImageUrl !== undefined) { @@ -430,11 +410,11 @@ export default define(meta, async (ps) => { } if (ps.maintainerName !== undefined) { - set['maintainer.name'] = ps.maintainerName; + set.maintainerName = ps.maintainerName; } if (ps.maintainerEmail !== undefined) { - set['maintainer.email'] = ps.maintainerEmail; + set.maintainerEmail = ps.maintainerEmail; } if (ps.langs !== undefined) { @@ -481,18 +461,6 @@ export default define(meta, async (ps) => { set.discordClientSecret = ps.discordClientSecret; } - if (ps.enableExternalUserRecommendation !== undefined) { - set.enableExternalUserRecommendation = ps.enableExternalUserRecommendation; - } - - if (ps.externalUserRecommendationEngine !== undefined) { - set.externalUserRecommendationEngine = ps.externalUserRecommendationEngine; - } - - if (ps.externalUserRecommendationTimeout !== undefined) { - set.externalUserRecommendationTimeout = ps.externalUserRecommendationTimeout; - } - if (ps.enableEmail !== undefined) { set.enableEmail = ps.enableEmail; } @@ -537,9 +505,11 @@ export default define(meta, async (ps) => { set.swPrivateKey = ps.swPrivateKey; } - await Meta.update({}, { - $set: set - }, { upsert: true }); + const meta = await Metas.findOne(); - return; + if (meta) { + await Metas.update(meta.id, set); + } else { + await Metas.save(set); + } }); diff --git a/src/server/api/endpoints/admin/update-remote-user.ts b/src/server/api/endpoints/admin/update-remote-user.ts index a74685912c..f9716328d5 100644 --- a/src/server/api/endpoints/admin/update-remote-user.ts +++ b/src/server/api/endpoints/admin/update-remote-user.ts @@ -1,6 +1,5 @@ -import * as mongo from 'mongodb'; import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { getRemoteUser } from '../../common/getters'; import { updatePerson } from '../../../../remote/activitypub/models/person'; @@ -19,7 +18,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to update' @@ -29,11 +27,6 @@ export const meta = { }; export default define(meta, async (ps) => { - await updatePersonById(ps.userId); - return; + const user = await getRemoteUser(ps.userId); + await updatePerson(user.uri!); }); - -async function updatePersonById(userId: mongo.ObjectID) { - const user = await getRemoteUser(userId); - await updatePerson(user.uri); -} diff --git a/src/server/api/endpoints/admin/verify-user.ts b/src/server/api/endpoints/admin/verify-user.ts index f67b6c3bf0..c1b447a92b 100644 --- a/src/server/api/endpoints/admin/verify-user.ts +++ b/src/server/api/endpoints/admin/verify-user.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -17,7 +17,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーID', 'en-US': 'The user ID which you want to verify' @@ -27,21 +26,13 @@ export const meta = { }; export default define(meta, async (ps) => { - const user = await User.findOne({ - _id: ps.userId - }); + const user = await Users.findOne(ps.userId as string); if (user == null) { throw new Error('user not found'); } - await User.findOneAndUpdate({ - _id: user._id - }, { - $set: { - isVerified: true - } + await Users.update(user.id, { + isVerified: true }); - - return; }); diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts deleted file mode 100644 index 978e9f64b7..0000000000 --- a/src/server/api/endpoints/aggregation/hashtags.ts +++ /dev/null @@ -1,72 +0,0 @@ -import Note from '../../../../models/note'; -import define from '../../define'; -import fetchMeta from '../../../../misc/fetch-meta'; - -export const meta = { - tags: ['hashtags'], - - requireCredential: false, -}; - -export default define(meta, async (ps) => { - const instance = await fetchMeta(); - const hidedTags = instance.hidedTags.map(t => t.toLowerCase()); - - // 重い - //const span = 1000 * 60 * 60 * 24 * 7; // 1週間 - const span = 1000 * 60 * 60 * 24; // 1日 - - //#region 1. 指定期間の内に投稿されたハッシュタグ(とユーザーのペア)を集計 - const data = await Note.aggregate([{ - $match: { - createdAt: { - $gt: new Date(Date.now() - span) - }, - tagsLower: { - $exists: true, - $ne: [] - } - } - }, { - $unwind: '$tagsLower' - }, { - $group: { - _id: { tag: '$tagsLower', userId: '$userId' } - } - }]) as { - _id: { - tag: string; - userId: any; - } - }[]; - //#endregion - - if (data.length == 0) { - return []; - } - - let tags: { - name: string; - count: number; - }[] = []; - - // カウント - for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) { - const i = tags.findIndex(tag => tag.name == x.tag); - if (i != -1) { - tags[i].count++; - } else { - tags.push({ - name: x.tag, - count: 1 - }); - } - } - - // タグを人気順に並べ替え - tags.sort((a, b) => b.count - a.count); - - tags = tags.slice(0, 30); - - return tags; -}); diff --git a/src/server/api/endpoints/ap/show.ts b/src/server/api/endpoints/ap/show.ts index 7f4afa1f6e..1b992eeaa7 100644 --- a/src/server/api/endpoints/ap/show.ts +++ b/src/server/api/endpoints/ap/show.ts @@ -1,15 +1,16 @@ import $ from 'cafy'; import define from '../../define'; import config from '../../../../config'; -import * as mongo from 'mongodb'; -import User, { pack as packUser, IUser } from '../../../../models/user'; import { createPerson } from '../../../../remote/activitypub/models/person'; -import Note, { pack as packNote, INote } from '../../../../models/note'; import { createNote } from '../../../../remote/activitypub/models/note'; import Resolver from '../../../../remote/activitypub/resolver'; import { ApiError } from '../../error'; -import Instance from '../../../../models/instance'; import { extractDbHost } from '../../../../misc/convert-host'; +import { Users, Notes } from '../../../../models'; +import { Note } from '../../../../models/entities/note'; +import { User } from '../../../../models/entities/user'; +import fetchMeta from '../../../../misc/fetch-meta'; +import { validActor } from '../../../../remote/activitypub/type'; export const meta = { tags: ['federation'], @@ -53,25 +54,40 @@ export default define(meta, async (ps) => { async function fetchAny(uri: string) { // URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - const [user, note] = await Promise.all([ - User.findOne({ _id: id }), - Note.findOne({ _id: id }) - ]); + const parts = uri.split('/'); + const id = parts.pop(); + const type = parts.pop(); - const packed = await mergePack(user, note); - if (packed !== null) return packed; + if (type === 'notes') { + const note = await Notes.findOne(id); + + if (note) { + return { + type: 'Note', + object: await Notes.pack(note, null, { detail: true }) + }; + } + } else if (type === 'users') { + const user = await Users.findOne(id); + + if (user) { + return { + type: 'User', + object: await Users.pack(user, null, { detail: true }) + }; + } + } } // ブロックしてたら中断 - const instance = await Instance.findOne({ host: extractDbHost(uri) }); - if (instance && instance.isBlocked) return null; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(extractDbHost(uri))) return null; // URI(AP Object id)としてDB検索 { const [user, note] = await Promise.all([ - User.findOne({ uri: uri }), - Note.findOne({ uri: uri }) + Users.findOne({ uri: uri }), + Notes.findOne({ uri: uri }) ]); const packed = await mergePack(user, note); @@ -86,8 +102,8 @@ async function fetchAny(uri: string) { // これはDBに存在する可能性があるため再度DB検索 if (uri !== object.id) { const [user, note] = await Promise.all([ - User.findOne({ uri: object.id }), - Note.findOne({ uri: object.id }) + Users.findOne({ uri: object.id }), + Notes.findOne({ uri: object.id }) ]); const packed = await mergePack(user, note); @@ -95,11 +111,11 @@ async function fetchAny(uri: string) { } // それでもみつからなければ新規であるため登録 - if (object.type === 'Person') { + if (validActor.includes(object.type)) { const user = await createPerson(object.id); return { type: 'User', - object: await packUser(user, null, { detail: true }) + object: await Users.pack(user, null, { detail: true }) }; } @@ -107,25 +123,25 @@ async function fetchAny(uri: string) { const note = await createNote(object.id); return { type: 'Note', - object: await packNote(note, null, { detail: true }) + object: await Notes.pack(note!, null, { detail: true }) }; } return null; } -async function mergePack(user: IUser, note: INote) { - if (user !== null) { +async function mergePack(user: User | null | undefined, note: Note | null | undefined) { + if (user != null) { return { type: 'User', - object: await packUser(user, null, { detail: true }) + object: await Users.pack(user, null, { detail: true }) }; } - if (note !== null) { + if (note != null) { return { type: 'Note', - object: await packNote(note, null, { detail: true }) + object: await Notes.pack(note, null, { detail: true }) }; } diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts index 67b1b8150a..71f21fdf47 100644 --- a/src/server/api/endpoints/app/create.ts +++ b/src/server/api/endpoints/app/create.ts @@ -1,7 +1,8 @@ import rndstr from 'rndstr'; import $ from 'cafy'; -import App, { pack } from '../../../../models/app'; import define from '../../define'; +import { Apps } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; export const meta = { tags: ['app'], @@ -34,9 +35,10 @@ export default define(meta, async (ps, user) => { const secret = rndstr('a-zA-Z0-9', 32); // Create account - const app = await App.insert({ + const app = await Apps.save({ + id: genId(), createdAt: new Date(), - userId: user && user._id, + userId: user ? user.id : null, name: ps.name, description: ps.description, permission: ps.permission, @@ -44,7 +46,7 @@ export default define(meta, async (ps, user) => { secret: secret }); - return await pack(app, null, { + return await Apps.pack(app, null, { detail: true, includeSecret: true }); diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts index f3f5b843b3..ce9baed2ae 100644 --- a/src/server/api/endpoints/app/show.ts +++ b/src/server/api/endpoints/app/show.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import App, { pack } from '../../../../models/app'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; +import { Apps } from '../../../../models'; export const meta = { tags: ['app'], @@ -10,7 +10,6 @@ export const meta = { params: { appId: { validator: $.type(ID), - transform: transform }, }, @@ -27,14 +26,14 @@ export default define(meta, async (ps, user, app) => { const isSecure = user != null && app == null; // Lookup app - const ap = await App.findOne({ _id: ps.appId }); + const ap = await Apps.findOne(ps.appId); - if (ap === null) { + if (ap == null) { throw new ApiError(meta.errors.noSuchApp); } - return await pack(ap, user, { + return await Apps.pack(ap, user, { detail: true, - includeSecret: isSecure && ap.userId.equals(user._id) + includeSecret: isSecure && (ap.userId === user.id) }); }); diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts index cedf7821fe..a584e7267b 100644 --- a/src/server/api/endpoints/auth/accept.ts +++ b/src/server/api/endpoints/auth/accept.ts @@ -1,11 +1,11 @@ import rndstr from 'rndstr'; import * as crypto from 'crypto'; import $ from 'cafy'; -import App from '../../../../models/app'; -import AuthSess from '../../../../models/auth-session'; -import AccessToken from '../../../../models/access-token'; import define from '../../define'; import { ApiError } from '../../error'; +import { AuthSessions, AccessTokens, Apps } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; +import { ensure } from '../../../../prelude/ensure'; export const meta = { tags: ['auth'], @@ -31,27 +31,25 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch token - const session = await AuthSess + const session = await AuthSessions .findOne({ token: ps.token }); - if (session === null) { + if (session == null) { throw new ApiError(meta.errors.noSuchSession); } // Generate access token - const accessToken = rndstr('a-zA-Z0-9', 32); + const accessToken = '1' + rndstr('a-zA-Z0-9', 15); // Fetch exist access token - const exist = await AccessToken.findOne({ + const exist = await AccessTokens.findOne({ appId: session.appId, - userId: user._id, + userId: user.id, }); - if (exist === null) { + if (exist == null) { // Lookup app - const app = await App.findOne({ - _id: session.appId - }); + const app = await Apps.findOne(session.appId).then(ensure); // Generate Hash const sha256 = crypto.createHash('sha256'); @@ -59,21 +57,18 @@ export default define(meta, async (ps, user) => { const hash = sha256.digest('hex'); // Insert access token doc - await AccessToken.insert({ + await AccessTokens.save({ + id: genId(), createdAt: new Date(), appId: session.appId, - userId: user._id, + userId: user.id, token: accessToken, hash: hash }); } // Update session - await AuthSess.update(session._id, { - $set: { - userId: user._id - } + await AuthSessions.update(session.id, { + userId: user.id }); - - return; }); diff --git a/src/server/api/endpoints/auth/session/generate.ts b/src/server/api/endpoints/auth/session/generate.ts index e12bea7681..5a9bfe6451 100644 --- a/src/server/api/endpoints/auth/session/generate.ts +++ b/src/server/api/endpoints/auth/session/generate.ts @@ -1,10 +1,10 @@ import * as uuid from 'uuid'; import $ from 'cafy'; -import App from '../../../../../models/app'; -import AuthSess from '../../../../../models/auth-session'; import config from '../../../../../config'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { Apps, AuthSessions } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { tags: ['auth'], @@ -46,7 +46,7 @@ export const meta = { export default define(meta, async (ps) => { // Lookup app - const app = await App.findOne({ + const app = await Apps.findOne({ secret: ps.appSecret }); @@ -58,9 +58,10 @@ export default define(meta, async (ps) => { const token = uuid.v4(); // Create session token document - const doc = await AuthSess.insert({ + const doc = await AuthSessions.save({ + id: genId(), createdAt: new Date(), - appId: app._id, + appId: app.id, token: token }); diff --git a/src/server/api/endpoints/auth/session/show.ts b/src/server/api/endpoints/auth/session/show.ts index 992e0a499e..e6ecd8b839 100644 --- a/src/server/api/endpoints/auth/session/show.ts +++ b/src/server/api/endpoints/auth/session/show.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import AuthSess, { pack } from '../../../../../models/auth-session'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { AuthSessions } from '../../../../../models'; export const meta = { tags: ['auth'], @@ -29,7 +29,7 @@ export const meta = { export default define(meta, async (ps, user) => { // Lookup session - const session = await AuthSess.findOne({ + const session = await AuthSessions.findOne({ token: ps.token }); @@ -37,5 +37,5 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.noSuchSession); } - return await pack(session, user); + return await AuthSessions.pack(session, user); }); diff --git a/src/server/api/endpoints/auth/session/userkey.ts b/src/server/api/endpoints/auth/session/userkey.ts index e09e16e658..7126ac52c1 100644 --- a/src/server/api/endpoints/auth/session/userkey.ts +++ b/src/server/api/endpoints/auth/session/userkey.ts @@ -1,10 +1,8 @@ import $ from 'cafy'; -import App from '../../../../../models/app'; -import AuthSess from '../../../../../models/auth-session'; -import AccessToken from '../../../../../models/access-token'; -import { pack } from '../../../../../models/user'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { Apps, AuthSessions, AccessTokens, Users } from '../../../../../models'; +import { ensure } from '../../../../../prelude/ensure'; export const meta = { tags: ['auth'], @@ -67,7 +65,7 @@ export const meta = { export default define(meta, async (ps) => { // Lookup app - const app = await App.findOne({ + const app = await Apps.findOne({ secret: ps.appSecret }); @@ -76,13 +74,12 @@ export default define(meta, async (ps) => { } // Fetch token - const session = await AuthSess - .findOne({ - token: ps.token, - appId: app._id - }); + const session = await AuthSessions.findOne({ + token: ps.token, + appId: app.id + }); - if (session === null) { + if (session == null) { throw new ApiError(meta.errors.noSuchSession); } @@ -91,25 +88,17 @@ export default define(meta, async (ps) => { } // Lookup access token - const accessToken = await AccessToken.findOne({ - appId: app._id, + const accessToken = await AccessTokens.findOne({ + appId: app.id, userId: session.userId - }); + }).then(ensure); // Delete session - - /* https://github.com/Automattic/monk/issues/178 - AuthSess.deleteOne({ - _id: session._id - }); - */ - AuthSess.remove({ - _id: session._id - }); + AuthSessions.delete(session.id); return { accessToken: accessToken.token, - user: await pack(session.userId, null, { + user: await Users.pack(session.userId, null, { detail: true }) }; diff --git a/src/server/api/endpoints/blocking/create.ts b/src/server/api/endpoints/blocking/create.ts index e723cb0386..0d6626b2d5 100644 --- a/src/server/api/endpoints/blocking/create.ts +++ b/src/server/api/endpoints/blocking/create.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../models/user'; -import Blocking from '../../../../models/blocking'; import create from '../../../../services/blocking/create'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Blockings, NoteWatchings } from '../../../../models'; export const meta = { stability: 'stable', @@ -25,12 +24,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:blocks', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => { const blocker = user; // 自分自身 - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.blockeeIsYourself); } @@ -74,19 +72,22 @@ export default define(meta, async (ps, user) => { }); // Check if already blocking - const exist = await Blocking.findOne({ - blockerId: blocker._id, - blockeeId: blockee._id + const exist = await Blockings.findOne({ + blockerId: blocker.id, + blockeeId: blockee.id }); - if (exist !== null) { + if (exist != null) { throw new ApiError(meta.errors.alreadyBlocking); } // Create blocking await create(blocker, blockee); - return await pack(blockee._id, user, { - detail: true + NoteWatchings.delete({ + userId: blocker.id, + noteUserId: blockee.id }); + + return await Blockings.pack(blockee.id, user); }); diff --git a/src/server/api/endpoints/blocking/delete.ts b/src/server/api/endpoints/blocking/delete.ts index 2a9fdc5e24..e304dca811 100644 --- a/src/server/api/endpoints/blocking/delete.ts +++ b/src/server/api/endpoints/blocking/delete.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../models/user'; -import Blocking from '../../../../models/blocking'; import deleteBlocking from '../../../../services/blocking/delete'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Blockings } from '../../../../models'; export const meta = { stability: 'stable', @@ -25,12 +24,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:blocks', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => { const blocker = user; // Check if the blockee is yourself - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.blockeeIsYourself); } @@ -74,19 +72,17 @@ export default define(meta, async (ps, user) => { }); // Check not blocking - const exist = await Blocking.findOne({ - blockerId: blocker._id, - blockeeId: blockee._id + const exist = await Blockings.findOne({ + blockerId: blocker.id, + blockeeId: blockee.id }); - if (exist === null) { + if (exist == null) { throw new ApiError(meta.errors.notBlocking); } // Delete blocking await deleteBlocking(blocker, blockee); - return await pack(blockee._id, user, { - detail: true - }); + return await Blockings.pack(blockee.id, user); }); diff --git a/src/server/api/endpoints/blocking/list.ts b/src/server/api/endpoints/blocking/list.ts index b9ad6e8a3f..97f353579d 100644 --- a/src/server/api/endpoints/blocking/list.ts +++ b/src/server/api/endpoints/blocking/list.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Blocking, { packMany } from '../../../../models/blocking'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { Blockings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -13,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'following-read', + kind: 'read:blocks', params: { limit: { @@ -23,12 +24,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -41,30 +40,12 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const query = { - blockerId: me._id - } as any; + const query = makePaginationQuery(Blockings.createQueryBuilder('blocking'), ps.sinceId, ps.untilId) + .andWhere(`blocking.blockerId = :meId`, { meId: me.id }); - const sort = { - _id: -1 - }; + const blockings = await query + .take(ps.limit!) + .getMany(); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const blockings = await Blocking - .find(query, { - limit: ps.limit, - sort: sort - }); - - return await packMany(blockings, me); + return await Blockings.packMany(blockings, me); }); diff --git a/src/server/api/endpoints/charts/active-users.ts b/src/server/api/endpoints/charts/active-users.ts index 9dad942e06..f0349b17f3 100644 --- a/src/server/api/endpoints/charts/active-users.ts +++ b/src/server/api/endpoints/charts/active-users.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import activeUsersChart from '../../../../services/chart/active-users'; +import { convertLog } from '../../../../services/chart/core'; +import { activeUsersChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -28,14 +29,9 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(activeUsersChart.schema), }; export default define(meta, async (ps) => { - return await activeUsersChart.getChart(ps.span as any, ps.limit); + return await activeUsersChart.getChart(ps.span as any, ps.limit!); }); diff --git a/src/server/api/endpoints/charts/drive.ts b/src/server/api/endpoints/charts/drive.ts index 6bbb266f96..ae6d894407 100644 --- a/src/server/api/endpoints/charts/drive.ts +++ b/src/server/api/endpoints/charts/drive.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import driveChart, { driveLogSchema } from '../../../../services/chart/drive'; -import { convertLog } from '../../../../services/chart'; +import { convertLog } from '../../../../services/chart/core'; +import { driveChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -29,9 +29,9 @@ export const meta = { }, }, - res: convertLog(driveLogSchema), + res: convertLog(driveChart.schema), }; export default define(meta, async (ps) => { - return await driveChart.getChart(ps.span as any, ps.limit); + return await driveChart.getChart(ps.span as any, ps.limit!); }); diff --git a/src/server/api/endpoints/charts/federation.ts b/src/server/api/endpoints/charts/federation.ts index c7b34f1015..34e9bfee5f 100644 --- a/src/server/api/endpoints/charts/federation.ts +++ b/src/server/api/endpoints/charts/federation.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import federationChart from '../../../../services/chart/federation'; +import { convertLog } from '../../../../services/chart/core'; +import { federationChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -28,14 +29,9 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(federationChart.schema), }; export default define(meta, async (ps) => { - return await federationChart.getChart(ps.span as any, ps.limit); + return await federationChart.getChart(ps.span as any, ps.limit!); }); diff --git a/src/server/api/endpoints/charts/hashtag.ts b/src/server/api/endpoints/charts/hashtag.ts index 4db6e62408..eceb0b275c 100644 --- a/src/server/api/endpoints/charts/hashtag.ts +++ b/src/server/api/endpoints/charts/hashtag.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import hashtagChart from '../../../../services/chart/hashtag'; +import { convertLog } from '../../../../services/chart/core'; +import { hashtagChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -35,14 +36,9 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(hashtagChart.schema), }; export default define(meta, async (ps) => { - return await hashtagChart.getChart(ps.span as any, ps.limit, ps.tag); + return await hashtagChart.getChart(ps.span as any, ps.limit!, ps.tag); }); diff --git a/src/server/api/endpoints/charts/instance.ts b/src/server/api/endpoints/charts/instance.ts index 3fe85f086a..e99c17ae65 100644 --- a/src/server/api/endpoints/charts/instance.ts +++ b/src/server/api/endpoints/charts/instance.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import instanceChart from '../../../../services/chart/instance'; +import { convertLog } from '../../../../services/chart/core'; +import { instanceChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -36,14 +37,9 @@ export const meta = { } }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(instanceChart.schema), }; export default define(meta, async (ps) => { - return await instanceChart.getChart(ps.span as any, ps.limit, ps.host); + return await instanceChart.getChart(ps.span as any, ps.limit!, ps.host); }); diff --git a/src/server/api/endpoints/charts/network.ts b/src/server/api/endpoints/charts/network.ts index 48b1d0f66f..648588fbe5 100644 --- a/src/server/api/endpoints/charts/network.ts +++ b/src/server/api/endpoints/charts/network.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import networkChart from '../../../../services/chart/network'; +import { convertLog } from '../../../../services/chart/core'; +import { networkChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -28,14 +29,9 @@ export const meta = { }, }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(networkChart.schema), }; export default define(meta, async (ps) => { - return await networkChart.getChart(ps.span as any, ps.limit); + return await networkChart.getChart(ps.span as any, ps.limit!); }); diff --git a/src/server/api/endpoints/charts/notes.ts b/src/server/api/endpoints/charts/notes.ts index cc0ca8bef7..074c4978cd 100644 --- a/src/server/api/endpoints/charts/notes.ts +++ b/src/server/api/endpoints/charts/notes.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import notesChart, { notesLogSchema } from '../../../../services/chart/notes'; -import { convertLog } from '../../../../services/chart'; +import { convertLog } from '../../../../services/chart/core'; +import { notesChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -29,9 +29,9 @@ export const meta = { }, }, - res: convertLog(notesLogSchema), + res: convertLog(notesChart.schema), }; export default define(meta, async (ps) => { - return await notesChart.getChart(ps.span as any, ps.limit); + return await notesChart.getChart(ps.span as any, ps.limit!); }); diff --git a/src/server/api/endpoints/charts/user/drive.ts b/src/server/api/endpoints/charts/user/drive.ts index 064c7c7b72..918fb62c6a 100644 --- a/src/server/api/endpoints/charts/user/drive.ts +++ b/src/server/api/endpoints/charts/user/drive.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import perUserDriveChart, { perUserDriveLogSchema } from '../../../../../services/chart/per-user-drive'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import { convertLog } from '../../../../../services/chart'; +import { ID } from '../../../../../misc/cafy-id'; +import { convertLog } from '../../../../../services/chart/core'; +import { perUserDriveChart } from '../../../../../services/chart'; export const meta = { stability: 'stable', @@ -31,7 +31,6 @@ export const meta = { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -39,9 +38,9 @@ export const meta = { } }, - res: convertLog(perUserDriveLogSchema), + res: convertLog(perUserDriveChart.schema), }; export default define(meta, async (ps) => { - return await perUserDriveChart.getChart(ps.span as any, ps.limit, ps.userId); + return await perUserDriveChart.getChart(ps.span as any, ps.limit!, ps.userId); }); diff --git a/src/server/api/endpoints/charts/user/following.ts b/src/server/api/endpoints/charts/user/following.ts index f5b1355038..5d86e85d31 100644 --- a/src/server/api/endpoints/charts/user/following.ts +++ b/src/server/api/endpoints/charts/user/following.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import perUserFollowingChart, { perUserFollowingLogSchema } from '../../../../../services/chart/per-user-following'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import { convertLog } from '../../../../../services/chart'; +import { ID } from '../../../../../misc/cafy-id'; +import { convertLog } from '../../../../../services/chart/core'; +import { perUserFollowingChart } from '../../../../../services/chart'; export const meta = { stability: 'stable', @@ -31,7 +31,6 @@ export const meta = { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -39,9 +38,9 @@ export const meta = { } }, - res: convertLog(perUserFollowingLogSchema), + res: convertLog(perUserFollowingChart.schema), }; export default define(meta, async (ps) => { - return await perUserFollowingChart.getChart(ps.span as any, ps.limit, ps.userId); + return await perUserFollowingChart.getChart(ps.span as any, ps.limit!, ps.userId); }); diff --git a/src/server/api/endpoints/charts/user/notes.ts b/src/server/api/endpoints/charts/user/notes.ts index 7e31978bf3..d39a20df16 100644 --- a/src/server/api/endpoints/charts/user/notes.ts +++ b/src/server/api/endpoints/charts/user/notes.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import perUserNotesChart, { perUserNotesLogSchema } from '../../../../../services/chart/per-user-notes'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import { convertLog } from '../../../../../services/chart'; +import { ID } from '../../../../../misc/cafy-id'; +import { convertLog } from '../../../../../services/chart/core'; +import { perUserNotesChart } from '../../../../../services/chart'; export const meta = { stability: 'stable', @@ -31,7 +31,6 @@ export const meta = { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -39,9 +38,9 @@ export const meta = { } }, - res: convertLog(perUserNotesLogSchema), + res: convertLog(perUserNotesChart.schema), }; export default define(meta, async (ps) => { - return await perUserNotesChart.getChart(ps.span as any, ps.limit, ps.userId); + return await perUserNotesChart.getChart(ps.span as any, ps.limit!, ps.userId); }); diff --git a/src/server/api/endpoints/charts/user/reactions.ts b/src/server/api/endpoints/charts/user/reactions.ts index 51ff83f20e..5b88a1d715 100644 --- a/src/server/api/endpoints/charts/user/reactions.ts +++ b/src/server/api/endpoints/charts/user/reactions.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; import define from '../../../define'; -import perUserReactionsChart from '../../../../../services/chart/per-user-reactions'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; +import { convertLog } from '../../../../../services/chart/core'; +import { perUserReactionsChart } from '../../../../../services/chart'; export const meta = { stability: 'stable', @@ -30,7 +31,6 @@ export const meta = { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -38,14 +38,9 @@ export const meta = { } }, - res: { - type: 'array', - items: { - type: 'object', - }, - }, + res: convertLog(perUserReactionsChart.schema), }; export default define(meta, async (ps) => { - return await perUserReactionsChart.getChart(ps.span as any, ps.limit, ps.userId); + return await perUserReactionsChart.getChart(ps.span as any, ps.limit!, ps.userId); }); diff --git a/src/server/api/endpoints/charts/users.ts b/src/server/api/endpoints/charts/users.ts index 9de54a630e..17de5756da 100644 --- a/src/server/api/endpoints/charts/users.ts +++ b/src/server/api/endpoints/charts/users.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import usersChart, { usersLogSchema } from '../../../../services/chart/users'; -import { convertLog } from '../../../../services/chart'; +import { convertLog } from '../../../../services/chart/core'; +import { usersChart } from '../../../../services/chart'; export const meta = { stability: 'stable', @@ -29,9 +29,9 @@ export const meta = { }, }, - res: convertLog(usersLogSchema), + res: convertLog(usersChart.schema), }; export default define(meta, async (ps) => { - return await usersChart.getChart(ps.span as any, ps.limit); + return await usersChart.getChart(ps.span as any, ps.limit!); }); diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts index 138adffad2..adf780301b 100644 --- a/src/server/api/endpoints/drive.ts +++ b/src/server/api/endpoints/drive.ts @@ -1,6 +1,6 @@ -import DriveFile from '../../../models/drive-file'; import define from '../define'; import fetchMeta from '../../../misc/fetch-meta'; +import { DriveFiles } from '../../../models'; export const meta = { desc: { @@ -12,7 +12,7 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', res: { type: 'object', @@ -31,27 +31,7 @@ export default define(meta, async (ps, user) => { const instance = await fetchMeta(); // Calculate drive usage - const usage = await DriveFile.aggregate([{ - $match: { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); + const usage = await DriveFiles.clacDriveUsageOf(user); return { capacity: 1024 * 1024 * instance.localDriveCapacityMb, diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index f108e820e7..4e4db6c780 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import DriveFile, { packMany } from '../../../../models/drive-file'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { DriveFiles } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -13,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { limit: { @@ -23,18 +24,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, folderId: { validator: $.optional.nullable.type(ID), default: null as any, - transform: transform, }, type: { @@ -51,36 +49,24 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const sort = { - _id: -1 - }; + const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: user.id }); - const query = { - 'metadata.userId': user._id, - 'metadata.folderId': ps.folderId, - 'metadata.deletedAt': { $exists: false } - } as any; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; + if (ps.folderId) { + query.andWhere('file.folderId = :folderId', { folderId: ps.folderId }); + } else { + query.andWhere('file.folderId IS NULL'); } if (ps.type) { - query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`); + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } } - const files = await DriveFile - .find(query, { - limit: ps.limit, - sort: sort - }); + const files = await query.take(ps.limit!).getMany(); - return await packMany(files, { detail: false, self: true }); + return await DriveFiles.packMany(files, { detail: false, self: true }); }); diff --git a/src/server/api/endpoints/drive/files/attached-notes.ts b/src/server/api/endpoints/drive/files/attached-notes.ts index c9eeab58c5..7214463dde 100644 --- a/src/server/api/endpoints/drive/files/attached-notes.ts +++ b/src/server/api/endpoints/drive/files/attached-notes.ts @@ -1,9 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import { packMany } from '../../../../../models/note'; import { ApiError } from '../../../error'; +import { DriveFiles } from '../../../../../models'; export const meta = { stability: 'stable', @@ -17,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { fileId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のファイルID', 'en-US': 'Target file ID' @@ -48,18 +46,17 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch file - const file = await DriveFile - .findOne({ - _id: ps.fileId, - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - }); + const file = await DriveFiles.findOne({ + id: ps.fileId, + userId: user.id, + }); - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } + /* v11 TODO return await packMany(file.metadata.attachedNoteIds || [], user, { detail: true - }); + });*/ }); diff --git a/src/server/api/endpoints/drive/files/check-existence.ts b/src/server/api/endpoints/drive/files/check-existence.ts index 926411c83a..3a87a9497f 100644 --- a/src/server/api/endpoints/drive/files/check-existence.ts +++ b/src/server/api/endpoints/drive/files/check-existence.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; -import DriveFile, { pack } from '../../../../../models/drive-file'; import define from '../../../define'; +import { DriveFiles } from '../../../../../models'; export const meta = { desc: { @@ -12,7 +12,7 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { md5: { @@ -29,11 +29,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const file = await DriveFile.findOne({ + const file = await DriveFiles.findOne({ md5: ps.md5, - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } + userId: user.id, }); - return { file: file ? await pack(file, { self: true }) : null }; + return { + file: file ? await DriveFiles.pack(file, { self: true }) : null + }; }); diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index b2979c4888..340a39a41c 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -1,11 +1,11 @@ import * as ms from 'ms'; import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import { validateFileName, pack } from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import create from '../../../../../services/drive/add-file'; import define from '../../../define'; import { apiLogger } from '../../../logger'; import { ApiError } from '../../../error'; +import { DriveFiles } from '../../../../../models'; export const meta = { desc: { @@ -24,12 +24,11 @@ export const meta = { requireFile: true, - kind: 'drive-write', + kind: 'write:drive', params: { folderId: { validator: $.optional.nullable.type(ID), - transform: transform, default: null as any, desc: { 'ja-JP': 'フォルダID' @@ -78,7 +77,7 @@ export default define(meta, async (ps, user, app, file, cleanup) => { name = null; } else if (name === 'blob') { name = null; - } else if (!validateFileName(name)) { + } else if (!DriveFiles.validateFileName(name)) { throw new ApiError(meta.errors.invalidFileName); } } else { @@ -88,11 +87,11 @@ export default define(meta, async (ps, user, app, file, cleanup) => { try { // Create file const driveFile = await create(user, file.path, name, null, ps.folderId, ps.force, false, null, null, ps.isSensitive); - return pack(driveFile, { self: true }); + return DriveFiles.pack(driveFile, { self: true }); } catch (e) { apiLogger.error(e); throw new ApiError(); } finally { - cleanup(); + cleanup!(); } }); diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts index dd4e187fcd..d8cc5ec0a1 100644 --- a/src/server/api/endpoints/drive/files/delete.ts +++ b/src/server/api/endpoints/drive/files/delete.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import del from '../../../../../services/drive/delete-file'; import { publishDriveStream } from '../../../../../services/stream'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { DriveFiles } from '../../../../../models'; export const meta = { stability: 'stable', @@ -18,12 +18,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { fileId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のファイルID', 'en-US': 'Target file ID' @@ -47,17 +46,13 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // Fetch file - const file = await DriveFile - .findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } - if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } @@ -65,7 +60,5 @@ export default define(meta, async (ps, user) => { await del(file); // Publish fileDeleted event - publishDriveStream(user._id, 'fileDeleted', file._id); - - return; + publishDriveStream(user.id, 'fileDeleted', file.id); }); diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts index 0d4102a48f..265850f84c 100644 --- a/src/server/api/endpoints/drive/files/find.ts +++ b/src/server/api/endpoints/drive/files/find.ts @@ -1,14 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile, { pack } from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; +import { DriveFiles } from '../../../../../models'; export const meta = { requireCredential: true, tags: ['drive'], - kind: 'drive-read', + kind: 'read:drive', params: { name: { @@ -17,7 +17,6 @@ export const meta = { folderId: { validator: $.optional.nullable.type(ID), - transform: transform, default: null as any, desc: { 'ja-JP': 'フォルダID' @@ -27,12 +26,11 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const files = await DriveFile - .find({ - filename: ps.name, - 'metadata.userId': user._id, - 'metadata.folderId': ps.folderId - }); + const files = await DriveFiles.find({ + name: ps.name, + userId: user.id, + folderId: ps.folderId + }); - return await Promise.all(files.map(file => pack(file, { self: true }))); + return await Promise.all(files.map(file => DriveFiles.pack(file, { self: true }))); }); diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts index 6d63a8605c..e8c0e683c9 100644 --- a/src/server/api/endpoints/drive/files/show.ts +++ b/src/server/api/endpoints/drive/files/show.ts @@ -1,10 +1,9 @@ import $ from 'cafy'; -import * as mongo from 'mongodb'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFile, { pack, IDriveFile } from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; -import config from '../../../../../config'; import { ApiError } from '../../../error'; +import { DriveFile } from '../../../../../models/entities/drive-file'; +import { DriveFiles } from '../../../../../models'; export const meta = { stability: 'stable', @@ -18,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { fileId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '対象のファイルID', 'en-US': 'Target file ID' @@ -65,49 +63,33 @@ export const meta = { }; export default define(meta, async (ps, user) => { - let file: IDriveFile; + let file: DriveFile | undefined; if (ps.fileId) { - file = await DriveFile.findOne({ - _id: ps.fileId, - 'metadata.deletedAt': { $exists: false } - }); + file = await DriveFiles.findOne(ps.fileId); } else if (ps.url) { - const isInternalStorageUrl = ps.url.startsWith(config.driveUrl); - if (isInternalStorageUrl) { - // Extract file ID from url - // e.g. - // http://misskey.local/files/foo?original=bar --> foo - const fileId = new mongo.ObjectID(ps.url.replace(config.driveUrl, '').replace(/\?(.*)$/, '').replace(/\//g, '')); - file = await DriveFile.findOne({ - _id: fileId, - 'metadata.deletedAt': { $exists: false } - }); - } else { - file = await DriveFile.findOne({ - $or: [{ - 'metadata.url': ps.url - }, { - 'metadata.webpublicUrl': ps.url - }, { - 'metadata.thumbnailUrl': ps.url - }], - 'metadata.deletedAt': { $exists: false } - }); - } + file = await DriveFiles.findOne({ + where: [{ + url: ps.url + }, { + webpublicUrl: ps.url + }, { + thumbnailUrl: ps.url + }], + }); } else { throw new ApiError(meta.errors.fileIdOrUrlRequired); } - if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { - throw new ApiError(meta.errors.accessDenied); + if (file == null) { + throw new ApiError(meta.errors.noSuchFile); } - if (file === null) { - throw new ApiError(meta.errors.noSuchFile); + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { + throw new ApiError(meta.errors.accessDenied); } - return await pack(file, { + return await DriveFiles.pack(file, { detail: true, self: true }); diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index c8803bec3a..81e86a2734 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -1,11 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder from '../../../../../models/drive-folder'; -import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file'; +import { ID } from '../../../../../misc/cafy-id'; import { publishDriveStream } from '../../../../../services/stream'; import define from '../../../define'; -import Note from '../../../../../models/note'; import { ApiError } from '../../../error'; +import { DriveFiles, DriveFolders } from '../../../../../models'; export const meta = { desc: { @@ -17,12 +15,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { fileId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のファイルID' } @@ -30,7 +27,6 @@ export const meta = { folderId: { validator: $.optional.nullable.type(ID), - transform: transform, default: undefined as any, desc: { 'ja-JP': 'フォルダID' @@ -38,7 +34,7 @@ export const meta = { }, name: { - validator: $.optional.str.pipe(validateFileName), + validator: $.optional.str.pipe(DriveFiles.validateFileName), default: undefined as any, desc: { 'ja-JP': 'ファイル名', @@ -78,69 +74,47 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // Fetch file - const file = await DriveFile - .findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } - if (!user.isAdmin && !user.isModerator && !file.metadata.userId.equals(user._id)) { + if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } - if (ps.name) file.filename = ps.name; + if (ps.name) file.name = ps.name; - if (ps.isSensitive !== undefined) file.metadata.isSensitive = ps.isSensitive; + if (ps.isSensitive !== undefined) file.isSensitive = ps.isSensitive; if (ps.folderId !== undefined) { if (ps.folderId === null) { - file.metadata.folderId = null; + file.folderId = null; } else { - // Fetch folder - const folder = await DriveFolder - .findOne({ - _id: ps.folderId, - userId: user._id - }); + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); - if (folder === null) { + if (folder == null) { throw new ApiError(meta.errors.noSuchFolder); } - file.metadata.folderId = folder._id; + file.folderId = folder.id; } } - await DriveFile.update(file._id, { - $set: { - filename: file.filename, - 'metadata.folderId': file.metadata.folderId, - 'metadata.isSensitive': file.metadata.isSensitive - } - }); - - // ドライブのファイルが非正規化されているドキュメントも更新 - Note.find({ - '_files._id': file._id - }).then(notes => { - for (const note of notes) { - note._files[note._files.findIndex(f => f._id.equals(file._id))] = file; - Note.update({ _id: note._id }, { - $set: { - _files: note._files - } - }); - } + await DriveFiles.update(file.id, { + name: file.name, + folderId: file.folderId, + isSensitive: file.isSensitive }); - const fileObj = await pack(file, { self: true }); + const fileObj = await DriveFiles.pack(file, { self: true }); // Publish fileUpdated event - publishDriveStream(user._id, 'fileUpdated', fileObj); + publishDriveStream(user.id, 'fileUpdated', fileObj); return fileObj; }); diff --git a/src/server/api/endpoints/drive/files/upload-from-url.ts b/src/server/api/endpoints/drive/files/upload-from-url.ts index 93a9fa62fa..034ab10f19 100644 --- a/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../../models/drive-file'; import uploadFromUrl from '../../../../../services/drive/upload-from-url'; import define from '../../../define'; +import { DriveFiles } from '../../../../../models'; export const meta = { desc: { @@ -19,7 +19,7 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { url: { @@ -30,7 +30,6 @@ export const meta = { folderId: { validator: $.optional.nullable.type(ID), default: null as any, - transform: transform }, isSensitive: { @@ -53,5 +52,5 @@ export const meta = { }; export default define(meta, async (ps, user) => { - return await pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true }); + return await DriveFiles.pack(await uploadFromUrl(ps.url, user, ps.folderId, null, ps.isSensitive, ps.force), { self: true }); }); diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts index 73c179f7be..08ae2ff709 100644 --- a/src/server/api/endpoints/drive/folders.ts +++ b/src/server/api/endpoints/drive/folders.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import DriveFolder, { pack } from '../../../../models/drive-folder'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { DriveFolders } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -13,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { limit: { @@ -23,18 +24,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, folderId: { validator: $.optional.nullable.type(ID), default: null as any, - transform: transform, } }, @@ -47,29 +45,16 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const sort = { - _id: -1 - }; - const query = { - userId: user._id, - parentId: ps.folderId - } as any; - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; + const query = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId) + .andWhere('folder.userId = :userId', { userId: user.id }); + + if (ps.folderId) { + query.andWhere('folder.parentId = :parentId', { parentId: ps.folderId }); + } else { + query.andWhere('folder.parentId IS NULL'); } - const folders = await DriveFolder - .find(query, { - limit: ps.limit, - sort: sort - }); + const folders = await query.take(ps.limit!).getMany(); - return await Promise.all(folders.map(folder => pack(folder))); + return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); }); diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts index 5fab0b91a1..5530abf9dc 100644 --- a/src/server/api/endpoints/drive/folders/create.ts +++ b/src/server/api/endpoints/drive/folders/create.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import { publishDriveStream } from '../../../../../services/stream'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { DriveFolders } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { stability: 'stable', @@ -17,11 +18,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { name: { - validator: $.optional.str.pipe(isValidFolderName), + validator: $.optional.str.pipe(DriveFolders.validateFolderName), default: 'Untitled', desc: { 'ja-JP': 'フォルダ名', @@ -31,7 +32,6 @@ export const meta = { parentId: { validator: $.optional.nullable.type(ID), - transform: transform, desc: { 'ja-JP': '親フォルダID', 'en-US': 'Parent folder ID' @@ -53,29 +53,29 @@ export default define(meta, async (ps, user) => { let parent = null; if (ps.parentId) { // Fetch parent folder - parent = await DriveFolder - .findOne({ - _id: ps.parentId, - userId: user._id - }); + parent = await DriveFolders.findOne({ + id: ps.parentId, + userId: user.id + }); - if (parent === null) { + if (parent == null) { throw new ApiError(meta.errors.noSuchFolder); } } // Create folder - const folder = await DriveFolder.insert({ + const folder = await DriveFolders.save({ + id: genId(), createdAt: new Date(), name: ps.name, - parentId: parent !== null ? parent._id : null, - userId: user._id + parentId: parent !== null ? parent.id : null, + userId: user.id }); - const folderObj = await pack(folder); + const folderObj = await DriveFolders.pack(folder); // Publish folderCreated event - publishDriveStream(user._id, 'folderCreated', folderObj); + publishDriveStream(user.id, 'folderCreated', folderObj); return folderObj; }); diff --git a/src/server/api/endpoints/drive/folders/delete.ts b/src/server/api/endpoints/drive/folders/delete.ts index 9f22bf9ea7..fe6c05ad07 100644 --- a/src/server/api/endpoints/drive/folders/delete.ts +++ b/src/server/api/endpoints/drive/folders/delete.ts @@ -1,10 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { publishDriveStream } from '../../../../../services/stream'; -import DriveFile from '../../../../../models/drive-file'; import { ApiError } from '../../../error'; +import { DriveFolders, DriveFiles } from '../../../../../models'; export const meta = { stability: 'stable', @@ -18,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { folderId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のフォルダID', 'en-US': 'Target folder ID' @@ -48,29 +46,26 @@ export const meta = { export default define(meta, async (ps, user) => { // Get folder - const folder = await DriveFolder - .findOne({ - _id: ps.folderId, - userId: user._id - }); + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); - if (folder === null) { + if (folder == null) { throw new ApiError(meta.errors.noSuchFolder); } const [childFoldersCount, childFilesCount] = await Promise.all([ - DriveFolder.count({ parentId: folder._id }), - DriveFile.count({ 'metadata.folderId': folder._id }) + DriveFolders.count({ parentId: folder.id }), + DriveFiles.count({ folderId: folder.id }) ]); if (childFoldersCount !== 0 || childFilesCount !== 0) { throw new ApiError(meta.errors.hasChildFilesOrFolders); } - await DriveFolder.remove({ _id: folder._id }); + await DriveFolders.delete(folder.id); // Publish folderCreated event - publishDriveStream(user._id, 'folderDeleted', folder._id); - - return; + publishDriveStream(user.id, 'folderDeleted', folder.id); }); diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts index 16b6c10633..f0989ec5ae 100644 --- a/src/server/api/endpoints/drive/folders/find.ts +++ b/src/server/api/endpoints/drive/folders/find.ts @@ -1,14 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder, { pack } from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; +import { DriveFolders } from '../../../../../models'; export const meta = { tags: ['drive'], requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { name: { @@ -17,7 +17,6 @@ export const meta = { parentId: { validator: $.optional.nullable.type(ID), - transform: transform, default: null as any, desc: { 'ja-JP': 'フォルダID' @@ -34,12 +33,11 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const folders = await DriveFolder - .find({ - name: ps.name, - userId: user._id, - parentId: ps.parentId - }); + const folders = await DriveFolders.find({ + name: ps.name, + userId: user.id, + parentId: ps.parentId + }); - return await Promise.all(folders.map(folder => pack(folder))); + return await Promise.all(folders.map(folder => DriveFolders.pack(folder))); }); diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts index bbfcbed51f..60507e7d7f 100644 --- a/src/server/api/endpoints/drive/folders/show.ts +++ b/src/server/api/endpoints/drive/folders/show.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder, { pack } from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { DriveFolders } from '../../../../../models'; export const meta = { stability: 'stable', @@ -16,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { folderId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のフォルダID', 'en-US': 'Target folder ID' @@ -44,17 +43,16 @@ export const meta = { export default define(meta, async (ps, user) => { // Get folder - const folder = await DriveFolder - .findOne({ - _id: ps.folderId, - userId: user._id - }); + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); - if (folder === null) { + if (folder == null) { throw new ApiError(meta.errors.noSuchFolder); } - return await pack(folder, { + return await DriveFolders.pack(folder, { detail: true }); }); diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts index a1ee2669f0..7d3ece00a3 100644 --- a/src/server/api/endpoints/drive/folders/update.ts +++ b/src/server/api/endpoints/drive/folders/update.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; +import { ID } from '../../../../../misc/cafy-id'; import { publishDriveStream } from '../../../../../services/stream'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { DriveFolders } from '../../../../../models'; export const meta = { stability: 'stable', @@ -17,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'drive-write', + kind: 'write:drive', params: { folderId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のフォルダID', 'en-US': 'Target folder ID' @@ -30,7 +29,7 @@ export const meta = { }, name: { - validator: $.optional.str.pipe(isValidFolderName), + validator: $.optional.str.pipe(DriveFolders.validateFolderName), desc: { 'ja-JP': 'フォルダ名', 'en-US': 'Folder name' @@ -39,7 +38,6 @@ export const meta = { parentId: { validator: $.optional.nullable.type(ID), - transform: transform, desc: { 'ja-JP': '親フォルダID', 'en-US': 'Parent folder ID' @@ -70,49 +68,44 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch folder - const folder = await DriveFolder - .findOne({ - _id: ps.folderId, - userId: user._id - }); + const folder = await DriveFolders.findOne({ + id: ps.folderId, + userId: user.id + }); - if (folder === null) { + if (folder == null) { throw new ApiError(meta.errors.noSuchFolder); } if (ps.name) folder.name = ps.name; if (ps.parentId !== undefined) { - if (ps.parentId.equals(folder._id)) { + if (ps.parentId === folder.id) { throw new ApiError(meta.errors.recursiveNesting); } else if (ps.parentId === null) { folder.parentId = null; } else { // Get parent folder - const parent = await DriveFolder - .findOne({ - _id: ps.parentId, - userId: user._id - }); + const parent = await DriveFolders.findOne({ + id: ps.parentId, + userId: user.id + }); - if (parent === null) { + if (parent == null) { throw new ApiError(meta.errors.noSuchParentFolder); } // Check if the circular reference will occur async function checkCircle(folderId: any): Promise<boolean> { // Fetch folder - const folder2 = await DriveFolder.findOne({ - _id: folderId - }, { - _id: true, - parentId: true + const folder2 = await DriveFolders.findOne({ + id: folderId }); - if (folder2._id.equals(folder._id)) { + if (folder2!.id === folder!.id) { return true; - } else if (folder2.parentId) { - return await checkCircle(folder2.parentId); + } else if (folder2!.parentId) { + return await checkCircle(folder2!.parentId); } else { return false; } @@ -124,22 +117,20 @@ export default define(meta, async (ps, user) => { } } - folder.parentId = parent._id; + folder.parentId = parent.id; } } // Update - DriveFolder.update(folder._id, { - $set: { - name: folder.name, - parentId: folder.parentId - } + DriveFolders.update(folder.id, { + name: folder.name, + parentId: folder.parentId }); - const folderObj = await pack(folder); + const folderObj = await DriveFolders.pack(folder); // Publish folderUpdated event - publishDriveStream(user._id, 'folderUpdated', folderObj); + publishDriveStream(user.id, 'folderUpdated', folderObj); return folderObj; }); diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts index 916482be4d..96d9f82421 100644 --- a/src/server/api/endpoints/drive/stream.ts +++ b/src/server/api/endpoints/drive/stream.ts @@ -1,14 +1,15 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import DriveFile, { packMany } from '../../../../models/drive-file'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { DriveFiles } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { tags: ['drive'], requireCredential: true, - kind: 'drive-read', + kind: 'read:drive', params: { limit: { @@ -18,12 +19,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, type: { @@ -40,35 +39,18 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const sort = { - _id: -1 - }; - - const query = { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } as any; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: user.id }); if (ps.type) { - query.contentType = new RegExp(`^${ps.type.replace(/\*/g, '.+?')}$`); + if (ps.type.endsWith('/*')) { + query.andWhere('file.type like :type', { type: ps.type.replace('/*', '/') + '%' }); + } else { + query.andWhere('file.type = :type', { type: ps.type }); + } } - const files = await DriveFile - .find(query, { - limit: ps.limit, - sort: sort - }); + const files = await query.take(ps.limit!).getMany(); - return await packMany(files, { self: true }); + return await DriveFiles.packMany(files, { detail: false, self: true }); }); diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts index f81f81822e..301338ed96 100644 --- a/src/server/api/endpoints/federation/instances.ts +++ b/src/server/api/endpoints/federation/instances.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import Instance from '../../../../models/instance'; +import { Instances } from '../../../../models'; +import fetchMeta from '../../../../misc/fetch-meta'; export const meta = { tags: ['federation'], @@ -37,92 +38,55 @@ export const meta = { }; export default define(meta, async (ps, me) => { - let sort; + const query = Instances.createQueryBuilder('instance'); - if (ps.sort) { - if (ps.sort == '+notes') { - sort = { - notesCount: -1 - }; - } else if (ps.sort == '-notes') { - sort = { - notesCount: 1 - }; - } else if (ps.sort == '+users') { - sort = { - usersCount: -1 - }; - } else if (ps.sort == '-users') { - sort = { - usersCount: 1 - }; - } else if (ps.sort == '+following') { - sort = { - followingCount: -1 - }; - } else if (ps.sort == '-following') { - sort = { - followingCount: 1 - }; - } else if (ps.sort == '+followers') { - sort = { - followersCount: -1 - }; - } else if (ps.sort == '-followers') { - sort = { - followersCount: 1 - }; - } else if (ps.sort == '+caughtAt') { - sort = { - caughtAt: -1 - }; - } else if (ps.sort == '-caughtAt') { - sort = { - caughtAt: 1 - }; - } else if (ps.sort == '+lastCommunicatedAt') { - sort = { - lastCommunicatedAt: -1 - }; - } else if (ps.sort == '-lastCommunicatedAt') { - sort = { - lastCommunicatedAt: 1 - }; - } else if (ps.sort == '+driveUsage') { - sort = { - driveUsage: -1 - }; - } else if (ps.sort == '-driveUsage') { - sort = { - driveUsage: 1 - }; - } else if (ps.sort == '+driveFiles') { - sort = { - driveFiles: -1 - }; - } else if (ps.sort == '-driveFiles') { - sort = { - driveFiles: 1 - }; + switch (ps.sort) { + case '+notes': query.orderBy('instance.notesCount', 'DESC'); break; + case '-notes': query.orderBy('instance.notesCount', 'ASC'); break; + case '+usersCount': query.orderBy('instance.usersCount', 'DESC'); break; + case '-usersCount': query.orderBy('instance.usersCount', 'ASC'); break; + case '+followingCount': query.orderBy('instance.followingCount', 'DESC'); break; + case '-followingCount': query.orderBy('instance.followingCount', 'ASC'); break; + case '+followersCount': query.orderBy('instance.followersCount', 'DESC'); break; + case '-followersCount': query.orderBy('instance.followersCount', 'ASC'); break; + case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break; + case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break; + case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break; + case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break; + case '+driveUsage': query.orderBy('instance.driveUsage', 'DESC'); break; + case '-driveUsage': query.orderBy('instance.driveUsage', 'ASC'); break; + case '+driveFiles': query.orderBy('instance.driveFiles', 'DESC'); break; + case '-driveFiles': query.orderBy('instance.driveFiles', 'ASC'); break; + + default: query.orderBy('instance.id', 'DESC'); break; + } + + if (typeof ps.blocked === 'boolean') { + const meta = await fetchMeta(); + if (ps.blocked) { + query.andWhere('instance.host IN (:...blocks)', { blocks: meta.blockedHosts }); + } else { + query.andWhere('instance.host NOT IN (:...blocks)', { blocks: meta.blockedHosts }); } - } else { - sort = { - _id: -1 - }; } - const q = {} as any; + if (typeof ps.notResponding === 'boolean') { + if (ps.notResponding) { + query.andWhere('instance.isNotResponding = TRUE'); + } else { + query.andWhere('instance.isNotResponding = FALSE'); + } + } - if (typeof ps.blocked === 'boolean') q.isBlocked = ps.blocked; - if (typeof ps.notResponding === 'boolean') q.isNotResponding = ps.notResponding; - if (typeof ps.markedAsClosed === 'boolean') q.isMarkedAsClosed = ps.markedAsClosed; + if (typeof ps.markedAsClosed === 'boolean') { + if (ps.markedAsClosed) { + query.andWhere('instance.isMarkedAsClosed = TRUE'); + } else { + query.andWhere('instance.isMarkedAsClosed = FALSE'); + } + } - const instances = await Instance - .find(q, { - limit: ps.limit, - sort: sort, - skip: ps.offset - }); + const instances = await query.take(ps.limit!).skip(ps.offset).getMany(); return instances; }); diff --git a/src/server/api/endpoints/federation/show-instance.ts b/src/server/api/endpoints/federation/show-instance.ts index e7f68620af..9afcf28a7c 100644 --- a/src/server/api/endpoints/federation/show-instance.ts +++ b/src/server/api/endpoints/federation/show-instance.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import define from '../../define'; -import Instance from '../../../../models/instance'; +import { Instances } from '../../../../models'; +import { toPuny } from '../../../../misc/convert-host'; export const meta = { tags: ['federation'], @@ -15,8 +16,8 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const instance = await Instance - .findOne({ host: ps.host }); + const instance = await Instances + .findOne({ host: toPuny(ps.host) }); return instance; }); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index 81b2399551..5b43815a5e 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../models/user'; -import Following from '../../../../models/following'; import create from '../../../../services/following/create'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Followings, Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -25,12 +24,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -75,7 +73,7 @@ export default define(meta, async (ps, user) => { const follower = user; // 自分自身 - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.followeeIsYourself); } @@ -86,12 +84,12 @@ export default define(meta, async (ps, user) => { }); // Check if already following - const exist = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id + const exist = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); - if (exist !== null) { + if (exist != null) { throw new ApiError(meta.errors.alreadyFollowing); } @@ -103,5 +101,5 @@ export default define(meta, async (ps, user) => { throw e; } - return await pack(followee._id, user); + return await Users.pack(followee.id, user); }); diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts index 8f8249b1e8..240a037c9e 100644 --- a/src/server/api/endpoints/following/delete.ts +++ b/src/server/api/endpoints/following/delete.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import * as ms from 'ms'; -import { pack } from '../../../../models/user'; -import Following from '../../../../models/following'; import deleteFollowing from '../../../../services/following/delete'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Followings, Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -25,12 +24,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -63,7 +61,7 @@ export default define(meta, async (ps, user) => { const follower = user; // Check if the followee is yourself - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.followeeIsYourself); } @@ -74,16 +72,16 @@ export default define(meta, async (ps, user) => { }); // Check not following - const exist = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id + const exist = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); - if (exist === null) { + if (exist == null) { throw new ApiError(meta.errors.notFollowing); } await deleteFollowing(follower, followee); - return await pack(followee._id, user); + return await Users.pack(followee.id, user); }); diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts index 0975990c02..65c24f7be9 100644 --- a/src/server/api/endpoints/following/requests/accept.ts +++ b/src/server/api/endpoints/following/requests/accept.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import acceptFollowRequest from '../../../../../services/following/requests/accept'; import define from '../../../define'; import { ApiError } from '../../../error'; @@ -15,12 +15,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts index 371f9f0ed3..79cdb776f2 100644 --- a/src/server/api/endpoints/following/requests/cancel.ts +++ b/src/server/api/endpoints/following/requests/cancel.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import cancelFollowRequest from '../../../../../services/following/requests/cancel'; -import { pack } from '../../../../../models/user'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; +import { Users } from '../../../../../models'; export const meta = { desc: { @@ -16,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -58,5 +57,5 @@ export default define(meta, async (ps, user) => { throw e; } - return await pack(followee._id, user); + return await Users.pack(followee.id, user); }); diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts index c9bcedf929..13e4a39388 100644 --- a/src/server/api/endpoints/following/requests/list.ts +++ b/src/server/api/endpoints/following/requests/list.ts @@ -1,5 +1,5 @@ -import FollowRequest, { pack } from '../../../../../models/follow-request'; import define from '../../../define'; +import { FollowRequests } from '../../../../../models'; export const meta = { desc: { @@ -11,13 +11,13 @@ export const meta = { requireCredential: true, - kind: 'following-read' + kind: 'read:following' }; export default define(meta, async (ps, user) => { - const reqs = await FollowRequest.find({ - followeeId: user._id + const reqs = await FollowRequests.find({ + followeeId: user.id }); - return await Promise.all(reqs.map(req => pack(req))); + return await Promise.all(reqs.map(req => FollowRequests.pack(req))); }); diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts index 5e59d4bc97..cccb60b243 100644 --- a/src/server/api/endpoints/following/requests/reject.ts +++ b/src/server/api/endpoints/following/requests/reject.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import rejectFollowRequest from '../../../../../services/following/requests/reject'; import define from '../../../define'; import { ApiError } from '../../../error'; @@ -15,12 +15,11 @@ export const meta = { requireCredential: true, - kind: 'following-write', + kind: 'write:following', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' diff --git a/src/server/api/endpoints/games/reversi/games.ts b/src/server/api/endpoints/games/reversi/games.ts index e3c22c7611..7267157e0e 100644 --- a/src/server/api/endpoints/games/reversi/games.ts +++ b/src/server/api/endpoints/games/reversi/games.ts @@ -1,7 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import ReversiGame, { pack } from '../../../../../models/games/reversi/game'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; +import { ReversiGames } from '../../../../../models'; +import { makePaginationQuery } from '../../../common/make-pagination-query'; +import { Brackets } from 'typeorm'; export const meta = { tags: ['games'], @@ -14,12 +16,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, my: { @@ -30,39 +30,20 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const q: any = ps.my ? { - isStarted: true, - $or: [{ - user1Id: user._id - }, { - user2Id: user._id - }] - } : { - isStarted: true - }; + const query = makePaginationQuery(ReversiGames.createQueryBuilder('game'), ps.sinceId, ps.untilId) + .andWhere('game.isStarted = TRUE'); - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - q._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - q._id = { - $lt: ps.untilId - }; + if (ps.my) { + query.andWhere(new Brackets(qb => { qb + .where('game.user1Id = :userId', { userId: user.id }) + .orWhere('game.user2Id = :userId', { userId: user.id }); + })); } // Fetch games - const games = await ReversiGame.find(q, { - sort: sort, - limit: ps.limit - }); + const games = await query.take(ps.limit!).getMany(); - return await Promise.all(games.map((g) => pack(g, user, { + return await Promise.all(games.map((g) => ReversiGames.pack(g, user, { detail: false }))); }); diff --git a/src/server/api/endpoints/games/reversi/games/show.ts b/src/server/api/endpoints/games/reversi/games/show.ts index 766ca90119..ea2776b16f 100644 --- a/src/server/api/endpoints/games/reversi/games/show.ts +++ b/src/server/api/endpoints/games/reversi/games/show.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../../misc/cafy-id'; -import ReversiGame, { pack } from '../../../../../../models/games/reversi/game'; +import { ID } from '../../../../../../misc/cafy-id'; import Reversi from '../../../../../../games/reversi/core'; import define from '../../../../define'; import { ApiError } from '../../../../error'; +import { ReversiGames } from '../../../../../../models'; export const meta = { tags: ['games'], @@ -11,7 +11,6 @@ export const meta = { params: { gameId: { validator: $.type(ID), - transform: transform, }, }, @@ -25,22 +24,23 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const game = await ReversiGame.findOne({ _id: ps.gameId }); + const game = await ReversiGames.findOne(ps.gameId); if (game == null) { throw new ApiError(meta.errors.noSuchGame); } - const o = new Reversi(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard + const o = new Reversi(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard }); - for (const log of game.logs) + for (const log of game.logs) { o.put(log.color, log.pos); + } - const packed = await pack(game, user); + const packed = await ReversiGames.pack(game, user); return Object.assign({ board: o.board, diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts index 446210894d..56d66fb205 100644 --- a/src/server/api/endpoints/games/reversi/games/surrender.ts +++ b/src/server/api/endpoints/games/reversi/games/surrender.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../../misc/cafy-id'; -import ReversiGame, { pack } from '../../../../../../models/games/reversi/game'; +import { ID } from '../../../../../../misc/cafy-id'; import { publishReversiGameStream } from '../../../../../../services/stream'; import define from '../../../../define'; import { ApiError } from '../../../../error'; +import { ReversiGames } from '../../../../../../models'; export const meta = { tags: ['games'], @@ -17,7 +17,6 @@ export const meta = { params: { gameId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '投了したい対局' } @@ -46,7 +45,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const game = await ReversiGame.findOne({ _id: ps.gameId }); + const game = await ReversiGames.findOne(ps.gameId); if (game == null) { throw new ApiError(meta.errors.noSuchGame); @@ -56,26 +55,20 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.alreadyEnded); } - if (!game.user1Id.equals(user._id) && !game.user2Id.equals(user._id)) { + if ((game.user1Id !== user.id) && (game.user2Id !== user.id)) { throw new ApiError(meta.errors.accessDenied); } - const winnerId = game.user1Id.equals(user._id) ? game.user2Id : game.user1Id; + const winnerId = game.user1Id === user.id ? game.user2Id : game.user1Id; - await ReversiGame.update({ - _id: game._id - }, { - $set: { - surrendered: user._id, - isEnded: true, - winnerId: winnerId - } + await ReversiGames.update(game.id, { + surrendered: user.id, + isEnded: true, + winnerId: winnerId }); - publishReversiGameStream(game._id, 'ended', { + publishReversiGameStream(game.id, 'ended', { winnerId: winnerId, - game: await pack(game._id, user) + game: await ReversiGames.pack(game.id, user) }); - - return; }); diff --git a/src/server/api/endpoints/games/reversi/invitations.ts b/src/server/api/endpoints/games/reversi/invitations.ts index c204770578..71f5aca1d1 100644 --- a/src/server/api/endpoints/games/reversi/invitations.ts +++ b/src/server/api/endpoints/games/reversi/invitations.ts @@ -1,5 +1,5 @@ -import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching'; import define from '../../../define'; +import { ReversiMatchings } from '../../../../../models'; export const meta = { tags: ['games'], @@ -9,13 +9,9 @@ export const meta = { export default define(meta, async (ps, user) => { // Find session - const invitations = await Matching.find({ - childId: user._id - }, { - sort: { - _id: -1 - } + const invitations = await ReversiMatchings.find({ + childId: user.id }); - return await Promise.all(invitations.map((i) => packMatching(i, user))); + return await Promise.all(invitations.map((i) => ReversiMatchings.pack(i, user))); }); diff --git a/src/server/api/endpoints/games/reversi/match.ts b/src/server/api/endpoints/games/reversi/match.ts index e66765944d..da367b5978 100644 --- a/src/server/api/endpoints/games/reversi/match.ts +++ b/src/server/api/endpoints/games/reversi/match.ts @@ -1,12 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Matching, { pack as packMatching } from '../../../../../models/games/reversi/matching'; -import ReversiGame, { pack as packGame } from '../../../../../models/games/reversi/game'; +import { ID } from '../../../../../misc/cafy-id'; import { publishMainStream, publishReversiStream } from '../../../../../services/stream'; import { eighteight } from '../../../../../games/reversi/maps'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; +import { genId } from '../../../../../misc/gen-id'; +import { ReversiMatchings, ReversiGames } from '../../../../../models'; +import { ReversiGame } from '../../../../../models/entities/games/reversi/game'; +import { ReversiMatching } from '../../../../../models/entities/games/reversi/matching'; export const meta = { tags: ['games'], @@ -16,7 +18,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -41,50 +42,47 @@ export const meta = { export default define(meta, async (ps, user) => { // Myself - if (ps.userId.equals(user._id)) { + if (ps.userId === user.id) { throw new ApiError(meta.errors.isYourself); } // Find session - const exist = await Matching.findOne({ + const exist = await ReversiMatchings.findOne({ parentId: ps.userId, - childId: user._id + childId: user.id }); if (exist) { // Destroy session - Matching.remove({ - _id: exist._id - }); + ReversiMatchings.delete(exist.id); // Create game - const game = await ReversiGame.insert({ + const game = await ReversiGames.save({ + id: genId(), createdAt: new Date(), user1Id: exist.parentId, - user2Id: user._id, + user2Id: user.id, user1Accepted: false, user2Accepted: false, isStarted: false, isEnded: false, logs: [], - settings: { - map: eighteight.data, - bw: 'random', - isLlotheo: false - } - }); + map: eighteight.data, + bw: 'random', + isLlotheo: false + } as Partial<ReversiGame>); - publishReversiStream(exist.parentId, 'matched', await packGame(game, exist.parentId)); + publishReversiStream(exist.parentId, 'matched', await ReversiGames.pack(game, exist.parentId)); - const other = await Matching.count({ - childId: user._id + const other = await ReversiMatchings.count({ + childId: user.id }); if (other == 0) { - publishMainStream(user._id, 'reversiNoInvites'); + publishMainStream(user.id, 'reversiNoInvites'); } - return await packGame(game, user); + return await ReversiGames.pack(game, user); } else { // Fetch child const child = await getUser(ps.userId).catch(e => { @@ -93,20 +91,21 @@ export default define(meta, async (ps, user) => { }); // 以前のセッションはすべて削除しておく - await Matching.remove({ - parentId: user._id + await ReversiMatchings.delete({ + parentId: user.id }); // セッションを作成 - const matching = await Matching.insert({ + const matching = await ReversiMatchings.save({ + id: genId(), createdAt: new Date(), - parentId: user._id, - childId: child._id - }); + parentId: user.id, + childId: child.id + } as ReversiMatching); - const packed = await packMatching(matching, child); - publishReversiStream(child._id, 'invited', packed); - publishMainStream(child._id, 'reversiInvited', packed); + const packed = await ReversiMatchings.pack(matching, child); + publishReversiStream(child.id, 'invited', packed); + publishMainStream(child.id, 'reversiInvited', packed); return; } diff --git a/src/server/api/endpoints/games/reversi/match/cancel.ts b/src/server/api/endpoints/games/reversi/match/cancel.ts index fb230032d8..71aaae5ee1 100644 --- a/src/server/api/endpoints/games/reversi/match/cancel.ts +++ b/src/server/api/endpoints/games/reversi/match/cancel.ts @@ -1,5 +1,5 @@ -import Matching from '../../../../../../models/games/reversi/matching'; import define from '../../../../define'; +import { ReversiMatchings } from '../../../../../../models'; export const meta = { tags: ['games'], @@ -8,9 +8,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { - await Matching.remove({ - parentId: user._id + await ReversiMatchings.delete({ + parentId: user.id }); - - return; }); diff --git a/src/server/api/endpoints/hashtags/list.ts b/src/server/api/endpoints/hashtags/list.ts index f454d47fed..2998bc1a13 100644 --- a/src/server/api/endpoints/hashtags/list.ts +++ b/src/server/api/endpoints/hashtags/list.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../define'; -import Hashtag from '../../../../models/hashtag'; +import { Hashtags } from '../../../../models'; export const meta = { tags: ['hashtags'], @@ -54,40 +54,39 @@ export const meta = { }, }; -const sort: any = { - '+mentionedUsers': { mentionedUsersCount: -1 }, - '-mentionedUsers': { mentionedUsersCount: 1 }, - '+mentionedLocalUsers': { mentionedLocalUsersCount: -1 }, - '-mentionedLocalUsers': { mentionedLocalUsersCount: 1 }, - '+mentionedRemoteUsers': { mentionedRemoteUsersCount: -1 }, - '-mentionedRemoteUsers': { mentionedRemoteUsersCount: 1 }, - '+attachedUsers': { attachedUsersCount: -1 }, - '-attachedUsers': { attachedUsersCount: 1 }, - '+attachedLocalUsers': { attachedLocalUsersCount: -1 }, - '-attachedLocalUsers': { attachedLocalUsersCount: 1 }, - '+attachedRemoteUsers': { attachedRemoteUsersCount: -1 }, - '-attachedRemoteUsers': { attachedRemoteUsersCount: 1 }, -}; - export default define(meta, async (ps, me) => { - const q = {} as any; - if (ps.attachedToUserOnly) q.attachedUsersCount = { $ne: 0 }; - if (ps.attachedToLocalUserOnly) q.attachedLocalUsersCount = { $ne: 0 }; - if (ps.attachedToRemoteUserOnly) q.attachedRemoteUsersCount = { $ne: 0 }; - const tags = await Hashtag - .find(q, { - limit: ps.limit, - sort: sort[ps.sort], - fields: { - tag: true, - mentionedUsersCount: true, - mentionedLocalUsersCount: true, - mentionedRemoteUsersCount: true, - attachedUsersCount: true, - attachedLocalUsersCount: true, - attachedRemoteUsersCount: true - } - }); + const query = Hashtags.createQueryBuilder('tag'); + + if (ps.attachedToUserOnly) query.andWhere('tag.attachedUsersCount != 0'); + if (ps.attachedToLocalUserOnly) query.andWhere('tag.attachedLocalUsersCount != 0'); + if (ps.attachedToRemoteUserOnly) query.andWhere('tag.attachedRemoteUsersCount != 0'); + + switch (ps.sort) { + case '+mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'DESC'); break; + case '-mentionedUsers': query.orderBy('tag.mentionedUsersCount', 'ASC'); break; + case '+mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'DESC'); break; + case '-mentionedLocalUsers': query.orderBy('tag.mentionedLocalUsersCount', 'ASC'); break; + case '+mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'DESC'); break; + case '-mentionedRemoteUsers': query.orderBy('tag.mentionedRemoteUsersCount', 'ASC'); break; + case '+attachedUsers': query.orderBy('tag.attachedUsersCount', 'DESC'); break; + case '-attachedUsers': query.orderBy('tag.attachedUsersCount', 'ASC'); break; + case '+attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'DESC'); break; + case '-attachedLocalUsers': query.orderBy('tag.attachedLocalUsersCount', 'ASC'); break; + case '+attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'DESC'); break; + case '-attachedRemoteUsers': query.orderBy('tag.attachedRemoteUsersCount', 'ASC'); break; + } + + query.select([ + 'tag.name', + 'tag.mentionedUsersCount', + 'tag.mentionedLocalUsersCount', + 'tag.mentionedRemoteUsersCount', + 'tag.attachedUsersCount', + 'tag.attachedLocalUsersCount', + 'tag.attachedRemoteUsersCount', + ]); + + const tags = await query.take(ps.limit!).getMany(); return tags; }); diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts index 19b2975adf..6a9a2df6ef 100644 --- a/src/server/api/endpoints/hashtags/search.ts +++ b/src/server/api/endpoints/hashtags/search.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import Hashtag from '../../../../models/hashtag'; import define from '../../define'; -import * as escapeRegexp from 'escape-regexp'; +import { Hashtags } from '../../../../models'; export const meta = { desc: { @@ -46,16 +45,12 @@ export const meta = { }; export default define(meta, async (ps) => { - const hashtags = await Hashtag - .find({ - tag: new RegExp('^' + escapeRegexp(ps.query.toLowerCase())) - }, { - sort: { - count: -1 - }, - limit: ps.limit, - skip: ps.offset - }); + const hashtags = await Hashtags.createQueryBuilder('tag') + .where('tag.name like :q', { q: ps.query.toLowerCase() + '%' }) + .orderBy('tag.count', 'DESC') + .take(ps.limit!) + .skip(ps.offset) + .getMany(); - return hashtags.map(tag => tag.tag); + return hashtags.map(tag => tag.name); }); diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index 8b8dd70245..e01e9d698f 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -1,17 +1,19 @@ -import Note from '../../../../models/note'; -import { erase } from '../../../../prelude/array'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; +import { Notes } from '../../../../models'; +import { Note } from '../../../../models/entities/note'; /* トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 ユニーク投稿数とはそのハッシュタグと投稿ユーザーのペアのカウントで、例えば同じユーザーが複数回同じハッシュタグを投稿してもそのハッシュタグのユニーク投稿数は1とカウントされる + +..が理想だけどPostgreSQLでどうするのか分からないので単に「直近Aの内に投稿されたユニーク投稿数が多いハッシュタグ」で妥協する */ const rangeA = 1000 * 60 * 30; // 30分 -const rangeB = 1000 * 60 * 120; // 2時間 -const coefficient = 1.25; // 「n倍」の部分 -const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか +//const rangeB = 1000 * 60 * 120; // 2時間 +//const coefficient = 1.25; // 「n倍」の部分 +//const requiredUsers = 3; // 最低何人がそのタグを投稿している必要があるか const max = 5; @@ -23,94 +25,49 @@ export const meta = { export default define(meta, async () => { const instance = await fetchMeta(); - const hidedTags = instance.hidedTags.map(t => t.toLowerCase()); + const hiddenTags = instance.hiddenTags.map(t => t.toLowerCase()); - //#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計 - const data = await Note.aggregate([{ - $match: { - createdAt: { - $gt: new Date(Date.now() - rangeA) - }, - tagsLower: { - $exists: true, - $ne: [] - } - } - }, { - $unwind: '$tagsLower' - }, { - $group: { - _id: { tag: '$tagsLower', userId: '$userId' } - } - }]) as { - _id: { - tag: string; - userId: any; - } - }[]; - //#endregion + const tagNotes = await Notes.createQueryBuilder('note') + .where(`note.createdAt > :date`, { date: new Date(Date.now() - rangeA) }) + .andWhere(`note.tags != '{}'`) + .select(['note.tags', 'note.userId']) + .getMany(); - if (data.length == 0) { + if (tagNotes.length === 0) { return []; } const tags: { name: string; - count: number; + users: Note['userId'][]; }[] = []; - // カウント - for (const x of data.map(x => x._id).filter(x => !hidedTags.includes(x.tag))) { - const i = tags.findIndex(tag => tag.name == x.tag); - if (i != -1) { - tags[i].count++; - } else { - tags.push({ - name: x.tag, - count: 1 - }); - } - } - - // 最低要求投稿者数を下回るならカットする - const limitedTags = tags.filter(tag => tag.count >= requiredUsers); + for (const note of tagNotes) { + for (const tag of note.tags) { + if (hiddenTags.includes(tag)) continue; - //#region 2. 1で取得したそれぞれのタグについて、「直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上」かどうかを判定する - const hotsPromises = limitedTags.map(async tag => { - const passedCount = (await Note.distinct('userId', { - tagsLower: tag.name, - createdAt: { - $lt: new Date(Date.now() - rangeA), - $gt: new Date(Date.now() - rangeB) + const x = tags.find(x => x.name === tag); + if (x) { + if (!x.users.includes(note.userId)) { + x.users.push(note.userId); + } + } else { + tags.push({ + name: tag, + users: [note.userId] + }); } - }) as any).length; - - if (tag.count >= (passedCount * coefficient)) { - return tag; - } else { - return null; } - }); - //#endregion + } // タグを人気順に並べ替え - let hots = erase(null, await Promise.all(hotsPromises)) - .sort((a, b) => b.count - a.count) + const hots = tags + .sort((a, b) => b.users.length - a.users.length) .map(tag => tag.name) .slice(0, max); - //#region 3. もし上記の方法でのトレンド抽出の結果、求められているタグ数に達しなければ「ただ単に現在投稿数が多いハッシュタグ」に切り替える - if (hots.length < max) { - hots = hots.concat(tags - .filter(tag => hots.indexOf(tag.name) == -1) - .sort((a, b) => b.count - a.count) - .map(tag => tag.name) - .slice(0, max - hots.length)); - } - //#endregion - //#region 2(または3)で話題と判定されたタグそれぞれについて過去の投稿数グラフを取得する - const countPromises: Promise<any[]>[] = []; + const countPromises: Promise<number[]>[] = []; const range = 20; @@ -118,29 +75,31 @@ export default define(meta, async () => { const interval = 1000 * 60 * 10; for (let i = 0; i < range; i++) { - countPromises.push(Promise.all(hots.map(tag => Note.distinct('userId', { - tagsLower: tag, - createdAt: { - $lt: new Date(Date.now() - (interval * i)), - $gt: new Date(Date.now() - (interval * (i + 1))) - } - })))); + countPromises.push(Promise.all(hots.map(tag => Notes.createQueryBuilder('note') + .select('count(distinct note.userId)') + .where(':tag = ANY(note.tags)', { tag: tag }) + .andWhere('note.createdAt < :lt', { lt: new Date(Date.now() - (interval * i)) }) + .andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * (i + 1))) }) + .getRawOne() + .then(x => parseInt(x.count, 10)) + ))); } const countsLog = await Promise.all(countPromises); - const totalCounts: any = await Promise.all(hots.map(tag => Note.distinct('userId', { - tagsLower: tag, - createdAt: { - $gt: new Date(Date.now() - (interval * range)) - } - }))); + const totalCounts = await Promise.all(hots.map(tag => Notes.createQueryBuilder('note') + .select('count(distinct note.userId)') + .where(':tag = ANY(note.tags)', { tag: tag }) + .andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * range)) }) + .getRawOne() + .then(x => parseInt(x.count, 10)) + )); //#endregion const stats = hots.map((tag, i) => ({ tag, - chart: countsLog.map(counts => counts[i].length), - usersCount: totalCounts[i].length + chart: countsLog.map(counts => counts[i]), + usersCount: totalCounts[i] })); return stats; diff --git a/src/server/api/endpoints/hashtags/users.ts b/src/server/api/endpoints/hashtags/users.ts index 4b047aee95..fa58f2f2c0 100644 --- a/src/server/api/endpoints/hashtags/users.ts +++ b/src/server/api/endpoints/hashtags/users.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; -import User, { pack } from '../../../../models/user'; import define from '../../define'; +import { Users } from '../../../../models'; export const meta = { requireCredential: false, @@ -54,39 +54,32 @@ export const meta = { }, }; -const sort: any = { - '+follower': { followersCount: -1 }, - '-follower': { followersCount: 1 }, - '+createdAt': { createdAt: -1 }, - '-createdAt': { createdAt: 1 }, - '+updatedAt': { updatedAt: -1 }, - '-updatedAt': { updatedAt: 1 }, -}; - export default define(meta, async (ps, me) => { - const q = { - tags: ps.tag, - $and: [] - } as any; + const query = Users.createQueryBuilder('user') + .where(':tag = ANY(user.tags)', { tag: ps.tag }); + + const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)); + + if (ps.state === 'alive') { + query.andWhere('user.updatedAt > :date', { date: recent }); + } - // state - q.$and.push( - ps.state == 'alive' ? { updatedAt: { $gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 5)) } } : - {} - ); + if (ps.origin === 'local') { + query.andWhere('user.host IS NULL'); + } else if (ps.origin === 'remote') { + query.andWhere('user.host IS NOT NULL'); + } - // origin - q.$and.push( - ps.origin == 'local' ? { host: null } : - ps.origin == 'remote' ? { host: { $ne: null } } : - {} - ); + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; + case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; + } - const users = await User - .find(q, { - limit: ps.limit, - sort: sort[ps.sort], - }); + const users = await query.take(ps.limit!).getMany(); - return await Promise.all(users.map(user => pack(user, me, { detail: true }))); + return await Users.packMany(users, me, { detail: true }); }); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index 7b50cc76c2..afad38c469 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -1,5 +1,5 @@ -import { pack } from '../../../models/user'; import define from '../define'; +import { Users } from '../../../models'; export const meta = { stability: 'stable', @@ -22,7 +22,7 @@ export const meta = { export default define(meta, async (ps, user, app) => { const isSecure = user != null && app == null; - return await pack(user, user, { + return await Users.pack(user, user, { detail: true, includeHasUnreadNotes: true, includeSecrets: isSecure diff --git a/src/server/api/endpoints/i/2fa/done.ts b/src/server/api/endpoints/i/2fa/done.ts index 556354c386..e23678dcbb 100644 --- a/src/server/api/endpoints/i/2fa/done.ts +++ b/src/server/api/endpoints/i/2fa/done.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; import * as speakeasy from 'speakeasy'; -import User from '../../../../../models/user'; import define from '../../../define'; +import { UserProfiles } from '../../../../../models'; +import { ensure } from '../../../../../prelude/ensure'; export const meta = { requireCredential: true, @@ -16,28 +17,26 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const _token = ps.token.replace(/\s/g, ''); + const token = ps.token.replace(/\s/g, ''); - if (user.twoFactorTempSecret == null) { + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + + if (profile.twoFactorTempSecret == null) { throw new Error('二段階認証の設定が開始されていません'); } const verified = (speakeasy as any).totp.verify({ - secret: user.twoFactorTempSecret, + secret: profile.twoFactorTempSecret, encoding: 'base32', - token: _token + token: token }); if (!verified) { throw new Error('not verified'); } - await User.update(user._id, { - $set: { - 'twoFactorSecret': user.twoFactorTempSecret, - 'twoFactorEnabled': true - } + await UserProfiles.update({ userId: user.id }, { + twoFactorSecret: profile.twoFactorTempSecret, + twoFactorEnabled: true }); - - return; }); diff --git a/src/server/api/endpoints/i/2fa/register.ts b/src/server/api/endpoints/i/2fa/register.ts index 302b51ec0b..76d79b3a49 100644 --- a/src/server/api/endpoints/i/2fa/register.ts +++ b/src/server/api/endpoints/i/2fa/register.ts @@ -2,9 +2,10 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; import * as QRCode from 'qrcode'; -import User from '../../../../../models/user'; import config from '../../../../../config'; import define from '../../../define'; +import { UserProfiles } from '../../../../../models'; +import { ensure } from '../../../../../prelude/ensure'; export const meta = { requireCredential: true, @@ -19,8 +20,10 @@ export const meta = { }; export default define(meta, async (ps, user) => { + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + // Compare password - const same = await bcrypt.compare(ps.password, user.password); + const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { throw new Error('incorrect password'); @@ -31,10 +34,8 @@ export default define(meta, async (ps, user) => { length: 32 }); - await User.update(user._id, { - $set: { - twoFactorTempSecret: secret.base32 - } + await UserProfiles.update({ userId: user.id }, { + twoFactorTempSecret: secret.base32 }); // Get the data URL of the authenticator URL diff --git a/src/server/api/endpoints/i/2fa/unregister.ts b/src/server/api/endpoints/i/2fa/unregister.ts index 37b2639198..9c7857e7ef 100644 --- a/src/server/api/endpoints/i/2fa/unregister.ts +++ b/src/server/api/endpoints/i/2fa/unregister.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../../../models/user'; import define from '../../../define'; +import { UserProfiles } from '../../../../../models'; +import { ensure } from '../../../../../prelude/ensure'; export const meta = { requireCredential: true, @@ -16,19 +17,17 @@ export const meta = { }; export default define(meta, async (ps, user) => { + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + // Compare password - const same = await bcrypt.compare(ps.password, user.password); + const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { throw new Error('incorrect password'); } - await User.update(user._id, { - $set: { - 'twoFactorSecret': null, - 'twoFactorEnabled': false - } + await UserProfiles.update({ userId: user.id }, { + twoFactorSecret: null, + twoFactorEnabled: false }); - - return; }); diff --git a/src/server/api/endpoints/i/authorized-apps.ts b/src/server/api/endpoints/i/authorized-apps.ts index cb8be9ed97..3e9fea19e2 100644 --- a/src/server/api/endpoints/i/authorized-apps.ts +++ b/src/server/api/endpoints/i/authorized-apps.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import AccessToken from '../../../../models/access-token'; -import { pack } from '../../../../models/app'; import define from '../../define'; +import { AccessTokens, Apps } from '../../../../models'; export const meta = { requireCredential: true, @@ -28,18 +27,18 @@ export const meta = { export default define(meta, async (ps, user) => { // Get tokens - const tokens = await AccessToken - .find({ - userId: user._id - }, { - limit: ps.limit, - skip: ps.offset, - sort: { - _id: ps.sort == 'asc' ? 1 : -1 - } - }); + const tokens = await AccessTokens.find({ + where: { + userId: user.id + }, + take: ps.limit!, + skip: ps.offset, + order: { + id: ps.sort == 'asc' ? 1 : -1 + } + }); - return await Promise.all(tokens.map(token => pack(token.appId, user, { + return await Promise.all(tokens.map(token => Apps.pack(token.appId, user, { detail: true }))); }); diff --git a/src/server/api/endpoints/i/change-password.ts b/src/server/api/endpoints/i/change-password.ts index 8ab286b4bf..0dda125b9c 100644 --- a/src/server/api/endpoints/i/change-password.ts +++ b/src/server/api/endpoints/i/change-password.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../../models/user'; import define from '../../define'; +import { UserProfiles } from '../../../../models'; +import { ensure } from '../../../../prelude/ensure'; export const meta = { requireCredential: true, @@ -20,8 +21,10 @@ export const meta = { }; export default define(meta, async (ps, user) => { + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + // Compare password - const same = await bcrypt.compare(ps.currentPassword, user.password); + const same = await bcrypt.compare(ps.currentPassword, profile.password!); if (!same) { throw new Error('incorrect password'); @@ -31,11 +34,7 @@ export default define(meta, async (ps, user) => { const salt = await bcrypt.genSalt(8); const hash = await bcrypt.hash(ps.newPassword, salt); - await User.update(user._id, { - $set: { - 'password': hash - } + await UserProfiles.update({ userId: user.id }, { + password: hash }); - - return; }); diff --git a/src/server/api/endpoints/i/clear-follow-request-notification.ts b/src/server/api/endpoints/i/clear-follow-request-notification.ts deleted file mode 100644 index 38c6ec1cef..0000000000 --- a/src/server/api/endpoints/i/clear-follow-request-notification.ts +++ /dev/null @@ -1,23 +0,0 @@ -import User from '../../../../models/user'; -import define from '../../define'; - -export const meta = { - tags: ['account', 'following'], - - requireCredential: true, - - kind: 'account-write', - - params: { - } -}; - -export default define(meta, async (ps, user) => { - await User.update({ _id: user._id }, { - $set: { - pendingReceivedFollowRequestsCount: 0 - } - }); - - return; -}); diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts index fed38eab5a..389d0b3212 100644 --- a/src/server/api/endpoints/i/delete-account.ts +++ b/src/server/api/endpoints/i/delete-account.ts @@ -1,10 +1,8 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../../models/user'; import define from '../../define'; -import { createDeleteNotesJob, createDeleteDriveFilesJob } from '../../../../queue'; -import Message from '../../../../models/messaging-message'; -import Signin from '../../../../models/signin'; +import { Users, UserProfiles } from '../../../../models'; +import { ensure } from '../../../../prelude/ensure'; export const meta = { requireCredential: true, @@ -19,34 +17,14 @@ export const meta = { }; export default define(meta, async (ps, user) => { + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + // Compare password - const same = await bcrypt.compare(ps.password, user.password); + const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { throw new Error('incorrect password'); } - await User.update({ _id: user._id }, { - $set: { - isDeleted: true, - name: null, - description: null, - pinnedNoteIds: [], - password: null, - email: null, - twitter: null, - github: null, - discord: null, - profile: {}, - fields: [], - clientSettings: {}, - } - }); - - Message.remove({ userId: user._id }); - Signin.remove({ userId: user._id }); - createDeleteNotesJob(user); - createDeleteDriveFilesJob(user); - - return; + await Users.delete(user.id); }); diff --git a/src/server/api/endpoints/i/export-blocking.ts b/src/server/api/endpoints/i/export-blocking.ts index 346b29c79d..14d49487e8 100644 --- a/src/server/api/endpoints/i/export-blocking.ts +++ b/src/server/api/endpoints/i/export-blocking.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportBlockingJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/export-following.ts b/src/server/api/endpoints/i/export-following.ts index 5977b03105..50dd28837f 100644 --- a/src/server/api/endpoints/i/export-following.ts +++ b/src/server/api/endpoints/i/export-following.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportFollowingJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/export-mute.ts b/src/server/api/endpoints/i/export-mute.ts index 22ceff3631..1eb51cd77e 100644 --- a/src/server/api/endpoints/i/export-mute.ts +++ b/src/server/api/endpoints/i/export-mute.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportMuteJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/export-notes.ts b/src/server/api/endpoints/i/export-notes.ts index 2881aa2697..dd32c18d11 100644 --- a/src/server/api/endpoints/i/export-notes.ts +++ b/src/server/api/endpoints/i/export-notes.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportNotesJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/export-user-lists.ts b/src/server/api/endpoints/i/export-user-lists.ts index 9d7424ad89..7650ca7210 100644 --- a/src/server/api/endpoints/i/export-user-lists.ts +++ b/src/server/api/endpoints/i/export-user-lists.ts @@ -13,6 +13,4 @@ export const meta = { export default define(meta, async (ps, user) => { createExportUserListsJob(user); - - return; }); diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index 7ea6f7b966..2c25250bea 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Favorite, { packMany } from '../../../../models/favorite'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { NoteFavorites } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -23,42 +24,22 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, - } + }, } }; export default define(meta, async (ps, user) => { - const query = { - userId: user._id - } as any; - - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) + .andWhere(`favorite.userId = :meId`, { meId: user.id }) + .leftJoinAndSelect('favorite.note', 'note'); - // Get favorites - const favorites = await Favorite - .find(query, { - limit: ps.limit, - sort: sort - }); + const favorites = await query + .take(ps.limit!) + .getMany(); - return await packMany(favorites, user); + return await NoteFavorites.packMany(favorites, user); }); diff --git a/src/server/api/endpoints/i/import-following.ts b/src/server/api/endpoints/i/import-following.ts index f188291bc2..deafec18ec 100644 --- a/src/server/api/endpoints/i/import-following.ts +++ b/src/server/api/endpoints/i/import-following.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { createImportFollowingJob } from '../../../../queue'; import ms = require('ms'); -import DriveFile from '../../../../models/drive-file'; import { ApiError } from '../../error'; +import { DriveFiles } from '../../../../models'; export const meta = { secure: true, @@ -17,7 +17,6 @@ export const meta = { params: { fileId: { validator: $.type(ID), - transform: transform, } }, @@ -49,16 +48,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const file = await DriveFile.findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.length > 50000) throw new ApiError(meta.errors.tooBigFile); - if (file.length === 0) throw new ApiError(meta.errors.emptyFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - createImportFollowingJob(user, file._id); - - return; + createImportFollowingJob(user, file.id); }); diff --git a/src/server/api/endpoints/i/import-user-lists.ts b/src/server/api/endpoints/i/import-user-lists.ts index ed3085e5f8..b7d9d029b7 100644 --- a/src/server/api/endpoints/i/import-user-lists.ts +++ b/src/server/api/endpoints/i/import-user-lists.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { createImportUserListsJob } from '../../../../queue'; import ms = require('ms'); -import DriveFile from '../../../../models/drive-file'; import { ApiError } from '../../error'; +import { DriveFiles } from '../../../../models'; export const meta = { secure: true, @@ -17,7 +17,6 @@ export const meta = { params: { fileId: { validator: $.type(ID), - transform: transform, } }, @@ -49,16 +48,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const file = await DriveFile.findOne({ - _id: ps.fileId - }); + const file = await DriveFiles.findOne(ps.fileId); if (file == null) throw new ApiError(meta.errors.noSuchFile); - //if (!file.contentType.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); - if (file.length > 30000) throw new ApiError(meta.errors.tooBigFile); - if (file.length === 0) throw new ApiError(meta.errors.emptyFile); + //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType); + if (file.size > 30000) throw new ApiError(meta.errors.tooBigFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); - createImportUserListsJob(user, file._id); - - return; + createImportUserListsJob(user, file.id); }); diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index d3e3064abd..56074c9d00 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -1,11 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Notification from '../../../../models/notification'; -import { packMany } from '../../../../models/notification'; -import { getFriendIds } from '../../common/get-friends'; -import read from '../../common/read-notification'; +import { ID } from '../../../../misc/cafy-id'; +import { readNotification } from '../../common/read-notification'; import define from '../../define'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notifications, Followings, Mutings } from '../../../../models'; export const meta = { desc: { @@ -17,7 +15,7 @@ export const meta = { requireCredential: true, - kind: 'account-read', + kind: 'read:notifications', params: { limit: { @@ -27,12 +25,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, following: { @@ -46,12 +42,12 @@ export const meta = { }, includeTypes: { - validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])), + validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])), default: [] as string[] }, excludeTypes: { - validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote', 'receiveFollowRequest'])), + validator: $.optional.arr($.str.or(['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest'])), default: [] as string[] } }, @@ -65,63 +61,38 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const hideUserIds = await getHideUserIds(user); + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: user.id }); - const query = { - notifieeId: user._id, - $and: [{ - notifierId: { - $nin: hideUserIds - } - }] - } as any; + const mutingQuery = Mutings.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: user.id }); - const sort = { - _id: -1 - }; + const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId) + .andWhere(`notification.notifieeId = :meId`, { meId: user.id }) + .leftJoinAndSelect('notification.notifier', 'notifier'); - if (ps.following) { - // ID list of the user itself and other users who the user follows - const followingIds = await getFriendIds(user._id); - - query.$and.push({ - notifierId: { - $in: followingIds - } - }); - } + query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`); + query.setParameters(mutingQuery.getParameters()); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; + if (ps.following) { + query.andWhere(`((notification.notifierId IN (${ followingQuery.getQuery() })) OR (notification.notifierId = :meId))`, { meId: user.id }); + query.setParameters(followingQuery.getParameters()); } - if (ps.includeTypes.length > 0) { - query.type = { - $in: ps.includeTypes - }; - } else if (ps.excludeTypes.length > 0) { - query.type = { - $nin: ps.excludeTypes - }; + if (ps.includeTypes!.length > 0) { + query.andWhere(`notification.type IN (:...includeTypes)`, { includeTypes: ps.includeTypes }); + } else if (ps.excludeTypes!.length > 0) { + query.andWhere(`notification.type NOT IN (:...excludeTypes)`, { excludeTypes: ps.excludeTypes }); } - const notifications = await Notification - .find(query, { - limit: ps.limit, - sort: sort - }); + const notifications = await query.take(ps.limit!).getMany(); // Mark all as read if (notifications.length > 0 && ps.markAsRead) { - read(user._id, notifications); + readNotification(user.id, notifications.map(x => x.id)); } - return await packMany(notifications); + return await Notifications.packMany(notifications); }); diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index 8d853d45c8..ac104b19f9 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import { pack } from '../../../../models/user'; +import { ID } from '../../../../misc/cafy-id'; import { addPinned } from '../../../../services/i/pin'; import define from '../../define'; import { ApiError } from '../../error'; +import { Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -16,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID' @@ -58,7 +57,7 @@ export default define(meta, async (ps, user) => { throw e; }); - return await pack(user, user, { + return await Users.pack(user, user, { detail: true }); }); diff --git a/src/server/api/endpoints/i/read-all-messaging-messages.ts b/src/server/api/endpoints/i/read-all-messaging-messages.ts index bbbfa0d7b3..e8ada277e9 100644 --- a/src/server/api/endpoints/i/read-all-messaging-messages.ts +++ b/src/server/api/endpoints/i/read-all-messaging-messages.ts @@ -1,7 +1,6 @@ -import User from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; -import Message from '../../../../models/messaging-message'; import define from '../../define'; +import { MessagingMessages } from '../../../../models'; export const meta = { desc: { @@ -13,7 +12,7 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { } @@ -21,24 +20,12 @@ export const meta = { export default define(meta, async (ps, user) => { // Update documents - await Message.update({ - recipientId: user._id, + await MessagingMessages.update({ + recipientId: user.id, isRead: false }, { - $set: { - isRead: true - } - }, { - multi: true - }); - - User.update({ _id: user._id }, { - $set: { - hasUnreadMessagingMessage: false - } + isRead: true }); - publishMainStream(user._id, 'readAllMessagingMessages'); - - return; + publishMainStream(user.id, 'readAllMessagingMessages'); }); diff --git a/src/server/api/endpoints/i/read-all-unread-notes.ts b/src/server/api/endpoints/i/read-all-unread-notes.ts index 742c2d9908..cc8ebf58ec 100644 --- a/src/server/api/endpoints/i/read-all-unread-notes.ts +++ b/src/server/api/endpoints/i/read-all-unread-notes.ts @@ -1,7 +1,6 @@ -import User from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; -import NoteUnread from '../../../../models/note-unread'; import define from '../../define'; +import { NoteUnreads } from '../../../../models'; export const meta = { desc: { @@ -13,7 +12,7 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { } @@ -21,20 +20,11 @@ export const meta = { export default define(meta, async (ps, user) => { // Remove documents - await NoteUnread.remove({ - userId: user._id - }); - - User.update({ _id: user._id }, { - $set: { - hasUnreadMentions: false, - hasUnreadSpecifiedNotes: false - } + await NoteUnreads.delete({ + userId: user.id }); // 全て既読になったイベントを発行 - publishMainStream(user._id, 'readAllUnreadMentions'); - publishMainStream(user._id, 'readAllUnreadSpecifiedNotes'); - - return; + publishMainStream(user.id, 'readAllUnreadMentions'); + publishMainStream(user.id, 'readAllUnreadSpecifiedNotes'); }); diff --git a/src/server/api/endpoints/i/regenerate-token.ts b/src/server/api/endpoints/i/regenerate-token.ts index ad10b99b36..56c0362c88 100644 --- a/src/server/api/endpoints/i/regenerate-token.ts +++ b/src/server/api/endpoints/i/regenerate-token.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; import * as bcrypt from 'bcryptjs'; -import User from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; import generateUserToken from '../../common/generate-native-user-token'; import define from '../../define'; +import { Users, UserProfiles } from '../../../../models'; +import { ensure } from '../../../../prelude/ensure'; export const meta = { requireCredential: true, @@ -18,8 +19,10 @@ export const meta = { }; export default define(meta, async (ps, user) => { + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + // Compare password - const same = await bcrypt.compare(ps.password, user.password); + const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { throw new Error('incorrect password'); @@ -28,14 +31,10 @@ export default define(meta, async (ps, user) => { // Generate secret const secret = generateUserToken(); - await User.update(user._id, { - $set: { - 'token': secret - } + await Users.update(user.id, { + token: secret }); // Publish event - publishMainStream(user._id, 'myTokenRegenerated'); - - return; + publishMainStream(user.id, 'myTokenRegenerated'); }); diff --git a/src/server/api/endpoints/i/signin-history.ts b/src/server/api/endpoints/i/signin-history.ts index 87160a9f91..74648951fd 100644 --- a/src/server/api/endpoints/i/signin-history.ts +++ b/src/server/api/endpoints/i/signin-history.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Signin, { pack } from '../../../../models/signin'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { Signins } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { requireCredential: true, @@ -16,41 +17,19 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, } } }; export default define(meta, async (ps, user) => { - const query = { - userId: user._id - } as any; + const query = makePaginationQuery(Signins.createQueryBuilder('signin'), ps.sinceId, ps.untilId) + .andWhere(`signin.userId = :meId`, { meId: user.id }); - const sort = { - _id: -1 - }; + const history = await query.take(ps.limit!).getMany(); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const history = await Signin - .find(query, { - limit: ps.limit, - sort: sort - }); - - return await Promise.all(history.map(record => pack(record))); + return await Promise.all(history.map(record => Signins.pack(record))); }); diff --git a/src/server/api/endpoints/i/unpin.ts b/src/server/api/endpoints/i/unpin.ts index 184d46f2c3..4688533578 100644 --- a/src/server/api/endpoints/i/unpin.ts +++ b/src/server/api/endpoints/i/unpin.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import { pack } from '../../../../models/user'; +import { ID } from '../../../../misc/cafy-id'; import { removePinned } from '../../../../services/i/pin'; import define from '../../define'; import { ApiError } from '../../error'; +import { Users } from '../../../../models'; export const meta = { stability: 'stable', @@ -16,12 +16,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID' @@ -44,7 +43,7 @@ export default define(meta, async (ps, user) => { throw e; }); - return await pack(user, user, { + return await Users.pack(user, user, { detail: true }); }); diff --git a/src/server/api/endpoints/i/update-client-setting.ts b/src/server/api/endpoints/i/update-client-setting.ts index 79cd04e169..36de183379 100644 --- a/src/server/api/endpoints/i/update-client-setting.ts +++ b/src/server/api/endpoints/i/update-client-setting.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import User from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; import define from '../../define'; +import { UserProfiles } from '../../../../models'; export const meta = { requireCredential: true, @@ -10,7 +10,7 @@ export const meta = { params: { name: { - validator: $.str + validator: $.str.match(/^[a-zA-Z]+$/) }, value: { @@ -20,18 +20,18 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const x: any = {}; - x[`clientSettings.${ps.name}`] = ps.value; - - await User.update(user._id, { - $set: x - }); + await UserProfiles.createQueryBuilder().update() + .set({ + clientData: { + [ps.name]: ps.value + }, + }) + .where('userId = :id', { id: user.id }) + .execute(); // Publish event - publishMainStream(user._id, 'clientSettingUpdated', { + publishMainStream(user.id, 'clientSettingUpdated', { key: ps.name, value: ps.value }); - - return; }); diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts index c90462d850..15c62a9d08 100644 --- a/src/server/api/endpoints/i/update-email.ts +++ b/src/server/api/endpoints/i/update-email.ts @@ -1,5 +1,4 @@ import $ from 'cafy'; -import User, { pack } from '../../../../models/user'; import { publishMainStream } from '../../../../services/stream'; import define from '../../define'; import * as nodemailer from 'nodemailer'; @@ -9,6 +8,8 @@ import config from '../../../../config'; import * as ms from 'ms'; import * as bcrypt from 'bcryptjs'; import { apiLogger } from '../../logger'; +import { Users, UserProfiles } from '../../../../models'; +import { ensure } from '../../../../prelude/ensure'; export const meta = { requireCredential: true, @@ -32,36 +33,34 @@ export const meta = { }; export default define(meta, async (ps, user) => { + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + // Compare password - const same = await bcrypt.compare(ps.password, user.password); + const same = await bcrypt.compare(ps.password, profile.password!); if (!same) { throw new Error('incorrect password'); } - await User.update(user._id, { - $set: { - email: ps.email, - emailVerified: false, - emailVerifyCode: null - } + await UserProfiles.update({ userId: user.id }, { + email: ps.email, + emailVerified: false, + emailVerifyCode: null }); - const iObj = await pack(user._id, user, { + const iObj = await Users.pack(user.id, user, { detail: true, includeSecrets: true }); // Publish meUpdated event - publishMainStream(user._id, 'meUpdated', iObj); + publishMainStream(user.id, 'meUpdated', iObj); if (ps.email != null) { const code = rndstr('a-z0-9', 16); - await User.update(user._id, { - $set: { - emailVerifyCode: code - } + await UserProfiles.update({ userId: user.id }, { + emailVerifyCode: code }); const meta = await fetchMeta(); @@ -77,14 +76,14 @@ export default define(meta, async (ps, user) => { user: meta.smtpUser, pass: meta.smtpPass } : undefined - }); + } as any); const link = `${config.url}/verify-email/${code}`; transporter.sendMail({ - from: meta.email, + from: meta.email!, to: ps.email, - subject: meta.name, + subject: meta.name || 'Misskey', text: `To verify email, please click this link: ${link}` }, (error, info) => { if (error) { diff --git a/src/server/api/endpoints/i/update-home.ts b/src/server/api/endpoints/i/update-home.ts deleted file mode 100644 index e2c319887f..0000000000 --- a/src/server/api/endpoints/i/update-home.ts +++ /dev/null @@ -1,33 +0,0 @@ -import $ from 'cafy'; -import User from '../../../../models/user'; -import { publishMainStream } from '../../../../services/stream'; -import define from '../../define'; - -export const meta = { - requireCredential: true, - - secure: true, - - params: { - home: { - validator: $.arr($.obj({ - name: $.str, - id: $.str, - place: $.str, - data: $.obj() - }).strict()) - } - } -}; - -export default define(meta, async (ps, user) => { - await User.update(user._id, { - $set: { - 'clientSettings.home': ps.home - } - }); - - publishMainStream(user._id, 'homeUpdated', ps.home); - - return; -}); diff --git a/src/server/api/endpoints/i/update-mobile-home.ts b/src/server/api/endpoints/i/update-mobile-home.ts deleted file mode 100644 index 642e2b3e09..0000000000 --- a/src/server/api/endpoints/i/update-mobile-home.ts +++ /dev/null @@ -1,32 +0,0 @@ -import $ from 'cafy'; -import User from '../../../../models/user'; -import { publishMainStream } from '../../../../services/stream'; -import define from '../../define'; - -export const meta = { - requireCredential: true, - - secure: true, - - params: { - home: { - validator: $.arr($.obj({ - name: $.str, - id: $.str, - data: $.obj() - }).strict()) - } - } -}; - -export default define(meta, async (ps, user) => { - await User.update(user._id, { - $set: { - 'clientSettings.mobileHome': ps.home - } - }); - - publishMainStream(user._id, 'mobileHomeUpdated', ps.home); - - return; -}); diff --git a/src/server/api/endpoints/i/update-widget.ts b/src/server/api/endpoints/i/update-widget.ts deleted file mode 100644 index 67d342278d..0000000000 --- a/src/server/api/endpoints/i/update-widget.ts +++ /dev/null @@ -1,88 +0,0 @@ -import $ from 'cafy'; -import User from '../../../../models/user'; -import { publishMainStream } from '../../../../services/stream'; -import define from '../../define'; - -export const meta = { - requireCredential: true, - - secure: true, - - params: { - id: { - validator: $.str - }, - - data: { - validator: $.obj() - } - } -}; - -export default define(meta, async (ps, user) => { - if (ps.id == null && ps.data == null) throw new Error('you need to set id and data params if home param unset'); - - let widget; - - //#region Desktop home - if (widget == null && user.clientSettings.home) { - const desktopHome = user.clientSettings.home; - widget = desktopHome.find((w: any) => w.id == ps.id); - if (widget) { - widget.data = ps.data; - - await User.update(user._id, { - $set: { - 'clientSettings.home': desktopHome - } - }); - } - } - //#endregion - - //#region Mobile home - if (widget == null && user.clientSettings.mobileHome) { - const mobileHome = user.clientSettings.mobileHome; - widget = mobileHome.find((w: any) => w.id == ps.id); - if (widget) { - widget.data = ps.data; - - await User.update(user._id, { - $set: { - 'clientSettings.mobileHome': mobileHome - } - }); - } - } - //#endregion - - //#region Deck - if (widget == null && user.clientSettings.deck && user.clientSettings.deck.columns) { - const deck = user.clientSettings.deck; - for (const c of deck.columns.filter((c: any) => c.type == 'widgets')) { - for (const w of c.widgets.filter((w: any) => w.id == ps.id)) { - widget = w; - } - } - if (widget) { - widget.data = ps.data; - - await User.update(user._id, { - $set: { - 'clientSettings.deck': deck - } - }); - } - } - //#endregion - - if (widget) { - publishMainStream(user._id, 'widgetUpdated', { - id: ps.id, data: ps.data - }); - - return; - } else { - throw new Error('widget not found'); - } -}); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 099ef33990..d06ab621c6 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -1,18 +1,18 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user'; +import { ID } from '../../../../misc/cafy-id'; import { publishMainStream } from '../../../../services/stream'; -import DriveFile from '../../../../models/drive-file'; import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; import { publishToFollowers } from '../../../../services/i/update'; import define from '../../define'; -import getDriveFileUrl from '../../../../misc/get-drive-file-url'; import { parse, parsePlain } from '../../../../mfm/parse'; import extractEmojis from '../../../../misc/extract-emojis'; import extractHashtags from '../../../../misc/extract-hashtags'; import * as langmap from 'langmap'; import { updateHashtag } from '../../../../services/update-hashtag'; import { ApiError } from '../../error'; +import { Users, DriveFiles, UserProfiles } from '../../../../models'; +import { User } from '../../../../models/entities/user'; +import { UserProfile } from '../../../../models/entities/user-profile'; export const meta = { desc: { @@ -24,18 +24,18 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { name: { - validator: $.optional.nullable.str.pipe(isValidName), + validator: $.optional.nullable.str.pipe(Users.isValidName), desc: { 'ja-JP': '名前(ハンドルネームやニックネーム)' } }, description: { - validator: $.optional.nullable.str.pipe(isValidDescription), + validator: $.optional.nullable.str.pipe(Users.isValidDescription), desc: { 'ja-JP': 'アカウントの説明や自己紹介' } @@ -49,14 +49,14 @@ export const meta = { }, location: { - validator: $.optional.nullable.str.pipe(isValidLocation), + validator: $.optional.nullable.str.pipe(Users.isValidLocation), desc: { 'ja-JP': '住んでいる地域、所在' } }, birthday: { - validator: $.optional.nullable.str.pipe(isValidBirthday), + validator: $.optional.nullable.str.pipe(Users.isValidBirthday), desc: { 'ja-JP': '誕生日 (YYYY-MM-DD形式)' } @@ -64,7 +64,6 @@ export const meta = { avatarId: { validator: $.optional.nullable.type(ID), - transform: transform, desc: { 'ja-JP': 'アイコンに設定する画像のドライブファイルID' } @@ -72,20 +71,11 @@ export const meta = { bannerId: { validator: $.optional.nullable.type(ID), - transform: transform, desc: { 'ja-JP': 'バナーに設定する画像のドライブファイルID' } }, - wallpaperId: { - validator: $.optional.nullable.type(ID), - transform: transform, - desc: { - 'ja-JP': '壁紙に設定する画像のドライブファイルID' - } - }, - isLocked: { validator: $.optional.bool, desc: { @@ -166,121 +156,83 @@ export const meta = { export default define(meta, async (ps, user, app) => { const isSecure = user != null && app == null; - const updates = {} as any; + const updates = {} as Partial<User>; + const profile = {} as Partial<UserProfile>; if (ps.name !== undefined) updates.name = ps.name; - if (ps.description !== undefined) updates.description = ps.description; - if (ps.lang !== undefined) updates.lang = ps.lang; - if (ps.location !== undefined) updates['profile.location'] = ps.location; - if (ps.birthday !== undefined) updates['profile.birthday'] = ps.birthday; + if (ps.description !== undefined) profile.description = ps.description; + //if (ps.lang !== undefined) updates.lang = ps.lang; + if (ps.location !== undefined) profile.location = ps.location; + if (ps.birthday !== undefined) profile.birthday = ps.birthday; if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; - if (ps.wallpaperId !== undefined) updates.wallpaperId = ps.wallpaperId; if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot; - if (typeof ps.carefulBot == 'boolean') updates.carefulBot = ps.carefulBot; - if (typeof ps.autoAcceptFollowed == 'boolean') updates.autoAcceptFollowed = ps.autoAcceptFollowed; + if (typeof ps.carefulBot == 'boolean') profile.carefulBot = ps.carefulBot; + if (typeof ps.autoAcceptFollowed == 'boolean') profile.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat; - if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch; - if (typeof ps.alwaysMarkNsfw == 'boolean') updates['settings.alwaysMarkNsfw'] = ps.alwaysMarkNsfw; + if (typeof ps.autoWatch == 'boolean') profile.autoWatch = ps.autoWatch; + if (typeof ps.alwaysMarkNsfw == 'boolean') profile.alwaysMarkNsfw = ps.alwaysMarkNsfw; if (ps.avatarId) { - const avatar = await DriveFile.findOne({ - _id: ps.avatarId - }); + const avatar = await DriveFiles.findOne(ps.avatarId); - if (avatar == null) throw new ApiError(meta.errors.noSuchAvatar); - if (!avatar.contentType.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); + if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); + if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage); - if (avatar.metadata.deletedAt) { - updates.avatarUrl = null; - } else { - updates.avatarUrl = getDriveFileUrl(avatar, true); + updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true); - if (avatar.metadata.properties.avgColor) { - updates.avatarColor = avatar.metadata.properties.avgColor; - } + if (avatar.properties.avgColor) { + updates.avatarColor = avatar.properties.avgColor; } } if (ps.bannerId) { - const banner = await DriveFile.findOne({ - _id: ps.bannerId - }); - - if (banner == null) throw new ApiError(meta.errors.noSuchBanner); - if (!banner.contentType.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); - - if (banner.metadata.deletedAt) { - updates.bannerUrl = null; - } else { - updates.bannerUrl = getDriveFileUrl(banner, false); - - if (banner.metadata.properties.avgColor) { - updates.bannerColor = banner.metadata.properties.avgColor; - } - } - } - - if (ps.wallpaperId !== undefined) { - if (ps.wallpaperId === null) { - updates.wallpaperUrl = null; - updates.wallpaperColor = null; - } else { - const wallpaper = await DriveFile.findOne({ - _id: ps.wallpaperId - }); + const banner = await DriveFiles.findOne(ps.bannerId); - if (wallpaper == null) throw new Error('wallpaper not found'); + if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); + if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage); - if (wallpaper.metadata.deletedAt) { - updates.wallpaperUrl = null; - } else { - updates.wallpaperUrl = getDriveFileUrl(wallpaper); + updates.bannerUrl = DriveFiles.getPublicUrl(banner, false); - if (wallpaper.metadata.properties.avgColor) { - updates.wallpaperColor = wallpaper.metadata.properties.avgColor; - } - } + if (banner.properties.avgColor) { + updates.bannerColor = banner.properties.avgColor; } } //#region emojis/tags - if (updates.name != null || updates.description != null) { - let emojis = [] as string[]; - let tags = [] as string[]; + let emojis = [] as string[]; + let tags = [] as string[]; - if (updates.name != null) { - const tokens = parsePlain(updates.name); - emojis = emojis.concat(extractEmojis(tokens)); - } + if (updates.name != null) { + const tokens = parsePlain(updates.name); + emojis = emojis.concat(extractEmojis(tokens!)); + } - if (updates.description != null) { - const tokens = parse(updates.description); - emojis = emojis.concat(extractEmojis(tokens)); - tags = extractHashtags(tokens).map(tag => tag.toLowerCase()); - } + if (profile.description != null) { + const tokens = parse(profile.description); + emojis = emojis.concat(extractEmojis(tokens!)); + tags = extractHashtags(tokens!).map(tag => tag.toLowerCase()); + } - updates.emojis = emojis; - updates.tags = tags; + updates.emojis = emojis; + updates.tags = tags; - // ハッシュタグ更新 - for (const tag of tags) updateHashtag(user, tag, true, true); - for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false); - } + // ハッシュタグ更新 + for (const tag of tags) updateHashtag(user, tag, true, true); + for (const tag of user.tags.filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false); //#endregion - await User.update(user._id, { - $set: updates - }); + if (Object.keys(updates).length > 0) await Users.update(user.id, updates); + if (Object.keys(profile).length > 0) await UserProfiles.update({ userId: user.id }, profile); - const iObj = await pack(user._id, user, { + const iObj = await Users.pack(user.id, user, { detail: true, includeSecrets: isSecure }); // Publish meUpdated event - publishMainStream(user._id, 'meUpdated', iObj); + publishMainStream(user.id, 'meUpdated', iObj); // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 if (user.isLocked && ps.isLocked === false) { @@ -288,7 +240,7 @@ export default define(meta, async (ps, user, app) => { } // フォロワーにUpdateを配信 - publishToFollowers(user._id); + publishToFollowers(user.id); return iObj; }); diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index 699dc7c253..c12378eb7e 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import Mute from '../../../../models/mute'; -import Message, { pack, IMessagingMessage } from '../../../../models/messaging-message'; import define from '../../define'; +import { MessagingMessage } from '../../../../models/entities/messaging-message'; +import { MessagingMessages, Mutings } from '../../../../models'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -31,34 +32,33 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const mute = await Mute.find({ - muterId: user._id, - deletedAt: { $exists: false } + const mute = await Mutings.find({ + muterId: user.id, }); - const history: IMessagingMessage[] = []; + const history: MessagingMessage[] = []; - for (let i = 0; i < ps.limit; i++) { - const found = history.map(m => m.userId.equals(user._id) ? m.recipientId : m.userId); + for (let i = 0; i < ps.limit!; i++) { + const found = history.map(m => (m.userId === user.id) ? m.recipientId : m.userId); - const message = await Message.findOne({ - $or: [{ - userId: user._id - }, { - recipientId: user._id - }], - $and: [{ - userId: { $nin: found }, - recipientId: { $nin: found } - }, { - userId: { $nin: mute.map(m => m.muteeId) }, - recipientId: { $nin: mute.map(m => m.muteeId) } - }] - }, { - sort: { - createdAt: -1 - } - }); + const query = MessagingMessages.createQueryBuilder('message') + .where(new Brackets(qb => { qb + .where(`message.userId = :userId`, { userId: user.id }) + .orWhere(`message.recipientId = :userId`, { userId: user.id }); + })) + .orderBy('message.createdAt', 'DESC'); + + if (found.length > 0) { + query.andWhere(`message.userId NOT IN (:...found)`, { found: found }); + query.andWhere(`message.recipientId NOT IN (:...found)`, { found: found }); + } + + if (mute.length > 0) { + query.andWhere(`message.userId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + query.andWhere(`message.recipientId NOT IN (:...mute)`, { mute: mute.map(m => m.muteeId) }); + } + + const message = await query.getOne(); if (message) { history.push(message); @@ -67,5 +67,5 @@ export default define(meta, async (ps, user) => { } } - return await Promise.all(history.map(h => pack(h._id, user))); + return await Promise.all(history.map(h => MessagingMessages.pack(h.id, user))); }); diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index c19db45f1f..02c57b8d03 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -1,11 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Message from '../../../../models/messaging-message'; -import { pack } from '../../../../models/messaging-message'; +import { ID } from '../../../../misc/cafy-id'; import read from '../../common/read-messaging-message'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { MessagingMessages } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; export const meta = { desc: { @@ -22,7 +22,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -36,12 +35,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, markAsRead: { @@ -73,43 +70,17 @@ export default define(meta, async (ps, user) => { throw e; }); - const query = { - $or: [{ - userId: user._id, - recipientId: recipient._id - }, { - userId: recipient._id, - recipientId: user._id - }] - } as any; + const query = makePaginationQuery(MessagingMessages.createQueryBuilder('message'), ps.sinceId, ps.untilId) + .andWhere(`(message.userId = :meId AND message.recipientId = :recipientId) OR (message.userId = :recipientId AND message.recipientId = :meId)`, { meId: user.id, recipientId: recipient.id }); - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const messages = await Message - .find(query, { - limit: ps.limit, - sort: sort - }); + const messages = await query.getMany(); // Mark all as read if (ps.markAsRead) { - read(user._id, recipient._id, messages); + read(user.id, recipient.id, messages.map(x => x.id)); } - return await Promise.all(messages.map(message => pack(message, user, { + return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, { populateRecipient: false }))); }); diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index fc048e6edd..2c7e5ad2d9 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -1,17 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Message from '../../../../../models/messaging-message'; -import { isValidText } from '../../../../../models/messaging-message'; -import User from '../../../../../models/user'; -import Mute from '../../../../../models/mute'; -import DriveFile from '../../../../../models/drive-file'; -import { pack } from '../../../../../models/messaging-message'; +import { ID } from '../../../../../misc/cafy-id'; import { publishMainStream } from '../../../../../services/stream'; import { publishMessagingStream, publishMessagingIndexStream } from '../../../../../services/stream'; import pushSw from '../../../../../services/push-notification'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; +import { MessagingMessages, DriveFiles, Mutings } from '../../../../../models'; +import { MessagingMessage } from '../../../../../models/entities/messaging-message'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { desc: { @@ -28,7 +25,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -36,12 +32,11 @@ export const meta = { }, text: { - validator: $.optional.str.pipe(isValidText) + validator: $.optional.str.pipe(MessagingMessages.isValidText) }, fileId: { validator: $.optional.type(ID), - transform: transform, } }, @@ -78,7 +73,7 @@ export const meta = { export default define(meta, async (ps, user) => { // Myself - if (ps.userId.equals(user._id)) { + if (ps.userId === user.id) { throw new ApiError(meta.errors.recipientIsYourself); } @@ -90,12 +85,12 @@ export default define(meta, async (ps, user) => { let file = null; if (ps.fileId != null) { - file = await DriveFile.findOne({ - _id: ps.fileId, - 'metadata.userId': user._id + file = await DriveFiles.findOne({ + id: ps.fileId, + userId: user.id }); - if (file === null) { + if (file == null) { throw new ApiError(meta.errors.noSuchFile); } } @@ -105,16 +100,17 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.contentRequired); } - const message = await Message.insert({ + const message = await MessagingMessages.save({ + id: genId(), createdAt: new Date(), - fileId: file ? file._id : undefined, - recipientId: recipient._id, - text: ps.text ? ps.text.trim() : undefined, - userId: user._id, + fileId: file ? file.id : null, + recipientId: recipient.id, + text: ps.text ? ps.text.trim() : null, + userId: user.id, isRead: false - }); + } as MessagingMessage); - const messageObj = await pack(message); + const messageObj = await MessagingMessages.pack(message); // 自分のストリーム publishMessagingStream(message.userId, message.recipientId, 'message', messageObj); @@ -126,25 +122,17 @@ export default define(meta, async (ps, user) => { publishMessagingIndexStream(message.recipientId, 'message', messageObj); publishMainStream(message.recipientId, 'messagingMessage', messageObj); - // Update flag - User.update({ _id: recipient._id }, { - $set: { - hasUnreadMessagingMessage: true - } - }); - // 2秒経っても(今回作成した)メッセージが既読にならなかったら「未読のメッセージがありますよ」イベントを発行する setTimeout(async () => { - const freshMessage = await Message.findOne({ _id: message._id }, { isRead: true }); + const freshMessage = await MessagingMessages.findOne({ id: message.id }); if (freshMessage == null) return; // メッセージが削除されている場合もある if (!freshMessage.isRead) { //#region ただしミュートされているなら発行しない - const mute = await Mute.find({ - muterId: recipient._id, - deletedAt: { $exists: false } + const mute = await Mutings.find({ + muterId: recipient.id, }); const mutedUserIds = mute.map(m => m.muteeId.toString()); - if (mutedUserIds.indexOf(user._id.toString()) != -1) { + if (mutedUserIds.indexOf(user.id) != -1) { return; } //#endregion diff --git a/src/server/api/endpoints/messaging/messages/delete.ts b/src/server/api/endpoints/messaging/messages/delete.ts index 0ca12846c1..9f55caba62 100644 --- a/src/server/api/endpoints/messaging/messages/delete.ts +++ b/src/server/api/endpoints/messaging/messages/delete.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Message from '../../../../../models/messaging-message'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { publishMessagingStream } from '../../../../../services/stream'; import * as ms from 'ms'; import { ApiError } from '../../../error'; +import { MessagingMessages } from '../../../../../models'; export const meta = { stability: 'stable', @@ -29,7 +29,6 @@ export const meta = { params: { messageId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のメッセージのID', 'en-US': 'Target message ID.' @@ -47,19 +46,17 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const message = await Message.findOne({ - _id: ps.messageId, - userId: user._id + const message = await MessagingMessages.findOne({ + id: ps.messageId, + userId: user.id }); - if (message === null) { + if (message == null) { throw new ApiError(meta.errors.noSuchMessage); } - await Message.remove({ _id: message._id }); + await MessagingMessages.delete(message.id); - publishMessagingStream(message.userId, message.recipientId, 'deleted', message._id); - publishMessagingStream(message.recipientId, message.userId, 'deleted', message._id); - - return; + publishMessagingStream(message.userId, message.recipientId, 'deleted', message.id); + publishMessagingStream(message.recipientId, message.userId, 'deleted', message.id); }); diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts index aa8ecdc4ff..24a28285bf 100644 --- a/src/server/api/endpoints/messaging/messages/read.ts +++ b/src/server/api/endpoints/messaging/messages/read.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Message from '../../../../../models/messaging-message'; +import { ID } from '../../../../../misc/cafy-id'; import read from '../../../common/read-messaging-message'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { MessagingMessages } from '../../../../../models'; export const meta = { desc: { @@ -20,7 +20,6 @@ export const meta = { params: { messageId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '既読にするメッセージのID', 'en-US': 'The ID of a message that you want to mark as read' @@ -38,16 +37,14 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const message = await Message.findOne({ - _id: ps.messageId, - recipientId: user._id + const message = await MessagingMessages.findOne({ + id: ps.messageId, + recipientId: user.id }); if (message == null) { throw new ApiError(meta.errors.noSuchMessage); } - read(user._id, message.userId, message); - - return; + read(user.id, message.userId, [message.id]); }); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index a297f47e0e..785f21f22b 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; import * as os from 'os'; import config from '../../../config'; -import Emoji from '../../../models/emoji'; import define from '../define'; import fetchMeta from '../../../misc/fetch-meta'; import * as pkg from '../../../../package.json'; +import { Emojis } from '../../../models'; export const meta = { stability: 'stable', @@ -81,14 +81,11 @@ export const meta = { export default define(meta, async (ps, me) => { const instance = await fetchMeta(); - const emojis = await Emoji.find({ host: null }, { - fields: { - _id: false - } - }); + const emojis = await Emojis.find({ host: null }); const response: any = { - maintainer: instance.maintainer, + maintainerName: instance.maintainerName, + maintainerEmail: instance.maintainerEmail, version: pkg.version, @@ -145,17 +142,12 @@ export default define(meta, async (ps, me) => { github: instance.enableGithubIntegration, discord: instance.enableDiscordIntegration, serviceWorker: instance.enableServiceWorker, - userRecommendation: { - external: instance.enableExternalUserRecommendation, - engine: instance.externalUserRecommendationEngine, - timeout: instance.externalUserRecommendationTimeout - } }; } if (me && (me.isAdmin || me.isModerator)) { response.useStarForReactionFallback = instance.useStarForReactionFallback; - response.hidedTags = instance.hidedTags; + response.hiddenTags = instance.hiddenTags; response.recaptchaSecretKey = instance.recaptchaSecretKey; response.proxyAccount = instance.proxyAccount; response.twitterConsumerKey = instance.twitterConsumerKey; @@ -164,9 +156,6 @@ export default define(meta, async (ps, me) => { response.githubClientSecret = instance.githubClientSecret; response.discordClientId = instance.discordClientId; response.discordClientSecret = instance.discordClientSecret; - response.enableExternalUserRecommendation = instance.enableExternalUserRecommendation; - response.externalUserRecommendationEngine = instance.externalUserRecommendationEngine; - response.externalUserRecommendationTimeout = instance.externalUserRecommendationTimeout; response.summalyProxy = instance.summalyProxy; response.email = instance.email; response.smtpSecure = instance.smtpSecure; diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts index 7eaee90a05..d13c546fdc 100644 --- a/src/server/api/endpoints/mute/create.ts +++ b/src/server/api/endpoints/mute/create.ts @@ -1,9 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Mute from '../../../../models/mute'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { genId } from '../../../../misc/gen-id'; +import { Mutings, NoteWatchings } from '../../../../models'; +import { Muting } from '../../../../models/entities/muting'; export const meta = { desc: { @@ -15,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'account/write', + kind: 'write:mutes', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -53,7 +54,7 @@ export default define(meta, async (ps, user) => { const muter = user; // 自分自身 - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.muteeIsYourself); } @@ -64,21 +65,25 @@ export default define(meta, async (ps, user) => { }); // Check if already muting - const exist = await Mute.findOne({ - muterId: muter._id, - muteeId: mutee._id + const exist = await Mutings.findOne({ + muterId: muter.id, + muteeId: mutee.id }); - if (exist !== null) { + if (exist != null) { throw new ApiError(meta.errors.alreadyMuting); } // Create mute - await Mute.insert({ + await Mutings.save({ + id: genId(), createdAt: new Date(), - muterId: muter._id, - muteeId: mutee._id, - }); + muterId: muter.id, + muteeId: mutee.id, + } as Muting); - return; + NoteWatchings.delete({ + userId: muter.id, + noteUserId: mutee.id + }); }); diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts index 1a03f6371b..1aae15af91 100644 --- a/src/server/api/endpoints/mute/delete.ts +++ b/src/server/api/endpoints/mute/delete.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Mute from '../../../../models/mute'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Mutings } from '../../../../models'; export const meta = { desc: { @@ -15,12 +15,11 @@ export const meta = { requireCredential: true, - kind: 'account/write', + kind: 'write:mutes', params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -53,7 +52,7 @@ export default define(meta, async (ps, user) => { const muter = user; // Check if the mutee is yourself - if (user._id.equals(ps.userId)) { + if (user.id === ps.userId) { throw new ApiError(meta.errors.muteeIsYourself); } @@ -64,19 +63,17 @@ export default define(meta, async (ps, user) => { }); // Check not muting - const exist = await Mute.findOne({ - muterId: muter._id, - muteeId: mutee._id + const exist = await Mutings.findOne({ + muterId: muter.id, + muteeId: mutee.id }); - if (exist === null) { + if (exist == null) { throw new ApiError(meta.errors.notMuting); } // Delete mute - await Mute.remove({ - _id: exist._id + await Mutings.delete({ + id: exist.id }); - - return; }); diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts index 1b8f759496..0fd8a4860d 100644 --- a/src/server/api/endpoints/mute/list.ts +++ b/src/server/api/endpoints/mute/list.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Mute, { packMany } from '../../../../models/mute'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Mutings } from '../../../../models'; export const meta = { desc: { @@ -13,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'account/read', + kind: 'read:mutes', params: { limit: { @@ -23,12 +24,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -41,30 +40,12 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const query = { - muterId: me._id - } as any; + const query = makePaginationQuery(Mutings.createQueryBuilder('muting'), ps.sinceId, ps.untilId) + .andWhere(`muting.muterId = :meId`, { meId: me.id }); - const sort = { - _id: -1 - }; + const mutings = await query + .take(ps.limit!) + .getMany(); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const mutes = await Mute - .find(query, { - limit: ps.limit, - sort: sort - }); - - return await packMany(mutes, me); + return await Mutings.packMany(mutings, me); }); diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts index 1a936c918b..e8b26362a4 100644 --- a/src/server/api/endpoints/my/apps.ts +++ b/src/server/api/endpoints/my/apps.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; -import App, { pack } from '../../../../models/app'; import define from '../../define'; +import { Apps } from '../../../../models'; export const meta = { tags: ['account', 'app'], @@ -27,19 +27,16 @@ export const meta = { export default define(meta, async (ps, user) => { const query = { - userId: user._id + userId: user.id }; - const apps = await App - .find(query, { - limit: ps.limit, - skip: ps.offset, - sort: { - _id: -1 - } - }); + const apps = await Apps.find({ + where: query, + take: ps.limit!, + skip: ps.offset, + }); - return await Promise.all(apps.map(app => pack(app, user, { + return await Promise.all(apps.map(app => Apps.pack(app, user, { detail: true }))); }); diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts index 835c515cfe..17ba969350 100644 --- a/src/server/api/endpoints/notes.ts +++ b/src/server/api/endpoints/notes.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../misc/cafy-id'; -import Note, { packMany } from '../../../models/note'; +import { ID } from '../../../misc/cafy-id'; import define from '../define'; +import { makePaginationQuery } from '../common/make-pagination-query'; +import { Notes } from '../../../models'; export const meta = { desc: { @@ -39,14 +40,6 @@ export const meta = { } }, - media: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' - } - }, - poll: { validator: $.optional.bool, desc: { @@ -61,12 +54,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -79,43 +70,29 @@ export const meta = { }; export default define(meta, async (ps) => { - const sort = { - _id: -1 - }; - const query = { - deletedAt: null, - visibility: 'public', - localOnly: { $ne: true }, - } as any; - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.visibility = 'public'`) + .andWhere(`note.localOnly = FALSE`) + .leftJoinAndSelect('note.user', 'user'); if (ps.local) { - query['_user.host'] = null; + query.andWhere('note.userHost IS NULL'); } if (ps.reply != undefined) { - query.replyId = ps.reply ? { $exists: true, $ne: null } : null; + query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL'); } if (ps.renote != undefined) { - query.renoteId = ps.renote ? { $exists: true, $ne: null } : null; + query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL'); } - const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media; - - if (withFiles) query.fileIds = { $exists: true, $ne: null }; + if (ps.withFiles != undefined) { + query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`); + } if (ps.poll != undefined) { - query.poll = ps.poll ? { $exists: true, $ne: null } : null; + query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE'); } // TODO @@ -123,10 +100,7 @@ export default define(meta, async (ps) => { // query.isBot = bot; //} - const notes = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const notes = await query.take(ps.limit!).getMany(); - return await packMany(notes); + return await Notes.packMany(notes); }); diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts index 3738459b71..2b4ae2a312 100644 --- a/src/server/api/endpoints/notes/children.ts +++ b/src/server/api/endpoints/notes/children.ts @@ -1,9 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { getFriends } from '../../common/get-friends'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { Brackets } from 'typeorm'; +import { Notes } from '../../../../models'; export const meta = { desc: { @@ -18,7 +20,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID' @@ -32,12 +33,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -50,83 +49,24 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const [followings, hideUserIds] = await Promise.all([ - // フォローを取得 - // Fetch following - user ? getFriends(user._id) : [], + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { qb + .where(`note.replyId = :noteId`, { noteId: ps.noteId }) + .orWhere(new Brackets(qb => { qb + .where(`note.renoteId = :noteId`, { noteId: ps.noteId }) + .andWhere(new Brackets(qb => { qb + .where(`note.text IS NOT NULL`) + .orWhere(`note.fileIds != '{}'`) + .orWhere(`note.hasPoll = TRUE`); + })); + })); + })) + .leftJoinAndSelect('note.user', 'user'); - // 隠すユーザーを取得 - getHideUserIds(user) - ]); + if (user) generateVisibilityQuery(query, user); + if (user) generateMuteQuery(query, user); - const visibleQuery = user == null ? [{ - visibility: { $in: [ 'public', 'home' ] } - }] : [{ - visibility: { $in: [ 'public', 'home' ] } - }, { - // myself (for followers/specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }, { - visibility: 'followers', - $or: [{ - // フォロワーの投稿 - userId: { $in: followings.map(f => f.id) }, - }, { - // 自分の投稿へのリプライ - '_reply.userId': user._id, - }, { - // 自分へのメンションが含まれている - mentions: { $in: [ user._id ] } - }] - }]; + const notes = await query.take(ps.limit!).getMany(); - const q = { - $and: [{ - $or: [{ - replyId: ps.noteId, - }, { - renoteId: ps.noteId, - $or: [{ - text: { $ne: null } - }, { - fileIds: { $ne: [] } - }, { - poll: { $ne: null } - }] - }] - }, { - $or: visibleQuery - }] - } as any; - - if (hideUserIds && hideUserIds.length > 0) { - q['userId'] = { - $nin: hideUserIds - }; - } - - const sort = { - _id: -1 - }; - - if (ps.sinceId) { - sort._id = 1; - q._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - q._id = { - $lt: ps.untilId - }; - } - - const notes = await Note.find(q, { - limit: ps.limit, - sort: sort - }); - - return await packMany(notes, user); + return await Notes.packMany(notes, user); }); diff --git a/src/server/api/endpoints/notes/conversation.ts b/src/server/api/endpoints/notes/conversation.ts index 702d8dc430..6b26e31c07 100644 --- a/src/server/api/endpoints/notes/conversation.ts +++ b/src/server/api/endpoints/notes/conversation.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany, INote } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; import { getNote } from '../../common/getters'; +import { Note } from '../../../../models/entities/note'; +import { Notes } from '../../../../models'; export const meta = { desc: { @@ -18,7 +19,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID' @@ -58,18 +58,19 @@ export default define(meta, async (ps, user) => { throw e; }); - const conversation: INote[] = []; + const conversation: Note[] = []; let i = 0; async function get(id: any) { i++; - const p = await Note.findOne({ _id: id }); + const p = await Notes.findOne(id); + if (p == null) return; - if (i > ps.offset) { + if (i > ps.offset!) { conversation.push(p); } - if (conversation.length == ps.limit) { + if (conversation.length == ps.limit!) { return; } @@ -82,5 +83,5 @@ export default define(meta, async (ps, user) => { await get(note.replyId); } - return await packMany(conversation, user); + return await Notes.packMany(conversation, user); }); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 8cc5e4b815..83649015d8 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -1,14 +1,15 @@ import $ from 'cafy'; -import ID, { transform, transformMany } from '../../../../misc/cafy-id'; import * as ms from 'ms'; import { length } from 'stringz'; -import Note, { INote, isValidCw, pack } from '../../../../models/note'; -import User, { IUser } from '../../../../models/user'; -import DriveFile, { IDriveFile } from '../../../../models/drive-file'; import create from '../../../../services/note/create'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; import { ApiError } from '../../error'; +import { ID } from '../../../../misc/cafy-id'; +import { User } from '../../../../models/entities/user'; +import { Users, DriveFiles, Notes } from '../../../../models'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { Note } from '../../../../models/entities/note'; let maxNoteTextLength = 1000; @@ -34,7 +35,7 @@ export const meta = { max: 300 }, - kind: 'note-write', + kind: 'write:notes', params: { visibility: { @@ -47,7 +48,6 @@ export const meta = { visibleUserIds: { validator: $.optional.arr($.type(ID)).unique().min(0), - transform: transformMany, desc: { 'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー' } @@ -64,7 +64,7 @@ export const meta = { }, cw: { - validator: $.optional.nullable.str.pipe(isValidCw), + validator: $.optional.nullable.str.pipe(Notes.validateCw), desc: { 'ja-JP': 'コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。' } @@ -129,7 +129,6 @@ export const meta = { fileIds: { validator: $.optional.arr($.type(ID)).unique().range(1, 4), - transform: transformMany, desc: { 'ja-JP': '添付するファイル' } @@ -137,7 +136,6 @@ export const meta = { mediaIds: { validator: $.optional.arr($.type(ID)).unique().range(1, 4), - transform: transformMany, deprecated: true, desc: { 'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)' @@ -146,7 +144,6 @@ export const meta = { replyId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '返信対象' } @@ -154,7 +151,6 @@ export const meta = { renoteId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': 'Renote対象' } @@ -227,32 +223,29 @@ export const meta = { }; export default define(meta, async (ps, user, app) => { - let visibleUsers: IUser[] = []; + let visibleUsers: User[] = []; if (ps.visibleUserIds) { - visibleUsers = await Promise.all(ps.visibleUserIds.map(id => User.findOne({ - _id: id - }))); + visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)))) + .filter(x => x != null) as User[]; } - let files: IDriveFile[] = []; + let files: DriveFile[] = []; const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; if (fileIds != null) { - files = await Promise.all(fileIds.map(fileId => { - return DriveFile.findOne({ - _id: fileId, - 'metadata.userId': user._id - }); - })); + files = (await Promise.all(fileIds.map(fileId => + DriveFiles.findOne({ + id: fileId, + userId: user.id + }) + ))).filter(file => file != null) as DriveFile[]; - files = files.filter(file => file != null); + files = files; } - let renote: INote = null; + let renote: Note | undefined; if (ps.renoteId != null) { // Fetch renote to note - renote = await Note.findOne({ - _id: ps.renoteId - }); + renote = await Notes.findOne(ps.renoteId); if (renote == null) { throw new ApiError(meta.errors.noSuchRenoteTarget); @@ -261,14 +254,12 @@ export default define(meta, async (ps, user, app) => { } } - let reply: INote = null; + let reply: Note | undefined; if (ps.replyId != null) { // Fetch reply - reply = await Note.findOne({ - _id: ps.replyId - }); + reply = await Notes.findOne(ps.replyId); - if (reply === null) { + if (reply == null) { throw new ApiError(meta.errors.noSuchReplyTarget); } @@ -279,12 +270,6 @@ export default define(meta, async (ps, user, app) => { } if (ps.poll) { - (ps.poll as any).choices = (ps.poll as any).choices.map((choice: string, i: number) => ({ - id: i, // IDを付与 - text: choice.trim(), - votes: 0 - })); - if (typeof ps.poll.expiresAt === 'number') { if (ps.poll.expiresAt < Date.now()) throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); @@ -298,11 +283,6 @@ export default define(meta, async (ps, user, app) => { throw new ApiError(meta.errors.contentRequired); } - // 後方互換性のため - if (ps.visibility == 'private') { - ps.visibility = 'specified'; - } - // 投稿を作成 const note = await create(user, { createdAt: new Date(), @@ -312,7 +292,7 @@ export default define(meta, async (ps, user, app) => { multiple: ps.poll.multiple || false, expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null } : undefined, - text: ps.text, + text: ps.text || undefined, reply, renote, cw: ps.cw, @@ -328,6 +308,6 @@ export default define(meta, async (ps, user, app) => { }); return { - createdNote: await pack(note, user) + createdNote: await Notes.pack(note, user) }; }); diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts index 399f9288d6..17d44c424d 100644 --- a/src/server/api/endpoints/notes/delete.ts +++ b/src/server/api/endpoints/notes/delete.ts @@ -1,11 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import deleteNote from '../../../../services/note/delete'; -import User from '../../../../models/user'; import define from '../../define'; import * as ms from 'ms'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; +import { Users } from '../../../../models'; +import { ensure } from '../../../../prelude/ensure'; export const meta = { stability: 'stable', @@ -19,7 +20,7 @@ export const meta = { requireCredential: true, - kind: 'note-write', + kind: 'write:notes', limit: { duration: ms('1hour'), @@ -30,7 +31,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID.' @@ -59,9 +59,10 @@ export default define(meta, async (ps, user) => { throw e; }); - if (!user.isAdmin && !user.isModerator && !note.userId.equals(user._id)) { + if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) { throw new ApiError(meta.errors.accessDenied); } - await deleteNote(await User.findOne({ _id: note.userId }), note); + // この操作を行うのが投稿者とは限らない(例えばモデレーター)ため + await deleteNote(await Users.findOne(note.userId).then(ensure), note); }); diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts index 9cde1a7dcf..7e04637758 100644 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Favorite from '../../../../../models/favorite'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getNote } from '../../../common/getters'; +import { NoteFavorites } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; export const meta = { stability: 'stable', @@ -22,7 +23,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID.' @@ -53,21 +53,20 @@ export default define(meta, async (ps, user) => { }); // if already favorited - const exist = await Favorite.findOne({ - noteId: note._id, - userId: user._id + const exist = await NoteFavorites.findOne({ + noteId: note.id, + userId: user.id }); - if (exist !== null) { + if (exist != null) { throw new ApiError(meta.errors.alreadyFavorited); } // Create favorite - await Favorite.insert({ + await NoteFavorites.save({ + id: genId(), createdAt: new Date(), - noteId: note._id, - userId: user._id + noteId: note.id, + userId: user.id }); - - return; }); diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts index e2c787f3b5..a889c84d4d 100644 --- a/src/server/api/endpoints/notes/favorites/delete.ts +++ b/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Favorite from '../../../../../models/favorite'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getNote } from '../../../common/getters'; +import { NoteFavorites } from '../../../../../models'; export const meta = { stability: 'stable', @@ -22,7 +22,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID.' @@ -53,19 +52,15 @@ export default define(meta, async (ps, user) => { }); // if already favorited - const exist = await Favorite.findOne({ - noteId: note._id, - userId: user._id + const exist = await NoteFavorites.findOne({ + noteId: note.id, + userId: user.id }); - if (exist === null) { + if (exist == null) { throw new ApiError(meta.errors.notFavorited); } // Delete favorite - await Favorite.remove({ - _id: exist._id - }); - - return; + await NoteFavorites.delete(exist.id); }); diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts index 3648b307d7..6a76fb8bc6 100644 --- a/src/server/api/endpoints/notes/featured.ts +++ b/src/server/api/endpoints/notes/featured.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; import define from '../../define'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { Notes } from '../../../../models'; export const meta = { desc: { @@ -35,25 +34,15 @@ export const meta = { export default define(meta, async (ps, user) => { const day = 1000 * 60 * 60 * 24 * 3; // 3日前まで - const hideUserIds = await getHideUserIds(user); + const query = Notes.createQueryBuilder('note') + .where('note.userHost IS NULL') + .andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) }) + .andWhere(`note.visibility = 'public'`) + .leftJoinAndSelect('note.user', 'user'); - const notes = await Note.find({ - createdAt: { - $gt: new Date(Date.now() - day) - }, - deletedAt: null, - visibility: 'public', - '_user.host': null, - ...(hideUserIds && hideUserIds.length > 0 ? { userId: { $nin: hideUserIds } } : {}) - }, { - limit: ps.limit, - sort: { - score: -1 - }, - hint: { - score: -1 - } - }); + if (user) generateMuteQuery(query, user); + + const notes = await query.orderBy('note.score', 'DESC').take(ps.limit!).getMany(); - return await packMany(notes, user); + return await Notes.packMany(notes, user); }); diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index 0eb761cdb6..ceffb1cf4a 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -1,11 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notes } from '../../../../models'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { activeUsersChart } from '../../../../services/chart'; export const meta = { desc: { @@ -22,14 +23,6 @@ export const meta = { } }, - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' - } - }, - limit: { validator: $.optional.num.range(1, 100), default: 10 @@ -37,12 +30,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, sinceDate: { @@ -71,6 +62,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { + // TODO どっかにキャッシュ const m = await fetchMeta(); if (m.disableGlobalTimeline) { if (user == null || (!user.isAdmin && !user.isModerator)) { @@ -78,68 +70,25 @@ export default define(meta, async (ps, user) => { } } - // 隠すユーザーを取得 - const hideUserIds = await getHideUserIds(user); - //#region Construct query - const sort = { - _id: -1 - }; - - const query = { - deletedAt: null, - - // public only - visibility: 'public', + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.visibility = \'public\'') + .andWhere('note.replyId IS NULL') + .leftJoinAndSelect('note.user', 'user'); - replyId: null - } as any; + if (user) generateMuteQuery(query, user); - if (hideUserIds && hideUserIds.length > 0) { - query.userId = { - $nin: hideUserIds - }; - - query['_reply.userId'] = { - $nin: hideUserIds - }; - - query['_renote.userId'] = { - $nin: hideUserIds - }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } + //#endregion - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + const timeline = await query.take(ps.limit!).getMany(); - if (withFiles) { - query.fileIds = { $exists: true, $ne: [] }; + if (user) { + activeUsersChart.update(user); } - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; - } - //#endregion - - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); - - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 9695547f04..6dfb143003 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -1,17 +1,18 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { getFriends } from '../../common/get-friends'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; -import activeUsersChart from '../../../../services/chart/active-users'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Followings, Notes } from '../../../../models'; +import { Brackets } from 'typeorm'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { activeUsersChart } from '../../../../services/chart'; export const meta = { desc: { - 'ja-JP': 'ハイブリッドタイムラインを取得します。' + 'ja-JP': 'ソーシャルタイムラインを取得します。' }, tags: ['notes'], @@ -27,17 +28,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します' } }, @@ -85,14 +84,6 @@ export const meta = { 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' } }, - - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' - } - }, }, res: { @@ -104,7 +95,7 @@ export const meta = { errors: { stlDisabled: { - message: 'Social timeline has been disabled.', + message: 'Hybrid timeline has been disabled.', code: 'STL_DISABLED', id: '620763f4-f621-4533-ab33-0577a1a3c342' }, @@ -112,94 +103,30 @@ export const meta = { }; export default define(meta, async (ps, user) => { + // TODO どっかにキャッシュ const m = await fetchMeta(); if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) { throw new ApiError(meta.errors.stlDisabled); } - const [followings, hideUserIds] = await Promise.all([ - // フォローを取得 - // Fetch following - getFriends(user._id, true, false), - - // 隠すユーザーを取得 - getHideUserIds(user) - ]); - //#region Construct query - const sort = { - _id: -1 - }; - - const followQuery = followings.map(f => ({ - userId: f.id, + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: user.id }); - /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) - $or: [{ - // リプライでない - replyId: null - }, { // または - // リプライだが返信先が投稿者自身の投稿 - $expr: { - $eq: ['$_reply.userId', '$userId'] - } - }, { // または - // リプライだが返信先が自分(フォロワー)の投稿 - '_reply.userId': user._id - }, { // または - // 自分(フォロワー)が送信したリプライ - userId: user._id - }]*/ - })); + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere(new Brackets(qb => { + qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }) + .orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)'); + })) + .leftJoinAndSelect('note.user', 'user') + .setParameters(followingQuery.getParameters()); - const visibleQuery = user == null ? [{ - visibility: { $in: ['public', 'home'] } - }] : [{ - visibility: { $in: ['public', 'home', 'followers'] } - }, { - // myself (for specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }]; - - const query = { - $and: [{ - deletedAt: null, - - $or: [{ - $and: [{ - // フォローしている人の投稿 - $or: followQuery - }, { - // visible for me - $or: visibleQuery - }] - }, { - // public only - visibility: 'public', - - // リプライでない - //replyId: null, - - // local - '_user.host': null - }], - - // hide - userId: { - $nin: hideUserIds - }, - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - }, - }] - } as any; + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); + /* TODO // MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。 // つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。 // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws @@ -207,7 +134,7 @@ export default define(meta, async (ps, user) => { if (ps.includeMyRenotes === false) { query.$and.push({ $or: [{ - userId: { $ne: user._id } + userId: { $ne: user.id } }, { renoteId: null }, { @@ -223,7 +150,7 @@ export default define(meta, async (ps, user) => { if (ps.includeRenotedMyNotes === false) { query.$and.push({ $or: [{ - '_renote.userId': { $ne: user._id } + '_renote.userId': { $ne: user.id } }, { renoteId: null }, { @@ -251,40 +178,18 @@ export default define(meta, async (ps, user) => { }] }); } + */ - if (ps.withFiles || ps.mediaOnly) { - query.$and.push({ - fileIds: { $exists: true, $ne: [] } - }); - } - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } //#endregion - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit!).getMany(); - activeUsersChart.update(user); + if (user) { + activeUsersChart.update(user); + } - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index 57ef4c3e15..c10c0d7482 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -1,12 +1,14 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; -import activeUsersChart from '../../../../services/chart/active-users'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; +import { Notes } from '../../../../models'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { activeUsersChart } from '../../../../services/chart'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -23,14 +25,6 @@ export const meta = { } }, - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' - } - }, - fileType: { validator: $.optional.arr($.str), desc: { @@ -53,12 +47,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, sinceDate: { @@ -87,6 +79,7 @@ export const meta = { }; export default define(meta, async (ps, user) => { + // TODO どっかにキャッシュ const m = await fetchMeta(); if (m.disableLocalTimeline) { if (user == null || (!user.isAdmin && !user.isModerator)) { @@ -94,90 +87,44 @@ export default define(meta, async (ps, user) => { } } - // 隠すユーザーを取得 - const hideUserIds = await getHideUserIds(user); - //#region Construct query - const sort = { - _id: -1 - }; - - const query = { - deletedAt: null, - - // public only - visibility: 'public', - - // リプライでない - //replyId: null, + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') + .leftJoinAndSelect('note.user', 'user'); - // local - '_user.host': null - } as any; + if (user) generateVisibilityQuery(query, user); + if (user) generateMuteQuery(query, user); - if (hideUserIds && hideUserIds.length > 0) { - query.userId = { - $nin: hideUserIds - }; - - query['_reply.userId'] = { - $nin: hideUserIds - }; - - query['_renote.userId'] = { - $nin: hideUserIds - }; - } - - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; - - if (withFiles) { - query.fileIds = { $exists: true, $ne: [] }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } - if (ps.fileType) { - query.fileIds = { $exists: true, $ne: [] }; - - query['_files.contentType'] = { - $in: ps.fileType - }; + if (ps.fileType != null) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType!) { + const i = ps.fileType!.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); if (ps.excludeNsfw) { - query['_files.metadata.isSensitive'] = { + // v11 TODO + /* + query['_files.isSensitive'] = { $ne: true }; + */ } } - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; - } //#endregion - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit!).getMany(); if (user) { activeUsersChart.update(user); } - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 91333174ed..b7f614915b 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -1,11 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { getFriendIds, getFriends } from '../../common/get-friends'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import read from '../../../../services/note/read'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { Notes, Followings } from '../../../../models'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -30,12 +31,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, visibility: { @@ -52,97 +51,34 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // フォローを取得 - const followings = await getFriends(user._id); + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: user.id }); - const visibleQuery = [{ - visibility: { $in: [ 'public', 'home' ] } - }, { - // myself (for followers/specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }, { - visibility: 'followers', - $or: [{ - // フォロワーの投稿 - userId: { $in: followings.map(f => f.id) }, - }, { - // 自分の投稿へのリプライ - '_reply.userId': user._id, - }, { - // 自分へのメンションが含まれている - mentions: { $in: [ user._id ] } - }] - }]; + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(new Brackets(qb => { qb + .where(`:meId = ANY(note.mentions)`, { meId: user.id }) + .orWhere(`:meId = ANY(note.visibleUserIds)`, { meId: user.id }); + })) + .leftJoinAndSelect('note.user', 'user'); - const query = { - $and: [{ - deletedAt: null, - }, { - $or: visibleQuery, - }], - - $or: [{ - mentions: user._id - }, { - visibleUserIds: user._id - }] - } as any; - - // 隠すユーザーを取得 - const hideUserIds = await getHideUserIds(user); - - if (hideUserIds && hideUserIds.length > 0) { - query.userId = { - $nin: hideUserIds - }; - - query['_reply.userId'] = { - $nin: hideUserIds - }; - - query['_renote.userId'] = { - $nin: hideUserIds - }; - } - - const sort = { - _id: -1 - }; + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); if (ps.visibility) { - query.visibility = ps.visibility; + query.andWhere('note.visibility = :visibility', { visibility: ps.visibility }); } if (ps.following) { - const followingIds = await getFriendIds(user._id); - - query.userId = { - $in: followingIds - }; - } - - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; + query.andWhere(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id }); + query.setParameters(followingQuery.getParameters()); } - const mentions = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const mentions = await query.take(ps.limit!).getMany(); for (const note of mentions) { - read(user._id, note._id); + read(user.id, note.id); } - return await packMany(mentions, user); + return await Notes.packMany(mentions, user); }); diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts index 9adabdf0e9..cbd4d35cd4 100644 --- a/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/src/server/api/endpoints/notes/polls/recommendation.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; -import Vote from '../../../../../models/poll-vote'; -import Note, { pack } from '../../../../../models/note'; import define from '../../../define'; -import { getHideUserIds } from '../../../common/get-hide-users'; +import { Polls, Mutings, Notes, PollVotes } from '../../../../../models'; +import { Brackets, In } from 'typeorm'; export const meta = { desc: { @@ -28,51 +27,46 @@ export const meta = { }; export default define(meta, async (ps, user) => { - // Get votes - const votes = await Vote.find({ - userId: user._id - }, { - fields: { - _id: false, - noteId: true - } - }); + const query = Polls.createQueryBuilder('poll') + .where('poll.userHost IS NULL') + .andWhere(`poll.userId != :meId`, { meId: user.id }) + .andWhere(`poll.noteVisibility = 'public'`) + .andWhere(new Brackets(qb => { qb + .where('poll.expiresAt IS NULL') + .orWhere('poll.expiresAt > :now', { now: new Date() }); + })); - const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : []; + //#region exclude arleady voted polls + const votedQuery = PollVotes.createQueryBuilder('vote') + .select('vote.noteId') + .where('vote.userId = :meId', { meId: user.id }); - // 隠すユーザーを取得 - const hideUserIds = await getHideUserIds(user); + query + .andWhere(`poll.noteId NOT IN (${ votedQuery.getQuery() })`); - const notes = await Note.find({ - '_user.host': null, - _id: { - $nin: nin - }, - userId: { - $ne: user._id, - $nin: hideUserIds - }, - visibility: 'public', - poll: { - $exists: true, - $ne: null - }, - $or: [{ - 'poll.expiresAt': null - }, { - 'poll.expiresAt': { - $gt: new Date() - } - }], - }, { - limit: ps.limit, - skip: ps.offset, - sort: { - _id: -1 - } + query.setParameters(votedQuery.getParameters()); + //#endregion + + //#region mute + const mutingQuery = Mutings.createQueryBuilder('muting') + .select('muting.muteeId') + .where('muting.muterId = :muterId', { muterId: user.id }); + + query + .andWhere(`poll.userId NOT IN (${ mutingQuery.getQuery() })`); + + query.setParameters(mutingQuery.getParameters()); + //#endregion + + const polls = await query.take(ps.limit!).skip(ps.offset).getMany(); + + if (polls.length === 0) return []; + + const notes = await Notes.find({ + id: In(polls.map(poll => poll.noteId)) }); - return await Promise.all(notes.map(note => pack(note, user, { + return await Notes.packMany(notes, user, { detail: true - }))); + }); }); diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index ed20e0221f..e8b8b66da5 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -1,19 +1,20 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import Vote from '../../../../../models/poll-vote'; -import Note from '../../../../../models/note'; -import Watching from '../../../../../models/note-watching'; +import { ID } from '../../../../../misc/cafy-id'; import watch from '../../../../../services/note/watch'; import { publishNoteStream } from '../../../../../services/stream'; -import notify from '../../../../../services/create-notification'; +import { createNotification } from '../../../../../services/create-notification'; import define from '../../../define'; -import User, { IRemoteUser } from '../../../../../models/user'; import { ApiError } from '../../../error'; import { getNote } from '../../../common/getters'; import { deliver } from '../../../../../queue'; import { renderActivity } from '../../../../../remote/activitypub/renderer'; import renderVote from '../../../../../remote/activitypub/renderer/vote'; import { deliverQuestionUpdate } from '../../../../../services/note/polls/update'; +import { PollVotes, NoteWatchings, Users, Polls, UserProfiles } from '../../../../../models'; +import { Not } from 'typeorm'; +import { IRemoteUser } from '../../../../../models/entities/user'; +import { genId } from '../../../../../misc/gen-id'; +import { ensure } from '../../../../../prelude/ensure'; export const meta = { desc: { @@ -30,7 +31,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID' @@ -84,26 +84,28 @@ export default define(meta, async (ps, user) => { throw e; }); - if (note.poll == null) { + if (!note.hasPoll) { throw new ApiError(meta.errors.noPoll); } - if (note.poll.expiresAt && note.poll.expiresAt < createdAt) { + const poll = await Polls.findOne({ noteId: note.id }).then(ensure); + + if (poll.expiresAt && poll.expiresAt < createdAt) { throw new ApiError(meta.errors.alreadyExpired); } - if (!note.poll.choices.some(x => x.id == ps.choice)) { + if (poll.choices[ps.choice] == null) { throw new ApiError(meta.errors.invalidChoice); } // if already voted - const exist = await Vote.find({ - noteId: note._id, - userId: user._id + const exist = await PollVotes.find({ + noteId: note.id, + userId: user.id }); if (exist.length) { - if (note.poll.multiple) { + if (poll.multiple) { if (exist.some(x => x.choice == ps.choice)) throw new ApiError(meta.errors.alreadyVoted); } else { @@ -112,69 +114,56 @@ export default define(meta, async (ps, user) => { } // Create vote - const vote = await Vote.insert({ + const vote = await PollVotes.save({ + id: genId(), createdAt, - noteId: note._id, - userId: user._id, + noteId: note.id, + userId: user.id, choice: ps.choice }); - const inc: any = {}; - inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == ps.choice)}.votes`] = 1; - // Increment votes count - await Note.update({ _id: note._id }, { - $inc: inc - }); + const index = ps.choice + 1; // In SQL, array index is 1 based + await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - publishNoteStream(note._id, 'pollVoted', { + publishNoteStream(note.id, 'pollVoted', { choice: ps.choice, - userId: user._id.toHexString() + userId: user.id }); // Notify - notify(note.userId, user._id, 'poll_vote', { - noteId: note._id, + createNotification(note.userId, user.id, 'pollVote', { + noteId: note.id, choice: ps.choice }); // Fetch watchers - Watching - .find({ - noteId: note._id, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - for (const watcher of watchers) { - notify(watcher.userId, user._id, 'poll_vote', { - noteId: note._id, - choice: ps.choice - }); - } - }); + NoteWatchings.find({ + noteId: note.id, + userId: Not(user.id), + }).then(watchers => { + for (const watcher of watchers) { + createNotification(watcher.userId, user.id, 'pollVote', { + noteId: note.id, + choice: ps.choice + }); + } + }); + + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); // この投稿をWatchする - if (user.settings.autoWatch !== false) { - watch(user._id, note); + if (profile.autoWatch !== false) { + watch(user.id, note); } // リモート投票の場合リプライ送信 - if (note._user.host != null) { - const pollOwner: IRemoteUser = await User.findOne({ - _id: note.userId - }); + if (note.userHost != null) { + const pollOwner = await Users.findOne(note.userId).then(ensure) as IRemoteUser; - deliver(user, renderActivity(await renderVote(user, vote, note, pollOwner)), pollOwner.inbox); + deliver(user, renderActivity(await renderVote(user, vote, note, poll, pollOwner)), pollOwner.inbox); } // リモートフォロワーにUpdate配信 - deliverQuestionUpdate(note._id); - - return; + deliverQuestionUpdate(note.id); }); diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts index 7d977154f2..bcb0b6d1ec 100644 --- a/src/server/api/endpoints/notes/reactions.ts +++ b/src/server/api/endpoints/notes/reactions.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import NoteReaction, { pack } from '../../../../models/note-reaction'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; +import { NoteReactions } from '../../../../models'; export const meta = { desc: { @@ -18,7 +18,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'The ID of the target note' @@ -37,12 +36,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, }, }, @@ -69,29 +66,17 @@ export default define(meta, async (ps, user) => { }); const query = { - noteId: note._id - } as any; - - const sort = { - _id: -1 + noteId: note.id }; - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const reactions = await NoteReaction.find(query, { - limit: ps.limit, + const reactions = await NoteReactions.find({ + where: query, + take: ps.limit!, skip: ps.offset, - sort: sort + order: { + id: -1 + } }); - return await Promise.all(reactions.map(reaction => pack(reaction, user))); + return await Promise.all(reactions.map(reaction => NoteReactions.pack(reaction, user))); }); diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts index 299ed30278..b6aa4c58f3 100644 --- a/src/server/api/endpoints/notes/reactions/create.ts +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import createReaction from '../../../../../services/note/reaction/create'; import define from '../../../define'; import { getNote } from '../../../common/getters'; @@ -17,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'reaction-write', + kind: 'write:reactions', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿' } diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts index 08442226c5..0bdea58027 100644 --- a/src/server/api/endpoints/notes/reactions/delete.ts +++ b/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import * as ms from 'ms'; import deleteReaction from '../../../../../services/note/reaction/delete'; @@ -16,7 +16,7 @@ export const meta = { requireCredential: true, - kind: 'reaction-write', + kind: 'write:reactions', limit: { duration: ms('1hour'), @@ -27,7 +27,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID' diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts index 15dcf55dce..74a8ea918e 100644 --- a/src/server/api/endpoints/notes/renotes.ts +++ b/src/server/api/endpoints/notes/renotes.ts @@ -1,9 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notes } from '../../../../models'; export const meta = { desc: { @@ -18,7 +21,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID' @@ -32,12 +34,10 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, }, untilId: { validator: $.optional.type(ID), - transform: transform, } }, @@ -63,29 +63,14 @@ export default define(meta, async (ps, user) => { throw e; }); - const sort = { - _id: -1 - }; + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.renoteId = :renoteId`, { renoteId: note.id }) + .leftJoinAndSelect('note.user', 'user'); - const query = { - renoteId: note._id - } as any; + if (user) generateVisibilityQuery(query, user); + if (user) generateMuteQuery(query, user); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } - - const renotes = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const renotes = await query.take(ps.limit!).getMany(); - return await packMany(renotes, user); + return await Notes.packMany(renotes, user); }); diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts index c80fd73205..980ff2446e 100644 --- a/src/server/api/endpoints/notes/replies.ts +++ b/src/server/api/endpoints/notes/replies.ts @@ -1,9 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { getFriends } from '../../common/get-friends'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { Notes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; export const meta = { desc: { @@ -18,22 +19,30 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID' } }, + sinceId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します' + } + }, + + untilId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します' + } + }, + limit: { validator: $.optional.num.range(1, 100), default: 10 }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, }, res: { @@ -45,54 +54,14 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const [followings, hideUserIds] = await Promise.all([ - // フォローを取得 - // Fetch following - user ? getFriends(user._id) : [], - - // 隠すユーザーを取得 - getHideUserIds(user) - ]); - - const visibleQuery = user == null ? [{ - visibility: { $in: [ 'public', 'home' ] } - }] : [{ - visibility: { $in: [ 'public', 'home' ] } - }, { - // myself (for followers/specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }, { - visibility: 'followers', - $or: [{ - // フォロワーの投稿 - userId: { $in: followings.map(f => f.id) }, - }, { - // 自分の投稿へのリプライ - '_reply.userId': user._id, - }, { - // 自分へのメンションが含まれている - mentions: { $in: [ user._id ] } - }] - }]; - - const q = { - replyId: ps.noteId, - $or: visibleQuery - } as any; + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.replyId = :replyId', { replyId: ps.noteId }) + .leftJoinAndSelect('note.user', 'user'); - if (hideUserIds && hideUserIds.length > 0) { - q['userId'] = { - $nin: hideUserIds - }; - } + if (user) generateVisibilityQuery(query, user); + if (user) generateMuteQuery(query, user); - const notes = await Note.find(q, { - limit: ps.limit, - skip: ps.offset - }); + const timeline = await query.take(ps.limit!).getMany(); - return await packMany(notes, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts index b33c884049..cba3724b6f 100644 --- a/src/server/api/endpoints/notes/search-by-tag.ts +++ b/src/server/api/endpoints/notes/search-by-tag.ts @@ -1,10 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { getFriendIds } from '../../common/get-friends'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notes } from '../../../../models'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -28,16 +29,6 @@ export const meta = { } }, - following: { - validator: $.optional.nullable.bool, - default: null as any - }, - - mute: { - validator: $.optional.str, - default: 'mute_all' - }, - reply: { validator: $.optional.nullable.bool, default: null as any, @@ -61,44 +52,28 @@ export const meta = { } }, - media: { + poll: { validator: $.optional.nullable.bool, default: null as any, - deprecated: true, desc: { - 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + 'ja-JP': 'アンケートが添付された投稿に限定するか否か' } }, - poll: { - validator: $.optional.nullable.bool, - default: null as any, + sinceId: { + validator: $.optional.type(ID), desc: { - 'ja-JP': 'アンケートが添付された投稿に限定するか否か' + 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します' } }, - sinceDate: { - validator: $.optional.num, - }, - - untilDate: { - validator: $.optional.num, - }, - - offset: { - validator: $.optional.num.min(0), - default: 0 - }, - limit: { validator: $.optional.num.range(1, 30), default: 10 @@ -114,226 +89,58 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const visibleQuery = me == null ? [{ - visibility: { $in: [ 'public', 'home' ] } - }] : [{ - visibility: { $in: [ 'public', 'home' ] } - }, { - // myself (for specified/private) - userId: me._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ me._id ] } - }]; - - const q: any = { - $and: [ps.tag ? { - tagsLower: ps.tag.toLowerCase() - } : { - $or: ps.query.map(tags => ({ - $and: tags.map(t => ({ - tagsLower: t.toLowerCase() - })) - })) - }], - deletedAt: { $exists: false }, - $or: visibleQuery - }; - - const push = (x: any) => q.$and.push(x); + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .leftJoinAndSelect('note.user', 'user'); - if (ps.following != null && me != null) { - const ids = await getFriendIds(me._id, false); - push({ - userId: ps.following ? { - $in: ids - } : { - $nin: ids.concat(me._id) - } - }); - } + if (me) generateVisibilityQuery(query, me); + if (me) generateMuteQuery(query, me); - if (me != null) { - const hideUserIds = await getHideUserIds(me); - - switch (ps.mute) { - case 'mute_all': - push({ - userId: { - $nin: hideUserIds - }, - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - } - }); - break; - case 'mute_related': - push({ - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - } - }); - break; - case 'mute_direct': - push({ - userId: { - $nin: hideUserIds + if (ps.tag) { + query.andWhere(':tag = ANY(note.tags)', { tag: ps.tag }); + } else { + let i = 0; + query.andWhere(new Brackets(qb => { + for (const tags of ps.query!) { + qb.orWhere(new Brackets(qb => { + for (const tag of tags) { + qb.andWhere(`:tag${i} = ANY(note.tags)`, { [`tag${i}`]: tag }); + i++; } - }); - break; - case 'direct_only': - push({ - userId: { - $in: hideUserIds - } - }); - break; - case 'related_only': - push({ - $or: [{ - '_reply.userId': { - $in: hideUserIds - } - }, { - '_renote.userId': { - $in: hideUserIds - } - }] - }); - break; - case 'all_only': - push({ - $or: [{ - userId: { - $in: hideUserIds - } - }, { - '_reply.userId': { - $in: hideUserIds - } - }, { - '_renote.userId': { - $in: hideUserIds - } - }] - }); - break; - } + })); + } + })); } if (ps.reply != null) { if (ps.reply) { - push({ - replyId: { - $exists: true, - $ne: null - } - }); + query.andWhere('note.replyId IS NOT NULL'); } else { - push({ - $or: [{ - replyId: { - $exists: false - } - }, { - replyId: null - }] - }); + query.andWhere('note.replyId IS NULL'); } } if (ps.renote != null) { if (ps.renote) { - push({ - renoteId: { - $exists: true, - $ne: null - } - }); + query.andWhere('note.renoteId IS NOT NULL'); } else { - push({ - $or: [{ - renoteId: { - $exists: false - } - }, { - renoteId: null - }] - }); + query.andWhere('note.renoteId IS NULL'); } } - const withFiles = ps.withFiles != null ? ps.withFiles : ps.media; - - if (withFiles) { - push({ - fileIds: { $exists: true, $ne: [] } - }); + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } if (ps.poll != null) { if (ps.poll) { - push({ - poll: { - $exists: true, - $ne: null - } - }); + query.andWhere('note.hasPoll = TRUE'); } else { - push({ - $or: [{ - poll: { - $exists: false - } - }, { - poll: null - }] - }); + query.andWhere('note.hasPoll = FALSE'); } } - if (ps.untilId) { - push({ - _id: { - $lt: ps.untilId - } - }); - } - - if (ps.sinceDate) { - push({ - createdAt: { - $gt: new Date(ps.sinceDate) - } - }); - } - - if (ps.untilDate) { - push({ - createdAt: { - $lt: new Date(ps.untilDate) - } - }); - } - - if (q.$and.length == 0) { - delete q.$and; - } - // Search notes - const notes = await Note.find(q, { - sort: { - _id: -1 - }, - limit: ps.limit, - skip: ps.offset - }); + const notes = await query.take(ps.limit!).getMany(); - return await packMany(notes, me); + return await Notes.packMany(notes, me); }); diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts index edc8a14560..4d5ac6fbe0 100644 --- a/src/server/api/endpoints/notes/search.ts +++ b/src/server/api/endpoints/notes/search.ts @@ -1,10 +1,9 @@ import $ from 'cafy'; -import * as mongo from 'mongodb'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; import es from '../../../../db/elasticsearch'; import define from '../../define'; import { ApiError } from '../../error'; +import { Notes } from '../../../../models'; +import { In } from 'typeorm'; export const meta = { desc: { @@ -55,7 +54,7 @@ export default define(meta, async (ps, me) => { index: 'misskey', type: 'note', body: { - size: ps.limit, + size: ps.limit!, from: ps.offset, query: { simple_query_string: { @@ -74,18 +73,19 @@ export default define(meta, async (ps, me) => { return []; } - const hits = response.hits.hits.map(hit => new mongo.ObjectID(hit._id)); + const hits = response.hits.hits.map((hit: any) => hit.id); + + if (hits.length === 0) return []; // Fetch found notes - const notes = await Note.find({ - _id: { - $in: hits - } - }, { - sort: { - _id: -1 + const notes = await Notes.find({ + where: { + id: In(hits) + }, + order: { + id: -1 } }); - return await packMany(notes, me); + return await Notes.packMany(notes, me); }); diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts index 6d8dc73ff2..d41dc20c54 100644 --- a/src/server/api/endpoints/notes/show.ts +++ b/src/server/api/endpoints/notes/show.ts @@ -1,9 +1,9 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import { pack } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { getNote } from '../../common/getters'; import { ApiError } from '../../error'; +import { Notes } from '../../../../models'; export const meta = { stability: 'stable', @@ -20,7 +20,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID.' @@ -47,7 +46,7 @@ export default define(meta, async (ps, user) => { throw e; }); - return await pack(note, user, { + return await Notes.pack(note, user, { detail: true }); }); diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts index 4944802849..df1d9d9fb0 100644 --- a/src/server/api/endpoints/notes/state.ts +++ b/src/server/api/endpoints/notes/state.ts @@ -1,8 +1,7 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import Favorite from '../../../../models/favorite'; -import NoteWatching from '../../../../models/note-watching'; +import { NoteFavorites, NoteWatchings } from '../../../../models'; export const meta = { stability: 'stable', @@ -19,7 +18,6 @@ export const meta = { params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID.' @@ -30,17 +28,19 @@ export const meta = { export default define(meta, async (ps, user) => { const [favorite, watching] = await Promise.all([ - Favorite.count({ - userId: user._id, + NoteFavorites.count({ + where: { + userId: user.id, noteId: ps.noteId - }, { - limit: 1 + }, + take: 1 }), - NoteWatching.count({ - userId: user._id, + NoteWatchings.count({ + where: { + userId: user.id, noteId: ps.noteId - }, { - limit: 1 + }, + take: 1 }) ]); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 6ff7690c74..c27f3df1b7 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -1,11 +1,12 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { getFriends } from '../../common/get-friends'; -import { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import activeUsersChart from '../../../../services/chart/active-users'; -import { getHideUserIds } from '../../common/get-hide-users'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { Notes, Followings } from '../../../../models'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { activeUsersChart } from '../../../../services/chart'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -28,17 +29,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します' } }, @@ -86,14 +85,6 @@ export const meta = { 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' } }, - - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' - } - }, }, res: { @@ -105,78 +96,24 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const [followings, hideUserIds] = await Promise.all([ - // フォローを取得 - // Fetch following - getFriends(user._id), - - // 隠すユーザーを取得 - getHideUserIds(user) - ]); - //#region Construct query - const sort = { - _id: -1 - }; - - const followQuery = followings.map(f => ({ - userId: f.id, - - /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) - $or: [{ - // リプライでない - replyId: null - }, { // または - // リプライだが返信先が投稿者自身の投稿 - $expr: { - $eq: ['$_reply.userId', '$userId'] - } - }, { // または - // リプライだが返信先が自分(フォロワー)の投稿 - '_reply.userId': user._id - }, { // または - // 自分(フォロワー)が送信したリプライ - userId: user._id - }]*/ - })); + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: user.id }); - const visibleQuery = user == null ? [{ - visibility: { $in: [ 'public', 'home' ] } - }] : [{ - visibility: { $in: [ 'public', 'home', 'followers' ] } - }, { - // myself (for specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ user._id ] } - }]; + const query = makePaginationQuery(Notes.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere(new Brackets(qb => { qb + .where(`note.userId IN (${ followingQuery.getQuery() })`) + .orWhere('note.userId = :meId', { meId: user.id }); + })) + .leftJoinAndSelect('note.user', 'user') + .setParameters(followingQuery.getParameters()); - const query = { - $and: [{ - deletedAt: null, - - $and: [{ - // フォローしている人の投稿 - $or: followQuery - }, { - // visible for me - $or: visibleQuery - }], - - // mute - userId: { - $nin: hideUserIds - }, - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - }, - }] - } as any; + generateVisibilityQuery(query, user); + generateMuteQuery(query, user); + /* v11 TODO // MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。 // つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。 // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws @@ -184,7 +121,7 @@ export default define(meta, async (ps, user) => { if (ps.includeMyRenotes === false) { query.$and.push({ $or: [{ - userId: { $ne: user._id } + userId: { $ne: user.id } }, { renoteId: null }, { @@ -200,7 +137,7 @@ export default define(meta, async (ps, user) => { if (ps.includeRenotedMyNotes === false) { query.$and.push({ $or: [{ - '_renote.userId': { $ne: user._id } + '_renote.userId': { $ne: user.id } }, { renoteId: null }, { @@ -227,43 +164,16 @@ export default define(meta, async (ps, user) => { poll: { $ne: null } }] }); - } - - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; - - if (withFiles) { - query.$and.push({ - fileIds: { $exists: true, $ne: [] } - }); - } + }*/ - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } //#endregion - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit!).getMany(); activeUsersChart.update(user); - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts index 17c24ab119..05f171af8b 100644 --- a/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/src/server/api/endpoints/notes/user-list-timeline.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { packMany } from '../../../../models/note'; -import UserList from '../../../../models/user-list'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { getFriends } from '../../common/get-friends'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; +import { UserLists, UserListJoinings, Notes } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { activeUsersChart } from '../../../../services/chart'; export const meta = { desc: { @@ -21,7 +20,6 @@ export const meta = { params: { listId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': 'リストのID' } @@ -37,17 +35,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します' } }, @@ -95,14 +91,6 @@ export const meta = { 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' } }, - - mediaOnly: { - validator: $.optional.bool, - deprecated: true, - desc: { - 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' - } - }, }, res: { @@ -122,94 +110,28 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const [list, followings, hideUserIds] = await Promise.all([ - // リストを取得 - // Fetch the list - UserList.findOne({ - _id: ps.listId, - userId: user._id - }), - - // フォローを取得 - // Fetch following - getFriends(user._id, true, false), - - // 隠すユーザーを取得 - getHideUserIds(user) - ]); + const list = await UserLists.findOne({ + id: ps.listId, + userId: user.id + }); if (list == null) { throw new ApiError(meta.errors.noSuchList); } - if (list.userIds.length == 0) { - return []; - } - //#region Construct query - const sort = { - _id: -1 - }; - - const listQuery = list.userIds.map(u => ({ - userId: u, - - /*// リプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) - $or: [{ - // リプライでない - replyId: null - }, { // または - // リプライだが返信先が投稿者自身の投稿 - $expr: { - $eq: ['$_reply.userId', '$userId'] - } - }, { // または - // リプライだが返信先が自分(フォロワー)の投稿 - '_reply.userId': user._id - }, { // または - // 自分(フォロワー)が送信したリプライ - userId: user._id - }]*/ - })); - - const visibleQuery = [{ - visibility: { $in: ['public', 'home'] } - }, { - // myself (for specified/private) - userId: user._id - }, { - // to me (for specified) - visibleUserIds: { $in: [user._id] } - }, { - visibility: 'followers', - userId: { $in: followings.map(f => f.id) } - }]; - - const query = { - $and: [{ - deletedAt: null, + const listQuery = UserListJoinings.createQueryBuilder('joining') + .select('joining.userId') + .where('joining.userListId = :userListId', { userListId: list.id }); - $and: [{ - // リストに入っている人のタイムラインへの投稿 - $or: listQuery - }, { - // visible for me - $or: visibleQuery - }], + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere(`note.userId IN (${ listQuery.getQuery() })`) + .leftJoinAndSelect('note.user', 'user') + .setParameters(listQuery.getParameters()); - // mute - userId: { - $nin: hideUserIds - }, - '_reply.userId': { - $nin: hideUserIds - }, - '_renote.userId': { - $nin: hideUserIds - }, - }] - } as any; + generateVisibilityQuery(query, user); + /* TODO // MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。 // つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。 // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws @@ -217,7 +139,7 @@ export default define(meta, async (ps, user) => { if (ps.includeMyRenotes === false) { query.$and.push({ $or: [{ - userId: { $ne: user._id } + userId: { $ne: user.id } }, { renoteId: null }, { @@ -233,7 +155,7 @@ export default define(meta, async (ps, user) => { if (ps.includeRenotedMyNotes === false) { query.$and.push({ $or: [{ - '_renote.userId': { $ne: user._id } + '_renote.userId': { $ne: user.id } }, { renoteId: null }, { @@ -260,41 +182,16 @@ export default define(meta, async (ps, user) => { poll: { $ne: null } }] }); - } - - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; - - if (withFiles) { - query.$and.push({ - fileIds: { $exists: true, $ne: [] } - }); - } + }*/ - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort._id = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - query.createdAt = { - $lt: new Date(ps.untilDate) - }; + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); } //#endregion - const timeline = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit!).getMany(); + + activeUsersChart.update(user); - return await packMany(timeline, user); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/notes/watching/create.ts b/src/server/api/endpoints/notes/watching/create.ts index 2b2de1bd3b..b4045fe93c 100644 --- a/src/server/api/endpoints/notes/watching/create.ts +++ b/src/server/api/endpoints/notes/watching/create.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import watch from '../../../../../services/note/watch'; import { getNote } from '../../../common/getters'; @@ -17,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID.' @@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => { throw e; }); - await watch(user._id, note); + await watch(user.id, note); }); diff --git a/src/server/api/endpoints/notes/watching/delete.ts b/src/server/api/endpoints/notes/watching/delete.ts index 512db793ea..a272ecc37d 100644 --- a/src/server/api/endpoints/notes/watching/delete.ts +++ b/src/server/api/endpoints/notes/watching/delete.ts @@ -1,5 +1,5 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import unwatch from '../../../../../services/note/unwatch'; import { getNote } from '../../../common/getters'; @@ -17,12 +17,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { noteId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象の投稿のID', 'en-US': 'Target note ID.' @@ -45,5 +44,5 @@ export default define(meta, async (ps, user) => { throw e; }); - await unwatch(user._id, note); + await unwatch(user.id, note); }); diff --git a/src/server/api/endpoints/notifications/mark-all-as-read.ts b/src/server/api/endpoints/notifications/mark-all-as-read.ts index e5df648285..9f34a32e80 100644 --- a/src/server/api/endpoints/notifications/mark-all-as-read.ts +++ b/src/server/api/endpoints/notifications/mark-all-as-read.ts @@ -1,7 +1,6 @@ -import Notification from '../../../../models/notification'; import { publishMainStream } from '../../../../services/stream'; -import User from '../../../../models/user'; import define from '../../define'; +import { Notifications } from '../../../../models'; export const meta = { desc: { @@ -13,29 +12,18 @@ export const meta = { requireCredential: true, - kind: 'notification-write' + kind: 'write:notifications' }; export default define(meta, async (ps, user) => { // Update documents - await Notification.update({ - notifieeId: user._id, - isRead: false + await Notifications.update({ + notifieeId: user.id, + isRead: false, }, { - $set: { - isRead: true - } - }, { - multi: true - }); - - // Update flag - User.update({ _id: user._id }, { - $set: { - hasUnreadNotification: false - } + isRead: true }); // 全ての通知を読みましたよというイベントを発行 - publishMainStream(user._id, 'readAllNotifications'); + publishMainStream(user.id, 'readAllNotifications'); }); diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts index 30c49cdd86..f3ebaa16ad 100644 --- a/src/server/api/endpoints/stats.ts +++ b/src/server/api/endpoints/stats.ts @@ -1,7 +1,6 @@ import define from '../define'; -import driveChart from '../../../services/chart/drive'; -import federationChart from '../../../services/chart/federation'; -import fetchMeta from '../../../misc/fetch-meta'; +import { Notes, Users } from '../../../models'; +import { federationChart, driveChart } from '../../../services/chart'; export const meta = { requireCredential: false, @@ -43,16 +42,17 @@ export const meta = { }; export default define(meta, async () => { - const instance = await fetchMeta(); + const [notesCount, originalNotesCount, usersCount, originalUsersCount, instances, driveUsageLocal, driveUsageRemote] = await Promise.all([ + Notes.count(), + Notes.count({ userHost: null }), + Users.count(), + Users.count({ host: null }), + federationChart.getChart('hour', 1).then(chart => chart.instance.total[0]), + driveChart.getChart('hour', 1).then(chart => chart.local.totalSize[0]), + driveChart.getChart('hour', 1).then(chart => chart.remote.totalSize[0]), + ]); - const stats: any = instance.stats; - - const driveStats = await driveChart.getChart('hour', 1); - stats.driveUsageLocal = driveStats.local.totalSize[0]; - stats.driveUsageRemote = driveStats.remote.totalSize[0]; - - const federationStats = await federationChart.getChart('hour', 1); - stats.instances = federationStats.instance.total[0]; - - return stats; + return { + notesCount, originalNotesCount, usersCount, originalUsersCount, instances, driveUsageLocal, driveUsageRemote + }; }); diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index 0b81b06abe..cb0572aa90 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -1,7 +1,8 @@ import $ from 'cafy'; -import Subscription from '../../../../models/sw-subscription'; import define from '../../define'; import fetchMeta from '../../../../misc/fetch-meta'; +import { genId } from '../../../../misc/gen-id'; +import { SwSubscriptions } from '../../../../models'; export const meta = { tags: ['account'], @@ -25,12 +26,11 @@ export const meta = { export default define(meta, async (ps, user) => { // if already subscribed - const exist = await Subscription.findOne({ - userId: user._id, + const exist = await SwSubscriptions.findOne({ + userId: user.id, endpoint: ps.endpoint, auth: ps.auth, publickey: ps.publickey, - deletedAt: { $exists: false } }); const instance = await fetchMeta(); @@ -42,8 +42,9 @@ export default define(meta, async (ps, user) => { }; } - await Subscription.insert({ - userId: user._id, + await SwSubscriptions.save({ + id: genId(), + userId: user.id, endpoint: ps.endpoint, auth: ps.auth, publickey: ps.publickey diff --git a/src/server/api/endpoints/username/available.ts b/src/server/api/endpoints/username/available.ts index 1d098eb399..42ab176652 100644 --- a/src/server/api/endpoints/username/available.ts +++ b/src/server/api/endpoints/username/available.ts @@ -1,7 +1,6 @@ import $ from 'cafy'; -import User from '../../../../models/user'; -import { validateUsername } from '../../../../models/user'; import define from '../../define'; +import { Users } from '../../../../models'; export const meta = { tags: ['users'], @@ -10,18 +9,16 @@ export const meta = { params: { username: { - validator: $.str.pipe(validateUsername) + validator: $.str.pipe(Users.validateUsername) } } }; export default define(meta, async (ps) => { // Get exist - const exist = await User.count({ + const exist = await Users.count({ host: null, usernameLower: ps.username.toLowerCase() - }, { - limit: 1 }); return { diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts index be83dcd9cc..18af0a2685 100644 --- a/src/server/api/endpoints/users.ts +++ b/src/server/api/endpoints/users.ts @@ -1,10 +1,7 @@ import $ from 'cafy'; -import User, { pack } from '../../../models/user'; import define from '../define'; -import { fallback } from '../../../prelude/symbol'; -import { getHideUserIds } from '../common/get-hide-users'; - -const nonnull = { $ne: null as any }; +import { Users } from '../../../models'; +import { generateMuteQueryForUsers } from '../common/generate-mute-query'; export const meta = { tags: ['users'], @@ -63,53 +60,38 @@ export const meta = { }, }; -const state: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - 'admin': { isAdmin: true }, - 'moderator': { isModerator: true }, - 'adminOrModerator': { - $or: [ - { isAdmin: true }, - { isModerator: true } - ] - }, - 'verified': { isVerified: true }, - 'alive': { - updatedAt: { $gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) } - }, - [fallback]: {} -}; +export default define(meta, async (ps, me) => { + const query = Users.createQueryBuilder('user'); -const origin: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - 'local': { host: null }, - 'remote': { host: nonnull }, - [fallback]: {} -}; + switch (ps.state) { + case 'admin': query.where('user.isAdmin = TRUE'); break; + case 'moderator': query.where('user.isModerator = TRUE'); break; + case 'adminOrModerator': query.where('user.isAdmin = TRUE OR isModerator = TRUE'); break; + case 'verified': query.where('user.isVerified = TRUE'); break; + case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break; + } -const sort: any = { // < https://github.com/Microsoft/TypeScript/issues/1863 - '+follower': { followersCount: -1 }, - '-follower': { followersCount: 1 }, - '+createdAt': { createdAt: -1 }, - '-createdAt': { createdAt: 1 }, - '+updatedAt': { updatedAt: -1 }, - '-updatedAt': { updatedAt: 1 }, - [fallback]: { _id: -1 } -}; + switch (ps.origin) { + case 'local': query.andWhere('user.host IS NULL'); break; + case 'remote': query.andWhere('user.host IS NOT NULL'); break; + } -export default define(meta, async (ps, me) => { - const hideUserIds = await getHideUserIds(me); + switch (ps.sort) { + case '+follower': query.orderBy('user.followersCount', 'DESC'); break; + case '-follower': query.orderBy('user.followersCount', 'ASC'); break; + case '+createdAt': query.orderBy('user.createdAt', 'DESC'); break; + case '-createdAt': query.orderBy('user.createdAt', 'ASC'); break; + case '+updatedAt': query.orderBy('user.updatedAt', 'DESC'); break; + case '-updatedAt': query.orderBy('user.updatedAt', 'ASC'); break; + default: query.orderBy('user.id', 'ASC'); break; + } + + if (me) generateMuteQueryForUsers(query, me); + + query.take(ps.limit!); + query.skip(ps.offset); - const users = await User - .find({ - $and: [ - state[ps.state] || state[fallback], - origin[ps.origin] || origin[fallback] - ], - ...(hideUserIds && hideUserIds.length > 0 ? { _id: { $nin: hideUserIds } } : {}) - }, { - limit: ps.limit, - sort: sort[ps.sort] || sort[fallback], - skip: ps.offset - }); + const users = await query.getMany(); - return await Promise.all(users.map(user => pack(user, me, { detail: true }))); + return await Users.packMany(users, me, { detail: true }); }); diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts index 3c8290a8b1..0cb68353ca 100644 --- a/src/server/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -1,11 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import User from '../../../../models/user'; -import Following from '../../../../models/following'; -import { pack } from '../../../../models/user'; -import { getFriendIds } from '../../common/get-friends'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; +import { Users, Followings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { toPunyNullable } from '../../../../misc/convert-host'; export const meta = { desc: { @@ -20,7 +19,6 @@ export const meta = { params: { userId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -35,38 +33,25 @@ export const meta = { validator: $.optional.nullable.str }, - limit: { - validator: $.optional.num.range(1, 100), - default: 10 + sinceId: { + validator: $.optional.type(ID), }, - cursor: { + untilId: { validator: $.optional.type(ID), - default: null as any, - transform: transform, }, - iknow: { - validator: $.optional.bool, - default: false, - } + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, }, res: { - type: 'object', - properties: { - users: { - type: 'array', - items: { - type: 'User', - } - }, - next: { - type: 'string', - format: 'id', - nullable: true - } - } + type: 'array', + items: { + type: 'Following', + }, }, errors: { @@ -79,54 +64,20 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const q: any = ps.userId != null - ? { _id: ps.userId } - : { usernameLower: ps.username.toLowerCase(), host: ps.host }; - - const user = await User.findOne(q); + const user = await Users.findOne(ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) }); - if (user === null) { + if (user == null) { throw new ApiError(meta.errors.noSuchUser); } - const query = { - followeeId: user._id - } as any; - - // ログインしていてかつ iknow フラグがあるとき - if (me && ps.iknow) { - // Get my friends - const myFriends = await getFriendIds(me._id); - - query.followerId = { - $in: myFriends - }; - } - - // カーソルが指定されている場合 - if (ps.cursor) { - query._id = { - $lt: ps.cursor - }; - } - - // Get followers - const following = await Following - .find(query, { - limit: ps.limit + 1, - sort: { _id: -1 } - }); - - // 「次のページ」があるかどうか - const inStock = following.length === ps.limit + 1; - if (inStock) { - following.pop(); - } + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followeeId = :userId`, { userId: user.id }); - const users = await Promise.all(following.map(f => pack(f.followerId, me, { detail: true }))); + const followings = await query + .take(ps.limit!) + .getMany(); - return { - users: users, - next: inStock ? following[following.length - 1]._id : null, - }; + return await Followings.packMany(followings, me, { populateFollower: true }); }); diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts index 4bc740cad9..2e273dc0c2 100644 --- a/src/server/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -1,11 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import User from '../../../../models/user'; -import Following from '../../../../models/following'; -import { pack } from '../../../../models/user'; -import { getFriendIds } from '../../common/get-friends'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { ApiError } from '../../error'; +import { Users, Followings } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { toPunyNullable } from '../../../../misc/convert-host'; export const meta = { desc: { @@ -20,7 +19,6 @@ export const meta = { params: { userId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -35,38 +33,25 @@ export const meta = { validator: $.optional.nullable.str }, - limit: { - validator: $.optional.num.range(1, 100), - default: 10 + sinceId: { + validator: $.optional.type(ID), }, - cursor: { + untilId: { validator: $.optional.type(ID), - default: null as any, - transform: transform, }, - iknow: { - validator: $.optional.bool, - default: false, - } + limit: { + validator: $.optional.num.range(1, 100), + default: 10 + }, }, res: { - type: 'object', - properties: { - users: { - type: 'array', - items: { - type: 'User', - } - }, - next: { - type: 'string', - format: 'id', - nullable: true - } - } + type: 'array', + items: { + type: 'Following', + }, }, errors: { @@ -79,54 +64,20 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const q: any = ps.userId != null - ? { _id: ps.userId } - : { usernameLower: ps.username.toLowerCase(), host: ps.host }; - - const user = await User.findOne(q); + const user = await Users.findOne(ps.userId != null + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: toPunyNullable(ps.host) }); - if (user === null) { + if (user == null) { throw new ApiError(meta.errors.noSuchUser); } - const query = { - followerId: user._id - } as any; - - // ログインしていてかつ iknow フラグがあるとき - if (me && ps.iknow) { - // Get my friends - const myFriends = await getFriendIds(me._id); - - query.followeeId = { - $in: myFriends - }; - } - - // カーソルが指定されている場合 - if (ps.cursor) { - query._id = { - $lt: ps.cursor - }; - } - - // Get followers - const following = await Following - .find(query, { - limit: ps.limit + 1, - sort: { _id: -1 } - }); - - // 「次のページ」があるかどうか - const inStock = following.length === ps.limit + 1; - if (inStock) { - following.pop(); - } + const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId) + .andWhere(`following.followerId = :userId`, { userId: user.id }); - const users = await Promise.all(following.map(f => pack(f.followeeId, me, { detail: true }))); + const followings = await query + .take(ps.limit!) + .getMany(); - return { - users: users, - next: inStock ? following[following.length - 1]._id : null, - }; + return await Followings.packMany(followings, me, { populateFollowee: true }); }); diff --git a/src/server/api/endpoints/users/get-frequently-replied-users.ts b/src/server/api/endpoints/users/get-frequently-replied-users.ts index 46c7fba2f6..a1d140c6c9 100644 --- a/src/server/api/endpoints/users/get-frequently-replied-users.ts +++ b/src/server/api/endpoints/users/get-frequently-replied-users.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note from '../../../../models/note'; -import { pack } from '../../../../models/user'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; import { maximum } from '../../../../prelude/array'; -import { getHideUserIds } from '../../common/get-hide-users'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { Not, In } from 'typeorm'; +import { Notes, Users } from '../../../../models'; export const meta = { tags: ['users'], @@ -16,7 +15,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -53,21 +51,16 @@ export default define(meta, async (ps, me) => { }); // Fetch recent notes - const recentNotes = await Note.find({ - userId: user._id, - replyId: { - $exists: true, - $ne: null - } - }, { - sort: { - _id: -1 + const recentNotes = await Notes.find({ + where: { + userId: user.id, + replyId: Not(null) }, - limit: 1000, - fields: { - _id: false, - replyId: true - } + order: { + id: -1 + }, + take: 1000, + select: ['replyId'] }); // 投稿が少なかったら中断 @@ -75,21 +68,12 @@ export default define(meta, async (ps, me) => { return []; } - const hideUserIds = await getHideUserIds(me); - hideUserIds.push(user._id); - - const replyTargetNotes = await Note.find({ - _id: { - $in: recentNotes.map(p => p.replyId) + // TODO ミュートを考慮 + const replyTargetNotes = await Notes.find({ + where: { + id: In(recentNotes.map(p => p.replyId)), }, - userId: { - $nin: hideUserIds - } - }, { - fields: { - _id: false, - userId: true - } + select: ['userId'] }); const repliedUsers: any = {}; @@ -110,11 +94,11 @@ export default define(meta, async (ps, me) => { const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]); // Extract top replied users - const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit); + const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit!); // Make replies object (includes weights) const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ - user: await pack(user, me, { detail: true }), + user: await Users.pack(user, me, { detail: true }), weight: repliedUsers[user] / peak }))); diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts index 00d2538c9f..21dc6d331d 100644 --- a/src/server/api/endpoints/users/lists/create.ts +++ b/src/server/api/endpoints/users/lists/create.ts @@ -1,6 +1,8 @@ import $ from 'cafy'; -import UserList, { pack } from '../../../../../models/user-list'; import define from '../../../define'; +import { UserLists } from '../../../../../models'; +import { genId } from '../../../../../misc/gen-id'; +import { UserList } from '../../../../../models/entities/user-list'; export const meta = { desc: { @@ -12,7 +14,7 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { title: { @@ -22,12 +24,12 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const userList = await UserList.insert({ + const userList = await UserLists.save({ + id: genId(), createdAt: new Date(), - userId: user._id, - title: ps.title, - userIds: [] - }); + userId: user.id, + name: ps.title, + } as UserList); - return await pack(userList); + return await UserLists.pack(userList); }); diff --git a/src/server/api/endpoints/users/lists/delete.ts b/src/server/api/endpoints/users/lists/delete.ts index d8faaa928c..0634bca4e3 100644 --- a/src/server/api/endpoints/users/lists/delete.ts +++ b/src/server/api/endpoints/users/lists/delete.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList from '../../../../../models/user-list'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { UserLists } from '../../../../../models'; export const meta = { desc: { @@ -14,12 +14,11 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { listId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象となるユーザーリストのID', 'en-US': 'ID of target user list' @@ -37,16 +36,14 @@ export const meta = { }; export default define(meta, async (ps, user) => { - const userList = await UserList.findOne({ - _id: ps.listId, - userId: user._id + const userList = await UserLists.findOne({ + id: ps.listId, + userId: user.id }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - await UserList.remove({ - _id: userList._id - }); + await UserLists.delete(userList.id); }); diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts index ece2af5603..b05fc45527 100644 --- a/src/server/api/endpoints/users/lists/list.ts +++ b/src/server/api/endpoints/users/lists/list.ts @@ -1,5 +1,5 @@ -import UserList, { pack } from '../../../../../models/user-list'; import define from '../../../define'; +import { UserLists } from '../../../../../models'; export const meta = { desc: { @@ -10,7 +10,7 @@ export const meta = { requireCredential: true, - kind: 'account-read', + kind: 'read:account', res: { type: 'array', @@ -21,9 +21,9 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const userLists = await UserList.find({ - userId: me._id, + const userLists = await UserLists.find({ + userId: me.id, }); - return await Promise.all(userLists.map(x => pack(x))); + return await Promise.all(userLists.map(x => UserLists.pack(x))); }); diff --git a/src/server/api/endpoints/users/lists/pull.ts b/src/server/api/endpoints/users/lists/pull.ts index 0eee1975db..524670b341 100644 --- a/src/server/api/endpoints/users/lists/pull.ts +++ b/src/server/api/endpoints/users/lists/pull.ts @@ -1,11 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList from '../../../../../models/user-list'; -import { pack as packUser } from '../../../../../models/user'; +import { ID } from '../../../../../misc/cafy-id'; import { publishUserListStream } from '../../../../../services/stream'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; +import { UserLists, UserListJoinings, Users } from '../../../../../models'; export const meta = { desc: { @@ -17,17 +16,15 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { listId: { validator: $.type(ID), - transform: transform, }, userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -52,9 +49,9 @@ export const meta = { export default define(meta, async (ps, me) => { // Fetch the list - const userList = await UserList.findOne({ - _id: ps.listId, - userId: me._id, + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, }); if (userList == null) { @@ -68,11 +65,7 @@ export default define(meta, async (ps, me) => { }); // Pull the user - await UserList.update({ _id: userList._id }, { - $pull: { - userIds: user._id - } - }); + await UserListJoinings.delete({ userId: user.id }); - publishUserListStream(userList._id, 'userRemoved', await packUser(user)); + publishUserListStream(userList.id, 'userRemoved', await Users.pack(user)); }); diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts index eea2f39a8c..2763b3a19c 100644 --- a/src/server/api/endpoints/users/lists/push.ts +++ b/src/server/api/endpoints/users/lists/push.ts @@ -1,10 +1,10 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList from '../../../../../models/user-list'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; import { pushUserToUserList } from '../../../../../services/user-list/push'; +import { UserLists, UserListJoinings } from '../../../../../models'; export const meta = { desc: { @@ -16,17 +16,15 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { listId: { validator: $.type(ID), - transform: transform, }, userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -57,9 +55,9 @@ export const meta = { export default define(meta, async (ps, me) => { // Fetch the list - const userList = await UserList.findOne({ - _id: ps.listId, - userId: me._id, + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, }); if (userList == null) { @@ -72,7 +70,12 @@ export default define(meta, async (ps, me) => { throw e; }); - if (userList.userIds.map(id => id.toHexString()).includes(user._id.toHexString())) { + const exist = await UserListJoinings.findOne({ + userListId: userList.id, + userId: user.id + }); + + if (exist) { throw new ApiError(meta.errors.alreadyAdded); } diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts index 0fab2fa499..1a997ec7c5 100644 --- a/src/server/api/endpoints/users/lists/show.ts +++ b/src/server/api/endpoints/users/lists/show.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList, { pack } from '../../../../../models/user-list'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { UserLists } from '../../../../../models'; export const meta = { desc: { @@ -14,12 +14,11 @@ export const meta = { requireCredential: true, - kind: 'account-read', + kind: 'read:account', params: { listId: { validator: $.type(ID), - transform: transform, }, }, @@ -38,14 +37,14 @@ export const meta = { export default define(meta, async (ps, me) => { // Fetch the list - const userList = await UserList.findOne({ - _id: ps.listId, - userId: me._id, + const userList = await UserLists.findOne({ + id: ps.listId, + userId: me.id, }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - return await pack(userList); + return await UserLists.pack(userList); }); diff --git a/src/server/api/endpoints/users/lists/update.ts b/src/server/api/endpoints/users/lists/update.ts index 5897693144..dc08d59f6a 100644 --- a/src/server/api/endpoints/users/lists/update.ts +++ b/src/server/api/endpoints/users/lists/update.ts @@ -1,8 +1,8 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../../misc/cafy-id'; -import UserList, { pack } from '../../../../../models/user-list'; +import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; +import { UserLists } from '../../../../../models'; export const meta = { desc: { @@ -14,19 +14,18 @@ export const meta = { requireCredential: true, - kind: 'account-write', + kind: 'write:account', params: { listId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象となるユーザーリストのID', 'en-US': 'ID of target user list' } }, - title: { + name: { validator: $.str.range(1, 100), desc: { 'ja-JP': 'このユーザーリストの名前', @@ -46,20 +45,18 @@ export const meta = { export default define(meta, async (ps, user) => { // Fetch the list - const userList = await UserList.findOne({ - _id: ps.listId, - userId: user._id + const userList = await UserLists.findOne({ + id: ps.listId, + userId: user.id }); if (userList == null) { throw new ApiError(meta.errors.noSuchList); } - await UserList.update({ _id: userList._id }, { - $set: { - title: ps.title - } + await UserLists.update(userList.id, { + name: ps.name }); - return await pack(userList._id); + return await UserLists.pack(userList.id); }); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts index 10d2f37fc2..da23be3c55 100644 --- a/src/server/api/endpoints/users/notes.ts +++ b/src/server/api/endpoints/users/notes.ts @@ -1,10 +1,13 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; -import Note, { packMany } from '../../../../models/note'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import Following from '../../../../models/following'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { generateVisibilityQuery } from '../../common/generate-visibility-query'; +import { Notes } from '../../../../models'; +import { generateMuteQuery } from '../../common/generate-mute-query'; +import { Brackets } from 'typeorm'; export const meta = { desc: { @@ -16,7 +19,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -42,17 +44,15 @@ export const meta = { sinceId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します' } }, untilId: { validator: $.optional.type(ID), - transform: transform, desc: { - 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' + 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します' } }, @@ -102,15 +102,6 @@ export const meta = { } }, - mediaOnly: { - validator: $.optional.bool, - default: false, - deprecated: true, - desc: { - 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' - } - }, - fileType: { validator: $.optional.arr($.str), desc: { @@ -150,67 +141,44 @@ export default define(meta, async (ps, me) => { throw e; }); - const isFollowing = me == null ? false : ((await Following.findOne({ - followerId: me._id, - followeeId: user._id - })) != null); - //#region Construct query - const sort = { } as any; + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.userId = :userId', { userId: user.id }) + .leftJoinAndSelect('note.user', 'user'); - const visibleQuery = me == null ? [{ - visibility: { $in: ['public', 'home'] } - }] : [{ - visibility: { - $in: isFollowing ? ['public', 'home', 'followers'] : ['public', 'home'] - } - }, { - // myself (for specified/private) - userId: me._id - }, { - // to me (for specified) - visibleUserIds: { $in: [ me._id ] } - }]; + if (me) generateVisibilityQuery(query, me); + if (me) generateMuteQuery(query, me); + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } - const query = { - $and: [ {} ], - deletedAt: null, - userId: user._id, - $or: visibleQuery - } as any; + if (ps.fileType != null) { + query.andWhere('note.fileIds != \'{}\''); + query.andWhere(new Brackets(qb => { + for (const type of ps.fileType!) { + const i = ps.fileType!.indexOf(type); + qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); + } + })); - if (ps.sinceId) { - sort._id = 1; - query._id = { - $gt: ps.sinceId - }; - } else if (ps.untilId) { - sort._id = -1; - query._id = { - $lt: ps.untilId - }; - } else if (ps.sinceDate) { - sort.createdAt = 1; - query.createdAt = { - $gt: new Date(ps.sinceDate) - }; - } else if (ps.untilDate) { - sort.createdAt = -1; - query.createdAt = { - $lt: new Date(ps.untilDate) - }; - } else { - sort._id = -1; + if (ps.excludeNsfw) { + // v11 TODO + /*query['_files.isSensitive'] = { + $ne: true + };*/ + } } if (!ps.includeReplies) { - query.replyId = null; + query.andWhere('note.replyId IS NULL'); } + /* TODO if (ps.includeMyRenotes === false) { query.$and.push({ $or: [{ - userId: { $ne: user._id } + userId: { $ne: user.id } }, { renoteId: null }, { @@ -222,35 +190,11 @@ export default define(meta, async (ps, me) => { }] }); } + */ - const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; - - if (withFiles) { - query.fileIds = { - $exists: true, - $ne: [] - }; - } - - if (ps.fileType) { - query.fileIds = { $exists: true, $ne: [] }; - - query['_files.contentType'] = { - $in: ps.fileType - }; - - if (ps.excludeNsfw) { - query['_files.metadata.isSensitive'] = { - $ne: true - }; - } - } //#endregion - const notes = await Note.find(query, { - limit: ps.limit, - sort: sort - }); + const timeline = await query.take(ps.limit!).getMany(); - return await packMany(notes, me); + return await Notes.packMany(timeline, user); }); diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 60710fffca..9e16e34e39 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -1,14 +1,8 @@ import * as ms from 'ms'; import $ from 'cafy'; -import User, { pack, ILocalUser } from '../../../../models/user'; -import { getFriendIds } from '../../common/get-friends'; -import * as request from 'request-promise-native'; -import config from '../../../../config'; import define from '../../define'; -import fetchMeta from '../../../../misc/fetch-meta'; -import resolveUser from '../../../../remote/resolve-user'; -import { getHideUserIds } from '../../common/get-hide-users'; -import { apiLogger } from '../../logger'; +import { Users, Followings } from '../../../../models'; +import { generateMuteQueryForUsers } from '../../common/generate-mute-query'; export const meta = { desc: { @@ -19,7 +13,7 @@ export const meta = { requireCredential: true, - kind: 'account-read', + kind: 'read:account', params: { limit: { @@ -42,83 +36,24 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const instance = await fetchMeta(); + const query = Users.createQueryBuilder('user') + .where('user.isLocked = FALSE') + .where('user.host IS NULL') + .where('user.updatedAt >= :date', { date: new Date(Date.now() - ms('7days')) }) + .orderBy('user.followersCount', 'DESC'); - if (instance.enableExternalUserRecommendation) { - const userName = me.username; - const hostName = config.hostname; - const limit = ps.limit; - const offset = ps.offset; - const timeout = instance.externalUserRecommendationTimeout; - const engine = instance.externalUserRecommendationEngine; - const url = engine - .replace('{{host}}', hostName) - .replace('{{user}}', userName) - .replace('{{limit}}', limit.toString()) - .replace('{{offset}}', offset.toString()); + generateMuteQueryForUsers(query, me); - const users = await request({ - url: url, - proxy: config.proxy, - timeout: timeout, - json: true, - followRedirect: true, - followAllRedirects: true - }) - .then(body => convertUsers(body, me)); + const followingQuery = Followings.createQueryBuilder('following') + .select('following.followeeId') + .where('following.followerId = :followerId', { followerId: me.id }); - return users; - } else { - // ID list of the user itself and other users who the user follows - const followingIds = await getFriendIds(me._id); + query + .andWhere(`user.id NOT IN (${ followingQuery.getQuery() })`); - // 隠すユーザーを取得 - const hideUserIds = await getHideUserIds(me); + query.setParameters(followingQuery.getParameters()); - const users = await User.find({ - _id: { - $nin: followingIds.concat(hideUserIds) - }, - isLocked: { $ne: true }, - updatedAt: { - $gte: new Date(Date.now() - ms('7days')) - }, - host: null - }, { - limit: ps.limit, - skip: ps.offset, - sort: { - followersCount: -1 - } - }); + const users = await query.take(ps.limit!).skip(ps.offset).getMany(); - return await Promise.all(users.map(user => pack(user, me, { detail: true }))); - } + return await Users.packMany(users, me, { detail: true }); }); - -type IRecommendUser = { - name: string; - username: string; - host: string; - description: string; - avatarUrl: string; -}; - -/** - * Resolve/Pack dummy users - */ -async function convertUsers(src: IRecommendUser[], me: ILocalUser) { - const packed = await Promise.all(src.map(async x => { - const user = await resolveUser(x.username, x.host) - .catch(() => { - apiLogger.warn(`Can't resolve ${x.username}@${x.host}`); - return null; - }); - - if (user == null) return x; - - return await pack(user, me, { detail: true }); - })); - - return packed; -} diff --git a/src/server/api/endpoints/users/relation.ts b/src/server/api/endpoints/users/relation.ts index f4121aa0d0..4971738d32 100644 --- a/src/server/api/endpoints/users/relation.ts +++ b/src/server/api/endpoints/users/relation.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import ID, { transform, ObjectId } from '../../../../misc/cafy-id'; -import { getRelation } from '../../../../models/user'; import define from '../../define'; +import { ID } from '../../../../misc/cafy-id'; +import { Users } from '../../../../models'; export const meta = { desc: { @@ -15,7 +15,6 @@ export const meta = { params: { userId: { validator: $.either($.type(ID), $.arr($.type(ID)).unique()), - transform: (v: any): ObjectId | ObjectId[] => Array.isArray(v) ? v.map(x => transform(x)) : transform(v), desc: { 'ja-JP': 'ユーザーID (配列でも可)' } @@ -26,7 +25,7 @@ export const meta = { export default define(meta, async (ps, me) => { const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; - const relations = await Promise.all(ids.map(id => getRelation(me._id, id))); + const relations = await Promise.all(ids.map(id => Users.getRelation(me.id, id))); return Array.isArray(ps.userId) ? relations : relations[0]; }); diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts index 0f23f8f0c3..2ee28c9002 100644 --- a/src/server/api/endpoints/users/report-abuse.ts +++ b/src/server/api/endpoints/users/report-abuse.ts @@ -1,11 +1,11 @@ import $ from 'cafy'; -import ID, { transform } from '../../../../misc/cafy-id'; +import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import User from '../../../../models/user'; -import AbuseUserReport from '../../../../models/abuse-user-report'; import { publishAdminStream } from '../../../../services/stream'; import { ApiError } from '../../error'; import { getUser } from '../../common/getters'; +import { AbuseUserReports, Users } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; export const meta = { desc: { @@ -19,7 +19,6 @@ export const meta = { params: { userId: { validator: $.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -62,7 +61,7 @@ export default define(meta, async (ps, me) => { throw e; }); - if (user._id.equals(me._id)) { + if (user.id === me.id) { throw new ApiError(meta.errors.cannotReportYourself); } @@ -70,17 +69,18 @@ export default define(meta, async (ps, me) => { throw new ApiError(meta.errors.cannotReportAdmin); } - const report = await AbuseUserReport.insert({ + const report = await AbuseUserReports.save({ + id: genId(), createdAt: new Date(), - userId: user._id, - reporterId: me._id, + userId: user.id, + reporterId: me.id, comment: ps.comment }); // Publish event to moderators setTimeout(async () => { - const moderators = await User.find({ - $or: [{ + const moderators = await Users.find({ + where: [{ isAdmin: true }, { isModerator: true @@ -88,8 +88,8 @@ export default define(meta, async (ps, me) => { }); for (const moderator of moderators) { - publishAdminStream(moderator._id, 'newAbuseUserReport', { - id: report._id, + publishAdminStream(moderator.id, 'newAbuseUserReport', { + id: report.id, userId: report.userId, reporterId: report.reporterId, comment: report.comment diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index a95f6df6de..96da221d97 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; -import * as escapeRegexp from 'escape-regexp'; -import User, { pack, validateUsername, IUser } from '../../../../models/user'; import define from '../../define'; +import { Users } from '../../../../models'; +import { User } from '../../../../models/entities/user'; export const meta = { desc: { @@ -62,34 +62,30 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const isUsername = validateUsername(ps.query.replace('@', ''), !ps.localOnly); + const isUsername = Users.validateUsername(ps.query.replace('@', ''), !ps.localOnly); - let users: IUser[] = []; + let users: User[] = []; if (isUsername) { - users = await User - .find({ - host: null, - usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())), - isSuspended: { $ne: true } - }, { - limit: ps.limit, - skip: ps.offset - }); + users = await Users.createQueryBuilder('user') + .where('user.host IS NULL') + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .take(ps.limit!) + .skip(ps.offset) + .getMany(); - if (users.length < ps.limit && !ps.localOnly) { - const otherUsers = await User - .find({ - host: { $ne: null }, - usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())), - isSuspended: { $ne: true } - }, { - limit: ps.limit - users.length - }); + if (users.length < ps.limit! && !ps.localOnly) { + const otherUsers = await Users.createQueryBuilder('user') + .where('user.host IS NOT NULL') + .andWhere('user.isSuspended = FALSE') + .andWhere('user.usernameLower like :username', { username: ps.query.replace('@', '').toLowerCase() + '%' }) + .take(ps.limit! - users.length) + .getMany(); users = users.concat(otherUsers); } } - return await Promise.all(users.map(user => pack(user, me, { detail: ps.detail }))); + return await Users.packMany(users, me, { detail: ps.detail }); }); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 4e59945eba..2be193f89b 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -1,12 +1,11 @@ import $ from 'cafy'; -import ID, { transform, transformMany } from '../../../../misc/cafy-id'; -import User, { pack, isRemoteUser } from '../../../../models/user'; -import resolveRemoteUser from '../../../../remote/resolve-user'; +import { resolveUser } from '../../../../remote/resolve-user'; import define from '../../define'; import { apiLogger } from '../../logger'; import { ApiError } from '../../error'; - -const cursorOption = { fields: { data: false } }; +import { ID } from '../../../../misc/cafy-id'; +import { Users } from '../../../../models'; +import { In } from 'typeorm'; export const meta = { desc: { @@ -20,7 +19,6 @@ export const meta = { params: { userId: { validator: $.optional.type(ID), - transform: transform, desc: { 'ja-JP': '対象のユーザーのID', 'en-US': 'Target user ID' @@ -29,7 +27,6 @@ export const meta = { userIds: { validator: $.optional.arr($.type(ID)).unique(), - transform: transformMany, desc: { 'ja-JP': 'ユーザーID (配列)' } @@ -68,42 +65,40 @@ export default define(meta, async (ps, me) => { let user; if (ps.userIds) { - const users = await User.find({ - _id: { - $in: ps.userIds - } + const users = await Users.find({ + id: In(ps.userIds) }); - return await Promise.all(users.map(u => pack(u, me, { + return await Promise.all(users.map(u => Users.pack(u, me, { detail: true }))); } else { // Lookup user - if (typeof ps.host === 'string') { - user = await resolveRemoteUser(ps.username, ps.host, cursorOption).catch(e => { + if (typeof ps.host === 'string' && typeof ps.username === 'string') { + user = await resolveUser(ps.username, ps.host).catch(e => { apiLogger.warn(`failed to resolve remote user: ${e}`); throw new ApiError(meta.errors.failedToResolveRemoteUser); }); } else { const q: any = ps.userId != null - ? { _id: ps.userId } - : { usernameLower: ps.username.toLowerCase(), host: null }; + ? { id: ps.userId } + : { usernameLower: ps.username!.toLowerCase(), host: null }; - user = await User.findOne(q, cursorOption); + user = await Users.findOne(q); } - if (user === null) { + if (user == null) { throw new ApiError(meta.errors.noSuchUser); } // ユーザー情報更新 - if (isRemoteUser(user)) { + if (Users.isRemoteUser(user)) { if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - resolveRemoteUser(ps.username, ps.host, { }, true); + resolveUser(user.username, user.host, { }, true); } } - return await pack(user, me, { + return await Users.pack(user, me, { detail: true }); } diff --git a/src/server/api/error.ts b/src/server/api/error.ts index ca441d5811..cb0bdc9f47 100644 --- a/src/server/api/error.ts +++ b/src/server/api/error.ts @@ -1,3 +1,5 @@ +type E = { message: string, code: string, id: string, kind?: 'client' | 'server', httpStatusCode?: number }; + export class ApiError extends Error { public message: string; public code: string; @@ -6,7 +8,7 @@ export class ApiError extends Error { public httpStatusCode?: number; public info?: any; - constructor(e?: { message: string, code: string, id: string, kind?: 'client' | 'server', httpStatusCode?: number }, info?: any) { + constructor(e?: E | null | undefined, info?: any | null | undefined) { if (e == null) e = { message: 'Internal error occurred. Please contact us if the error persists.', code: 'INTERNAL_ERROR', diff --git a/src/server/api/index.ts b/src/server/api/index.ts index fac57ca06e..8c2b97775f 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -15,8 +15,7 @@ import signin from './private/signin'; import discord from './service/discord'; import github from './service/github'; import twitter from './service/twitter'; -import Instance from '../../models/instance'; -import { toASCII } from 'punycode'; +import { Instances } from '../../models'; // Init app const app = new Koa(); @@ -67,14 +66,11 @@ router.use(github.routes()); router.use(twitter.routes()); router.get('/v1/instance/peers', async ctx => { - const instances = await Instance.find({ - }, { - host: 1 - }); + const instances = await Instances.find({ + select: ['host'] + }); - const punyCodes = instances.map(instance => toASCII(instance.host)); - - ctx.body = punyCodes; + ctx.body = instances.map(instance => instance.host); }); // Return 404 for unknown API diff --git a/src/server/api/limiter.ts b/src/server/api/limiter.ts index 3d66172fd8..48d12d3cc6 100644 --- a/src/server/api/limiter.ts +++ b/src/server/api/limiter.ts @@ -2,19 +2,13 @@ import * as Limiter from 'ratelimiter'; import limiterDB from '../../db/redis'; import { IEndpoint } from './endpoints'; import getAcct from '../../misc/acct/render'; -import { IUser } from '../../models/user'; +import { User } from '../../models/entities/user'; import Logger from '../../services/logger'; const logger = new Logger('limiter'); -export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => { - // Redisがインストールされてない場合は常に許可 - if (limiterDB == null) { - ok(); - return; - } - - const limitation = endpoint.meta.limit; +export default (endpoint: IEndpoint, user: User) => new Promise((ok, reject) => { + const limitation = endpoint.meta.limit!; const key = limitation.hasOwnProperty('key') ? limitation.key @@ -38,10 +32,10 @@ export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => // Short-term limit function min() { const minIntervalLimiter = new Limiter({ - id: `${user._id}:${key}:min`, + id: `${user.id}:${key}:min`, duration: limitation.minInterval, max: 1, - db: limiterDB + db: limiterDB! }); minIntervalLimiter.get((err, info) => { @@ -66,10 +60,10 @@ export default (endpoint: IEndpoint, user: IUser) => new Promise((ok, reject) => // Long term limit function max() { const limiter = new Limiter({ - id: `${user._id}:${key}`, + id: `${user.id}:${key}`, duration: limitation.duration, max: limitation.max, - db: limiterDB + db: limiterDB! }); limiter.get((err, info) => { diff --git a/src/server/api/openapi/schemas.ts b/src/server/api/openapi/schemas.ts index 70a0d6faf0..5992fee835 100644 --- a/src/server/api/openapi/schemas.ts +++ b/src/server/api/openapi/schemas.ts @@ -221,7 +221,7 @@ export const schemas = { }, type: { type: 'string', - enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'poll_vote'], + enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'], description: 'The type of the notification.' }, }, @@ -258,7 +258,7 @@ export const schemas = { description: 'The MD5 hash of this Drive file.', example: '15eca7fba0480996e2245f5185bf39f2' }, - datasize: { + size: { type: 'number', description: 'The size of this Drive file. (bytes)', example: 51469 @@ -275,7 +275,7 @@ export const schemas = { description: 'Whether this Drive file is sensitive.', }, }, - required: ['id', 'createdAt', 'name', 'type', 'datasize', 'md5'] + required: ['id', 'createdAt', 'name', 'type', 'size', 'md5'] }, DriveFolder: { @@ -318,6 +318,40 @@ export const schemas = { required: ['id', 'createdAt', 'name'] }, + Following: { + type: 'object', + properties: { + id: { + type: 'string', + format: 'id', + description: 'The unique identifier for this following.', + example: 'xxxxxxxxxxxxxxxxxxxxxxxx', + }, + createdAt: { + type: 'string', + format: 'date-time', + description: 'The date that the following was created.' + }, + followeeId: { + type: 'string', + format: 'id', + }, + followee: { + $ref: '#/components/schemas/User', + description: 'The followee.' + }, + followerId: { + type: 'string', + format: 'id', + }, + follower: { + $ref: '#/components/schemas/User', + description: 'The follower.' + }, + }, + required: ['id', 'createdAt', 'followeeId', 'followerId'] + }, + Muting: { type: 'object', properties: { diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts index 40bcd2c5d6..676546f2aa 100644 --- a/src/server/api/private/signin.ts +++ b/src/server/api/private/signin.ts @@ -1,11 +1,13 @@ import * as Koa from 'koa'; import * as bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; -import User, { ILocalUser } from '../../../models/user'; -import Signin, { pack } from '../../../models/signin'; import { publishMainStream } from '../../../services/stream'; import signin from '../common/signin'; import config from '../../../config'; +import { Users, Signins, UserProfiles } from '../../../models'; +import { ILocalUser } from '../../../models/entities/user'; +import { genId } from '../../../misc/gen-id'; +import { ensure } from '../../../prelude/ensure'; export default async (ctx: Koa.BaseContext) => { ctx.set('Access-Control-Allow-Origin', config.url); @@ -32,30 +34,27 @@ export default async (ctx: Koa.BaseContext) => { } // Fetch user - const user = await User.findOne({ + const user = await Users.findOne({ usernameLower: username.toLowerCase(), host: null - }, { - fields: { - data: false, - profile: false - } - }) as ILocalUser; + }) as ILocalUser; - if (user === null) { + if (user == null) { ctx.throw(404, { error: 'user not found' }); return; } + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + // Compare password - const same = await bcrypt.compare(password, user.password); + const same = await bcrypt.compare(password, profile.password!); if (same) { - if (user.twoFactorEnabled) { + if (profile.twoFactorEnabled) { const verified = (speakeasy as any).totp.verify({ - secret: user.twoFactorSecret, + secret: profile.twoFactorSecret, encoding: 'base32', token: token }); @@ -77,14 +76,15 @@ export default async (ctx: Koa.BaseContext) => { } // Append signin history - const record = await Signin.insert({ + const record = await Signins.save({ + id: genId(), createdAt: new Date(), - userId: user._id, + userId: user.id, ip: ctx.ip, headers: ctx.headers, success: same }); // Publish signin event - publishMainStream(user._id, 'signin', await pack(record)); + publishMainStream(user.id, 'signin', await Signins.pack(record)); }; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 89b7b330d2..f8dba2eb29 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -1,14 +1,18 @@ import * as Koa from 'koa'; import * as bcrypt from 'bcryptjs'; -import { generate as generateKeypair } from '../../../crypto_key'; -import User, { IUser, validateUsername, validatePassword, pack } from '../../../models/user'; +import { generateKeyPair } from 'crypto'; import generateUserToken from '../common/generate-native-user-token'; import config from '../../../config'; -import Meta from '../../../models/meta'; -import RegistrationTicket from '../../../models/registration-tickets'; -import usersChart from '../../../services/chart/users'; import fetchMeta from '../../../misc/fetch-meta'; import * as recaptcha from 'recaptcha-promise'; +import { Users, RegistrationTickets } from '../../../models'; +import { genId } from '../../../misc/gen-id'; +import { usersChart } from '../../../services/chart'; +import { User } from '../../../models/entities/user'; +import { UserKeypair } from '../../../models/entities/user-keypair'; +import { toPunyNullable } from '../../../misc/convert-host'; +import { UserProfile } from '../../../models/entities/user-profile'; +import { getConnection } from 'typeorm'; export default async (ctx: Koa.BaseContext) => { const body = ctx.request.body as any; @@ -17,7 +21,7 @@ export default async (ctx: Koa.BaseContext) => { // Verify recaptcha // ただしテスト時はこの機構は障害となるため無効にする - if (process.env.NODE_ENV !== 'test' && instance.enableRecaptcha) { + if (process.env.NODE_ENV !== 'test' && instance.enableRecaptcha && instance.recaptchaSecretKey) { recaptcha.init({ secret_key: instance.recaptchaSecretKey }); @@ -32,6 +36,7 @@ export default async (ctx: Koa.BaseContext) => { const username = body['username']; const password = body['password']; + const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null; const invitationCode = body['invitationCode']; if (instance && instance.disableRegistration) { @@ -40,7 +45,7 @@ export default async (ctx: Koa.BaseContext) => { return; } - const ticket = await RegistrationTicket.findOne({ + const ticket = await RegistrationTickets.findOne({ code: invitationCode }); @@ -49,39 +54,22 @@ export default async (ctx: Koa.BaseContext) => { return; } - RegistrationTicket.remove({ - _id: ticket._id - }); + RegistrationTickets.delete(ticket.id); } // Validate username - if (!validateUsername(username)) { + if (!Users.validateUsername(username)) { ctx.status = 400; return; } // Validate password - if (!validatePassword(password)) { + if (!Users.validatePassword(password)) { ctx.status = 400; return; } - const usersCount = await User.count({}); - - // Fetch exist user that same username - const usernameExist = await User - .count({ - usernameLower: username.toLowerCase(), - host: null - }, { - limit: 1 - }); - - // Check username already used - if (usernameExist !== 0) { - ctx.status = 400; - return; - } + const usersCount = await Users.count({}); // Generate hash of password const salt = await bcrypt.genSalt(8); @@ -90,46 +78,59 @@ export default async (ctx: Koa.BaseContext) => { // Generate secret const secret = generateUserToken(); - // Create account - const account: IUser = await User.insert({ - avatarId: null, - bannerId: null, - createdAt: new Date(), - description: null, - followersCount: 0, - followingCount: 0, - name: null, - notesCount: 0, - username: username, - usernameLower: username.toLowerCase(), - host: null, - keypair: generateKeypair(), - token: secret, - password: hash, - isAdmin: config.autoAdmin && usersCount === 0, - autoAcceptFollowed: true, - profile: { - bio: null, - birthday: null, - location: null - }, - settings: { - autoWatch: false - } - }); + if (await Users.findOne({ usernameLower: username.toLowerCase(), host: null })) { + ctx.status = 400; + return; + } - //#region Increment users count - Meta.update({}, { - $inc: { - 'stats.usersCount': 1, - 'stats.originalUsersCount': 1 - } - }, { upsert: true }); - //#endregion + const keyPair = await new Promise<string[]>((s, j) => + generateKeyPair('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + cipher: undefined, + passphrase: undefined + } + } as any, (e, publicKey, privateKey) => + e ? j(e) : s([publicKey, privateKey]) + )); + + let account!: User; + + // Start transaction + await getConnection().transaction(async transactionalEntityManager => { + account = await transactionalEntityManager.save(new User({ + id: genId(), + createdAt: new Date(), + username: username, + usernameLower: username.toLowerCase(), + host: toPunyNullable(host), + token: secret, + isAdmin: config.autoAdmin && usersCount === 0, + })); + + await transactionalEntityManager.save(new UserKeypair({ + publicKey: keyPair[0], + privateKey: keyPair[1], + userId: account.id + })); + + await transactionalEntityManager.save(new UserProfile({ + userId: account.id, + autoAcceptFollowed: true, + autoWatch: false, + password: hash, + })); + }); usersChart.update(account, true); - const res = await pack(account, account, { + const res = await Users.pack(account, account, { detail: true, includeSecrets: true }); diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts index 92f5bbf72d..d8a979b5b2 100644 --- a/src/server/api/service/discord.ts +++ b/src/server/api/service/discord.ts @@ -2,13 +2,15 @@ import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as request from 'request'; import { OAuth2 } from 'oauth'; -import User, { pack, ILocalUser } from '../../../models/user'; import config from '../../../config'; import { publishMainStream } from '../../../services/stream'; import redis from '../../../db/redis'; import * as uuid from 'uuid'; import signin from '../common/signin'; import fetchMeta from '../../../misc/fetch-meta'; +import { Users, UserProfiles } from '../../../models'; +import { ILocalUser } from '../../../models/entities/user'; +import { ensure } from '../../../prelude/ensure'; function getUserToken(ctx: Koa.BaseContext) { return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; @@ -39,19 +41,27 @@ router.get('/disconnect/discord', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, - 'token': userToken + token: userToken + }).then(ensure); + + await UserProfiles.update({ + userId: user.id }, { - $set: { - 'discord': null - } + discord: false, + discordAccessToken: null, + discordRefreshToken: null, + discordExpiresDate: null, + discordId: null, + discordUsername: null, + discordDiscriminator: null, }); ctx.body = `Discordの連携を解除しました :v:`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); @@ -62,8 +72,8 @@ async function getOAuth2() { if (meta.enableDiscordIntegration) { return new OAuth2( - meta.discordClientId, - meta.discordClientSecret, + meta.discordClientId!, + meta.discordClientSecret!, 'https://discordapp.com/', 'api/oauth2/authorize', 'api/oauth2/token'); @@ -94,7 +104,7 @@ router.get('/connect/discord', async ctx => { redis.set(userToken, JSON.stringify(params)); const oauth2 = await getOAuth2(); - ctx.redirect(oauth2.getAuthorizeUrl(params)); + ctx.redirect(oauth2!.getAuthorizeUrl(params)); }); router.get('/signin/discord', async ctx => { @@ -120,7 +130,7 @@ router.get('/signin/discord', async ctx => { redis.set(sessid, JSON.stringify(params)); const oauth2 = await getOAuth2(); - ctx.redirect(oauth2.getAuthorizeUrl(params)); + ctx.redirect(oauth2!.getAuthorizeUrl(params)); }); router.get('/dc/cb', async ctx => { @@ -155,24 +165,22 @@ router.get('/dc/cb', async ctx => { } const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) => - oauth2.getOAuthAccessToken( - code, - { - grant_type: 'authorization_code', - redirect_uri - }, - (err, accessToken, refreshToken, result) => { - if (err) - rej(err); - else if (result.error) - rej(result.error); - else + oauth2!.getOAuthAccessToken(code, { + grant_type: 'authorization_code', + redirect_uri + }, (err, accessToken, refreshToken, result) => { + if (err) { + rej(err); + } else if (result.error) { + rej(result.error); + } else { res({ accessToken, refreshToken, expiresDate: Date.now() + Number(result.expires_in) * 1000 }); - })); + } + })); const { id, username, discriminator } = await new Promise<any>((res, rej) => request({ @@ -182,10 +190,11 @@ router.get('/dc/cb', async ctx => { 'User-Agent': config.userAgent } }, (err, response, body) => { - if (err) + if (err) { rej(err); - else + } else { res(JSON.parse(body)); + } })); if (!id || !username || !discriminator) { @@ -193,32 +202,30 @@ router.get('/dc/cb', async ctx => { return; } - let user = await User.findOne({ - host: null, - 'discord.id': id - }) as ILocalUser; + const profile = await UserProfiles.createQueryBuilder() + .where('discord @> :discord', { + discord: { + id: id, + }, + }) + .andWhere('userHost IS NULL') + .getOne(); - if (!user) { + if (profile == null) { ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`); return; } - user = await User.findOneAndUpdate({ - host: null, - 'discord.id': id - }, { - $set: { - discord: { - accessToken, - refreshToken, - expiresDate, - username, - discriminator - } - } - }) as ILocalUser; + await UserProfiles.update({ userId: profile.userId }, { + discord: true, + discordAccessToken: accessToken, + discordRefreshToken: refreshToken, + discordExpiresDate: expiresDate, + discordUsername: username, + discordDiscriminator: discriminator + }); - signin(ctx, user, true); + signin(ctx, await Users.findOne(profile.userId) as ILocalUser, true); } else { const code = ctx.query.code; @@ -239,24 +246,22 @@ router.get('/dc/cb', async ctx => { } const { accessToken, refreshToken, expiresDate } = await new Promise<any>((res, rej) => - oauth2.getOAuthAccessToken( - code, - { - grant_type: 'authorization_code', - redirect_uri - }, - (err, accessToken, refreshToken, result) => { - if (err) - rej(err); - else if (result.error) - rej(result.error); - else - res({ - accessToken, - refreshToken, - expiresDate: Date.now() + Number(result.expires_in) * 1000 - }); - })); + oauth2!.getOAuthAccessToken(code, { + grant_type: 'authorization_code', + redirect_uri + }, (err, accessToken, refreshToken, result) => { + if (err) { + rej(err); + } else if (result.error) { + rej(result.error); + } else { + res({ + accessToken, + refreshToken, + expiresDate: Date.now() + Number(result.expires_in) * 1000 + }); + } + })); const { id, username, discriminator } = await new Promise<any>((res, rej) => request({ @@ -266,10 +271,11 @@ router.get('/dc/cb', async ctx => { 'User-Agent': config.userAgent } }, (err, response, body) => { - if (err) + if (err) { rej(err); - else + } else { res(JSON.parse(body)); + } })); if (!id || !username || !discriminator) { @@ -277,26 +283,25 @@ router.get('/dc/cb', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, token: userToken - }, { - $set: { - discord: { - accessToken, - refreshToken, - expiresDate, - id, - username, - discriminator - } - } + }).then(ensure); + + await UserProfiles.update({ userId: user.id }, { + discord: true, + discordAccessToken: accessToken, + discordRefreshToken: refreshToken, + discordExpiresDate: expiresDate, + discordId: id, + discordUsername: username, + discordDiscriminator: discriminator }); ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index cf3589a4b7..a4d274cc62 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -2,13 +2,15 @@ import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as request from 'request'; import { OAuth2 } from 'oauth'; -import User, { pack, ILocalUser } from '../../../models/user'; import config from '../../../config'; import { publishMainStream } from '../../../services/stream'; import redis from '../../../db/redis'; import * as uuid from 'uuid'; import signin from '../common/signin'; import fetchMeta from '../../../misc/fetch-meta'; +import { Users, UserProfiles } from '../../../models'; +import { ILocalUser } from '../../../models/entities/user'; +import { ensure } from '../../../prelude/ensure'; function getUserToken(ctx: Koa.BaseContext) { return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; @@ -39,19 +41,24 @@ router.get('/disconnect/github', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, - 'token': userToken + token: userToken + }).then(ensure); + + await UserProfiles.update({ + userId: user.id }, { - $set: { - 'github': null - } + github: false, + githubAccessToken: null, + githubId: null, + githubLogin: null, }); ctx.body = `GitHubの連携を解除しました :v:`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); @@ -60,7 +67,7 @@ router.get('/disconnect/github', async ctx => { async function getOath2() { const meta = await fetchMeta(); - if (meta.enableGithubIntegration) { + if (meta.enableGithubIntegration && meta.githubClientId && meta.githubClientSecret) { return new OAuth2( meta.githubClientId, meta.githubClientSecret, @@ -93,7 +100,7 @@ router.get('/connect/github', async ctx => { redis.set(userToken, JSON.stringify(params)); const oauth2 = await getOath2(); - ctx.redirect(oauth2.getAuthorizeUrl(params)); + ctx.redirect(oauth2!.getAuthorizeUrl(params)); }); router.get('/signin/github', async ctx => { @@ -118,7 +125,7 @@ router.get('/signin/github', async ctx => { redis.set(sessid, JSON.stringify(params)); const oauth2 = await getOath2(); - ctx.redirect(oauth2.getAuthorizeUrl(params)); + ctx.redirect(oauth2!.getAuthorizeUrl(params)); }); router.get('/gh/cb', async ctx => { @@ -153,17 +160,17 @@ router.get('/gh/cb', async ctx => { } const { accessToken } = await new Promise<any>((res, rej) => - oauth2.getOAuthAccessToken( - code, - { redirect_uri }, - (err, accessToken, refresh, result) => { - if (err) - rej(err); - else if (result.error) - rej(result.error); - else - res({ accessToken }); - })); + oauth2!.getOAuthAccessToken(code, { + redirect_uri + }, (err, accessToken, refresh, result) => { + if (err) { + rej(err); + } else if (result.error) { + rej(result.error); + } else { + res({ accessToken }); + } + })); const { login, id } = await new Promise<any>((res, rej) => request({ @@ -185,17 +192,21 @@ router.get('/gh/cb', async ctx => { return; } - const user = await User.findOne({ - host: null, - 'github.id': id - }) as ILocalUser; + const link = await UserProfiles.createQueryBuilder() + .where('github @> :github', { + github: { + id: id, + }, + }) + .andWhere('userHost IS NULL') + .getOne(); - if (!user) { + if (link == null) { ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`); return; } - signin(ctx, user, true); + signin(ctx, await Users.findOne(link.userId) as ILocalUser, true); } else { const code = ctx.query.code; @@ -216,7 +227,7 @@ router.get('/gh/cb', async ctx => { } const { accessToken } = await new Promise<any>((res, rej) => - oauth2.getOAuthAccessToken( + oauth2!.getOAuthAccessToken( code, { redirect_uri }, (err, accessToken, refresh, result) => { @@ -248,23 +259,22 @@ router.get('/gh/cb', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, token: userToken - }, { - $set: { - github: { - accessToken, - id, - login - } - } + }).then(ensure); + + await UserProfiles.update({ userId: user.id }, { + github: true, + githubAccessToken: accessToken, + githubId: id, + githubLogin: login, }); ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index fc23808e21..39fdfd8654 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -3,11 +3,13 @@ import * as Router from 'koa-router'; import * as uuid from 'uuid'; import autwh from 'autwh'; import redis from '../../../db/redis'; -import User, { pack, ILocalUser } from '../../../models/user'; import { publishMainStream } from '../../../services/stream'; import config from '../../../config'; import signin from '../common/signin'; import fetchMeta from '../../../misc/fetch-meta'; +import { Users, UserProfiles } from '../../../models'; +import { ILocalUser } from '../../../models/entities/user'; +import { ensure } from '../../../prelude/ensure'; function getUserToken(ctx: Koa.BaseContext) { return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; @@ -38,19 +40,25 @@ router.get('/disconnect/twitter', async ctx => { return; } - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, - 'token': userToken + token: userToken + }).then(ensure); + + await UserProfiles.update({ + userId: user.id }, { - $set: { - 'twitter': null - } + twitter: false, + twitterAccessToken: null, + twitterAccessTokenSecret: null, + twitterUserId: null, + twitterScreenName: null, }); ctx.body = `Twitterの連携を解除しました :v:`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); @@ -59,7 +67,7 @@ router.get('/disconnect/twitter', async ctx => { async function getTwAuth() { const meta = await fetchMeta(); - if (meta.enableTwitterIntegration) { + if (meta.enableTwitterIntegration && meta.twitterConsumerKey && meta.twitterConsumerSecret) { return autwh({ consumerKey: meta.twitterConsumerKey, consumerSecret: meta.twitterConsumerSecret, @@ -83,14 +91,14 @@ router.get('/connect/twitter', async ctx => { } const twAuth = await getTwAuth(); - const twCtx = await twAuth.begin(); + const twCtx = await twAuth!.begin(); redis.set(userToken, JSON.stringify(twCtx)); ctx.redirect(twCtx.url); }); router.get('/signin/twitter', async ctx => { const twAuth = await getTwAuth(); - const twCtx = await twAuth.begin(); + const twCtx = await twAuth!.begin(); const sessid = uuid(); @@ -130,19 +138,23 @@ router.get('/tw/cb', async ctx => { const twCtx = await get; - const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier); + const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier); - const user = await User.findOne({ - host: null, - 'twitter.userId': result.userId - }) as ILocalUser; + const link = await UserProfiles.createQueryBuilder() + .where('twitter @> :twitter', { + twitter: { + userId: result.userId, + }, + }) + .andWhere('userHost IS NULL') + .getOne(); - if (user == null) { + if (link == null) { ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); return; } - signin(ctx, user, true); + signin(ctx, await Users.findOne(link.userId) as ILocalUser, true); } else { const verifier = ctx.query.oauth_verifier; @@ -159,26 +171,25 @@ router.get('/tw/cb', async ctx => { const twCtx = await get; - const result = await twAuth.done(JSON.parse(twCtx), verifier); + const result = await twAuth!.done(JSON.parse(twCtx), verifier); - const user = await User.findOneAndUpdate({ + const user = await Users.findOne({ host: null, token: userToken - }, { - $set: { - twitter: { - accessToken: result.accessToken, - accessTokenSecret: result.accessTokenSecret, - userId: result.userId, - screenName: result.screenName - } - } + }).then(ensure); + + await UserProfiles.update({ userId: user.id }, { + twitter: true, + twitterAccessToken: result.accessToken, + twitterAccessTokenSecret: result.accessTokenSecret, + twitterUserId: result.userId, + twitterScreenName: result.screenName, }); ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { + publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, { detail: true, includeSecrets: true })); diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts index bdbe4605cf..18fa651820 100644 --- a/src/server/api/stream/channel.ts +++ b/src/server/api/stream/channel.ts @@ -15,6 +15,14 @@ export default abstract class Channel { return this.connection.user; } + protected get following() { + return this.connection.following; + } + + protected get muting() { + return this.connection.muting; + } + protected get subscriber() { return this.connection.subscriber; } diff --git a/src/server/api/stream/channels/admin.ts b/src/server/api/stream/channels/admin.ts index 6bcd1a7e0b..1ff932d1dd 100644 --- a/src/server/api/stream/channels/admin.ts +++ b/src/server/api/stream/channels/admin.ts @@ -9,7 +9,7 @@ export default class extends Channel { @autobind public async init(params: any) { // Subscribe admin stream - this.subscriber.on(`adminStream:${this.user._id}`, data => { + this.subscriber.on(`adminStream:${this.user!.id}`, data => { this.send(data); }); } diff --git a/src/server/api/stream/channels/drive.ts b/src/server/api/stream/channels/drive.ts index 391c4b5c32..4112dd9b04 100644 --- a/src/server/api/stream/channels/drive.ts +++ b/src/server/api/stream/channels/drive.ts @@ -9,7 +9,7 @@ export default class extends Channel { @autobind public async init(params: any) { // Subscribe drive stream - this.subscriber.on(`driveStream:${this.user._id}`, data => { + this.subscriber.on(`driveStream:${this.user!.id}`, data => { this.send(data); }); } diff --git a/src/server/api/stream/channels/games/reversi-game.ts b/src/server/api/stream/channels/games/reversi-game.ts index 87df9e194c..7c13666c51 100644 --- a/src/server/api/stream/channels/games/reversi-game.ts +++ b/src/server/api/stream/channels/games/reversi-game.ts @@ -1,22 +1,22 @@ import autobind from 'autobind-decorator'; import * as CRC32 from 'crc-32'; -import * as mongo from 'mongodb'; -import ReversiGame, { pack } from '../../../../../models/games/reversi/game'; import { publishReversiGameStream } from '../../../../../services/stream'; import Reversi from '../../../../../games/reversi/core'; import * as maps from '../../../../../games/reversi/maps'; import Channel from '../../channel'; +import { ReversiGame } from '../../../../../models/entities/games/reversi/game'; +import { ReversiGames } from '../../../../../models'; export default class extends Channel { public readonly chName = 'gamesReversiGame'; public static shouldShare = false; public static requireCredential = false; - private gameId: mongo.ObjectID; + private gameId: ReversiGame['id'] | null = null; @autobind public async init(params: any) { - this.gameId = new mongo.ObjectID(params.gameId as string); + this.gameId = params.gameId; // Subscribe game stream this.subscriber.on(`reversiGameStream:${this.gameId}`, data => { @@ -29,7 +29,7 @@ export default class extends Channel { switch (type) { case 'accept': this.accept(true); break; case 'cancelAccept': this.accept(false); break; - case 'updateSettings': this.updateSettings(body.settings); break; + case 'updateSettings': this.updateSettings(body.key, body.value); break; case 'initForm': this.initForm(body); break; case 'updateForm': this.updateForm(body.id, body.value); break; case 'message': this.message(body); break; @@ -39,54 +39,64 @@ export default class extends Channel { } @autobind - private async updateSettings(settings: any) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + private async updateSettings(key: string, value: any) { + if (this.user == null) return; + + const game = await ReversiGames.findOne(this.gameId!); + if (game == null) throw new Error('game not found'); if (game.isStarted) return; - if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; - if (game.user1Id.equals(this.user._id) && game.user1Accepted) return; - if (game.user2Id.equals(this.user._id) && game.user2Accepted) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; + if ((game.user1Id === this.user.id) && game.user1Accepted) return; + if ((game.user2Id === this.user.id) && game.user2Accepted) return; - await ReversiGame.update({ _id: this.gameId }, { - $set: { - settings - } + if (!['map', 'bw', 'isLlotheo', 'canPutEverywhere', 'loopedBoard'].includes(key)) return; + + await ReversiGames.update(this.gameId!, { + [key]: value }); - publishReversiGameStream(this.gameId, 'updateSettings', settings); + publishReversiGameStream(this.gameId!, 'updateSettings', { + key: key, + value: value + }); } @autobind private async initForm(form: any) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + if (this.user == null) return; + + const game = await ReversiGames.findOne(this.gameId!); + if (game == null) throw new Error('game not found'); if (game.isStarted) return; - if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - const set = game.user1Id.equals(this.user._id) ? { + const set = game.user1Id === this.user.id ? { form1: form } : { - form2: form - }; + form2: form + }; - await ReversiGame.update({ _id: this.gameId }, { - $set: set - }); + await ReversiGames.update(this.gameId!, set); - publishReversiGameStream(this.gameId, 'initForm', { - userId: this.user._id, + publishReversiGameStream(this.gameId!, 'initForm', { + userId: this.user.id, form }); } @autobind private async updateForm(id: string, value: any) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + if (this.user == null) return; + + const game = await ReversiGames.findOne(this.gameId!); + if (game == null) throw new Error('game not found'); if (game.isStarted) return; - if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - const form = game.user1Id.equals(this.user._id) ? game.form2 : game.form1; + const form = game.user1Id === this.user.id ? game.form2 : game.form1; const item = form.find((i: any) => i.id == id); @@ -94,18 +104,16 @@ export default class extends Channel { item.value = value; - const set = game.user1Id.equals(this.user._id) ? { + const set = game.user1Id === this.user.id ? { form2: form } : { form1: form }; - await ReversiGame.update({ _id: this.gameId }, { - $set: set - }); + await ReversiGames.update(this.gameId!, set); - publishReversiGameStream(this.gameId, 'updateForm', { - userId: this.user._id, + publishReversiGameStream(this.gameId!, 'updateForm', { + userId: this.user.id, id, value }); @@ -113,42 +121,43 @@ export default class extends Channel { @autobind private async message(message: any) { + if (this.user == null) return; + message.id = Math.random(); - publishReversiGameStream(this.gameId, 'message', { - userId: this.user._id, + publishReversiGameStream(this.gameId!, 'message', { + userId: this.user.id, message }); } @autobind private async accept(accept: boolean) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + if (this.user == null) return; + + const game = await ReversiGames.findOne(this.gameId!); + if (game == null) throw new Error('game not found'); if (game.isStarted) return; let bothAccepted = false; - if (game.user1Id.equals(this.user._id)) { - await ReversiGame.update({ _id: this.gameId }, { - $set: { - user1Accepted: accept - } + if (game.user1Id === this.user.id) { + await ReversiGames.update(this.gameId!, { + user1Accepted: accept }); - publishReversiGameStream(this.gameId, 'changeAccepts', { + publishReversiGameStream(this.gameId!, 'changeAccepts', { user1: accept, user2: game.user2Accepted }); if (accept && game.user2Accepted) bothAccepted = true; - } else if (game.user2Id.equals(this.user._id)) { - await ReversiGame.update({ _id: this.gameId }, { - $set: { - user2Accepted: accept - } + } else if (game.user2Id === this.user.id) { + await ReversiGames.update(this.gameId!, { + user2Accepted: accept }); - publishReversiGameStream(this.gameId, 'changeAccepts', { + publishReversiGameStream(this.gameId!, 'changeAccepts', { user1: game.user1Accepted, user2: accept }); @@ -161,15 +170,15 @@ export default class extends Channel { if (bothAccepted) { // 3秒後、まだacceptされていたらゲーム開始 setTimeout(async () => { - const freshGame = await ReversiGame.findOne({ _id: this.gameId }); + const freshGame = await ReversiGames.findOne(this.gameId!); if (freshGame == null || freshGame.isStarted || freshGame.isEnded) return; if (!freshGame.user1Accepted || !freshGame.user2Accepted) return; let bw: number; - if (freshGame.settings.bw == 'random') { + if (freshGame.bw == 'random') { bw = Math.random() > 0.5 ? 1 : 2; } else { - bw = freshGame.settings.bw as number; + bw = parseInt(freshGame.bw, 10); } function getRandomMap() { @@ -178,22 +187,20 @@ export default class extends Channel { return Object.values(maps)[rnd].data; } - const map = freshGame.settings.map != null ? freshGame.settings.map : getRandomMap(); + const map = freshGame.map != null ? freshGame.map : getRandomMap(); - await ReversiGame.update({ _id: this.gameId }, { - $set: { - startedAt: new Date(), - isStarted: true, - black: bw, - 'settings.map': map - } + await ReversiGames.update(this.gameId!, { + startedAt: new Date(), + isStarted: true, + black: bw, + map: map }); //#region 盤面に最初から石がないなどして始まった瞬間に勝敗が決定する場合があるのでその処理 const o = new Reversi(map, { - isLlotheo: freshGame.settings.isLlotheo, - canPutEverywhere: freshGame.settings.canPutEverywhere, - loopedBoard: freshGame.settings.loopedBoard + isLlotheo: freshGame.isLlotheo, + canPutEverywhere: freshGame.canPutEverywhere, + loopedBoard: freshGame.loopedBoard }); if (o.isEnded) { @@ -206,23 +213,20 @@ export default class extends Channel { winner = null; } - await ReversiGame.update({ - _id: this.gameId - }, { - $set: { - isEnded: true, - winnerId: winner - } - }); + await ReversiGames.update(this.gameId!, { + isEnded: true, + winnerId: winner + }); - publishReversiGameStream(this.gameId, 'ended', { + publishReversiGameStream(this.gameId!, 'ended', { winnerId: winner, - game: await pack(this.gameId, this.user) + game: await ReversiGames.pack(this.gameId!, this.user) }); } //#endregion - publishReversiGameStream(this.gameId, 'started', await pack(this.gameId, this.user)); + publishReversiGameStream(this.gameId!, 'started', + await ReversiGames.pack(this.gameId!, this.user)); }, 3000); } } @@ -230,16 +234,19 @@ export default class extends Channel { // 石を打つ @autobind private async set(pos: number) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + if (this.user == null) return; + + const game = await ReversiGames.findOne(this.gameId!); + if (game == null) throw new Error('game not found'); if (!game.isStarted) return; if (game.isEnded) return; - if (!game.user1Id.equals(this.user._id) && !game.user2Id.equals(this.user._id)) return; + if ((game.user1Id !== this.user.id) && (game.user2Id !== this.user.id)) return; - const o = new Reversi(game.settings.map, { - isLlotheo: game.settings.isLlotheo, - canPutEverywhere: game.settings.canPutEverywhere, - loopedBoard: game.settings.loopedBoard + const o = new Reversi(game.map, { + isLlotheo: game.isLlotheo, + canPutEverywhere: game.canPutEverywhere, + loopedBoard: game.loopedBoard }); for (const log of game.logs) { @@ -247,7 +254,7 @@ export default class extends Channel { } const myColor = - (game.user1Id.equals(this.user._id) && game.black == 1) || (game.user2Id.equals(this.user._id) && game.black == 2) + ((game.user1Id === this.user.id) && game.black == 1) || ((game.user2Id === this.user.id) && game.black == 2) ? true : false; @@ -271,36 +278,33 @@ export default class extends Channel { pos }; - const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()); + const crc32 = CRC32.str(game.logs.map(x => x.pos.toString()).join('') + pos.toString()).toString(); - await ReversiGame.update({ - _id: this.gameId - }, { - $set: { - crc32, - isEnded: o.isEnded, - winnerId: winner - }, - $push: { - logs: log - } - }); + game.logs.push(log); + + await ReversiGames.update(this.gameId!, { + crc32, + isEnded: o.isEnded, + winnerId: winner, + logs: game.logs + }); - publishReversiGameStream(this.gameId, 'set', Object.assign(log, { + publishReversiGameStream(this.gameId!, 'set', Object.assign(log, { next: o.turn })); if (o.isEnded) { - publishReversiGameStream(this.gameId, 'ended', { + publishReversiGameStream(this.gameId!, 'ended', { winnerId: winner, - game: await pack(this.gameId, this.user) + game: await ReversiGames.pack(this.gameId!, this.user) }); } } @autobind private async check(crc32: string) { - const game = await ReversiGame.findOne({ _id: this.gameId }); + const game = await ReversiGames.findOne(this.gameId!); + if (game == null) throw new Error('game not found'); if (!game.isStarted) return; @@ -308,7 +312,7 @@ export default class extends Channel { if (game.crc32 == null) return; if (crc32 !== game.crc32) { - this.send('rescue', await pack(game, this.user)); + this.send('rescue', await ReversiGames.pack(game, this.user)); } } } diff --git a/src/server/api/stream/channels/games/reversi.ts b/src/server/api/stream/channels/games/reversi.ts index 1b1ad187a3..3db338386a 100644 --- a/src/server/api/stream/channels/games/reversi.ts +++ b/src/server/api/stream/channels/games/reversi.ts @@ -1,8 +1,7 @@ import autobind from 'autobind-decorator'; -import * as mongo from 'mongodb'; -import Matching, { pack } from '../../../../../models/games/reversi/matching'; import { publishMainStream } from '../../../../../services/stream'; import Channel from '../../channel'; +import { ReversiMatchings } from '../../../../../models'; export default class extends Channel { public readonly chName = 'gamesReversi'; @@ -12,7 +11,7 @@ export default class extends Channel { @autobind public async init(params: any) { // Subscribe reversi stream - this.subscriber.on(`reversiStream:${this.user._id}`, data => { + this.subscriber.on(`reversiStream:${this.user!.id}`, data => { this.send(data); }); } @@ -22,12 +21,12 @@ export default class extends Channel { switch (type) { case 'ping': if (body.id == null) return; - const matching = await Matching.findOne({ - parentId: this.user._id, - childId: new mongo.ObjectID(body.id) + const matching = await ReversiMatchings.findOne({ + parentId: this.user!.id, + childId: body.id }); if (matching == null) return; - publishMainStream(matching.childId, 'reversiInvited', await pack(matching, matching.childId)); + publishMainStream(matching.childId, 'reversiInvited', await ReversiMatchings.pack(matching, matching.childId)); break; } } diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts index b3689d47f5..bfb7697ba7 100644 --- a/src/server/api/stream/channels/global-timeline.ts +++ b/src/server/api/stream/channels/global-timeline.ts @@ -1,17 +1,14 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; import fetchMeta from '../../../../misc/fetch-meta'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'globalTimeline'; public static shouldShare = true; public static requireCredential = false; - private mutedUserIds: string[] = []; - @autobind public async init(params: any) { const meta = await fetchMeta(); @@ -20,29 +17,26 @@ export default class extends Channel { } // Subscribe events - this.subscriber.on('globalTimeline', this.onNote); - - const mute = await Mute.find({ muterId: this.user._id }); - this.mutedUserIds = mute.map(m => m.muteeId.toString()); + this.subscriber.on('notesStream', this.onNote); } @autobind private async onNote(note: any) { // リプライなら再pack if (note.replyId != null) { - note.reply = await pack(note.replyId, this.user, { + note.reply = await Notes.pack(note.replyId, this.user, { detail: true }); } // Renoteなら再pack if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { + note.renote = await Notes.pack(note.renoteId, this.user, { detail: true }); } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (shouldMuteThisNote(note, this.mutedUserIds)) return; + if (shouldMuteThisNote(note, this.muting)) return; this.send('note', note); } @@ -50,6 +44,6 @@ export default class extends Channel { @autobind public dispose() { // Unsubscribe events - this.subscriber.off('globalTimeline', this.onNote); + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts index 586ce02f06..36c56c7ab6 100644 --- a/src/server/api/stream/channels/hashtag.ts +++ b/src/server/api/stream/channels/hashtag.ts @@ -1,40 +1,46 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'hashtag'; public static shouldShare = false; public static requireCredential = false; + private q: string[][]; @autobind public async init(params: any) { - const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null; - const mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; + this.q = params.q; - const q: string[][] = params.q; - - if (q == null) return; + if (this.q == null) return; // Subscribe stream - this.subscriber.on('hashtag', async note => { - const noteTags = note.tags.map((t: string) => t.toLowerCase()); - const matched = q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase()))); - if (!matched) return; + this.subscriber.on('notesStream', this.onNote); + } + + @autobind + private async onNote(note: any) { + const noteTags = note.tags.map((t: string) => t.toLowerCase()); + const matched = this.q.some(tags => tags.every(tag => noteTags.includes(tag.toLowerCase()))); + if (!matched) return; - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { - detail: true - }); - } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } - // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (shouldMuteThisNote(note, mutedUserIds)) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (shouldMuteThisNote(note, this.muting)) return; - this.send('note', note); - }); + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts index 3c0b238720..61960657b4 100644 --- a/src/server/api/stream/channels/home-timeline.ts +++ b/src/server/api/stream/channels/home-timeline.ts @@ -1,42 +1,49 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'homeTimeline'; public static shouldShare = true; public static requireCredential = true; - private mutedUserIds: string[] = []; - @autobind public async init(params: any) { // Subscribe events - this.subscriber.on(`homeTimeline:${this.user._id}`, this.onNote); - - const mute = await Mute.find({ muterId: this.user._id }); - this.mutedUserIds = mute.map(m => m.muteeId.toString()); + this.subscriber.on('notesStream', this.onNote); } @autobind private async onNote(note: any) { - // リプライなら再pack - if (note.replyId != null) { - note.reply = await pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { + // その投稿のユーザーをフォローしていなかったら弾く + if (this.user!.id !== note.userId && !this.following.includes(note.userId)) return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await Notes.pack(note.id, this.user!, { detail: true }); + + if (note.isHidden) { + return; + } + } else { + // リプライなら再pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user!, { + detail: true + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user!, { + detail: true + }); + } } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (shouldMuteThisNote(note, this.mutedUserIds)) return; + if (shouldMuteThisNote(note, this.muting)) return; this.send('note', note); } @@ -44,6 +51,6 @@ export default class extends Channel { @autobind public dispose() { // Unsubscribe events - this.subscriber.off(`homeTimeline:${this.user._id}`, this.onNote); + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts index 35ef17b56b..18e6aa8350 100644 --- a/src/server/api/stream/channels/hybrid-timeline.ts +++ b/src/server/api/stream/channels/hybrid-timeline.ts @@ -1,47 +1,57 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; import fetchMeta from '../../../../misc/fetch-meta'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'hybridTimeline'; public static shouldShare = true; public static requireCredential = true; - private mutedUserIds: string[] = []; - @autobind public async init(params: any) { const meta = await fetchMeta(); - if (meta.disableLocalTimeline && !this.user.isAdmin && !this.user.isModerator) return; + if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return; // Subscribe events - this.subscriber.on('hybridTimeline', this.onNewNote); - this.subscriber.on(`hybridTimeline:${this.user._id}`, this.onNewNote); - - const mute = await Mute.find({ muterId: this.user._id }); - this.mutedUserIds = mute.map(m => m.muteeId.toString()); + this.subscriber.on('notesStream', this.onNote); } @autobind - private async onNewNote(note: any) { - // リプライなら再pack - if (note.replyId != null) { - note.reply = await pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { + private async onNote(note: any) { + // 自分自身の投稿 または その投稿のユーザーをフォローしている または ローカルの投稿 の場合だけ + if (!( + this.user!.id === note.userId || + this.following.includes(note.userId) || + note.user.host == null + )) return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await Notes.pack(note.id, this.user!, { detail: true }); - } + + if (note.isHidden) { + return; + } + } else { + // リプライなら再pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user!, { + detail: true + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user!, { + detail: true + }); + } + } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (shouldMuteThisNote(note, this.mutedUserIds)) return; + if (shouldMuteThisNote(note, this.muting)) return; this.send('note', note); } @@ -49,7 +59,6 @@ export default class extends Channel { @autobind public dispose() { // Unsubscribe events - this.subscriber.off('hybridTimeline', this.onNewNote); - this.subscriber.off(`hybridTimeline:${this.user._id}`, this.onNewNote); + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts index 3402023192..4aec2d66b4 100644 --- a/src/server/api/stream/channels/local-timeline.ts +++ b/src/server/api/stream/channels/local-timeline.ts @@ -1,17 +1,14 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; -import { pack } from '../../../../models/note'; import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; import Channel from '../channel'; import fetchMeta from '../../../../misc/fetch-meta'; +import { Notes } from '../../../../models'; export default class extends Channel { public readonly chName = 'localTimeline'; public static shouldShare = true; public static requireCredential = false; - private mutedUserIds: string[] = []; - @autobind public async init(params: any) { const meta = await fetchMeta(); @@ -20,29 +17,39 @@ export default class extends Channel { } // Subscribe events - this.subscriber.on('localTimeline', this.onNote); - - const mute = this.user ? await Mute.find({ muterId: this.user._id }) : null; - this.mutedUserIds = mute ? mute.map(m => m.muteeId.toString()) : []; + this.subscriber.on('notesStream', this.onNote); } @autobind private async onNote(note: any) { - // リプライなら再pack - if (note.replyId != null) { - note.reply = await pack(note.replyId, this.user, { - detail: true - }); - } - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, this.user, { + if (note.user.host !== null) return; + if (note.visibility === 'home') return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await Notes.pack(note.id, this.user, { detail: true }); + + if (note.isHidden) { + return; + } + } else { + // リプライなら再pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user, { + detail: true + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } } // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する - if (shouldMuteThisNote(note, this.mutedUserIds)) return; + if (shouldMuteThisNote(note, this.muting)) return; this.send('note', note); } @@ -50,6 +57,6 @@ export default class extends Channel { @autobind public dispose() { // Unsubscribe events - this.subscriber.off('localTimeline', this.onNote); + this.subscriber.off('notesStream', this.onNote); } } diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts index 175d914fa5..1100f87acb 100644 --- a/src/server/api/stream/channels/main.ts +++ b/src/server/api/stream/channels/main.ts @@ -1,6 +1,6 @@ import autobind from 'autobind-decorator'; -import Mute from '../../../../models/mute'; import Channel from '../channel'; +import { Mutings } from '../../../../models'; export default class extends Channel { public readonly chName = 'main'; @@ -9,16 +9,15 @@ export default class extends Channel { @autobind public async init(params: any) { - const mute = await Mute.find({ muterId: this.user._id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); + const mute = await Mutings.find({ muterId: this.user!.id }); // Subscribe main stream channel - this.subscriber.on(`mainStream:${this.user._id}`, async data => { + this.subscriber.on(`mainStream:${this.user!.id}`, async data => { const { type, body } = data; switch (type) { case 'notification': { - if (mutedUserIds.includes(body.userId)) return; + if (mute.map(m => m.muteeId).includes(body.userId)) return; if (body.note && body.note.isHidden) return; break; } diff --git a/src/server/api/stream/channels/messaging-index.ts b/src/server/api/stream/channels/messaging-index.ts index 148ff7f935..0c495398ab 100644 --- a/src/server/api/stream/channels/messaging-index.ts +++ b/src/server/api/stream/channels/messaging-index.ts @@ -9,7 +9,7 @@ export default class extends Channel { @autobind public async init(params: any) { // Subscribe messaging index stream - this.subscriber.on(`messagingIndexStream:${this.user._id}`, data => { + this.subscriber.on(`messagingIndexStream:${this.user!.id}`, data => { this.send(data); }); } diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts index 0d81b4e45c..8397f849ff 100644 --- a/src/server/api/stream/channels/messaging.ts +++ b/src/server/api/stream/channels/messaging.ts @@ -14,7 +14,7 @@ export default class extends Channel { this.otherpartyId = params.otherparty as string; // Subscribe messaging stream - this.subscriber.on(`messagingStream:${this.user._id}-${this.otherpartyId}`, data => { + this.subscriber.on(`messagingStream:${this.user!.id}-${this.otherpartyId}`, data => { this.send(data); }); } @@ -23,7 +23,7 @@ export default class extends Channel { public onMessage(type: string, body: any) { switch (type) { case 'read': - read(this.user._id, this.otherpartyId, body.id); + read(this.user!.id, this.otherpartyId, body.id); break; } } diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts index 5debf41770..f5434b8f08 100644 --- a/src/server/api/stream/channels/user-list.ts +++ b/src/server/api/stream/channels/user-list.ts @@ -1,23 +1,81 @@ import autobind from 'autobind-decorator'; import Channel from '../channel'; -import { pack } from '../../../../models/note'; +import { Notes, UserListJoinings } from '../../../../models'; +import shouldMuteThisNote from '../../../../misc/should-mute-this-note'; +import { User } from '../../../../models/entities/user'; export default class extends Channel { public readonly chName = 'userList'; public static shouldShare = false; public static requireCredential = false; + private listId: string; + public listUsers: User['id'][] = []; + private listUsersClock: NodeJS.Timer; @autobind public async init(params: any) { - const listId = params.listId as string; + this.listId = params.listId as string; // Subscribe stream - this.subscriber.on(`userListStream:${listId}`, async data => { - // 再パック - if (data.type == 'note') data.body = await pack(data.body.id, this.user, { + this.subscriber.on(`userListStream:${this.listId}`, this.send); + + this.subscriber.on('notesStream', this.onNote); + + this.updateListUsers(); + this.listUsersClock = setInterval(this.updateListUsers, 5000); + } + + @autobind + private async updateListUsers() { + const users = await UserListJoinings.find({ + where: { + userListId: this.listId, + }, + select: ['userId'] + }); + + this.listUsers = users.map(x => x.userId); + } + + @autobind + private async onNote(note: any) { + if (!this.listUsers.includes(note.userId)) return; + + if (['followers', 'specified'].includes(note.visibility)) { + note = await Notes.pack(note.id, this.user, { detail: true }); - this.send(data); - }); + + if (note.isHidden) { + return; + } + } else { + // リプライなら再pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user, { + detail: true + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (shouldMuteThisNote(note, this.muting)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off(`userListStream:${this.listId}`, this.send); + this.subscriber.off('notesStream', this.onNote); + + clearInterval(this.listUsersClock); } } diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index 22f7646cb9..f73f3229d5 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -1,40 +1,50 @@ import autobind from 'autobind-decorator'; import * as websocket from 'websocket'; - -import User, { IUser } from '../../../models/user'; -import readNotification from '../common/read-notification'; +import { readNotification } from '../common/read-notification'; import call from '../call'; -import { IApp } from '../../../models/app'; import readNote from '../../../services/note/read'; - import Channel from './channel'; import channels from './channels'; import { EventEmitter } from 'events'; +import { User } from '../../../models/entities/user'; +import { App } from '../../../models/entities/app'; +import { Users, Followings, Mutings } from '../../../models'; /** * Main stream connection */ export default class Connection { - public user?: IUser; - public app: IApp; + public user?: User; + public following: User['id'][] = []; + public muting: User['id'][] = []; + public app: App; private wsConnection: websocket.connection; public subscriber: EventEmitter; private channels: Channel[] = []; private subscribingNotes: any = {}; - public sendMessageToWsOverride: any = null; // 後方互換性のため + private followingClock: NodeJS.Timer; + private mutingClock: NodeJS.Timer; constructor( wsConnection: websocket.connection, subscriber: EventEmitter, - user: IUser, - app: IApp + user: User | null | undefined, + app: App | null | undefined ) { this.wsConnection = wsConnection; - this.user = user; - this.app = app; this.subscriber = subscriber; + if (user) this.user = user; + if (app) this.app = app; this.wsConnection.on('message', this.onWsConnectionMessage); + + if (this.user) { + this.updateFollowing(); + this.followingClock = setInterval(this.updateFollowing, 5000); + + this.updateMuting(); + this.mutingClock = setInterval(this.updateMuting, 5000); + } } /** @@ -42,6 +52,8 @@ export default class Connection { */ @autobind private async onWsConnectionMessage(data: websocket.IMessage) { + if (data.utf8Data == null) return; + const { type, body } = JSON.parse(data.utf8Data); switch (type) { @@ -64,7 +76,7 @@ export default class Connection { @autobind private async onApiRequest(payload: any) { // 新鮮なデータを利用するためにユーザーをフェッチ - const user = this.user ? await User.findOne({ _id: this.user._id }) : null; + const user = this.user ? await Users.findOne(this.user.id) : null; const endpoint = payload.endpoint || payload.ep; // alias @@ -79,7 +91,7 @@ export default class Connection { @autobind private onReadNotification(payload: any) { if (!payload.id) return; - readNotification(this.user._id, payload.id); + readNotification(this.user!.id, [payload.id]); } /** @@ -99,8 +111,8 @@ export default class Connection { this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage); } - if (payload.read) { - readNote(this.user._id, payload.id); + if (payload.read && this.user) { + readNote(this.user.id, payload.id); } } @@ -150,7 +162,6 @@ export default class Connection { */ @autobind public sendMessageToWs(type: string, payload: any) { - if (this.sendMessageToWsOverride) return this.sendMessageToWsOverride(type, payload); // 後方互換性のため this.wsConnection.send(JSON.stringify({ type: type, body: payload @@ -208,13 +219,40 @@ export default class Connection { } } + @autobind + private async updateFollowing() { + const followings = await Followings.find({ + where: { + followerId: this.user!.id + }, + select: ['followeeId'] + }); + + this.following = followings.map(x => x.followeeId); + } + + @autobind + private async updateMuting() { + const mutings = await Mutings.find({ + where: { + muterId: this.user!.id + }, + select: ['muteeId'] + }); + + this.muting = mutings.map(x => x.muteeId); + } + /** * ストリームが切れたとき */ @autobind public dispose() { for (const c of this.channels.filter(c => c.dispose)) { - c.dispose(); + if (c.dispose) c.dispose(); } + + if (this.followingClock) clearInterval(this.followingClock); + if (this.mutingClock) clearInterval(this.mutingClock); } } diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index f8f3c0ff4a..902c62ef98 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -1,7 +1,6 @@ import * as http from 'http'; import * as websocket from 'websocket'; import * as redis from 'redis'; -import Xev from 'xev'; import MainStreamConnection from './stream'; import { ParsedUrlQuery } from 'querystring'; @@ -23,58 +22,27 @@ module.exports = (server: http.Server) => { let ev: EventEmitter; - if (config.redis) { - // Connect to Redis - const subscriber = redis.createClient( - config.redis.port, config.redis.host); + // Connect to Redis + const subscriber = redis.createClient( + config.redis.port, config.redis.host); - subscriber.subscribe('misskey'); + subscriber.subscribe('misskey'); - ev = new EventEmitter(); + ev = new EventEmitter(); - subscriber.on('message', async (_, data) => { - const obj = JSON.parse(data); + subscriber.on('message', async (_, data) => { + const obj = JSON.parse(data); - ev.emit(obj.channel, obj.message); - }); + ev.emit(obj.channel, obj.message); + }); - connection.once('close', () => { - subscriber.unsubscribe(); - subscriber.quit(); - }); - } else { - ev = new Xev(); - } + connection.once('close', () => { + subscriber.unsubscribe(); + subscriber.quit(); + }); const main = new MainStreamConnection(connection, ev, user, app); - // 後方互換性のため - if (request.resourceURL.pathname !== '/streaming') { - main.sendMessageToWsOverride = (type: string, payload: any) => { - if (type == 'channel') { - type = payload.type; - payload = payload.body; - } - if (type.startsWith('api:')) { - type = type.replace('api:', 'api-res:'); - } - connection.send(JSON.stringify({ - type: type, - body: payload - })); - }; - - main.connectChannel(Math.random().toString().substr(2, 8), null, - request.resourceURL.pathname === '/' ? 'homeTimeline' : - request.resourceURL.pathname === '/local-timeline' ? 'localTimeline' : - request.resourceURL.pathname === '/hybrid-timeline' ? 'hybridTimeline' : - request.resourceURL.pathname === '/global-timeline' ? 'globalTimeline' : null); - - if (request.resourceURL.pathname === '/') { - main.connectChannel(Math.random().toString().substr(2, 8), null, 'main'); - } - } - connection.once('close', () => { ev.removeAllListeners(); main.dispose(); diff --git a/src/server/file/index.ts b/src/server/file/index.ts index 973528da33..e3487a2636 100644 --- a/src/server/file/index.ts +++ b/src/server/file/index.ts @@ -33,8 +33,8 @@ router.get('/app-default.jpg', ctx => { ctx.body = file; }); -router.get('/:id', sendDriveFile); -router.get('/:id/*', sendDriveFile); +router.get('/:key', sendDriveFile); +router.get('/:key/*', sendDriveFile); // Register router app.use(router.routes()); diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts index b22124240a..5da3d79eb5 100644 --- a/src/server/file/send-drive-file.ts +++ b/src/server/file/send-drive-file.ts @@ -1,12 +1,10 @@ import * as Koa from 'koa'; import * as send from 'koa-send'; -import * as mongodb from 'mongodb'; import * as rename from 'rename'; -import DriveFile, { getDriveFileBucket } from '../../models/drive-file'; -import DriveFileThumbnail, { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; -import DriveFileWebpublic, { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic'; import { serverLogger } from '..'; import { contentDisposition } from '../../misc/content-disposition'; +import { DriveFiles } from '../../models'; +import { InternalStorage } from '../../services/drive/internal-storage'; const assets = `${__dirname}/../../server/file/assets/`; @@ -16,16 +14,14 @@ const commonReadableHandlerGenerator = (ctx: Koa.BaseContext) => (e: Error): voi }; export default async function(ctx: Koa.BaseContext) { - // Validate id - if (!mongodb.ObjectID.isValid(ctx.params.id)) { - ctx.throw(400, 'incorrect id'); - return; - } - - const fileId = new mongodb.ObjectID(ctx.params.id); + const key = ctx.params.key; // Fetch drive file - const file = await DriveFile.findOne({ _id: fileId }); + const file = await DriveFiles.createQueryBuilder('file') + .where('file.accessKey = :accessKey', { accessKey: key }) + .orWhere('file.thumbnailAccessKey = :thumbnailAccessKey', { thumbnailAccessKey: key }) + .orWhere('file.webpublicAccessKey = :webpublicAccessKey', { webpublicAccessKey: key }) + .getOne(); if (file == null) { ctx.status = 404; @@ -33,69 +29,30 @@ export default async function(ctx: Koa.BaseContext) { return; } - if (file.metadata.deletedAt) { - ctx.status = 410; - await send(ctx as any, '/tombstone.png', { root: assets }); - return; - } - - if (file.metadata.withoutChunks) { + if (!file.storedInternal) { ctx.status = 204; return; } - const sendRaw = async () => { - if (file.metadata && file.metadata.accessKey && file.metadata.accessKey != ctx.query['original']) { - ctx.status = 403; - return; - } + const isThumbnail = file.thumbnailAccessKey === key; + const isWebpublic = file.webpublicAccessKey === key; - const bucket = await getDriveFileBucket(); - const readable = bucket.openDownloadStream(fileId); - readable.on('error', commonReadableHandlerGenerator(ctx)); - ctx.set('Content-Type', file.contentType); - ctx.body = readable; - }; - - if ('thumbnail' in ctx.query) { - const thumb = await DriveFileThumbnail.findOne({ - 'metadata.originalId': fileId - }); - - if (thumb != null) { - ctx.set('Content-Type', 'image/jpeg'); - ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-thumb', extname: '.jpeg' })}`)); - const bucket = await getDriveFileThumbnailBucket(); - ctx.body = bucket.openDownloadStream(thumb._id); - } else { - if (file.contentType.startsWith('image/')) { - ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}`)); - await sendRaw(); - } else { - ctx.status = 404; - await send(ctx as any, '/thumbnail-not-available.png', { root: assets }); - } - } - } else if ('web' in ctx.query) { - const web = await DriveFileWebpublic.findOne({ - 'metadata.originalId': fileId - }); - - if (web != null) { - ctx.set('Content-Type', file.contentType); - ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.filename, { suffix: '-web' })}`)); - - const bucket = await getDriveFileWebpublicBucket(); - ctx.body = bucket.openDownloadStream(web._id); - } else { - ctx.set('Content-Disposition', contentDisposition('inline', `${file.filename}`)); - await sendRaw(); - } + if (isThumbnail) { + ctx.set('Content-Type', 'image/jpeg'); + ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-thumb', extname: '.jpeg' })}`)); + ctx.body = InternalStorage.read(key); + } else if (isWebpublic) { + ctx.set('Content-Type', file.type); + ctx.set('Content-Disposition', contentDisposition('inline', `${rename(file.name, { suffix: '-web' })}`)); + ctx.body = InternalStorage.read(key); } else { if ('download' in ctx.query) { - ctx.set('Content-Disposition', contentDisposition('attachment', `${file.filename}`)); + ctx.set('Content-Disposition', contentDisposition('attachment', `${file.name}`)); } - await sendRaw(); + const readable = InternalStorage.read(file.accessKey!); + readable.on('error', commonReadableHandlerGenerator(ctx)); + ctx.set('Content-Type', file.type); + ctx.body = readable; } } diff --git a/src/server/index.ts b/src/server/index.ts index 7c51923f9e..601e288f3b 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -19,12 +19,12 @@ import activityPub from './activitypub'; import nodeinfo from './nodeinfo'; import wellKnown from './well-known'; import config from '../config'; -import networkChart from '../services/chart/network'; import apiServer from './api'; import { sum } from '../prelude/array'; -import User from '../models/user'; import Logger from '../services/logger'; import { program } from '../argv'; +import { UserProfiles } from '../models'; +import { networkChart } from '../services/chart'; export const serverLogger = new Logger('server', 'gray', false); @@ -32,7 +32,7 @@ export const serverLogger = new Logger('server', 'gray', false); const app = new Koa(); app.proxy = true; -if (!['production', 'test'].includes(process.env.NODE_ENV)) { +if (!['production', 'test'].includes(process.env.NODE_ENV || '')) { // Logger app.use(koaLogger(str => { serverLogger.info(str); @@ -73,17 +73,17 @@ router.use(nodeinfo.routes()); router.use(wellKnown.routes()); router.get('/verify-email/:code', async ctx => { - const user = await User.findOne({ emailVerifyCode: ctx.params.code }); + const profile = await UserProfiles.findOne({ + emailVerifyCode: ctx.params.code + }); - if (user != null) { + if (profile != null) { ctx.body = 'Verify succeeded!'; ctx.status = 200; - User.update({ _id: user._id }, { - $set: { - emailVerified: true, - emailVerifyCode: null - } + UserProfiles.update({ userId: profile.userId }, { + emailVerified: true, + emailVerifyCode: null }); } else { ctx.status = 404; diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts index a783eea90b..686412383e 100644 --- a/src/server/nodeinfo.ts +++ b/src/server/nodeinfo.ts @@ -20,7 +20,24 @@ export const links = [/* (awaiting release) { const nodeinfo2 = async () => { const [ - { name, description, maintainer, langs, announcements, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker }, + { + name, + description, + maintainerName, + maintainerEmail, + langs, + announcements, + disableRegistration, + disableLocalTimeline, + disableGlobalTimeline, + enableRecaptcha, + maxNoteTextLength, + enableTwitterIntegration, + enableGithubIntegration, + enableDiscordIntegration, + enableEmail, + enableServiceWorker + }, // total, // activeHalfyear, // activeMonth, @@ -52,7 +69,26 @@ const nodeinfo2 = async () => { // localPosts, // localComments }, - metadata: { name, description, maintainer, langs, announcements, disableRegistration, disableLocalTimeline, disableGlobalTimeline, enableRecaptcha, maxNoteTextLength, enableTwitterIntegration, enableGithubIntegration, enableDiscordIntegration, enableEmail, enableServiceWorker } + metadata: { + name, + description, + maintainer: { + name: maintainerName, + email: maintainerEmail + }, + langs, + announcements, + disableRegistration, + disableLocalTimeline, + disableGlobalTimeline, + enableRecaptcha, + maxNoteTextLength, + enableTwitterIntegration, + enableGithubIntegration, + enableDiscordIntegration, + enableEmail, + enableServiceWorker + } }; }; diff --git a/src/server/web/docs.ts b/src/server/web/docs.ts index d9ba14a8ed..374dbf3bd2 100644 --- a/src/server/web/docs.ts +++ b/src/server/web/docs.ts @@ -34,14 +34,14 @@ async function genVars(lang: string): Promise<{ [key: string]: any }> { const docs = glob.sync(`src/docs/**/*.${lang}.md`, { cwd }); vars['docs'] = {}; for (const x of docs) { - const [, name] = x.match(/docs\/(.+?)\.(.+?)\.md$/); + const [, name] = x.match(/docs\/(.+?)\.(.+?)\.md$/)!; if (vars['docs'][name] == null) { vars['docs'][name] = { name, title: {} }; } - vars['docs'][name]['title'][lang] = fs.readFileSync(cwd + x, 'utf-8').match(/^# (.+?)\r?\n/)[1]; + vars['docs'][name]['title'][lang] = fs.readFileSync(cwd + x, 'utf-8').match(/^# (.+?)\r?\n/)![1]; } vars['kebab'] = (string: string) => string.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/\s+/g, '-').toLowerCase(); @@ -97,7 +97,7 @@ router.get('/*/*', async ctx => { await ctx.render('../../../../src/docs/article', Object.assign({ id: doc, html: conv.makeHtml(md), - title: md.match(/^# (.+?)\r?\n/)[1], + title: md.match(/^# (.+?)\r?\n/)![1], src: `https://github.com/syuilo/misskey/tree/master/src/docs/${doc}.${lang}.md` }, await genVars(lang))); diff --git a/src/server/web/feed.ts b/src/server/web/feed.ts index 09ac10c576..6b660fe188 100644 --- a/src/server/web/feed.ts +++ b/src/server/web/feed.ts @@ -1,25 +1,26 @@ import { Feed } from 'feed'; import config from '../../config'; -import Note from '../../models/note'; -import { IUser } from '../../models/user'; -import { getOriginalUrl } from '../../misc/get-drive-file-url'; +import { User } from '../../models/entities/user'; +import { Notes, DriveFiles, UserProfiles } from '../../models'; +import { In } from 'typeorm'; +import { ensure } from '../../prelude/ensure'; -export default async function(user: IUser) { +export default async function(user: User) { const author: Author = { link: `${config.url}/@${user.username}`, name: user.name || user.username }; - const notes = await Note.find({ - userId: user._id, - renoteId: null, - $or: [ - { visibility: 'public' }, - { visibility: 'home' } - ] - }, { - sort: { createdAt: -1 }, - limit: 20 + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + + const notes = await Notes.find({ + where: { + userId: user.id, + renoteId: null, + visibility: In(['public', 'home']) + }, + order: { createdAt: -1 }, + take: 20 }); const feed = new Feed({ @@ -27,7 +28,7 @@ export default async function(user: IUser) { title: `${author.name} (@${user.username}@${config.host})`, updated: notes[0].createdAt, generator: 'Misskey', - description: `${user.notesCount} Notes, ${user.followingCount} Following, ${user.followersCount} Followers${user.description ? ` · ${user.description}` : ''}`, + description: `${user.notesCount} Notes, ${user.followingCount} Following, ${user.followersCount} Followers${profile.description ? ` · ${profile.description}` : ''}`, link: author.link, image: user.avatarUrl, feedLinks: { @@ -38,15 +39,18 @@ export default async function(user: IUser) { } as FeedOptions); for (const note of notes) { - const file = note._files && note._files.find(file => file.contentType.startsWith('image/')); + const files = note.fileIds.length > 0 ? await DriveFiles.find({ + id: In(note.fileIds) + }) : []; + const file = files.find(file => file.type.startsWith('image/')); feed.addItem({ title: `New note by ${author.name}`, - link: `${config.url}/notes/${note._id}`, + link: `${config.url}/notes/${note.id}`, date: note.createdAt, - description: note.cw, - content: note.text, - image: file && getOriginalUrl(file) + description: note.cw || undefined, + content: note.text || undefined, + image: file ? DriveFiles.getPublicUrl(file) || undefined : undefined }); } diff --git a/src/server/web/index.ts b/src/server/web/index.ts index d8525ba114..5cadf1b124 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -9,19 +9,17 @@ import * as Router from 'koa-router'; import * as send from 'koa-send'; import * as favicon from 'koa-favicon'; import * as views from 'koa-views'; -import { ObjectID } from 'mongodb'; import docs from './docs'; import packFeed from './feed'; -import User from '../../models/user'; -import parseAcct from '../../misc/acct/parse'; -import config from '../../config'; -import Note, { pack as packNote } from '../../models/note'; -import getNoteSummary from '../../misc/get-note-summary'; import fetchMeta from '../../misc/fetch-meta'; -import Emoji from '../../models/emoji'; import * as pkg from '../../../package.json'; import { genOpenapiSpec } from '../api/openapi/gen-spec'; +import config from '../../config'; +import { Users, Notes, Emojis, UserProfiles } from '../../models'; +import parseAcct from '../../misc/acct/parse'; +import getNoteSummary from '../../misc/get-note-summary'; +import { ensure } from '../../prelude/ensure'; const client = `${__dirname}/../../client/`; @@ -100,7 +98,7 @@ router.get('/api.json', async ctx => { const getFeed = async (acct: string) => { const { username, host } = parseAcct(acct); - const user = await User.findOne({ + const user = await Users.findOne({ usernameLower: username.toLowerCase(), host }); @@ -148,16 +146,17 @@ router.get('/@:user.json', async ctx => { // User router.get('/@:user', async (ctx, next) => { const { username, host } = parseAcct(ctx.params.user); - const user = await User.findOne({ + const user = await Users.findOne({ usernameLower: username.toLowerCase(), host }); if (user != null) { + const profile = await UserProfiles.findOne(user.id).then(ensure); const meta = await fetchMeta(); await ctx.render('user', { - user, - instanceName: meta.name + user, profile, + instanceName: meta.name || 'Misskey' }); ctx.set('Cache-Control', 'public, max-age=180'); } else { @@ -167,19 +166,12 @@ router.get('/@:user', async (ctx, next) => { }); router.get('/users/:user', async ctx => { - if (!ObjectID.isValid(ctx.params.user)) { - ctx.status = 404; - return; - } - - const userId = new ObjectID(ctx.params.user); - - const user = await User.findOne({ - _id: userId, + const user = await Users.findOne({ + id: ctx.params.user, host: null }); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -189,26 +181,24 @@ router.get('/users/:user', async ctx => { // Note router.get('/notes/:note', async ctx => { - if (ObjectID.isValid(ctx.params.note)) { - const note = await Note.findOne({ _id: ctx.params.note }); + const note = await Notes.findOne(ctx.params.note); - if (note) { - const _note = await packNote(note); - const meta = await fetchMeta(); - await ctx.render('note', { - note: _note, - summary: getNoteSummary(_note), - instanceName: meta.name - }); - - if (['public', 'home'].includes(note.visibility)) { - ctx.set('Cache-Control', 'public, max-age=180'); - } else { - ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); - } + if (note) { + const _note = await Notes.pack(note); + const meta = await fetchMeta(); + await ctx.render('note', { + note: _note, + summary: getNoteSummary(_note), + instanceName: meta.name || 'Misskey' + }); - return; + if (['public', 'home'].includes(note.visibility)) { + ctx.set('Cache-Control', 'public, max-age=180'); + } else { + ctx.set('Cache-Control', 'private, max-age=0, must-revalidate'); } + + return; } ctx.status = 404; @@ -217,10 +207,8 @@ router.get('/notes/:note', async ctx => { router.get('/info', async ctx => { const meta = await fetchMeta(); - const emojis = await Emoji.find({ host: null }, { - fields: { - _id: false - } + const emojis = await Emojis.find({ + where: { host: null } }); await ctx.render('info', { version: pkg.version, @@ -232,7 +220,9 @@ router.get('/info', async ctx => { cores: os.cpus().length }, emojis: emojis, - meta: meta + meta: meta, + originalUsersCount: await Users.count({ host: null }), + originalNotesCount: await Notes.count({ userHost: null }) }); }); @@ -247,7 +237,7 @@ router.get('*', async ctx => { const meta = await fetchMeta(); await ctx.render('base', { img: meta.bannerUrl, - title: meta.name, + title: meta.name || 'Misskey', desc: meta.description, icon: meta.iconUrl }); diff --git a/src/server/web/manifest.ts b/src/server/web/manifest.ts index 35d3d1b666..4acfb22de5 100644 --- a/src/server/web/manifest.ts +++ b/src/server/web/manifest.ts @@ -1,10 +1,9 @@ import * as Koa from 'koa'; import * as manifest from '../../client/assets/manifest.json'; -import * as deepcopy from 'deepcopy'; import fetchMeta from '../../misc/fetch-meta'; module.exports = async (ctx: Koa.BaseContext) => { - const json = deepcopy(manifest); + const json = JSON.parse(JSON.stringify(manifest)); const instance = await fetchMeta(); diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts index aed475e6ff..7d0080b4d2 100644 --- a/src/server/web/url-preview.ts +++ b/src/server/web/url-preview.ts @@ -43,7 +43,7 @@ module.exports = async (ctx: Koa.BaseContext) => { } }; -function wrap(url: string): string { +function wrap(url?: string): string | null { return url != null ? url.match(/^https?:\/\//) ? `${config.url}/proxy/preview.jpg?${query({ diff --git a/src/server/web/views/info.pug b/src/server/web/views/info.pug index 1c4b272a62..c8b0bd939a 100644 --- a/src/server/web/views/info.pug +++ b/src/server/web/views/info.pug @@ -70,15 +70,15 @@ html table tr th Instance - td= meta.name + td= meta.name || 'Misskey' tr th Description td= meta.description tr th Maintainer td - = meta.maintainer.name - | <#{meta.maintainer.email}> + = meta.maintainerName + | <#{meta.maintainerEmail}> tr th System td= os @@ -93,10 +93,10 @@ html td= cpu.model tr th Original users - td= meta.stats.originalUsersCount + td= originalUsersCount tr th Original notes - td= meta.stats.originalNotesCount + td= originalNotesCount tr th Registration td= !meta.disableRegistration ? 'yes' : 'no' diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug index 3f32933f52..bff98ba80f 100644 --- a/src/server/web/views/user.pug +++ b/src/server/web/views/user.pug @@ -9,12 +9,12 @@ block title = `${title} | ${instanceName}` block desc - meta(name='description' content= user.description) + meta(name='description' content= profile.description) block og meta(property='og:type' content='blog') meta(property='og:title' content= title) - meta(property='og:description' content= user.description) + meta(property='og:description' content= profile.description) meta(property='og:url' content= url) meta(property='og:image' content= img) @@ -24,12 +24,12 @@ block meta meta(name='twitter:card' content='summary') - if user.twitter - meta(name='twitter:creator' content=`@${user.twitter.screenName}`) + if profile.twitter + meta(name='twitter:creator' content=`@${profile.twitter.screenName}`) if !user.host - link(rel='alternate' href=`${config.url}/users/${user._id}` type='application/activity+json') + link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json') if user.uri link(rel='alternate' href=user.uri type='application/activity+json') - if user.url - link(rel='alternate' href=user.url type='text/html') + if profile.url + link(rel='alternate' href=profile.url type='text/html') diff --git a/src/server/well-known.ts b/src/server/well-known.ts index 18c080acc7..d29b7a8888 100644 --- a/src/server/well-known.ts +++ b/src/server/well-known.ts @@ -1,12 +1,12 @@ -import * as mongo from 'mongodb'; import * as Router from 'koa-router'; import config from '../config'; import parseAcct from '../misc/acct/parse'; -import User from '../models/user'; import Acct from '../misc/acct/type'; import { links } from './nodeinfo'; import { escapeAttribute, escapeValue } from '../prelude/xml'; +import { Users } from '../models'; +import { User } from '../models/entities/user'; // Init router const router = new Router(); @@ -47,19 +47,19 @@ router.get('/.well-known/nodeinfo', async ctx => { }); router.get(webFingerPath, async ctx => { + const fromId = (id: User['id']): Record<string, any> => ({ + id, + host: null + }); + const generateQuery = (resource: string) => resource.startsWith(`${config.url.toLowerCase()}/users/`) ? - fromId(new mongo.ObjectID(resource.split('/').pop())) : + fromId(resource.split('/').pop()!) : fromAcct(parseAcct( - resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop() : + resource.startsWith(`${config.url.toLowerCase()}/@`) ? resource.split('/').pop()! : resource.startsWith('acct:') ? resource.slice('acct:'.length) : resource)); - const fromId = (_id: mongo.ObjectID): Record<string, any> => ({ - _id, - host: null - }); - const fromAcct = (acct: Acct): Record<string, any> | number => !acct.host || acct.host === config.host.toLowerCase() ? { usernameLower: acct.username, @@ -78,9 +78,9 @@ router.get(webFingerPath, async ctx => { return; } - const user = await User.findOne(query); + const user = await Users.findOne(query); - if (user === null) { + if (user == null) { ctx.status = 404; return; } @@ -89,7 +89,7 @@ router.get(webFingerPath, async ctx => { const self = { rel: 'self', type: 'application/activity+json', - href: `${config.url}/users/${user._id}` + href: `${config.url}/users/${user.id}` }; const profilePage = { rel: 'http://webfinger.net/rel/profile-page', diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts index c20666ef26..def4f33585 100644 --- a/src/services/blocking/create.ts +++ b/src/services/blocking/create.ts @@ -1,6 +1,3 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; -import FollowRequest from '../../models/follow-request'; import { publishMainStream } from '../stream'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; @@ -8,11 +5,12 @@ import renderUndo from '../../remote/activitypub/renderer/undo'; import renderBlock from '../../remote/activitypub/renderer/block'; import { deliver } from '../../queue'; import renderReject from '../../remote/activitypub/renderer/reject'; -import perUserFollowingChart from '../../services/chart/per-user-following'; -import Blocking from '../../models/blocking'; - -export default async function(blocker: IUser, blockee: IUser) { +import { User } from '../../models/entities/user'; +import { Blockings, Users, FollowRequests, Followings } from '../../models'; +import { perUserFollowingChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +export default async function(blocker: User, blockee: User) { await Promise.all([ cancelRequest(blocker, blockee), cancelRequest(blockee, blocker), @@ -20,105 +18,90 @@ export default async function(blocker: IUser, blockee: IUser) { unFollow(blockee, blocker) ]); - await Blocking.insert({ + await Blockings.save({ + id: genId(), createdAt: new Date(), - blockerId: blocker._id, - blockeeId: blockee._id, + blockerId: blocker.id, + blockeeId: blockee.id, }); - if (isLocalUser(blocker) && isRemoteUser(blockee)) { + if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { const content = renderActivity(renderBlock(blocker, blockee)); deliver(blocker, content, blockee.inbox); } } -async function cancelRequest(follower: IUser, followee: IUser) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +async function cancelRequest(follower: User, followee: User) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); if (request == null) { return; } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id - }); - - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - if (isLocalUser(followee)) { - packUser(followee, followee, { + if (Users.isLocalUser(followee)) { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } // リモートにフォローリクエストをしていたらUndoFollow送信 - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } // リモートからフォローリクエストを受けていたらReject送信 - if (isRemoteUser(follower) && isLocalUser(followee)) { - const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee)); + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { + const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId!), followee)); deliver(followee, content, follower.inbox); } } -async function unFollow(follower: IUser, followee: IUser) { - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id +async function unFollow(follower: User, followee: User) { + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); if (following == null) { return; } - Following.remove({ - _id: following._id - }); + Followings.delete(following.id); //#region Decrement following count - User.update({ _id: follower._id }, { - $inc: { - followingCount: -1 - } - }); + Users.decrement({ id: follower.id }, 'followingCount', 1); //#endregion //#region Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: -1 - } - }); + Users.decrement({ id: followee.id }, 'followersCount', 1); //#endregion perUserFollowingChart.update(follower, followee, false); // Publish unfollow event - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } // リモートにフォローをしていたらUndoFollow送信 - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } diff --git a/src/services/blocking/delete.ts b/src/services/blocking/delete.ts index 099fa14b37..2c05cb7f3f 100644 --- a/src/services/blocking/delete.ts +++ b/src/services/blocking/delete.ts @@ -1,17 +1,17 @@ -import { isLocalUser, isRemoteUser, IUser } from '../../models/user'; -import Blocking from '../../models/blocking'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderBlock from '../../remote/activitypub/renderer/block'; import renderUndo from '../../remote/activitypub/renderer/undo'; import { deliver } from '../../queue'; import Logger from '../logger'; +import { User } from '../../models/entities/user'; +import { Blockings, Users } from '../../models'; const logger = new Logger('blocking/delete'); -export default async function(blocker: IUser, blockee: IUser) { - const blocking = await Blocking.findOne({ - blockerId: blocker._id, - blockeeId: blockee._id +export default async function(blocker: User, blockee: User) { + const blocking = await Blockings.findOne({ + blockerId: blocker.id, + blockeeId: blockee.id }); if (blocking == null) { @@ -19,12 +19,10 @@ export default async function(blocker: IUser, blockee: IUser) { return; } - Blocking.remove({ - _id: blocking._id - }); + Blockings.delete(blocking.id); // deliver if remote bloking - if (isLocalUser(blocker) && isRemoteUser(blockee)) { + if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker)); deliver(blocker, content, blockee.inbox); } diff --git a/src/services/chart/active-users.ts b/src/services/chart/active-users.ts deleted file mode 100644 index 2a4e1a97ac..0000000000 --- a/src/services/chart/active-users.ts +++ /dev/null @@ -1,48 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import { IUser, isLocalUser } from '../../models/user'; - -/** - * アクティブユーザーに関するチャート - */ -type ActiveUsersLog = { - local: { - /** - * アクティブユーザー数 - */ - count: number; - }; - - remote: ActiveUsersLog['local']; -}; - -class ActiveUsersChart extends Chart<ActiveUsersLog> { - constructor() { - super('activeUsers'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: ActiveUsersLog): Promise<ActiveUsersLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(user: IUser) { - const update: Obj = { - count: 1 - }; - - await this.incIfUnique({ - [isLocalUser(user) ? 'local' : 'remote']: update - }, 'users', user._id.toHexString()); - } -} - -export default new ActiveUsersChart(); diff --git a/src/services/chart/charts/classes/active-users.ts b/src/services/chart/charts/classes/active-users.ts new file mode 100644 index 0000000000..5128150de6 --- /dev/null +++ b/src/services/chart/charts/classes/active-users.ts @@ -0,0 +1,35 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/active-users'; + +type ActiveUsersLog = SchemaType<typeof schema>; + +export default class ActiveUsersChart extends Chart<ActiveUsersLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: ActiveUsersLog): DeepPartial<ActiveUsersLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<ActiveUsersLog>> { + return {}; + } + + @autobind + public async update(user: User) { + const update: Obj = { + count: 1 + }; + + await this.incIfUnique({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }, 'users', user.id); + } +} diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts new file mode 100644 index 0000000000..ae52df19ac --- /dev/null +++ b/src/services/chart/charts/classes/drive.ts @@ -0,0 +1,69 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles } from '../../../../models'; +import { Not } from 'typeorm'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/drive'; + +type DriveLog = SchemaType<typeof schema>; + +export default class DriveChart extends Chart<DriveLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: DriveLog): DeepPartial<DriveLog> { + return { + local: { + totalCount: latest.local.totalCount, + totalSize: latest.local.totalSize, + }, + remote: { + totalCount: latest.remote.totalCount, + totalSize: latest.remote.totalSize, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<DriveLog>> { + const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([ + DriveFiles.count({ userHost: null }), + DriveFiles.count({ userHost: Not(null) }), + DriveFiles.clacDriveUsageOfLocal(), + DriveFiles.clacDriveUsageOfRemote() + ]); + + return { + local: { + totalCount: localCount, + totalSize: localSize, + }, + remote: { + totalCount: remoteCount, + totalSize: remoteSize, + } + }; + } + + @autobind + public async update(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalCount = isAdditional ? 1 : -1; + update.totalSize = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incCount = 1; + update.incSize = file.size; + } else { + update.decCount = 1; + update.decSize = file.size; + } + + await this.inc({ + [file.userHost === null ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/classes/federation.ts b/src/services/chart/charts/classes/federation.ts new file mode 100644 index 0000000000..bd2c497e7b --- /dev/null +++ b/src/services/chart/charts/classes/federation.ts @@ -0,0 +1,51 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Instances } from '../../../../models'; +import { name, schema } from '../schemas/federation'; + +type FederationLog = SchemaType<typeof schema>; + +export default class FederationChart extends Chart<FederationLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: FederationLog): DeepPartial<FederationLog> { + return { + instance: { + total: latest.instance.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<FederationLog>> { + const [total] = await Promise.all([ + Instances.count({}) + ]); + + return { + instance: { + total: total, + } + }; + } + + @autobind + public async update(isAdditional: boolean) { + const update: Obj = {}; + + update.total = isAdditional ? 1 : -1; + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + await this.inc({ + instance: update + }); + } +} diff --git a/src/services/chart/charts/classes/hashtag.ts b/src/services/chart/charts/classes/hashtag.ts new file mode 100644 index 0000000000..38c3a94f0c --- /dev/null +++ b/src/services/chart/charts/classes/hashtag.ts @@ -0,0 +1,35 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/hashtag'; + +type HashtagLog = SchemaType<typeof schema>; + +export default class HashtagChart extends Chart<HashtagLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: HashtagLog): DeepPartial<HashtagLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<HashtagLog>> { + return {}; + } + + @autobind + public async update(hashtag: string, user: User) { + const update: Obj = { + count: 1 + }; + + await this.incIfUnique({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }, 'users', user.id, hashtag); + } +} diff --git a/src/services/chart/charts/classes/instance.ts b/src/services/chart/charts/classes/instance.ts new file mode 100644 index 0000000000..f3d341f383 --- /dev/null +++ b/src/services/chart/charts/classes/instance.ts @@ -0,0 +1,173 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles, Followings, Users, Notes } from '../../../../models'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/instance'; +import { Note } from '../../../../models/entities/note'; +import { toPuny } from '../../../../misc/convert-host'; + +type InstanceLog = SchemaType<typeof schema>; + +export default class InstanceChart extends Chart<InstanceLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: InstanceLog): DeepPartial<InstanceLog> { + return { + notes: { + total: latest.notes.total, + }, + users: { + total: latest.users.total, + }, + following: { + total: latest.following.total, + }, + followers: { + total: latest.followers.total, + }, + drive: { + totalFiles: latest.drive.totalFiles, + totalUsage: latest.drive.totalUsage, + } + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<InstanceLog>> { + const [ + notesCount, + usersCount, + followingCount, + followersCount, + driveFiles, + driveUsage, + ] = await Promise.all([ + Notes.count({ userHost: group }), + Users.count({ host: group }), + Followings.count({ followerHost: group }), + Followings.count({ followeeHost: group }), + DriveFiles.count({ userHost: group }), + DriveFiles.clacDriveUsageOfHost(group), + ]); + + return { + notes: { + total: notesCount, + }, + users: { + total: usersCount, + }, + following: { + total: followingCount, + }, + followers: { + total: followersCount, + }, + drive: { + totalFiles: driveFiles, + totalUsage: driveUsage, + } + }; + } + + @autobind + public async requestReceived(host: string) { + await this.inc({ + requests: { + received: 1 + } + }, toPuny(host)); + } + + @autobind + public async requestSent(host: string, isSucceeded: boolean) { + const update: Obj = {}; + + if (isSucceeded) { + update.succeeded = 1; + } else { + update.failed = 1; + } + + await this.inc({ + requests: update + }, toPuny(host)); + } + + @autobind + public async newUser(host: string) { + await this.inc({ + users: { + total: 1, + inc: 1 + } + }, toPuny(host)); + } + + @autobind + public async updateNote(host: string, note: Note, isAdditional: boolean) { + const diffs = {} as any; + + if (note.replyId != null) { + diffs.reply = isAdditional ? 1 : -1; + } else if (note.renoteId != null) { + diffs.renote = isAdditional ? 1 : -1; + } else { + diffs.normal = isAdditional ? 1 : -1; + } + + await this.inc({ + notes: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + diffs: diffs + } + }, toPuny(host)); + } + + @autobind + public async updateFollowing(host: string, isAdditional: boolean) { + await this.inc({ + following: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + } + }, toPuny(host)); + } + + @autobind + public async updateFollowers(host: string, isAdditional: boolean) { + await this.inc({ + followers: { + total: isAdditional ? 1 : -1, + inc: isAdditional ? 1 : 0, + dec: isAdditional ? 0 : 1, + } + }, toPuny(host)); + } + + @autobind + public async updateDrive(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalFiles = isAdditional ? 1 : -1; + update.totalUsage = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incFiles = 1; + update.incUsage = file.size; + } else { + update.decFiles = 1; + update.decUsage = file.size; + } + + await this.inc({ + drive: update + }, file.userHost); + } +} diff --git a/src/services/chart/charts/classes/network.ts b/src/services/chart/charts/classes/network.ts new file mode 100644 index 0000000000..8b26e5c4c2 --- /dev/null +++ b/src/services/chart/charts/classes/network.ts @@ -0,0 +1,34 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/network'; + +type NetworkLog = SchemaType<typeof schema>; + +export default class NetworkChart extends Chart<NetworkLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: NetworkLog): DeepPartial<NetworkLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<NetworkLog>> { + return {}; + } + + @autobind + public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) { + const inc: DeepPartial<NetworkLog> = { + incomingRequests: incomingRequests, + totalTime: time, + incomingBytes: incomingBytes, + outgoingBytes: outgoingBytes + }; + + await this.inc(inc); + } +} diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts new file mode 100644 index 0000000000..85ccf000d8 --- /dev/null +++ b/src/services/chart/charts/classes/notes.ts @@ -0,0 +1,71 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Notes } from '../../../../models'; +import { Not } from 'typeorm'; +import { Note } from '../../../../models/entities/note'; +import { name, schema } from '../schemas/notes'; + +type NotesLog = SchemaType<typeof schema>; + +export default class NotesChart extends Chart<NotesLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: NotesLog): DeepPartial<NotesLog> { + return { + local: { + total: latest.local.total, + }, + remote: { + total: latest.remote.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<NotesLog>> { + const [localCount, remoteCount] = await Promise.all([ + Notes.count({ userHost: null }), + Notes.count({ userHost: Not(null) }) + ]); + + return { + local: { + total: localCount, + }, + remote: { + total: remoteCount, + } + }; + } + + @autobind + public async update(note: Note, isAdditional: boolean) { + const update: Obj = { + diffs: {} + }; + + update.total = isAdditional ? 1 : -1; + + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + if (note.replyId != null) { + update.diffs.reply = isAdditional ? 1 : -1; + } else if (note.renoteId != null) { + update.diffs.renote = isAdditional ? 1 : -1; + } else { + update.diffs.normal = isAdditional ? 1 : -1; + } + + await this.inc({ + [note.userHost === null ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/classes/per-user-drive.ts b/src/services/chart/charts/classes/per-user-drive.ts new file mode 100644 index 0000000000..822f4eda0f --- /dev/null +++ b/src/services/chart/charts/classes/per-user-drive.ts @@ -0,0 +1,52 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { DriveFiles } from '../../../../models'; +import { DriveFile } from '../../../../models/entities/drive-file'; +import { name, schema } from '../schemas/per-user-drive'; + +type PerUserDriveLog = SchemaType<typeof schema>; + +export default class PerUserDriveChart extends Chart<PerUserDriveLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserDriveLog): DeepPartial<PerUserDriveLog> { + return { + totalCount: latest.totalCount, + totalSize: latest.totalSize, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserDriveLog>> { + const [count, size] = await Promise.all([ + DriveFiles.count({ userId: group }), + DriveFiles.clacDriveUsageOf(group) + ]); + + return { + totalCount: count, + totalSize: size, + }; + } + + @autobind + public async update(file: DriveFile, isAdditional: boolean) { + const update: Obj = {}; + + update.totalCount = isAdditional ? 1 : -1; + update.totalSize = isAdditional ? file.size : -file.size; + if (isAdditional) { + update.incCount = 1; + update.incSize = file.size; + } else { + update.decCount = 1; + update.decSize = file.size; + } + + await this.inc(update, file.userId); + } +} diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts new file mode 100644 index 0000000000..f3809a7c94 --- /dev/null +++ b/src/services/chart/charts/classes/per-user-following.ts @@ -0,0 +1,91 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Followings, Users } from '../../../../models'; +import { Not } from 'typeorm'; +import { User } from '../../../../models/entities/user'; +import { name, schema } from '../schemas/per-user-following'; + +type PerUserFollowingLog = SchemaType<typeof schema>; + +export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserFollowingLog): DeepPartial<PerUserFollowingLog> { + return { + local: { + followings: { + total: latest.local.followings.total, + }, + followers: { + total: latest.local.followers.total, + } + }, + remote: { + followings: { + total: latest.remote.followings.total, + }, + followers: { + total: latest.remote.followers.total, + } + } + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserFollowingLog>> { + const [ + localFollowingsCount, + localFollowersCount, + remoteFollowingsCount, + remoteFollowersCount + ] = await Promise.all([ + Followings.count({ followerId: group, followeeHost: null }), + Followings.count({ followeeId: group, followerHost: null }), + Followings.count({ followerId: group, followeeHost: Not(null) }), + Followings.count({ followeeId: group, followerHost: Not(null) }) + ]); + + return { + local: { + followings: { + total: localFollowingsCount, + }, + followers: { + total: localFollowersCount, + } + }, + remote: { + followings: { + total: remoteFollowingsCount, + }, + followers: { + total: remoteFollowersCount, + } + } + }; + } + + @autobind + public async update(follower: User, followee: User, isFollow: boolean) { + const update: Obj = {}; + + update.total = isFollow ? 1 : -1; + + if (isFollow) { + update.inc = 1; + } else { + update.dec = 1; + } + + this.inc({ + [Users.isLocalUser(follower) ? 'local' : 'remote']: { followings: update } + }, follower.id); + this.inc({ + [Users.isLocalUser(followee) ? 'local' : 'remote']: { followers: update } + }, followee.id); + } +} diff --git a/src/services/chart/charts/classes/per-user-notes.ts b/src/services/chart/charts/classes/per-user-notes.ts new file mode 100644 index 0000000000..cccd495604 --- /dev/null +++ b/src/services/chart/charts/classes/per-user-notes.ts @@ -0,0 +1,58 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { SchemaType } from '../../../../misc/schema'; +import { Notes } from '../../../../models'; +import { Note } from '../../../../models/entities/note'; +import { name, schema } from '../schemas/per-user-notes'; + +type PerUserNotesLog = SchemaType<typeof schema>; + +export default class PerUserNotesChart extends Chart<PerUserNotesLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserNotesLog): DeepPartial<PerUserNotesLog> { + return { + total: latest.total, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserNotesLog>> { + const [count] = await Promise.all([ + Notes.count({ userId: group }), + ]); + + return { + total: count, + }; + } + + @autobind + public async update(user: User, note: Note, isAdditional: boolean) { + const update: Obj = { + diffs: {} + }; + + update.total = isAdditional ? 1 : -1; + + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + if (note.replyId != null) { + update.diffs.reply = isAdditional ? 1 : -1; + } else if (note.renoteId != null) { + update.diffs.renote = isAdditional ? 1 : -1; + } else { + update.diffs.normal = isAdditional ? 1 : -1; + } + + await this.inc(update, user.id); + } +} diff --git a/src/services/chart/charts/classes/per-user-reactions.ts b/src/services/chart/charts/classes/per-user-reactions.ts new file mode 100644 index 0000000000..124fb4153c --- /dev/null +++ b/src/services/chart/charts/classes/per-user-reactions.ts @@ -0,0 +1,32 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { User } from '../../../../models/entities/user'; +import { Note } from '../../../../models/entities/note'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { name, schema } from '../schemas/per-user-reactions'; + +type PerUserReactionsLog = SchemaType<typeof schema>; + +export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> { + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: PerUserReactionsLog): DeepPartial<PerUserReactionsLog> { + return {}; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<PerUserReactionsLog>> { + return {}; + } + + @autobind + public async update(user: User, note: Note) { + this.inc({ + [Users.isLocalUser(user) ? 'local' : 'remote']: { count: 1 } + }, note.userId); + } +} diff --git a/src/services/chart/charts/classes/test-grouped.ts b/src/services/chart/charts/classes/test-grouped.ts new file mode 100644 index 0000000000..e32cbcf416 --- /dev/null +++ b/src/services/chart/charts/classes/test-grouped.ts @@ -0,0 +1,47 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test-grouped'; + +type TestGroupedLog = SchemaType<typeof schema>; + +export default class TestGroupedChart extends Chart<TestGroupedLog> { + private total = {} as Record<string, number>; + + constructor() { + super(name, schema, true); + } + + @autobind + protected genNewLog(latest: TestGroupedLog): DeepPartial<TestGroupedLog> { + return { + foo: { + total: latest.foo.total, + }, + }; + } + + @autobind + protected async fetchActual(group: string): Promise<DeepPartial<TestGroupedLog>> { + return { + foo: { + total: this.total[group], + }, + }; + } + + @autobind + public async increment(group: string) { + if (this.total[group] == null) this.total[group] = 0; + + const update: Obj = {}; + + update.total = 1; + update.inc = 1; + this.total[group]++; + + await this.inc({ + foo: update + }, group); + } +} diff --git a/src/services/chart/charts/classes/test-unique.ts b/src/services/chart/charts/classes/test-unique.ts new file mode 100644 index 0000000000..1eb396c293 --- /dev/null +++ b/src/services/chart/charts/classes/test-unique.ts @@ -0,0 +1,29 @@ +import autobind from 'autobind-decorator'; +import Chart, { DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test-unique'; + +type TestUniqueLog = SchemaType<typeof schema>; + +export default class TestUniqueChart extends Chart<TestUniqueLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: TestUniqueLog): DeepPartial<TestUniqueLog> { + return {}; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<TestUniqueLog>> { + return {}; + } + + @autobind + public async uniqueIncrement(key: string) { + await this.incIfUnique({ + foo: 1 + }, 'foos', key); + } +} diff --git a/src/services/chart/charts/classes/test.ts b/src/services/chart/charts/classes/test.ts new file mode 100644 index 0000000000..57c22822f2 --- /dev/null +++ b/src/services/chart/charts/classes/test.ts @@ -0,0 +1,45 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { name, schema } from '../schemas/test'; + +type TestLog = SchemaType<typeof schema>; + +export default class TestChart extends Chart<TestLog> { + private total = 0; + + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: TestLog): DeepPartial<TestLog> { + return { + foo: { + total: latest.foo.total, + }, + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<TestLog>> { + return { + foo: { + total: this.total, + }, + }; + } + + @autobind + public async increment() { + const update: Obj = {}; + + update.total = 1; + update.inc = 1; + this.total++; + + await this.inc({ + foo: update + }); + } +} diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts new file mode 100644 index 0000000000..eec30de8dc --- /dev/null +++ b/src/services/chart/charts/classes/users.ts @@ -0,0 +1,60 @@ +import autobind from 'autobind-decorator'; +import Chart, { Obj, DeepPartial } from '../../core'; +import { SchemaType } from '../../../../misc/schema'; +import { Users } from '../../../../models'; +import { Not } from 'typeorm'; +import { User } from '../../../../models/entities/user'; +import { name, schema } from '../schemas/users'; + +type UsersLog = SchemaType<typeof schema>; + +export default class UsersChart extends Chart<UsersLog> { + constructor() { + super(name, schema); + } + + @autobind + protected genNewLog(latest: UsersLog): DeepPartial<UsersLog> { + return { + local: { + total: latest.local.total, + }, + remote: { + total: latest.remote.total, + } + }; + } + + @autobind + protected async fetchActual(): Promise<DeepPartial<UsersLog>> { + const [localCount, remoteCount] = await Promise.all([ + Users.count({ host: null }), + Users.count({ host: Not(null) }) + ]); + + return { + local: { + total: localCount, + }, + remote: { + total: remoteCount, + } + }; + } + + @autobind + public async update(user: User, isAdditional: boolean) { + const update: Obj = {}; + + update.total = isAdditional ? 1 : -1; + if (isAdditional) { + update.inc = 1; + } else { + update.dec = 1; + } + + await this.inc({ + [Users.isLocalUser(user) ? 'local' : 'remote']: update + }); + } +} diff --git a/src/services/chart/charts/schemas/active-users.ts b/src/services/chart/charts/schemas/active-users.ts new file mode 100644 index 0000000000..da8c63389c --- /dev/null +++ b/src/services/chart/charts/schemas/active-users.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * アクティブユーザー数 + */ + count: { + type: 'number' as 'number', + description: 'アクティブユーザー数', + }, +}; + +/** + * アクティブユーザーに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'activeUsers'; diff --git a/src/services/chart/charts/schemas/drive.ts b/src/services/chart/charts/schemas/drive.ts new file mode 100644 index 0000000000..47530e8417 --- /dev/null +++ b/src/services/chart/charts/schemas/drive.ts @@ -0,0 +1,65 @@ +const logSchema = { + /** + * 集計期間時点での、全ドライブファイル数 + */ + totalCount: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイル数' + }, + + /** + * 集計期間時点での、全ドライブファイルの合計サイズ + */ + totalSize: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイルの合計サイズ' + }, + + /** + * 増加したドライブファイル数 + */ + incCount: { + type: 'number' as 'number', + description: '増加したドライブファイル数' + }, + + /** + * 増加したドライブ使用量 + */ + incSize: { + type: 'number' as 'number', + description: '増加したドライブ使用量' + }, + + /** + * 減少したドライブファイル数 + */ + decCount: { + type: 'number' as 'number', + description: '減少したドライブファイル数' + }, + + /** + * 減少したドライブ使用量 + */ + decSize: { + type: 'number' as 'number', + description: '減少したドライブ使用量' + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'drive'; diff --git a/src/services/chart/charts/schemas/federation.ts b/src/services/chart/charts/schemas/federation.ts new file mode 100644 index 0000000000..d1d275fc95 --- /dev/null +++ b/src/services/chart/charts/schemas/federation.ts @@ -0,0 +1,27 @@ +/** + * フェデレーションに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + instance: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: 'インスタンス数の合計' + }, + inc: { + type: 'number' as 'number', + description: '増加インスタンス数' + }, + dec: { + type: 'number' as 'number', + description: '減少インスタンス数' + }, + } + } + } +}; + +export const name = 'federation'; diff --git a/src/services/chart/charts/schemas/hashtag.ts b/src/services/chart/charts/schemas/hashtag.ts new file mode 100644 index 0000000000..c1904b6701 --- /dev/null +++ b/src/services/chart/charts/schemas/hashtag.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * 投稿された数 + */ + count: { + type: 'number' as 'number', + description: '投稿された数', + }, +}; + +/** + * ハッシュタグに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'hashtag'; diff --git a/src/services/chart/charts/schemas/instance.ts b/src/services/chart/charts/schemas/instance.ts new file mode 100644 index 0000000000..001f2428b5 --- /dev/null +++ b/src/services/chart/charts/schemas/instance.ts @@ -0,0 +1,149 @@ +/** + * インスタンスごとのチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + requests: { + type: 'object' as 'object', + properties: { + failed: { + type: 'number' as 'number', + description: '失敗したリクエスト数' + }, + succeeded: { + type: 'number' as 'number', + description: '成功したリクエスト数' + }, + received: { + type: 'number' as 'number', + description: '受信したリクエスト数' + }, + } + }, + + notes: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全投稿数' + }, + inc: { + type: 'number' as 'number', + description: '増加した投稿数' + }, + dec: { + type: 'number' as 'number', + description: '減少した投稿数' + }, + + diffs: { + type: 'object' as 'object', + properties: { + normal: { + type: 'number' as 'number', + description: '通常の投稿数の差分' + }, + + reply: { + type: 'number' as 'number', + description: 'リプライの投稿数の差分' + }, + + renote: { + type: 'number' as 'number', + description: 'Renoteの投稿数の差分' + }, + } + }, + } + }, + + users: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全ユーザー数' + }, + inc: { + type: 'number' as 'number', + description: '増加したユーザー数' + }, + dec: { + type: 'number' as 'number', + description: '減少したユーザー数' + }, + } + }, + + following: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全フォロー数' + }, + inc: { + type: 'number' as 'number', + description: '増加したフォロー数' + }, + dec: { + type: 'number' as 'number', + description: '減少したフォロー数' + }, + } + }, + + followers: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全フォロワー数' + }, + inc: { + type: 'number' as 'number', + description: '増加したフォロワー数' + }, + dec: { + type: 'number' as 'number', + description: '減少したフォロワー数' + }, + } + }, + + drive: { + type: 'object' as 'object', + properties: { + totalFiles: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイル数' + }, + totalUsage: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイルの合計サイズ' + }, + incFiles: { + type: 'number' as 'number', + description: '増加したドライブファイル数' + }, + incUsage: { + type: 'number' as 'number', + description: '増加したドライブ使用量' + }, + decFiles: { + type: 'number' as 'number', + description: '減少したドライブファイル数' + }, + decUsage: { + type: 'number' as 'number', + description: '減少したドライブ使用量' + }, + } + }, + } +}; + +export const name = 'instance'; diff --git a/src/services/chart/charts/schemas/network.ts b/src/services/chart/charts/schemas/network.ts new file mode 100644 index 0000000000..4ef530c07c --- /dev/null +++ b/src/services/chart/charts/schemas/network.ts @@ -0,0 +1,30 @@ +/** + * ネットワークに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + incomingRequests: { + type: 'number' as 'number', + description: '受信したリクエスト数' + }, + outgoingRequests: { + type: 'number' as 'number', + description: '送信したリクエスト数' + }, + totalTime: { + type: 'number' as 'number', + description: '応答時間の合計' // TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる + }, + incomingBytes: { + type: 'number' as 'number', + description: '合計受信データ量' + }, + outgoingBytes: { + type: 'number' as 'number', + description: '合計送信データ量' + }, + } +}; + +export const name = 'network'; diff --git a/src/services/chart/charts/schemas/notes.ts b/src/services/chart/charts/schemas/notes.ts new file mode 100644 index 0000000000..133d1e3730 --- /dev/null +++ b/src/services/chart/charts/schemas/notes.ts @@ -0,0 +1,52 @@ +const logSchema = { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全投稿数' + }, + + inc: { + type: 'number' as 'number', + description: '増加した投稿数' + }, + + dec: { + type: 'number' as 'number', + description: '減少した投稿数' + }, + + diffs: { + type: 'object' as 'object', + properties: { + normal: { + type: 'number' as 'number', + description: '通常の投稿数の差分' + }, + + reply: { + type: 'number' as 'number', + description: 'リプライの投稿数の差分' + }, + + renote: { + type: 'number' as 'number', + description: 'Renoteの投稿数の差分' + }, + } + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'notes'; diff --git a/src/services/chart/charts/schemas/per-user-drive.ts b/src/services/chart/charts/schemas/per-user-drive.ts new file mode 100644 index 0000000000..713bd7ed84 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-drive.ts @@ -0,0 +1,54 @@ +export const schema = { + type: 'object' as 'object', + properties: { + /** + * 集計期間時点での、全ドライブファイル数 + */ + totalCount: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイル数' + }, + + /** + * 集計期間時点での、全ドライブファイルの合計サイズ + */ + totalSize: { + type: 'number' as 'number', + description: '集計期間時点での、全ドライブファイルの合計サイズ' + }, + + /** + * 増加したドライブファイル数 + */ + incCount: { + type: 'number' as 'number', + description: '増加したドライブファイル数' + }, + + /** + * 増加したドライブ使用量 + */ + incSize: { + type: 'number' as 'number', + description: '増加したドライブ使用量' + }, + + /** + * 減少したドライブファイル数 + */ + decCount: { + type: 'number' as 'number', + description: '減少したドライブファイル数' + }, + + /** + * 減少したドライブ使用量 + */ + decSize: { + type: 'number' as 'number', + description: '減少したドライブ使用量' + }, + } +}; + +export const name = 'perUserDrive'; diff --git a/src/services/chart/charts/schemas/per-user-following.ts b/src/services/chart/charts/schemas/per-user-following.ts new file mode 100644 index 0000000000..d6ca1130e0 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-following.ts @@ -0,0 +1,81 @@ +export const logSchema = { + /** + * フォローしている + */ + followings: { + type: 'object' as 'object', + properties: { + /** + * フォローしている合計 + */ + total: { + type: 'number' as 'number', + description: 'フォローしている合計', + }, + + /** + * フォローした数 + */ + inc: { + type: 'number' as 'number', + description: 'フォローした数', + }, + + /** + * フォロー解除した数 + */ + dec: { + type: 'number' as 'number', + description: 'フォロー解除した数', + }, + } + }, + + /** + * フォローされている + */ + followers: { + type: 'object' as 'object', + properties: { + /** + * フォローされている合計 + */ + total: { + type: 'number' as 'number', + description: 'フォローされている合計', + }, + + /** + * フォローされた数 + */ + inc: { + type: 'number' as 'number', + description: 'フォローされた数', + }, + + /** + * フォロー解除された数 + */ + dec: { + type: 'number' as 'number', + description: 'フォロー解除された数', + }, + } + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'perUserFollowing'; diff --git a/src/services/chart/charts/schemas/per-user-notes.ts b/src/services/chart/charts/schemas/per-user-notes.ts new file mode 100644 index 0000000000..3c448c4cee --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-notes.ts @@ -0,0 +1,41 @@ +export const schema = { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '集計期間時点での、全投稿数' + }, + + inc: { + type: 'number' as 'number', + description: '増加した投稿数' + }, + + dec: { + type: 'number' as 'number', + description: '減少した投稿数' + }, + + diffs: { + type: 'object' as 'object', + properties: { + normal: { + type: 'number' as 'number', + description: '通常の投稿数の差分' + }, + + reply: { + type: 'number' as 'number', + description: 'リプライの投稿数の差分' + }, + + renote: { + type: 'number' as 'number', + description: 'Renoteの投稿数の差分' + }, + } + }, + } +}; + +export const name = 'perUserNotes'; diff --git a/src/services/chart/charts/schemas/per-user-reactions.ts b/src/services/chart/charts/schemas/per-user-reactions.ts new file mode 100644 index 0000000000..1278184da6 --- /dev/null +++ b/src/services/chart/charts/schemas/per-user-reactions.ts @@ -0,0 +1,28 @@ +export const logSchema = { + /** + * フォローしている合計 + */ + count: { + type: 'number' as 'number', + description: 'リアクションされた数', + }, +}; + +/** + * ユーザーごとのリアクションに関するチャート + */ +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'perUserReaction'; diff --git a/src/services/chart/charts/schemas/test-grouped.ts b/src/services/chart/charts/schemas/test-grouped.ts new file mode 100644 index 0000000000..acf3fddb31 --- /dev/null +++ b/src/services/chart/charts/schemas/test-grouped.ts @@ -0,0 +1,26 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '' + }, + + inc: { + type: 'number' as 'number', + description: '' + }, + + dec: { + type: 'number' as 'number', + description: '' + }, + } + } + } +}; + +export const name = 'testGrouped'; diff --git a/src/services/chart/charts/schemas/test-unique.ts b/src/services/chart/charts/schemas/test-unique.ts new file mode 100644 index 0000000000..8fcfbf3c72 --- /dev/null +++ b/src/services/chart/charts/schemas/test-unique.ts @@ -0,0 +1,11 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'number' as 'number', + description: '' + }, + } +}; + +export const name = 'testUnique'; diff --git a/src/services/chart/charts/schemas/test.ts b/src/services/chart/charts/schemas/test.ts new file mode 100644 index 0000000000..b1344500bf --- /dev/null +++ b/src/services/chart/charts/schemas/test.ts @@ -0,0 +1,26 @@ +export const schema = { + type: 'object' as 'object', + properties: { + foo: { + type: 'object' as 'object', + properties: { + total: { + type: 'number' as 'number', + description: '' + }, + + inc: { + type: 'number' as 'number', + description: '' + }, + + dec: { + type: 'number' as 'number', + description: '' + }, + } + } + } +}; + +export const name = 'test'; diff --git a/src/services/chart/charts/schemas/users.ts b/src/services/chart/charts/schemas/users.ts new file mode 100644 index 0000000000..db7e2dd057 --- /dev/null +++ b/src/services/chart/charts/schemas/users.ts @@ -0,0 +1,41 @@ +const logSchema = { + /** + * 集計期間時点での、全ユーザー数 + */ + total: { + type: 'number' as 'number', + description: '集計期間時点での、全ユーザー数' + }, + + /** + * 増加したユーザー数 + */ + inc: { + type: 'number' as 'number', + description: '増加したユーザー数' + }, + + /** + * 減少したユーザー数 + */ + dec: { + type: 'number' as 'number', + description: '減少したユーザー数' + }, +}; + +export const schema = { + type: 'object' as 'object', + properties: { + local: { + type: 'object' as 'object', + properties: logSchema + }, + remote: { + type: 'object' as 'object', + properties: logSchema + }, + } +}; + +export const name = 'users'; diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts new file mode 100644 index 0000000000..0a9ec8dae0 --- /dev/null +++ b/src/services/chart/core.ts @@ -0,0 +1,460 @@ +/** + * チャートエンジン + * + * Tests located in test/chart + */ + +import * as moment from 'moment'; +import * as nestedProperty from 'nested-property'; +import autobind from 'autobind-decorator'; +import Logger from '../logger'; +import { Schema } from '../../misc/schema'; +import { EntitySchema, getRepository, Repository, LessThan, MoreThanOrEqual } from 'typeorm'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; + +const logger = new Logger('chart', 'white', process.env.NODE_ENV !== 'test'); + +const utc = moment.utc; + +export type Obj = { [key: string]: any }; + +export type DeepPartial<T> = { + [P in keyof T]?: DeepPartial<T[P]>; +}; + +type ArrayValue<T> = { + [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>; +}; + +type Span = 'day' | 'hour'; + +type Log = { + id: number; + + /** + * 集計のグループ + */ + group: string | null; + + /** + * 集計日時のUnixタイムスタンプ(秒) + */ + date: number; + + /** + * 集計期間 + */ + span: Span; + + /** + * ユニークインクリメント用 + */ + unique?: Record<string, any>; +}; + +const camelToSnake = (str: string) => { + return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase()); +}; + +/** + * 様々なチャートの管理を司るクラス + */ +export default abstract class Chart<T extends Record<string, any>> { + private static readonly columnPrefix = '___'; + private static readonly columnDot = '_'; + + private name: string; + public schema: Schema; + protected repository: Repository<Log>; + protected abstract genNewLog(latest: T): DeepPartial<T>; + protected abstract async fetchActual(group?: string): Promise<DeepPartial<T>>; + + @autobind + private static convertSchemaToFlatColumnDefinitions(schema: Schema) { + const columns = {} as any; + const flatColumns = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}${this.columnDot}${k}` : k; + if (v.type === 'object') { + flatColumns(v.properties, p); + } else { + columns[this.columnPrefix + p] = { + type: 'integer', + }; + } + } + }; + flatColumns(schema.properties!); + return columns; + } + + @autobind + private static convertFlattenColumnsToObject(x: Record<string, number>) { + const obj = {} as any; + for (const k of Object.keys(x).filter(k => k.startsWith(Chart.columnPrefix))) { + // now k is ___x_y_z + const path = k.substr(Chart.columnPrefix.length).split(Chart.columnDot).join('.'); + nestedProperty.set(obj, path, x[k]); + } + return obj; + } + + @autobind + private static convertObjectToFlattenColumns(x: Record<string, any>) { + const columns = {} as Record<string, number>; + const flatten = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}${this.columnDot}${k}` : k; + if (typeof v === 'object') { + flatten(v, p); + } else { + columns[this.columnPrefix + p] = v; + } + } + }; + flatten(x); + return columns; + } + + @autobind + private static convertQuery(x: Record<string, any>) { + const query: Record<string, Function> = {}; + + const columns = Chart.convertObjectToFlattenColumns(x); + + for (const [k, v] of Object.entries(columns)) { + if (v > 0) query[k] = () => `"${k}" + ${v}`; + if (v < 0) query[k] = () => `"${k}" - ${v}`; + } + + return query; + } + + @autobind + private static momentToTimestamp(x: moment.Moment): Log['date'] { + return x.unix(); + } + + @autobind + public static schemaToEntity(name: string, schema: Schema): EntitySchema { + return new EntitySchema({ + name: `__chart__${camelToSnake(name)}`, + columns: { + id: { + type: 'integer', + primary: true, + generated: true + }, + date: { + type: 'integer', + }, + group: { + type: 'varchar', + length: 128, + nullable: true + }, + span: { + type: 'enum', + enum: ['hour', 'day'] + }, + unique: { + type: 'jsonb', + default: {} + }, + ...Chart.convertSchemaToFlatColumnDefinitions(schema) + }, + }); + } + + constructor(name: string, schema: Schema, grouped = false) { + this.name = name; + this.schema = schema; + const entity = Chart.schemaToEntity(name, schema); + + const keys = ['span', 'date']; + if (grouped) keys.push('group'); + + entity.options.uniques = [{ + columns: keys + }]; + + this.repository = getRepository<Log>(entity); + } + + @autobind + private getNewLog(latest: T | null): T { + const log = latest ? this.genNewLog(latest) : {}; + const flatColumns = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}.${k}` : k; + if (v.type === 'object') { + flatColumns(v.properties, p); + } else { + if (nestedProperty.get(log, p) == null) { + nestedProperty.set(log, p, 0); + } + } + } + }; + flatColumns(this.schema.properties!); + return log as T; + } + + @autobind + private getCurrentDate(): [number, number, number, number] { + const now = moment().utc(); + + const y = now.year(); + const m = now.month(); + const d = now.date(); + const h = now.hour(); + + return [y, m, d, h]; + } + + @autobind + private getLatestLog(span: Span, group: string | null = null): Promise<Log | null> { + return this.repository.findOne({ + group: group, + span: span + }, { + order: { + date: -1 + } + }).then(x => x || null); + } + + @autobind + private async getCurrentLog(span: Span, group: string | null = null): Promise<Log> { + const [y, m, d, h] = this.getCurrentDate(); + + const current = + span == 'day' ? utc([y, m, d]) : + span == 'hour' ? utc([y, m, d, h]) : + null as never; + + // 現在(今日または今のHour)のログ + const currentLog = await this.repository.findOne({ + span: span, + date: Chart.momentToTimestamp(current), + ...(group ? { group: group } : {}) + }); + + // ログがあればそれを返して終了 + if (currentLog != null) { + return currentLog; + } + + let log: Log; + let data: T; + + // 集計期間が変わってから、初めてのチャート更新なら + // 最も最近のログを持ってくる + // * 例えば集計期間が「日」である場合で考えると、 + // * 昨日何もチャートを更新するような出来事がなかった場合は、 + // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 + // * 「昨日の」と決め打ちせずに「もっとも最近の」とします + const latest = await this.getLatestLog(span, group); + + if (latest != null) { + const obj = Chart.convertFlattenColumnsToObject( + latest as Record<string, any>); + + // 空ログデータを作成 + data = await this.getNewLog(obj); + } else { + // ログが存在しなかったら + // (Misskeyインスタンスを建てて初めてのチャート更新時) + + // 初期ログデータを作成 + data = await this.getNewLog(null); + + logger.info(`${this.name}: Initial commit created`); + } + + try { + // 新規ログ挿入 + log = await this.repository.save({ + group: group, + span: span, + date: Chart.momentToTimestamp(current), + ...Chart.convertObjectToFlattenColumns(data) + }); + } catch (e) { + // duplicate key error + // 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある + // その場合は再度最も新しいログを持ってくる + if (isDuplicateKeyValueError(e)) { + log = await this.getLatestLog(span, group) as Log; + } else { + logger.error(e); + throw e; + } + } + + return log; + } + + @autobind + protected commit(query: Record<string, Function>, group: string | null = null, uniqueKey?: string, uniqueValue?: string): Promise<any> { + const update = async (log: Log) => { + // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く + if ( + uniqueKey && log.unique && + log.unique[uniqueKey] && + log.unique[uniqueKey].includes(uniqueValue) + ) return; + + // ユニークインクリメントの指定のキーに値を追加 + if (uniqueKey && log.unique) { + if (log.unique[uniqueKey]) { + const sql = `jsonb_set("unique", '{${uniqueKey}}', ("unique"->>'${uniqueKey}')::jsonb || '["${uniqueValue}"]'::jsonb)`; + query['unique'] = () => sql; + } else { + const sql = `jsonb_set("unique", '{${uniqueKey}}', '["${uniqueValue}"]')`; + query['unique'] = () => sql; + } + } + + // ログ更新 + await this.repository.createQueryBuilder() + .update() + .set(query) + .where('id = :id', { id: log.id }) + .execute(); + }; + + return Promise.all([ + this.getCurrentLog('day', group).then(log => update(log)), + this.getCurrentLog('hour', group).then(log => update(log)), + ]); + } + + @autobind + protected async inc(inc: DeepPartial<T>, group: string | null = null): Promise<void> { + await this.commit(Chart.convertQuery(inc as any), group); + } + + @autobind + protected async incIfUnique(inc: DeepPartial<T>, key: string, value: string, group: string | null = null): Promise<void> { + await this.commit(Chart.convertQuery(inc as any), group, key, value); + } + + @autobind + public async getChart(span: Span, range: number, group: string | null = null): Promise<ArrayValue<T>> { + const [y, m, d, h] = this.getCurrentDate(); + + const gt = + span == 'day' ? utc([y, m, d]).subtract(range, 'days') : + span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') : + null as never; + + // ログ取得 + let logs = await this.repository.find({ + where: { + group: group, + span: span, + date: MoreThanOrEqual(Chart.momentToTimestamp(gt)) + }, + order: { + date: -1 + }, + }); + + // 要求された範囲にログがひとつもなかったら + if (logs.length === 0) { + // もっとも新しいログを持ってくる + // (すくなくともひとつログが無いと隙間埋めできないため) + const recentLog = await this.repository.findOne({ + group: group, + span: span + }, { + order: { + date: -1 + }, + }); + + if (recentLog) { + logs = [recentLog]; + } + + // 要求された範囲の最も古い箇所に位置するログが存在しなかったら + } else if (!utc(logs[logs.length - 1].date * 1000).isSame(gt)) { + // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する + // (隙間埋めできないため) + const outdatedLog = await this.repository.findOne({ + group: group, + span: span, + date: LessThan(Chart.momentToTimestamp(gt)) + }, { + order: { + date: -1 + }, + }); + + if (outdatedLog) { + logs.push(outdatedLog); + } + } + + const chart: T[] = []; + + // 整形 + for (let i = (range - 1); i >= 0; i--) { + const current = + span == 'day' ? utc([y, m, d]).subtract(i, 'days') : + span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') : + null as never; + + const log = logs.find(l => utc(l.date * 1000).isSame(current)); + + if (log) { + const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>); + chart.unshift(data); + } else { + // 隙間埋め + const latest = logs.find(l => utc(l.date * 1000).isBefore(current)); + const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null; + chart.unshift(this.getNewLog(data)); + } + } + + const res: ArrayValue<T> = {} as any; + + /** + * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] + * を + * { foo: [1, 2, 3], bar: [5, 6, 7] } + * にする + */ + const dive = (x: Obj, path?: string) => { + for (const [k, v] of Object.entries(x)) { + const p = path ? `${path}.${k}` : k; + if (typeof v == 'object') { + dive(v, p); + } else { + nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p))); + } + } + }; + + dive(chart[0]); + + return res; + } +} + +export function convertLog(logSchema: Schema): Schema { + const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy + if (v.type === 'number') { + v.type = 'array'; + v.items = { + type: 'number' + }; + } else if (v.type === 'object') { + for (const k of Object.keys(v.properties!)) { + v.properties![k] = convertLog(v.properties![k]); + } + } + return v; +} diff --git a/src/services/chart/drive.ts b/src/services/chart/drive.ts deleted file mode 100644 index dd23412c7d..0000000000 --- a/src/services/chart/drive.ts +++ /dev/null @@ -1,150 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import { isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - /** - * 集計期間時点での、全ドライブファイル数 - */ - totalCount: { - type: 'number' as 'number', - description: '集計期間時点での、全ドライブファイル数' - }, - - /** - * 集計期間時点での、全ドライブファイルの合計サイズ - */ - totalSize: { - type: 'number' as 'number', - description: '集計期間時点での、全ドライブファイルの合計サイズ' - }, - - /** - * 増加したドライブファイル数 - */ - incCount: { - type: 'number' as 'number', - description: '増加したドライブファイル数' - }, - - /** - * 増加したドライブ使用量 - */ - incSize: { - type: 'number' as 'number', - description: '増加したドライブ使用量' - }, - - /** - * 減少したドライブファイル数 - */ - decCount: { - type: 'number' as 'number', - description: '減少したドライブファイル数' - }, - - /** - * 減少したドライブ使用量 - */ - decSize: { - type: 'number' as 'number', - description: '減少したドライブ使用量' - }, -}; - -export const driveLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type DriveLog = SchemaType<typeof driveLogSchema>; - -class DriveChart extends Chart<DriveLog> { - constructor() { - super('drive'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: DriveLog): Promise<DriveLog> { - const calcSize = (local: boolean) => DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': local ? null : { $ne: null }, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [localCount, remoteCount, localSize, remoteSize] = init ? await Promise.all([ - DriveFile.count({ 'metadata._user.host': null }), - DriveFile.count({ 'metadata._user.host': { $ne: null } }), - calcSize(true), - calcSize(false) - ]) : [ - latest ? latest.local.totalCount : 0, - latest ? latest.remote.totalCount : 0, - latest ? latest.local.totalSize : 0, - latest ? latest.remote.totalSize : 0 - ]; - - return { - local: { - totalCount: localCount, - totalSize: localSize, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - }, - remote: { - totalCount: remoteCount, - totalSize: remoteSize, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - } - }; - } - - @autobind - public async update(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalCount = isAdditional ? 1 : -1; - update.totalSize = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incCount = 1; - update.incSize = file.length; - } else { - update.decCount = 1; - update.decSize = file.length; - } - - await this.inc({ - [isLocalUser(file.metadata._user) ? 'local' : 'remote']: update - }); - } -} - -export default new DriveChart(); diff --git a/src/services/chart/entities.ts b/src/services/chart/entities.ts new file mode 100644 index 0000000000..14fd3adba0 --- /dev/null +++ b/src/services/chart/entities.ts @@ -0,0 +1,8 @@ +import Chart from './core'; + +export const entities = Object.values(require('require-all')({ + dirname: __dirname + '/charts/schemas', + resolve: (x: any) => { + return Chart.schemaToEntity(x.name, x.schema); + } +})); diff --git a/src/services/chart/federation.ts b/src/services/chart/federation.ts deleted file mode 100644 index 20da7a7421..0000000000 --- a/src/services/chart/federation.ts +++ /dev/null @@ -1,66 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import Instance from '../../models/instance'; - -/** - * フェデレーションに関するチャート - */ -type FederationLog = { - instance: { - /** - * インスタンス数の合計 - */ - total: number; - - /** - * 増加インスタンス数 - */ - inc: number; - - /** - * 減少インスタンス数 - */ - dec: number; - }; -}; - -class FederationChart extends Chart<FederationLog> { - constructor() { - super('federation'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: FederationLog): Promise<FederationLog> { - const [total] = init ? await Promise.all([ - Instance.count({}) - ]) : [ - latest ? latest.instance.total : 0 - ]; - - return { - instance: { - total: total, - inc: 0, - dec: 0 - } - }; - } - - @autobind - public async update(isAdditional: boolean) { - const update: Obj = {}; - - update.total = isAdditional ? 1 : -1; - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - await this.inc({ - instance: update - }); - } -} - -export default new FederationChart(); diff --git a/src/services/chart/hashtag.ts b/src/services/chart/hashtag.ts deleted file mode 100644 index 7a31e9cced..0000000000 --- a/src/services/chart/hashtag.ts +++ /dev/null @@ -1,56 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import { IUser, isLocalUser } from '../../models/user'; -import db from '../../db/mongodb'; - -/** - * ハッシュタグに関するチャート - */ -type HashtagLog = { - local: { - /** - * 投稿された数 - */ - count: number; - }; - - remote: HashtagLog['local']; -}; - -class HashtagChart extends Chart<HashtagLog> { - constructor() { - super('hashtag', true); - - // 後方互換性のため - db.get('chart.hashtag').findOne().then(doc => { - if (doc != null && doc.data.local == null) { - db.get('chart.hashtag').drop(); - } - }); - } - - @autobind - protected async getTemplate(init: boolean, latest?: HashtagLog): Promise<HashtagLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(hashtag: string, user: IUser) { - const update: Obj = { - count: 1 - }; - - await this.incIfUnique({ - [isLocalUser(user) ? 'local' : 'remote']: update - }, 'users', user._id.toHexString(), hashtag); - } -} - -export default new HashtagChart(); diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts index 7a6470f4d8..9626e3d6b3 100644 --- a/src/services/chart/index.ts +++ b/src/services/chart/index.ts @@ -1,364 +1,25 @@ -/** - * チャートエンジン - */ +import FederationChart from './charts/classes/federation'; +import NotesChart from './charts/classes/notes'; +import UsersChart from './charts/classes/users'; +import NetworkChart from './charts/classes/network'; +import ActiveUsersChart from './charts/classes/active-users'; +import InstanceChart from './charts/classes/instance'; +import PerUserNotesChart from './charts/classes/per-user-notes'; +import DriveChart from './charts/classes/drive'; +import PerUserReactionsChart from './charts/classes/per-user-reactions'; +import HashtagChart from './charts/classes/hashtag'; +import PerUserFollowingChart from './charts/classes/per-user-following'; +import PerUserDriveChart from './charts/classes/per-user-drive'; -import * as moment from 'moment'; -import * as nestedProperty from 'nested-property'; -import autobind from 'autobind-decorator'; -import * as mongo from 'mongodb'; -import db from '../../db/mongodb'; -import { ICollection } from 'monk'; -import Logger from '../logger'; -import { Schema } from '../../misc/schema'; - -const logger = new Logger('chart'); - -const utc = moment.utc; - -export type Obj = { [key: string]: any }; - -export type Partial<T> = { - [P in keyof T]?: Partial<T[P]>; -}; - -type ArrayValue<T> = { - [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>; -}; - -type Span = 'day' | 'hour'; - -type Log<T extends Obj> = { - _id: mongo.ObjectID; - - /** - * 集計のグループ - */ - group?: any; - - /** - * 集計日時 - */ - date: Date; - - /** - * 集計期間 - */ - span: Span; - - /** - * データ - */ - data: T; - - /** - * ユニークインクリメント用 - */ - unique?: Obj; -}; - -/** - * 様々なチャートの管理を司るクラス - */ -export default abstract class Chart<T extends Obj> { - protected collection: ICollection<Log<T>>; - protected abstract async getTemplate(init: boolean, latest?: T, group?: any): Promise<T>; - private name: string; - - constructor(name: string, grouped = false) { - this.name = name; - this.collection = db.get<Log<T>>(`chart.${name}`); - - const keys = { - span: -1, - date: -1 - } as { [key: string]: 1 | -1; }; - if (grouped) keys.group = -1; - - this.collection.createIndex(keys, { unique: true }); - } - - @autobind - private convertQuery(x: Obj, path: string): Obj { - const query: Obj = {}; - - const dive = (x: Obj, path: string) => { - for (const [k, v] of Object.entries(x)) { - const p = path ? `${path}.${k}` : k; - if (typeof v === 'number') { - query[p] = v; - } else { - dive(v, p); - } - } - }; - - dive(x, path); - - return query; - } - - @autobind - private getCurrentDate(): [number, number, number, number] { - const now = moment().utc(); - - const y = now.year(); - const m = now.month(); - const d = now.date(); - const h = now.hour(); - - return [y, m, d, h]; - } - - @autobind - private getLatestLog(span: Span, group?: any): Promise<Log<T>> { - return this.collection.findOne({ - group: group, - span: span - }, { - sort: { - date: -1 - } - }); - } - - @autobind - private async getCurrentLog(span: Span, group?: any): Promise<Log<T>> { - const [y, m, d, h] = this.getCurrentDate(); - - const current = - span == 'day' ? utc([y, m, d]) : - span == 'hour' ? utc([y, m, d, h]) : - null; - - // 現在(今日または今のHour)のログ - const currentLog = await this.collection.findOne({ - group: group, - span: span, - date: current.toDate() - }); - - // ログがあればそれを返して終了 - if (currentLog != null) { - return currentLog; - } - - let log: Log<T>; - let data: T; - - // 集計期間が変わってから、初めてのチャート更新なら - // 最も最近のログを持ってくる - // * 例えば集計期間が「日」である場合で考えると、 - // * 昨日何もチャートを更新するような出来事がなかった場合は、 - // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、 - // * 「昨日の」と決め打ちせずに「もっとも最近の」とします - const latest = await this.getLatestLog(span, group); - - if (latest != null) { - // 空ログデータを作成 - data = await this.getTemplate(false, latest.data); - } else { - // ログが存在しなかったら - // (Misskeyインスタンスを建てて初めてのチャート更新時など - // または何らかの理由でチャートコレクションを抹消した場合) - - // 初期ログデータを作成 - data = await this.getTemplate(true, null, group); - - logger.info(`${this.name}: Initial commit created`); - } - - try { - // 新規ログ挿入 - log = await this.collection.insert({ - group: group, - span: span, - date: current.toDate(), - data: data - }); - } catch (e) { - // 11000 is duplicate key error - // 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある - // その場合は再度最も新しいログを持ってくる - if (e.code === 11000) { - log = await this.getLatestLog(span, group); - } else { - logger.error(e); - throw e; - } - } - - return log; - } - - @autobind - protected commit(query: Obj, group?: any, uniqueKey?: string, uniqueValue?: string): void { - const update = (log: Log<T>) => { - // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く - if ( - uniqueKey && - log.unique && - log.unique[uniqueKey] && - log.unique[uniqueKey].includes(uniqueValue) - ) return; - - // ユニークインクリメントの指定のキーに値を追加 - if (uniqueKey) { - query['$push'] = { - [`unique.${uniqueKey}`]: uniqueValue - }; - } - - // ログ更新 - this.collection.update({ - _id: log._id - }, query); - }; - - this.getCurrentLog('day', group).then(log => update(log)); - this.getCurrentLog('hour', group).then(log => update(log)); - } - - @autobind - protected inc(inc: Partial<T>, group?: any): void { - this.commit({ - $inc: this.convertQuery(inc, 'data') - }, group); - } - - @autobind - protected incIfUnique(inc: Partial<T>, key: string, value: string, group?: any): void { - this.commit({ - $inc: this.convertQuery(inc, 'data') - }, group, key, value); - } - - @autobind - public async getChart(span: Span, range: number, group?: any): Promise<ArrayValue<T>> { - const promisedChart: Promise<T>[] = []; - - const [y, m, d, h] = this.getCurrentDate(); - - const gt = - span == 'day' ? utc([y, m, d]).subtract(range, 'days') : - span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') : - null; - - // ログ取得 - let logs = await this.collection.find({ - group: group, - span: span, - date: { - $gte: gt.toDate() - } - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - // 要求された範囲にログがひとつもなかったら - if (logs.length == 0) { - // もっとも新しいログを持ってくる - // (すくなくともひとつログが無いと隙間埋めできないため) - const recentLog = await this.collection.findOne({ - group: group, - span: span - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - if (recentLog) { - logs = [recentLog]; - } - - // 要求された範囲の最も古い箇所に位置するログが存在しなかったら - } else if (!utc(logs[logs.length - 1].date).isSame(gt)) { - // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する - // (隙間埋めできないため) - const outdatedLog = await this.collection.findOne({ - group: group, - span: span, - date: { - $lt: gt.toDate() - } - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - if (outdatedLog) { - logs.push(outdatedLog); - } - } - - // 整形 - for (let i = (range - 1); i >= 0; i--) { - const current = - span == 'day' ? utc([y, m, d]).subtract(i, 'days') : - span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') : - null; - - const log = logs.find(l => utc(l.date).isSame(current)); - - if (log) { - promisedChart.unshift(Promise.resolve(log.data)); - } else { - // 隙間埋め - const latest = logs.find(l => utc(l.date).isBefore(current)); - promisedChart.unshift(this.getTemplate(false, latest ? latest.data : null)); - } - } - - const chart = await Promise.all(promisedChart); - - const res: ArrayValue<T> = {} as any; - - /** - * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }] - * を - * { foo: [1, 2, 3], bar: [5, 6, 7] } - * にする - */ - const dive = (x: Obj, path?: string) => { - for (const [k, v] of Object.entries(x)) { - const p = path ? `${path}.${k}` : k; - if (typeof v == 'object') { - dive(v, p); - } else { - nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p))); - } - } - }; - - dive(chart[0]); - - return res; - } -} - -export function convertLog(logSchema: Schema): Schema { - const v: Schema = JSON.parse(JSON.stringify(logSchema)); // copy - if (v.type === 'number') { - v.type = 'array'; - v.items = { - type: 'number' - }; - } else if (v.type === 'object') { - for (const k of Object.keys(v.properties)) { - v.properties[k] = convertLog(v.properties[k]); - } - } - return v; -} +export const federationChart = new FederationChart(); +export const notesChart = new NotesChart(); +export const usersChart = new UsersChart(); +export const networkChart = new NetworkChart(); +export const activeUsersChart = new ActiveUsersChart(); +export const instanceChart = new InstanceChart(); +export const perUserNotesChart = new PerUserNotesChart(); +export const driveChart = new DriveChart(); +export const perUserReactionsChart = new PerUserReactionsChart(); +export const hashtagChart = new HashtagChart(); +export const perUserFollowingChart = new PerUserFollowingChart(); +export const perUserDriveChart = new PerUserDriveChart(); diff --git a/src/services/chart/instance.ts b/src/services/chart/instance.ts deleted file mode 100644 index 5af398b902..0000000000 --- a/src/services/chart/instance.ts +++ /dev/null @@ -1,302 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import User from '../../models/user'; -import Note from '../../models/note'; -import Following from '../../models/following'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; - -/** - * インスタンスごとのチャート - */ -type InstanceLog = { - requests: { - /** - * 失敗したリクエスト数 - */ - failed: number; - - /** - * 成功したリクエスト数 - */ - succeeded: number; - - /** - * 受信したリクエスト数 - */ - received: number; - }; - - notes: { - /** - * 集計期間時点での、全投稿数 - */ - total: number; - - /** - * 増加した投稿数 - */ - inc: number; - - /** - * 減少した投稿数 - */ - dec: number; - }; - - users: { - /** - * 集計期間時点での、全ユーザー数 - */ - total: number; - - /** - * 増加したユーザー数 - */ - inc: number; - - /** - * 減少したユーザー数 - */ - dec: number; - }; - - following: { - /** - * 集計期間時点での、全フォロー数 - */ - total: number; - - /** - * 増加したフォロー数 - */ - inc: number; - - /** - * 減少したフォロー数 - */ - dec: number; - }; - - followers: { - /** - * 集計期間時点での、全フォロワー数 - */ - total: number; - - /** - * 増加したフォロワー数 - */ - inc: number; - - /** - * 減少したフォロワー数 - */ - dec: number; - }; - - drive: { - /** - * 集計期間時点での、全ドライブファイル数 - */ - totalFiles: number; - - /** - * 集計期間時点での、全ドライブファイルの合計サイズ - */ - totalUsage: number; - - /** - * 増加したドライブファイル数 - */ - incFiles: number; - - /** - * 増加したドライブ使用量 - */ - incUsage: number; - - /** - * 減少したドライブファイル数 - */ - decFiles: number; - - /** - * 減少したドライブ使用量 - */ - decUsage: number; - }; -}; - -class InstanceChart extends Chart<InstanceLog> { - constructor() { - super('instance', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: InstanceLog, group?: any): Promise<InstanceLog> { - const calcUsage = () => DriveFile - .aggregate([{ - $match: { - 'metadata._user.host': group, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [ - notesCount, - usersCount, - followingCount, - followersCount, - driveFiles, - driveUsage, - ] = init ? await Promise.all([ - Note.count({ '_user.host': group }), - User.count({ host: group }), - Following.count({ '_follower.host': group }), - Following.count({ '_followee.host': group }), - DriveFile.count({ 'metadata._user.host': group }), - calcUsage(), - ]) : [ - latest ? latest.notes.total : 0, - latest ? latest.users.total : 0, - latest ? latest.following.total : 0, - latest ? latest.followers.total : 0, - latest ? latest.drive.totalFiles : 0, - latest ? latest.drive.totalUsage : 0, - ]; - - return { - requests: { - failed: 0, - succeeded: 0, - received: 0 - }, - notes: { - total: notesCount, - inc: 0, - dec: 0 - }, - users: { - total: usersCount, - inc: 0, - dec: 0 - }, - following: { - total: followingCount, - inc: 0, - dec: 0 - }, - followers: { - total: followersCount, - inc: 0, - dec: 0 - }, - drive: { - totalFiles: driveFiles, - totalUsage: driveUsage, - incFiles: 0, - incUsage: 0, - decFiles: 0, - decUsage: 0 - } - }; - } - - @autobind - public async requestReceived(host: string) { - await this.inc({ - requests: { - received: 1 - } - }, host); - } - - @autobind - public async requestSent(host: string, isSucceeded: boolean) { - const update: Obj = {}; - - if (isSucceeded) { - update.succeeded = 1; - } else { - update.failed = 1; - } - - await this.inc({ - requests: update - }, host); - } - - @autobind - public async newUser(host: string) { - await this.inc({ - users: { - total: 1, - inc: 1 - } - }, host); - } - - @autobind - public async updateNote(host: string, isAdditional: boolean) { - await this.inc({ - notes: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateFollowing(host: string, isAdditional: boolean) { - await this.inc({ - following: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateFollowers(host: string, isAdditional: boolean) { - await this.inc({ - followers: { - total: isAdditional ? 1 : -1, - inc: isAdditional ? 1 : 0, - dec: isAdditional ? 0 : 1, - } - }, host); - } - - @autobind - public async updateDrive(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalFiles = isAdditional ? 1 : -1; - update.totalUsage = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incFiles = 1; - update.incUsage = file.length; - } else { - update.decFiles = 1; - update.decUsage = file.length; - } - - await this.inc({ - drive: update - }, file.metadata._user.host); - } -} - -export default new InstanceChart(); diff --git a/src/services/chart/network.ts b/src/services/chart/network.ts deleted file mode 100644 index fce47099d1..0000000000 --- a/src/services/chart/network.ts +++ /dev/null @@ -1,64 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Partial } from './'; - -/** - * ネットワークに関するチャート - */ -type NetworkLog = { - /** - * 受信したリクエスト数 - */ - incomingRequests: number; - - /** - * 送信したリクエスト数 - */ - outgoingRequests: number; - - /** - * 応答時間の合計 - * TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる - */ - totalTime: number; - - /** - * 合計受信データ量 - */ - incomingBytes: number; - - /** - * 合計送信データ量 - */ - outgoingBytes: number; -}; - -class NetworkChart extends Chart<NetworkLog> { - constructor() { - super('network'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: NetworkLog): Promise<NetworkLog> { - return { - incomingRequests: 0, - outgoingRequests: 0, - totalTime: 0, - incomingBytes: 0, - outgoingBytes: 0 - }; - } - - @autobind - public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) { - const inc: Partial<NetworkLog> = { - incomingRequests: incomingRequests, - totalTime: time, - incomingBytes: incomingBytes, - outgoingBytes: outgoingBytes - }; - - await this.inc(inc); - } -} - -export default new NetworkChart(); diff --git a/src/services/chart/notes.ts b/src/services/chart/notes.ts deleted file mode 100644 index b047ec273f..0000000000 --- a/src/services/chart/notes.ts +++ /dev/null @@ -1,127 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from '.'; -import Note, { INote } from '../../models/note'; -import { isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - total: { - type: 'number' as 'number', - description: '集計期間時点での、全投稿数' - }, - - inc: { - type: 'number' as 'number', - description: '増加した投稿数' - }, - - dec: { - type: 'number' as 'number', - description: '減少した投稿数' - }, - - diffs: { - type: 'object' as 'object', - properties: { - normal: { - type: 'number' as 'number', - description: '通常の投稿数の差分' - }, - - reply: { - type: 'number' as 'number', - description: 'リプライの投稿数の差分' - }, - - renote: { - type: 'number' as 'number', - description: 'Renoteの投稿数の差分' - }, - } - }, -}; - -export const notesLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type NotesLog = SchemaType<typeof notesLogSchema>; - -class NotesChart extends Chart<NotesLog> { - constructor() { - super('notes'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: NotesLog): Promise<NotesLog> { - const [localCount, remoteCount] = init ? await Promise.all([ - Note.count({ '_user.host': null }), - Note.count({ '_user.host': { $ne: null } }) - ]) : [ - latest ? latest.local.total : 0, - latest ? latest.remote.total : 0 - ]; - - return { - local: { - total: localCount, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }, - remote: { - total: remoteCount, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - } - }; - } - - @autobind - public async update(note: INote, isAdditional: boolean) { - const update: Obj = { - diffs: {} - }; - - update.total = isAdditional ? 1 : -1; - - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - if (note.replyId != null) { - update.diffs.reply = isAdditional ? 1 : -1; - } else if (note.renoteId != null) { - update.diffs.renote = isAdditional ? 1 : -1; - } else { - update.diffs.normal = isAdditional ? 1 : -1; - } - - await this.inc({ - [isLocalUser(note._user) ? 'local' : 'remote']: update - }); - } -} - -export default new NotesChart(); diff --git a/src/services/chart/per-user-drive.ts b/src/services/chart/per-user-drive.ts deleted file mode 100644 index 4f335f1688..0000000000 --- a/src/services/chart/per-user-drive.ts +++ /dev/null @@ -1,122 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import { SchemaType } from '../../misc/schema'; - -export const perUserDriveLogSchema = { - type: 'object' as 'object', - properties: { - /** - * 集計期間時点での、全ドライブファイル数 - */ - totalCount: { - type: 'number' as 'number', - description: '集計期間時点での、全ドライブファイル数' - }, - - /** - * 集計期間時点での、全ドライブファイルの合計サイズ - */ - totalSize: { - type: 'number' as 'number', - description: '集計期間時点での、全ドライブファイルの合計サイズ' - }, - - /** - * 増加したドライブファイル数 - */ - incCount: { - type: 'number' as 'number', - description: '増加したドライブファイル数' - }, - - /** - * 増加したドライブ使用量 - */ - incSize: { - type: 'number' as 'number', - description: '増加したドライブ使用量' - }, - - /** - * 減少したドライブファイル数 - */ - decCount: { - type: 'number' as 'number', - description: '減少したドライブファイル数' - }, - - /** - * 減少したドライブ使用量 - */ - decSize: { - type: 'number' as 'number', - description: '減少したドライブ使用量' - }, - } -}; - -type PerUserDriveLog = SchemaType<typeof perUserDriveLogSchema>; - -class PerUserDriveChart extends Chart<PerUserDriveLog> { - constructor() { - super('perUserDrive', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserDriveLog, group?: any): Promise<PerUserDriveLog> { - const calcSize = () => DriveFile - .aggregate([{ - $match: { - 'metadata.userId': group, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then(res => res.length > 0 ? res[0].usage : 0); - - const [count, size] = init ? await Promise.all([ - DriveFile.count({ 'metadata.userId': group }), - calcSize() - ]) : [ - latest ? latest.totalCount : 0, - latest ? latest.totalSize : 0 - ]; - - return { - totalCount: count, - totalSize: size, - incCount: 0, - incSize: 0, - decCount: 0, - decSize: 0 - }; - } - - @autobind - public async update(file: IDriveFile, isAdditional: boolean) { - const update: Obj = {}; - - update.totalCount = isAdditional ? 1 : -1; - update.totalSize = isAdditional ? file.length : -file.length; - if (isAdditional) { - update.incCount = 1; - update.incSize = file.length; - } else { - update.decCount = 1; - update.decSize = file.length; - } - - await this.inc(update, file.metadata.userId); - } -} - -export default new PerUserDriveChart(); diff --git a/src/services/chart/per-user-following.ts b/src/services/chart/per-user-following.ts deleted file mode 100644 index 8a94a4f155..0000000000 --- a/src/services/chart/per-user-following.ts +++ /dev/null @@ -1,162 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import Following from '../../models/following'; -import { IUser, isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -export const logSchema = { - /** - * フォローしている - */ - followings: { - type: 'object' as 'object', - properties: { - /** - * フォローしている合計 - */ - total: { - type: 'number', - description: 'フォローしている合計', - }, - - /** - * フォローした数 - */ - inc: { - type: 'number', - description: 'フォローした数', - }, - - /** - * フォロー解除した数 - */ - dec: { - type: 'number', - description: 'フォロー解除した数', - }, - } - }, - - /** - * フォローされている - */ - followers: { - type: 'object' as 'object', - properties: { - /** - * フォローされている合計 - */ - total: { - type: 'number', - description: 'フォローされている合計', - }, - - /** - * フォローされた数 - */ - inc: { - type: 'number', - description: 'フォローされた数', - }, - - /** - * フォロー解除された数 - */ - dec: { - type: 'number', - description: 'フォロー解除された数', - }, - } - }, -}; - -export const perUserFollowingLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type PerUserFollowingLog = SchemaType<typeof perUserFollowingLogSchema>; - -class PerUserFollowingChart extends Chart<PerUserFollowingLog> { - constructor() { - super('perUserFollowing', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserFollowingLog, group?: any): Promise<PerUserFollowingLog> { - const [ - localFollowingsCount, - localFollowersCount, - remoteFollowingsCount, - remoteFollowersCount - ] = init ? await Promise.all([ - Following.count({ followerId: group, '_followee.host': null }), - Following.count({ followeeId: group, '_follower.host': null }), - Following.count({ followerId: group, '_followee.host': { $ne: null } }), - Following.count({ followeeId: group, '_follower.host': { $ne: null } }) - ]) : [ - latest ? latest.local.followings.total : 0, - latest ? latest.local.followers.total : 0, - latest ? latest.remote.followings.total : 0, - latest ? latest.remote.followers.total : 0 - ]; - - return { - local: { - followings: { - total: localFollowingsCount, - inc: 0, - dec: 0 - }, - followers: { - total: localFollowersCount, - inc: 0, - dec: 0 - } - }, - remote: { - followings: { - total: remoteFollowingsCount, - inc: 0, - dec: 0 - }, - followers: { - total: remoteFollowersCount, - inc: 0, - dec: 0 - } - } - }; - } - - @autobind - public async update(follower: IUser, followee: IUser, isFollow: boolean) { - const update: Obj = {}; - - update.total = isFollow ? 1 : -1; - - if (isFollow) { - update.inc = 1; - } else { - update.dec = 1; - } - - this.inc({ - [isLocalUser(follower) ? 'local' : 'remote']: { followings: update } - }, follower._id); - this.inc({ - [isLocalUser(followee) ? 'local' : 'remote']: { followers: update } - }, followee._id); - } -} - -export default new PerUserFollowingChart(); diff --git a/src/services/chart/per-user-notes.ts b/src/services/chart/per-user-notes.ts deleted file mode 100644 index 2f4f882091..0000000000 --- a/src/services/chart/per-user-notes.ts +++ /dev/null @@ -1,100 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import Note, { INote } from '../../models/note'; -import { IUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -export const perUserNotesLogSchema = { - type: 'object' as 'object', - properties: { - total: { - type: 'number' as 'number', - description: '集計期間時点での、全投稿数' - }, - - inc: { - type: 'number' as 'number', - description: '増加した投稿数' - }, - - dec: { - type: 'number' as 'number', - description: '減少した投稿数' - }, - - diffs: { - type: 'object' as 'object', - properties: { - normal: { - type: 'number' as 'number', - description: '通常の投稿数の差分' - }, - - reply: { - type: 'number' as 'number', - description: 'リプライの投稿数の差分' - }, - - renote: { - type: 'number' as 'number', - description: 'Renoteの投稿数の差分' - }, - } - }, - } -}; - -type PerUserNotesLog = SchemaType<typeof perUserNotesLogSchema>; - -class PerUserNotesChart extends Chart<PerUserNotesLog> { - constructor() { - super('perUserNotes', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserNotesLog, group?: any): Promise<PerUserNotesLog> { - const [count] = init ? await Promise.all([ - Note.count({ userId: group, deletedAt: null }), - ]) : [ - latest ? latest.total : 0 - ]; - - return { - total: count, - inc: 0, - dec: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }; - } - - @autobind - public async update(user: IUser, note: INote, isAdditional: boolean) { - const update: Obj = { - diffs: {} - }; - - update.total = isAdditional ? 1 : -1; - - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - if (note.replyId != null) { - update.diffs.reply = isAdditional ? 1 : -1; - } else if (note.renoteId != null) { - update.diffs.renote = isAdditional ? 1 : -1; - } else { - update.diffs.normal = isAdditional ? 1 : -1; - } - - await this.inc(update, user._id); - } -} - -export default new PerUserNotesChart(); diff --git a/src/services/chart/per-user-reactions.ts b/src/services/chart/per-user-reactions.ts deleted file mode 100644 index 60495aeb02..0000000000 --- a/src/services/chart/per-user-reactions.ts +++ /dev/null @@ -1,45 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart from './'; -import { IUser, isLocalUser } from '../../models/user'; -import { INote } from '../../models/note'; - -/** - * ユーザーごとのリアクションに関するチャート - */ -type PerUserReactionsLog = { - local: { - /** - * リアクションされた数 - */ - count: number; - }; - - remote: PerUserReactionsLog['local']; -}; - -class PerUserReactionsChart extends Chart<PerUserReactionsLog> { - constructor() { - super('perUserReaction', true); - } - - @autobind - protected async getTemplate(init: boolean, latest?: PerUserReactionsLog, group?: any): Promise<PerUserReactionsLog> { - return { - local: { - count: 0 - }, - remote: { - count: 0 - } - }; - } - - @autobind - public async update(user: IUser, note: INote) { - this.inc({ - [isLocalUser(user) ? 'local' : 'remote']: { count: 1 } - }, note.userId); - } -} - -export default new PerUserReactionsChart(); diff --git a/src/services/chart/users.ts b/src/services/chart/users.ts deleted file mode 100644 index cca9590842..0000000000 --- a/src/services/chart/users.ts +++ /dev/null @@ -1,94 +0,0 @@ -import autobind from 'autobind-decorator'; -import Chart, { Obj } from './'; -import User, { IUser, isLocalUser } from '../../models/user'; -import { SchemaType } from '../../misc/schema'; - -const logSchema = { - /** - * 集計期間時点での、全ユーザー数 - */ - total: { - type: 'number' as 'number', - description: '集計期間時点での、全ユーザー数' - }, - - /** - * 増加したユーザー数 - */ - inc: { - type: 'number' as 'number', - description: '増加したユーザー数' - }, - - /** - * 減少したユーザー数 - */ - dec: { - type: 'number' as 'number', - description: '減少したユーザー数' - }, -}; - -export const usersLogSchema = { - type: 'object' as 'object', - properties: { - local: { - type: 'object' as 'object', - properties: logSchema - }, - remote: { - type: 'object' as 'object', - properties: logSchema - }, - } -}; - -type UsersLog = SchemaType<typeof usersLogSchema>; - -class UsersChart extends Chart<UsersLog> { - constructor() { - super('users'); - } - - @autobind - protected async getTemplate(init: boolean, latest?: UsersLog): Promise<UsersLog> { - const [localCount, remoteCount] = init ? await Promise.all([ - User.count({ host: null }), - User.count({ host: { $ne: null } }) - ]) : [ - latest ? latest.local.total : 0, - latest ? latest.remote.total : 0 - ]; - - return { - local: { - total: localCount, - inc: 0, - dec: 0 - }, - remote: { - total: remoteCount, - inc: 0, - dec: 0 - } - }; - } - - @autobind - public async update(user: IUser, isAdditional: boolean) { - const update: Obj = {}; - - update.total = isAdditional ? 1 : -1; - if (isAdditional) { - update.inc = 1; - } else { - update.dec = 1; - } - - await this.inc({ - [isLocalUser(user) ? 'local' : 'remote']: update - }); - } -} - -export default new UsersChart(); diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts index 3e000ef2ed..5bff8adfd4 100644 --- a/src/services/create-notification.ts +++ b/src/services/create-notification.ts @@ -1,62 +1,67 @@ -import * as mongo from 'mongodb'; -import Notification from '../models/notification'; -import Mute from '../models/mute'; -import { pack } from '../models/notification'; import { publishMainStream } from './stream'; -import User from '../models/user'; import pushSw from './push-notification'; +import { Notifications, Mutings } from '../models'; +import { genId } from '../misc/gen-id'; +import { User } from '../models/entities/user'; +import { Note } from '../models/entities/note'; +import { Notification } from '../models/entities/notification'; -export default ( - notifiee: mongo.ObjectID, - notifier: mongo.ObjectID, +export async function createNotification( + notifieeId: User['id'], + notifierId: User['id'], type: string, - content?: any -) => new Promise<any>(async (resolve, reject) => { - if (notifiee.equals(notifier)) { - return resolve(); + content?: { + noteId?: Note['id']; + reaction?: string; + choice?: number; + } +) { + if (notifieeId === notifierId) { + return null; } - // Create notification - const notification = await Notification.insert(Object.assign({ + const data = { + id: genId(), createdAt: new Date(), - notifieeId: notifiee, - notifierId: notifier, + notifieeId: notifieeId, + notifierId: notifierId, type: type, - isRead: false - }, content)); + isRead: false, + } as Partial<Notification>; + + if (content) { + if (content.noteId) data.noteId = content.noteId; + if (content.reaction) data.reaction = content.reaction; + if (content.choice) data.choice = content.choice; + } - resolve(notification); + // Create notification + const notification = await Notifications.save(data); - const packed = await pack(notification); + const packed = await Notifications.pack(notification); // Publish notification event - publishMainStream(notifiee, 'notification', packed); - - // Update flag - User.update({ _id: notifiee }, { - $set: { - hasUnreadNotification: true - } - }); + publishMainStream(notifieeId, 'notification', packed); // 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する setTimeout(async () => { - const fresh = await Notification.findOne({ _id: notification._id }, { isRead: true }); + const fresh = await Notifications.findOne(notification.id); + if (fresh == null) return; // 既に削除されているかもしれない if (!fresh.isRead) { //#region ただしミュートしているユーザーからの通知なら無視 - const mute = await Mute.find({ - muterId: notifiee, - deletedAt: { $exists: false } + const mutings = await Mutings.find({ + muterId: notifieeId }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - if (mutedUserIds.indexOf(notifier.toString()) != -1) { + if (mutings.map(m => m.muteeId).includes(notifierId)) { return; } //#endregion - publishMainStream(notifiee, 'unreadNotification', packed); + publishMainStream(notifieeId, 'unreadNotification', packed); - pushSw(notifiee, 'notification', packed); + pushSw(notifieeId, 'notification', packed); } }, 2000); -}); + + return notification; +} diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index cdbcb34de4..54d7f2d13a 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -1,31 +1,27 @@ import { Buffer } from 'buffer'; import * as fs from 'fs'; -import * as mongodb from 'mongodb'; import * as crypto from 'crypto'; import * as Minio from 'minio'; import * as uuid from 'uuid'; import * as sharp from 'sharp'; -import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../models/drive-file'; -import DriveFolder from '../../models/drive-folder'; -import { pack } from '../../models/drive-file'; import { publishMainStream, publishDriveStream } from '../stream'; -import { isLocalUser, IUser, IRemoteUser, isRemoteUser } from '../../models/user'; import delFile from './delete-file'; import config from '../../config'; -import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic'; -import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail'; -import driveChart from '../../services/chart/drive'; -import perUserDriveChart from '../../services/chart/per-user-drive'; -import instanceChart from '../../services/chart/instance'; import fetchMeta from '../../misc/fetch-meta'; import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor'; -import Instance from '../../models/instance'; import { contentDisposition } from '../../misc/content-disposition'; import { detectMine } from '../../misc/detect-mine'; +import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; +import { InternalStorage } from './internal-storage'; +import { DriveFile } from '../../models/entities/drive-file'; +import { IRemoteUser, User } from '../../models/entities/user'; +import { driveChart, perUserDriveChart, instanceChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; const logger = driveLogger.createSubLogger('register', 'yellow'); @@ -36,11 +32,10 @@ const logger = driveLogger.createSubLogger('register', 'yellow'); * @param type Content-Type for original * @param hash Hash for original * @param size Size for original - * @param metadata */ -async function save(path: string, name: string, type: string, hash: string, size: number, metadata: IMetadata): Promise<IDriveFile> { +async function save(file: DriveFile, path: string, name: string, type: string, hash: string, size: number): Promise<DriveFile> { // thunbnail, webpublic を必要なら生成 - const alts = await generateAlts(path, type, !metadata.uri); + const alts = await generateAlts(path, type, !file.uri); if (config.drive && config.drive.storage == 'minio') { //#region ObjectStorage params @@ -60,10 +55,10 @@ async function save(path: string, name: string, type: string, hash: string, size const url = `${ baseUrl }/${ key }`; // for alts - let webpublicKey = null as string; - let webpublicUrl = null as string; - let thumbnailKey = null as string; - let thumbnailUrl = null as string; + let webpublicKey: string | null = null; + let webpublicUrl: string | null = null; + let thumbnailKey: string | null = null; + let thumbnailUrl: string | null = null; //#endregion //#region Uploads @@ -91,58 +86,52 @@ async function save(path: string, name: string, type: string, hash: string, size await Promise.all(uploads); //#endregion - //#region DB - Object.assign(metadata, { - withoutChunks: true, - storage: 'minio', - storageProps: { - key, - webpublicKey, - thumbnailKey, - }, - url, - webpublicUrl, - thumbnailUrl, - } as IMetadata); + file.url = url; + file.thumbnailUrl = thumbnailUrl; + file.webpublicUrl = webpublicUrl; + file.accessKey = key; + file.thumbnailAccessKey = thumbnailKey; + file.webpublicAccessKey = webpublicKey; + file.name = name; + file.type = type; + file.md5 = hash; + file.size = size; + file.storedInternal = false; - const file = await DriveFile.insert({ - length: size, - uploadDate: new Date(), - md5: hash, - filename: name, - metadata: metadata, - contentType: type - }); - //#endregion + return await DriveFiles.save(file); + } else { // use internal storage + const accessKey = uuid.v4(); + const thumbnailAccessKey = uuid.v4(); + const webpublicAccessKey = uuid.v4(); - return file; - } else { // use MongoDB GridFS - // #region store original - const originalDst = await getDriveFileBucket(); + const url = InternalStorage.saveFromPath(accessKey, path); - // web用(Exif削除済み)がある場合はオリジナルにアクセス制限 - if (alts.webpublic) metadata.accessKey = uuid.v4(); + let thumbnailUrl: string | null = null; + let webpublicUrl: string | null = null; - const originalFile = await storeOriginal(originalDst, name, path, type, metadata); - - logger.info(`original stored to ${originalFile._id}`); - // #endregion store original + if (alts.thumbnail) { + thumbnailUrl = InternalStorage.saveFromBuffer(thumbnailAccessKey, alts.thumbnail.data); + logger.info(`thumbnail stored: ${thumbnailAccessKey}`); + } - // #region store webpublic if (alts.webpublic) { - const webDst = await getDriveFileWebpublicBucket(); - const webFile = await storeAlts(webDst, name, alts.webpublic.data, alts.webpublic.type, originalFile._id); - logger.info(`web stored ${webFile._id}`); + webpublicUrl = InternalStorage.saveFromBuffer(webpublicAccessKey, alts.webpublic.data); + logger.info(`web stored: ${webpublicAccessKey}`); } - // #endregion store webpublic - if (alts.thumbnail) { - const thumDst = await getDriveFileThumbnailBucket(); - const thumFile = await storeAlts(thumDst, name, alts.thumbnail.data, alts.thumbnail.type, originalFile._id); - logger.info(`web stored ${thumFile._id}`); - } + file.storedInternal = true; + file.url = url; + file.thumbnailUrl = thumbnailUrl; + file.webpublicUrl = webpublicUrl; + file.accessKey = accessKey; + file.thumbnailAccessKey = thumbnailAccessKey; + file.webpublicAccessKey = webpublicAccessKey; + file.name = name; + file.type = type; + file.md5 = hash; + file.size = size; - return originalFile; + return await DriveFiles.save(file); } } @@ -154,7 +143,7 @@ async function save(path: string, name: string, type: string, hash: string, size */ export async function generateAlts(path: string, type: string, generateWeb: boolean) { // #region webpublic - let webpublic: IImage; + let webpublic: IImage | null = null; if (generateWeb) { logger.info(`creating web image`); @@ -174,7 +163,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool // #endregion webpublic // #region thumbnail - let thumbnail: IImage; + let thumbnail: IImage | null = null; if (['image/jpeg', 'image/webp'].includes(type)) { thumbnail = await ConvertToJpeg(path, 498, 280); @@ -199,7 +188,7 @@ export async function generateAlts(path: string, type: string, generateWeb: bool * Upload to ObjectStorage */ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, filename?: string) { - const minio = new Minio.Client(config.drive.config); + const minio = new Minio.Client(config.drive!.config); const metadata = { 'Content-Type': type, @@ -208,54 +197,24 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, type: string, if (filename) metadata['Content-Disposition'] = contentDisposition('inline', filename); - await minio.putObject(config.drive.bucket, key, stream, null, metadata); + await minio.putObject(config.drive!.bucket!, key, stream, undefined, metadata); } -/** - * GridFSBucketにオリジナルを格納する - */ -export async function storeOriginal(bucket: mongodb.GridFSBucket, name: string, path: string, contentType: string, metadata: any) { - return new Promise<IDriveFile>((resolve, reject) => { - const writeStream = bucket.openUploadStream(name, { - contentType, - metadata - }); +async function deleteOldFile(user: IRemoteUser) { + const q = DriveFiles.createQueryBuilder('file') + .where('file.userId = :userId', { userId: user.id }); - writeStream.once('finish', resolve); - writeStream.on('error', reject); - fs.createReadStream(path).pipe(writeStream); - }); -} + if (user.avatarId) { + q.andWhere('file.id != :avatarId', { avatarId: user.avatarId }); + } -/** - * GridFSBucketにオリジナル以外を格納する - */ -export async function storeAlts(bucket: mongodb.GridFSBucket, name: string, data: Buffer, contentType: string, originalId: mongodb.ObjectID) { - return new Promise<IDriveFile>((resolve, reject) => { - const writeStream = bucket.openUploadStream(name, { - contentType, - metadata: { - originalId - } - }); + if (user.bannerId) { + q.andWhere('file.id != :bannerId', { bannerId: user.bannerId }) + } - writeStream.once('finish', resolve); - writeStream.on('error', reject); - writeStream.end(data); - }); -} + q.orderBy('file.id', 'DESC'); -async function deleteOldFile(user: IRemoteUser) { - const oldFile = await DriveFile.findOne({ - _id: { - $nin: [user.avatarId, user.bannerId] - }, - 'metadata.userId': user._id - }, { - sort: { - _id: 1 - } - }); + const oldFile = await q.getOne(); if (oldFile) { delFile(oldFile, true); @@ -278,17 +237,17 @@ async function deleteOldFile(user: IRemoteUser) { * @return Created drive file */ export default async function( - user: IUser, + user: User, path: string, - name: string = null, - comment: string = null, - folderId: mongodb.ObjectID = null, + name: string | null = null, + comment: string | null = null, + folderId: any = null, force: boolean = false, isLink: boolean = false, - url: string = null, - uri: string = null, - sensitive: boolean = null -): Promise<IDriveFile> { + url: string | null = null, + uri: string | null = null, + sensitive: boolean | null = null +): Promise<DriveFile> { // Calc md5 hash const calcHash = new Promise<string>((res, rej) => { const readable = fs.createReadStream(path); @@ -322,55 +281,33 @@ export default async function( if (!force) { // Check if there is a file with the same hash - const much = await DriveFile.findOne({ + const much = await DriveFiles.findOne({ md5: hash, - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } + userId: user.id, }); if (much) { - logger.info(`file with same hash is found: ${much._id}`); + logger.info(`file with same hash is found: ${much.id}`); return much; } } //#region Check drive usage if (!isLink) { - const usage = await DriveFile - .aggregate([{ - $match: { - 'metadata.userId': user._id, - 'metadata.deletedAt': { $exists: false } - } - }, { - $project: { - length: true - } - }, { - $group: { - _id: null, - usage: { $sum: '$length' } - } - }]) - .then((aggregates: any[]) => { - if (aggregates.length > 0) { - return aggregates[0].usage; - } - return 0; - }); - - logger.debug(`drive usage is ${usage}`); + const usage = await DriveFiles.clacDriveUsageOf(user); const instance = await fetchMeta(); - const driveCapacity = 1024 * 1024 * (isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + + logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); // If usage limit exceeded if (usage + size > driveCapacity) { - if (isLocalUser(user)) { - throw 'no-free-space'; + if (Users.isLocalUser(user)) { + throw new Error('no-free-space'); } else { // (アバターまたはバナーを含まず)最も古いファイルを削除する - deleteOldFile(user); + deleteOldFile(user as IRemoteUser); } } } @@ -381,12 +318,12 @@ export default async function( return null; } - const driveFolder = await DriveFolder.findOne({ - _id: folderId, - userId: user._id + const driveFolder = await DriveFolders.findOne({ + id: folderId, + userId: user.id }); - if (driveFolder == null) throw 'folder-not-found'; + if (driveFolder == null) throw new Error('folder-not-found'); return driveFolder; }; @@ -426,7 +363,7 @@ export default async function( logger.debug(`average color is calculated: ${r}, ${g}, ${b}`); - const value = info.isOpaque ? [r, g, b] : [r, g, b, 255]; + const value = info.isOpaque ? `rgba(${r},${g},${b},0)` : `rgba(${r},${g},${b},255)`; properties['avgColor'] = value; } catch (e) { } @@ -435,86 +372,78 @@ export default async function( propPromises = [calcWh(), calcAvg()]; } + const profile = await UserProfiles.findOne({ userId: user.id }); + const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]); - const metadata = { - userId: user._id, - _user: { - host: user.host - }, - folderId: folder !== null ? folder._id : null, - comment: comment, - properties: properties, - withoutChunks: isLink, - isRemote: isLink, - isSensitive: isLocalUser(user) && user.settings.alwaysMarkNsfw ? true : - (sensitive !== null && sensitive !== undefined) - ? sensitive - : false - } as IMetadata; + let file = new DriveFile(); + file.id = genId(); + file.createdAt = new Date(); + file.userId = user.id; + file.userHost = user.host; + file.folderId = folder !== null ? folder.id : null; + file.comment = comment; + file.properties = properties; + file.isLink = isLink; + file.isSensitive = Users.isLocalUser(user) && profile!.alwaysMarkNsfw ? true : + (sensitive !== null && sensitive !== undefined) + ? sensitive + : false; if (url !== null) { - metadata.src = url; + file.src = url; if (isLink) { - metadata.url = url; + file.url = url; } } if (uri !== null) { - metadata.uri = uri; + file.uri = uri; } - let driveFile: IDriveFile; - if (isLink) { try { - driveFile = await DriveFile.insert({ - length: 0, - uploadDate: new Date(), - md5: hash, - filename: detectedName, - metadata: metadata, - contentType: mime - }); + file.size = 0; + file.md5 = hash; + file.name = detectedName; + file.type = mime; + + file = await DriveFiles.save(file); } catch (e) { // duplicate key error (when already registered) - if (e.code === 11000) { - logger.info(`already registered ${metadata.uri}`); + if (isDuplicateKeyValueError(e)) { + logger.info(`already registered ${file.uri}`); - driveFile = await DriveFile.findOne({ - 'metadata.uri': metadata.uri, - 'metadata.userId': user._id - }); + file = await DriveFiles.findOne({ + uri: file.uri, + userId: user.id + }) as DriveFile; } else { logger.error(e); throw e; } } } else { - driveFile = await (save(path, detectedName, mime, hash, size, metadata)); + file = await (save(file, path, detectedName, mime, hash, size)); } - logger.succ(`drive file has been created ${driveFile._id}`); + logger.succ(`drive file has been created ${file.id}`); - pack(driveFile).then(packedFile => { + DriveFiles.pack(file).then(packedFile => { // Publish driveFileCreated event - publishMainStream(user._id, 'driveFileCreated', packedFile); - publishDriveStream(user._id, 'fileCreated', packedFile); + publishMainStream(user.id, 'driveFileCreated', packedFile); + publishDriveStream(user.id, 'fileCreated', packedFile); }); // 統計を更新 - driveChart.update(driveFile, true); - perUserDriveChart.update(driveFile, true); - if (isRemoteUser(driveFile.metadata._user)) { - instanceChart.updateDrive(driveFile, true); - Instance.update({ host: driveFile.metadata._user.host }, { - $inc: { - driveUsage: driveFile.length, - driveFiles: 1 - } - }); + driveChart.update(file, true); + perUserDriveChart.update(file, true); + if (file.userHost !== null) { + instanceChart.updateDrive(file, true); + Instances.increment({ host: file.userHost }, 'driveUsage', file.size); + Instances.increment({ host: file.userHost }, 'driveFiles', 1); } - return driveFile; + return file; } diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts index c5c15ca20b..bba453b982 100644 --- a/src/services/drive/delete-file.ts +++ b/src/services/drive/delete-file.ts @@ -1,99 +1,53 @@ import * as Minio from 'minio'; -import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file'; -import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail'; import config from '../../config'; -import driveChart from '../../services/chart/drive'; -import perUserDriveChart from '../../services/chart/per-user-drive'; -import instanceChart from '../../services/chart/instance'; -import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic'; -import Instance from '../../models/instance'; -import { isRemoteUser } from '../../models/user'; +import { DriveFile } from '../../models/entities/drive-file'; +import { InternalStorage } from './internal-storage'; +import { DriveFiles, Instances } from '../../models'; +import { driveChart, perUserDriveChart, instanceChart } from '../chart'; -export default async function(file: IDriveFile, isExpired = false) { - if (file.metadata.storage == 'minio') { - const minio = new Minio.Client(config.drive.config); +export default async function(file: DriveFile, isExpired = false) { + if (file.storedInternal) { + InternalStorage.del(file.accessKey!); - // 後方互換性のため、file.metadata.storageProps.key があるかどうかチェックしています。 - // 将来的には const obj = file.metadata.storageProps.key; とします。 - const obj = file.metadata.storageProps.key ? file.metadata.storageProps.key : `${config.drive.prefix}/${file.metadata.storageProps.id}`; - await minio.removeObject(config.drive.bucket, obj); - - if (file.metadata.thumbnailUrl) { - // 後方互換性のため、file.metadata.storageProps.thumbnailKey があるかどうかチェックしています。 - // 将来的には const thumbnailObj = file.metadata.storageProps.thumbnailKey; とします。 - const thumbnailObj = file.metadata.storageProps.thumbnailKey ? file.metadata.storageProps.thumbnailKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-thumbnail`; - await minio.removeObject(config.drive.bucket, thumbnailObj); + if (file.thumbnailUrl) { + InternalStorage.del(file.thumbnailAccessKey!); } - if (file.metadata.webpublicUrl) { - const webpublicObj = file.metadata.storageProps.webpublicKey ? file.metadata.storageProps.webpublicKey : `${config.drive.prefix}/${file.metadata.storageProps.id}-original`; - await minio.removeObject(config.drive.bucket, webpublicObj); + if (file.webpublicUrl) { + InternalStorage.del(file.webpublicAccessKey!); } - } + } else if (!file.isLink) { + const minio = new Minio.Client(config.drive!.config); - // チャンクをすべて削除 - await DriveFileChunk.remove({ - files_id: file._id - }); + await minio.removeObject(config.drive!.bucket!, file.accessKey!); - const set = { - metadata: { - deletedAt: new Date(), - isExpired: isExpired + if (file.thumbnailUrl) { + await minio.removeObject(config.drive!.bucket!, file.thumbnailAccessKey!); } - } as any; - - // リモートファイル期限切れ削除後は直リンクにする - if (isExpired && file.metadata && file.metadata._user && file.metadata._user.host != null) { - set.metadata.withoutChunks = true; - set.metadata.isRemote = true; - set.metadata.url = file.metadata.uri; - set.metadata.thumbnailUrl = undefined; - set.metadata.webpublicUrl = undefined; - } - - await DriveFile.update({ _id: file._id }, { - $set: set - }); - - //#region サムネイルもあれば削除 - const thumbnail = await DriveFileThumbnail.findOne({ - 'metadata.originalId': file._id - }); - if (thumbnail) { - await DriveFileThumbnailChunk.remove({ - files_id: thumbnail._id - }); - - await DriveFileThumbnail.remove({ _id: thumbnail._id }); + if (file.webpublicUrl) { + await minio.removeObject(config.drive!.bucket!, file.webpublicAccessKey!); + } } - //#endregion - - //#region Web公開用もあれば削除 - const webpublic = await DriveFileWebpublic.findOne({ - 'metadata.originalId': file._id - }); - if (webpublic) { - await DriveFileWebpublicChunk.remove({ - files_id: webpublic._id + // リモートファイル期限切れ削除後は直リンクにする + if (isExpired && file.userHost !== null && file.uri != null) { + DriveFiles.update(file.id, { + isLink: true, + url: file.uri, + thumbnailUrl: null, + webpublicUrl: null }); - - await DriveFileWebpublic.remove({ _id: webpublic._id }); + } else { + DriveFiles.delete(file.id); } - //#endregion // 統計を更新 driveChart.update(file, false); perUserDriveChart.update(file, false); - if (isRemoteUser(file.metadata._user)) { + if (file.userHost !== null) { instanceChart.updateDrive(file, false); - Instance.update({ host: file.metadata._user.host }, { - $inc: { - driveUsage: -file.length, - driveFiles: -1 - } - }); + Instances.decrement({ host: file.userHost }, 'driveUsage', file.size); + Instances.decrement({ host: file.userHost }, 'driveFiles', 1); } } diff --git a/src/services/drive/image-processor.ts b/src/services/drive/image-processor.ts index 3c538390b0..89ac331ca1 100644 --- a/src/services/drive/image-processor.ts +++ b/src/services/drive/image-processor.ts @@ -2,7 +2,7 @@ import * as sharp from 'sharp'; export type IImage = { data: Buffer; - ext: string; + ext: string | null; type: string; }; diff --git a/src/services/drive/internal-storage.ts b/src/services/drive/internal-storage.ts new file mode 100644 index 0000000000..ff890d7d47 --- /dev/null +++ b/src/services/drive/internal-storage.ts @@ -0,0 +1,27 @@ +import * as fs from 'fs'; +import * as Path from 'path'; +import config from '../../config'; + +export class InternalStorage { + private static readonly path = Path.resolve(`${__dirname}/../../../files`); + + public static read(key: string) { + return fs.createReadStream(`${InternalStorage.path}/${key}`); + } + + public static saveFromPath(key: string, srcPath: string) { + fs.mkdirSync(InternalStorage.path, { recursive: true }); + fs.copyFileSync(srcPath, `${InternalStorage.path}/${key}`); + return `${config.url}/files/${key}`; + } + + public static saveFromBuffer(key: string, data: Buffer) { + fs.mkdirSync(InternalStorage.path, { recursive: true }); + fs.writeFileSync(`${InternalStorage.path}/${key}`, data); + return `${config.url}/files/${key}`; + } + + public static del(key: string) { + fs.unlink(`${InternalStorage.path}/${key}`, () => {}); + } +} diff --git a/src/services/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts index cdf6ba0cef..bfe8c5c3b2 100644 --- a/src/services/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -1,26 +1,25 @@ -import * as URL from 'url'; - -import { IDriveFile, validateFileName } from '../../models/drive-file'; import create from './add-file'; -import { IUser } from '../../models/user'; -import * as mongodb from 'mongodb'; +import { User } from '../../models/entities/user'; import { driveLogger } from './logger'; import { createTemp } from '../../misc/create-temp'; import { downloadUrl } from '../../misc/donwload-url'; +import { DriveFolder } from '../../models/entities/drive-folder'; +import { DriveFile } from '../../models/entities/drive-file'; +import { DriveFiles } from '../../models'; const logger = driveLogger.createSubLogger('downloader'); export default async ( url: string, - user: IUser, - folderId: mongodb.ObjectID = null, - uri: string = null, + user: User, + folderId: DriveFolder['id'] | null = null, + uri: string | null = null, sensitive = false, force = false, link = false -): Promise<IDriveFile> => { - let name = URL.parse(url).pathname.split('/').pop(); - if (!validateFileName(name)) { +): Promise<DriveFile> => { + let name = new URL(url).pathname.split('/').pop() || null; + if (name == null || !DriveFiles.validateFileName(name)) { name = null; } @@ -30,12 +29,12 @@ export default async ( // write content at URL to temp file await downloadUrl(url, path); - let driveFile: IDriveFile; + let driveFile: DriveFile; let error; try { driveFile = await create(user, path, name, null, folderId, force, link, url, uri, sensitive); - logger.succ(`Got: ${driveFile._id}`); + logger.succ(`Got: ${driveFile.id}`); } catch (e) { error = e; logger.error(`Failed to create drive file: ${e}`, { @@ -50,6 +49,6 @@ export default async ( if (error) { throw error; } else { - return driveFile; + return driveFile!; } }; diff --git a/src/services/following/create.ts b/src/services/following/create.ts index 1eaad750f7..b69dfe42b9 100644 --- a/src/services/following/create.ts +++ b/src/services/following/create.ts @@ -1,100 +1,71 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; -import Blocking from '../../models/blocking'; import { publishMainStream } from '../stream'; -import notify from '../../services/create-notification'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderAccept from '../../remote/activitypub/renderer/accept'; import renderReject from '../../remote/activitypub/renderer/reject'; import { deliver } from '../../queue'; import createFollowRequest from './requests/create'; -import perUserFollowingChart from '../../services/chart/per-user-following'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; import Logger from '../logger'; -import FollowRequest from '../../models/follow-request'; import { IdentifiableError } from '../../misc/identifiable-error'; +import { User } from '../../models/entities/user'; +import { Followings, Users, FollowRequests, Blockings, Instances, UserProfiles } from '../../models'; +import { instanceChart, perUserFollowingChart } from '../chart'; +import { genId } from '../../misc/gen-id'; +import { createNotification } from '../create-notification'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; +import { ensure } from '../../prelude/ensure'; const logger = new Logger('following/create'); -export async function insertFollowingDoc(followee: IUser, follower: IUser) { +export async function insertFollowingDoc(followee: User, follower: User) { + if (follower.id === followee.id) return; + let alreadyFollowed = false; - await Following.insert({ + await Followings.save({ + id: genId(), createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id, + followerId: follower.id, + followeeId: followee.id, // 非正規化 - _follower: { - host: follower.host, - inbox: isRemoteUser(follower) ? follower.inbox : undefined, - sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined - }, - _followee: { - host: followee.host, - inbox: isRemoteUser(followee) ? followee.inbox : undefined, - sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined - } + followerHost: follower.host, + followerInbox: Users.isRemoteUser(follower) ? follower.inbox : null, + followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : null, + followeeHost: followee.host, + followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : null, + followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : null }).catch(e => { - if (e.code === 11000 && isRemoteUser(follower) && isLocalUser(followee)) { - logger.info(`Insert duplicated ignore. ${follower._id} => ${followee._id}`); + if (isDuplicateKeyValueError(e) && Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { + logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`); alreadyFollowed = true; } else { throw e; } }); - const removed = await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - if (removed.deletedCount === 1) { - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - } - if (alreadyFollowed) return; //#region Increment counts - User.update({ _id: follower._id }, { - $inc: { - followingCount: 1 - } - }); - - User.update({ _id: followee._id }, { - $inc: { - followersCount: 1 - } - }); + Users.increment({ id: follower.id }, 'followingCount', 1); + Users.increment({ id: followee.id }, 'followersCount', 1); //#endregion //#region Update instance stats - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { registerOrFetchInstanceDoc(follower.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followingCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'followingCount', 1); instanceChart.updateFollowing(i.host, true); }); - } else if (isLocalUser(follower) && isRemoteUser(followee)) { + } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { registerOrFetchInstanceDoc(followee.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followersCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'followersCount', 1); instanceChart.updateFollowers(i.host, true); }); } @@ -103,71 +74,71 @@ export async function insertFollowingDoc(followee: IUser, follower: IUser) { perUserFollowingChart.update(follower, followee, true); // Publish follow event - if (isLocalUser(follower)) { - packUser(followee, follower, { + if (Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'follow', packed)); + }).then(packed => publishMainStream(follower.id, 'follow', packed)); } // Publish followed event - if (isLocalUser(followee)) { - packUser(follower, followee).then(packed => publishMainStream(followee._id, 'followed', packed)), + if (Users.isLocalUser(followee)) { + Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)), // 通知を作成 - notify(followee._id, follower._id, 'follow'); + createNotification(followee.id, follower.id, 'follow'); } } -export default async function(follower: IUser, followee: IUser, requestId?: string) { +export default async function(follower: User, followee: User, requestId?: string) { // check blocking const [blocking, blocked] = await Promise.all([ - Blocking.findOne({ - blockerId: follower._id, - blockeeId: followee._id, + Blockings.findOne({ + blockerId: follower.id, + blockeeId: followee.id, }), - Blocking.findOne({ - blockerId: followee._id, - blockeeId: follower._id, + Blockings.findOne({ + blockerId: followee.id, + blockeeId: follower.id, }) ]); - if (isRemoteUser(follower) && isLocalUser(followee) && blocked) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocked) { // リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。 const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee)); deliver(followee , content, follower.inbox); return; - } else if (isRemoteUser(follower) && isLocalUser(followee) && blocking) { + } else if (Users.isRemoteUser(follower) && Users.isLocalUser(followee) && blocking) { // リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。 - await Blocking.remove({ - _id: blocking._id - }); + await Blockings.delete(blocking.id); } else { // それ以外は単純に例外 if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking'); if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); } + const followeeProfile = await UserProfiles.findOne(followee.id).then(ensure); + // フォロー対象が鍵アカウントである or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく - if (followee.isLocked || (followee.carefulBot && follower.isBot) || (isLocalUser(follower) && isRemoteUser(followee))) { + if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) { let autoAccept = false; // 鍵アカウントであっても、既にフォローされていた場合はスルー - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id, + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id, }); if (following) { autoAccept = true; } // フォローしているユーザーは自動承認オプション - if (!autoAccept && (isLocalUser(followee) && followee.autoAcceptFollowed)) { - const followed = await Following.findOne({ - followerId: followee._id, - followeeId: follower._id + if (!autoAccept && (Users.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) { + const followed = await Followings.findOne({ + followerId: followee.id, + followeeId: follower.id }); if (followed) autoAccept = true; @@ -181,7 +152,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri await insertFollowingDoc(followee, follower); - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee)); deliver(followee, content, follower.inbox); } diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts index d85c8472bb..ad09f0e6d1 100644 --- a/src/services/following/delete.ts +++ b/src/services/following/delete.ts @@ -1,22 +1,20 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; -import Following from '../../models/following'; import { publishMainStream } from '../stream'; import { renderActivity } from '../../remote/activitypub/renderer'; import renderFollow from '../../remote/activitypub/renderer/follow'; import renderUndo from '../../remote/activitypub/renderer/undo'; import { deliver } from '../../queue'; -import perUserFollowingChart from '../../services/chart/per-user-following'; import Logger from '../logger'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; +import { User } from '../../models/entities/user'; +import { Followings, Users, Instances } from '../../models'; +import { instanceChart, perUserFollowingChart } from '../chart'; const logger = new Logger('following/delete'); -export default async function(follower: IUser, followee: IUser, silent = false) { - const following = await Following.findOne({ - followerId: follower._id, - followeeId: followee._id +export default async function(follower: User, followee: User, silent = false) { + const following = await Followings.findOne({ + followerId: follower.id, + followeeId: followee.id }); if (following == null) { @@ -24,45 +22,25 @@ export default async function(follower: IUser, followee: IUser, silent = false) return; } - Following.remove({ - _id: following._id - }); + Followings.delete(following.id); //#region Decrement following count - User.update({ _id: follower._id }, { - $inc: { - followingCount: -1 - } - }); + Users.decrement({ id: follower.id }, 'followingCount', 1); //#endregion //#region Decrement followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: -1 - } - }); + Users.decrement({ id: followee.id }, 'followersCount', 1); //#endregion //#region Update instance stats - if (isRemoteUser(follower) && isLocalUser(followee)) { + if (Users.isRemoteUser(follower) && Users.isLocalUser(followee)) { registerOrFetchInstanceDoc(follower.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followingCount: -1 - } - }); - + Instances.decrement({ id: i.id }, 'followingCount', 1); instanceChart.updateFollowing(i.host, false); }); - } else if (isLocalUser(follower) && isRemoteUser(followee)) { + } else if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { registerOrFetchInstanceDoc(followee.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - followersCount: -1 - } - }); - + Instances.decrement({ id: i.id }, 'followersCount', 1); instanceChart.updateFollowers(i.host, false); }); } @@ -71,13 +49,13 @@ export default async function(follower: IUser, followee: IUser, silent = false) perUserFollowingChart.update(follower, followee, false); // Publish unfollow event - if (!silent && isLocalUser(follower)) { - packUser(followee, follower, { + if (!silent && Users.isLocalUser(follower)) { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } diff --git a/src/services/following/requests/accept-all.ts b/src/services/following/requests/accept-all.ts index cf1a9e923d..70e7448aad 100644 --- a/src/services/following/requests/accept-all.ts +++ b/src/services/following/requests/accept-all.ts @@ -1,24 +1,19 @@ -import User, { IUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import accept from './accept'; +import { User } from '../../../models/entities/user'; +import { FollowRequests, Users } from '../../../models'; +import { ensure } from '../../../prelude/ensure'; /** * 指定したユーザー宛てのフォローリクエストをすべて承認 * @param user ユーザー */ -export default async function(user: IUser) { - const requests = await FollowRequest.find({ - followeeId: user._id +export default async function(user: User) { + const requests = await FollowRequests.find({ + followeeId: user.id }); for (const request of requests) { - const follower = await User.findOne({ _id: request.followerId }); + const follower = await Users.findOne(request.followerId).then(ensure); accept(user, follower); } - - User.update({ _id: user._id }, { - $set: { - pendingReceivedFollowRequestsCount: 0 - } - }); } diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts index 284c6d5e19..1397514ad1 100644 --- a/src/services/following/requests/accept.ts +++ b/src/services/following/requests/accept.ts @@ -1,26 +1,26 @@ -import { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderAccept from '../../../remote/activitypub/renderer/accept'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; import { insertFollowingDoc } from '../create'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { FollowRequests, Users } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +export default async function(followee: User, follower: User) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); await insertFollowingDoc(followee, follower); - if (isRemoteUser(follower) && request) { - const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId), followee as ILocalUser)); + if (Users.isRemoteUser(follower) && request) { + const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId!), followee as ILocalUser)); deliver(followee as ILocalUser, content, follower.inbox); } - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts index af4cca85fe..98fec5d331 100644 --- a/src/services/following/requests/cancel.ts +++ b/src/services/following/requests/cancel.ts @@ -1,39 +1,33 @@ -import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderUndo from '../../../remote/activitypub/renderer/undo'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; import { IdentifiableError } from '../../../misc/identifiable-error'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users, FollowRequests } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - if (isRemoteUser(followee)) { +export default async function(followee: User, follower: User) { + if (Users.isRemoteUser(followee)) { const content = renderActivity(renderUndo(renderFollow(follower, followee), follower)); deliver(follower as ILocalUser, content, followee.inbox); } - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); if (request == null) { throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found'); } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); } diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts index 10c534f529..32e79d136d 100644 --- a/src/services/following/requests/create.ts +++ b/src/services/following/requests/create.ts @@ -1,66 +1,59 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user'; import { publishMainStream } from '../../stream'; -import notify from '../../../services/create-notification'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import { deliver } from '../../../queue'; -import FollowRequest from '../../../models/follow-request'; -import Blocking from '../../../models/blocking'; +import { User } from '../../../models/entities/user'; +import { Blockings, FollowRequests, Users } from '../../../models'; +import { genId } from '../../../misc/gen-id'; +import { createNotification } from '../../create-notification'; + +export default async function(follower: User, followee: User, requestId?: string) { + if (follower.id === followee.id) return; -export default async function(follower: IUser, followee: IUser, requestId?: string) { // check blocking const [blocking, blocked] = await Promise.all([ - Blocking.findOne({ - blockerId: follower._id, - blockeeId: followee._id, + Blockings.findOne({ + blockerId: follower.id, + blockeeId: followee.id, }), - Blocking.findOne({ - blockerId: followee._id, - blockeeId: follower._id, + Blockings.findOne({ + blockerId: followee.id, + blockeeId: follower.id, }) ]); if (blocking != null) throw new Error('blocking'); if (blocked != null) throw new Error('blocked'); - await FollowRequest.insert({ + await FollowRequests.save({ + id: genId(), createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id, + followerId: follower.id, + followeeId: followee.id, requestId, // 非正規化 - _follower: { - host: follower.host, - inbox: isRemoteUser(follower) ? follower.inbox : undefined, - sharedInbox: isRemoteUser(follower) ? follower.sharedInbox : undefined - }, - _followee: { - host: followee.host, - inbox: isRemoteUser(followee) ? followee.inbox : undefined, - sharedInbox: isRemoteUser(followee) ? followee.sharedInbox : undefined - } - }); - - await User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: 1 - } + followerHost: follower.host, + followerInbox: Users.isRemoteUser(follower) ? follower.inbox : undefined, + followerSharedInbox: Users.isRemoteUser(follower) ? follower.sharedInbox : undefined, + followeeHost: followee.host, + followeeInbox: Users.isRemoteUser(followee) ? followee.inbox : undefined, + followeeSharedInbox: Users.isRemoteUser(followee) ? followee.sharedInbox : undefined }); // Publish receiveRequest event - if (isLocalUser(followee)) { - packUser(follower, followee).then(packed => publishMainStream(followee._id, 'receiveFollowRequest', packed)); + if (Users.isLocalUser(followee)) { + Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'receiveFollowRequest', packed)); - packUser(followee, followee, { + Users.pack(followee, followee, { detail: true - }).then(packed => publishMainStream(followee._id, 'meUpdated', packed)); + }).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); // 通知を作成 - notify(followee._id, follower._id, 'receiveFollowRequest'); + createNotification(followee.id, follower.id, 'receiveFollowRequest'); } - if (isLocalUser(follower) && isRemoteUser(followee)) { + if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) { const content = renderActivity(renderFollow(follower, followee)); deliver(follower, content, followee.inbox); } diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts index cb924df811..d5b5e48c41 100644 --- a/src/services/following/requests/reject.ts +++ b/src/services/following/requests/reject.ts @@ -1,34 +1,28 @@ -import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user'; -import FollowRequest from '../../../models/follow-request'; import { renderActivity } from '../../../remote/activitypub/renderer'; import renderFollow from '../../../remote/activitypub/renderer/follow'; import renderReject from '../../../remote/activitypub/renderer/reject'; import { deliver } from '../../../queue'; import { publishMainStream } from '../../stream'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users, FollowRequests } from '../../../models'; -export default async function(followee: IUser, follower: IUser) { - if (isRemoteUser(follower)) { - const request = await FollowRequest.findOne({ - followeeId: followee._id, - followerId: follower._id +export default async function(followee: User, follower: User) { + if (Users.isRemoteUser(follower)) { + const request = await FollowRequests.findOne({ + followeeId: followee.id, + followerId: follower.id }); - const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee as ILocalUser)); + const content = renderActivity(renderReject(renderFollow(follower, followee, request!.requestId!), followee as ILocalUser)); deliver(followee as ILocalUser, content, follower.inbox); } - await FollowRequest.remove({ - followeeId: followee._id, - followerId: follower._id + await FollowRequests.delete({ + followeeId: followee.id, + followerId: follower.id }); - User.update({ _id: followee._id }, { - $inc: { - pendingReceivedFollowRequestsCount: -1 - } - }); - - packUser(followee, follower, { + Users.pack(followee, follower, { detail: true - }).then(packed => publishMainStream(follower._id, 'unfollow', packed)); + }).then(packed => publishMainStream(follower.id, 'unfollow', packed)); } diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts index 4d0ae3c149..a6d2dfcdbf 100644 --- a/src/services/i/pin.ts +++ b/src/services/i/pin.ts @@ -1,59 +1,51 @@ import config from '../../config'; -import * as mongo from 'mongodb'; -import User, { isLocalUser, isRemoteUser, ILocalUser, IUser } from '../../models/user'; -import Note, { packMany } from '../../models/note'; -import Following from '../../models/following'; import renderAdd from '../../remote/activitypub/renderer/add'; import renderRemove from '../../remote/activitypub/renderer/remove'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import { IdentifiableError } from '../../misc/identifiable-error'; +import { User, ILocalUser } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { Notes, UserNotePinings, Users, Followings } from '../../models'; +import { UserNotePining } from '../../models/entities/user-note-pinings'; +import { genId } from '../../misc/gen-id'; /** * 指定した投稿をピン留めします * @param user * @param noteId */ -export async function addPinned(user: IUser, noteId: mongo.ObjectID) { +export async function addPinned(user: User, noteId: Note['id']) { // Fetch pinee - const note = await Note.findOne({ - _id: noteId, - userId: user._id + const note = await Notes.findOne({ + id: noteId, + userId: user.id }); - if (note === null) { + if (note == null) { throw new IdentifiableError('70c4e51f-5bea-449c-a030-53bee3cce202', 'No such note.'); } - let pinnedNoteIds = user.pinnedNoteIds || []; + const pinings = await UserNotePinings.find({ userId: user.id }); - //#region 現在ピン留め投稿している投稿が実際にデータベースに存在しているのかチェック - // データベースの欠損などで存在していない(または破損している)場合があるので。 - // 存在していなかったらピン留め投稿から外す - const pinnedNotes = await packMany(pinnedNoteIds, null, { detail: true }); - - pinnedNoteIds = pinnedNoteIds.filter(id => pinnedNotes.some(n => n.id.toString() === id.toHexString())); - //#endregion - - if (pinnedNoteIds.length >= 5) { + if (pinings.length >= 5) { throw new IdentifiableError('15a018eb-58e5-4da1-93be-330fcc5e4e1a', 'You can not pin notes any more.'); } - if (pinnedNoteIds.some(id => id.equals(note._id))) { + if (pinings.some(pining => pining.noteId === note.id)) { throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.'); } - pinnedNoteIds.unshift(note._id); - - await User.update(user._id, { - $set: { - pinnedNoteIds: pinnedNoteIds - } - }); + await UserNotePinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + noteId: note.id + } as UserNotePining); // Deliver to remote followers - if (isLocalUser(user)) { - deliverPinnedChange(user._id, note._id, true); + if (Users.isLocalUser(user)) { + deliverPinnedChange(user.id, note.id, true); } } @@ -62,43 +54,39 @@ export async function addPinned(user: IUser, noteId: mongo.ObjectID) { * @param user * @param noteId */ -export async function removePinned(user: IUser, noteId: mongo.ObjectID) { +export async function removePinned(user: User, noteId: Note['id']) { // Fetch unpinee - const note = await Note.findOne({ - _id: noteId, - userId: user._id + const note = await Notes.findOne({ + id: noteId, + userId: user.id }); - if (note === null) { + if (note == null) { throw new IdentifiableError('b302d4cf-c050-400a-bbb3-be208681f40c', 'No such note.'); } - const pinnedNoteIds = (user.pinnedNoteIds || []).filter(id => !id.equals(note._id)); - - await User.update(user._id, { - $set: { - pinnedNoteIds: pinnedNoteIds - } + UserNotePinings.delete({ + userId: user.id, + noteId: note.id }); // Deliver to remote followers - if (isLocalUser(user)) { - deliverPinnedChange(user._id, noteId, false); + if (Users.isLocalUser(user)) { + deliverPinnedChange(user.id, noteId, false); } } -export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.ObjectID, isAddition: boolean) { - const user = await User.findOne({ - _id: userId - }); +export async function deliverPinnedChange(userId: User['id'], noteId: Note['id'], isAddition: boolean) { + const user = await Users.findOne(userId); + if (user == null) throw new Error('user not found'); - if (!isLocalUser(user)) return; + if (!Users.isLocalUser(user)) return; const queue = await CreateRemoteInboxes(user); if (queue.length < 1) return; - const target = `${config.url}/users/${user._id}/collections/featured`; + const target = `${config.url}/users/${user.id}/collections/featured`; const item = `${config.url}/notes/${noteId}`; const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item)); @@ -112,17 +100,15 @@ export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo. * @param user ローカルユーザー */ async function CreateRemoteInboxes(user: ILocalUser): Promise<string[]> { - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; for (const following of followers) { - const follower = following._follower; - - if (isRemoteUser(follower)) { - const inbox = follower.sharedInbox || follower.inbox; + if (Followings.isRemoteFollower(following)) { + const inbox = following.followerSharedInbox || following.followerInbox; if (!queue.includes(inbox)) queue.push(inbox); } } diff --git a/src/services/i/update.ts b/src/services/i/update.ts index 887cecb04c..ddb6704a03 100644 --- a/src/services/i/update.ts +++ b/src/services/i/update.ts @@ -1,29 +1,25 @@ -import * as mongo from 'mongodb'; -import User, { isLocalUser, isRemoteUser } from '../../models/user'; -import Following from '../../models/following'; -import renderPerson from '../../remote/activitypub/renderer/person'; import renderUpdate from '../../remote/activitypub/renderer/update'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; +import { Followings, Users } from '../../models'; +import { User } from '../../models/entities/user'; +import { renderPerson } from '../../remote/activitypub/renderer/person'; -export async function publishToFollowers(userId: mongo.ObjectID) { - const user = await User.findOne({ - _id: userId - }); +export async function publishToFollowers(userId: User['id']) { + const user = await Users.findOne(userId); + if (user == null) throw new Error('user not found'); - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { for (const following of followers) { - const follower = following._follower; - - if (isRemoteUser(follower)) { - const inbox = follower.sharedInbox || follower.inbox; + if (Followings.isRemoteFollower(following)) { + const inbox = following.followerSharedInbox || following.followerInbox; if (!queue.includes(inbox)) queue.push(inbox); } } diff --git a/src/services/logger.ts b/src/services/logger.ts index aa93954bc1..190bbdd253 100644 --- a/src/services/logger.ts +++ b/src/services/logger.ts @@ -3,18 +3,20 @@ import * as os from 'os'; import chalk from 'chalk'; import * as dateformat from 'dateformat'; import { program } from '../argv'; -import Log from '../models/log'; +import { getRepository } from 'typeorm'; +import { Log } from '../models/entities/log'; +import { genId } from '../misc/gen-id'; type Domain = { name: string; - color: string; + color?: string; }; type Level = 'error' | 'success' | 'warning' | 'debug' | 'info'; export default class Logger { private domain: Domain; - private parentLogger: Logger; + private parentLogger: Logger | null = null; private store: boolean; constructor(domain: string, color?: string, store = true) { @@ -31,9 +33,8 @@ export default class Logger { return logger; } - private log(level: Level, message: string, data: Record<string, any>, important = false, subDomains: Domain[] = [], store = true): void { + private log(level: Level, message: string, data?: Record<string, any> | null, important = false, subDomains: Domain[] = [], store = true): void { if (program.quiet) return; - if (process.env.NODE_ENV === 'test') return; if (!this.store) store = false; if (this.parentLogger) { @@ -65,19 +66,21 @@ export default class Logger { console.log(important ? chalk.bold(log) : log); if (store) { - Log.insert({ + const Logs = getRepository(Log); + Logs.insert({ + id: genId(), createdAt: new Date(), machine: os.hostname(), - worker: worker, + worker: worker.toString(), domain: [this.domain].concat(subDomains).map(d => d.name), level: level, message: message, data: data, - }); + } as Log); } } - public error(x: string | Error, data?: Record<string, any>, important = false): void { // 実行を継続できない状況で使う + public error(x: string | Error, data?: Record<string, any> | null, important = false): void { // 実行を継続できない状況で使う if (x instanceof Error) { data = data || {}; data.e = x; @@ -87,21 +90,21 @@ export default class Logger { } } - public warn(message: string, data?: Record<string, any>, important = false): void { // 実行を継続できるが改善すべき状況で使う + public warn(message: string, data?: Record<string, any> | null, important = false): void { // 実行を継続できるが改善すべき状況で使う this.log('warning', message, data, important); } - public succ(message: string, data?: Record<string, any>, important = false): void { // 何かに成功した状況で使う + public succ(message: string, data?: Record<string, any> | null, important = false): void { // 何かに成功した状況で使う this.log('success', message, data, important); } - public debug(message: string, data?: Record<string, any>, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報) + public debug(message: string, data?: Record<string, any> | null, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報) if (process.env.NODE_ENV != 'production' || program.verbose) { this.log('debug', message, data, important); } } - public info(message: string, data?: Record<string, any>, important = false): void { // それ以外 + public info(message: string, data?: Record<string, any> | null, important = false): void { // それ以外 this.log('info', message, data, important); } } diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 85201086d4..8c85a5c275 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -1,61 +1,55 @@ import es from '../../db/elasticsearch'; -import Note, { pack, INote, IChoice } from '../../models/note'; -import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user'; -import { publishMainStream, publishHomeTimelineStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../stream'; -import Following from '../../models/following'; +import { publishMainStream, publishNotesStream } from '../stream'; import { deliver } from '../../queue'; import renderNote from '../../remote/activitypub/renderer/note'; import renderCreate from '../../remote/activitypub/renderer/create'; import renderAnnounce from '../../remote/activitypub/renderer/announce'; import { renderActivity } from '../../remote/activitypub/renderer'; -import DriveFile, { IDriveFile } from '../../models/drive-file'; -import notify from '../../services/create-notification'; -import NoteWatching from '../../models/note-watching'; import watch from './watch'; -import Mute from '../../models/mute'; import { parse } from '../../mfm/parse'; -import { IApp } from '../../models/app'; -import UserList from '../../models/user-list'; -import resolveUser from '../../remote/resolve-user'; -import Meta from '../../models/meta'; +import { resolveUser } from '../../remote/resolve-user'; import config from '../../config'; import { updateHashtag } from '../update-hashtag'; -import isQuote from '../../misc/is-quote'; -import notesChart from '../../services/chart/notes'; -import perUserNotesChart from '../../services/chart/per-user-notes'; -import activeUsersChart from '../../services/chart/active-users'; -import instanceChart from '../../services/chart/instance'; -import * as deepcopy from 'deepcopy'; - -import { erase, concat } from '../../prelude/array'; +import { concat } from '../../prelude/array'; import insertNoteUnread from './unread'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; import extractMentions from '../../misc/extract-mentions'; import extractEmojis from '../../misc/extract-emojis'; import extractHashtags from '../../misc/extract-hashtags'; +import { Note } from '../../models/entities/note'; +import { Mutings, Users, NoteWatchings, Followings, Notes, Instances, UserProfiles } from '../../models'; +import { DriveFile } from '../../models/entities/drive-file'; +import { App } from '../../models/entities/app'; +import { Not, getConnection } from 'typeorm'; +import { User, ILocalUser, IRemoteUser } from '../../models/entities/user'; +import { genId } from '../../misc/gen-id'; +import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '../chart'; +import { Poll, IPoll } from '../../models/entities/poll'; +import { createNotification } from '../create-notification'; +import { isDuplicateKeyValueError } from '../../misc/is-duplicate-key-value-error'; +import { ensure } from '../../prelude/ensure'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; class NotificationManager { - private notifier: IUser; - private note: INote; + private notifier: User; + private note: Note; private queue: { - target: ILocalUser['_id']; + target: ILocalUser['id']; reason: NotificationType; }[]; - constructor(notifier: IUser, note: INote) { + constructor(notifier: User, note: Note) { this.notifier = notifier; this.note = note; this.queue = []; } - public push(notifiee: ILocalUser['_id'], reason: NotificationType) { + public push(notifiee: ILocalUser['id'], reason: NotificationType) { // 自分自身へは通知しない - if (this.notifier._id.equals(notifiee)) return; + if (this.notifier.id === notifiee) return; - const exist = this.queue.find(x => x.target.equals(notifiee)); + const exist = this.queue.find(x => x.target === notifiee); if (exist) { // 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする @@ -73,16 +67,16 @@ class NotificationManager { public async deliver() { for (const x of this.queue) { // ミュート情報を取得 - const mentioneeMutes = await Mute.find({ + const mentioneeMutes = await Mutings.find({ muterId: x.target }); - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId); // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する - if (!mentioneesMutedUserIds.includes(this.notifier._id.toString())) { - notify(x.target, this.notifier._id, x.reason, { - noteId: this.note._id + if (!mentioneesMutedUserIds.includes(this.notifier.id)) { + createNotification(x.target, this.notifier.id, x.reason, { + noteId: this.note.id }); } } @@ -90,28 +84,28 @@ class NotificationManager { } type Option = { - createdAt?: Date; - name?: string; - text?: string; - reply?: INote; - renote?: INote; - files?: IDriveFile[]; - geo?: any; - poll?: any; - viaMobile?: boolean; - localOnly?: boolean; - cw?: string; + createdAt?: Date | null; + name?: string | null; + text?: string | null; + reply?: Note | null; + renote?: Note | null; + files?: DriveFile[] | null; + geo?: any | null; + poll?: IPoll | null; + viaMobile?: boolean | null; + localOnly?: boolean | null; + cw?: string | null; visibility?: string; - visibleUsers?: IUser[]; - apMentions?: IUser[]; - apHashtags?: string[]; - apEmojis?: string[]; - questionUri?: string; - uri?: string; - app?: IApp; + visibleUsers?: User[] | null; + apMentions?: User[] | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; + questionUri?: string | null; + uri?: string | null; + app?: App | null; }; -export default async (user: IUser, data: Option, silent = false) => new Promise<INote>(async (res, rej) => { +export default async (user: User, data: Option, silent = false) => new Promise<Note>(async (res, rej) => { const isFirstNote = user.notesCount === 0; if (data.createdAt == null) data.createdAt = new Date(); @@ -124,20 +118,6 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< data.visibility = 'home'; } - if (data.visibleUsers) { - data.visibleUsers = erase(null, data.visibleUsers); - } - - // リプライ対象が削除された投稿だったらreject - if (data.reply && data.reply.deletedAt != null) { - return rej('Reply target has been deleted'); - } - - // Renote対象が削除された投稿だったらreject - if (data.renote && data.renote.deletedAt != null) { - return rej('Renote target has been deleted'); - } - // Renote対象が「ホームまたは全体」以外の公開範囲ならreject if (data.renote && data.renote.visibility != 'public' && data.renote.visibility != 'home') { return rej('Renote target is not public or home'); @@ -173,10 +153,10 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< // Parse MFM if needed if (!tags || !emojis || !mentionedUsers) { - const tokens = data.text ? parse(data.text) : []; - const cwTokens = data.cw ? parse(data.cw) : []; + const tokens = data.text ? parse(data.text)! : []; + const cwTokens = data.cw ? parse(data.cw)! : []; const choiceTokens = data.poll && data.poll.choices - ? concat((data.poll.choices as IChoice[]).map(choice => parse(choice.text))) + ? concat(data.poll.choices.map(choice => parse(choice)!)) : []; const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); @@ -188,24 +168,23 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens); } - // MongoDBのインデックス対象は128文字以上にできない tags = tags.filter(tag => tag.length <= 100); - if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) { - mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); + if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) { + mentionedUsers.push(await Users.findOne(data.reply.userId).then(ensure)); } if (data.visibility == 'specified') { + if (data.visibleUsers == null) throw new Error('invalid param'); + for (const u of data.visibleUsers) { - if (!mentionedUsers.some(x => x._id.equals(u._id))) { + if (!mentionedUsers.some(x => x.id === u.id)) { mentionedUsers.push(u); } } - for (const u of mentionedUsers) { - if (!data.visibleUsers.some(x => x._id.equals(u._id))) { - data.visibleUsers.push(u); - } + if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) { + data.visibleUsers.push(await Users.findOne(data.reply.userId).then(ensure)); } } @@ -213,51 +192,30 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< res(note); - if (note == null) { - return; - } - // 統計を更新 notesChart.update(note, true); perUserNotesChart.update(user, note, true); // ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい - if (isRemoteUser(user)) activeUsersChart.update(user); + if (Users.isRemoteUser(user)) activeUsersChart.update(user); // Register host - if (isRemoteUser(user)) { + if (Users.isRemoteUser(user)) { registerOrFetchInstanceDoc(user.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - notesCount: 1 - } - }); - - instanceChart.updateNote(i.host, true); + Instances.increment({ id: i.id }, 'notesCount', 1); + instanceChart.updateNote(i.host, note, true); }); } // ハッシュタグ更新 for (const tag of tags) updateHashtag(user, tag); - // ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティにこの投稿を追加 - if (data.files) { - for (const file of data.files) { - DriveFile.update({ _id: file._id }, { - $push: { - 'metadata.attachedNoteIds': note._id - } - }); - } - } - - // Increment notes count - incNotesCount(user); - // Increment notes count (user) incNotesCountOfUser(user); // 未読通知を作成 if (data.visibility == 'specified') { + if (data.visibleUsers == null) throw new Error('invalid param'); + for (const u of data.visibleUsers) { insertNoteUnread(u, note, true); } @@ -275,20 +233,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< incRenoteCount(data.renote); } - if (isQuote(note)) { - saveQuote(data.renote, note); - } - // Pack the note - const noteObj = await pack(note); + const noteObj = await Notes.pack(note); if (isFirstNote) { noteObj.isFirstNote = true; } - if (tags.length > 0) { - publishHashtagStream(noteObj); - } + publishNotesStream(noteObj); const nm = new NotificationManager(user, note); const nmRelatedPromises = []; @@ -297,22 +249,24 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const noteActivity = await renderNoteOrRenoteActivity(data, note); - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity); } + const profile = await UserProfiles.findOne({ userId: user.id }).then(ensure); + // If has in reply to note if (data.reply) { // Fetch watchers nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm)); // この投稿をWatchする - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, data.reply); + if (Users.isLocalUser(user) && profile.autoWatch) { + watch(user.id, data.reply); } // 通知 - if (isLocalUser(data.reply._user)) { + if (data.reply.userHost === null) { nm.push(data.reply.userId, 'reply'); publishMainStream(data.reply.userId, 'reply', noteObj); } @@ -323,7 +277,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const type = data.text ? 'quote' : 'renote'; // Notify - if (isLocalUser(data.renote._user)) { + if (data.renote.userHost === null) { nm.push(data.renote.userId, type); } @@ -331,18 +285,18 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type)); // この投稿をWatchする - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, data.renote); + if (Users.isLocalUser(user) && profile.autoWatch) { + watch(user.id, data.renote); } // Publish event - if (!user._id.equals(data.renote.userId) && isLocalUser(data.renote._user)) { + if ((user.id !== data.renote.userId) && data.renote.userHost === null) { publishMainStream(data.renote.userId, 'renote', noteObj); } } if (!silent) { - publish(user, note, noteObj, data.reply, data.renote, data.visibleUsers, noteActivity); + publish(user, note, data.reply, data.renote, noteActivity); } Promise.all(nmRelatedPromises).then(() => { @@ -353,245 +307,178 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< index(note); }); -async function renderNoteOrRenoteActivity(data: Option, note: INote) { +async function renderNoteOrRenoteActivity(data: Option, note: Note) { if (data.localOnly) return null; const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0) - ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note) + ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote.id}`, note) : renderCreate(await renderNote(note, false), note); return renderActivity(content); } -function incRenoteCount(renote: INote) { - Note.update({ _id: renote._id }, { - $inc: { - renoteCount: 1, - score: 1 - } - }); +function incRenoteCount(renote: Note) { + Notes.increment({ id: renote.id }, 'renoteCount', 1); + Notes.increment({ id: renote.id }, 'score', 1); } -async function publish(user: IUser, note: INote, noteObj: any, reply: INote, renote: INote, visibleUsers: IUser[], noteActivity: any) { - if (isLocalUser(user)) { +async function publish(user: User, note: Note, reply: Note | null | undefined, renote: Note | null | undefined, noteActivity: any) { + if (Users.isLocalUser(user)) { // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 - if (reply && isRemoteUser(reply._user)) { - deliver(user, noteActivity, reply._user.inbox); + if (reply && reply.userHost !== null) { + Users.findOne(reply.userId).then(ensure).then(u => { + deliver(user, noteActivity, u.inbox); + }); } // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 - if (renote && isRemoteUser(renote._user)) { - deliver(user, noteActivity, renote._user.inbox); - } - - if (['followers', 'specified'].includes(note.visibility)) { - const detailPackedNote = await pack(note, user, { - detail: true + if (renote && renote.userHost !== null) { + Users.findOne(renote.userId).then(ensure).then(u => { + deliver(user, noteActivity, u.inbox); }); - // Publish event to myself's stream - publishHomeTimelineStream(note.userId, detailPackedNote); - publishHybridTimelineStream(note.userId, detailPackedNote); - - if (note.visibility == 'specified') { - for (const u of visibleUsers) { - if (!u._id.equals(user._id)) { - publishHomeTimelineStream(u._id, detailPackedNote); - publishHybridTimelineStream(u._id, detailPackedNote); - } - } - } - } else { - // Publish event to myself's stream - publishHomeTimelineStream(note.userId, noteObj); - - // Publish note to local and hybrid timeline stream - if (note.visibility != 'home') { - publishLocalTimelineStream(noteObj); - } - - if (note.visibility == 'public') { - publishHybridTimelineStream(null, noteObj); - } else { - // Publish event to myself's stream - publishHybridTimelineStream(note.userId, noteObj); - } } } - // Publish note to global timeline stream - if (note.visibility == 'public' && note.replyId == null) { - publishGlobalTimelineStream(noteObj); - } - if (['public', 'home', 'followers'].includes(note.visibility)) { // フォロワーに配信 publishToFollowers(note, user, noteActivity); } - - // リストに配信 - publishToUserLists(note, noteObj); } -async function insertNote(user: IUser, data: Option, tags: string[], emojis: string[], mentionedUsers: IUser[]) { - const insert: any = { - createdAt: data.createdAt, - fileIds: data.files ? data.files.map(file => file._id) : [], - replyId: data.reply ? data.reply._id : null, - renoteId: data.renote ? data.renote._id : null, +async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) { + const insert = new Note({ + id: genId(data.createdAt!), + createdAt: data.createdAt!, + fileIds: data.files ? data.files.map(file => file.id) : [], + replyId: data.reply ? data.reply.id : null, + renoteId: data.renote ? data.renote.id : null, name: data.name, text: data.text, - poll: data.poll, + hasPoll: data.poll != null, cw: data.cw == null ? null : data.cw, - tags, - tagsLower: tags.map(tag => tag.toLowerCase()), + tags: tags.map(tag => tag.toLowerCase()), emojis, - userId: user._id, - viaMobile: data.viaMobile, - localOnly: data.localOnly, + userId: user.id, + viaMobile: data.viaMobile!, + localOnly: data.localOnly!, geo: data.geo || null, - appId: data.app ? data.app._id : null, - visibility: data.visibility, + appId: data.app ? data.app.id : null, + visibility: data.visibility as any, visibleUserIds: data.visibility == 'specified' ? data.visibleUsers - ? data.visibleUsers.map(u => u._id) + ? data.visibleUsers.map(u => u.id) : [] : [], + attachedFileTypes: data.files ? data.files.map(file => file.type) : [], + // 以下非正規化データ - _reply: data.reply ? { - userId: data.reply.userId, - user: { - host: data.reply._user.host - } - } : null, - _renote: data.renote ? { - userId: data.renote.userId, - user: { - host: data.renote._user.host - } - } : null, - _user: { - host: user.host, - inbox: isRemoteUser(user) ? user.inbox : undefined - }, - _files: data.files ? data.files : [] - }; + replyUserId: data.reply ? data.reply.userId : null, + replyUserHost: data.reply ? data.reply.userHost : null, + renoteUserId: data.renote ? data.renote.userId : null, + renoteUserHost: data.renote ? data.renote.userHost : null, + userHost: user.host, + }); if (data.uri != null) insert.uri = data.uri; // Append mentions data if (mentionedUsers.length > 0) { - insert.mentions = mentionedUsers.map(u => u._id); - insert.mentionedRemoteUsers = mentionedUsers.filter(u => isRemoteUser(u)).map(u => ({ + insert.mentions = mentionedUsers.map(u => u.id); + insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => ({ uri: (u as IRemoteUser).uri, username: u.username, host: u.host - })); + }))); } // 投稿を作成 try { - return await Note.insert(insert); + let note: Note; + if (insert.hasPoll) { + // Start transaction + await getConnection().transaction(async transactionalEntityManager => { + note = await transactionalEntityManager.save(insert); + + const poll = new Poll({ + noteId: note.id, + choices: data.poll!.choices, + expiresAt: data.poll!.expiresAt, + multiple: data.poll!.multiple, + votes: new Array(data.poll!.choices.length).fill(0), + noteVisibility: note.visibility, + userId: user.id, + userHost: user.host + }); + + await transactionalEntityManager.save(poll); + }); + } else { + note = await Notes.save(insert); + } + + return note!; } catch (e) { // duplicate key error - if (e.code === 11000) { - return null; + if (isDuplicateKeyValueError(e)) { + const err = new Error('Duplicated note'); + err.name = 'duplicated'; + throw err; } - throw 'something happened'; + console.error(e); + + throw new Error('something happened'); } } -function index(note: INote) { +function index(note: Note) { if (note.text == null || config.elasticsearch == null) return; - es.index({ + es!.index({ index: 'misskey', type: 'note', - id: note._id.toString(), + id: note.id.toString(), body: { text: note.text } }); } -async function notifyToWatchersOfRenotee(renote: INote, user: IUser, nm: NotificationManager, type: NotificationType) { - const watchers = await NoteWatching.find({ - noteId: renote._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }); +async function notifyToWatchersOfRenotee(renote: Note, user: User, nm: NotificationManager, type: NotificationType) { + const watchers = await NoteWatchings.find({ + noteId: renote.id, + userId: Not(user.id) + }); for (const watcher of watchers) { nm.push(watcher.userId, type); } } -async function notifyToWatchersOfReplyee(reply: INote, user: IUser, nm: NotificationManager) { - const watchers = await NoteWatching.find({ - noteId: reply._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }); +async function notifyToWatchersOfReplyee(reply: Note, user: User, nm: NotificationManager) { + const watchers = await NoteWatchings.find({ + noteId: reply.id, + userId: Not(user.id) + }); for (const watcher of watchers) { nm.push(watcher.userId, 'reply'); } } -async function publishToUserLists(note: INote, noteObj: any) { - const lists = await UserList.find({ - userIds: note.userId - }); - - for (const list of lists) { - if (note.visibility == 'specified') { - if (note.visibleUserIds.some(id => id.equals(list.userId))) { - publishUserListStream(list._id, 'note', noteObj); - } - } else { - publishUserListStream(list._id, 'note', noteObj); - } - } -} - -async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { - const detailPackedNote = await pack(note, null, { - detail: true, - skipHide: true - }); - - const followers = await Following.find({ - followeeId: note.userId, - followerId: { $ne: note.userId } // バグでフォロワーに自分がいることがあるため +async function publishToFollowers(note: Note, user: User, noteActivity: any) { + const followers = await Followings.find({ + followeeId: note.userId }); const queue: string[] = []; for (const following of followers) { - const follower = following._follower; - - if (isLocalUser(follower)) { - // この投稿が返信ならスキップ - if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) - continue; - - // Publish event to followers stream - publishHomeTimelineStream(following.followerId, detailPackedNote); - - if (isRemoteUser(user) || note.visibility != 'public') { - publishHybridTimelineStream(following.followerId, detailPackedNote); - } - } else { + if (Followings.isRemoteFollower(following)) { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 - if (isLocalUser(user)) { - const inbox = follower.sharedInbox || follower.inbox; + if (Users.isLocalUser(user)) { + const inbox = following.followerSharedInbox || following.followerInbox; if (!queue.includes(inbox)) queue.push(inbox); } } @@ -600,104 +487,50 @@ async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { for (const inbox of queue) { deliver(user as any, noteActivity, inbox); } - - // 後方互換製のため、Questionは時間差でNoteでも送る - // Questionに対応してないインスタンスは、2つめのNoteだけを採用する - // Questionに対応しているインスタンスは、同IDで採番されている2つめのNoteを無視する - setTimeout(() => { - if (noteActivity.object.type === 'Question') { - const asNote = deepcopy(noteActivity); - - asNote.object.type = 'Note'; - asNote.object.content = asNote.object._misskey_fallback_content; - - for (const inbox of queue) { - deliver(user as any, asNote, inbox); - } - } - }, 10 * 1000); } -function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocalUser, noteActivity: any) { - for (const u of mentionedUsers.filter(u => isRemoteUser(u))) { +function deliverNoteToMentionedRemoteUsers(mentionedUsers: User[], user: ILocalUser, noteActivity: any) { + for (const u of mentionedUsers.filter(u => Users.isRemoteUser(u))) { deliver(user, noteActivity, (u as IRemoteUser).inbox); } } -async function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) { - for (const u of mentionedUsers.filter(u => isLocalUser(u))) { - const detailPackedNote = await pack(note, u, { +async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) { + for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { + const detailPackedNote = await Notes.pack(note, u, { detail: true }); - publishMainStream(u._id, 'mention', detailPackedNote); + publishMainStream(u.id, 'mention', detailPackedNote); // Create notification - nm.push(u._id, 'mention'); + nm.push(u.id, 'mention'); } } -function saveQuote(renote: INote, note: INote) { - Note.update({ _id: renote._id }, { - $push: { - _quoteIds: note._id - } - }); +function saveReply(reply: Note, note: Note) { + Notes.increment({ id: reply.id }, 'repliesCount', 1); } -function saveReply(reply: INote, note: INote) { - Note.update({ _id: reply._id }, { - $inc: { - repliesCount: 1 - } +function incNotesCountOfUser(user: User) { + Users.increment({ id: user.id }, 'notesCount', 1); + Users.update({ id: user.id }, { + updatedAt: new Date() }); } -function incNotesCountOfUser(user: IUser) { - User.update({ _id: user._id }, { - $set: { - updatedAt: new Date() - }, - $inc: { - notesCount: 1 - } - }); -} - -function incNotesCount(user: IUser) { - if (isLocalUser(user)) { - Meta.update({}, { - $inc: { - 'stats.notesCount': 1, - 'stats.originalNotesCount': 1 - } - }, { upsert: true }); - } else { - Meta.update({}, { - $inc: { - 'stats.notesCount': 1 - } - }, { upsert: true }); - } -} - -async function extractMentionedUsers(user: IUser, tokens: ReturnType<typeof parse>): Promise<IUser[]> { +async function extractMentionedUsers(user: User, tokens: ReturnType<typeof parse>): Promise<User[]> { if (tokens == null) return []; const mentions = extractMentions(tokens); - let mentionedUsers = - erase(null, await Promise.all(mentions.map(async m => { - try { - return await resolveUser(m.username, m.host ? m.host : user.host); - } catch (e) { - return null; - } - }))); + let mentionedUsers = (await Promise.all(mentions.map(m => + resolveUser(m.username, m.host || user.host).catch(() => null) + ))).filter(x => x != null) as User[]; // Drop duplicate users mentionedUsers = mentionedUsers.filter((u, i, self) => - i === self.findIndex(u2 => u._id.equals(u2._id)) + i === self.findIndex(u2 => u.id === u2.id) ); return mentionedUsers; diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index d71c97b2ca..c03c742ee1 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -1,99 +1,50 @@ -import Note, { INote } from '../../models/note'; -import { IUser, isLocalUser, isRemoteUser } from '../../models/user'; import { publishNoteStream } from '../stream'; import renderDelete from '../../remote/activitypub/renderer/delete'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; -import Following from '../../models/following'; import renderTombstone from '../../remote/activitypub/renderer/tombstone'; -import notesChart from '../../services/chart/notes'; -import perUserNotesChart from '../../services/chart/per-user-notes'; import config from '../../config'; -import NoteUnread from '../../models/note-unread'; -import read from './read'; -import DriveFile from '../../models/drive-file'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; -import Instance from '../../models/instance'; -import instanceChart from '../../services/chart/instance'; -import Favorite from '../../models/favorite'; +import { User } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { Notes, Users, Followings, Instances } from '../../models'; +import { Not } from 'typeorm'; +import { notesChart, perUserNotesChart, instanceChart } from '../chart'; /** * 投稿を削除します。 * @param user 投稿者 * @param note 投稿 */ -export default async function(user: IUser, note: INote, quiet = false) { +export default async function(user: User, note: Note, quiet = false) { const deletedAt = new Date(); - await Note.update({ - _id: note._id, - userId: user._id - }, { - $set: { - deletedAt: deletedAt, - text: null, - tags: [], - fileIds: [], - renoteId: null, - poll: null, - geo: null, - cw: null - } + await Notes.delete({ + id: note.id, + userId: user.id }); if (note.renoteId) { - Note.update({ _id: note.renoteId }, { - $inc: { - renoteCount: -1, - score: -1 - }, - $pull: { - _quoteIds: note._id - } - }); - } - - // この投稿が関わる未読通知を削除 - NoteUnread.find({ - noteId: note._id - }).then(unreads => { - for (const unread of unreads) { - read(unread.userId, unread.noteId); - } - }); - - // この投稿をお気に入りから削除 - Favorite.remove({ - noteId: note._id - }); - - // ファイルが添付されていた場合ドライブのファイルの「このファイルが添付された投稿一覧」プロパティからこの投稿を削除 - if (note.fileIds) { - for (const fileId of note.fileIds) { - DriveFile.update({ _id: fileId }, { - $pull: { - 'metadata.attachedNoteIds': note._id - } - }); - } + Notes.decrement({ id: note.renoteId }, 'renoteCount', 1); + Notes.decrement({ id: note.renoteId }, 'score', 1); } if (!quiet) { - publishNoteStream(note._id, 'deleted', { + publishNoteStream(note.id, 'deleted', { deletedAt: deletedAt }); //#region ローカルの投稿なら削除アクティビティを配送 - if (isLocalUser(user)) { - const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user)); + if (Users.isLocalUser(user)) { + const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note.id}`), user)); - const followings = await Following.find({ - followeeId: user._id, - '_follower.host': { $ne: null } + const followings = await Followings.find({ + followeeId: user.id, + followerHost: Not(null) }); for (const following of followings) { - deliver(user, content, following._follower.inbox); + deliver(user, content, following.followerInbox); } } //#endregion @@ -102,15 +53,10 @@ export default async function(user: IUser, note: INote, quiet = false) { notesChart.update(note, false); perUserNotesChart.update(user, note, false); - if (isRemoteUser(user)) { + if (Users.isRemoteUser(user)) { registerOrFetchInstanceDoc(user.host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - notesCount: -1 - } - }); - - instanceChart.updateNote(i.host, false); + Instances.decrement({ id: i.id }, 'notesCount', 1); + instanceChart.updateNote(i.host, note, false); }); } } diff --git a/src/services/note/polls/update.ts b/src/services/note/polls/update.ts index d4e183889d..f979ef2f0a 100644 --- a/src/services/note/polls/update.ts +++ b/src/services/note/polls/update.ts @@ -1,52 +1,28 @@ -import * as mongo from 'mongodb'; -import Note, { INote } from '../../../models/note'; -import { updateQuestion } from '../../../remote/activitypub/models/question'; -import ms = require('ms'); -import Logger from '../../logger'; -import User, { isLocalUser, isRemoteUser } from '../../../models/user'; -import Following from '../../../models/following'; import renderUpdate from '../../../remote/activitypub/renderer/update'; import { renderActivity } from '../../../remote/activitypub/renderer'; import { deliver } from '../../../queue'; import renderNote from '../../../remote/activitypub/renderer/note'; +import { Users, Notes, Followings } from '../../../models'; +import { Note } from '../../../models/entities/note'; -const logger = new Logger('pollsUpdate'); +export async function deliverQuestionUpdate(noteId: Note['id']) { + const note = await Notes.findOne(noteId); + if (note == null) throw new Error('note not found'); -export async function triggerUpdate(note: INote) { - if (!note.updatedAt || Date.now() - new Date(note.updatedAt).getTime() > ms('1min')) { - logger.info(`Updating ${note._id}`); + const user = await Users.findOne(note.userId); + if (user == null) throw new Error('note not found'); - try { - const updated = await updateQuestion(note.uri); - logger.info(`Updated ${note._id} ${updated ? 'changed' : 'nochange'}`); - } catch (e) { - logger.error(e); - } - } -} - -export async function deliverQuestionUpdate(noteId: mongo.ObjectID) { - const note = await Note.findOne({ - _id: noteId, - }); - - const user = await User.findOne({ - _id: note.userId - }); - - const followers = await Following.find({ - followeeId: user._id + const followers = await Followings.find({ + followeeId: user.id }); const queue: string[] = []; // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 - if (isLocalUser(user)) { + if (Users.isLocalUser(user)) { for (const following of followers) { - const follower = following._follower; - - if (isRemoteUser(follower)) { - const inbox = follower.sharedInbox || follower.inbox; + if (Followings.isRemoteFollower(following)) { + const inbox = following.followerSharedInbox || following.followerInbox; if (!queue.includes(inbox)) queue.push(inbox); } } diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts index a23cdc1cb4..c6876484f5 100644 --- a/src/services/note/polls/vote.ts +++ b/src/services/note/polls/vote.ts @@ -1,79 +1,76 @@ -import Vote from '../../../models/poll-vote'; -import Note, { INote } from '../../../models/note'; -import Watching from '../../../models/note-watching'; import watch from '../../../services/note/watch'; import { publishNoteStream } from '../../stream'; -import notify from '../../../services/create-notification'; -import { isLocalUser, IUser } from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { PollVotes, Users, NoteWatchings, Polls, UserProfiles } from '../../../models'; +import { Not } from 'typeorm'; +import { genId } from '../../../misc/gen-id'; +import { createNotification } from '../../create-notification'; -export default (user: IUser, note: INote, choice: number) => new Promise(async (res, rej) => { - if (!note.poll.choices.some(x => x.id == choice)) return rej('invalid choice param'); +export default async function(user: User, note: Note, choice: number) { + const poll = await Polls.findOne(note.id); + + if (poll == null) throw new Error('poll not found'); + + // Check whether is valid choice + if (poll.choices[choice] == null) throw new Error('invalid choice param'); // if already voted - const exist = await Vote.find({ - noteId: note._id, - userId: user._id + const exist = await PollVotes.find({ + noteId: note.id, + userId: user.id }); - if (note.poll.multiple) { - if (exist.some(x => x.choice === choice)) - return rej('already voted'); - } else if (exist.length) { - return rej('already voted'); + if (poll.multiple) { + if (exist.some(x => x.choice === choice)) { + throw new Error('already voted'); + } + } else if (exist.length !== 0) { + throw new Error('already voted'); } // Create vote - await Vote.insert({ + await PollVotes.save({ + id: genId(), createdAt: new Date(), - noteId: note._id, - userId: user._id, + noteId: note.id, + userId: user.id, choice: choice }); - res(); - - const inc: any = {}; - inc[`poll.choices.${note.poll.choices.findIndex(c => c.id == choice)}.votes`] = 1; - // Increment votes count - await Note.update({ _id: note._id }, { - $inc: inc - }); + const index = choice + 1; // In SQL, array index is 1 based + await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`); - publishNoteStream(note._id, 'pollVoted', { + publishNoteStream(note.id, 'pollVoted', { choice: choice, - userId: user._id.toHexString() + userId: user.id }); // Notify - notify(note.userId, user._id, 'poll_vote', { - noteId: note._id, + createNotification(note.userId, user.id, 'pollVote', { + noteId: note.id, choice: choice }); // Fetch watchers - Watching - .find({ - noteId: note._id, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - for (const watcher of watchers) { - notify(watcher.userId, user._id, 'poll_vote', { - noteId: note._id, - choice: choice - }); - } - }); + NoteWatchings.find({ + noteId: note.id, + userId: Not(user.id), + }) + .then(watchers => { + for (const watcher of watchers) { + createNotification(watcher.userId, user.id, 'pollVote', { + noteId: note.id, + choice: choice + }); + } + }); + + const profile = await UserProfiles.findOne({ userId: user.id }); // ローカルユーザーが投票した場合この投稿をWatchする - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, note); + if (Users.isLocalUser(user) && profile!.autoWatch) { + watch(user.id, note); } -}); +} diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 4fdaf92ac6..0e8bcebdbc 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -1,21 +1,24 @@ -import { IUser, isLocalUser, isRemoteUser } from '../../../models/user'; -import Note, { INote } from '../../../models/note'; -import NoteReaction from '../../../models/note-reaction'; import { publishNoteStream } from '../../stream'; -import notify from '../../create-notification'; -import NoteWatching from '../../../models/note-watching'; import watch from '../watch'; import renderLike from '../../../remote/activitypub/renderer/like'; import { deliver } from '../../../queue'; import { renderActivity } from '../../../remote/activitypub/renderer'; -import perUserReactionsChart from '../../../services/chart/per-user-reactions'; import { IdentifiableError } from '../../../misc/identifiable-error'; import { toDbReaction } from '../../../misc/reaction-lib'; import fetchMeta from '../../../misc/fetch-meta'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { NoteReactions, Users, NoteWatchings, Notes, UserProfiles } from '../../../models'; +import { Not } from 'typeorm'; +import { perUserReactionsChart } from '../../chart'; +import { genId } from '../../../misc/gen-id'; +import { NoteReaction } from '../../../models/entities/note-reaction'; +import { createNotification } from '../../create-notification'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; -export default async (user: IUser, note: INote, reaction: string) => { +export default async (user: User, note: Note, reaction?: string) => { // Myself - if (note.userId.equals(user._id)) { + if (note.userId === user.id) { throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note'); } @@ -23,14 +26,15 @@ export default async (user: IUser, note: INote, reaction: string) => { reaction = await toDbReaction(reaction, meta.enableEmojiReaction); // Create reaction - await NoteReaction.insert({ + await NoteReactions.save({ + id: genId(), createdAt: new Date(), - noteId: note._id, - userId: user._id, + noteId: note.id, + userId: user.id, reaction - }).catch(e => { + } as NoteReaction).catch(e => { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298', 'already reacted'); } @@ -38,59 +42,58 @@ export default async (user: IUser, note: INote, reaction: string) => { }); // Increment reactions count - await Note.update({ _id: note._id }, { - $inc: { - [`reactionCounts.${reaction}`]: 1, - score: 1 - } - }); + const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; + await Notes.createQueryBuilder().update() + .set({ + reactions: () => sql, + }) + .where('id = :id', { id: note.id }) + .execute(); + + Notes.increment({ id: note.id }, 'score', 1); perUserReactionsChart.update(user, note); - publishNoteStream(note._id, 'reacted', { + publishNoteStream(note.id, 'reacted', { reaction: reaction, - userId: user._id + userId: user.id }); // リアクションされたユーザーがローカルユーザーなら通知を作成 - if (isLocalUser(note._user)) { - notify(note.userId, user._id, 'reaction', { - noteId: note._id, + if (note.userHost === null) { + createNotification(note.userId, user.id, 'reaction', { + noteId: note.id, reaction: reaction }); } // Fetch watchers - NoteWatching - .find({ - noteId: note._id, - userId: { $ne: user._id } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - for (const watcher of watchers) { - notify(watcher.userId, user._id, 'reaction', { - noteId: note._id, - reaction: reaction - }); - } - }); + NoteWatchings.find({ + noteId: note.id, + userId: Not(user.id) + }).then(watchers => { + for (const watcher of watchers) { + createNotification(watcher.userId, user.id, 'reaction', { + noteId: note.id, + reaction: reaction + }); + } + }); + + const profile = await UserProfiles.findOne({ userId: user.id }); // ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする - if (isLocalUser(user) && user.settings.autoWatch !== false) { - watch(user._id, note); + if (Users.isLocalUser(user) && profile!.autoWatch) { + watch(user.id, note); } //#region 配信 // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 - if (isLocalUser(user) && isRemoteUser(note._user)) { + if (Users.isLocalUser(user) && note.userHost !== null) { const content = renderActivity(renderLike(user, note, reaction)); - deliver(user, content, note._user.inbox); + Users.findOne(note.userId).then(u => { + deliver(user, content, u!.inbox); + }); } //#endregion - - return; }; diff --git a/src/services/note/reaction/delete.ts b/src/services/note/reaction/delete.ts index 695534db61..6e9611ca5a 100644 --- a/src/services/note/reaction/delete.ts +++ b/src/services/note/reaction/delete.ts @@ -1,50 +1,50 @@ -import { IUser, isLocalUser, isRemoteUser } from '../../../models/user'; -import Note, { INote } from '../../../models/note'; -import NoteReaction from '../../../models/note-reaction'; import { publishNoteStream } from '../../stream'; import renderLike from '../../../remote/activitypub/renderer/like'; import renderUndo from '../../../remote/activitypub/renderer/undo'; import { renderActivity } from '../../../remote/activitypub/renderer'; import { deliver } from '../../../queue'; import { IdentifiableError } from '../../../misc/identifiable-error'; +import { User } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { NoteReactions, Users, Notes } from '../../../models'; -export default async (user: IUser, note: INote) => { +export default async (user: User, note: Note) => { // if already unreacted - const exist = await NoteReaction.findOne({ - noteId: note._id, - userId: user._id, - deletedAt: { $exists: false } + const exist = await NoteReactions.findOne({ + noteId: note.id, + userId: user.id, }); - if (exist === null) { + if (exist == null) { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } // Delete reaction - await NoteReaction.remove({ - _id: exist._id - }); - - const dec: any = {}; - dec[`reactionCounts.${exist.reaction}`] = -1; + await NoteReactions.delete(exist.id); // Decrement reactions count - Note.update({ _id: note._id }, { - $inc: dec - }); + const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; + await Notes.createQueryBuilder().update() + .set({ + reactions: () => sql, + }) + .where('id = :id', { id: note.id }) + .execute(); - publishNoteStream(note._id, 'unreacted', { + Notes.decrement({ id: note.id }, 'score', 1); + + publishNoteStream(note.id, 'unreacted', { reaction: exist.reaction, - userId: user._id + userId: user.id }); //#region 配信 // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 - if (isLocalUser(user) && isRemoteUser(note._user)) { + if (Users.isLocalUser(user) && (note.userHost !== null)) { const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user)); - deliver(user, content, note._user.inbox); + Users.findOne(note.userId).then(u => { + deliver(user, content, u!.inbox); + }); } //#endregion - - return; }; diff --git a/src/services/note/read.ts b/src/services/note/read.ts index 8b52445cf0..44d75bd850 100644 --- a/src/services/note/read.ts +++ b/src/services/note/read.ts @@ -1,59 +1,35 @@ -import * as mongo from 'mongodb'; -import isObjectId from '../../misc/is-objectid'; import { publishMainStream } from '../stream'; -import User from '../../models/user'; -import NoteUnread from '../../models/note-unread'; +import { Note } from '../../models/entities/note'; +import { User } from '../../models/entities/user'; +import { NoteUnreads } from '../../models'; /** * Mark a note as read */ export default ( - user: string | mongo.ObjectID, - note: string | mongo.ObjectID + userId: User['id'], + noteId: Note['id'] ) => new Promise<any>(async (resolve, reject) => { - - const userId: mongo.ObjectID = isObjectId(user) - ? user as mongo.ObjectID - : new mongo.ObjectID(user); - - const noteId: mongo.ObjectID = isObjectId(note) - ? note as mongo.ObjectID - : new mongo.ObjectID(note); - // Remove document - const res = await NoteUnread.remove({ + const res = await NoteUnreads.delete({ userId: userId, noteId: noteId }); - if (res.deletedCount == 0) { + // v11 TODO: https://github.com/typeorm/typeorm/issues/2415 + if (res.affected == 0) { return; } - const count1 = await NoteUnread - .count({ - userId: userId, - isSpecified: false - }, { - limit: 1 - }); - - const count2 = await NoteUnread - .count({ - userId: userId, - isSpecified: true - }, { - limit: 1 - }); + const count1 = await NoteUnreads.count({ + userId: userId, + isSpecified: false + }); - if (count1 == 0 || count2 == 0) { - User.update({ _id: userId }, { - $set: { - hasUnreadMentions: count1 != 0 || count2 != 0, - hasUnreadSpecifiedNotes: count2 != 0 - } - }); - } + const count2 = await NoteUnreads.count({ + userId: userId, + isSpecified: true + }); if (count1 == 0) { // 全て既読になったイベントを発行 diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts index e70c63c765..203cff8d39 100644 --- a/src/services/note/unread.ts +++ b/src/services/note/unread.ts @@ -1,47 +1,34 @@ -import NoteUnread from '../../models/note-unread'; -import User, { IUser } from '../../models/user'; -import { INote } from '../../models/note'; -import Mute from '../../models/mute'; +import { Note } from '../../models/entities/note'; import { publishMainStream } from '../stream'; +import { User } from '../../models/entities/user'; +import { Mutings, NoteUnreads } from '../../models'; +import { genId } from '../../misc/gen-id'; -export default async function(user: IUser, note: INote, isSpecified = false) { +export default async function(user: User, note: Note, isSpecified = false) { //#region ミュートしているなら無視 - const mute = await Mute.find({ - muterId: user._id + const mute = await Mutings.find({ + muterId: user.id }); - const mutedUserIds = mute.map(m => m.muteeId.toString()); - if (mutedUserIds.includes(note.userId.toString())) return; + if (mute.map(m => m.muteeId).includes(note.userId)) return; //#endregion - const unread = await NoteUnread.insert({ - noteId: note._id, - userId: user._id, + const unread = await NoteUnreads.save({ + id: genId(), + noteId: note.id, + userId: user.id, isSpecified, - _note: { - userId: note.userId - } + noteUserId: note.userId }); // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する setTimeout(async () => { - const exist = await NoteUnread.findOne({ _id: unread._id }); + const exist = await NoteUnreads.findOne(unread.id); if (exist == null) return; - User.update({ - _id: user._id - }, { - $set: isSpecified ? { - hasUnreadSpecifiedNotes: true, - hasUnreadMentions: true - } : { - hasUnreadMentions: true - } - }); - - publishMainStream(user._id, 'unreadMention', note._id); + publishMainStream(user.id, 'unreadMention', note.id); if (isSpecified) { - publishMainStream(user._id, 'unreadSpecifiedNote', note._id); + publishMainStream(user.id, 'unreadSpecifiedNote', note.id); } }, 2000); } diff --git a/src/services/note/unwatch.ts b/src/services/note/unwatch.ts index ef5783231b..047ac343be 100644 --- a/src/services/note/unwatch.ts +++ b/src/services/note/unwatch.ts @@ -1,9 +1,10 @@ -import * as mongodb from 'mongodb'; -import Watching from '../../models/note-watching'; +import { User } from '../../models/entities/user'; +import { NoteWatchings } from '../../models'; +import { Note } from '../../models/entities/note'; -export default async (me: mongodb.ObjectID, note: object) => { - await Watching.remove({ - noteId: (note as any)._id, +export default async (me: User['id'], note: Note) => { + await NoteWatchings.delete({ + noteId: note.id, userId: me }); }; diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts index aad53610d8..d3c9553696 100644 --- a/src/services/note/watch.ts +++ b/src/services/note/watch.ts @@ -1,25 +1,20 @@ -import * as mongodb from 'mongodb'; -import Watching from '../../models/note-watching'; +import { User } from '../../models/entities/user'; +import { Note } from '../../models/entities/note'; +import { NoteWatchings } from '../../models'; +import { genId } from '../../misc/gen-id'; +import { NoteWatching } from '../../models/entities/note-watching'; -export default async (me: mongodb.ObjectID, note: object) => { +export default async (me: User['id'], note: Note) => { // 自分の投稿はwatchできない - if (me.equals((note as any).userId)) { + if (me === note.userId) { return; } - // if watching now - const exist = await Watching.findOne({ - noteId: (note as any)._id, - userId: me - }); - - if (exist !== null) { - return; - } - - await Watching.insert({ + await NoteWatchings.save({ + id: genId(), createdAt: new Date(), - noteId: (note as any)._id, - userId: me - }); + noteId: note.id, + userId: me, + noteUserId: note.userId + } as NoteWatching); }; diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts index ceb762b2fa..1830cad623 100644 --- a/src/services/push-notification.ts +++ b/src/services/push-notification.ts @@ -1,34 +1,20 @@ import * as push from 'web-push'; -import * as mongo from 'mongodb'; -import Subscription from '../models/sw-subscription'; import config from '../config'; +import { SwSubscriptions } from '../models'; import fetchMeta from '../misc/fetch-meta'; -import { IMeta } from '../models/meta'; -let meta: IMeta = null; +export default async function(userId: string, type: string, body?: any) { + const meta = await fetchMeta(); -setInterval(() => { - fetchMeta().then(m => { - meta = m; + if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; - if (meta.enableServiceWorker) { - // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 - push.setVapidDetails(config.url, - meta.swPublicKey, - meta.swPrivateKey); - } - }); -}, 3000); - -export default async function(userId: mongo.ObjectID | string, type: string, body?: any) { - if (!meta.enableServiceWorker) return; - - if (typeof userId === 'string') { - userId = new mongo.ObjectID(userId); - } + // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 + push.setVapidDetails(config.url, + meta.swPublicKey, + meta.swPrivateKey); // Fetch - const subscriptions = await Subscription.find({ + const subscriptions = await SwSubscriptions.find({ userId: userId }); @@ -49,7 +35,7 @@ export default async function(userId: mongo.ObjectID | string, type: string, bod //swLogger.info(err.body); if (err.statusCode == 410) { - Subscription.remove({ + SwSubscriptions.delete({ userId: userId, endpoint: subscription.endpoint, auth: subscription.auth, diff --git a/src/services/register-or-fetch-instance-doc.ts b/src/services/register-or-fetch-instance-doc.ts index d418cd12ce..9957edd3db 100644 --- a/src/services/register-or-fetch-instance-doc.ts +++ b/src/services/register-or-fetch-instance-doc.ts @@ -1,15 +1,20 @@ -import Instance, { IInstance } from '../models/instance'; -import federationChart from '../services/chart/federation'; +import { Instance } from '../models/entities/instance'; +import { Instances } from '../models'; +import { federationChart } from './chart'; +import { genId } from '../misc/gen-id'; +import { toPuny } from '../misc/convert-host'; -export async function registerOrFetchInstanceDoc(host: string): Promise<IInstance> { - if (host == null) return null; +export async function registerOrFetchInstanceDoc(host: string): Promise<Instance> { + host = toPuny(host); - const index = await Instance.findOne({ host }); + const index = await Instances.findOne({ host }); if (index == null) { - const i = await Instance.insert({ + const i = await Instances.save({ + id: genId(), host, caughtAt: new Date(), + lastCommunicatedAt: new Date(), system: null // TODO }); diff --git a/src/services/stream.ts b/src/services/stream.ts index 813c9eb7c0..28cb2057e2 100644 --- a/src/services/stream.ts +++ b/src/services/stream.ts @@ -1,94 +1,65 @@ -import * as mongo from 'mongodb'; import redis from '../db/redis'; -import Xev from 'xev'; - -type ID = string | mongo.ObjectID; +import { User } from '../models/entities/user'; +import { Note } from '../models/entities/note'; +import { UserList } from '../models/entities/user-list'; +import { ReversiGame } from '../models/entities/games/reversi/game'; class Publisher { - private ev: Xev; - - constructor() { - // Redisがインストールされてないときはプロセス間通信を使う - if (redis == null) { - this.ev = new Xev(); - } - } - - private publish = (channel: string, type: string, value?: any): void => { + private publish = (channel: string, type: string | null, value?: any): void => { const message = type == null ? value : value == null ? { type: type, body: null } : { type: type, body: value }; - if (this.ev) { - this.ev.emit(channel, message); - } else { - redis.publish('misskey', JSON.stringify({ - channel: channel, - message: message - })); - } + redis.publish('misskey', JSON.stringify({ + channel: channel, + message: message + })); } - public publishMainStream = (userId: ID, type: string, value?: any): void => { + public publishMainStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishDriveStream = (userId: ID, type: string, value?: any): void => { + public publishDriveStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishNoteStream = (noteId: ID, type: string, value: any): void => { + public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => { this.publish(`noteStream:${noteId}`, type, { id: noteId, body: value }); } - public publishUserListStream = (listId: ID, type: string, value?: any): void => { + public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingStream = (userId: ID, otherpartyId: ID, type: string, value?: any): void => { + public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => { this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingIndexStream = (userId: ID, type: string, value?: any): void => { + public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiStream = (userId: ID, type: string, value?: any): void => { + public publishReversiStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiGameStream = (gameId: ID, type: string, value?: any): void => { + public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => { this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value); } - public publishHomeTimelineStream = (userId: ID, note: any): void => { - this.publish(`homeTimeline:${userId}`, null, note); - } - - public publishLocalTimelineStream = async (note: any): Promise<void> => { - this.publish('localTimeline', null, note); - } - - public publishHybridTimelineStream = async (userId: ID, note: any): Promise<void> => { - this.publish(userId ? `hybridTimeline:${userId}` : 'hybridTimeline', null, note); - } - - public publishGlobalTimelineStream = (note: any): void => { - this.publish('globalTimeline', null, note); - } - - public publishHashtagStream = (note: any): void => { - this.publish('hashtag', null, note); + public publishNotesStream = (note: any): void => { + this.publish('notesStream', null, note); } public publishApLogStream = (log: any): void => { this.publish('apLog', null, log); } - public publishAdminStream = (userId: ID, type: string, value?: any): void => { + public publishAdminStream = (userId: User['id'], type: string, value?: any): void => { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } } @@ -100,15 +71,11 @@ export default publisher; export const publishMainStream = publisher.publishMainStream; export const publishDriveStream = publisher.publishDriveStream; export const publishNoteStream = publisher.publishNoteStream; +export const publishNotesStream = publisher.publishNotesStream; export const publishUserListStream = publisher.publishUserListStream; export const publishMessagingStream = publisher.publishMessagingStream; export const publishMessagingIndexStream = publisher.publishMessagingIndexStream; export const publishReversiStream = publisher.publishReversiStream; export const publishReversiGameStream = publisher.publishReversiGameStream; -export const publishHomeTimelineStream = publisher.publishHomeTimelineStream; -export const publishLocalTimelineStream = publisher.publishLocalTimelineStream; -export const publishHybridTimelineStream = publisher.publishHybridTimelineStream; -export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream; -export const publishHashtagStream = publisher.publishHashtagStream; export const publishApLogStream = publisher.publishApLogStream; export const publishAdminStream = publisher.publishAdminStream; diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts index 23c39312c0..8dbbf04cbb 100644 --- a/src/services/update-hashtag.ts +++ b/src/services/update-hashtag.ts @@ -1,103 +1,106 @@ -import { IUser, isLocalUser, isRemoteUser } from '../models/user'; -import Hashtag from '../models/hashtag'; -import hashtagChart from './chart/hashtag'; +import { User } from '../models/entities/user'; +import { Hashtags, Users } from '../models'; +import { hashtagChart } from './chart'; +import { genId } from '../misc/gen-id'; +import { Hashtag } from '../models/entities/hashtag'; -export async function updateHashtag(user: IUser, tag: string, isUserAttached = false, inc = true) { +export async function updateHashtag(user: User, tag: string, isUserAttached = false, inc = true) { tag = tag.toLowerCase(); - const index = await Hashtag.findOne({ tag }); + const index = await Hashtags.findOne({ name: tag }); if (index == null && !inc) return; if (index != null) { - const $push = {} as any; - const $pull = {} as any; - const $inc = {} as any; + const q = Hashtags.createQueryBuilder('tag').update() + .where('tag.name = :name', { name: tag }); + + const set = {} as any; if (isUserAttached) { if (inc) { // 自分が初めてこのタグを使ったなら - if (!index.attachedUserIds.some(id => id.equals(user._id))) { - $push.attachedUserIds = user._id; - $inc.attachedUsersCount = 1; + if (!index.attachedUserIds.some(id => id === user.id)) { + set.attachedUserIds = () => `array_append(tag.attachedUserIds, '${user.id}')`; + set.attachedUsersCount = () => `tag.attachedUsersCount + 1`; } // 自分が(ローカル内で)初めてこのタグを使ったなら - if (isLocalUser(user) && !index.attachedLocalUserIds.some(id => id.equals(user._id))) { - $push.attachedLocalUserIds = user._id; - $inc.attachedLocalUsersCount = 1; + if (Users.isLocalUser(user) && !index.attachedLocalUserIds.some(id => id === user.id)) { + set.attachedLocalUserIds = () => `array_append(tag.attachedLocalUserIds, '${user.id}')`; + set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount + 1`; } // 自分が(リモートで)初めてこのタグを使ったなら - if (isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id.equals(user._id))) { - $push.attachedRemoteUserIds = user._id; - $inc.attachedRemoteUsersCount = 1; + if (Users.isRemoteUser(user) && !index.attachedRemoteUserIds.some(id => id === user.id)) { + set.attachedRemoteUserIds = () => `array_append(tag.attachedRemoteUserIds, '${user.id}')`; + set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount + 1`; } } else { - $pull.attachedUserIds = user._id; - $inc.attachedUsersCount = -1; - if (isLocalUser(user)) { - $pull.attachedLocalUserIds = user._id; - $inc.attachedLocalUsersCount = -1; + set.attachedUserIds = () => `array_remove(tag.attachedUserIds, '${user.id}')`; + set.attachedUsersCount = () => `tag.attachedUsersCount - 1`; + if (Users.isLocalUser(user)) { + set.attachedLocalUserIds = () => `array_remove(tag.attachedLocalUserIds, '${user.id}')`; + set.attachedLocalUsersCount = () => `tag.attachedLocalUsersCount - 1`; } else { - $pull.attachedRemoteUserIds = user._id; - $inc.attachedRemoteUsersCount = -1; + set.attachedRemoteUserIds = () => `array_remove(tag.attachedRemoteUserIds, '${user.id}')`; + set.attachedRemoteUsersCount = () => `tag.attachedRemoteUsersCount - 1`; } } } else { // 自分が初めてこのタグを使ったなら - if (!index.mentionedUserIds.some(id => id.equals(user._id))) { - $push.mentionedUserIds = user._id; - $inc.mentionedUsersCount = 1; + if (!index.mentionedUserIds.some(id => id === user.id)) { + set.mentionedUserIds = () => `array_append(tag.mentionedUserIds, '${user.id}')`; + set.mentionedUsersCount = () => `tag.mentionedUsersCount + 1`; } // 自分が(ローカル内で)初めてこのタグを使ったなら - if (isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id.equals(user._id))) { - $push.mentionedLocalUserIds = user._id; - $inc.mentionedLocalUsersCount = 1; + if (Users.isLocalUser(user) && !index.mentionedLocalUserIds.some(id => id === user.id)) { + set.mentionedLocalUserIds = () => `array_append(tag.mentionedLocalUserIds, '${user.id}')`; + set.mentionedLocalUsersCount = () => `tag.mentionedLocalUsersCount + 1`; } // 自分が(リモートで)初めてこのタグを使ったなら - if (isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id.equals(user._id))) { - $push.mentionedRemoteUserIds = user._id; - $inc.mentionedRemoteUsersCount = 1; + if (Users.isRemoteUser(user) && !index.mentionedRemoteUserIds.some(id => id === user.id)) { + set.mentionedRemoteUserIds = () => `array_append(tag.mentionedRemoteUserIds, '${user.id}')`; + set.mentionedRemoteUsersCount = () => `tag.mentionedRemoteUsersCount + 1`; } } - const q = {} as any; - if (Object.keys($push).length > 0) q.$push = $push; - if (Object.keys($pull).length > 0) q.$pull = $pull; - if (Object.keys($inc).length > 0) q.$inc = $inc; - if (Object.keys(q).length > 0) Hashtag.update({ tag }, q); + if (Object.keys(set).length > 0) { + q.execute(); + } } else { if (isUserAttached) { - Hashtag.insert({ - tag, + Hashtags.save({ + id: genId(), + name: tag, mentionedUserIds: [], mentionedUsersCount: 0, mentionedLocalUserIds: [], mentionedLocalUsersCount: 0, mentionedRemoteUserIds: [], mentionedRemoteUsersCount: 0, - attachedUserIds: [user._id], + attachedUserIds: [user.id], attachedUsersCount: 1, - attachedLocalUserIds: isLocalUser(user) ? [user._id] : [], - attachedLocalUsersCount: isLocalUser(user) ? 1 : 0, - attachedRemoteUserIds: isRemoteUser(user) ? [user._id] : [], - attachedRemoteUsersCount: isRemoteUser(user) ? 1 : 0, - }); + attachedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], + attachedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, + attachedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], + attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, + } as Hashtag); } else { - Hashtag.insert({ - tag, - mentionedUserIds: [user._id], + Hashtags.save({ + id: genId(), + name: tag, + mentionedUserIds: [user.id], mentionedUsersCount: 1, - mentionedLocalUserIds: isLocalUser(user) ? [user._id] : [], - mentionedLocalUsersCount: isLocalUser(user) ? 1 : 0, - mentionedRemoteUserIds: isRemoteUser(user) ? [user._id] : [], - mentionedRemoteUsersCount: isRemoteUser(user) ? 1 : 0, + mentionedLocalUserIds: Users.isLocalUser(user) ? [user.id] : [], + mentionedLocalUsersCount: Users.isLocalUser(user) ? 1 : 0, + mentionedRemoteUserIds: Users.isRemoteUser(user) ? [user.id] : [], + mentionedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0, attachedUserIds: [], attachedUsersCount: 0, attachedLocalUserIds: [], attachedLocalUsersCount: 0, attachedRemoteUserIds: [], attachedRemoteUsersCount: 0, - }); + } as Hashtag); } } diff --git a/src/services/user-list/push.ts b/src/services/user-list/push.ts index 5ad4a14827..958d54b090 100644 --- a/src/services/user-list/push.ts +++ b/src/services/user-list/push.ts @@ -1,21 +1,26 @@ -import { pack as packUser, IUser, isRemoteUser, fetchProxyAccount } from '../../models/user'; -import UserList, { IUserList } from '../../models/user-list'; import { renderActivity } from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import renderFollow from '../../remote/activitypub/renderer/follow'; import { publishUserListStream } from '../stream'; +import { User } from '../../models/entities/user'; +import { UserList } from '../../models/entities/user-list'; +import { UserListJoinings, Users } from '../../models'; +import { UserListJoining } from '../../models/entities/user-list-joining'; +import { genId } from '../../misc/gen-id'; +import { fetchProxyAccount } from '../../misc/fetch-proxy-account'; -export async function pushUserToUserList(target: IUser, list: IUserList) { - await UserList.update({ _id: list._id }, { - $push: { - userIds: target._id - } - }); +export async function pushUserToUserList(target: User, list: UserList) { + await UserListJoinings.save({ + id: genId(), + createdAt: new Date(), + userId: target.id, + userListId: list.id + } as UserListJoining); - publishUserListStream(list._id, 'userAdded', await packUser(target)); + publishUserListStream(list.id, 'userAdded', await Users.pack(target)); // このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする - if (isRemoteUser(target)) { + if (Users.isRemoteUser(target)) { const proxy = await fetchProxyAccount(); const content = renderActivity(renderFollow(proxy, target)); deliver(proxy, content, target.inbox); diff --git a/src/tools/add-emoji.ts b/src/tools/add-emoji.ts index 2aa99e37ae..3745b48889 100644 --- a/src/tools/add-emoji.ts +++ b/src/tools/add-emoji.ts @@ -1,9 +1,11 @@ -import Emoji from '../models/emoji'; +import { Emojis } from '../models'; +import { genId } from '../misc/gen-id'; async function main(name: string, url: string, alias?: string): Promise<any> { const aliases = alias != null ? [ alias ] : []; - await Emoji.insert({ + await Emojis.save({ + id: genId(), host: null, name, url, @@ -16,8 +18,8 @@ const args = process.argv.slice(2); const name = args[0]; const url = args[1]; -if (!name) throw 'require name'; -if (!url) throw 'require url'; +if (!name) throw new Error('require name'); +if (!url) throw new Error('require url'); main(name, url).then(() => { console.log('success'); diff --git a/src/tools/clean-remote-files.ts b/src/tools/clean-remote-files.ts index 28c76345c7..e722552e14 100644 --- a/src/tools/clean-remote-files.ts +++ b/src/tools/clean-remote-files.ts @@ -1,18 +1,14 @@ import * as promiseLimit from 'promise-limit'; -import DriveFile, { IDriveFile } from '../models/drive-file'; import del from '../services/drive/delete-file'; +import { DriveFiles } from '../models'; +import { Not } from 'typeorm'; +import { DriveFile } from '../models/entities/drive-file'; +import { ensure } from '../prelude/ensure'; const limit = promiseLimit(16); -DriveFile.find({ - 'metadata._user.host': { - $ne: null - }, - 'metadata.deletedAt': { $exists: false } -}, { - fields: { - _id: true - } +DriveFiles.find({ + userHost: Not(null) }).then(async files => { console.log(`there is ${files.length} files`); @@ -21,10 +17,10 @@ DriveFile.find({ console.log('ALL DONE'); }); -async function job(file: IDriveFile): Promise<any> { - file = await DriveFile.findOne({ _id: file._id }); +async function job(file: DriveFile): Promise<any> { + file = await DriveFiles.findOne(file.id).then(ensure); await del(file, true); - console.log('done', file._id); + console.log('done', file.id); } diff --git a/src/tools/move-drive-files.ts b/src/tools/move-drive-files.ts deleted file mode 100644 index 8a1e944503..0000000000 --- a/src/tools/move-drive-files.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as Minio from 'minio'; -import * as uuid from 'uuid'; -import * as promiseLimit from 'promise-limit'; -import DriveFile, { DriveFileChunk, getDriveFileBucket, IDriveFile } from '../models/drive-file'; -import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../models/drive-file-thumbnail'; -import config from '../config'; - -const limit = promiseLimit(16); - -DriveFile.find({ - $or: [{ - 'metadata.withoutChunks': { $exists: false } - }, { - 'metadata.withoutChunks': false - }], - 'metadata.deletedAt': { $exists: false } -}, { - fields: { - _id: true - } -}).then(async files => { - console.log(`there is ${files.length} files`); - - await Promise.all(files.map(file => limit(() => job(file)))); - - console.log('ALL DONE'); -}); - -async function job(file: IDriveFile): Promise<any> { - file = await DriveFile.findOne({ _id: file._id }); - - const minio = new Minio.Client(config.drive.config); - - const name = file.filename.substr(0, 50); - const keyDir = `${config.drive.prefix}/${uuid.v4()}`; - const key = `${keyDir}/${name}`; - const thumbnailKeyDir = `${config.drive.prefix}/${uuid.v4()}`; - const thumbnailKey = `${thumbnailKeyDir}/${name}.thumbnail.jpg`; - - const baseUrl = config.drive.baseUrl - || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? `:${config.drive.config.port}` : '' }/${ config.drive.bucket }`; - - const bucket = await getDriveFileBucket(); - const readable = bucket.openDownloadStream(file._id); - - await minio.putObject(config.drive.bucket, key, readable, file.length, { - 'Content-Type': file.contentType, - 'Cache-Control': 'max-age=31536000, immutable' - }); - - await DriveFile.findOneAndUpdate({ _id: file._id }, { - $set: { - 'metadata.withoutChunks': true, - 'metadata.storage': 'minio', - 'metadata.storageProps': { - key: key, - thumbnailKey: thumbnailKey - }, - 'metadata.url': `${ baseUrl }/${ keyDir }/${ encodeURIComponent(name) }`, - } - }); - - // チャンクをすべて削除 - await DriveFileChunk.remove({ - files_id: file._id - }); - - //#region サムネイルもあれば削除 - const thumbnail = await DriveFileThumbnail.findOne({ - 'metadata.originalId': file._id - }); - - if (thumbnail) { - await DriveFileThumbnailChunk.remove({ - files_id: thumbnail._id - }); - - await DriveFileThumbnail.remove({ _id: thumbnail._id }); - } - //#endregion - - console.log('done', file._id); -} diff --git a/src/tools/resync-remote-user.ts b/src/tools/resync-remote-user.ts index 7db5fe82ef..52f63cf1f1 100644 --- a/src/tools/resync-remote-user.ts +++ b/src/tools/resync-remote-user.ts @@ -1,5 +1,5 @@ import parseAcct from '../misc/acct/parse'; -import resolveUser from '../remote/resolve-user'; +import { resolveUser } from '../remote/resolve-user'; async function main(acct: string): Promise<any> { const { username, host } = parseAcct(acct); diff --git a/src/tools/show-signin-history.ts b/src/tools/show-signin-history.ts index e770710322..fd7cd39e38 100644 --- a/src/tools/show-signin-history.ts +++ b/src/tools/show-signin-history.ts @@ -1,3 +1,5 @@ +import { Users, Signins } from '../models'; + // node built/tools/show-signin-history username // => {Success} {Date} {IPAddrsss} @@ -7,19 +9,16 @@ // node built/tools/show-signin-history username all // with full request headers -import User from '../models/user'; -import Signin from '../models/signin'; - -async function main(username: string, headers: string[]) { - const user = await User.findOne({ +async function main(username: string, headers?: string[]) { + const user = await Users.findOne({ host: null, usernameLower: username.toLowerCase(), }); - if (user === null) throw 'User not found'; + if (user == null) throw new Error('User not found'); - const history = await Signin.find({ - userId: user._id + const history = await Signins.find({ + userId: user.id }); for (const signin of history) { @@ -40,7 +39,7 @@ async function main(username: string, headers: string[]) { const args = process.argv.slice(2); let username = args[0]; -let headers: string[]; +let headers: string[] | undefined; if (args[1] != null) { headers = args[1].split(/,/).map(header => header.toLowerCase()); diff --git a/test/api-visibility.ts b/test/api-visibility.ts index 8380d54f1d..894d0d0753 100644 --- a/test/api-visibility.ts +++ b/test/api-visibility.ts @@ -6,40 +6,33 @@ * * To specify test: * > mocha test/api-visibility.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ -import * as http from 'http'; -import * as assert from 'chai'; -import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils'; - -const expect = assert.expect; - -//#region process -Error.stackTraceLimit = Infinity; -// During the test the env variable is set to test process.env.NODE_ENV = 'test'; -// Display detail of unhandled promise rejection -process.on('unhandledRejection', console.dir); -//#endregion - -const app = require('../built/server/api').default; -const db = require('../built/db/mongodb').default; - -const server = http.createServer(app.callback()); - -//#region Utilities -const request = _request(server); -const signup = _signup(request); -const post = _post(request); -//#endregion +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post } from './utils'; describe('API visibility', () => { - // Reset database each test - before(resetDb(db)); + let p: childProcess.ChildProcess; + + before(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', message => { + if (message === 'ok') done(); + }); + }); after(() => { - server.close(); + p.kill(); }); describe('Note visibility', async () => { @@ -61,8 +54,6 @@ describe('API visibility', () => { let fol: any; /** specified-post */ let spe: any; - /** private-post */ - let pri: any; /** public-reply to target's post */ let pubR: any; @@ -72,8 +63,6 @@ describe('API visibility', () => { let folR: any; /** specified-reply to target's post */ let speR: any; - /** private-reply to target's post */ - let priR: any; /** public-mention to target */ let pubM: any; @@ -83,8 +72,6 @@ describe('API visibility', () => { let folM: any; /** specified-mention to target */ let speM: any; - /** private-mention to target */ - let priM: any; /** reply target post */ let tgt: any; @@ -112,7 +99,6 @@ describe('API visibility', () => { home = await post(alice, { text: 'x', visibility: 'home' }); fol = await post(alice, { text: 'x', visibility: 'followers' }); spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] }); - pri = await post(alice, { text: 'x', visibility: 'private' }); // replies tgt = await post(target, { text: 'y', visibility: 'public' }); @@ -120,14 +106,12 @@ describe('API visibility', () => { homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' }); folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' }); speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' }); - priR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'private' }); // mentions pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' }); homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' }); folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' }); speM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'specified' }); - priM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'private' }); //#endregion }); @@ -135,111 +119,90 @@ describe('API visibility', () => { // public it('[show] public-postを自分が見れる', async(async () => { const res = await show(pub.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-postをフォロワーが見れる', async(async () => { const res = await show(pub.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-postを非フォロワーが見れる', async(async () => { const res = await show(pub.id, other); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-postを未認証が見れる', async(async () => { const res = await show(pub.id, null); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); // home it('[show] home-postを自分が見れる', async(async () => { const res = await show(home.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-postをフォロワーが見れる', async(async () => { const res = await show(home.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-postを非フォロワーが見れる', async(async () => { const res = await show(home.id, other); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-postを未認証が見れる', async(async () => { const res = await show(home.id, null); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); // followers it('[show] followers-postを自分が見れる', async(async () => { const res = await show(fol.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-postをフォロワーが見れる', async(async () => { const res = await show(fol.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-postを非フォロワーが見れない', async(async () => { const res = await show(fol.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] followers-postを未認証が見れない', async(async () => { const res = await show(fol.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); // specified it('[show] specified-postを自分が見れる', async(async () => { const res = await show(spe.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-postを指定ユーザーが見れる', async(async () => { const res = await show(spe.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-postをフォロワーが見れない', async(async () => { const res = await show(spe.id, follower); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-postを非フォロワーが見れない', async(async () => { const res = await show(spe.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-postを未認証が見れない', async(async () => { const res = await show(spe.id, null); - expect(res.body).have.property('isHidden').eql(true); - })); - - // private - it('[show] private-postを自分が見れる', async(async () => { - const res = await show(pri.id, alice); - expect(res.body).have.property('text').eql('x'); - })); - - it('[show] private-postをフォロワーが見れない', async(async () => { - const res = await show(pri.id, follower); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-postを非フォロワーが見れない', async(async () => { - const res = await show(pri.id, other); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-postを未認証が見れない', async(async () => { - const res = await show(pri.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); //#endregion @@ -247,131 +210,110 @@ describe('API visibility', () => { // public it('[show] public-replyを自分が見れる', async(async () => { const res = await show(pubR.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-replyをされた人が見れる', async(async () => { const res = await show(pubR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-replyをフォロワーが見れる', async(async () => { const res = await show(pubR.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-replyを非フォロワーが見れる', async(async () => { const res = await show(pubR.id, other); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] public-replyを未認証が見れる', async(async () => { const res = await show(pubR.id, null); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); // home it('[show] home-replyを自分が見れる', async(async () => { const res = await show(homeR.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-replyをされた人が見れる', async(async () => { const res = await show(homeR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-replyをフォロワーが見れる', async(async () => { const res = await show(homeR.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-replyを非フォロワーが見れる', async(async () => { const res = await show(homeR.id, other); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] home-replyを未認証が見れる', async(async () => { const res = await show(homeR.id, null); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); // followers it('[show] followers-replyを自分が見れる', async(async () => { const res = await show(folR.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async(async () => { const res = await show(folR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-replyをフォロワーが見れる', async(async () => { const res = await show(folR.id, follower); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] followers-replyを非フォロワーが見れない', async(async () => { const res = await show(folR.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] followers-replyを未認証が見れない', async(async () => { const res = await show(folR.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); // specified it('[show] specified-replyを自分が見れる', async(async () => { const res = await show(speR.id, alice); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-replyを指定ユーザーが見れる', async(async () => { const res = await show(speR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-replyをされた人が指定されてなくても見れる', async(async () => { const res = await show(speR.id, target); - expect(res.body).have.property('text').eql('x'); + assert.strictEqual(res.body.text, 'x'); })); it('[show] specified-replyをフォロワーが見れない', async(async () => { const res = await show(speR.id, follower); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-replyを非フォロワーが見れない', async(async () => { const res = await show(speR.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-replyを未認証が見れない', async(async () => { const res = await show(speR.id, null); - expect(res.body).have.property('isHidden').eql(true); - })); - - // private - it('[show] private-replyを自分が見れる', async(async () => { - const res = await show(priR.id, alice); - expect(res.body).have.property('text').eql('x'); - })); - - it('[show] private-replyをフォロワーが見れない', async(async () => { - const res = await show(priR.id, follower); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-replyを非フォロワーが見れない', async(async () => { - const res = await show(priR.id, other); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-replyを未認証が見れない', async(async () => { - const res = await show(priR.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); //#endregion @@ -379,193 +321,172 @@ describe('API visibility', () => { // public it('[show] public-mentionを自分が見れる', async(async () => { const res = await show(pubM.id, alice); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] public-mentionをされた人が見れる', async(async () => { const res = await show(pubM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] public-mentionをフォロワーが見れる', async(async () => { const res = await show(pubM.id, follower); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] public-mentionを非フォロワーが見れる', async(async () => { const res = await show(pubM.id, other); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] public-mentionを未認証が見れる', async(async () => { const res = await show(pubM.id, null); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); // home it('[show] home-mentionを自分が見れる', async(async () => { const res = await show(homeM.id, alice); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] home-mentionをされた人が見れる', async(async () => { const res = await show(homeM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] home-mentionをフォロワーが見れる', async(async () => { const res = await show(homeM.id, follower); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] home-mentionを非フォロワーが見れる', async(async () => { const res = await show(homeM.id, other); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] home-mentionを未認証が見れる', async(async () => { const res = await show(homeM.id, null); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); // followers it('[show] followers-mentionを自分が見れる', async(async () => { const res = await show(folM.id, alice); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); - it('[show] followers-mentionを非フォロワーでもメンションされていれば見れる', async(async () => { + it('[show] followers-mentionを非フォロワーがメンションされていても見れない', async(async () => { const res = await show(folM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.isHidden, true); })); it('[show] followers-mentionをフォロワーが見れる', async(async () => { const res = await show(folM.id, follower); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] followers-mentionを非フォロワーが見れない', async(async () => { const res = await show(folM.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] followers-mentionを未認証が見れない', async(async () => { const res = await show(folM.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); // specified it('[show] specified-mentionを自分が見れる', async(async () => { const res = await show(speM.id, alice); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); it('[show] specified-mentionを指定ユーザーが見れる', async(async () => { const res = await show(speM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.text, '@target x'); })); - it('[show] specified-mentionをされた人が指定されてなくても見れる', async(async () => { + it('[show] specified-mentionをされた人が指定されてなかったら見れない', async(async () => { const res = await show(speM.id, target); - expect(res.body).have.property('text').eql('@target x'); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-mentionをフォロワーが見れない', async(async () => { const res = await show(speM.id, follower); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-mentionを非フォロワーが見れない', async(async () => { const res = await show(speM.id, other); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); it('[show] specified-mentionを未認証が見れない', async(async () => { const res = await show(speM.id, null); - expect(res.body).have.property('isHidden').eql(true); - })); - - // private - it('[show] private-mentionを自分が見れる', async(async () => { - const res = await show(priM.id, alice); - expect(res.body).have.property('text').eql('@target x'); - })); - - it('[show] private-mentionをフォロワーが見れない', async(async () => { - const res = await show(priM.id, follower); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-mentionを非フォロワーが見れない', async(async () => { - const res = await show(priM.id, other); - expect(res.body).have.property('isHidden').eql(true); - })); - - it('[show] private-mentionを未認証が見れない', async(async () => { - const res = await show(priM.id, null); - expect(res.body).have.property('isHidden').eql(true); + assert.strictEqual(res.body.isHidden, true); })); //#endregion //#region HTL it('[HTL] public-post が 自分が見れる', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, alice); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == pub.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); it('[HTL] public-post が 非フォロワーから見れない', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, other); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == pub.id); - expect(notes).length(0); + assert.strictEqual(notes.length, 0); })); it('[HTL] followers-post が フォロワーから見れる', async(async () => { const res = await request('/notes/timeline', { limit: 100 }, follower); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == fol.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); //#endregion //#region RTL it('[replies] followers-reply が フォロワーから見れる', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folR.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folR.id); - expect(notes).length(0); + assert.strictEqual(notes.length, 0); })); it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => { const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folR.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); //#endregion //#region MTL it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => { const res = await request('/notes/mentions', { limit: 100 }, target); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folR.id); - expect(notes[0]).have.property('text').eql('x'); + assert.strictEqual(notes[0].text, 'x'); })); it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async(async () => { const res = await request('/notes/mentions', { limit: 100 }, target); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); const notes = res.body.filter((n: any) => n.id == folM.id); - expect(notes[0]).have.property('text').eql('@target x'); + assert.strictEqual(notes[0].text, '@target x'); })); //#endregion }); diff --git a/test/api.ts b/test/api.ts index cc4521d3dc..71443c5730 100644 --- a/test/api.ts +++ b/test/api.ts @@ -6,44 +6,35 @@ * * To specify test: * > mocha test/api.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ -import * as http from 'http'; -import * as fs from 'fs'; -import * as assert from 'chai'; -import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils'; - -const expect = assert.expect; - -//#region process -Error.stackTraceLimit = Infinity; - -// During the test the env variable is set to test process.env.NODE_ENV = 'test'; -// Display detail of unhandled promise rejection -process.on('unhandledRejection', console.dir); -//#endregion - -const app = require('../built/server/api').default; -const db = require('../built/db/mongodb').default; - -const server = http.createServer(app.callback()); - -//#region Utilities -const request = _request(server); -const signup = _signup(request); -const post = _post(request); -const react = _react(request); -const uploadFile = _uploadFile(server); -//#endregion +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, react, uploadFile } from './utils'; describe('API', () => { - // Reset database each test - beforeEach(resetDb(db)); + let p: childProcess.ChildProcess; - after(() => { - server.close(); + beforeEach(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', message => { + if (message === 'ok') { + done(); + } + }); + }); + + afterEach(() => { + p.kill(); }); describe('signup', () => { @@ -52,7 +43,7 @@ describe('API', () => { username: 'test.', password: 'test' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('空のパスワードでアカウントが作成できない', async(async () => { @@ -60,7 +51,7 @@ describe('API', () => { username: 'test', password: '' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('正しくアカウントが作成できる', async(async () => { @@ -71,9 +62,9 @@ describe('API', () => { const res = await request('/signup', me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('username').eql(me.username); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.username, me.username); })); it('同じユーザー名のアカウントは作成できない', async(async () => { @@ -86,7 +77,7 @@ describe('API', () => { password: 'test' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -102,7 +93,7 @@ describe('API', () => { password: 'bar' }); - expect(res).have.status(403); + assert.strictEqual(res.status, 403); })); it('クエリをインジェクションできない', async(async () => { @@ -117,7 +108,7 @@ describe('API', () => { } }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('正しい情報でサインインできる', async(async () => { @@ -131,7 +122,7 @@ describe('API', () => { password: 'foo' }); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); })); }); @@ -149,12 +140,11 @@ describe('API', () => { birthday: myBirthday }, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql(myName); - expect(res.body).have.nested.property('profile').a('object'); - expect(res.body).have.nested.property('profile.location').eql(myLocation); - expect(res.body).have.nested.property('profile.birthday').eql(myBirthday); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, myName); + assert.strictEqual(res.body.location, myLocation); + assert.strictEqual(res.body.birthday, myBirthday); })); it('名前を空白にできない', async(async () => { @@ -162,7 +152,7 @@ describe('API', () => { const res = await request('/i/update', { name: ' ' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('誕生日の設定を削除できる', async(async () => { @@ -175,10 +165,9 @@ describe('API', () => { birthday: null }, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.nested.property('profile').a('object'); - expect(res.body).have.nested.property('profile.birthday').eql(null); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.birthday, null); })); it('不正な誕生日の形式で怒られる', async(async () => { @@ -186,7 +175,7 @@ describe('API', () => { const res = await request('/i/update', { birthday: '2000/09/07' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -198,365 +187,23 @@ describe('API', () => { userId: me.id }, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('id').eql(me.id); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.id, me.id); })); it('ユーザーが存在しなかったら怒る', async(async () => { const res = await request('/users/show', { userId: '000000000000000000000000' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('間違ったIDで怒られる', async(async () => { const res = await request('/users/show', { userId: 'kyoppie' }); - expect(res).have.status(400); - })); - }); - - describe('notes/create', () => { - it('投稿できる', async(async () => { - const me = await signup(); - const post = { - text: 'test' - }; - - const res = await request('/notes/create', post, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('text').eql(post.text); - })); - - it('ファイルを添付できる', async(async () => { - const me = await signup(); - const file = await uploadFile(me); - - const res = await request('/notes/create', { - fileIds: [file.id] - }, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('fileIds').eql([file.id]); - })); - - it('他人のファイルは無視', async(async () => { - const me = await signup({ username: 'alice' }); - const bob = await signup({ username: 'bob' }); - const file = await uploadFile(bob); - - const res = await request('/notes/create', { - text: 'test', - fileIds: [file.id] - }, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('fileIds').eql([]); - })); - - it('存在しないファイルは無視', async(async () => { - const me = await signup(); - - const res = await request('/notes/create', { - text: 'test', - fileIds: ['000000000000000000000000'] - }, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('fileIds').eql([]); - })); - - it('不正なファイルIDで怒られる', async(async () => { - const me = await signup(); - const res = await request('/notes/create', { - fileIds: ['kyoppie'] - }, me); - expect(res).have.status(400); - })); - - it('返信できる', async(async () => { - const bob = await signup({ username: 'bob' }); - const bobPost = await post(bob); - - const alice = await signup({ username: 'alice' }); - const alicePost = { - text: 'test', - replyId: bobPost.id - }; - - const res = await request('/notes/create', alicePost, alice); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('text').eql(alicePost.text); - expect(res.body.createdNote).have.property('replyId').eql(alicePost.replyId); - expect(res.body.createdNote).have.property('reply'); - expect(res.body.createdNote.reply).have.property('text').eql(alicePost.text); - })); - - it('renoteできる', async(async () => { - const bob = await signup({ username: 'bob' }); - const bobPost = await post(bob, { - text: 'test' - }); - - const alice = await signup({ username: 'alice' }); - const alicePost = { - renoteId: bobPost.id - }; - - const res = await request('/notes/create', alicePost, alice); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('renoteId').eql(alicePost.renoteId); - expect(res.body.createdNote).have.property('renote'); - expect(res.body.createdNote.renote).have.property('text').eql(bobPost.text); - })); - - it('引用renoteできる', async(async () => { - const bob = await signup({ username: 'bob' }); - const bobPost = await post(bob, { - text: 'test' - }); - - const alice = await signup({ username: 'alice' }); - const alicePost = { - text: 'test', - renoteId: bobPost.id - }; - - const res = await request('/notes/create', alicePost, alice); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('text').eql(alicePost.text); - expect(res.body.createdNote).have.property('renoteId').eql(alicePost.renoteId); - expect(res.body.createdNote).have.property('renote'); - expect(res.body.createdNote.renote).have.property('text').eql(bobPost.text); - })); - - it('文字数ぎりぎりで怒られない', async(async () => { - const me = await signup(); - const post = { - text: '!'.repeat(1000) - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(200); - })); - - it('文字数オーバーで怒られる', async(async () => { - const me = await signup(); - const post = { - text: '!'.repeat(1001) - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('存在しないリプライ先で怒られる', async(async () => { - const me = await signup(); - const post = { - text: 'test', - replyId: '000000000000000000000000' - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('存在しないrenote対象で怒られる', async(async () => { - const me = await signup(); - const post = { - renoteId: '000000000000000000000000' - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('不正なリプライ先IDで怒られる', async(async () => { - const me = await signup(); - const post = { - text: 'test', - replyId: 'foo' - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('不正なrenote対象IDで怒られる', async(async () => { - const me = await signup(); - const post = { - renoteId: 'foo' - }; - const res = await request('/notes/create', post, me); - expect(res).have.status(400); - })); - - it('投票を添付できる', async(async () => { - const me = await signup(); - - const res = await request('/notes/create', { - text: 'test', - poll: { - choices: ['foo', 'bar'] - } - }, me); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('poll'); - })); - - it('投票の選択肢が無くて怒られる', async(async () => { - const me = await signup(); - const res = await request('/notes/create', { - poll: {} - }, me); - expect(res).have.status(400); - })); - - it('投票の選択肢が無くて怒られる (空の配列)', async(async () => { - const me = await signup(); - const res = await request('/notes/create', { - poll: { - choices: [] - } - }, me); - expect(res).have.status(400); - })); - - it('投票の選択肢が1つで怒られる', async(async () => { - const me = await signup(); - const res = await request('/notes/create', { - poll: { - choices: ['Strawberry Pasta'] - } - }, me); - expect(res).have.status(400); - })); - - it('投票できる', async(async () => { - const me = await signup(); - - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'] - } - }, me); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1 - }, me); - - expect(res).have.status(204); - })); - - it('複数投票できない', async(async () => { - const me = await signup(); - - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'] - } - }, me); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 0 - }, me); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 2 - }, me); - - expect(res).have.status(400); - })); - - it('許可されている場合は複数投票できる', async(async () => { - const me = await signup(); - - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - multiple: true - } - }, me); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 0 - }, me); - - await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1 - }, me); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 2 - }, me); - - expect(res).have.status(204); - })); - - it('締め切られている場合は投票できない', async(async () => { - const me = await signup(); - - const { body } = await request('/notes/create', { - text: 'test', - poll: { - choices: ['sakura', 'izumi', 'ako'], - expiredAfter: 1 - } - }, me); - - await new Promise(x => setTimeout(x, 2)); - - const res = await request('/notes/polls/vote', { - noteId: body.createdNote.id, - choice: 1 - }, me); - - expect(res).have.status(400); - })); - - it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => { - const alice = await signup({ username: 'alice' }); - const bob = await signup({ username: 'bob' }); - const post = { - text: '@bob @bob @bob yo' - }; - - const res = await request('/notes/create', post, alice); - - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('createdNote'); - expect(res.body.createdNote).have.property('text').eql(post.text); - - const noteDoc = await db.get('notes').findOne({ _id: res.body.createdNote.id }); - expect(noteDoc.mentions.map((id: any) => id.toString())).eql([bob.id.toString()]); + assert.strictEqual(res.status, 400); })); }); @@ -571,24 +218,24 @@ describe('API', () => { noteId: myPost.id }, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('id').eql(myPost.id); - expect(res.body).have.property('text').eql(myPost.text); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.id, myPost.id); + assert.strictEqual(res.body.text, myPost.text); })); it('投稿が存在しなかったら怒る', async(async () => { const res = await request('/notes/show', { noteId: '000000000000000000000000' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('間違ったIDで怒られる', async(async () => { const res = await request('/notes/show', { noteId: 'kyoppie' }); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -603,7 +250,7 @@ describe('API', () => { reaction: 'like' }, alice); - expect(res).have.status(204); + assert.strictEqual(res.status, 204); })); it('自分の投稿にはリアクションできない', async(async () => { @@ -615,7 +262,7 @@ describe('API', () => { reaction: 'like' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('二重にリアクションできない', async(async () => { @@ -630,7 +277,7 @@ describe('API', () => { reaction: 'like' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('存在しない投稿にはリアクションできない', async(async () => { @@ -641,7 +288,7 @@ describe('API', () => { reaction: 'like' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('空のパラメータで怒られる', async(async () => { @@ -649,7 +296,7 @@ describe('API', () => { const res = await request('/notes/reactions/create', {}, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('間違ったIDで怒られる', async(async () => { @@ -660,7 +307,7 @@ describe('API', () => { reaction: 'like' }, me); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -673,7 +320,7 @@ describe('API', () => { userId: alice.id }, bob); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); })); it('既にフォローしている場合は怒る', async(async () => { @@ -687,7 +334,7 @@ describe('API', () => { userId: alice.id }, bob); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('存在しないユーザーはフォローできない', async(async () => { @@ -697,7 +344,7 @@ describe('API', () => { userId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('自分自身はフォローできない', async(async () => { @@ -707,7 +354,7 @@ describe('API', () => { userId: alice.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('空のパラメータで怒られる', async(async () => { @@ -715,7 +362,7 @@ describe('API', () => { const res = await request('/following/create', {}, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('間違ったIDで怒られる', async(async () => { @@ -725,7 +372,7 @@ describe('API', () => { userId: 'foo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -741,7 +388,7 @@ describe('API', () => { userId: alice.id }, bob); - expect(res).have.status(200); + assert.strictEqual(res.status, 200); })); it('フォローしていない場合は怒る', async(async () => { @@ -752,7 +399,7 @@ describe('API', () => { userId: alice.id }, bob); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('存在しないユーザーはフォロー解除できない', async(async () => { @@ -762,7 +409,7 @@ describe('API', () => { userId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('自分自身はフォロー解除できない', async(async () => { @@ -772,7 +419,7 @@ describe('API', () => { userId: alice.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('空のパラメータで怒られる', async(async () => { @@ -780,7 +427,7 @@ describe('API', () => { const res = await request('/following/delete', {}, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('間違ったIDで怒られる', async(async () => { @@ -790,7 +437,7 @@ describe('API', () => { userId: 'kyoppie' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -799,20 +446,20 @@ describe('API', () => { it('ドライブ情報を取得できる', async(async () => { const bob = await signup({ username: 'bob' }); await uploadFile({ - userId: me._id, - datasize: 256 + userId: me.id, + size: 256 }); await uploadFile({ - userId: me._id, - datasize: 512 + userId: me.id, + size: 512 }); await uploadFile({ - userId: me._id, - datasize: 1024 + userId: me.id, + size: 1024 }); const res = await request('/drive', {}, me); - expect(res).have.status(200); - expect(res.body).be.a('object'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); expect(res.body).have.property('usage').eql(1792); }));*/ }); @@ -821,14 +468,11 @@ describe('API', () => { it('ファイルを作成できる', async(async () => { const alice = await signup({ username: 'alice' }); - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', alice.token) - .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); + const res = await uploadFile(alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('Lenna.png'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'Lenna.png'); })); it('ファイル無しで怒られる', async(async () => { @@ -836,21 +480,18 @@ describe('API', () => { const res = await request('/drive/files/create', {}, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('SVGファイルを作成できる', async(async () => { const izumi = await signup({ username: 'izumi' }); - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', izumi.token) - .attach('file', fs.readFileSync(__dirname + '/resources/image.svg'), 'image.svg'); + const res = await uploadFile(izumi, __dirname + '/resources/image.svg'); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('image.svg'); - expect(res.body).have.property('type').eql('image/svg+xml'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'image.svg'); + assert.strictEqual(res.body.type, 'image/svg+xml'); })); }); @@ -865,9 +506,9 @@ describe('API', () => { name: newName }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql(newName); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, newName); })); it('他人のファイルは更新できない', async(async () => { @@ -880,7 +521,7 @@ describe('API', () => { name: 'いちごパスタ.png' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('親フォルダを更新できる', async(async () => { @@ -895,9 +536,9 @@ describe('API', () => { folderId: folder.id }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('folderId').eql(folder.id); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.folderId, folder.id); })); it('親フォルダを無しにできる', async(async () => { @@ -918,9 +559,9 @@ describe('API', () => { folderId: null }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('folderId').eql(null); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.folderId, null); })); it('他人のフォルダには入れられない', async(async () => { @@ -936,7 +577,7 @@ describe('API', () => { folderId: folder.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('存在しないフォルダで怒られる', async(async () => { @@ -948,7 +589,7 @@ describe('API', () => { folderId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('不正なフォルダIDで怒られる', async(async () => { @@ -960,7 +601,7 @@ describe('API', () => { folderId: 'foo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('ファイルが存在しなかったら怒る', async(async () => { @@ -971,7 +612,7 @@ describe('API', () => { name: 'いちごパスタ.png' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('間違ったIDで怒られる', async(async () => { @@ -982,7 +623,7 @@ describe('API', () => { name: 'いちごパスタ.png' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -994,9 +635,9 @@ describe('API', () => { name: 'test' }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('test'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'test'); })); }); @@ -1012,9 +653,9 @@ describe('API', () => { name: 'new name' }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('name').eql('new name'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.name, 'new name'); })); it('他人のフォルダを更新できない', async(async () => { @@ -1029,7 +670,7 @@ describe('API', () => { name: 'new name' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('親フォルダを更新できる', async(async () => { @@ -1046,9 +687,9 @@ describe('API', () => { parentId: parentFolder.id }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('parentId').eql(parentFolder.id); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.parentId, parentFolder.id); })); it('親フォルダを無しに更新できる', async(async () => { @@ -1069,9 +710,9 @@ describe('API', () => { parentId: null }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('parentId').eql(null); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.parentId, null); })); it('他人のフォルダを親フォルダに設定できない', async(async () => { @@ -1089,7 +730,7 @@ describe('API', () => { parentId: parentFolder.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('フォルダが循環するような構造にできない', async(async () => { @@ -1110,7 +751,7 @@ describe('API', () => { parentId: parentFolder.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('フォルダが循環するような構造にできない(再帰的)', async(async () => { @@ -1138,7 +779,7 @@ describe('API', () => { parentId: folderC.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('フォルダが循環するような構造にできない(自身)', async(async () => { @@ -1166,7 +807,7 @@ describe('API', () => { parentId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('不正な親フォルダIDで怒られる', async(async () => { @@ -1180,7 +821,7 @@ describe('API', () => { parentId: 'foo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('存在しないフォルダを更新できない', async(async () => { @@ -1190,7 +831,7 @@ describe('API', () => { folderId: '000000000000000000000000' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('不正なフォルダIDで怒られる', async(async () => { @@ -1200,7 +841,7 @@ describe('API', () => { folderId: 'foo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -1214,9 +855,9 @@ describe('API', () => { text: 'test' }, alice); - expect(res).have.status(200); - expect(res.body).be.a('object'); - expect(res.body).have.property('text').eql('test'); + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.text, 'test'); })); it('自分自身にはメッセージを送信できない', async(async () => { @@ -1227,7 +868,7 @@ describe('API', () => { text: 'Yo' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('存在しないユーザーにはメッセージを送信できない', async(async () => { @@ -1238,7 +879,7 @@ describe('API', () => { text: 'test' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('不正なユーザーIDで怒られる', async(async () => { @@ -1249,7 +890,7 @@ describe('API', () => { text: 'test' }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('テキストが無くて怒られる', async(async () => { @@ -1260,7 +901,7 @@ describe('API', () => { userId: bob.id }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); it('文字数オーバーで怒られる', async(async () => { @@ -1272,7 +913,7 @@ describe('API', () => { text: '!'.repeat(1001) }, alice); - expect(res).have.status(400); + assert.strictEqual(res.status, 400); })); }); @@ -1297,9 +938,9 @@ describe('API', () => { noteId: alicePost.id }, carol); - expect(res).have.status(200); - expect(res.body).be.a('array'); - expect(res.body).length(0); + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 0); })); }); @@ -1319,10 +960,10 @@ describe('API', () => { const res = await request('/notes/timeline', {}, bob); - expect(res).have.status(200); - expect(res.body).be.a('array'); - expect(res.body).length(1); - expect(res.body[0].id).equals(alicePost.id); + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 1); + assert.strictEqual(res.body[0].id, alicePost.id); })); }); }); diff --git a/test/chart.ts b/test/chart.ts new file mode 100644 index 0000000000..b3976b03ba --- /dev/null +++ b/test/chart.ts @@ -0,0 +1,323 @@ +/* + * Tests of chart engine + * + * How to run the tests: + * > mocha test/chart.ts --require ts-node/register + * + * To specify test: + * > mocha test/chart.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as lolex from 'lolex'; +import { async } from './utils'; +import { getConnection, createConnection } from 'typeorm'; +const config = require('../built/config').default; +const Chart = require('../built/services/chart/core').default; +const _TestChart = require('../built/services/chart/charts/schemas/test'); +const _TestGroupedChart = require('../built/services/chart/charts/schemas/test-grouped'); +const _TestUniqueChart = require('../built/services/chart/charts/schemas/test-unique'); + +function initDb() { + try { + const conn = getConnection(); + return Promise.resolve(conn); + } catch (e) {} + + return createConnection({ + type: 'postgres', + host: config.db.host, + port: config.db.port, + username: config.db.user, + password: config.db.pass, + database: config.db.db, + synchronize: true, + dropSchema: true, + entities: [ + Chart.schemaToEntity(_TestChart.name, _TestChart.schema), + Chart.schemaToEntity(_TestGroupedChart.name, _TestGroupedChart.schema), + Chart.schemaToEntity(_TestUniqueChart.name, _TestUniqueChart.schema) + ] + }); +} + +describe('Chart', () => { + let testChart: any; + let testGroupedChart: any; + let testUniqueChart: any; + let connection: any; + let clock: lolex.InstalledClock<lolex.Clock>; + + before(done => { + initDb().then(c => { + connection = c; + done(); + }); + }); + + beforeEach(done => { + const TestChart = require('../built/services/chart/charts/classes/test').default; + testChart = new TestChart(); + + const TestGroupedChart = require('../built/services/chart/charts/classes/test-grouped').default; + testGroupedChart = new TestGroupedChart(); + + const TestUniqueChart = require('../built/services/chart/charts/classes/test-unique').default; + testUniqueChart = new TestUniqueChart(); + + clock = lolex.install({ + now: new Date('2000-01-01 00:00:00') + }); + + connection.synchronize().then(done); + }); + + afterEach(done => { + clock.uninstall(); + connection.dropDatabase().then(done); + }); + + it('Can updates', async(async () => { + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + })); + + it('Empty chart', async(async () => { + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0] + }, + }); + })); + + it('Can updates at multiple times at same time', async(async () => { + await testChart.increment(); + await testChart.increment(); + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [3, 0, 0], + total: [3, 0, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [3, 0, 0], + total: [3, 0, 0] + }, + }); + })); + + it('Can updates at different times', async(async () => { + await testChart.increment(); + + clock.tick('01:00:00'); + + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 1, 0], + total: [2, 1, 0] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0] + }, + }); + })); + + it('Can padding', async(async () => { + await testChart.increment(); + + clock.tick('02:00:00'); + + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 1], + total: [2, 1, 1] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0] + }, + }); + })); + + // 要求された範囲にログがひとつもない場合でもパディングできる + it('Can padding from past range', async(async () => { + await testChart.increment(); + + clock.tick('05:00:00'); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [1, 1, 1] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + })); + + // 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる + // Issue #3190 + it('Can padding from past range 2', async(async () => { + await testChart.increment(); + clock.tick('05:00:00'); + await testChart.increment(); + + const chartHours = await testChart.getChart('hour', 3); + const chartDays = await testChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [2, 1, 1] + }, + }); + + assert.deepStrictEqual(chartDays, { + foo: { + dec: [0, 0, 0], + inc: [2, 0, 0], + total: [2, 0, 0] + }, + }); + })); + + describe('Grouped', () => { + it('Can updates', async(async () => { + await testGroupedChart.increment('alice'); + + const aliceChartHours = await testGroupedChart.getChart('hour', 3, 'alice'); + const aliceChartDays = await testGroupedChart.getChart('day', 3, 'alice'); + const bobChartHours = await testGroupedChart.getChart('hour', 3, 'bob'); + const bobChartDays = await testGroupedChart.getChart('day', 3, 'bob'); + + assert.deepStrictEqual(aliceChartHours, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + + assert.deepStrictEqual(aliceChartDays, { + foo: { + dec: [0, 0, 0], + inc: [1, 0, 0], + total: [1, 0, 0] + }, + }); + + assert.deepStrictEqual(bobChartHours, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0] + }, + }); + + assert.deepStrictEqual(bobChartDays, { + foo: { + dec: [0, 0, 0], + inc: [0, 0, 0], + total: [0, 0, 0] + }, + }); + })); + }); + + describe('Unique increment', () => { + it('Can updates', async(async () => { + await testUniqueChart.uniqueIncrement('alice'); + await testUniqueChart.uniqueIncrement('alice'); + await testUniqueChart.uniqueIncrement('bob'); + + const chartHours = await testUniqueChart.getChart('hour', 3); + const chartDays = await testUniqueChart.getChart('day', 3); + + assert.deepStrictEqual(chartHours, { + foo: [2, 0, 0], + }); + + assert.deepStrictEqual(chartDays, { + foo: [2, 0, 0], + }); + })); + }); +}); diff --git a/test/mfm.ts b/test/mfm.ts index 191ee5e0ed..69260a5415 100644 --- a/test/mfm.ts +++ b/test/mfm.ts @@ -6,6 +6,10 @@ * * To specify test: * > mocha test/mfm.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ import * as assert from 'assert'; diff --git a/test/mocha.opts b/test/mocha.opts index 907011807d..e114c53bd8 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1 +1,2 @@ ---timeout 10000 +--timeout 30000 +--slow 1000 diff --git a/test/mute.ts b/test/mute.ts new file mode 100644 index 0000000000..bf24b55ee5 --- /dev/null +++ b/test/mute.ts @@ -0,0 +1,170 @@ +/* + * Tests of mute + * + * How to run the tests: + * > mocha test/mute.ts --require ts-node/register + * + * To specify test: + * > mocha test/mute.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, react, connectStream } from './utils'; + +describe('Mute', () => { + let p: childProcess.ChildProcess; + + // alice mutes carol + let alice: any; + let bob: any; + let carol: any; + + before(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', async message => { + if (message === 'ok') { + (p.channel as any).onread = () => {}; + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + done(); + } + }); + }); + + after(() => { + p.kill(); + }); + + it('ミュート作成', async(async () => { + const res = await request('/mute/create', { + userId: carol.id + }, alice); + + assert.strictEqual(res.status, 204); + })); + + it('「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない', async(async () => { + const bobNote = await post(bob, { text: '@alice hi' }); + const carolNote = await post(carol, { text: '@alice hi' }); + + const res = await request('/notes/mentions', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + })); + + it('ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない', async(async () => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + await post(carol, { text: '@alice hi' }); + + const res = await request('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + })); + + it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + let fired = false; + + const ws = await connectStream(alice, 'main', ({ type }) => { + if (type == 'unreadMention') { + fired = true; + } + }); + + post(carol, { text: '@alice hi' }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 5000); + })); + + it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', () => new Promise(async done => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + await request('/notifications/mark-all-as-read', {}, alice); + + let fired = false; + + const ws = await connectStream(alice, 'main', ({ type }) => { + if (type == 'unreadNotification') { + fired = true; + } + }); + + post(carol, { text: '@alice hi' }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 5000); + })); + + describe('Timeline', () => { + it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => { + const aliceNote = await post(alice); + const bobNote = await post(bob); + const carolNote = await post(carol); + + const res = await request('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + })); + + it('タイムラインにミュートしているユーザーの投稿のRenoteが含まれない', async(async () => { + const aliceNote = await post(alice); + const carolNote = await post(carol); + const bobNote = await post(bob, { + renoteId: carolNote.id + }); + + const res = await request('/notes/local-timeline', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), false); + assert.strictEqual(res.body.some(note => note.id === carolNote.id), false); + })); + }); + + describe('Notification', () => { + it('通知にミュートしているユーザーの通知が含まれない(リアクション)', async(async () => { + const aliceNote = await post(alice); + await react(bob, aliceNote, 'like'); + await react(carol, aliceNote, 'like'); + + const res = await request('/i/notifications', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some(notification => notification.userId === bob.id), true); + assert.strictEqual(res.body.some(notification => notification.userId === carol.id), false); + })); + }); +}); diff --git a/test/note.ts b/test/note.ts new file mode 100644 index 0000000000..7a05930eae --- /dev/null +++ b/test/note.ts @@ -0,0 +1,361 @@ +/* + * Tests of Note + * + * How to run the tests: + * > mocha test/note.ts --require ts-node/register + * + * To specify test: + * > mocha test/note.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, uploadFile } from './utils'; +import { Note } from '../built/models/entities/note'; +const initDb = require('../built/db/postgre.js').initDb; + +describe('Note', () => { + let p: childProcess.ChildProcess; + let Notes: any; + + let alice: any; + let bob: any; + + before(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', message => { + if (message === 'ok') { + (p.channel as any).onread = () => {}; + initDb(true).then(async connection => { + Notes = connection.getRepository(Note); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + done(); + }); + } + }); + }); + + after(() => { + p.kill(); + }); + + it('投稿できる', async(async () => { + const post = { + text: 'test' + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + })); + + it('ファイルを添付できる', async(async () => { + const file = await uploadFile(alice); + + const res = await request('/notes/create', { + fileIds: [file.id] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]); + })); + + it('他人のファイルは無視', async(async () => { + const file = await uploadFile(bob); + + const res = await request('/notes/create', { + text: 'test', + fileIds: [file.id] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, []); + })); + + it('存在しないファイルは無視', async(async () => { + const res = await request('/notes/create', { + text: 'test', + fileIds: ['000000000000000000000000'] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.deepStrictEqual(res.body.createdNote.fileIds, []); + })); + + it('不正なファイルIDで怒られる', async(async () => { + const res = await request('/notes/create', { + fileIds: ['kyoppie'] + }, alice); + assert.strictEqual(res.status, 400); + })); + + it('返信できる', async(async () => { + const bobPost = await post(bob, { + text: 'foo' + }); + + const alicePost = { + text: 'bar', + replyId: bobPost.id + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, alicePost.text); + assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId); + assert.strictEqual(res.body.createdNote.reply.text, bobPost.text); + })); + + it('renoteできる', async(async () => { + const bobPost = await post(bob, { + text: 'test' + }); + + const alicePost = { + renoteId: bobPost.id + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); + })); + + it('引用renoteできる', async(async () => { + const bobPost = await post(bob, { + text: 'test' + }); + + const alicePost = { + text: 'test', + renoteId: bobPost.id + }; + + const res = await request('/notes/create', alicePost, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, alicePost.text); + assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId); + assert.strictEqual(res.body.createdNote.renote.text, bobPost.text); + })); + + it('文字数ぎりぎりで怒られない', async(async () => { + const post = { + text: '!'.repeat(1000) + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 200); + })); + + it('文字数オーバーで怒られる', async(async () => { + const post = { + text: '!'.repeat(1001) + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('存在しないリプライ先で怒られる', async(async () => { + const post = { + text: 'test', + replyId: '000000000000000000000000' + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('存在しないrenote対象で怒られる', async(async () => { + const post = { + renoteId: '000000000000000000000000' + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('不正なリプライ先IDで怒られる', async(async () => { + const post = { + text: 'test', + replyId: 'foo' + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('不正なrenote対象IDで怒られる', async(async () => { + const post = { + renoteId: 'foo' + }; + const res = await request('/notes/create', post, alice); + assert.strictEqual(res.status, 400); + })); + + it('存在しないユーザーにメンションできる', async(async () => { + const post = { + text: '@ghost yo' + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + })); + + it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => { + const post = { + text: '@bob @bob @bob yo' + }; + + const res = await request('/notes/create', post, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.text, post.text); + + const noteDoc = await Notes.findOne(res.body.createdNote.id); + assert.deepStrictEqual(noteDoc.mentions, [bob.id]); + })); + + describe('notes/create', () => { + it('投票を添付できる', async(async () => { + const res = await request('/notes/create', { + text: 'test', + poll: { + choices: ['foo', 'bar'] + } + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); + assert.strictEqual(res.body.createdNote.poll != null, true); + })); + + it('投票の選択肢が無くて怒られる', async(async () => { + const res = await request('/notes/create', { + poll: {} + }, alice); + assert.strictEqual(res.status, 400); + })); + + it('投票の選択肢が無くて怒られる (空の配列)', async(async () => { + const res = await request('/notes/create', { + poll: { + choices: [] + } + }, alice); + assert.strictEqual(res.status, 400); + })); + + it('投票の選択肢が1つで怒られる', async(async () => { + const res = await request('/notes/create', { + poll: { + choices: ['Strawberry Pasta'] + } + }, alice); + assert.strictEqual(res.status, 400); + })); + + it('投票できる', async(async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'] + } + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1 + }, alice); + + assert.strictEqual(res.status, 204); + })); + + it('複数投票できない', async(async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'] + } + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 0 + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 2 + }, alice); + + assert.strictEqual(res.status, 400); + })); + + it('許可されている場合は複数投票できる', async(async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + multiple: true + } + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 0 + }, alice); + + await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1 + }, alice); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 2 + }, alice); + + assert.strictEqual(res.status, 204); + })); + + it('締め切られている場合は投票できない', async(async () => { + const { body } = await request('/notes/create', { + text: 'test', + poll: { + choices: ['sakura', 'izumi', 'ako'], + expiredAfter: 1 + } + }, alice); + + await new Promise(x => setTimeout(x, 2)); + + const res = await request('/notes/polls/vote', { + noteId: body.createdNote.id, + choice: 1 + }, alice); + + assert.strictEqual(res.status, 400); + })); + }); +}); diff --git a/test/reaction-lib.ts b/test/reaction-lib.ts index 2f6c8ea81b..3a7ff1ab33 100644 --- a/test/reaction-lib.ts +++ b/test/reaction-lib.ts @@ -6,6 +6,10 @@ * * To specify test: * > mocha test/reaction-lib.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ /* diff --git a/test/streaming.ts b/test/streaming.ts index 500324d520..7594728618 100644 --- a/test/streaming.ts +++ b/test/streaming.ts @@ -6,143 +6,844 @@ * * To specify test: * > mocha test/streaming.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 */ -import * as http from 'http'; -import * as WebSocket from 'ws'; +process.env.NODE_ENV = 'test'; + import * as assert from 'assert'; -import { _signup, _request, _uploadFile, _post, _react, resetDb } from './utils'; +import * as childProcess from 'child_process'; +import { connectStream, signup, request, post } from './utils'; +import { Following } from '../built/models/entities/following'; +const initDb = require('../built/db/postgre.js').initDb; -//#region process -Error.stackTraceLimit = Infinity; +describe('Streaming', () => { + let p: childProcess.ChildProcess; + let Followings: any; -// During the test the env variable is set to test -process.env.NODE_ENV = 'test'; + beforeEach(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', message => { + if (message === 'ok') { + (p.channel as any).onread = () => {}; + initDb(true).then(async connection => { + Followings = connection.getRepository(Following); + done(); + }); + } + }); + }); -// Display detail of unhandled promise rejection -process.on('unhandledRejection', console.dir); -//#endregion + afterEach(() => { + p.kill(); + }); -const app = require('../built/server/api').default; -const server = require('../built/server').startServer(); -const db = require('../built/db/mongodb').default; + const follow = async (follower, followee) => { + await Followings.save({ + id: 'a', + createdAt: new Date(), + followerId: follower.id, + followeeId: followee.id, + followerHost: follower.host, + followerInbox: null, + followerSharedInbox: null, + followeeHost: followee.host, + followeeInbox: null, + followeeSharedInbox: null + }); + }; -const apiServer = http.createServer(app.callback()); + it('mention event', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); -//#region Utilities -const request = _request(apiServer); -const signup = _signup(request); -const post = _post(request); -//#endregion + const ws = await connectStream(bob, 'main', ({ type, body }) => { + if (type == 'mention') { + assert.deepStrictEqual(body.userId, alice.id); + ws.close(); + done(); + } + }); -describe('Streaming', () => { - // Reset database each test - beforeEach(resetDb(db)); + post(alice, { + text: 'foo @bob bar' + }); + })); + + it('renote event', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + const bobNote = await post(bob, { + text: 'foo' + }); + + const ws = await connectStream(bob, 'main', ({ type, body }) => { + if (type == 'renote') { + assert.deepStrictEqual(body.renoteId, bobNote.id); + ws.close(); + done(); + } + }); + + post(alice, { + renoteId: bobNote.id + }); + })); + + describe('Home Timeline', () => { + it('自分の投稿が流れる', () => new Promise(async done => { + const post = { + text: 'foo' + }; + + const me = await signup(); + + const ws = await connectStream(me, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.text, post.text); + ws.close(); + done(); + } + }); + + request('/notes/create', post, me); + })); + + it('フォローしているユーザーの投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // Alice が Bob をフォロー + await request('/following/create', { + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + + it('フォローしていないユーザーの投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + let fired = false; + + const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }); + + post(bob, { + text: 'foo' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // Alice が Bob をフォロー + await request('/following/create', { + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + assert.deepStrictEqual(body.text, 'foo'); + ws.close(); + done(); + } + }); + + // Bob が Alice 宛てのダイレクト投稿 + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + })); + + it('フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + const carol = await signup({ username: 'carol' }); + + // Alice が Bob をフォロー + await request('/following/create', { + userId: bob.id + }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }); - after(() => { - server.close(); + // Bob が Carol 宛てのダイレクト投稿 + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [carol.id] + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); }); - it('投稿がタイムラインに流れる', () => new Promise(async done => { - const post = { - text: 'foo' - }; + describe('Local Timeline', () => { + it('自分の投稿が流れる', () => new Promise(async done => { + const me = await signup(); + + const ws = await connectStream(me, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, me.id); + ws.close(); + done(); + } + }); + + post(me, { + text: 'foo' + }); + })); - const me = await signup(); - const ws = new WebSocket(`ws://localhost/streaming?i=${me.token}`); + it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); - ws.on('open', () => { - ws.on('message', data => { - const msg = JSON.parse(data.toString()); - if (msg.type == 'channel' && msg.body.id == 'a') { - if (msg.body.type == 'note') { - assert.deepStrictEqual(msg.body.body.text, post.text); - ws.close(); - done(); - } - } else if (msg.type == 'connected' && msg.body.id == 'a') { - request('/notes/create', post, me); + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); } }); - ws.send(JSON.stringify({ - type: 'connect', - body: { - channel: 'homeTimeline', - id: 'a', - pong: true + post(bob, { + text: 'foo' + }); + })); + + it('リモートユーザーの投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + let fired = false; + + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; } - })); - }); - })); + }); - it('mention event', () => new Promise(async done => { - const alice = await signup({ username: 'alice' }); - const bob = await signup({ username: 'bob' }); - const aliceNote = { - text: 'foo @bob bar' - }; + post(bob, { + text: 'foo' + }); - const ws = new WebSocket(`ws://localhost/streaming?i=${bob.token}`); + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); - ws.on('open', () => { - ws.on('message', data => { - const msg = JSON.parse(data.toString()); - if (msg.type == 'channel' && msg.body.id == 'a') { - if (msg.body.type == 'mention') { - assert.deepStrictEqual(msg.body.body.text, aliceNote.text); - ws.close(); - done(); - } - } else if (msg.type == 'connected' && msg.body.id == 'a') { - request('/notes/create', aliceNote, alice); + it('フォローしてたとしてもリモートユーザーの投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + // Alice が Bob をフォロー + await request('/following/create', { + userId: bob.id + }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; } }); - ws.send(JSON.stringify({ - type: 'connect', - body: { - channel: 'main', - id: 'a', - pong: true + post(bob, { + text: 'foo' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + it('ホーム指定の投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + let fired = false; + + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; } - })); - }); - })); + }); - it('renote event', () => new Promise(async done => { - const alice = await signup({ username: 'alice' }); - const bob = await signup({ username: 'bob' }); - const bobNote = await post(bob, { - text: 'foo' - }); + // ホーム指定 + post(bob, { + text: 'foo', + visibility: 'home' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + it('フォローしているローカルユーザーのダイレクト投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); - const ws = new WebSocket(`ws://localhost/streaming?i=${bob.token}`); + // Alice が Bob をフォロー + await request('/following/create', { + userId: bob.id + }, alice); - ws.on('open', () => { - ws.on('message', data => { - const msg = JSON.parse(data.toString()); - if (msg.type == 'channel' && msg.body.id == 'a') { - if (msg.body.type == 'renote') { - assert.deepStrictEqual(msg.body.body.renoteId, bobNote.id); - ws.close(); - done(); - } - } else if (msg.type == 'connected' && msg.body.id == 'a') { - request('/notes/create', { - renoteId: bobNote.id - }, alice); + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + assert.deepStrictEqual(body.text, 'foo'); + ws.close(); + done(); } }); - ws.send(JSON.stringify({ - type: 'connect', - body: { - channel: 'main', - id: 'a', - pong: true + // Bob が Alice 宛てのダイレクト投稿 + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + })); + + it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + let fired = false; + + const ws = await connectStream(alice, 'localTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; } - })); - }); - })); + }); + + // フォロワー宛て投稿 + post(bob, { + text: 'foo', + visibility: 'followers' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + }); + + describe('Hybrid Timeline', () => { + it('自分の投稿が流れる', () => new Promise(async done => { + const me = await signup(); + + const ws = await connectStream(me, 'hybridTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, me.id); + ws.close(); + done(); + } + }); + + post(me, { + text: 'foo' + }); + })); + + it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + + it('フォローしているリモートユーザーの投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + // Alice が Bob をフォロー + await follow(alice, bob); + + const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + + it('フォローしていないリモートユーザーの投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + let fired = false; + + const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }); + + post(bob, { + text: 'foo' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + it('フォローしているユーザーのダイレクト投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // Alice が Bob をフォロー + await request('/following/create', { + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + assert.deepStrictEqual(body.text, 'foo'); + ws.close(); + done(); + } + }); + + // Bob が Alice 宛てのダイレクト投稿 + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + })); + + it('フォローしていないローカルユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + let fired = false; + + const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }); + + // フォロワー宛て投稿 + post(bob, { + text: 'foo', + visibility: 'followers' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + }); + + describe('Global Timeline', () => { + it('フォローしていないローカルユーザーの投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + + it('フォローしていないリモートユーザーの投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob', host: 'example.com' }); + + const ws = await connectStream(alice, 'globalTimeline', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }); + + post(bob, { + text: 'foo' + }); + })); + }); + + describe('UserList Timeline', () => { + it('リストに入れているユーザーの投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // リスト作成 + const list = await request('/users/lists/create', { + title: 'my list' + }, alice).then(x => x.body); + + // Alice が Bob をリスイン + await request('/users/lists/push', { + listId: list.id, + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'userList', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + ws.close(); + done(); + } + }, { + listId: list.id + }); + + post(bob, { + text: 'foo' + }); + })); + + it('リストに入れていないユーザーの投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // リスト作成 + const list = await request('/users/lists/create', { + title: 'my list' + }, alice).then(x => x.body); + + let fired = false; + + const ws = await connectStream(alice, 'userList', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }, { + listId: list.id + }); + + post(bob, { + text: 'foo' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + + // #4471 + it('リストに入れているユーザーのダイレクト投稿が流れる', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // リスト作成 + const list = await request('/users/lists/create', { + title: 'my list' + }, alice).then(x => x.body); + + // Alice が Bob をリスイン + await request('/users/lists/push', { + listId: list.id, + userId: bob.id + }, alice); + + const ws = await connectStream(alice, 'userList', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.userId, bob.id); + assert.deepStrictEqual(body.text, 'foo'); + ws.close(); + done(); + } + }, { + listId: list.id + }); + + // Bob が Alice 宛てのダイレクト投稿 + post(bob, { + text: 'foo', + visibility: 'specified', + visibleUserIds: [alice.id] + }); + })); + + // #4335 + it('リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない', () => new Promise(async done => { + const alice = await signup({ username: 'alice' }); + const bob = await signup({ username: 'bob' }); + + // リスト作成 + const list = await request('/users/lists/create', { + title: 'my list' + }, alice).then(x => x.body); + + // Alice が Bob をリスイン + await request('/users/lists/push', { + listId: list.id, + userId: bob.id + }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'userList', ({ type, body }) => { + if (type == 'note') { + fired = true; + } + }, { + listId: list.id + }); + + // フォロワー宛て投稿 + post(bob, { + text: 'foo', + visibility: 'followers' + }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 3000); + })); + }); + + describe('Hashtag Timeline', () => { + it('指定したハッシュタグの投稿が流れる', () => new Promise(async done => { + const me = await signup(); + + const ws = await connectStream(me, 'hashtag', ({ type, body }) => { + if (type == 'note') { + assert.deepStrictEqual(body.text, '#foo'); + ws.close(); + done(); + } + }, { + q: [ + ['foo'] + ] + }); + + post(me, { + text: '#foo' + }); + })); + + it('指定したハッシュタグの投稿が流れる (AND)', () => new Promise(async done => { + const me = await signup(); + + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + + const ws = await connectStream(me, 'hashtag', ({ type, body }) => { + if (type == 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + } + }, { + q: [ + ['foo', 'bar'] + ] + }); + + post(me, { + text: '#foo' + }); + + post(me, { + text: '#bar' + }); + + post(me, { + text: '#foo #bar' + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 0); + assert.strictEqual(barCount, 0); + assert.strictEqual(fooBarCount, 1); + ws.close(); + done(); + }, 3000); + })); + + it('指定したハッシュタグの投稿が流れる (OR)', () => new Promise(async done => { + const me = await signup(); + + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + let piyoCount = 0; + + const ws = await connectStream(me, 'hashtag', ({ type, body }) => { + if (type == 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + if (body.text === '#piyo') piyoCount++; + } + }, { + q: [ + ['foo'], + ['bar'] + ] + }); + + post(me, { + text: '#foo' + }); + + post(me, { + text: '#bar' + }); + + post(me, { + text: '#foo #bar' + }); + + post(me, { + text: '#piyo' + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 1); + assert.strictEqual(barCount, 1); + assert.strictEqual(fooBarCount, 1); + assert.strictEqual(piyoCount, 0); + ws.close(); + done(); + }, 3000); + })); + + it('指定したハッシュタグの投稿が流れる (AND + OR)', () => new Promise(async done => { + const me = await signup(); + + let fooCount = 0; + let barCount = 0; + let fooBarCount = 0; + let piyoCount = 0; + let waaaCount = 0; + + const ws = await connectStream(me, 'hashtag', ({ type, body }) => { + if (type == 'note') { + if (body.text === '#foo') fooCount++; + if (body.text === '#bar') barCount++; + if (body.text === '#foo #bar') fooBarCount++; + if (body.text === '#piyo') piyoCount++; + if (body.text === '#waaa') waaaCount++; + } + }, { + q: [ + ['foo', 'bar'], + ['piyo'] + ] + }); + + post(me, { + text: '#foo' + }); + + post(me, { + text: '#bar' + }); + + post(me, { + text: '#foo #bar' + }); + + post(me, { + text: '#piyo' + }); + + post(me, { + text: '#waaa' + }); + + setTimeout(() => { + assert.strictEqual(fooCount, 0); + assert.strictEqual(barCount, 0); + assert.strictEqual(fooBarCount, 1); + assert.strictEqual(piyoCount, 1); + assert.strictEqual(waaaCount, 0); + ws.close(); + done(); + }, 3000); + })); + }); }); diff --git a/test/user-notes.ts b/test/user-notes.ts new file mode 100644 index 0000000000..5e457d6692 --- /dev/null +++ b/test/user-notes.ts @@ -0,0 +1,86 @@ +/* + * Tests of Note + * + * How to run the tests: + * > mocha test/user-notes.ts --require ts-node/register + * + * To specify test: + * > mocha test/user-notes.ts --require ts-node/register -g 'test name' + * + * If the tests not start, try set following enviroment variables: + * TS_NODE_FILES=true and TS_NODE_TRANSPILE_ONLY=true + * for more details, please see: https://github.com/TypeStrong/ts-node/issues/754 + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, uploadFile } from './utils'; + +describe('users/notes', () => { + let p: childProcess.ChildProcess; + + let alice: any; + let jpgNote: any; + let pngNote: any; + let jpgPngNote: any; + + before(done => { + p = childProcess.spawn('node', [__dirname + '/../index.js'], { + stdio: ['inherit', 'inherit', 'ipc'], + env: { NODE_ENV: 'test' } + }); + p.on('message', async message => { + if (message === 'ok') { + (p.channel as any).onread = () => {}; + + alice = await signup({ username: 'alice' }); + const jpg = await uploadFile(alice, __dirname + '/resources/Lenna.jpg'); + const png = await uploadFile(alice, __dirname + '/resources/Lenna.png'); + jpgNote = await post(alice, { + fileIds: [jpg.id] + }); + pngNote = await post(alice, { + fileIds: [png.id] + }); + jpgPngNote = await post(alice, { + fileIds: [jpg.id, png.id] + }); + + done(); + } + }); + }); + + after(() => { + p.kill(); + }); + + it('ファイルタイプ指定 (jpg)', async(async () => { + const res = await request('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg'] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 2); + assert.strictEqual(res.body.some(note => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some(note => note.id === jpgPngNote.id), true); + })); + + it('ファイルタイプ指定 (jpg or png)', async(async () => { + const res = await request('/users/notes', { + userId: alice.id, + fileType: ['image/jpeg', 'image/png'] + }, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.length, 3); + assert.strictEqual(res.body.some(note => note.id === jpgNote.id), true); + assert.strictEqual(res.body.some(note => note.id === pngNote.id), true); + assert.strictEqual(res.body.some(note => note.id === jpgPngNote.id), true); + })); +}); diff --git a/test/utils.ts b/test/utils.ts index 1377122478..fbba9a68c9 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; -import * as http from 'http'; -import * as assert from 'chai'; -assert.use(require('chai-http')); +import * as WebSocket from 'ws'; +const fetch = require('node-fetch'); +import * as req from 'request'; export const async = (fn: Function) => (done: Function) => { fn().then(() => { @@ -11,19 +11,31 @@ export const async = (fn: Function) => (done: Function) => { }); }; -export const _request = (server: http.Server) => async (endpoint: string, params: any, me?: any): Promise<ChaiHttp.Response> => { +export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => { const auth = me ? { i: me.token } : {}; - const res = await assert.request(server) - .post(endpoint) - .send(Object.assign(auth, params)); + try { + const res = await fetch('http://localhost:80/api' + endpoint, { + method: 'POST', + body: JSON.stringify(Object.assign(auth, params)) + }); - return res; + const status = res.status; + const body = res.status !== 204 ? await res.json().catch() : null; + + return { + body, status + }; + } catch (e) { + return { + body: null, status: 500 + }; + } }; -export const _signup = (request: ReturnType<typeof _request>) => async (params?: any): Promise<any> => { +export const signup = async (params?: any): Promise<any> => { const q = Object.assign({ username: 'test', password: 'test' @@ -34,50 +46,59 @@ export const _signup = (request: ReturnType<typeof _request>) => async (params?: return res.body; }; -export const _post = (request: ReturnType<typeof _request>) => async (user: any, params?: any): Promise<any> => { +export const post = async (user: any, params?: any): Promise<any> => { const q = Object.assign({ text: 'test' }, params); const res = await request('/notes/create', q, user); - return res.body.createdNote; + return res.body ? res.body.createdNote : null; }; -export const _react = (request: ReturnType<typeof _request>) => async (user: any, note: any, reaction: string): Promise<any> => { +export const react = async (user: any, note: any, reaction: string): Promise<any> => { await request('/notes/reactions/create', { noteId: note.id, reaction: reaction }, user); }; -export const _uploadFile = (server: http.Server) => async (user: any): Promise<any> => { - const res = await assert.request(server) - .post('/drive/files/create') - .field('i', user.token) - .attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png'); +export const uploadFile = (user: any, path?: string): Promise<any> => new Promise((ok, rej) => { + req.post({ + url: 'http://localhost:80/api/drive/files/create', + formData: { + i: user.token, + file: fs.createReadStream(path || __dirname + '/resources/Lenna.png') + }, + json: true + }, (err, httpResponse, body) => { + ok(body); + }); +}); - return res.body; -}; +export function connectStream(user: any, channel: string, listener: any, params?: any): Promise<WebSocket> { + return new Promise((res, rej) => { + const ws = new WebSocket(`ws://localhost/streaming?i=${user.token}`); -export const resetDb = (db: any) => () => new Promise(res => { - // APIがなにかレスポンスを返した後に、後処理を行う場合があり、 - // レスポンスを受け取ってすぐデータベースをリセットすると - // その後処理と競合し(テスト自体は合格するものの)エラーがコンソールに出力され - // 見た目的に気持ち悪くなるので、後処理が終るのを待つために500msくらい待ってから - // データベースをリセットするようにする - setTimeout(async () => { - await Promise.all([ - db.get('users').drop(), - db.get('notes').drop(), - db.get('driveFiles.files').drop(), - db.get('driveFiles.chunks').drop(), - db.get('driveFolders').drop(), - db.get('apps').drop(), - db.get('accessTokens').drop(), - db.get('authSessions').drop() - ]); + ws.on('open', () => { + ws.on('message', data => { + const msg = JSON.parse(data.toString()); + if (msg.type == 'channel' && msg.body.id == 'a') { + listener(msg.body); + } else if (msg.type == 'connected' && msg.body.id == 'a') { + res(ws); + } + }); - res(); - }, 500); -}); + ws.send(JSON.stringify({ + type: 'connect', + body: { + channel: channel, + id: 'a', + pong: true, + params: params + } + })); + }); + }); +} diff --git a/tsconfig.json b/tsconfig.json index 09da750c35..4f1d1b9cd5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,10 @@ "removeComments": false, "noLib": false, "strict": true, - "strictNullChecks": false, + "strictNullChecks": true, + "strictPropertyInitialization": false, "experimentalDecorators": true, + "emitDecoratorMetadata": true, "resolveJsonModule": true, "typeRoots": [ "node_modules/@types", |