Compare commits

...

71 Commits

Author SHA1 Message Date
f6f8cdbcf2 11.0.0-beta.13 2019-04-14 02:36:21 +09:00
f46f53b8a3 Refactor 2019-04-14 02:36:00 +09:00
a2fcae4383 Fix bug 2019-04-14 02:33:50 +09:00
483f043768 11.0.0-beta.12 2019-04-14 02:24:27 +09:00
f8e0f4f21f Fix bug 2019-04-14 02:21:57 +09:00
9c3613e96d Improve error handling 2019-04-14 02:19:59 +09:00
d9cacdc86d Update meid.ts 2019-04-14 01:47:46 +09:00
aa3d2deeaa Add meid 2019-04-14 01:40:29 +09:00
e64912545a Update id generation methods 2019-04-14 01:08:26 +09:00
b247be80cc 11.0.0-beta.11 2019-04-13 20:02:47 +09:00
343f2f1f33 Fix bug 2019-04-13 20:02:31 +09:00
2d590df900 Clean up 2019-04-13 20:01:32 +09:00
ba56b2b9fd Update CHANGELOG.md 2019-04-13 19:40:03 +09:00
bf472b0c5e refactor 2019-04-13 19:36:57 +09:00
f7961f34c5 Update CHANGELOG.md 2019-04-13 19:23:32 +09:00
e369031a28 Redis必須に 2019-04-13 19:19:32 +09:00
186d7bbfd9 Fix bug 2019-04-13 19:08:41 +09:00
60a11f8da5 11.0.0-beta.10 2019-04-13 18:58:48 +09:00
1181fcdceb Fix bug 2019-04-13 18:58:29 +09:00
8cb9852058 リプライ先をエラー時に無視すると本来は投票なのに投票じゃないと扱われおかしくなるのでエラーにするように 2019-04-13 18:45:07 +09:00
53d46d1cbe Fix error 2019-04-13 18:23:32 +09:00
275e1c8de9 Refactor 2019-04-13 18:17:27 +09:00
d46eca4c87 Refactor 2019-04-13 18:14:32 +09:00
2927fb1597 11.0.0-beta.9 2019-04-13 17:27:17 +09:00
8c72e011d2 Fix bug 2019-04-13 17:26:38 +09:00
69662e24c3 Fix bug 2019-04-13 17:24:56 +09:00
96099ffe98 11.0.0-beta.8 2019-04-13 16:55:13 +09:00
ae16b45c11 Fix bug 2019-04-13 16:54:21 +09:00
cfee87d3ef 11.0.0-beta.7 2019-04-13 15:18:27 +09:00
5fcf5bc635 Fix bug 2019-04-13 15:18:12 +09:00
14bcb813cc Update migrate.ts 2019-04-13 15:09:16 +09:00
6af19794b6 11.0.0-beta.6 2019-04-13 15:03:56 +09:00
8316186695 Clean packed responses 2019-04-13 15:02:15 +09:00
85d3023cd5 Clean packed responses 2019-04-13 14:55:59 +09:00
78414dee29 Add note 2019-04-13 14:45:51 +09:00
084135141f typo 2019-04-13 14:37:45 +09:00
467a21f028 Update CONTRIBUTING.md 2019-04-13 14:36:35 +09:00
6e284c44d6 Update CONTRIBUTING.md 2019-04-13 14:34:34 +09:00
daf9a449e8 Update CONTRIBUTING.md 2019-04-13 14:31:05 +09:00
960268fd33 typo 2019-04-13 14:17:50 +09:00
8c331da315 Fix bug 2019-04-13 14:11:15 +09:00
b0d626d862 Update CONTRIBUTING.md 2019-04-13 14:04:29 +09:00
a51fbd7316 Suppress errors 2019-04-13 04:00:02 +09:00
063f372f3c 11.0.0-beta.5 2019-04-13 01:53:00 +09:00
987168b863 strictNullChecks (#4666)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip
2019-04-13 01:43:22 +09:00
4ee40c3345 Fix bug 2019-04-12 14:24:56 +09:00
654daff7ce Better error handling 2019-04-12 13:07:56 +09:00
64e10e9619 11.0.0-beta.4 2019-04-12 02:25:56 +09:00
b89cffe98d Fix bug 2019-04-12 02:22:22 +09:00
bd76ba702f 🎨 2019-04-12 02:15:22 +09:00
5d52e9ce6b 11.0.0-beta.3 2019-04-12 01:56:48 +09:00
3c29027ca3 Clean up 2019-04-12 01:54:28 +09:00
2ff3069d23 トランザクションを使うようにしたり 2019-04-12 01:52:25 +09:00
4198246351 トランザクションを使用してアンケートレコードの挿入に失敗した場合に投稿レコードの挿入もなかったことにするように 2019-04-12 01:30:10 +09:00
2b6389b4dc Fix bug 2019-04-12 01:03:40 +09:00
d7df75ae6c Clean up 2019-04-12 01:01:25 +09:00
11c30eccb3 非正規化カラムを削除
非正規化するほどの情報じゃない
2019-04-12 00:42:39 +09:00
ab8c6515b8 Fix error log 2019-04-12 00:33:26 +09:00
d4ad36fa41 Update migrate.ts 2019-04-11 22:49:12 +09:00
4d688be3df Update migrate.ts 2019-04-11 22:44:04 +09:00
d2b75f3501 Update migrate.ts 2019-04-11 19:42:35 +09:00
46b78cb4ff Increase url column length 2019-04-11 19:03:49 +09:00
5d6e0d0f37 Update migrate.ts 2019-04-11 16:15:27 +09:00
e19d0a37bb Update migrate.ts 2019-04-11 16:09:33 +09:00
dea3e2132e Update migrate.ts 2019-04-11 15:53:15 +09:00
91c1ceefbd Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-04-11 12:59:16 +09:00
5c50989970 Fix bug 2019-04-11 12:59:09 +09:00
2a7e3b9c51 Fix: AP actor Service のサポートが不完全 (v11) (#4662) 2019-04-11 03:09:12 +09:00
ab302df0ae Update CHANGELOG.md 2019-04-11 00:18:37 +09:00
21e5809993 Clean up 2019-04-10 23:57:39 +09:00
c58afc67e8 Update migrate.ts 2019-04-10 20:13:14 +09:00
250 changed files with 1432 additions and 1179 deletions

View File

@ -65,10 +65,10 @@ db:
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Redis configuration └───────────────────────────────────── #───┘ Redis configuration └─────────────────────────────────────
#redis: redis:
# host: localhost host: localhost
# port: 6379 port: 6379
# pass: example-pass #pass: example-pass
# ┌─────────────────────────────┐ # ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └───────────────────────────── #───┘ Elasticsearch configuration └─────────────────────────────
@ -127,28 +127,12 @@ drive:
# change it according to your preferences. # change it according to your preferences.
# Available methods: # Available methods:
# aid1 ... Use AID for ID generation (with random 1 char) # aid ... Short, Millisecond accuracy
# aid2 ... Use AID for ID generation (with random 2 chars) # meid ... Similar to ObjectID, Millisecond accuracy
# aid3 ... Use AID for ID generation (with random 3 chars) # ulid ... Millisecond accuracy
# aid4 ... Use AID for ID generation (with random 4 chars) # objectid ... This is left for backward compatibility
# ulid ... Use ulid for ID generation
# objectid ... This is left for backward compatibility.
# AID(n) is the original ID generation method. id: 'aid'
# The trailing n represents the number of random characters that
# will be suffixed.
# The larger n is the safer. If n is small, the possibility of
# collision at the same time increases, but there are also
# advantages such as shortening of the URL.
# ULID: Universally Unique Lexicographically Sortable Identifier.
# for more details: https://github.com/ulid/spec
# * Normally, AID should be sufficient.
# ObjectID is the method used in previous versions of Misskey.
# * Choose this if you are migrating from a previous Misskey.
id: 'aid2'
# ┌─────────────────────┐ # ┌─────────────────────┐
#───┘ Other configuration └───────────────────────────────────── #───┘ Other configuration └─────────────────────────────────────

View File

@ -7,11 +7,37 @@ If you encounter any problems with updating, please try the following:
11.0.0 11.0.0
---------- ----------
* データベースがMongoDBからPostgreSQLに変更されました * **データベースがMongoDBからPostgreSQLに変更されました**
* **Redisが必須に**
* アカウントを完全に削除できるように
* ミュート/ブロック時にそのユーザーの投稿のウォッチをすべて解除するように
* フォロー申請数が実際より1すくなくなる問題を修正
* リストからアカウント削除したユーザーを削除できない問題を修正
* リストTLでフォローしていないユーザーの非公開投稿が流れる問題を修正
* リストTLでダイレクト投稿が流れない問題を修正
* ミュートしているユーザーの投稿がタイムラインに流れてくることがある問題を修正
### APIの破壊的変更 ### APIの破壊的変更
* v10時点で deprecated だったパラメータなどを削除 * v10時点で deprecated だったパラメータなどを削除
* ユーザーリストの title が name に * ユーザーリストの title が name に
* リバーシの対局の`settings`プロパティがなくなり、その中にあったプロパティがすべて上の階層に
* 例えば`game.settings.map``game.map`になる
### 既知の問題
* アプリが作成できない
* 依存ライブラリの問題と思わるため、対応が難しい
### Migration
coming soon...
10.100.0
----------
* ユーザーリストでフォローボタンを表示するように
* ドライブのファイルのサムネイルを修正
* 投稿ウィジットでローカルのみの公開範囲で投稿できない問題を修正
* TLを遡った時に抜けがある時がある問題を修正
* ユーザータイムラインが投稿日時順ではなくなっているのを修正
* 10.99.0 でチャートのレンダリングがおかしい問題を修正
10.99.0 10.99.0
---------- ----------

View File

@ -130,6 +130,40 @@ const users = userIds.length > 0 ? await Users.find({
}) : []; }) : [];
``` ```
### 配列のインデックス in SQL
SQLでは配列のインデックスは**1始まり**。
`[a, b, c]`の `a`にアクセスしたいなら`[0]`ではなく`[1]`と書く
### `undefined`にご用心 ### `undefined`にご用心
MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。 MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。
MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください 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';
// }
// の糖衣構文のような扱いです
```

View File

@ -24,18 +24,13 @@ Please install and setup these softwares:
#### Dependencies :package: #### Dependencies :package:
* **[Node.js](https://nodejs.org/en/)** >= 11.7.0 * **[Node.js](https://nodejs.org/en/)** >= 11.7.0
* **[PostgreSQL](https://www.postgresql.org/)** >= 10 * **[PostgreSQL](https://www.postgresql.org/)** >= 10
* **[Redis](https://redis.io/)**
##### Optional ##### 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 * [Elasticsearch](https://www.elastic.co/) - required to enable the search feature
* [FFmpeg](https://www.ffmpeg.org/) * [FFmpeg](https://www.ffmpeg.org/)
*3.* Setup PostgreSQL *3.* Install Misskey
----------------------------------------------------------------
:)
*4.* Install Misskey
---------------------------------------------------------------- ----------------------------------------------------------------
1. `su - misskey` Connect to misskey user. 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. 2. `git clone -b master git://github.com/syuilo/misskey.git` Clone the misskey repo from master branch.
@ -43,12 +38,12 @@ Please install and setup these softwares:
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) 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. `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`. 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`.
2. Edit `default.yml` 2. Edit `default.yml`
*6.* Build Misskey *5.* Build Misskey
---------------------------------------------------------------- ----------------------------------------------------------------
Build misskey with the following: Build misskey with the following:
@ -64,13 +59,13 @@ If you're still encountering errors about some modules, use node-gyp:
3. `node-gyp build` 3. `node-gyp build`
4. `NODE_ENV=production npm run build` 4. `NODE_ENV=production npm run build`
*7.* Init DB *6.* Init DB
---------------------------------------------------------------- ----------------------------------------------------------------
``` shell ``` shell
npm run init npm run init
``` ```
*8.* That is it. *7.* That is it.
---------------------------------------------------------------- ----------------------------------------------------------------
Well done! Now, you have an environment that run to Misskey. Well done! Now, you have an environment that run to Misskey.

View File

@ -24,18 +24,13 @@ Installez les paquets suivants :
#### Dépendences :package: #### Dépendences :package:
* **[Node.js](https://nodejs.org/en/)** >= 11.7.0 * **[Node.js](https://nodejs.org/en/)** >= 11.7.0
* **[PostgreSQL](https://www.postgresql.org/)** >= 10 * **[PostgreSQL](https://www.postgresql.org/)** >= 10
* **[Redis](https://redis.io/)**
##### Optionnels ##### 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 * [Elasticsearch](https://www.elastic.co/) - requis pour pouvoir activer la fonctionnalité de recherche
* [FFmpeg](https://www.ffmpeg.org/) * [FFmpeg](https://www.ffmpeg.org/)
*3.* Paramètrage de PostgreSQL *3.* Installation de Misskey
----------------------------------------------------------------
:)
*4.* Installation de Misskey
---------------------------------------------------------------- ----------------------------------------------------------------
1. `su - misskey` Basculez vers l'utilisateur 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. 2. `git clone -b master git://github.com/syuilo/misskey.git` Clonez la branche master du dépôt misskey.
@ -43,12 +38,12 @@ Installez les paquets suivants :
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) 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. `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`. 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` 2. Editez le fichier `default.yml`
*6.* Construction de Misskey *5.* Construction de Misskey
---------------------------------------------------------------- ----------------------------------------------------------------
Construisez Misskey comme ceci : Construisez Misskey comme ceci :
@ -64,7 +59,7 @@ Si vous rencontrez des erreurs concernant certains modules, utilisez node-gyp:
3. `node-gyp build` 3. `node-gyp build`
4. `NODE_ENV=production npm run 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 Excellent ! Maintenant, vous avez un environnement prêt pour lancer Misskey

View File

@ -24,25 +24,14 @@ adduser --disabled-password --disabled-login misskey
#### 依存関係 :package: #### 依存関係 :package:
* **[Node.js](https://nodejs.org/en/)** (11.7.0以上) * **[Node.js](https://nodejs.org/en/)** (11.7.0以上)
* **[PostgreSQL](https://www.postgresql.org/)** (10以上) * **[PostgreSQL](https://www.postgresql.org/)** (10以上)
* **[Redis](https://redis.io/)**
##### オプション ##### オプション
* [Redis](https://redis.io/)
* Redisはオプションですが、インストールすることを強く推奨します。
* インストールしなくていいのは、あなたのインスタンスが自分専用のときだけとお考えください。
* 具体的には、Redisをインストールしないと、次の事が出来なくなります:
* Misskeyプロセスを複数起動しての負荷分散
* レートリミット
* ジョブキュー
* Twitter連携
* [Elasticsearch](https://www.elastic.co/) * [Elasticsearch](https://www.elastic.co/)
* 検索機能を有効にするためにはインストールが必要です。 * 検索機能を有効にするためにはインストールが必要です。
* [FFmpeg](https://www.ffmpeg.org/) * [FFmpeg](https://www.ffmpeg.org/)
*3.* PostgreSQLの設定 *3.* Misskeyのインストール
----------------------------------------------------------------
:)
*4.* Misskeyのインストール
---------------------------------------------------------------- ----------------------------------------------------------------
1. `su - misskey` misskeyユーザーを使用 1. `su - misskey` misskeyユーザーを使用
2. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン 2. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン
@ -50,12 +39,12 @@ adduser --disabled-password --disabled-login misskey
4. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[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. `npm install` Misskeyの依存パッケージをインストール
*5.* 設定ファイルを作成する *4.* 設定ファイルを作成する
---------------------------------------------------------------- ----------------------------------------------------------------
1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。 1. `cp .config/example.yml .config/default.yml` `.config/example.yml`をコピーし名前を`default.yml`にする。
2. `default.yml` を編集する。 2. `default.yml` を編集する。
*6.* Misskeyのビルド *5.* Misskeyのビルド
---------------------------------------------------------------- ----------------------------------------------------------------
次のコマンドでMisskeyをビルドしてください: 次のコマンドでMisskeyをビルドしてください:
@ -70,13 +59,13 @@ Debianをお使いであれば、`build-essential`パッケージをインスト
3. `node-gyp build` 3. `node-gyp build`
4. `NODE_ENV=production npm run build` 4. `NODE_ENV=production npm run build`
*7.* データベースを初期化 *6.* データベースを初期化
---------------------------------------------------------------- ----------------------------------------------------------------
``` shell ``` shell
npm run init npm run init
``` ```
*8.* 以上です! *7.* 以上です!
---------------------------------------------------------------- ----------------------------------------------------------------
お疲れ様でした。これでMisskeyを動かす準備は整いました。 お疲れ様でした。これでMisskeyを動かす準備は整いました。

View File

@ -120,7 +120,7 @@ gulp.task('copy:client', () =>
]) ])
.pipe(isProduction ? (imagemin as any)() : gutil.noop()) .pipe(isProduction ? (imagemin as any)() : gutil.noop())
.pipe(rename(path => { .pipe(rename(path => {
path.dirname = path.dirname.replace('assets', '.'); path.dirname = path.dirname!.replace('assets', '.');
})) }))
.pipe(gulp.dest('./built/client/assets/')) .pipe(gulp.dest('./built/client/assets/'))
); );

View File

@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "11.0.0-beta.2", "version": "11.0.0-beta.13",
"codename": "daybreak", "codename": "daybreak",
"repository": { "repository": {
"type": "git", "type": "git",
@ -106,7 +106,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bootstrap-vue": "2.0.0-rc.13", "bootstrap-vue": "2.0.0-rc.13",
"bull": "3.7.0", "bull": "3.7.0",
"cafy": "15.1.0", "cafy": "15.1.1",
"chai": "4.2.0", "chai": "4.2.0",
"chai-http": "4.2.1", "chai-http": "4.2.1",
"chalk": "2.4.2", "chalk": "2.4.2",

View File

@ -44,7 +44,7 @@ function greet() {
export async function masterMain() { export async function masterMain() {
greet(); greet();
let config: Config; let config!: Config;
try { try {
// initialize app // initialize app

View File

@ -15,6 +15,6 @@ export async function workerMain() {
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process
process.send('ready'); process.send!('ready');
} }
} }

View File

@ -8,7 +8,7 @@
<div class="is-remote" v-if="user.host != null"> <div class="is-remote" v-if="user.host != null">
<details> <details>
<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary> <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> </details>
</div> </div>
<header :style="bannerStyle"> <header :style="bannerStyle">

View File

@ -4,7 +4,7 @@
<fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }} <fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}
</div> </div>
<div class="is-remote" v-if="user.host != null" :class="{ shadow: $store.state.device.useShadow, round: $store.state.device.roundedCorners }"> <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>
<div class="main"> <div class="main">
<x-header class="header" :user="user"/> <x-header class="header" :user="user"/>

View File

@ -5,7 +5,7 @@
</template> </template>
<div class="wwtwuxyh" v-if="!fetching"> <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-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> <header>
<div class="banner" :style="style"></div> <div class="banner" :style="style"></div>
<div class="body"> <div class="body">

View File

@ -7,7 +7,7 @@
kind: 'light', kind: 'light',
vars: { vars: {
primary: '#fb4e4e', primary: '#f18570',
secondary: '#fff', secondary: '#fff',
text: '#666', text: '#666',
}, },

View File

@ -29,7 +29,7 @@ export default function load() {
config.url = url.origin; 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.host = url.host;
mixin.hostname = url.hostname; mixin.hostname = url.hostname;

View File

@ -19,7 +19,7 @@ initDb().then(() => {
all, local all, local
}; };
process.send(stats); process.send!(stats);
} }
tick(); tick();

View File

@ -76,7 +76,7 @@ class MyCustomLogger implements Logger {
} }
export function initDb(justBorrow = false, sync = false, log = false) { export function initDb(justBorrow = false, sync = false, log = false) {
const enableLogging = log || !['production', 'test'].includes(process.env.NODE_ENV); const enableLogging = log || !['production', 'test'].includes(process.env.NODE_ENV || '');
try { try {
const conn = getConnection(); const conn = getConnection();
@ -93,7 +93,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
synchronize: process.env.NODE_ENV === 'test' || sync, synchronize: process.env.NODE_ENV === 'test' || sync,
dropSchema: process.env.NODE_ENV === 'test' && !justBorrow, dropSchema: process.env.NODE_ENV === 'test' && !justBorrow,
logging: enableLogging, logging: enableLogging,
logger: enableLogging ? new MyCustomLogger() : null, logger: enableLogging ? new MyCustomLogger() : undefined,
entities: [ entities: [
Meta, Meta,
Instance, Instance,

View File

@ -1,7 +1,7 @@
import * as redis from 'redis'; import * as redis from 'redis';
import config from '../config'; import config from '../config';
export default config.redis ? redis.createClient( export default redis.createClient(
config.redis.port, config.redis.port,
config.redis.host, config.redis.host,
{ {
@ -9,4 +9,4 @@ export default config.redis ? redis.createClient(
prefix: config.redis.prefix, prefix: config.redis.prefix,
db: config.redis.db || 0 db: config.redis.db || 0
} }
) : null; );

View File

@ -37,7 +37,7 @@ export type Undo = {
/** /**
* ターン * ターン
*/ */
turn: Color; turn: Color | null;
}; };
/** /**
@ -47,12 +47,12 @@ export default class Reversi {
public map: MapPixel[]; public map: MapPixel[];
public mapWidth: number; public mapWidth: number;
public mapHeight: number; public mapHeight: number;
public board: Color[]; public board: (Color | null | undefined)[];
public turn: Color = BLACK; public turn: Color | null = BLACK;
public opts: Options; public opts: Options;
public prevPos = -1; public prevPos = -1;
public prevColor: Color = null; public prevColor: Color | null = null;
private logs: Undo[] = []; private logs: Undo[] = [];
@ -145,12 +145,12 @@ export default class Reversi {
// ターン計算 // ターン計算
this.turn = this.turn =
this.canPutSomewhere(!this.prevColor) ? !this.prevColor : this.canPutSomewhere(!this.prevColor) ? !this.prevColor :
this.canPutSomewhere(this.prevColor) ? this.prevColor : this.canPutSomewhere(this.prevColor!) ? this.prevColor :
null; null;
} }
public undo() { public undo() {
const undo = this.logs.pop(); const undo = this.logs.pop()!;
this.prevColor = undo.color; this.prevColor = undo.color;
this.prevPos = undo.pos; this.prevPos = undo.pos;
this.board[undo.pos] = null; this.board[undo.pos] = null;
@ -254,10 +254,10 @@ export default class Reversi {
/** /**
* ゲームの勝者 (null = 引き分け) * ゲームの勝者 (null = 引き分け)
*/ */
public get winner(): Color { public get winner(): Color | null {
return this.isEnded ? return this.isEnded ?
this.blackCount == this.whiteCount ? null : this.blackCount == this.whiteCount ? null :
this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK : this.opts.isLlotheo === this.blackCount > this.whiteCount ? WHITE : BLACK :
undefined; undefined as never;
} }
} }

View File

@ -3,8 +3,6 @@ import { URL } from 'url';
import { urlRegex } from './prelude'; import { urlRegex } from './prelude';
export function fromHtml(html: string): string { export function fromHtml(html: string): string {
if (html == null) return null;
const dom = parseFragment(html) as DefaultTreeDocumentFragment; const dom = parseFragment(html) as DefaultTreeDocumentFragment;
let text = ''; let text = '';

View File

@ -2,7 +2,7 @@ import { mfmLanguage } from './language';
import { MfmForest } from './prelude'; import { MfmForest } from './prelude';
import { normalize } from './normalize'; import { normalize } from './normalize';
export function parse(source: string): MfmForest { export function parse(source: string | null): MfmForest | null {
if (source == null || source == '') { if (source == null || source == '') {
return null; return null;
} }
@ -10,7 +10,7 @@ export function parse(source: string): MfmForest {
return normalize(mfmLanguage.root.tryParse(source)); return normalize(mfmLanguage.root.tryParse(source));
} }
export function parsePlain(source: string): MfmForest { export function parsePlain(source: string | null): MfmForest | null {
if (source == null || source == '') { if (source == null || source == '') {
return null; return null;
} }

View File

@ -4,7 +4,7 @@ import { intersperse } from '../prelude/array';
import { MfmForest, MfmTree } from './prelude'; import { MfmForest, MfmTree } from './prelude';
import { IMentionedRemoteUsers } from '../models/entities/note'; import { IMentionedRemoteUsers } from '../models/entities/note';
export function toHtml(tokens: MfmForest, mentionedRemoteUsers: IMentionedRemoteUsers = []) { export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
if (tokens == null) { if (tokens == null) {
return null; return null;
} }

View File

@ -16,7 +16,6 @@ import { InternalStorage } from './services/drive/internal-storage';
import { createTemp } from './misc/create-temp'; import { createTemp } from './misc/create-temp';
import { Note } from './models/entities/note'; import { Note } from './models/entities/note';
import { Following } from './models/entities/following'; import { Following } from './models/entities/following';
import { genId } from './misc/gen-id';
import { Poll } from './models/entities/poll'; import { Poll } from './models/entities/poll';
import { PollVote } from './models/entities/poll-vote'; import { PollVote } from './models/entities/poll-vote';
import { NoteFavorite } from './models/entities/note-favorite'; import { NoteFavorite } from './models/entities/note-favorite';
@ -25,9 +24,14 @@ import { UserPublickey } from './models/entities/user-publickey';
import { UserKeypair } from './models/entities/user-keypair'; import { UserKeypair } from './models/entities/user-keypair';
import { extractPublic } from './crypto_key'; import { extractPublic } from './crypto_key';
import { Emoji } from './models/entities/emoji'; import { Emoji } from './models/entities/emoji';
import { toPuny } from './misc/convert-host'; import { toPuny as _toPuny } from './misc/convert-host';
import { UserProfile } from './models/entities/user-profile'; 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 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 p = (config as any).mongodb.pass ? encodeURIComponent((config as any).mongodb.pass) : null;
@ -36,6 +40,9 @@ const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${(config as any).mongodb.ho
const db = monk(uri); const db = monk(uri);
let mdb: mongo.Db; let mdb: mongo.Db;
const test = false;
const limit = 500;
const nativeDbConn = async (): Promise<mongo.Db> => { const nativeDbConn = async (): Promise<mongo.Db> => {
if (mdb) return mdb; if (mdb) return mdb;
@ -92,14 +99,14 @@ async function main() {
usernameLower: user.username.toLowerCase(), usernameLower: user.username.toLowerCase(),
host: toPuny(user.host), host: toPuny(user.host),
token: generateUserToken(), token: generateUserToken(),
isAdmin: user.isAdmin, isAdmin: user.isAdmin || false,
name: user.name, name: user.name,
followersCount: user.followersCount, followersCount: user.followersCount || 0,
followingCount: user.followingCount, followingCount: user.followingCount || 0,
notesCount: user.notesCount, notesCount: user.notesCount || 0,
isBot: user.isBot, isBot: user.isBot || false,
isCat: user.isCat, isCat: user.isCat || false,
isVerified: user.isVerified, isVerified: user.isVerified || false,
inbox: user.inbox, inbox: user.inbox,
sharedInbox: user.sharedInbox, sharedInbox: user.sharedInbox,
uri: user.uri, uri: user.uri,
@ -133,7 +140,7 @@ async function main() {
async function migrateFollowing(following: any) { async function migrateFollowing(following: any) {
await Followings.save({ await Followings.save({
id: following._id.toHexString(), id: following._id.toHexString(),
createdAt: following.createdAt || new Date(), createdAt: new Date(),
followerId: following.followerId.toHexString(), followerId: following.followerId.toHexString(),
followeeId: following.followeeId.toHexString(), followeeId: following.followeeId.toHexString(),
@ -160,6 +167,7 @@ async function main() {
const user = await _User.findOne({ const user = await _User.findOne({
_id: file.metadata.userId _id: file.metadata.userId
}); });
if (user == null) return;
if (file.metadata.storage && file.metadata.storage.key) { // when object storage if (file.metadata.storage && file.metadata.storage.key) { // when object storage
await DriveFiles.save({ await DriveFiles.save({
id: file._id.toHexString(), id: file._id.toHexString(),
@ -169,7 +177,7 @@ async function main() {
md5: file.md5, md5: file.md5,
name: file.filename, name: file.filename,
type: file.contentType, type: file.contentType,
properties: file.metadata.properties, properties: file.metadata.properties || {},
size: file.length, size: file.length,
url: file.metadata.url, url: file.metadata.url,
uri: file.metadata.uri, uri: file.metadata.uri,
@ -255,7 +263,6 @@ async function main() {
if (note.poll) { if (note.poll) {
await Polls.save({ await Polls.save({
id: genId(),
noteId: note._id.toHexString(), noteId: note._id.toHexString(),
choices: note.poll.choices.map((x: any) => x.text), choices: note.poll.choices.map((x: any) => x.text),
expiresAt: note.poll.expiresAt, expiresAt: note.poll.expiresAt,
@ -301,13 +308,13 @@ async function main() {
const u = await _User.findOne({ const u = await _User.findOne({
_id: new mongo.ObjectId(user.id) _id: new mongo.ObjectId(user.id)
}); });
const avatar = await DriveFiles.findOne(u.avatarId.toHexString()); const avatar = u.avatarId ? await DriveFiles.findOne(u.avatarId.toHexString()) : null;
const banner = await DriveFiles.findOne(u.bannerId.toHexString()); const banner = u.bannerId ? await DriveFiles.findOne(u.bannerId.toHexString()) : null;
await Users.update(user.id, { await Users.update(user.id, {
avatarId: avatar.id, avatarId: avatar ? avatar.id : null,
bannerId: banner.id, bannerId: banner ? banner.id : null,
avatarUrl: avatar.url, avatarUrl: avatar ? avatar.url : null,
bannerUrl: banner.url bannerUrl: banner ? banner.url : null
}); });
} }
@ -323,9 +330,14 @@ async function main() {
}); });
} }
const allUsersCount = await _User.count(); let allUsersCount = await _User.count({
deletedAt: { $exists: false }
});
if (test && allUsersCount > limit) allUsersCount = limit;
for (let i = 0; i < allUsersCount; i++) { for (let i = 0; i < allUsersCount; i++) {
const user = await _User.findOne({}, { const user = await _User.findOne({
deletedAt: { $exists: false }
}, {
skip: i skip: i
}); });
try { try {
@ -337,7 +349,8 @@ async function main() {
} }
} }
const allFollowingsCount = await _Following.count(); let allFollowingsCount = await _Following.count();
if (test && allFollowingsCount > limit) allFollowingsCount = limit;
for (let i = 0; i < allFollowingsCount; i++) { for (let i = 0; i < allFollowingsCount; i++) {
const following = await _Following.findOne({}, { const following = await _Following.findOne({}, {
skip: i skip: i
@ -351,7 +364,8 @@ async function main() {
} }
} }
const allDriveFoldersCount = await _DriveFolder.count(); let allDriveFoldersCount = await _DriveFolder.count();
if (test && allDriveFoldersCount > limit) allDriveFoldersCount = limit;
for (let i = 0; i < allDriveFoldersCount; i++) { for (let i = 0; i < allDriveFoldersCount; i++) {
const folder = await _DriveFolder.findOne({}, { const folder = await _DriveFolder.findOne({}, {
skip: i skip: i
@ -365,9 +379,16 @@ async function main() {
} }
} }
const allDriveFilesCount = await _DriveFile.count(); 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++) { for (let i = 0; i < allDriveFilesCount; i++) {
const file = await _DriveFile.findOne({}, { const file = await _DriveFile.findOne({
'metadata._user.host': null,
'metadata.deletedAt': { $exists: false }
}, {
skip: i skip: i
}); });
try { try {
@ -379,12 +400,15 @@ async function main() {
} }
} }
const allNotesCount = await _Note.count({ let allNotesCount = await _Note.count({
'_user.host': null '_user.host': null,
'metadata.deletedAt': { $exists: false }
}); });
if (test && allNotesCount > limit) allNotesCount = limit;
for (let i = 0; i < allNotesCount; i++) { for (let i = 0; i < allNotesCount; i++) {
const note = await _Note.findOne({ const note = await _Note.findOne({
'_user.host': null '_user.host': null,
'metadata.deletedAt': { $exists: false }
}, { }, {
skip: i skip: i
}); });
@ -397,7 +421,8 @@ async function main() {
} }
} }
const allPollVotesCount = await _PollVote.count(); let allPollVotesCount = await _PollVote.count();
if (test && allPollVotesCount > limit) allPollVotesCount = limit;
for (let i = 0; i < allPollVotesCount; i++) { for (let i = 0; i < allPollVotesCount; i++) {
const vote = await _PollVote.findOne({}, { const vote = await _PollVote.findOne({}, {
skip: i skip: i
@ -411,7 +436,8 @@ async function main() {
} }
} }
const allNoteFavoritesCount = await _Favorite.count(); let allNoteFavoritesCount = await _Favorite.count();
if (test && allNoteFavoritesCount > limit) allNoteFavoritesCount = limit;
for (let i = 0; i < allNoteFavoritesCount; i++) { for (let i = 0; i < allNoteFavoritesCount; i++) {
const favorite = await _Favorite.findOne({}, { const favorite = await _Favorite.findOne({}, {
skip: i skip: i
@ -425,7 +451,8 @@ async function main() {
} }
} }
const allNoteReactionsCount = await _NoteReaction.count(); let allNoteReactionsCount = await _NoteReaction.count();
if (test && allNoteReactionsCount > limit) allNoteReactionsCount = limit;
for (let i = 0; i < allNoteReactionsCount; i++) { for (let i = 0; i < allNoteReactionsCount; i++) {
const reaction = await _NoteReaction.findOne({}, { const reaction = await _NoteReaction.findOne({}, {
skip: i skip: i
@ -439,7 +466,8 @@ async function main() {
} }
} }
const allActualUsersCount = await Users.count(); let allActualUsersCount = await Users.count();
if (test && allActualUsersCount > limit) allActualUsersCount = limit;
for (let i = 0; i < allActualUsersCount; i++) { for (let i = 0; i < allActualUsersCount; i++) {
const [user] = await Users.find({ const [user] = await Users.find({
take: 1, take: 1,

View File

@ -1,6 +1,6 @@
type Acct = { type Acct = {
username: string; username: string;
host: string; host: string | null;
}; };
export default Acct; export default Acct;

View File

@ -1,26 +0,0 @@
// AID
// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さnの[ランダムな文字列]
const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
const TIME2000 = 946684800000;
function getTime(time: number) {
time = time - TIME2000;
if (time < 0) time = 0;
return time.toString(36);
}
function getRandom(length: number) {
let str = '';
for (let i = 0; i < length; i++) {
str += CHARS[Math.floor(Math.random() * CHARS.length)];
}
return str;
}
export function genAid(date: Date, rand: number): string {
return getTime(date.getTime()).padStart(8, CHARS[0]) + getRandom(rand);
}

View File

@ -1,26 +0,0 @@
// AID(Cheep)
// 長さ6の[2000年1月1日からの経過秒をbase36でエンコードしたもの] + 長さ3の[ランダムな文字列]
const CHARS = '0123456789abcdefghijklmnopqrstuvwxyz';
const TIME2000 = 946684800000;
function getTime(time: number) {
time = time - TIME2000;
if (time < 0) time = 0;
time = Math.floor(time / 1000);
return time.toString(36);
}
function getRandom() {
let str = '';
for (let i = 0; i < 3; i++) {
str += CHARS[Math.floor(Math.random() * CHARS.length)];
}
return str;
}
export function genAidc(date: Date): string {
return getTime(date.getTime()).padStart(6, CHARS[0]) + getRandom();
}

View File

@ -2,7 +2,7 @@ import config from '../config';
import { toASCII } from 'punycode'; import { toASCII } from 'punycode';
import { URL } from 'url'; import { URL } from 'url';
export function getFullApAccount(username: string, host: string) { export function getFullApAccount(username: string, host: string | null) {
return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`; return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`;
} }
@ -17,6 +17,10 @@ export function extractDbHost(uri: string) {
} }
export function toPuny(host: string) { export function toPuny(host: string) {
return toASCII(host.toLowerCase());
}
export function toPunyNullable(host: string | null | undefined): string | null {
if (host == null) return null; if (host == null) return null;
return toASCII(host.toLowerCase()); return toASCII(host.toLowerCase());
} }

View File

@ -3,7 +3,7 @@ import fileType from 'file-type';
import checkSvg from '../misc/check-svg'; import checkSvg from '../misc/check-svg';
export async function detectMine(path: string) { 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); const readable = fs.createReadStream(path);
readable readable
.on('error', rej) .on('error', rej)

View File

@ -1,5 +1,4 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as URL from 'url';
import * as request from 'request'; import * as request from 'request';
import config from '../config'; import config from '../config';
import chalk from 'chalk'; import chalk from 'chalk';
@ -26,7 +25,7 @@ export async function downloadUrl(url: string, path: string) {
rej(error); 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({ const req = request({
url: requestUrl, url: requestUrl,

View File

@ -9,7 +9,6 @@ export default async function(): Promise<Meta> {
} else { } else {
return Metas.save({ return Metas.save({
id: genId(), id: genId(),
hiddenTags: []
} as Meta); } as Meta);
} }
} }

View File

@ -1,8 +1,9 @@
import fetchMeta from './fetch-meta'; import fetchMeta from './fetch-meta';
import { ILocalUser } from '../models/entities/user'; import { ILocalUser } from '../models/entities/user';
import { Users } from '../models'; import { Users } from '../models';
import { ensure } from '../prelude/ensure';
export async function fetchProxyAccount(): Promise<ILocalUser> { export async function fetchProxyAccount(): Promise<ILocalUser> {
const meta = await fetchMeta(); const meta = await fetchMeta();
return await Users.findOne({ username: meta.proxyAccount, host: null }) as ILocalUser; return await Users.findOne({ username: meta.proxyAccount!, host: null }).then(ensure) as ILocalUser;
} }

View File

@ -1,7 +1,7 @@
import { ulid } from 'ulid'; import { ulid } from 'ulid';
import { genAid } from './aid'; import { genAid } from './id/aid';
import { genAidc } from './aidc'; import { genMeid } from './id/meid';
import { genObjectId } from './object-id'; import { genObjectId } from './id/object-id';
import config from '../config'; import config from '../config';
const metohd = config.id.toLowerCase(); const metohd = config.id.toLowerCase();
@ -10,11 +10,8 @@ export function genId(date?: Date): string {
if (!date || (date > new Date())) date = new Date(); if (!date || (date > new Date())) date = new Date();
switch (metohd) { switch (metohd) {
case 'aidc': return genAidc(date); case 'aid': return genAid(date);
case 'aid1': return genAid(date, 1); case 'meid': return genMeid(date);
case 'aid2': return genAid(date, 2);
case 'aid3': return genAid(date, 3);
case 'aid4': return genAid(date, 4);
case 'ulid': return ulid(date.getTime()); case 'ulid': return ulid(date.getTime());
case 'objectid': return genObjectId(date); case 'objectid': return genObjectId(date);
default: throw 'unknown id generation method'; default: throw 'unknown id generation method';

23
src/misc/id/aid.ts Normal file
View File

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

26
src/misc/id/meid.ts Normal file
View File

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

View File

@ -8,7 +8,7 @@ function getTime(time: number) {
time = Math.floor(time / 1000); time = Math.floor(time / 1000);
return time.toString(16); return time.toString(16).padStart(8, CHARS[0]);
} }
function getRandom() { function getRandom() {

View File

@ -7,7 +7,7 @@ export class IdentifiableError extends Error {
constructor(id: string, message?: string) { constructor(id: string, message?: string) {
super(message); super(message);
this.message = message; this.message = message || '';
this.id = id; this.id = id;
} }
} }

View File

@ -3,7 +3,7 @@ export function nyaize(text: string): string {
// ja-JP // ja-JP
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ') .replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
// ko-KR // ko-KR
.replace(/[나-낳]/g, (match: string) => String.fromCharCode( .replace(/[나-낳]/g, match => String.fromCharCode(
match.codePointAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0) match.codePointAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
)); ));
} }

View File

@ -20,7 +20,7 @@ export async function getFallbackReaction(): Promise<string> {
return meta.useStarForReactionFallback ? 'star' : 'like'; 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(); if (reaction == null) return await getFallbackReaction();
// 既存の文字列リアクションはそのまま // 既存の文字列リアクションはそのまま

View File

@ -19,8 +19,8 @@ type MyType<T extends Schema> = {
export type SchemaType<p extends Schema> = export type SchemaType<p extends Schema> =
p['type'] extends 'number' ? number : p['type'] extends 'number' ? number :
p['type'] extends 'string' ? string : p['type'] extends 'string' ? string :
p['type'] extends 'array' ? MyType<p['items']>[] : p['type'] extends 'array' ? MyType<NonNullable<p['items']>>[] :
p['type'] extends 'object' ? ObjType<p['properties']> : p['type'] extends 'object' ? ObjType<NonNullable<p['properties']>> :
any; any;
export function convertOpenApiSchema(schema: Schema) { export function convertOpenApiSchema(schema: Schema) {

View File

@ -53,7 +53,7 @@ export class App {
public permission: string[]; public permission: string[];
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The callbackUrl of the App.' comment: 'The callbackUrl of the App.'
}) })
public callbackUrl: string | null; public callbackUrl: string | null;

View File

@ -19,11 +19,15 @@ export class AuthSession {
}) })
public token: string; public token: string;
@Column(id()) @Column({
...id(),
nullable: true
})
public userId: User['id']; public userId: User['id'];
@ManyToOne(type => User, { @ManyToOne(type => User, {
onDelete: 'CASCADE' onDelete: 'CASCADE',
nullable: true
}) })
@JoinColumn() @JoinColumn()
public user: User | null; public user: User | null;

View File

@ -25,12 +25,12 @@ export class Emoji {
public host: string | null; public host: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, length: 512,
}) })
public url: string; public url: string;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true length: 512, nullable: true
}) })
public uri: string | null; public uri: string | null;

View File

@ -53,13 +53,13 @@ export class FollowRequest {
public followerHost: string | null; public followerHost: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: '[Denormalized]' comment: '[Denormalized]'
}) })
public followerInbox: string | null; public followerInbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: '[Denormalized]' comment: '[Denormalized]'
}) })
public followerSharedInbox: string | null; public followerSharedInbox: string | null;
@ -71,13 +71,13 @@ export class FollowRequest {
public followeeHost: string | null; public followeeHost: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: '[Denormalized]' comment: '[Denormalized]'
}) })
public followeeInbox: string | null; public followeeInbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: '[Denormalized]' comment: '[Denormalized]'
}) })
public followeeSharedInbox: string | null; public followeeSharedInbox: string | null;

View File

@ -48,13 +48,13 @@ export class Following {
public followerHost: string | null; public followerHost: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: '[Denormalized]' comment: '[Denormalized]'
}) })
public followerInbox: string | null; public followerInbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: '[Denormalized]' comment: '[Denormalized]'
}) })
public followerSharedInbox: string | null; public followerSharedInbox: string | null;
@ -66,13 +66,13 @@ export class Following {
public followeeHost: string | null; public followeeHost: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: '[Denormalized]' comment: '[Denormalized]'
}) })
public followeeInbox: string | null; public followeeInbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: '[Denormalized]' comment: '[Denormalized]'
}) })
public followeeSharedInbox: string | null; public followeeSharedInbox: string | null;

View File

@ -78,27 +78,27 @@ export class Meta {
public blockedHosts: string[]; public blockedHosts: string[];
@Column('varchar', { @Column('varchar', {
length: 256, length: 512,
nullable: true, nullable: true,
default: '/assets/ai.png' default: '/assets/ai.png'
}) })
public mascotImageUrl: string | null; public mascotImageUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, length: 512,
nullable: true nullable: true
}) })
public bannerUrl: string | null; public bannerUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, length: 512,
nullable: true, nullable: true,
default: 'https://ai.misskey.xyz/aiart/yubitun.png' default: 'https://ai.misskey.xyz/aiart/yubitun.png'
}) })
public errorImageUrl: string | null; public errorImageUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, length: 512,
nullable: true nullable: true
}) })
public iconUrl: string | null; public iconUrl: string | null;

View File

@ -15,13 +15,6 @@ export class Note {
}) })
public createdAt: Date; public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true,
comment: 'The updated date of the Note.'
})
public updatedAt: Date | null;
@Index() @Index()
@Column({ @Column({
...id(), ...id(),
@ -126,7 +119,7 @@ export class Note {
@Index({ unique: true }) @Index({ unique: true })
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The URI of a note. it will be null when the note is local.' comment: 'The URI of a note. it will be null when the note is local.'
}) })
public uri: string | null; public uri: string | null;
@ -195,12 +188,6 @@ export class Note {
}) })
public userHost: string | null; public userHost: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userInbox: string | null;
@Column({ @Column({
...id(), ...id(),
nullable: true, nullable: true,
@ -227,6 +214,14 @@ export class Note {
}) })
public renoteUserHost: string | null; public renoteUserHost: string | null;
//#endregion //#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 = { export type IMentionedRemoteUsers = {

View File

@ -53,11 +53,19 @@ export class Poll {
}) })
public userHost: string | null; public userHost: string | null;
//#endregion //#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 = { export type IPoll = {
choices: string[]; choices: string[];
votes?: number[]; votes?: number[];
multiple: boolean; multiple: boolean;
expiresAt: Date; expiresAt: Date | null;
}; };

View File

@ -21,7 +21,7 @@ export class SwSubscription {
public user: User | null; public user: User | null;
@Column('varchar', { @Column('varchar', {
length: 256, length: 512,
}) })
public endpoint: string; public endpoint: string;

View File

@ -22,4 +22,12 @@ export class UserKeypair {
length: 4096, length: 4096,
}) })
public privateKey: string; public privateKey: string;
constructor(data: Partial<UserKeypair>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
} }

View File

@ -39,6 +39,12 @@ export class UserProfile {
value: string; value: string;
}[]; }[];
@Column('varchar', {
length: 512, nullable: true,
comment: 'Remote URL of the user.'
})
public url: string | null;
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 128, nullable: true,
comment: 'The email address of the User.' comment: 'The email address of the User.'
@ -192,4 +198,12 @@ export class UserProfile {
}) })
public userHost: string | null; public userHost: string | null;
//#endregion //#endregion
constructor(data: Partial<UserProfile>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
} }

View File

@ -23,4 +23,12 @@ export class UserPublickey {
length: 4096, length: 4096,
}) })
public keyPem: string; public keyPem: string;
constructor(data: Partial<UserPublickey>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
} }

View File

@ -96,12 +96,12 @@ export class User {
public tags: string[]; public tags: string[];
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
}) })
public avatarUrl: string | null; public avatarUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
}) })
public bannerUrl: string | null; public bannerUrl: string | null;
@ -175,26 +175,26 @@ export class User {
public host: string | null; public host: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The inbox of the User. It will be null if the origin of the user is local.' comment: 'The inbox URL of the User. It will be null if the origin of the user is local.'
}) })
public inbox: string | null; public inbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The sharedInbox of the User. It will be null if the origin of the user is local.' comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'
}) })
public sharedInbox: string | null; public sharedInbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The featured of the User. It will be null if the origin of the user is local.' comment: 'The featured URL of the User. It will be null if the origin of the user is local.'
}) })
public featured: string | null; public featured: string | null;
@Index() @Index()
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The URI of the User. It will be null if the origin of the user is local.' comment: 'The URI of the User. It will be null if the origin of the user is local.'
}) })
public uri: string | null; public uri: string | null;
@ -205,6 +205,14 @@ export class User {
comment: 'The native access token of the User. It will be null if the origin of the user is local.' comment: 'The native access token of the User. It will be null if the origin of the user is local.'
}) })
public token: string | null; 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 { export interface ILocalUser extends User {

View File

@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { Users } from '..'; import { Users } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { AbuseUserReport } from '../entities/abuse-user-report'; import { AbuseUserReport } from '../entities/abuse-user-report';
import { ensure } from '../../prelude/ensure';
@EntityRepository(AbuseUserReport) @EntityRepository(AbuseUserReport)
export class AbuseUserReportRepository extends Repository<AbuseUserReport> { export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
@ -14,7 +15,7 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
public async pack( public async pack(
src: AbuseUserReport['id'] | AbuseUserReport, src: AbuseUserReport['id'] | AbuseUserReport,
) { ) {
const report = typeof src === 'object' ? src : await this.findOne(src); const report = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return await rap({ return await rap({
id: report.id, id: report.id,

View File

@ -1,6 +1,7 @@
import { EntityRepository, Repository } from 'typeorm'; import { EntityRepository, Repository } from 'typeorm';
import { App } from '../entities/app'; import { App } from '../entities/app';
import { AccessTokens } from '..'; import { AccessTokens } from '..';
import { ensure } from '../../prelude/ensure';
@EntityRepository(App) @EntityRepository(App)
export class AppRepository extends Repository<App> { export class AppRepository extends Repository<App> {
@ -19,7 +20,7 @@ export class AppRepository extends Repository<App> {
includeProfileImageIds: false includeProfileImageIds: false
}, options); }, options);
const app = typeof src === 'object' ? src : await this.findOne(src); const app = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return { return {
id: app.id, id: app.id,

View File

@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { Apps } from '..'; import { Apps } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { AuthSession } from '../entities/auth-session'; import { AuthSession } from '../entities/auth-session';
import { ensure } from '../../prelude/ensure';
@EntityRepository(AuthSession) @EntityRepository(AuthSession)
export class AuthSessionRepository extends Repository<AuthSession> { export class AuthSessionRepository extends Repository<AuthSession> {
@ -9,7 +10,7 @@ export class AuthSessionRepository extends Repository<AuthSession> {
src: AuthSession['id'] | AuthSession, src: AuthSession['id'] | AuthSession,
me?: any me?: any
) { ) {
const session = typeof src === 'object' ? src : await this.findOne(src); const session = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return await rap({ return await rap({
id: session.id, id: session.id,

View File

@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { Users } from '..'; import { Users } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { Blocking } from '../entities/blocking'; import { Blocking } from '../entities/blocking';
import { ensure } from '../../prelude/ensure';
@EntityRepository(Blocking) @EntityRepository(Blocking)
export class BlockingRepository extends Repository<Blocking> { export class BlockingRepository extends Repository<Blocking> {
@ -16,7 +17,7 @@ export class BlockingRepository extends Repository<Blocking> {
src: Blocking['id'] | Blocking, src: Blocking['id'] | Blocking,
me?: any me?: any
) { ) {
const blocking = typeof src === 'object' ? src : await this.findOne(src); const blocking = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return await rap({ return await rap({
id: blocking.id, id: blocking.id,

View File

@ -4,6 +4,7 @@ import { Users, DriveFolders } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { User } from '../entities/user'; import { User } from '../entities/user';
import { toPuny } from '../../misc/convert-host'; import { toPuny } from '../../misc/convert-host';
import { ensure } from '../../prelude/ensure';
@EntityRepository(DriveFile) @EntityRepository(DriveFile)
export class DriveFileRepository extends Repository<DriveFile> { export class DriveFileRepository extends Repository<DriveFile> {
@ -91,7 +92,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
self: false self: false
}, options); }, options);
const file = typeof src === 'object' ? src : await this.findOne(src); const file = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return await rap({ return await rap({
id: file.id, id: file.id,
@ -108,7 +109,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, { folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
detail: true detail: true
}) : null, }) : null,
user: opts.withUser ? Users.pack(file.userId) : null user: opts.withUser ? Users.pack(file.userId!) : null
}); });
} }
} }

View File

@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { DriveFolders, DriveFiles } from '..'; import { DriveFolders, DriveFiles } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { DriveFolder } from '../entities/drive-folder'; import { DriveFolder } from '../entities/drive-folder';
import { ensure } from '../../prelude/ensure';
@EntityRepository(DriveFolder) @EntityRepository(DriveFolder)
export class DriveFolderRepository extends Repository<DriveFolder> { export class DriveFolderRepository extends Repository<DriveFolder> {
@ -22,7 +23,7 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
detail: false detail: false
}, options); }, options);
const folder = typeof src === 'object' ? src : await this.findOne(src); const folder = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return await rap({ return await rap({
id: folder.id, id: folder.id,

View File

@ -1,6 +1,7 @@
import { EntityRepository, Repository } from 'typeorm'; import { EntityRepository, Repository } from 'typeorm';
import { FollowRequest } from '../entities/follow-request'; import { FollowRequest } from '../entities/follow-request';
import { Users } from '..'; import { Users } from '..';
import { ensure } from '../../prelude/ensure';
@EntityRepository(FollowRequest) @EntityRepository(FollowRequest)
export class FollowRequestRepository extends Repository<FollowRequest> { export class FollowRequestRepository extends Repository<FollowRequest> {
@ -8,7 +9,7 @@ export class FollowRequestRepository extends Repository<FollowRequest> {
src: FollowRequest['id'] | FollowRequest, src: FollowRequest['id'] | FollowRequest,
me?: any me?: any
) { ) {
const request = typeof src === 'object' ? src : await this.findOne(src); const request = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return { return {
id: request.id, id: request.id,

View File

@ -2,9 +2,50 @@ import { EntityRepository, Repository } from 'typeorm';
import { Users } from '..'; import { Users } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { Following } from '../entities/following'; 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) @EntityRepository(Following)
export class FollowingRepository extends Repository<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( public packMany(
followings: any[], followings: any[],
me?: any, me?: any,
@ -24,7 +65,7 @@ export class FollowingRepository extends Repository<Following> {
populateFollower?: boolean; populateFollower?: boolean;
} }
) { ) {
const following = typeof src === 'object' ? src : await this.findOne(src); const following = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
if (opts == null) opts = {}; if (opts == null) opts = {};

View File

@ -1,6 +1,7 @@
import { EntityRepository, Repository } from 'typeorm'; import { EntityRepository, Repository } from 'typeorm';
import { Users } from '../../..'; import { Users } from '../../..';
import { ReversiGame } from '../../../entities/games/reversi/game'; import { ReversiGame } from '../../../entities/games/reversi/game';
import { ensure } from '../../../../prelude/ensure';
@EntityRepository(ReversiGame) @EntityRepository(ReversiGame)
export class ReversiGameRepository extends Repository<ReversiGame> { export class ReversiGameRepository extends Repository<ReversiGame> {
@ -15,7 +16,7 @@ export class ReversiGameRepository extends Repository<ReversiGame> {
detail: true detail: true
}, options); }, options);
const game = typeof src === 'object' ? src : await this.findOne(src); const game = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const meId = me ? typeof me === 'string' ? me : me.id : null; const meId = me ? typeof me === 'string' ? me : me.id : null;
return { return {

View File

@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { ReversiMatching } from '../../../entities/games/reversi/matching'; import { ReversiMatching } from '../../../entities/games/reversi/matching';
import { Users } from '../../..'; import { Users } from '../../..';
import { ensure } from '../../../../prelude/ensure';
@EntityRepository(ReversiMatching) @EntityRepository(ReversiMatching)
export class ReversiMatchingRepository extends Repository<ReversiMatching> { export class ReversiMatchingRepository extends Repository<ReversiMatching> {
@ -9,7 +10,7 @@ export class ReversiMatchingRepository extends Repository<ReversiMatching> {
src: ReversiMatching['id'] | ReversiMatching, src: ReversiMatching['id'] | ReversiMatching,
me: any me: any
) { ) {
const matching = typeof src === 'object' ? src : await this.findOne(src); const matching = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return await rap({ return await rap({
id: matching.id, id: matching.id,

View File

@ -1,6 +1,7 @@
import { EntityRepository, Repository } from 'typeorm'; import { EntityRepository, Repository } from 'typeorm';
import { MessagingMessage } from '../entities/messaging-message'; import { MessagingMessage } from '../entities/messaging-message';
import { Users, DriveFiles } from '..'; import { Users, DriveFiles } from '..';
import { ensure } from '../../prelude/ensure';
@EntityRepository(MessagingMessage) @EntityRepository(MessagingMessage)
export class MessagingMessageRepository extends Repository<MessagingMessage> { export class MessagingMessageRepository extends Repository<MessagingMessage> {
@ -19,7 +20,7 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
populateRecipient: true populateRecipient: true
}; };
const message = typeof src === 'object' ? src : await this.findOne(src); const message = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return { return {
id: message.id, id: message.id,

View File

@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { Users } from '..'; import { Users } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { Muting } from '../entities/muting'; import { Muting } from '../entities/muting';
import { ensure } from '../../prelude/ensure';
@EntityRepository(Muting) @EntityRepository(Muting)
export class MutingRepository extends Repository<Muting> { export class MutingRepository extends Repository<Muting> {
@ -16,7 +17,7 @@ export class MutingRepository extends Repository<Muting> {
src: Muting['id'] | Muting, src: Muting['id'] | Muting,
me?: any me?: any
) { ) {
const muting = typeof src === 'object' ? src : await this.findOne(src); const muting = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return await rap({ return await rap({
id: muting.id, id: muting.id,

View File

@ -1,6 +1,7 @@
import { EntityRepository, Repository } from 'typeorm'; import { EntityRepository, Repository } from 'typeorm';
import { NoteFavorite } from '../entities/note-favorite'; import { NoteFavorite } from '../entities/note-favorite';
import { Notes } from '..'; import { Notes } from '..';
import { ensure } from '../../prelude/ensure';
@EntityRepository(NoteFavorite) @EntityRepository(NoteFavorite)
export class NoteFavoriteRepository extends Repository<NoteFavorite> { export class NoteFavoriteRepository extends Repository<NoteFavorite> {
@ -15,7 +16,7 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
src: NoteFavorite['id'] | NoteFavorite, src: NoteFavorite['id'] | NoteFavorite,
me?: any me?: any
) { ) {
const favorite = typeof src === 'object' ? src : await this.findOne(src); const favorite = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return { return {
id: favorite.id, id: favorite.id,

View File

@ -1,6 +1,7 @@
import { EntityRepository, Repository } from 'typeorm'; import { EntityRepository, Repository } from 'typeorm';
import { NoteReaction } from '../entities/note-reaction'; import { NoteReaction } from '../entities/note-reaction';
import { Users } from '..'; import { Users } from '..';
import { ensure } from '../../prelude/ensure';
@EntityRepository(NoteReaction) @EntityRepository(NoteReaction)
export class NoteReactionRepository extends Repository<NoteReaction> { export class NoteReactionRepository extends Repository<NoteReaction> {
@ -8,7 +9,7 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
src: NoteReaction['id'] | NoteReaction, src: NoteReaction['id'] | NoteReaction,
me?: any me?: any
) { ) {
const reaction = typeof src === 'object' ? src : await this.findOne(src); const reaction = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return { return {
id: reaction.id, id: reaction.id,

View File

@ -5,6 +5,7 @@ import { unique, concat } from '../../prelude/array';
import { nyaize } from '../../misc/nyaize'; import { nyaize } from '../../misc/nyaize';
import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..'; import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { ensure } from '../../prelude/ensure';
@EntityRepository(Note) @EntityRepository(Note)
export class NoteRepository extends Repository<Note> { export class NoteRepository extends Repository<Note> {
@ -12,7 +13,7 @@ export class NoteRepository extends Repository<Note> {
return x.trim().length <= 100; return x.trim().length <= 100;
} }
private async hideNote(packedNote: any, meId: User['id']) { private async hideNote(packedNote: any, meId: User['id'] | null) {
let hide = false; let hide = false;
// visibility が specified かつ自分が指定されていなかったら非表示 // visibility が specified かつ自分が指定されていなかったら非表示
@ -75,7 +76,7 @@ export class NoteRepository extends Repository<Note> {
public packMany( public packMany(
notes: (Note['id'] | Note)[], notes: (Note['id'] | Note)[],
me?: User['id'] | User, me?: User['id'] | User | null | undefined,
options?: { options?: {
detail?: boolean; detail?: boolean;
skipHide?: boolean; skipHide?: boolean;
@ -86,7 +87,7 @@ export class NoteRepository extends Repository<Note> {
public async pack( public async pack(
src: Note['id'] | Note, src: Note['id'] | Note,
me?: User['id'] | User, me?: User['id'] | User | null | undefined,
options?: { options?: {
detail?: boolean; detail?: boolean;
skipHide?: boolean; skipHide?: boolean;
@ -98,11 +99,11 @@ export class NoteRepository extends Repository<Note> {
}, options); }, options);
const meId = me ? typeof me === 'string' ? me : me.id : null; const meId = me ? typeof me === 'string' ? me : me.id : null;
const note = typeof src === 'object' ? src : await this.findOne(src); const note = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const host = note.userHost; const host = note.userHost;
async function populatePoll() { async function populatePoll() {
const poll = await Polls.findOne({ noteId: note.id }); const poll = await Polls.findOne({ noteId: note.id }).then(ensure);
const choices = poll.choices.map(c => ({ const choices = poll.choices.map(c => ({
text: c, text: c,
votes: poll.votes[poll.choices.indexOf(c)], votes: poll.votes[poll.choices.indexOf(c)],
@ -111,7 +112,7 @@ export class NoteRepository extends Repository<Note> {
if (poll.multiple) { if (poll.multiple) {
const votes = await PollVotes.find({ const votes = await PollVotes.find({
userId: meId, userId: meId!,
noteId: note.id noteId: note.id
}); });
@ -121,7 +122,7 @@ export class NoteRepository extends Repository<Note> {
} }
} else { } else {
const vote = await PollVotes.findOne({ const vote = await PollVotes.findOne({
userId: meId, userId: meId!,
noteId: note.id noteId: note.id
}); });
@ -139,7 +140,7 @@ export class NoteRepository extends Repository<Note> {
async function populateMyReaction() { async function populateMyReaction() {
const reaction = await NoteReactions.findOne({ const reaction = await NoteReactions.findOne({
userId: meId, userId: meId!,
noteId: note.id, noteId: note.id,
}); });
@ -147,7 +148,7 @@ export class NoteRepository extends Repository<Note> {
return reaction.reaction; return reaction.reaction;
} }
return null; return undefined;
} }
let text = note.text; let text = note.text;
@ -161,15 +162,15 @@ export class NoteRepository extends Repository<Note> {
const packed = await rap({ const packed = await rap({
id: note.id, id: note.id,
createdAt: note.createdAt, createdAt: note.createdAt,
app: note.appId ? Apps.pack(note.appId) : null, app: note.appId ? Apps.pack(note.appId) : undefined,
userId: note.userId, userId: note.userId,
user: Users.pack(note.user || note.userId, meId), user: Users.pack(note.user || note.userId, meId),
text: text, text: text,
cw: note.cw, cw: note.cw,
visibility: note.visibility, visibility: note.visibility,
localOnly: note.localOnly, localOnly: note.localOnly || undefined,
visibleUserIds: note.visibleUserIds, visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
viaMobile: note.viaMobile, viaMobile: note.viaMobile || undefined,
renoteCount: note.renoteCount, renoteCount: note.renoteCount,
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactions: note.reactions, reactions: note.reactions,
@ -182,17 +183,18 @@ export class NoteRepository extends Repository<Note> {
files: DriveFiles.packMany(note.fileIds), files: DriveFiles.packMany(note.fileIds),
replyId: note.replyId, replyId: note.replyId,
renoteId: note.renoteId, renoteId: note.renoteId,
uri: note.uri,
...(opts.detail ? { ...(opts.detail ? {
reply: note.replyId ? this.pack(note.replyId, meId, { reply: note.replyId ? this.pack(note.replyId, meId, {
detail: false detail: false
}) : null, }) : undefined,
renote: note.renoteId ? this.pack(note.renoteId, meId, { renote: note.renoteId ? this.pack(note.renoteId, meId, {
detail: true detail: true
}) : null, }) : undefined,
poll: note.hasPoll ? populatePoll() : null, poll: note.hasPoll ? populatePoll() : undefined,
...(meId ? { ...(meId ? {
myReaction: populateMyReaction() myReaction: populateMyReaction()

View File

@ -2,6 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
import { Users, Notes } from '..'; import { Users, Notes } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { Notification } from '../entities/notification'; import { Notification } from '../entities/notification';
import { ensure } from '../../prelude/ensure';
@EntityRepository(Notification) @EntityRepository(Notification)
export class NotificationRepository extends Repository<Notification> { export class NotificationRepository extends Repository<Notification> {
@ -14,7 +15,7 @@ export class NotificationRepository extends Repository<Notification> {
public async pack( public async pack(
src: Notification['id'] | Notification, src: Notification['id'] | Notification,
) { ) {
const notification = typeof src === 'object' ? src : await this.findOne(src); const notification = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
return await rap({ return await rap({
id: notification.id, id: notification.id,
@ -23,23 +24,23 @@ export class NotificationRepository extends Repository<Notification> {
userId: notification.notifierId, userId: notification.notifierId,
user: Users.pack(notification.notifier || notification.notifierId), user: Users.pack(notification.notifier || notification.notifierId),
...(notification.type === 'mention' ? { ...(notification.type === 'mention' ? {
note: Notes.pack(notification.note || notification.noteId), note: Notes.pack(notification.note || notification.noteId!),
} : {}), } : {}),
...(notification.type === 'reply' ? { ...(notification.type === 'reply' ? {
note: Notes.pack(notification.note || notification.noteId), note: Notes.pack(notification.note || notification.noteId!),
} : {}), } : {}),
...(notification.type === 'renote' ? { ...(notification.type === 'renote' ? {
note: Notes.pack(notification.note || notification.noteId), note: Notes.pack(notification.note || notification.noteId!),
} : {}), } : {}),
...(notification.type === 'quote' ? { ...(notification.type === 'quote' ? {
note: Notes.pack(notification.note || notification.noteId), note: Notes.pack(notification.note || notification.noteId!),
} : {}), } : {}),
...(notification.type === 'reaction' ? { ...(notification.type === 'reaction' ? {
note: Notes.pack(notification.note || notification.noteId), note: Notes.pack(notification.note || notification.noteId!),
reaction: notification.reaction reaction: notification.reaction
} : {}), } : {}),
...(notification.type === 'pollVote' ? { ...(notification.type === 'pollVote' ? {
note: Notes.pack(notification.note || notification.noteId), note: Notes.pack(notification.note || notification.noteId!),
choice: notification.choice choice: notification.choice
} : {}) } : {})
}); });

View File

@ -1,16 +1,23 @@
import { EntityRepository, Repository } from 'typeorm'; import { EntityRepository, Repository } from 'typeorm';
import { UserList } from '../entities/user-list'; import { UserList } from '../entities/user-list';
import { ensure } from '../../prelude/ensure';
import { UserListJoinings } from '..';
@EntityRepository(UserList) @EntityRepository(UserList)
export class UserListRepository extends Repository<UserList> { export class UserListRepository extends Repository<UserList> {
public async pack( public async pack(
src: any, src: UserList['id'] | UserList,
) { ) {
const userList = typeof src === 'object' ? src : await this.findOne(src); const userList = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const users = await UserListJoinings.find({
userListId: userList.id
});
return { return {
id: userList.id, id: userList.id,
name: userList.name name: userList.name,
userIds: users.map(x => x.userId)
}; };
} }
} }

View File

@ -2,6 +2,7 @@ import { EntityRepository, Repository, In } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user'; import { User, ILocalUser, IRemoteUser } from '../entities/user';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..'; import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { ensure } from '../../prelude/ensure';
@EntityRepository(User) @EntityRepository(User)
export class UserRepository extends Repository<User> { export class UserRepository extends Repository<User> {
@ -51,7 +52,7 @@ export class UserRepository extends Repository<User> {
public packMany( public packMany(
users: (User['id'] | User)[], users: (User['id'] | User)[],
me?: User['id'] | User, me?: User['id'] | User | null | undefined,
options?: { options?: {
detail?: boolean, detail?: boolean,
includeSecrets?: boolean, includeSecrets?: boolean,
@ -63,7 +64,7 @@ export class UserRepository extends Repository<User> {
public async pack( public async pack(
src: User['id'] | User, src: User['id'] | User,
me?: User['id'] | User, me?: User['id'] | User | null | undefined,
options?: { options?: {
detail?: boolean, detail?: boolean,
includeSecrets?: boolean, includeSecrets?: boolean,
@ -75,26 +76,24 @@ export class UserRepository extends Repository<User> {
includeSecrets: false includeSecrets: false
}, options); }, options);
const user = typeof src === 'object' ? src : await this.findOne(src); const user = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
const meId = me ? typeof me === 'string' ? me : me.id : null; 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 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 pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : [];
const profile = opts.detail ? await UserProfiles.findOne({ userId: user.id }) : null; const profile = opts.detail ? await UserProfiles.findOne({ userId: user.id }).then(ensure) : null;
return await rap({ return await rap({
id: user.id, id: user.id,
name: user.name, name: user.name,
username: user.username, username: user.username,
host: user.host, host: user.host,
uri: user.uri,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
bannerUrl: user.bannerUrl,
avatarColor: user.avatarColor, avatarColor: user.avatarColor,
bannerColor: user.bannerColor, isAdmin: user.isAdmin || undefined,
isAdmin: user.isAdmin, isBot: user.isBot || undefined,
isBot: user.isBot, isCat: user.isCat || undefined,
isCat: user.isCat, isVerified: user.isVerified || undefined,
// カスタム絵文字添付 // カスタム絵文字添付
emojis: user.emojis.length > 0 ? Emojis.find({ emojis: user.emojis.length > 0 ? Emojis.find({
@ -117,9 +116,14 @@ export class UserRepository extends Repository<User> {
} : {}), } : {}),
...(opts.detail ? { ...(opts.detail ? {
description: profile.description, url: profile!.url,
location: profile.location, createdAt: user.createdAt,
birthday: profile.birthday, updatedAt: user.updatedAt,
bannerUrl: user.bannerUrl,
bannerColor: user.bannerColor,
description: profile!.description,
location: profile!.location,
birthday: profile!.birthday,
followersCount: user.followersCount, followersCount: user.followersCount,
followingCount: user.followingCount, followingCount: user.followingCount,
notesCount: user.notesCount, notesCount: user.notesCount,
@ -132,9 +136,9 @@ export class UserRepository extends Repository<User> {
...(opts.detail && meId === user.id ? { ...(opts.detail && meId === user.id ? {
avatarId: user.avatarId, avatarId: user.avatarId,
bannerId: user.bannerId, bannerId: user.bannerId,
autoWatch: profile.autoWatch, autoWatch: profile!.autoWatch,
alwaysMarkNsfw: profile.alwaysMarkNsfw, alwaysMarkNsfw: profile!.alwaysMarkNsfw,
carefulBot: profile.carefulBot, carefulBot: profile!.carefulBot,
hasUnreadMessagingMessage: MessagingMessages.count({ hasUnreadMessagingMessage: MessagingMessages.count({
where: { where: {
recipientId: user.id, recipientId: user.id,
@ -155,9 +159,9 @@ export class UserRepository extends Repository<User> {
} : {}), } : {}),
...(opts.includeSecrets ? { ...(opts.includeSecrets ? {
clientData: profile.clientData, clientData: profile!.clientData,
email: profile.email, email: profile!.email,
emailVerified: profile.emailVerified, emailVerified: profile!.emailVerified,
} : {}), } : {}),
...(relation ? { ...(relation ? {

10
src/prelude/ensure.ts Normal file
View File

@ -0,0 +1,10 @@
/**
* 値が null または undefined の場合はエラーを発生させ、そうでない場合は値をそのまま返します
*/
export function ensure<T>(x: T): NonNullable<T> {
if (x == null) {
throw 'ぬるぽ';
} else {
return x!;
}
}

View File

@ -12,7 +12,7 @@ import { queueLogger } from './logger';
import { DriveFile } from '../models/entities/drive-file'; import { DriveFile } from '../models/entities/drive-file';
function initializeQueue(name: string) { function initializeQueue(name: string) {
return new Queue(name, config.redis != null ? { return new Queue(name, {
redis: { redis: {
port: config.redis.port, port: config.redis.port,
host: config.redis.host, host: config.redis.host,
@ -20,7 +20,15 @@ function initializeQueue(name: string) {
db: config.redis.db || 0, db: config.redis.db || 0,
}, },
prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue' 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'); export const deliverQueue = initializeQueue('deliver');
@ -34,16 +42,16 @@ deliverQueue
.on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => deliverLogger.debug(`active id=${job.id} to=${job.data.to}`)) .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('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('failed', (job, err) => deliverLogger.warn(`failed(${err}) id=${job.id} to=${job.data.to}`, renderError(err)))
.on('error', (error) => deliverLogger.error(`error ${error}`)) .on('error', (error) => deliverLogger.error(`error ${error}`, renderError(error)))
.on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`)); .on('stalled', (job) => deliverLogger.warn(`stalled id=${job.id} to=${job.data.to}`));
inboxQueue inboxQueue
.on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`)) .on('waiting', (jobId) => inboxLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => inboxLogger.debug(`active id=${job.id}`)) .on('active', (job) => inboxLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) 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('failed', (job, err) => inboxLogger.warn(`failed(${err}) id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`, renderError(err)))
.on('error', (error) => inboxLogger.error(`error ${error}`)) .on('error', (error) => inboxLogger.error(`error ${error}`, renderError(error)))
.on('stalled', (job) => inboxLogger.warn(`stalled id=${job.id} activity=${job.data.activity ? job.data.activity.id : 'none'}`)); .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) { export function deliver(user: ILocalUser, content: any, to: any) {

View File

@ -10,9 +10,11 @@ const logger = queueLogger.createSubLogger('delete-drive-files');
export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> { 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 Users.findOne({ const user = await Users.findOne(job.data.user.id);
id: job.data.user.id if (user == null) {
}); done();
return;
}
let deletedCount = 0; let deletedCount = 0;
let ended = false; let ended = false;

View File

@ -14,9 +14,11 @@ const logger = queueLogger.createSubLogger('export-blocking');
export async function exportBlocking(job: Bull.Job, done: any): Promise<void> { 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 Users.findOne({ const user = await Users.findOne(job.data.user.id);
id: job.data.user.id if (user == null) {
}); done();
return;
}
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
@ -56,6 +58,10 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
for (const block of blockings) { for (const block of blockings) {
const u = await Users.findOne({ id: block.blockeeId }); const u = await Users.findOne({ id: block.blockeeId });
if (u == null) {
exportedCount++; continue;
}
const content = getFullApAccount(u.username, u.host); const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => { await new Promise((res, rej) => {
stream.write(content + '\n', err => { stream.write(content + '\n', err => {

View File

@ -14,9 +14,11 @@ const logger = queueLogger.createSubLogger('export-following');
export async function exportFollowing(job: Bull.Job, done: any): Promise<void> { 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 Users.findOne({ const user = await Users.findOne(job.data.user.id);
id: job.data.user.id if (user == null) {
}); done();
return;
}
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
@ -56,6 +58,10 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
for (const following of followings) { for (const following of followings) {
const u = await Users.findOne({ id: following.followeeId }); const u = await Users.findOne({ id: following.followeeId });
if (u == null) {
exportedCount++; continue;
}
const content = getFullApAccount(u.username, u.host); const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => { await new Promise((res, rej) => {
stream.write(content + '\n', err => { stream.write(content + '\n', err => {

View File

@ -14,9 +14,11 @@ const logger = queueLogger.createSubLogger('export-mute');
export async function exportMute(job: Bull.Job, done: any): Promise<void> { 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 Users.findOne({ const user = await Users.findOne(job.data.user.id);
id: job.data.user.id if (user == null) {
}); done();
return;
}
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
@ -56,6 +58,10 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
for (const mute of mutes) { for (const mute of mutes) {
const u = await Users.findOne({ id: mute.muteeId }); const u = await Users.findOne({ id: mute.muteeId });
if (u == null) {
exportedCount++; continue;
}
const content = getFullApAccount(u.username, u.host); const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => { await new Promise((res, rej) => {
stream.write(content + '\n', err => { stream.write(content + '\n', err => {

View File

@ -9,15 +9,18 @@ import { Users, Notes, Polls } from '../../../models';
import { MoreThan } from 'typeorm'; import { MoreThan } from 'typeorm';
import { Note } from '../../../models/entities/note'; import { Note } from '../../../models/entities/note';
import { Poll } from '../../../models/entities/poll'; import { Poll } from '../../../models/entities/poll';
import { ensure } from '../../../prelude/ensure';
const logger = queueLogger.createSubLogger('export-notes'); const logger = queueLogger.createSubLogger('export-notes');
export async function exportNotes(job: Bull.Job, done: any): Promise<void> { 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 Users.findOne({ const user = await Users.findOne(job.data.user.id);
id: job.data.user.id if (user == null) {
}); done();
return;
}
// Create temp file // Create temp file
const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
@ -67,9 +70,9 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
cursor = notes[notes.length - 1].id; cursor = notes[notes.length - 1].id;
for (const note of notes) { for (const note of notes) {
let poll: Poll; let poll: Poll | undefined;
if (note.hasPoll) { if (note.hasPoll) {
poll = await Polls.findOne({ noteId: note.id }); poll = await Polls.findOne({ noteId: note.id }).then(ensure);
} }
const content = JSON.stringify(serialize(note, poll)); const content = JSON.stringify(serialize(note, poll));
await new Promise((res, rej) => { await new Promise((res, rej) => {
@ -114,7 +117,7 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
done(); done();
} }
function serialize(note: Note, poll: Poll): any { function serialize(note: Note, poll: Poll | null = null): any {
return { return {
id: note.id, id: note.id,
text: note.text, text: note.text,

View File

@ -14,9 +14,11 @@ const logger = queueLogger.createSubLogger('export-user-lists');
export async function exportUserLists(job: Bull.Job, done: any): Promise<void> { 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 Users.findOne({ const user = await Users.findOne(job.data.user.id);
id: job.data.user.id if (user == null) {
}); done();
return;
}
const lists = await UserLists.find({ const lists = await UserLists.find({
userId: user.id userId: user.id

View File

@ -13,13 +13,19 @@ const logger = queueLogger.createSubLogger('import-following');
export async function importFollowing(job: Bull.Job, done: any): Promise<void> { 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 Users.findOne({ const user = await Users.findOne(job.data.user.id);
id: job.data.user.id if (user == null) {
}); done();
return;
}
const file = await DriveFiles.findOne({ const file = await DriveFiles.findOne({
id: job.data.fileId id: job.data.fileId
}); });
if (file == null) {
done();
return;
}
const csv = await downloadTextFile(file.url); const csv = await downloadTextFile(file.url);
@ -31,11 +37,11 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
try { try {
const { username, host } = parseAcct(line.trim()); const { username, host } = parseAcct(line.trim());
let target = isSelfHost(host) ? await Users.findOne({ let target = isSelfHost(host!) ? await Users.findOne({
host: null, host: null,
usernameLower: username.toLowerCase() usernameLower: username.toLowerCase()
}) : await Users.findOne({ }) : await Users.findOne({
host: toPuny(host), host: toPuny(host!),
usernameLower: username.toLowerCase() usernameLower: username.toLowerCase()
}); });

View File

@ -14,13 +14,19 @@ const logger = queueLogger.createSubLogger('import-user-lists');
export async function importUserLists(job: Bull.Job, done: any): Promise<void> { 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 Users.findOne({ const user = await Users.findOne(job.data.user.id);
id: job.data.user.id if (user == null) {
}); done();
return;
}
const file = await DriveFiles.findOne({ const file = await DriveFiles.findOne({
id: job.data.fileId id: job.data.fileId
}); });
if (file == null) {
done();
return;
}
const csv = await downloadTextFile(file.url); const csv = await downloadTextFile(file.url);
@ -43,22 +49,20 @@ export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
}); });
} }
let target = isSelfHost(host) ? await Users.findOne({ let target = isSelfHost(host!) ? await Users.findOne({
host: null, host: null,
usernameLower: username.toLowerCase() usernameLower: username.toLowerCase()
}) : await Users.findOne({ }) : await Users.findOne({
host: toPuny(host), host: toPuny(host!),
usernameLower: username.toLowerCase() usernameLower: username.toLowerCase()
}); });
if (host == null && target == null) continue;
if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
if (target == null) { if (target == null) {
target = await resolveUser(username, host); target = await resolveUser(username, host);
} }
if (await UserListJoinings.findOne({ userListId: list.id, userId: target.id }) != null) continue;
pushUserToUserList(target, list); pushUserToUserList(target, list);
} }

View File

@ -7,7 +7,7 @@ import { instanceChart } from '../../services/chart';
const logger = new Logger('deliver'); const logger = new Logger('deliver');
let latest: string = null; let latest: string | null = null;
export default async (job: Bull.Job) => { export default async (job: Bull.Job) => {
const { host } = new URL(job.data.to); const { host } = new URL(job.data.to);

View File

@ -12,7 +12,9 @@ import { Instances, Users, UserPublickeys } from '../../models';
import { instanceChart } from '../../services/chart'; import { instanceChart } from '../../services/chart';
import { UserPublickey } from '../../models/entities/user-publickey'; import { UserPublickey } from '../../models/entities/user-publickey';
import fetchMeta from '../../misc/fetch-meta'; import fetchMeta from '../../misc/fetch-meta';
import { toPuny } from '../../misc/convert-host'; import { toPuny, toPunyNullable } from '../../misc/convert-host';
import { validActor } from '../../remote/activitypub/type';
import { ensure } from '../../prelude/ensure';
const logger = new Logger('inbox'); const logger = new Logger('inbox');
@ -34,7 +36,7 @@ export default async (job: Bull.Job): Promise<void> => {
if (keyIdLower.startsWith('acct:')) { if (keyIdLower.startsWith('acct:')) {
const acct = parseAcct(keyIdLower.slice('acct:'.length)); const acct = parseAcct(keyIdLower.slice('acct:'.length));
const host = toPuny(acct.host); const host = toPunyNullable(acct.host);
const username = toPuny(acct.username); const username = toPuny(acct.username);
if (host === null) { if (host === null) {
@ -63,9 +65,7 @@ export default async (job: Bull.Job): Promise<void> => {
host: host host: host
}) as IRemoteUser; }) as IRemoteUser;
key = await UserPublickeys.findOne({ key = await UserPublickeys.findOne(user.id).then(ensure);
userId: user.id
});
} else { } else {
// アクティビティ内のホストの検証 // アクティビティ内のホストの検証
const host = toPuny(new URL(signature.keyId).hostname); const host = toPuny(new URL(signature.keyId).hostname);
@ -86,14 +86,14 @@ export default async (job: Bull.Job): Promise<void> => {
key = await UserPublickeys.findOne({ key = await UserPublickeys.findOne({
keyId: signature.keyId keyId: signature.keyId
}); }).then(ensure);
user = await Users.findOne(key.userId) as IRemoteUser; user = await Users.findOne(key.userId) as IRemoteUser;
} }
// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了 // Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') { if (activity.type === 'Update') {
if (activity.object && activity.object.type === 'Person') { if (activity.object && validActor.includes(activity.object.type)) {
if (user == null) { if (user == null) {
logger.warn('Update activity received, but user not registed.'); logger.warn('Update activity received, but user not registed.');
} else if (!httpSignature.verifySignature(signature, key.keyPem)) { } else if (!httpSignature.verifySignature(signature, key.keyPem)) {

View File

@ -6,9 +6,10 @@ import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id; const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
if (id == null) throw 'missing id';
if (!id.startsWith(config.url + '/')) { if (!id.startsWith(config.url + '/')) {
return null; return;
} }
const follower = await Users.findOne({ const follower = await Users.findOne({

View File

@ -14,6 +14,7 @@ export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
if (activity.target === actor.featured) { if (activity.target === actor.featured) {
const note = await resolveNote(activity.object); const note = await resolveNote(activity.object);
if (note == null) throw new Error('note not found');
await addPinned(actor, note.id); await addPinned(actor, note.id);
return; return;
} }

View File

@ -53,16 +53,16 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
logger.info(`Creating the (Re)Note: ${uri}`); logger.info(`Creating the (Re)Note: ${uri}`);
//#region Visibility //#region Visibility
const visibility = getVisibility(activity.to, activity.cc, actor); const visibility = getVisibility(activity.to || [], activity.cc || [], actor);
let visibleUsers: User[] = []; let visibleUsers: User[] = [];
if (visibility == 'specified') { if (visibility == 'specified') {
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri))); visibleUsers = await Promise.all((note.to || []).map(uri => resolvePerson(uri)));
} }
//#endergion //#endergion
await post(actor, { await post(actor, {
createdAt: new Date(activity.published), createdAt: activity.published ? new Date(activity.published) : null,
renote, renote,
visibility, visibility,
visibleUsers, visibleUsers,
@ -75,9 +75,6 @@ type visibility = 'public' | 'home' | 'followers' | 'specified';
function getVisibility(to: string[], cc: string[], actor: IRemoteUser): visibility { function getVisibility(to: string[], cc: string[], actor: IRemoteUser): visibility {
const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
to = to || [];
cc = cc || [];
if (to.includes(PUBLIC)) { if (to.includes(PUBLIC)) {
return 'public'; return 'public';
} else if (cc.includes(PUBLIC)) { } else if (cc.includes(PUBLIC)) {

View File

@ -9,13 +9,14 @@ const logger = apLogger;
export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id; const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
if (id == null) throw 'missing id';
const uri = activity.id || activity; const uri = activity.id || activity;
logger.info(`Block: ${uri}`); logger.info(`Block: ${uri}`);
if (!id.startsWith(config.url + '/')) { if (!id.startsWith(config.url + '/')) {
return null; return;
} }
const blockee = await Users.findOne(id.split('/').pop()); const blockee = await Users.findOne(id.split('/').pop());

View File

@ -6,9 +6,10 @@ import { Users } from '../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id; const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
if (id == null) throw 'missing id';
if (!id.startsWith(config.url + '/')) { if (!id.startsWith(config.url + '/')) {
return null; return;
} }
const followee = await Users.findOne(id.split('/').pop()); const followee = await Users.findOne(id.split('/').pop());

View File

@ -71,7 +71,7 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
default: default:
apLogger.warn(`unknown activity type: ${(activity as any).type}`); apLogger.warn(`unknown activity type: ${(activity as any).type}`);
return null; return;
} }
}; };

View File

@ -5,6 +5,7 @@ import { Notes } from '../../../models';
export default async (actor: IRemoteUser, activity: ILike) => { export default async (actor: IRemoteUser, activity: ILike) => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id; const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
if (id == null) throw 'missing id';
// Transform: // Transform:
// https://misskey.ex/notes/xxxx to // https://misskey.ex/notes/xxxx to

View File

@ -6,9 +6,10 @@ import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id; const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
if (id == null) throw 'missing id';
if (!id.startsWith(config.url + '/')) { if (!id.startsWith(config.url + '/')) {
return null; return;
} }
const follower = await Users.findOne(id.split('/').pop()); const follower = await Users.findOne(id.split('/').pop());

View File

@ -14,6 +14,7 @@ export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
if (activity.target === actor.featured) { if (activity.target === actor.featured) {
const note = await resolveNote(activity.object); const note = await resolveNote(activity.object);
if (note == null) throw new Error('note not found');
await removePinned(actor, note.id); await removePinned(actor, note.id);
return; return;
} }

View File

@ -9,13 +9,14 @@ const logger = apLogger;
export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id; const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
if (id == null) throw 'missing id';
const uri = activity.id || activity; const uri = activity.id || activity;
logger.info(`UnBlock: ${uri}`); logger.info(`UnBlock: ${uri}`);
if (!id.startsWith(config.url + '/')) { if (!id.startsWith(config.url + '/')) {
return null; return;
} }
const blockee = await Users.findOne(id.split('/').pop()); const blockee = await Users.findOne(id.split('/').pop());

View File

@ -7,9 +7,10 @@ import { Users, FollowRequests, Followings } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id; const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
if (id == null) throw 'missing id';
if (!id.startsWith(config.url + '/')) { if (!id.startsWith(config.url + '/')) {
return null; return;
} }
const followee = await Users.findOne(id.split('/').pop()); const followee = await Users.findOne(id.split('/').pop());

View File

@ -39,6 +39,4 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => {
undoLike(actor, object as ILike); undoLike(actor, object as ILike);
break; break;
} }
return null;
}; };

View File

@ -8,6 +8,7 @@ import { Notes } from '../../../../models';
*/ */
export default async (actor: IRemoteUser, activity: ILike): Promise<void> => { export default async (actor: IRemoteUser, activity: ILike): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id; const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
if (id == null) throw 'missing id';
const noteId = id.split('/').pop(); const noteId = id.split('/').pop();

View File

@ -5,6 +5,7 @@ import fetchMeta from '../../../misc/fetch-meta';
import { apLogger } from '../logger'; import { apLogger } from '../logger';
import { DriveFile } from '../../../models/entities/drive-file'; import { DriveFile } from '../../../models/entities/drive-file';
import { DriveFiles } from '../../../models'; import { DriveFiles } from '../../../models';
import { ensure } from '../../../prelude/ensure';
const logger = apLogger; const logger = apLogger;
@ -14,7 +15,7 @@ const logger = apLogger;
export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> { export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ // 投稿者が凍結されていたらスキップ
if (actor.isSuspended) { if (actor.isSuspended) {
return null; throw new Error('actor has been suspended');
} }
const image = await new Resolver().resolve(value) as any; const image = await new Resolver().resolve(value) as any;
@ -28,17 +29,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
const instance = await fetchMeta(); const instance = await fetchMeta();
const cache = instance.cacheRemoteFiles; const cache = instance.cacheRemoteFiles;
let file; let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache);
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;
}
if (file.isLink) { if (file.isLink) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
@ -49,7 +40,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
uri: image.url uri: image.url
}); });
file = DriveFiles.findOne(file.id); file = await DriveFiles.findOne(file.id).then(ensure);
} }
} }

View File

@ -21,6 +21,7 @@ import { IObject, INote } from '../type';
import { Emoji } from '../../../models/entities/emoji'; import { Emoji } from '../../../models/entities/emoji';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
import fetchMeta from '../../../misc/fetch-meta'; import fetchMeta from '../../../misc/fetch-meta';
import { ensure } from '../../../prelude/ensure';
const logger = apLogger; const logger = apLogger;
@ -29,13 +30,14 @@ const logger = apLogger;
* *
* Misskeyに対象のNoteが登録されていればそれを返します。 * Misskeyに対象のNoteが登録されていればそれを返します。
*/ */
export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note> { export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
const uri = typeof value == 'string' ? value : value.id; const uri = typeof value == 'string' ? value : value.id;
if (uri == null) throw 'missing uri';
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) { if (uri.startsWith(config.url + '/')) {
const id = uri.split('/').pop(); const id = uri.split('/').pop();
return await Notes.findOne(id); return await Notes.findOne(id).then(x => x || null);
} }
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
@ -52,7 +54,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
/** /**
* Noteを作成します。 * Noteを作成します。
*/ */
export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note> { export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note | null> {
if (resolver == null) resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
const object: any = await resolver.resolve(value); const object: any = await resolver.resolve(value);
@ -65,7 +67,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
value: value, value: value,
object: object object: object
}); });
return null; throw 'invalid note';
} }
const note: INote = object; const note: INote = object;
@ -75,11 +77,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
logger.info(`Creating the Note: ${note.id}`); 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) { if (actor.isSuspended) {
return null; throw 'actor has been suspended';
} }
//#region Visibility //#region Visibility
@ -95,9 +97,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
visibility = 'followers'; visibility = 'followers';
} else { } else {
visibility = 'specified'; 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 //#endergion
const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver); const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver);
@ -118,20 +120,22 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
: []; : [];
// リプライ // リプライ
const reply: Note = note.inReplyTo const reply: Note | null = note.inReplyTo
? await resolveNote(note.inReplyTo, resolver).catch(e => { ? await resolveNote(note.inReplyTo, resolver).then(x => {
// 4xxの場合はリプライしてないことにする if (x == null) {
if (e.statusCode >= 400 && e.statusCode < 500) { logger.warn(`Specified inReplyTo, but nout found`);
logger.warn(`Ignored inReplyTo ${note.inReplyTo} - ${e.statusCode} `); throw 'inReplyTo not found';
return null; } else {
return x;
} }
}).catch(e => {
logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`); logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`);
throw e; throw e;
}) })
: null; : null;
// 引用 // 引用
let quote: Note; let quote: Note | undefined | null;
if (note._misskey_quote && typeof note._misskey_quote == 'string') { if (note._misskey_quote && typeof note._misskey_quote == 'string') {
quote = await resolveNote(note._misskey_quote).catch(e => { quote = await resolveNote(note._misskey_quote).catch(e => {
@ -148,11 +152,12 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const cw = note.summary === '' ? null : note.summary; 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 // vote
if (reply && reply.hasPoll) { if (reply && reply.hasPoll) {
const poll = await Polls.findOne({ noteId: reply.id }); const poll = await Polls.findOne(reply.id).then(ensure);
const tryCreateVote = async (name: string, index: number): Promise<null> => { const tryCreateVote = async (name: string, index: number): Promise<null> => {
if (poll.expiresAt && Date.now() > new Date(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}`); logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
@ -180,7 +185,7 @@ 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}`); logger.info(`extractEmojis: ${e}`);
return [] as Emoji[]; return [] as Emoji[];
}); });
@ -196,7 +201,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
} }
return await post(actor, { return await post(actor, {
createdAt: new Date(note.published), createdAt: note.published ? new Date(note.published) : null,
files, files,
reply, reply,
renote: quote, renote: quote,
@ -223,8 +228,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
* Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/ */
export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note> { export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
const uri = typeof value == 'string' ? value : value.id; const uri = typeof value == 'string' ? value : value.id;
if (uri == null) throw 'missing uri';
// ブロックしてたら中断 // ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
@ -242,71 +248,81 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
// リモートサーバーからフェッチしてきて登録 // リモートサーバーからフェッチしてきて登録
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 // 添付されてきた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 'something happened';
} else {
return note;
}
});
} else {
throw e;
}
});
} }
export async function extractEmojis(tags: ITag[], host: string) { export async function extractEmojis(tags: ITag[], host: string): Promise<Emoji[]> {
host = toPuny(host); host = toPuny(host);
if (!tags) return []; 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( return await Promise.all(eomjiTags.map(async tag => {
eomjiTags.map(async tag => { const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
const name = tag.name.replace(/^:/, '').replace(/:$/, '');
const exists = await Emojis.findOne({ const exists = await Emojis.findOne({
host, host,
name name
}); });
if (exists) { if (exists) {
if ((tag.updated != null && exists.updatedAt == null) if ((tag.updated != null && exists.updatedAt == null)
|| (tag.id != null && exists.uri == null) || (tag.id != null && exists.uri == null)
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
) { ) {
await Emojis.update({ await Emojis.update({
host, host,
name, name,
}, { }, {
uri: tag.id, uri: tag.id,
url: tag.icon.url, url: tag.icon!.url,
updatedAt: new Date(tag.updated), updatedAt: new Date(tag.updated!),
}); });
return await Emojis.findOne({ return await Emojis.findOne({
host, host,
name name
}); }) as Emoji;
}
return exists;
} }
logger.info(`register emoji host=${host}, name=${name}`); return exists;
}
return await Emojis.save({ logger.info(`register emoji host=${host}, name=${name}`);
id: genId(),
host, return await Emojis.save({
name, id: genId(),
uri: tag.id, host,
url: tag.icon.url, name,
updatedAt: tag.updated ? new Date(tag.updated) : undefined, uri: tag.id,
aliases: [] url: tag.icon!.url,
} as Emoji); updatedAt: tag.updated ? new Date(tag.updated) : undefined,
}) aliases: []
); } as Partial<Emoji>);
}));
} }
async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: string[], resolver: Resolver) { async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: string[], resolver: Resolver) {
const ignoreUris = ['https://www.w3.org/ns/activitystreams#Public', `${actor.uri}/followers`]; const ignoreUris = ['https://www.w3.org/ns/activitystreams#Public', `${actor.uri}/followers`];
const uris = difference(unique(concat([to || [], cc || []])), ignoreUris); const uris = difference(unique(concat([to || [], cc || []])), ignoreUris);
const limit = promiseLimit(2); const limit = promiseLimit<User | null>(2);
const users = await Promise.all( const users = await Promise.all(
uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<User>) 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[];
} }

View File

@ -24,6 +24,9 @@ import { UserPublickey } from '../../../models/entities/user-publickey';
import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
import { toPuny } from '../../../misc/convert-host'; import { toPuny } from '../../../misc/convert-host';
import { UserProfile } from '../../../models/entities/user-profile'; 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; const logger = apLogger;
/** /**
@ -38,7 +41,7 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: object is 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}'`); return new Error(`invalid person: object is not a person or service '${x.type}'`);
} }
@ -84,13 +87,13 @@ function validatePerson(x: any, uri: string) {
* *
* Misskeyに対象のPersonが登録されていればそれを返します。 * Misskeyに対象のPersonが登録されていればそれを返します。
*/ */
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User> { export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
if (typeof uri !== 'string') throw 'uri is not string'; if (typeof uri !== 'string') throw 'uri is not string';
// URIがこのサーバーを指しているならデータベースからフェッチ // URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) { if (uri.startsWith(config.url + '/')) {
const id = uri.split('/').pop(); const id = uri.split('/').pop();
return await Users.findOne(id); return await Users.findOne(id).then(x => x || null);
} }
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
@ -126,7 +129,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const host = toPuny(new URL(object.id).hostname); const host = toPuny(new URL(object.id).hostname);
const { fields } = analyzeAttachments(person.attachment); const { fields } = analyzeAttachments(person.attachment || []);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()); const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase());
@ -135,27 +138,42 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
// Create user // Create user
let user: IRemoteUser; let user: IRemoteUser;
try { try {
user = await Users.save({ // Start transaction
id: genId(), await getConnection().transaction(async transactionalEntityManager => {
avatarId: null, user = await transactionalEntityManager.save(new User({
bannerId: null, id: genId(),
createdAt: Date.parse(person.published) || new Date(), avatarId: null,
lastFetchedAt: new Date(), bannerId: null,
name: person.name, createdAt: new Date(),
isLocked: person.manuallyApprovesFollowers, lastFetchedAt: new Date(),
username: person.preferredUsername, name: person.name,
usernameLower: person.preferredUsername.toLowerCase(), isLocked: person.manuallyApprovesFollowers,
host, username: person.preferredUsername,
inbox: person.inbox, usernameLower: person.preferredUsername.toLowerCase(),
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), host,
featured: person.featured, inbox: person.inbox,
endpoints: person.endpoints, sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
uri: person.id, featured: person.featured,
url: person.url, uri: person.id,
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true isCat: (person as any).isCat === true
} as Partial<User>) as IRemoteUser; })) 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) { } catch (e) {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {
@ -166,39 +184,26 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
throw e; throw e;
} }
await UserProfiles.save({
userId: user.id,
description: fromHtml(person.summary),
fields,
userHost: host
} as Partial<UserProfile>);
await UserPublickeys.save({
userId: user.id,
keyId: person.publicKey.id,
keyPem: person.publicKey.publicKeyPem
} as UserPublickey);
// Register host // Register host
registerOrFetchInstanceDoc(host).then(i => { registerOrFetchInstanceDoc(host).then(i => {
Instances.increment({ id: i.id }, 'usersCount', 1); Instances.increment({ id: i.id }, 'usersCount', 1);
instanceChart.newUser(i.host); instanceChart.newUser(i.host);
}); });
usersChart.update(user, true); usersChart.update(user!, true);
// ハッシュタグ更新 // ハッシュタグ更新
for (const tag of tags) updateHashtag(user, tag, true, 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 (user!.tags || []).filter(x => !tags.includes(x))) updateHashtag(user!, tag, true, false);
//#region アイコンとヘッダー画像をフェッチ //#region アイコンとヘッダー画像をフェッチ
const [avatar, banner] = (await Promise.all<DriveFile>([ const [avatar, banner] = (await Promise.all<DriveFile | null>([
person.icon, person.icon,
person.image person.image
].map(img => ].map(img =>
img == null img == null
? Promise.resolve(null) ? Promise.resolve(null)
: resolveImage(user, img).catch(() => null) : resolveImage(user!, img).catch(() => null)
))); )));
const avatarId = avatar ? avatar.id : null; const avatarId = avatar ? avatar.id : null;
@ -206,9 +211,9 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar) : null; const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar) : null;
const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null; const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null; const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null; const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null;
await Users.update(user.id, { await Users.update(user!.id, {
avatarId, avatarId,
bannerId, bannerId,
avatarUrl, avatarUrl,
@ -217,30 +222,30 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
bannerColor bannerColor
}); });
user.avatarId = avatarId; user!.avatarId = avatarId;
user.bannerId = bannerId; user!.bannerId = bannerId;
user.avatarUrl = avatarUrl; user!.avatarUrl = avatarUrl;
user.bannerUrl = bannerUrl; user!.bannerUrl = bannerUrl;
user.avatarColor = avatarColor; user!.avatarColor = avatarColor;
user.bannerColor = bannerColor; user!.bannerColor = bannerColor;
//#endregion //#endregion
//#region カスタム絵文字取得 //#region カスタム絵文字取得
const emojis = await extractEmojis(person.tag, host).catch(e => { const emojis = await extractEmojis(person.tag || [], host).catch(e => {
logger.info(`extractEmojis: ${e}`); logger.info(`extractEmojis: ${e}`);
return [] as Emoji[]; return [] as Emoji[];
}); });
const emojiNames = emojis.map(emoji => emoji.name); const emojiNames = emojis.map(emoji => emoji.name);
await Users.update(user.id, { await Users.update(user!.id, {
emojis: emojiNames emojis: emojiNames
}); });
//#endregion //#endregion
await updateFeatured(user.id).catch(err => logger.error(err)); await updateFeatured(user!.id).catch(err => logger.error(err));
return user; return user!;
} }
/** /**
@ -250,7 +255,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
* @param resolver Resolver * @param resolver Resolver
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
*/ */
export async function updatePerson(uri: string, resolver?: Resolver, hint?: object): Promise<void> { export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: object): Promise<void> {
if (typeof uri !== 'string') throw 'uri is not string'; if (typeof uri !== 'string') throw 'uri is not string';
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
@ -286,7 +291,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
logger.info(`Updating the Person: ${person.id}`); logger.info(`Updating the Person: ${person.id}`);
// アイコンとヘッダー画像をフェッチ // アイコンとヘッダー画像をフェッチ
const [avatar, banner] = (await Promise.all<DriveFile>([ const [avatar, banner] = (await Promise.all<DriveFile | null>([
person.icon, person.icon,
person.image person.image
].map(img => ].map(img =>
@ -296,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}`); logger.info(`extractEmojis: ${e}`);
return [] as Emoji[]; return [] as Emoji[];
}); });
const emojiNames = emojis.map(emoji => emoji.name); 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()); const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase());
@ -313,7 +318,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
featured: person.featured, featured: person.featured,
emojis: emojiNames, emojis: emojiNames,
description: fromHtml(person.summary), description: person.summary ? fromHtml(person.summary) : null,
name: person.name, name: person.name,
url: person.url, url: person.url,
endpoints: person.endpoints, endpoints: person.endpoints,
@ -322,7 +327,6 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
isBot: object.type == 'Service', isBot: object.type == 'Service',
isCat: (person as any).isCat === true, isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers, isLocked: person.manuallyApprovesFollowers,
createdAt: new Date(Date.parse(person.published)) || null,
} as Partial<User>; } as Partial<User>;
if (avatar) { if (avatar) {
@ -375,7 +379,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
* Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
*/ */
export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<User> { export async function resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
if (typeof uri !== 'string') throw 'uri is not string'; if (typeof uri !== 'string') throw 'uri is not string';
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
@ -435,21 +439,24 @@ export function analyzeAttachments(attachments: ITag[]) {
}[] = []; }[] = [];
const services: { [x: string]: any } = {}; const services: { [x: string]: any } = {};
if (Array.isArray(attachments)) if (Array.isArray(attachments)) {
for (const attachment of attachments.filter(isPropertyValue)) for (const attachment of attachments.filter(isPropertyValue)) {
if (isPropertyValue(attachment.identifier)) if (isPropertyValue(attachment.identifier!)) {
addService(services, attachment.identifier); addService(services, attachment.identifier!);
else } else {
fields.push({ fields.push({
name: attachment.name, name: attachment.name!,
value: fromHtml(attachment.value) value: fromHtml(attachment.value!)
}); });
}
}
}
return { fields, services }; return { fields, services };
} }
export async function updateFeatured(userId: User['id']) { export async function updateFeatured(userId: User['id']) {
const user = await Users.findOne(userId); const user = await Users.findOne(userId).then(ensure);
if (!Users.isRemoteUser(user)) return; if (!Users.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
@ -467,18 +474,18 @@ export async function updateFeatured(userId: User['id']) {
if (!Array.isArray(items)) throw new Error(`Collection items is not an array`); if (!Array.isArray(items)) throw new Error(`Collection items is not an array`);
// Resolve and regist Notes // Resolve and regist Notes
const limit = promiseLimit(2); const limit = promiseLimit<Note | null>(2);
const featuredNotes = await Promise.all(items const featuredNotes = await Promise.all(items
.filter(item => item.type === 'Note') .filter(item => item.type === 'Note')
.slice(0, 5) .slice(0, 5)
.map(item => limit(() => resolveNote(item, resolver)) as Promise<Note>)); .map(item => limit(() => resolveNote(item, resolver))));
for (const note of featuredNotes.filter(note => note != null)) { for (const note of featuredNotes.filter(note => note != null)) {
UserNotePinings.save({ UserNotePinings.save({
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
noteId: note.id noteId: note!.id
} as UserNotePining); } as UserNotePining);
} }
} }

View File

@ -14,10 +14,10 @@ export async function extractPollFromQuestion(source: string | IQuestion): Promi
throw 'invalid question'; throw 'invalid question';
} }
const choices = question[multiple ? 'anyOf' : 'oneOf'] const choices = question[multiple ? 'anyOf' : 'oneOf']!
.map((x, i) => x.name); .map((x, i) => x.name!);
const votes = question[multiple ? 'anyOf' : 'oneOf'] const votes = question[multiple ? 'anyOf' : 'oneOf']!
.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
return { return {
@ -60,7 +60,7 @@ export async function updateQuestion(value: any) {
for (const choice of poll.choices) { for (const choice of poll.choices) {
const oldCount = poll.votes[poll.choices.indexOf(choice)]; const oldCount = poll.votes[poll.choices.indexOf(choice)];
const newCount = apChoices.filter(ap => ap.name === choice)[0].replies.totalItems; const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
if (oldCount != newCount) { if (oldCount != newCount) {
changed = true; changed = true;
@ -68,10 +68,6 @@ export async function updateQuestion(value: any) {
} }
} }
await Notes.update(note.id, {
updatedAt: new Date(),
});
await Polls.update({ noteId: note.id }, { await Polls.update({ noteId: note.id }, {
votes: poll.votes votes: poll.votes
}); });

View File

@ -14,13 +14,13 @@ export type ITag = {
identifier?: IIdentifier; identifier?: IIdentifier;
}; };
export function extractHashtags(tags: ITag[]) { export function extractHashtags(tags: ITag[] | null | undefined): string[] {
if (!tags) return []; if (tags == null) return [];
const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string'); const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string');
return hashtags.map(tag => { return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/); const m = tag.name ? tag.name.match(/^#(.+)/) : null;
return m ? m[1] : null; return m ? m[1] : null;
}).filter(x => x != null); }).filter(x => x != null) as string[];
} }

Some files were not shown because too many files have changed in this diff Show More