Compare commits
72 Commits
11.0.0-alp
...
11.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
0e9fba9287 | |||
2e3dd2a30a | |||
dda5f6559d | |||
4152e59638 | |||
9d5a92bce6 | |||
30172b92e6 | |||
8468a9d4c7 | |||
626cfb61ac | |||
9603f3fa4f | |||
619a1b9e53 | |||
62e76ad588 | |||
236d72685d | |||
72a5f7b1e2 | |||
d1c16a90b4 | |||
33a9783ae5 | |||
4d64fd665e | |||
96443384fe | |||
69939f1edb | |||
e3c0058942 | |||
3dc2361654 | |||
ec2f709018 | |||
ea06665c51 | |||
74a4bd704c | |||
c62f3e0e45 | |||
a56a969433 | |||
1016f94bbb | |||
e98cce9aee | |||
d4bdb5d327 | |||
db4378415e | |||
9b594880c8 | |||
d44fc3db2f | |||
5133b0a0c0 | |||
815469304f | |||
c651921c79 | |||
d18c835221 | |||
e38335077e | |||
34c150cf73 | |||
24cb3ba091 | |||
c07eaef2d1 | |||
5df708ac9f | |||
38ffbe95e9 | |||
8bec4042fc | |||
ee7ef89dfb | |||
54a5246061 | |||
fa6eae2937 | |||
795be20ee1 | |||
87d3a06dcd | |||
8b5c917195 | |||
c7da0e59ff | |||
eb14acbe0c | |||
56860a6bef | |||
735687be21 | |||
fab0cc51b3 | |||
3a26acbdb2 | |||
8d0c31bcda | |||
0b99293d4c | |||
802153ff9b | |||
ec73e2d237 | |||
c32b020535 | |||
c4441804e2 | |||
6c9d80d4e8 | |||
a231f8d26c | |||
08da258b63 | |||
5994926440 | |||
30f2da4215 | |||
1a2229f886 | |||
1e166490d9 | |||
1c57e6f80a | |||
a6537a8748 | |||
842b75977b | |||
9b3dccf60c | |||
2d533e0cd8 |
@ -5,6 +5,14 @@ If you encounter any problems with updating, please try the following:
|
||||
1. `npm run clean` or `npm run cleanall`
|
||||
2. Retry update (Don't forget `npm i`)
|
||||
|
||||
11.0.0
|
||||
----------
|
||||
* データベースがMongoDBからPostgreSQLに変更されました
|
||||
|
||||
### APIの破壊的変更
|
||||
* v10時点で deprecated だったパラメータなどを削除
|
||||
* ユーザーリストの title が name に
|
||||
|
||||
10.99.0
|
||||
----------
|
||||
* manifest.json にインスタンス名を反映させるように
|
||||
|
@ -1,4 +1,4 @@
|
||||
<a href="https://ai.misskey.xyz/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
|
||||
<a href="https://xn--931a.moe/"><img src="https://github.com/syuilo/misskey/blob/develop/assets/ai-orig.png?raw=true" align="right" height="320px"/></a>
|
||||
|
||||
[](https://misskey.xyz/)
|
||||
================================================================
|
||||
@ -102,7 +102,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<!-- PATREON_START -->
|
||||
<table><tr>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5888816/36da0f7c15954df0ab13f9abdf227f66/1?token-time=2145916800&token-hash=HGkZJ7s4bSaQVoOJ5q30mTWHTxDLiw1LuyaogKPLy24%3D" alt="Hiroshi Seki" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/1?token-time=2145916800&token-hash=WeuDzzz24cRXJogyIkU-mxARqkdyms-rcZKbO-GpGjw%3D" alt="weep" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12190916/fb7fa7983c14425f890369535b1506a4/3?token-time=2145916800&token-hash=gr7DF8BuwflHvNoP24He4aa-5j8_KycrAQe3fHwQIUE%3D" alt="weep" width="100"></td>
|
||||
<td><img src="https://c8.patreon.com/2/200/12059069" alt="naga_rus" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/4?token-time=2145916800&token-hash=vZdDTTF-ahiKBjjgppS2ev4rkD8H7TTKkXXoxsucs6Y%3D" alt="Melilot" width="100"></td>
|
||||
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/5670915/ee175f0bfb6347ffa4ea101a8c097bff/1?token-time=2145916800&token-hash=ubqJzjhBQUo8Nw6h_8jMDlJ5ocIO46EflpiRkp2jIw4%3D" alt="osapon" width="100"></td>
|
||||
@ -158,7 +158,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
|
||||
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
|
||||
</tr></table>
|
||||
|
||||
**Last updated:** Sat, 06 Apr 2019 03:35:05 UTC
|
||||
**Last updated:** Sun, 07 Apr 2019 19:21:08 UTC
|
||||
<!-- PATREON_END -->
|
||||
|
||||
:four_leaf_clover: Copyright
|
||||
|
9
binding.gyp
Normal file
9
binding.gyp
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
'targets': [
|
||||
{
|
||||
'target_name': 'crypto_key',
|
||||
'sources': ['src/crypto_key.cc'],
|
||||
'include_dirs': ['<!(node -e "require(\'nan\')")']
|
||||
}
|
||||
]
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
const mongo = require('mongodb');
|
||||
const User = require('../built/models/user').default;
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const user = args[0];
|
||||
|
||||
const q = user.startsWith('@') ? {
|
||||
username: user.split('@')[1],
|
||||
host: user.split('@')[2] || null
|
||||
} : { _id: new mongo.ObjectID(user) };
|
||||
|
||||
console.log(`Mark as admin ${user}...`);
|
||||
|
||||
User.update(q, {
|
||||
$set: {
|
||||
isAdmin: true
|
||||
}
|
||||
}).then(() => {
|
||||
console.log(`Done ${user}`);
|
||||
}, e => {
|
||||
console.error(e);
|
||||
});
|
@ -11,7 +11,7 @@ This guide describes how to install and setup Misskey with Docker.
|
||||
----------------------------------------------------------------
|
||||
1. `git clone -b master git://github.com/syuilo/misskey.git` Clone Misskey repository's master branch.
|
||||
2. `cd misskey` Move to misskey directory.
|
||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) tag.
|
||||
3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest) tag.
|
||||
|
||||
*2.* Configure Misskey
|
||||
----------------------------------------------------------------
|
||||
@ -67,7 +67,7 @@ Just `docker-compose up -d`. GLHF!
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git fetch`
|
||||
2. `git stash`
|
||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
|
||||
3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)`
|
||||
4. `git stash pop`
|
||||
5. `docker-compose build`
|
||||
6. Check [ChangeLog](../CHANGELOG.md) for migration information
|
||||
|
@ -12,7 +12,7 @@ Ce guide explique comment installer et configurer Misskey avec Docker.
|
||||
----------------------------------------------------------------
|
||||
1. `git clone -b master git://github.com/syuilo/misskey.git` Clone le dépôt de Misskey sur la branche master.
|
||||
2. `cd misskey` Naviguez dans le dossier du dépôt.
|
||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout sur le tag de la [dernière version](https://github.com/syuilo/misskey/releases/latest).
|
||||
3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` Checkout sur le tag de la [dernière version](https://github.com/syuilo/misskey/releases/latest).
|
||||
|
||||
*2.* Configuration de Misskey
|
||||
----------------------------------------------------------------
|
||||
@ -40,7 +40,7 @@ Utilisez la commande `docker-compose up -d`. GLHF!
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git fetch`
|
||||
2. `git stash`
|
||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
|
||||
3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)`
|
||||
4. `git stash pop`
|
||||
5. `docker-compose build`
|
||||
6. Consultez le [ChangeLog](../CHANGELOG.md) pour avoir les éventuelles informations de migration
|
||||
|
@ -11,7 +11,7 @@ Dockerを使ったMisskey構築方法
|
||||
----------------------------------------------------------------
|
||||
1. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン
|
||||
2. `cd misskey` misskeyディレクトリに移動
|
||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
|
||||
3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
|
||||
|
||||
*2.* 設定ファイルの作成と編集
|
||||
----------------------------------------------------------------
|
||||
@ -67,7 +67,7 @@ cp docker_example.env docker.env
|
||||
### Misskeyを最新バージョンにアップデートする方法:
|
||||
1. `git fetch`
|
||||
2. `git stash`
|
||||
3. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
|
||||
3. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)`
|
||||
4. `git stash pop`
|
||||
5. `docker-compose build`
|
||||
6. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
|
||||
|
@ -40,7 +40,7 @@ Please install and setup these softwares:
|
||||
1. `su - misskey` Connect to misskey user.
|
||||
2. `git clone -b master git://github.com/syuilo/misskey.git` Clone the misskey repo from master branch.
|
||||
3. `cd misskey` Navigate to misskey directory
|
||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest)
|
||||
4. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` Checkout to the [latest release](https://github.com/syuilo/misskey/releases/latest)
|
||||
5. `npm install` Install misskey dependencies.
|
||||
|
||||
*5.* Configure Misskey
|
||||
@ -109,7 +109,7 @@ You can check if the service is running with `systemctl status misskey`.
|
||||
|
||||
### How to update your Misskey server to the latest version
|
||||
1. `git fetch`
|
||||
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
|
||||
2. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)`
|
||||
3. `npm install`
|
||||
4. `NODE_ENV=production npm run build`
|
||||
5. Check [ChangeLog](../CHANGELOG.md) for migration information
|
||||
|
@ -40,7 +40,7 @@ Installez les paquets suivants :
|
||||
1. `su - misskey` Basculez vers l'utilisateur misskey.
|
||||
2. `git clone -b master git://github.com/syuilo/misskey.git` Clonez la branche master du dépôt misskey.
|
||||
3. `cd misskey` Accédez au dossier misskey.
|
||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` Checkout sur le tag de la [version la plus récente](https://github.com/syuilo/misskey/releases/latest)
|
||||
4. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` Checkout sur le tag de la [version la plus récente](https://github.com/syuilo/misskey/releases/latest)
|
||||
5. `npm install` Installez les dépendances de misskey.
|
||||
|
||||
*5.* Création du fichier de configuration
|
||||
@ -103,7 +103,7 @@ Vous pouvez vérifier si le service a démarré en utilisant la commande `system
|
||||
|
||||
### Méthode de mise à jour vers la plus récente version de Misskey
|
||||
1. `git fetch`
|
||||
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
|
||||
2. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)`
|
||||
3. `npm install`
|
||||
4. `NODE_ENV=production npm run build`
|
||||
5. Consultez [ChangeLog](../CHANGELOG.md) pour les information de migration.
|
||||
|
@ -47,7 +47,7 @@ adduser --disabled-password --disabled-login misskey
|
||||
1. `su - misskey` misskeyユーザーを使用
|
||||
2. `git clone -b master git://github.com/syuilo/misskey.git` masterブランチからMisskeyレポジトリをクローン
|
||||
3. `cd misskey` misskeyディレクトリに移動
|
||||
4. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
|
||||
4. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)` [最新のリリース](https://github.com/syuilo/misskey/releases/latest)を確認
|
||||
5. `npm install` Misskeyの依存パッケージをインストール
|
||||
|
||||
*5.* 設定ファイルを作成する
|
||||
@ -115,7 +115,7 @@ CentOSで1024以下のポートを使用してMisskeyを使用する場合は`Ex
|
||||
|
||||
### Misskeyを最新バージョンにアップデートする方法:
|
||||
1. `git fetch`
|
||||
2. `git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)`
|
||||
2. `git checkout $(git tag -l | grep -Ev -- '-(rc|alpha)\.[0-9]+$' | sort -V | tail -n 1)`
|
||||
3. `npm install`
|
||||
4. `NODE_ENV=production npm run build`
|
||||
5. [ChangeLog](../CHANGELOG.md)でマイグレーション情報を確認する
|
||||
|
@ -49,6 +49,7 @@ gulp.task('build:copy:views', () =>
|
||||
|
||||
gulp.task('build:copy', gulp.parallel('build:copy:views', () =>
|
||||
gulp.src([
|
||||
'./build/Release/crypto_key.node',
|
||||
'./src/const.json',
|
||||
'./src/server/web/views/**/*',
|
||||
'./src/**/assets/**/*',
|
||||
|
@ -73,6 +73,12 @@ common:
|
||||
followers: "フォロワー"
|
||||
favorites: "お気に入り"
|
||||
|
||||
permissions:
|
||||
'read:account': "アカウントの情報を見る"
|
||||
'write:account': "アカウントの情報を変更する"
|
||||
'read:drive': "ドライブを見る"
|
||||
'write:drive': "ドライブを操作する"
|
||||
|
||||
empty-timeline-info:
|
||||
follow-users-to-make-your-timeline: "ユーザーをフォローすると投稿がタイムラインに表示されます。"
|
||||
explore: "ユーザーを探索する"
|
||||
@ -299,15 +305,6 @@ common:
|
||||
auth/views/form.vue:
|
||||
share-access: "<i>{name}</i>があなたのアカウントにアクセスすることを許可しますか?"
|
||||
permission-ask: "このアプリは次の権限を要求しています:"
|
||||
account-read: "アカウントの情報を見る。"
|
||||
account-write: "アカウントの情報を操作する。"
|
||||
note-write: "投稿する。"
|
||||
like-write: "いいねしたりいいね解除する。"
|
||||
following-write: "フォローしたりフォロー解除する。"
|
||||
drive-read: "ドライブを見る。"
|
||||
drive-write: "ドライブを操作する。"
|
||||
notification-read: "通知を見る。"
|
||||
notification-write: "通知を操作する。"
|
||||
cancel: "キャンセル"
|
||||
accept: "アクセスを許可"
|
||||
|
||||
@ -1085,9 +1082,6 @@ desktop/views/components/settings.tags.vue:
|
||||
add: "追加"
|
||||
save: "保存"
|
||||
|
||||
desktop/views/components/taskmanager.vue:
|
||||
title: "タスクマネージャ"
|
||||
|
||||
desktop/views/components/timeline.vue:
|
||||
home: "ホーム"
|
||||
local: "ローカル"
|
||||
@ -1818,12 +1812,3 @@ dev/views/new-app.vue:
|
||||
authority: "権限"
|
||||
authority-desc: "ここで要求した機能だけがAPIからアクセスできます。"
|
||||
authority-warning: "アプリ作成後も変更できますが、新たな権限を付与する場合、その時点で関連付けられているユーザーキーはすべて無効になります。"
|
||||
account-read: "アカウントの情報を見る。"
|
||||
account-write: "アカウントの情報を操作する。"
|
||||
note-write: "投稿する。"
|
||||
reaction-write: "リアクションしたりリアクションをキャンセルする。"
|
||||
following-write: "フォローしたりフォロー解除する。"
|
||||
drive-read: "ドライブを見る。"
|
||||
drive-write: "ドライブを操作する。"
|
||||
notification-read: "通知を見る。"
|
||||
notification-write: "通知を操作する。"
|
||||
|
15
package.json
15
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "11.0.0-alpha.1",
|
||||
"version": "11.0.0-beta.1",
|
||||
"codename": "daybreak",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -12,6 +12,7 @@
|
||||
"scripts": {
|
||||
"start": "node ./index.js",
|
||||
"init": "node ./built/init.js",
|
||||
"migrate": "node ./built/migrate.js",
|
||||
"debug": "DEBUG=misskey:* node ./index.js",
|
||||
"build": "webpack && gulp build",
|
||||
"webpack": "webpack",
|
||||
@ -66,6 +67,8 @@
|
||||
"@types/lolex": "3.1.1",
|
||||
"@types/minio": "7.0.1",
|
||||
"@types/mocha": "5.2.6",
|
||||
"@types/mongodb": "3.1.22",
|
||||
"@types/monk": "6.0.0",
|
||||
"@types/node": "11.10.4",
|
||||
"@types/nodemailer": "4.6.6",
|
||||
"@types/nprogress": "0.0.29",
|
||||
@ -96,7 +99,7 @@
|
||||
"@types/websocket": "0.0.40",
|
||||
"@types/ws": "6.0.1",
|
||||
"animejs": "3.0.1",
|
||||
"apexcharts": "3.6.5",
|
||||
"apexcharts": "3.6.6",
|
||||
"autobind-decorator": "2.4.0",
|
||||
"autosize": "4.0.2",
|
||||
"autwh": "0.1.0",
|
||||
@ -168,7 +171,10 @@
|
||||
"mocha": "6.0.2",
|
||||
"moji": "0.5.1",
|
||||
"moment": "2.24.0",
|
||||
"mongodb": "3.2.3",
|
||||
"monk": "6.0.6",
|
||||
"ms": "2.1.1",
|
||||
"nan": "2.12.1",
|
||||
"nested-property": "0.0.7",
|
||||
"node-fetch": "2.3.0",
|
||||
"nodemailer": "5.1.1",
|
||||
@ -234,7 +240,7 @@
|
||||
"video-thumbnail-generator": "1.1.3",
|
||||
"vue": "2.6.10",
|
||||
"vue-color": "2.7.0",
|
||||
"vue-content-loading": "1.5.3",
|
||||
"vue-content-loading": "1.6.0",
|
||||
"vue-cropperjs": "3.0.0",
|
||||
"vue-i18n": "8.10.0",
|
||||
"vue-js-modal": "1.3.28",
|
||||
@ -242,7 +248,7 @@
|
||||
"vue-loader": "15.7.0",
|
||||
"vue-marquee-text-component": "1.1.1",
|
||||
"vue-prism-component": "1.1.1",
|
||||
"vue-router": "3.0.2",
|
||||
"vue-router": "3.0.3",
|
||||
"vue-sequential-entrance": "1.1.3",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"vue-svg-inline-loader": "1.2.15",
|
||||
@ -252,7 +258,6 @@
|
||||
"vuex": "3.1.0",
|
||||
"vuex-persistedstate": "2.5.4",
|
||||
"web-push": "3.3.3",
|
||||
"webfinger.js": "2.7.0",
|
||||
"webpack": "4.28.4",
|
||||
"webpack-cli": "3.2.3",
|
||||
"websocket": "1.0.28",
|
||||
|
65
src/@types/webfinger.js.d.ts
vendored
65
src/@types/webfinger.js.d.ts
vendored
@ -1,65 +0,0 @@
|
||||
declare module 'webfinger.js' {
|
||||
interface IWebFingerConstructorConfig {
|
||||
tls_only?: boolean;
|
||||
webfist_fallback?: boolean;
|
||||
uri_fallback?: boolean;
|
||||
request_timeout?: number;
|
||||
}
|
||||
|
||||
type JRDProperties = { [type: string]: string };
|
||||
|
||||
interface IJRDLink {
|
||||
rel: string;
|
||||
type?: string;
|
||||
href?: string;
|
||||
template?: string;
|
||||
titles?: { [lang: string]: string };
|
||||
properties?: JRDProperties;
|
||||
}
|
||||
|
||||
interface IJRD {
|
||||
subject?: string;
|
||||
expires?: Date;
|
||||
aliases?: string[];
|
||||
properties?: JRDProperties;
|
||||
links?: IJRDLink[];
|
||||
}
|
||||
|
||||
interface IIDXLinks {
|
||||
'avatar': IJRDLink[];
|
||||
'remotestorage': IJRDLink[];
|
||||
'blog': IJRDLink[];
|
||||
'vcard': IJRDLink[];
|
||||
'updates': IJRDLink[];
|
||||
'share': IJRDLink[];
|
||||
'profile': IJRDLink[];
|
||||
'webfist': IJRDLink[];
|
||||
'camlistore': IJRDLink[];
|
||||
[type: string]: IJRDLink[];
|
||||
}
|
||||
|
||||
interface IIDXProperties {
|
||||
'name': string;
|
||||
[type: string]: string;
|
||||
}
|
||||
|
||||
interface IIDX {
|
||||
links: IIDXLinks;
|
||||
properties: IIDXProperties;
|
||||
}
|
||||
|
||||
interface ILookupCallbackResult {
|
||||
object: IJRD;
|
||||
json: string;
|
||||
idx: IIDX;
|
||||
}
|
||||
|
||||
type LookupCallback = (err: Error | string, result?: ILookupCallbackResult) => void;
|
||||
|
||||
export class WebFinger {
|
||||
constructor(config?: IWebFingerConstructorConfig);
|
||||
|
||||
public lookup(address: string, cb: LookupCallback): NodeJS.Timeout;
|
||||
public lookupLink(address: string, rel: string, cb: IJRDLink): void;
|
||||
}
|
||||
}
|
@ -153,7 +153,7 @@ export default Vue.extend({
|
||||
|
||||
thumbnail(file: any): any {
|
||||
return {
|
||||
'background-color': file.properties.avgColor && file.properties.avgColor.length == 3 ? `rgb(${file.properties.avgColor.join(',')})` : 'transparent',
|
||||
'background-color': file.properties.avgColor || 'transparent',
|
||||
'background-image': `url(${file.thumbnailUrl})`
|
||||
};
|
||||
},
|
||||
|
@ -14,15 +14,7 @@
|
||||
<h2>{{ $t('permission-ask') }}</h2>
|
||||
<ul>
|
||||
<template v-for="p in app.permission">
|
||||
<li v-if="p == 'read:account'">{{ $t('read:account') }}</li>
|
||||
<li v-if="p == 'write:account'">{{ $t('write:account') }}</li>
|
||||
<li v-if="p == 'write:notes'">{{ $t('write:notes') }}</li>
|
||||
<li v-if="p == 'like-write'">{{ $t('like-write') }}</li>
|
||||
<li v-if="p == 'write:following'">{{ $t('write:following') }}</li>
|
||||
<li v-if="p == 'read:drive'">{{ $t('read:drive') }}</li>
|
||||
<li v-if="p == 'write:drive'">{{ $t('write:drive') }}</li>
|
||||
<li v-if="p == 'read:notifications'">{{ $t('read:notifications') }}</li>
|
||||
<li v-if="p == 'write:notifications'">{{ $t('write:notifications') }}</li>
|
||||
<li :key="p">{{ $t(`@.permissions.${p}`) }}</li>
|
||||
</template>
|
||||
</ul>
|
||||
</section>
|
||||
|
@ -55,12 +55,7 @@ export default Vue.extend({
|
||||
},
|
||||
icon(): any {
|
||||
return {
|
||||
backgroundColor: this.user.avatarColor ? this.lightmode
|
||||
? this.user.avatarColor
|
||||
: this.user.avatarColor.startsWith('rgb(')
|
||||
? this.user.avatarColor
|
||||
: null
|
||||
: null,
|
||||
backgroundColor: this.user.avatarColor,
|
||||
backgroundImage: this.lightmode ? null : `url(${this.url})`,
|
||||
borderRadius: this.$store.state.settings.circleIcons ? '100%' : null
|
||||
};
|
||||
|
@ -111,9 +111,7 @@ export default Vue.extend({
|
||||
: false;
|
||||
},
|
||||
background(): string {
|
||||
return this.file.properties.avgColor && this.file.properties.avgColor.length == 3
|
||||
? `rgb(${this.file.properties.avgColor.join(',')})`
|
||||
: 'transparent';
|
||||
return this.file.properties.avgColor || 'transparent';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -122,10 +120,10 @@ export default Vue.extend({
|
||||
},
|
||||
methods: {
|
||||
onThumbnailLoaded() {
|
||||
if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) {
|
||||
if (this.file.properties.avgColor) {
|
||||
anime({
|
||||
targets: this.$refs.thumbnail,
|
||||
backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
|
||||
backgroundColor: this.file.properties.avgColor.replace('255)', '0)'),
|
||||
duration: 100,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
@ -52,7 +52,7 @@ export default Vue.extend({
|
||||
}
|
||||
|
||||
return {
|
||||
'background-color': this.image.properties.avgColor && this.image.properties.avgColor.length == 3 ? `rgb(${this.image.properties.avgColor.join(',')})` : 'transparent',
|
||||
'background-color': this.image.properties.avgColor || 'transparent',
|
||||
'background-image': url
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
<div class="file" v-if="message.file">
|
||||
<a :href="message.file.url" target="_blank" :title="message.file.name">
|
||||
<img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"
|
||||
:style="{ backgroundColor: message.file.properties.avgColor && message.file.properties.avgColor.length == 3 ? `rgb(${message.file.properties.avgColor.join(',')})` : 'transparent' }"/>
|
||||
:style="{ backgroundColor: message.file.properties.avgColor || 'transparent' }"/>
|
||||
<p v-else>{{ message.file.name }}</p>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -165,7 +165,7 @@ export default Vue.extend({
|
||||
bannerStyle(): any {
|
||||
if (this.$store.state.i.bannerUrl == null) return {};
|
||||
return {
|
||||
backgroundColor: this.$store.state.i.bannerColor ? this.$store.state.i.bannerColor : null,
|
||||
backgroundColor: this.$store.state.i.bannerColor,
|
||||
backgroundImage: `url(${ this.$store.state.i.bannerUrl })`
|
||||
};
|
||||
},
|
||||
|
@ -8,7 +8,7 @@
|
||||
<div class="no-users" v-if="inited && us.length == 0">
|
||||
<p>{{ $t('no-users') }}</p>
|
||||
</div>
|
||||
<div class="user" v-for="user in us">
|
||||
<div class="user" v-for="user in us" :key="user.id">
|
||||
<mk-avatar class="avatar" :user="user"/>
|
||||
<div class="body" v-if="!iconOnly">
|
||||
<div class="name">
|
||||
@ -18,6 +18,7 @@
|
||||
<div class="description" v-if="user.description" :title="user.description">
|
||||
<mfm :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :should-break="false" :plain-text="true"/>
|
||||
</div>
|
||||
<mk-follow-button class="follow-button" v-if="$store.getters.isSignedIn && user.id != $store.state.i.id" :user="user" mini/>
|
||||
</div>
|
||||
</div>
|
||||
<button class="more" :class="{ fetching: fetchingMoreUsers }" v-if="cursor != null" @click="fetchMoreUsers()" :disabled="fetchingMoreUsers">
|
||||
@ -50,7 +51,7 @@ export default Vue.extend({
|
||||
fetchingMoreUsers: false,
|
||||
us: [],
|
||||
inited: false,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
},
|
||||
|
||||
@ -160,6 +161,12 @@ export default Vue.extend({
|
||||
text-overflow ellipsis
|
||||
opacity 0.7
|
||||
font-size 14px
|
||||
padding-right 40px
|
||||
|
||||
> .follow-button
|
||||
position absolute
|
||||
top 8px
|
||||
right 0px
|
||||
|
||||
> .more
|
||||
display block
|
||||
|
@ -3,7 +3,7 @@
|
||||
<x-notifications-column v-else-if="column.type == 'notifications'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type == 'home'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type == 'local'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type == 'social'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type == 'hybrid'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type == 'global'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type == 'list'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
<x-tl-column v-else-if="column.type == 'hashtag'" :column="column" :is-stacked="isStacked" v-on="$listeners"/>
|
||||
|
@ -28,12 +28,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -37,12 +37,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -41,12 +41,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -41,12 +41,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -27,12 +27,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -26,8 +26,8 @@
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<footer v-if="cursor != null">
|
||||
<button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<footer v-if="more">
|
||||
<button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
|
||||
<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
|
||||
</button>
|
||||
@ -61,7 +61,7 @@ export default Vue.extend({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
inited: false,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
},
|
||||
|
||||
@ -119,7 +119,7 @@ export default Vue.extend({
|
||||
this.notes = x;
|
||||
} else {
|
||||
this.notes = x.notes;
|
||||
this.cursor = x.cursor;
|
||||
this.more = x.more;
|
||||
}
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
@ -129,12 +129,12 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
more() {
|
||||
if (this.cursor == null || this.moreFetching) return;
|
||||
fetchMore() {
|
||||
if (!this.more || this.moreFetching) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.cursor).then(x => {
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.cursor = x.cursor;
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
}, e => {
|
||||
this.moreFetching = false;
|
||||
@ -157,6 +157,7 @@ export default Vue.extend({
|
||||
// オーバーフローしたら古い投稿は捨てる
|
||||
if (this.notes.length >= displayLimit) {
|
||||
this.notes = this.notes.slice(0, displayLimit);
|
||||
this.more = true;
|
||||
}
|
||||
} else {
|
||||
this.queue.push(note);
|
||||
@ -179,7 +180,7 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
onBottom() {
|
||||
this.more();
|
||||
this.fetchMore();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -39,7 +39,7 @@ export default Vue.extend({
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -3,7 +3,7 @@
|
||||
<template #header>
|
||||
<fa v-if="column.type == 'home'" icon="home"/>
|
||||
<fa v-if="column.type == 'local'" :icon="['far', 'comments']"/>
|
||||
<fa v-if="column.type == 'social'" icon="share-alt"/>
|
||||
<fa v-if="column.type == 'hybrid'" icon="share-alt"/>
|
||||
<fa v-if="column.type == 'global'" icon="globe"/>
|
||||
<fa v-if="column.type == 'list'" icon="list"/>
|
||||
<fa v-if="column.type == 'hashtag'" icon="hashtag"/>
|
||||
@ -80,7 +80,7 @@ export default Vue.extend({
|
||||
switch (this.column.type) {
|
||||
case 'home': return this.$t('@deck.home');
|
||||
case 'local': return this.$t('@deck.local');
|
||||
case 'social': return this.$t('@deck.social');
|
||||
case 'hybrid': return this.$t('@deck.hybrid');
|
||||
case 'global': return this.$t('@deck.global');
|
||||
case 'list': return this.column.list.name;
|
||||
case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title;
|
||||
|
@ -51,7 +51,7 @@ export default Vue.extend({
|
||||
switch (this.src) {
|
||||
case 'home': return this.$root.stream.useSharedConnection('homeTimeline');
|
||||
case 'local': return this.$root.stream.useSharedConnection('localTimeline');
|
||||
case 'social': return this.$root.stream.useSharedConnection('socialTimeline');
|
||||
case 'hybrid': return this.$root.stream.useSharedConnection('hybridTimeline');
|
||||
case 'global': return this.$root.stream.useSharedConnection('globalTimeline');
|
||||
}
|
||||
},
|
||||
@ -60,7 +60,7 @@ export default Vue.extend({
|
||||
switch (this.src) {
|
||||
case 'home': return 'notes/timeline';
|
||||
case 'local': return 'notes/local-timeline';
|
||||
case 'social': return 'notes/social-timeline';
|
||||
case 'hybrid': return 'notes/hybrid-timeline';
|
||||
case 'global': return 'notes/global-timeline';
|
||||
}
|
||||
},
|
||||
@ -85,12 +85,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -107,7 +107,7 @@ export default Vue.extend({
|
||||
|
||||
this.$root.getMeta().then(meta => {
|
||||
this.disabled = !this.$store.state.i.isModerator && !this.$store.state.i.isAdmin && (
|
||||
meta.disableLocalTimeline && ['local', 'social'].includes(this.src) ||
|
||||
meta.disableLocalTimeline && ['local', 'hybrid'].includes(this.src) ||
|
||||
meta.disableGlobalTimeline && ['global'].includes(this.src));
|
||||
});
|
||||
},
|
||||
|
@ -95,12 +95,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -88,7 +88,7 @@ export default Vue.extend({
|
||||
if (this.user == null) return {};
|
||||
if (this.user.bannerUrl == null) return {};
|
||||
return {
|
||||
backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
|
||||
backgroundColor: this.user.bannerColor,
|
||||
backgroundImage: `url(${ this.user.bannerUrl })`
|
||||
};
|
||||
},
|
||||
|
@ -145,11 +145,11 @@ export default Vue.extend({
|
||||
}
|
||||
}, {
|
||||
icon: 'share-alt',
|
||||
text: this.$t('@deck.social'),
|
||||
text: this.$t('@deck.hybrid'),
|
||||
action: () => {
|
||||
this.$store.commit('device/addDeckColumn', {
|
||||
id: uuid(),
|
||||
type: 'social'
|
||||
type: 'hybrid'
|
||||
});
|
||||
}
|
||||
}, {
|
||||
@ -302,7 +302,7 @@ export default Vue.extend({
|
||||
|
||||
isTlColumn(id) {
|
||||
const column = this.columns.find(c => c.id === id);
|
||||
return ['home', 'local', 'social', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type);
|
||||
return ['home', 'local', 'hybrid', 'global', 'list', 'hashtag', 'mentions', 'direct'].includes(column.type);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -57,7 +57,7 @@ export default Vue.extend({
|
||||
bannerStyle(): any {
|
||||
if (this.user.bannerUrl == null) return {};
|
||||
return {
|
||||
backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
|
||||
backgroundColor: this.user.bannerColor,
|
||||
backgroundImage: `url(${ this.user.bannerUrl })`
|
||||
};
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ export default Vue.extend({
|
||||
} else {
|
||||
return {
|
||||
users: followings.map(following => following.follower),
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
@ -30,7 +30,7 @@ export default Vue.extend({
|
||||
} else {
|
||||
return {
|
||||
users: followings.map(following => following.followee),
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
@ -176,10 +176,22 @@ export default define({
|
||||
post() {
|
||||
this.posting = true;
|
||||
|
||||
let visibility = 'public';
|
||||
let localOnly = false;
|
||||
|
||||
const m = this.$store.state.settings.defaultNoteVisibility.match(/^local-(.+)/);
|
||||
if (m) {
|
||||
visibility = m[1];
|
||||
localOnly = true;
|
||||
} else {
|
||||
visibility = this.$store.state.settings.defaultNoteVisibility;
|
||||
}
|
||||
|
||||
this.$root.api('notes/create', {
|
||||
text: this.text == '' ? undefined : this.text,
|
||||
fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined,
|
||||
visibility: this.$store.state.settings.defaultNoteVisibility
|
||||
visibility,
|
||||
localOnly,
|
||||
}).then(data => {
|
||||
this.clear();
|
||||
}).catch(err => {
|
||||
|
@ -139,10 +139,10 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
onThumbnailLoaded() {
|
||||
if (this.file.properties.avgColor && this.file.properties.avgColor.length == 3) {
|
||||
if (this.file.properties.avgColor) {
|
||||
anime({
|
||||
targets: this.$refs.thumbnail,
|
||||
backgroundColor: `rgba(${this.file.properties.avgColor.join(',')}, 0)`,
|
||||
backgroundColor: this.file.properties.avgColor.replace('255)', '0)'),
|
||||
duration: 100,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
@ -25,8 +25,8 @@
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<footer v-if="cursor != null">
|
||||
<button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<footer v-if="more">
|
||||
<button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
|
||||
<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
|
||||
</button>
|
||||
@ -58,7 +58,7 @@ export default Vue.extend({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
inited: false,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
},
|
||||
|
||||
@ -112,7 +112,7 @@ export default Vue.extend({
|
||||
this.notes = x;
|
||||
} else {
|
||||
this.notes = x.notes;
|
||||
this.cursor = x.cursor;
|
||||
this.more = x.more;
|
||||
}
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
@ -122,12 +122,12 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
more() {
|
||||
if (this.cursor == null || this.moreFetching) return;
|
||||
fetchMore() {
|
||||
if (!this.more || this.moreFetching) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.cursor).then(x => {
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.cursor = x.cursor;
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
}, e => {
|
||||
this.moreFetching = false;
|
||||
@ -157,6 +157,7 @@ export default Vue.extend({
|
||||
// オーバーフローしたら古い投稿は捨てる
|
||||
if (this.notes.length >= displayLimit) {
|
||||
this.notes = this.notes.slice(0, displayLimit);
|
||||
this.more = true;
|
||||
}
|
||||
} else {
|
||||
this.queue.push(note);
|
||||
@ -181,7 +182,7 @@ export default Vue.extend({
|
||||
|
||||
if (this.$store.state.settings.fetchOnScroll !== false) {
|
||||
const current = window.scrollY + window.innerHeight;
|
||||
if (current > document.body.offsetHeight - 8) this.more();
|
||||
if (current > document.body.offsetHeight - 8) this.fetchMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,12 +30,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -6,7 +6,7 @@
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
<div class="more" v-if="existMore">
|
||||
<ui-button inline @click="more">{{ $t('@.load-more') }}</ui-button>
|
||||
<ui-button inline @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -48,7 +48,7 @@ export default Vue.extend({
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
more() {
|
||||
fetchMore() {
|
||||
this.moreFetching = true;
|
||||
this.$root.api('i/favorites', {
|
||||
limit: 11,
|
||||
|
@ -35,7 +35,7 @@ export default Vue.extend({
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -35,7 +35,7 @@ export default Vue.extend({
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -77,9 +77,9 @@ export default Vue.extend({
|
||||
this.endpoint = 'notes/local-timeline';
|
||||
this.connection = this.$root.stream.useSharedConnection('localTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'social') {
|
||||
this.endpoint = 'notes/social-timeline';
|
||||
this.connection = this.$root.stream.useSharedConnection('socialTimeline');
|
||||
} else if (this.src == 'hybrid') {
|
||||
this.endpoint = 'notes/hybrid-timeline';
|
||||
this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'global') {
|
||||
this.endpoint = 'notes/global-timeline';
|
||||
@ -113,12 +113,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -6,7 +6,7 @@
|
||||
<header class="zahtxcqi">
|
||||
<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
|
||||
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
|
||||
<span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span>
|
||||
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
|
||||
<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span>
|
||||
<span :data-active="src == 'tag'" @click="src = 'tag'" v-if="tagTl"><fa icon="hashtag"/> {{ tagTl.title }}</span>
|
||||
<span :data-active="src == 'list'" @click="src = 'list'" v-if="list"><fa icon="list"/> {{ list.name }}</span>
|
||||
@ -78,7 +78,7 @@ export default Vue.extend({
|
||||
) && this.src === 'global') this.src = 'local';
|
||||
if (!(
|
||||
this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
|
||||
) && ['local', 'social'].includes(this.src)) this.src = 'home';
|
||||
) && ['local', 'hybrid'].includes(this.src)) this.src = 'home';
|
||||
});
|
||||
|
||||
if (this.$store.state.device.tl) {
|
||||
@ -89,7 +89,7 @@ export default Vue.extend({
|
||||
this.tagTl = this.$store.state.device.tl.arg;
|
||||
}
|
||||
} else if (this.$store.state.i.followingCount == 0) {
|
||||
this.src = 'social';
|
||||
this.src = 'hybrid';
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -65,7 +65,7 @@ export default Vue.extend({
|
||||
style(): any {
|
||||
if (this.user.bannerUrl == null) return {};
|
||||
return {
|
||||
backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
|
||||
backgroundColor: this.user.bannerColor,
|
||||
backgroundImage: `url(${ this.user.bannerUrl })`
|
||||
};
|
||||
},
|
||||
|
@ -42,12 +42,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -85,8 +85,8 @@ export default Vue.extend({
|
||||
},
|
||||
|
||||
style(): any {
|
||||
return this.file.properties.avgColor && this.file.properties.avgColor.length == 3 ? {
|
||||
'background-color': `rgb(${ this.file.properties.avgColor.join(',') })`
|
||||
return this.file.properties.avgColor ? {
|
||||
'background-color': this.file.properties.avgColor
|
||||
} : {};
|
||||
},
|
||||
|
||||
|
@ -21,8 +21,8 @@
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<footer v-if="cursor != null">
|
||||
<button @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<footer v-if="more">
|
||||
<button @click="fetchMore()" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
|
||||
<template v-if="!moreFetching">{{ $t('@.load-more') }}</template>
|
||||
<template v-if="moreFetching"><fa icon="spinner" pulse fixed-width/></template>
|
||||
</button>
|
||||
@ -53,7 +53,7 @@ export default Vue.extend({
|
||||
fetching: true,
|
||||
moreFetching: false,
|
||||
inited: false,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
},
|
||||
|
||||
@ -113,7 +113,7 @@ export default Vue.extend({
|
||||
this.notes = x;
|
||||
} else {
|
||||
this.notes = x.notes;
|
||||
this.cursor = x.cursor;
|
||||
this.more = x.more;
|
||||
}
|
||||
this.inited = true;
|
||||
this.fetching = false;
|
||||
@ -123,12 +123,12 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
more() {
|
||||
if (this.cursor == null || this.moreFetching) return;
|
||||
fetchMore() {
|
||||
if (!this.more || this.moreFetching) return;
|
||||
this.moreFetching = true;
|
||||
this.makePromise(this.cursor).then(x => {
|
||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||
this.notes = this.notes.concat(x.notes);
|
||||
this.cursor = x.cursor;
|
||||
this.more = x.more;
|
||||
this.moreFetching = false;
|
||||
}, e => {
|
||||
this.moreFetching = false;
|
||||
@ -151,6 +151,7 @@ export default Vue.extend({
|
||||
// オーバーフローしたら古い投稿は捨てる
|
||||
if (this.notes.length >= displayLimit) {
|
||||
this.notes = this.notes.slice(0, displayLimit);
|
||||
this.more = true;
|
||||
}
|
||||
} else {
|
||||
this.queue.push(note);
|
||||
@ -180,7 +181,7 @@ export default Vue.extend({
|
||||
if (this.$el.offsetHeight == 0) return;
|
||||
|
||||
const current = window.scrollY + window.innerHeight;
|
||||
if (current > document.body.offsetHeight - 8) this.more();
|
||||
if (current > document.body.offsetHeight - 8) this.fetchMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,12 +27,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -27,12 +27,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -8,7 +8,7 @@
|
||||
<mk-note-detail class="post" :note="favorite.note" :key="favorite.note.id"/>
|
||||
</template>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="existMore" @click="more">{{ $t('@.load-more') }}</ui-button>
|
||||
<ui-button v-if="existMore" @click="fetchMore()">{{ $t('@.load-more') }}</ui-button>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
@ -53,7 +53,7 @@ export default Vue.extend({
|
||||
Progress.done();
|
||||
});
|
||||
},
|
||||
more() {
|
||||
fetchMore() {
|
||||
this.moreFetching = true;
|
||||
this.$root.api('i/favorites', {
|
||||
limit: 11,
|
||||
|
@ -78,9 +78,9 @@ export default Vue.extend({
|
||||
this.endpoint = 'notes/local-timeline';
|
||||
this.connection = this.$root.stream.useSharedConnection('localTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'social') {
|
||||
this.endpoint = 'notes/social-timeline';
|
||||
this.connection = this.$root.stream.useSharedConnection('socialTimeline');
|
||||
} else if (this.src == 'hybrid') {
|
||||
this.endpoint = 'notes/hybrid-timeline';
|
||||
this.connection = this.$root.stream.useSharedConnection('hybridTimeline');
|
||||
this.connection.on('note', prepend);
|
||||
} else if (this.src == 'global') {
|
||||
this.endpoint = 'notes/global-timeline';
|
||||
@ -114,12 +114,12 @@ export default Vue.extend({
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: notes[notes.length - 1].id
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -5,7 +5,7 @@
|
||||
<span :class="$style.title">
|
||||
<span v-if="src == 'home'"><fa icon="home"/>{{ $t('home') }}</span>
|
||||
<span v-if="src == 'local'"><fa :icon="['far', 'comments']"/>{{ $t('local') }}</span>
|
||||
<span v-if="src == 'social'"><fa icon="share-alt"/>{{ $t('social') }}</span>
|
||||
<span v-if="src == 'hybrid'"><fa icon="share-alt"/>{{ $t('hybrid') }}</span>
|
||||
<span v-if="src == 'global'"><fa icon="globe"/>{{ $t('global') }}</span>
|
||||
<span v-if="src == 'mentions'"><fa icon="at"/>{{ $t('mentions') }}</span>
|
||||
<span v-if="src == 'messages'"><fa :icon="['far', 'envelope']"/>{{ $t('messages') }}</span>
|
||||
@ -32,7 +32,7 @@
|
||||
<div>
|
||||
<span :data-active="src == 'home'" @click="src = 'home'"><fa icon="home"/> {{ $t('home') }}</span>
|
||||
<span :data-active="src == 'local'" @click="src = 'local'" v-if="enableLocalTimeline"><fa :icon="['far', 'comments']"/> {{ $t('local') }}</span>
|
||||
<span :data-active="src == 'social'" @click="src = 'social'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('social') }}</span>
|
||||
<span :data-active="src == 'hybrid'" @click="src = 'hybrid'" v-if="enableLocalTimeline"><fa icon="share-alt"/> {{ $t('hybrid') }}</span>
|
||||
<span :data-active="src == 'global'" @click="src = 'global'" v-if="enableGlobalTimeline"><fa icon="globe"/> {{ $t('global') }}</span>
|
||||
<div class="hr"></div>
|
||||
<span :data-active="src == 'mentions'" @click="src = 'mentions'"><fa icon="at"/> {{ $t('mentions') }}<i class="badge" v-if="$store.state.i.hasUnreadMentions"><fa icon="circle"/></i></span>
|
||||
@ -50,7 +50,7 @@
|
||||
<div class="tl">
|
||||
<x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/>
|
||||
<x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/>
|
||||
<x-tl v-if="src == 'social'" ref="tl" key="social" src="social"/>
|
||||
<x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/>
|
||||
<x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/>
|
||||
<x-tl v-if="src == 'mentions'" ref="tl" key="mentions" src="mentions"/>
|
||||
<x-tl v-if="src == 'messages'" ref="tl" key="messages" src="messages"/>
|
||||
@ -120,7 +120,7 @@ export default Vue.extend({
|
||||
) && this.src === 'global') this.src = 'local';
|
||||
if (!(
|
||||
this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
|
||||
) && ['local', 'social'].includes(this.src)) this.src = 'home';
|
||||
) && ['local', 'hybrid'].includes(this.src)) this.src = 'home';
|
||||
});
|
||||
|
||||
if (this.$store.state.device.tl) {
|
||||
@ -131,7 +131,7 @@ export default Vue.extend({
|
||||
this.tagTl = this.$store.state.device.tl.arg;
|
||||
}
|
||||
} else if (this.$store.state.i.followingCount == 0) {
|
||||
this.src = 'social';
|
||||
this.src = 'hybrid';
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -21,19 +21,19 @@ export default Vue.extend({
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
query: this.q
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: cursor ? cursor + limit : limit
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -21,19 +21,19 @@ export default Vue.extend({
|
||||
return {
|
||||
makePromise: cursor => this.$root.api('notes/search-by-tag', {
|
||||
limit: limit + 1,
|
||||
offset: cursor ? cursor : undefined,
|
||||
untilId: cursor ? cursor : undefined,
|
||||
tag: this.$route.params.tag
|
||||
}).then(notes => {
|
||||
if (notes.length == limit + 1) {
|
||||
notes.pop();
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: cursor ? cursor + limit : limit
|
||||
more: true
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
notes: notes,
|
||||
cursor: null
|
||||
more: false
|
||||
};
|
||||
}
|
||||
})
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="stream" v-if="!fetching && images.length > 0">
|
||||
<a v-for="(image, i) in images" :key="i"
|
||||
class="img"
|
||||
:style="`background-image: url(${thumbnail(image.media)})`"
|
||||
:style="`background-image: url(${thumbnail(image.file)})`"
|
||||
:href="image.note | notePage"
|
||||
></a>
|
||||
</div>
|
||||
@ -40,11 +40,11 @@ export default Vue.extend({
|
||||
untilDate: new Date().getTime() + 1000 * 86400 * 365
|
||||
}).then(notes => {
|
||||
for (const note of notes) {
|
||||
for (const media of note.media) {
|
||||
for (const file of note.files) {
|
||||
if (this.images.length < 9) {
|
||||
this.images.push({
|
||||
note,
|
||||
media
|
||||
file
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ export default Vue.extend({
|
||||
style(): any {
|
||||
if (this.user.bannerUrl == null) return {};
|
||||
return {
|
||||
backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
|
||||
backgroundColor: this.user.bannerColor,
|
||||
backgroundImage: `url(${ this.user.bannerUrl })`
|
||||
};
|
||||
}
|
||||
|
@ -25,9 +25,9 @@ export default function load() {
|
||||
|
||||
const mixin = {} as Mixin;
|
||||
|
||||
const url = validateUrl(config.url);
|
||||
const url = tryCreateUrl(config.url);
|
||||
|
||||
config.url = normalizeUrl(config.url);
|
||||
config.url = url.origin;
|
||||
|
||||
config.port = config.port || parseInt(process.env.PORT, 10);
|
||||
|
||||
@ -53,14 +53,3 @@ function tryCreateUrl(url: string) {
|
||||
throw `url="${url}" is not a valid URL.`;
|
||||
}
|
||||
}
|
||||
|
||||
function validateUrl(url: string) {
|
||||
const result = tryCreateUrl(url);
|
||||
if (result.pathname.replace('/', '').length) throw `url="${url}" is not a valid URL, has a pathname.`;
|
||||
if (!url.includes(result.host)) throw `url="${url}" is not a valid URL, has an invalid hostname.`;
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeUrl(url: string) {
|
||||
return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
|
||||
}
|
||||
|
111
src/crypto_key.cc
Normal file
111
src/crypto_key.cc
Normal file
@ -0,0 +1,111 @@
|
||||
#include <nan.h>
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/buffer.h>
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
|
||||
NAN_METHOD(extractPublic)
|
||||
{
|
||||
const auto sourceString = info[0]->ToString(Nan::GetCurrentContext()).ToLocalChecked();
|
||||
if (!sourceString->IsOneByte()) {
|
||||
Nan::ThrowError("Malformed character found");
|
||||
return;
|
||||
}
|
||||
|
||||
size_t sourceLength = sourceString->Length();
|
||||
const auto sourceBuf = new char[sourceLength];
|
||||
|
||||
Nan::DecodeWrite(sourceBuf, sourceLength, sourceString);
|
||||
|
||||
const auto source = BIO_new_mem_buf(sourceBuf, sourceLength);
|
||||
if (source == nullptr) {
|
||||
Nan::ThrowError("Memory allocation failed");
|
||||
delete[] sourceBuf;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto rsa = PEM_read_bio_RSAPrivateKey(source, nullptr, nullptr, nullptr);
|
||||
|
||||
BIO_free(source);
|
||||
delete[] sourceBuf;
|
||||
|
||||
if (rsa == nullptr) {
|
||||
Nan::ThrowError("Decode failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto destination = BIO_new(BIO_s_mem());
|
||||
if (destination == nullptr) {
|
||||
Nan::ThrowError("Memory allocation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto result = PEM_write_bio_RSAPublicKey(destination, rsa);
|
||||
|
||||
RSA_free(rsa);
|
||||
|
||||
if (result != 1) {
|
||||
Nan::ThrowError("Public key extraction failed");
|
||||
BIO_free(destination);
|
||||
return;
|
||||
}
|
||||
|
||||
char *pem;
|
||||
const auto pemLength = BIO_get_mem_data(destination, &pem);
|
||||
|
||||
info.GetReturnValue().Set(Nan::Encode(pem, pemLength));
|
||||
BIO_free(destination);
|
||||
}
|
||||
|
||||
NAN_METHOD(generate)
|
||||
{
|
||||
const auto exponent = BN_new();
|
||||
const auto mem = BIO_new(BIO_s_mem());
|
||||
const auto rsa = RSA_new();
|
||||
char *data;
|
||||
long result;
|
||||
|
||||
if (exponent == nullptr || mem == nullptr || rsa == nullptr) {
|
||||
Nan::ThrowError("Memory allocation failed");
|
||||
goto done;
|
||||
}
|
||||
|
||||
result = BN_set_word(exponent, 65537);
|
||||
if (result != 1) {
|
||||
Nan::ThrowError("Exponent setting failed");
|
||||
goto done;
|
||||
}
|
||||
|
||||
result = RSA_generate_key_ex(rsa, 2048, exponent, nullptr);
|
||||
if (result != 1) {
|
||||
Nan::ThrowError("Key generation failed");
|
||||
goto done;
|
||||
}
|
||||
|
||||
result = PEM_write_bio_RSAPrivateKey(mem, rsa, NULL, NULL, 0, NULL, NULL);
|
||||
if (result != 1) {
|
||||
Nan::ThrowError("Key export failed");
|
||||
goto done;
|
||||
}
|
||||
|
||||
result = BIO_get_mem_data(mem, &data);
|
||||
info.GetReturnValue().Set(Nan::Encode(data, result));
|
||||
|
||||
done:
|
||||
RSA_free(rsa);
|
||||
BIO_free(mem);
|
||||
BN_free(exponent);
|
||||
}
|
||||
|
||||
NAN_MODULE_INIT(InitAll)
|
||||
{
|
||||
Nan::Set(target, Nan::New<v8::String>("extractPublic").ToLocalChecked(),
|
||||
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(extractPublic)).ToLocalChecked());
|
||||
|
||||
Nan::Set(target, Nan::New<v8::String>("generate").ToLocalChecked(),
|
||||
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(generate)).ToLocalChecked());
|
||||
}
|
||||
|
||||
NODE_MODULE(crypto_key, InitAll);
|
2
src/crypto_key.d.ts
vendored
Normal file
2
src/crypto_key.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export function extractPublic(keypair: string): string;
|
||||
export function generate(): string;
|
@ -36,10 +36,10 @@ import { Emoji } from '../models/entities/emoji';
|
||||
import { ReversiGame } from '../models/entities/games/reversi/game';
|
||||
import { ReversiMatching } from '../models/entities/games/reversi/matching';
|
||||
import { UserNotePining } from '../models/entities/user-note-pinings';
|
||||
import { UserServiceLinking } from '../models/entities/user-service-linking';
|
||||
import { Poll } from '../models/entities/poll';
|
||||
import { UserKeypair } from '../models/entities/user-keypair';
|
||||
import { UserPublickey } from '../models/entities/user-publickey';
|
||||
import { UserProfile } from '../models/entities/user-profile';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||
|
||||
@ -101,12 +101,12 @@ export function initDb(justBorrow = false, sync = false, log = false) {
|
||||
AuthSession,
|
||||
AccessToken,
|
||||
User,
|
||||
UserProfile,
|
||||
UserKeypair,
|
||||
UserPublickey,
|
||||
UserList,
|
||||
UserListJoining,
|
||||
UserNotePining,
|
||||
UserServiceLinking,
|
||||
Following,
|
||||
FollowRequest,
|
||||
Muting,
|
||||
|
@ -339,7 +339,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
|
||||
#### `note`
|
||||
ローカルタイムラインに新しい投稿が流れてきたときに発生するイベントです。
|
||||
|
||||
## `socialTimeline`
|
||||
## `hybridTimeline`
|
||||
ソーシャルタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。
|
||||
|
||||
### 流れてくるイベント一覧
|
||||
|
18
src/init.ts
18
src/init.ts
@ -1,16 +1,10 @@
|
||||
import { initDb } from './db/postgre';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
console.log('Connecting database...');
|
||||
await initDb(false, true, true);
|
||||
} catch (e) {
|
||||
console.error('Cannot connect to database', null, true);
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Init database...');
|
||||
|
||||
initDb(false, true, true).then(() => {
|
||||
console.log('Done :)');
|
||||
}
|
||||
|
||||
main();
|
||||
}, e => {
|
||||
console.error('Failed to init database');
|
||||
console.error(e);
|
||||
});
|
||||
|
474
src/migrate.ts
Normal file
474
src/migrate.ts
Normal file
@ -0,0 +1,474 @@
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
import monk from 'monk';
|
||||
import * as mongo from 'mongodb';
|
||||
import * as fs from 'fs';
|
||||
import * as uuid from 'uuid';
|
||||
import chalk from 'chalk';
|
||||
import config from './config';
|
||||
import { initDb } from './db/postgre';
|
||||
import { User } from './models/entities/user';
|
||||
import { getRepository } from 'typeorm';
|
||||
import generateUserToken from './server/api/common/generate-native-user-token';
|
||||
import { DriveFile } from './models/entities/drive-file';
|
||||
import { DriveFolder } from './models/entities/drive-folder';
|
||||
import { InternalStorage } from './services/drive/internal-storage';
|
||||
import { createTemp } from './misc/create-temp';
|
||||
import { Note } from './models/entities/note';
|
||||
import { Following } from './models/entities/following';
|
||||
import { genId } from './misc/gen-id';
|
||||
import { Poll } from './models/entities/poll';
|
||||
import { PollVote } from './models/entities/poll-vote';
|
||||
import { NoteFavorite } from './models/entities/note-favorite';
|
||||
import { NoteReaction } from './models/entities/note-reaction';
|
||||
import { UserPublickey } from './models/entities/user-publickey';
|
||||
import { UserKeypair } from './models/entities/user-keypair';
|
||||
import { extractPublic } from './crypto_key';
|
||||
import { Emoji } from './models/entities/emoji';
|
||||
import { toPuny } from './misc/convert-host';
|
||||
import { UserProfile } from './models/entities/user-profile';
|
||||
|
||||
const u = (config as any).mongodb.user ? encodeURIComponent((config as any).mongodb.user) : null;
|
||||
const p = (config as any).mongodb.pass ? encodeURIComponent((config as any).mongodb.pass) : null;
|
||||
|
||||
const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${(config as any).mongodb.host}:${(config as any).mongodb.port}/${(config as any).mongodb.db}`;
|
||||
|
||||
const db = monk(uri);
|
||||
let mdb: mongo.Db;
|
||||
|
||||
const nativeDbConn = async (): Promise<mongo.Db> => {
|
||||
if (mdb) return mdb;
|
||||
|
||||
const db = await ((): Promise<mongo.Db> => new Promise((resolve, reject) => {
|
||||
mongo.MongoClient.connect(uri, { useNewUrlParser: true }, (e: Error, client: any) => {
|
||||
if (e) return reject(e);
|
||||
resolve(client.db((config as any).mongodb.db));
|
||||
});
|
||||
}))();
|
||||
|
||||
mdb = db;
|
||||
|
||||
return db;
|
||||
};
|
||||
|
||||
const _User = db.get<any>('users');
|
||||
const _DriveFile = db.get<any>('driveFiles.files');
|
||||
const _DriveFolder = db.get<any>('driveFolders');
|
||||
const _Note = db.get<any>('notes');
|
||||
const _Following = db.get<any>('following');
|
||||
const _PollVote = db.get<any>('pollVotes');
|
||||
const _Favorite = db.get<any>('favorites');
|
||||
const _NoteReaction = db.get<any>('noteReactions');
|
||||
const _Emoji = db.get<any>('emoji');
|
||||
const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => {
|
||||
const db = await nativeDbConn();
|
||||
const bucket = new mongo.GridFSBucket(db, {
|
||||
bucketName: 'driveFiles'
|
||||
});
|
||||
return bucket;
|
||||
};
|
||||
|
||||
async function main() {
|
||||
await initDb();
|
||||
const Users = getRepository(User);
|
||||
const UserProfiles = getRepository(UserProfile);
|
||||
const DriveFiles = getRepository(DriveFile);
|
||||
const DriveFolders = getRepository(DriveFolder);
|
||||
const Notes = getRepository(Note);
|
||||
const Followings = getRepository(Following);
|
||||
const Polls = getRepository(Poll);
|
||||
const PollVotes = getRepository(PollVote);
|
||||
const NoteFavorites = getRepository(NoteFavorite);
|
||||
const NoteReactions = getRepository(NoteReaction);
|
||||
const UserPublickeys = getRepository(UserPublickey);
|
||||
const UserKeypairs = getRepository(UserKeypair);
|
||||
const Emojis = getRepository(Emoji);
|
||||
|
||||
async function migrateUser(user: any) {
|
||||
await Users.save({
|
||||
id: user._id.toHexString(),
|
||||
createdAt: user.createdAt || new Date(),
|
||||
username: user.username,
|
||||
usernameLower: user.username.toLowerCase(),
|
||||
host: toPuny(user.host),
|
||||
token: generateUserToken(),
|
||||
isAdmin: user.isAdmin,
|
||||
name: user.name,
|
||||
followersCount: user.followersCount,
|
||||
followingCount: user.followingCount,
|
||||
notesCount: user.notesCount,
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
isVerified: user.isVerified,
|
||||
inbox: user.inbox,
|
||||
sharedInbox: user.sharedInbox,
|
||||
uri: user.uri,
|
||||
});
|
||||
await UserProfiles.save({
|
||||
userId: user._id.toHexString(),
|
||||
description: user.description,
|
||||
userHost: toPuny(user.host),
|
||||
autoAcceptFollowed: true,
|
||||
autoWatch: false,
|
||||
password: user.password,
|
||||
location: user.profile ? user.profile.location : null,
|
||||
birthday: user.profile ? user.profile.birthday : null,
|
||||
});
|
||||
if (user.publicKey) {
|
||||
await UserPublickeys.save({
|
||||
userId: user._id.toHexString(),
|
||||
keyId: user.publicKey.id,
|
||||
keyPem: user.publicKey.publicKeyPem
|
||||
});
|
||||
}
|
||||
if (user.keypair) {
|
||||
await UserKeypairs.save({
|
||||
userId: user._id.toHexString(),
|
||||
publicKey: extractPublic(user.keypair),
|
||||
privateKey: user.keypair,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateFollowing(following: any) {
|
||||
await Followings.save({
|
||||
id: following._id.toHexString(),
|
||||
createdAt: following.createdAt || new Date(),
|
||||
followerId: following.followerId.toHexString(),
|
||||
followeeId: following.followeeId.toHexString(),
|
||||
|
||||
// 非正規化
|
||||
followerHost: following._follower ? toPuny(following._follower.host) : null,
|
||||
followerInbox: following._follower ? following._follower.inbox : null,
|
||||
followerSharedInbox: following._follower ? following._follower.sharedInbox : null,
|
||||
followeeHost: following._followee ? toPuny(following._followee.host) : null,
|
||||
followeeInbox: following._followee ? following._followee.inbox : null,
|
||||
followeeSharedInbox: following._followee ? following._followee.sharedInbo : null
|
||||
});
|
||||
}
|
||||
|
||||
async function migrateDriveFolder(folder: any) {
|
||||
await DriveFolders.save({
|
||||
id: folder._id.toHexString(),
|
||||
createdAt: folder.createdAt || new Date(),
|
||||
name: folder.name,
|
||||
parentId: folder.parentId ? folder.parentId.toHexString() : null,
|
||||
});
|
||||
}
|
||||
|
||||
async function migrateDriveFile(file: any) {
|
||||
const user = await _User.findOne({
|
||||
_id: file.metadata.userId
|
||||
});
|
||||
if (file.metadata.storage && file.metadata.storage.key) { // when object storage
|
||||
await DriveFiles.save({
|
||||
id: file._id.toHexString(),
|
||||
userId: user._id.toHexString(),
|
||||
userHost: toPuny(user.host),
|
||||
createdAt: file.uploadDate || new Date(),
|
||||
md5: file.md5,
|
||||
name: file.filename,
|
||||
type: file.contentType,
|
||||
properties: file.metadata.properties,
|
||||
size: file.length,
|
||||
url: file.metadata.url,
|
||||
uri: file.metadata.uri,
|
||||
accessKey: file.metadata.storage.key,
|
||||
folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null,
|
||||
storedInternal: false,
|
||||
isLink: false
|
||||
});
|
||||
} else if (!file.metadata.isLink) {
|
||||
const [temp, clean] = await createTemp();
|
||||
await new Promise(async (res, rej) => {
|
||||
const bucket = await getDriveFileBucket();
|
||||
const readable = bucket.openDownloadStream(file._id);
|
||||
const dest = fs.createWriteStream(temp);
|
||||
readable.pipe(dest);
|
||||
readable.on('end', () => {
|
||||
dest.end();
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
||||
const key = uuid.v4();
|
||||
const url = InternalStorage.saveFromPath(key, temp);
|
||||
await DriveFiles.save({
|
||||
id: file._id.toHexString(),
|
||||
userId: user._id.toHexString(),
|
||||
userHost: toPuny(user.host),
|
||||
createdAt: file.uploadDate || new Date(),
|
||||
md5: file.md5,
|
||||
name: file.filename,
|
||||
type: file.contentType,
|
||||
properties: file.metadata.properties,
|
||||
size: file.length,
|
||||
url: url,
|
||||
uri: file.metadata.uri,
|
||||
accessKey: key,
|
||||
folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null,
|
||||
storedInternal: true,
|
||||
isLink: false
|
||||
});
|
||||
clean();
|
||||
} else {
|
||||
await DriveFiles.save({
|
||||
id: file._id.toHexString(),
|
||||
userId: user._id.toHexString(),
|
||||
userHost: toPuny(user.host),
|
||||
createdAt: file.uploadDate || new Date(),
|
||||
md5: file.md5,
|
||||
name: file.filename,
|
||||
type: file.contentType,
|
||||
properties: file.metadata.properties,
|
||||
size: file.length,
|
||||
url: file.metadata.url,
|
||||
uri: file.metadata.uri,
|
||||
accessKey: null,
|
||||
folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null,
|
||||
storedInternal: false,
|
||||
isLink: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function migrateNote(note: any) {
|
||||
await Notes.save({
|
||||
id: note._id.toHexString(),
|
||||
createdAt: note.createdAt || new Date(),
|
||||
text: note.text,
|
||||
cw: note.cw || null,
|
||||
tags: note.tags || [],
|
||||
userId: note.userId.toHexString(),
|
||||
viaMobile: note.viaMobile || false,
|
||||
geo: note.geo,
|
||||
appId: null,
|
||||
visibility: note.visibility || 'public',
|
||||
visibleUserIds: note.visibleUserIds ? note.visibleUserIds.map((id: any) => id.toHexString()) : [],
|
||||
replyId: note.replyId ? note.replyId.toHexString() : null,
|
||||
renoteId: note.renoteId ? note.renoteId.toHexString() : null,
|
||||
userHost: null,
|
||||
fileIds: note.fileIds ? note.fileIds.map((id: any) => id.toHexString()) : [],
|
||||
localOnly: note.localOnly || false,
|
||||
hasPoll: note.poll != null
|
||||
});
|
||||
|
||||
if (note.poll) {
|
||||
await Polls.save({
|
||||
id: genId(),
|
||||
noteId: note._id.toHexString(),
|
||||
choices: note.poll.choices.map((x: any) => x.text),
|
||||
expiresAt: note.poll.expiresAt,
|
||||
multiple: note.poll.multiple,
|
||||
votes: note.poll.choices.map((x: any) => x.votes),
|
||||
noteVisibility: note.visibility,
|
||||
userId: note.userId.toHexString(),
|
||||
userHost: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function migratePollVote(vote: any) {
|
||||
await PollVotes.save({
|
||||
id: vote._id.toHexString(),
|
||||
createdAt: vote.createdAt,
|
||||
noteId: vote.noteId.toHexString(),
|
||||
userId: vote.userId.toHexString(),
|
||||
choice: vote.choice
|
||||
});
|
||||
}
|
||||
|
||||
async function migrateNoteFavorite(favorite: any) {
|
||||
await NoteFavorites.save({
|
||||
id: favorite._id.toHexString(),
|
||||
createdAt: favorite.createdAt,
|
||||
noteId: favorite.noteId.toHexString(),
|
||||
userId: favorite.userId.toHexString(),
|
||||
});
|
||||
}
|
||||
|
||||
async function migrateNoteReaction(reaction: any) {
|
||||
await NoteReactions.save({
|
||||
id: reaction._id.toHexString(),
|
||||
createdAt: reaction.createdAt,
|
||||
noteId: reaction.noteId.toHexString(),
|
||||
userId: reaction.userId.toHexString(),
|
||||
reaction: reaction.reaction
|
||||
});
|
||||
}
|
||||
|
||||
async function reMigrateUser(user: any) {
|
||||
const u = await _User.findOne({
|
||||
_id: new mongo.ObjectId(user.id)
|
||||
});
|
||||
const avatar = await DriveFiles.findOne(u.avatarId.toHexString());
|
||||
const banner = await DriveFiles.findOne(u.bannerId.toHexString());
|
||||
await Users.update(user.id, {
|
||||
avatarId: avatar.id,
|
||||
bannerId: banner.id,
|
||||
avatarUrl: avatar.url,
|
||||
bannerUrl: banner.url
|
||||
});
|
||||
}
|
||||
|
||||
async function migrateEmoji(emoji: any) {
|
||||
await Emojis.save({
|
||||
id: emoji._id.toHexString(),
|
||||
updatedAt: emoji.createdAt,
|
||||
aliases: emoji.aliases,
|
||||
url: emoji.url,
|
||||
uri: emoji.uri,
|
||||
host: toPuny(emoji.host),
|
||||
name: emoji.name
|
||||
});
|
||||
}
|
||||
|
||||
const allUsersCount = await _User.count();
|
||||
for (let i = 0; i < allUsersCount; i++) {
|
||||
const user = await _User.findOne({}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migrateUser(user);
|
||||
console.log(`USER (${i + 1}/${allUsersCount}) ${user._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`USER (${i + 1}/${allUsersCount}) ${user._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allFollowingsCount = await _Following.count();
|
||||
for (let i = 0; i < allFollowingsCount; i++) {
|
||||
const following = await _Following.findOne({}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migrateFollowing(following);
|
||||
console.log(`FOLLOWING (${i + 1}/${allFollowingsCount}) ${following._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`FOLLOWING (${i + 1}/${allFollowingsCount}) ${following._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allDriveFoldersCount = await _DriveFolder.count();
|
||||
for (let i = 0; i < allDriveFoldersCount; i++) {
|
||||
const folder = await _DriveFolder.findOne({}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migrateDriveFolder(folder);
|
||||
console.log(`FOLDER (${i + 1}/${allDriveFoldersCount}) ${folder._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`FOLDER (${i + 1}/${allDriveFoldersCount}) ${folder._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allDriveFilesCount = await _DriveFile.count();
|
||||
for (let i = 0; i < allDriveFilesCount; i++) {
|
||||
const file = await _DriveFile.findOne({}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migrateDriveFile(file);
|
||||
console.log(`FILE (${i + 1}/${allDriveFilesCount}) ${file._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`FILE (${i + 1}/${allDriveFilesCount}) ${file._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allNotesCount = await _Note.count({
|
||||
'_user.host': null
|
||||
});
|
||||
for (let i = 0; i < allNotesCount; i++) {
|
||||
const note = await _Note.findOne({
|
||||
'_user.host': null
|
||||
}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migrateNote(note);
|
||||
console.log(`NOTE (${i + 1}/${allNotesCount}) ${note._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`NOTE (${i + 1}/${allNotesCount}) ${note._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allPollVotesCount = await _PollVote.count();
|
||||
for (let i = 0; i < allPollVotesCount; i++) {
|
||||
const vote = await _PollVote.findOne({}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migratePollVote(vote);
|
||||
console.log(`VOTE (${i + 1}/${allPollVotesCount}) ${vote._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`VOTE (${i + 1}/${allPollVotesCount}) ${vote._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allNoteFavoritesCount = await _Favorite.count();
|
||||
for (let i = 0; i < allNoteFavoritesCount; i++) {
|
||||
const favorite = await _Favorite.findOne({}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migrateNoteFavorite(favorite);
|
||||
console.log(`FAVORITE (${i + 1}/${allNoteFavoritesCount}) ${favorite._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`FAVORITE (${i + 1}/${allNoteFavoritesCount}) ${favorite._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allNoteReactionsCount = await _NoteReaction.count();
|
||||
for (let i = 0; i < allNoteReactionsCount; i++) {
|
||||
const reaction = await _NoteReaction.findOne({}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migrateNoteReaction(reaction);
|
||||
console.log(`REACTION (${i + 1}/${allNoteReactionsCount}) ${reaction._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`REACTION (${i + 1}/${allNoteReactionsCount}) ${reaction._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allActualUsersCount = await Users.count();
|
||||
for (let i = 0; i < allActualUsersCount; i++) {
|
||||
const [user] = await Users.find({
|
||||
take: 1,
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await reMigrateUser(user);
|
||||
console.log(`RE:USER (${i + 1}/${allActualUsersCount}) ${user.id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`RE:USER (${i + 1}/${allActualUsersCount}) ${user.id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const allEmojisCount = await _Emoji.count();
|
||||
for (let i = 0; i < allEmojisCount; i++) {
|
||||
const emoji = await _Emoji.findOne({}, {
|
||||
skip: i
|
||||
});
|
||||
try {
|
||||
await migrateEmoji(emoji);
|
||||
console.log(`EMOJI (${i + 1}/${allEmojisCount}) ${emoji._id} ${chalk.green('DONE')}`);
|
||||
} catch (e) {
|
||||
console.log(`EMOJI (${i + 1}/${allEmojisCount}) ${emoji._id} ${chalk.red('ERR')}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('DONE :)');
|
||||
}
|
||||
|
||||
main();
|
@ -1,5 +1,5 @@
|
||||
import Acct from './type';
|
||||
|
||||
export default (user: Acct) => {
|
||||
return user.host === null ? user.username : `${user.username}@${user.host}`;
|
||||
return user.host == null ? user.username : `${user.username}@${user.host}`;
|
||||
};
|
||||
|
@ -1,27 +1,22 @@
|
||||
import config from '../config';
|
||||
import { toUnicode, toASCII } from 'punycode';
|
||||
import { toASCII } from 'punycode';
|
||||
import { URL } from 'url';
|
||||
|
||||
export function getFullApAccount(username: string, host: string) {
|
||||
return host ? `${username}@${toApHost(host)}` : `${username}@${toApHost(config.host)}`;
|
||||
return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`;
|
||||
}
|
||||
|
||||
export function isSelfHost(host: string) {
|
||||
if (host == null) return true;
|
||||
return toApHost(config.host) === toApHost(host);
|
||||
return toPuny(config.host) === toPuny(host);
|
||||
}
|
||||
|
||||
export function extractDbHost(uri: string) {
|
||||
const url = new URL(uri);
|
||||
return toDbHost(url.hostname);
|
||||
return toPuny(url.hostname);
|
||||
}
|
||||
|
||||
export function toDbHost(host: string) {
|
||||
if (host == null) return null;
|
||||
return toUnicode(host.toLowerCase());
|
||||
}
|
||||
|
||||
export function toApHost(host: string) {
|
||||
export function toPuny(host: string) {
|
||||
if (host == null) return null;
|
||||
return toASCII(host.toLowerCase());
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
import getAcct from './acct/render';
|
||||
import getUserName from './get-user-name';
|
||||
import { User } from '../models/entities/user';
|
||||
import { Users } from '../models';
|
||||
|
||||
/**
|
||||
* ユーザーを表す文字列を取得します。
|
||||
* @param user ユーザー
|
||||
*/
|
||||
export default function(user: User): string {
|
||||
let string = `${getUserName(user)} (@${getAcct(user)})\n` +
|
||||
`${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
|
||||
|
||||
if (Users.isLocalUser(user)) {
|
||||
string += `場所: ${user.location}、誕生日: ${user.birthday}\n`;
|
||||
}
|
||||
|
||||
return string + `「${user.description}」`;
|
||||
}
|
@ -95,9 +95,9 @@ export class DriveFile {
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column('varchar', {
|
||||
length: 256,
|
||||
length: 256, nullable: true,
|
||||
})
|
||||
public accessKey: string;
|
||||
public accessKey: string | null;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column('varchar', {
|
||||
@ -150,5 +150,5 @@ export class DriveFile {
|
||||
default: false,
|
||||
comment: 'Whether the DriveFile is direct link to remote server.'
|
||||
})
|
||||
public isRemote: boolean;
|
||||
public isLink: boolean;
|
||||
}
|
||||
|
@ -183,7 +183,7 @@ export class Note {
|
||||
public hasPoll: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
nullable: true, default: {}
|
||||
nullable: true, default: null
|
||||
})
|
||||
public geo: any | null;
|
||||
|
||||
|
@ -4,11 +4,8 @@ import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
export class UserKeypair {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column(id())
|
||||
@PrimaryColumn(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@OneToOne(type => User, {
|
||||
|
@ -1,14 +1,11 @@
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { id } from '../id';
|
||||
import { User } from './user';
|
||||
|
||||
@Entity()
|
||||
export class UserServiceLinking {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
export class UserProfile {
|
||||
@Index({ unique: true })
|
||||
@Column(id())
|
||||
@PrimaryColumn(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@OneToOne(type => User, {
|
||||
@ -17,6 +14,96 @@ export class UserServiceLinking {
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The location of the User.'
|
||||
})
|
||||
public location: string | null;
|
||||
|
||||
@Column('char', {
|
||||
length: 10, nullable: true,
|
||||
comment: 'The birthday (YYYY-MM-DD) of the User.'
|
||||
})
|
||||
public birthday: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, nullable: true,
|
||||
comment: 'The description (bio) of the User.'
|
||||
})
|
||||
public description: string | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public fields: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The email address of the User.'
|
||||
})
|
||||
public email: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public emailVerifyCode: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public emailVerified: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public twoFactorTempSecret: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public twoFactorSecret: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public twoFactorEnabled: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The password hash of the User. It will be null if the origin of the user is local.'
|
||||
})
|
||||
public password: string | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
comment: 'The client-specific data of the User.'
|
||||
})
|
||||
public clientData: Record<string, any>;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public autoWatch: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public autoAcceptFollowed: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public alwaysMarkNsfw: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public carefulBot: boolean;
|
||||
|
||||
//#region Linking
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
@ -96,6 +183,7 @@ export class UserServiceLinking {
|
||||
length: 64, nullable: true, default: null,
|
||||
})
|
||||
public discordDiscriminator: string | null;
|
||||
//#endregion
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
@ -4,11 +4,8 @@ import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
export class UserPublickey {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column(id())
|
||||
@PrimaryColumn(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@OneToOne(type => User, {
|
||||
|
@ -45,18 +45,6 @@ export class User {
|
||||
})
|
||||
public name: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The location of the User.'
|
||||
})
|
||||
public location: string | null;
|
||||
|
||||
@Column('char', {
|
||||
length: 10, nullable: true,
|
||||
comment: 'The birthday (YYYY-MM-DD) of the User.'
|
||||
})
|
||||
public birthday: string | null;
|
||||
|
||||
@Column('integer', {
|
||||
default: 0,
|
||||
comment: 'The count of followers.'
|
||||
@ -101,44 +89,12 @@ export class User {
|
||||
@JoinColumn()
|
||||
public banner: DriveFile | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, nullable: true,
|
||||
comment: 'The description (bio) of the User.'
|
||||
})
|
||||
public description: string | null;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}'
|
||||
})
|
||||
public tags: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The email address of the User.'
|
||||
})
|
||||
public email: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public emailVerifyCode: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public emailVerified: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public twoFactorTempSecret: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
})
|
||||
public twoFactorSecret: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 256, nullable: true,
|
||||
})
|
||||
@ -206,11 +162,6 @@ export class User {
|
||||
})
|
||||
public isVerified: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public twoFactorEnabled: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}'
|
||||
})
|
||||
@ -248,44 +199,12 @@ export class User {
|
||||
})
|
||||
public uri: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The password hash of the User. It will be null if the origin of the user is local.'
|
||||
})
|
||||
public password: string | null;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column('varchar', {
|
||||
length: 32, nullable: true, unique: true,
|
||||
@Column('char', {
|
||||
length: 16, nullable: true, unique: true,
|
||||
comment: 'The native access token of the User. It will be null if the origin of the user is local.'
|
||||
})
|
||||
public token: string | null;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: {},
|
||||
comment: 'The client-specific data of the User.'
|
||||
})
|
||||
public clientData: Record<string, any>;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public autoWatch: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public autoAcceptFollowed: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public alwaysMarkNsfw: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public carefulBot: boolean;
|
||||
}
|
||||
|
||||
export interface ILocalUser extends User {
|
||||
|
@ -25,7 +25,6 @@ import { FollowRequestRepository } from './repositories/follow-request';
|
||||
import { MutingRepository } from './repositories/muting';
|
||||
import { BlockingRepository } from './repositories/blocking';
|
||||
import { NoteReactionRepository } from './repositories/note-reaction';
|
||||
import { UserServiceLinking } from './entities/user-service-linking';
|
||||
import { NotificationRepository } from './repositories/notification';
|
||||
import { NoteFavoriteRepository } from './repositories/note-favorite';
|
||||
import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
|
||||
@ -35,6 +34,7 @@ import { AppRepository } from './repositories/app';
|
||||
import { FollowingRepository } from './repositories/following';
|
||||
import { AbuseUserReportRepository } from './repositories/abuse-user-report';
|
||||
import { AuthSessionRepository } from './repositories/auth-session';
|
||||
import { UserProfile } from './entities/user-profile';
|
||||
|
||||
export const Apps = getCustomRepository(AppRepository);
|
||||
export const Notes = getCustomRepository(NoteRepository);
|
||||
@ -45,12 +45,12 @@ export const NoteUnreads = getRepository(NoteUnread);
|
||||
export const Polls = getRepository(Poll);
|
||||
export const PollVotes = getRepository(PollVote);
|
||||
export const Users = getCustomRepository(UserRepository);
|
||||
export const UserProfiles = getRepository(UserProfile);
|
||||
export const UserKeypairs = getRepository(UserKeypair);
|
||||
export const UserPublickeys = getRepository(UserPublickey);
|
||||
export const UserLists = getCustomRepository(UserListRepository);
|
||||
export const UserListJoinings = getRepository(UserListJoining);
|
||||
export const UserNotePinings = getRepository(UserNotePining);
|
||||
export const UserServiceLinkings = getRepository(UserServiceLinking);
|
||||
export const Followings = getCustomRepository(FollowingRepository);
|
||||
export const FollowRequests = getCustomRepository(FollowRequestRepository);
|
||||
export const Instances = getRepository(Instance);
|
||||
|
@ -3,6 +3,7 @@ import { DriveFile } from '../entities/drive-file';
|
||||
import { Users, DriveFolders } from '..';
|
||||
import rap from '@prezzemolo/rap';
|
||||
import { User } from '../entities/user';
|
||||
import { toPuny } from '../../misc/convert-host';
|
||||
|
||||
@EntityRepository(DriveFile)
|
||||
export class DriveFileRepository extends Repository<DriveFile> {
|
||||
@ -39,7 +40,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
||||
public async clacDriveUsageOfHost(host: string): Promise<number> {
|
||||
const { sum } = await this
|
||||
.createQueryBuilder('file')
|
||||
.where('file.userHost = :host', { host: host })
|
||||
.where('file.userHost = :host', { host: toPuny(host) })
|
||||
.select('SUM(file.size)', 'sum')
|
||||
.getRawOne();
|
||||
|
||||
|
@ -167,8 +167,11 @@ export class NoteRepository extends Repository<Note> {
|
||||
text: text,
|
||||
cw: note.cw,
|
||||
visibility: note.visibility,
|
||||
localOnly: note.localOnly,
|
||||
visibleUserIds: note.visibleUserIds,
|
||||
viaMobile: note.viaMobile,
|
||||
renoteCount: note.renoteCount,
|
||||
repliesCount: note.repliesCount,
|
||||
reactions: note.reactions,
|
||||
emojis: reactionEmojis.length > 0 ? Emojis.find({
|
||||
name: In(reactionEmojis),
|
||||
@ -186,7 +189,7 @@ export class NoteRepository extends Repository<Note> {
|
||||
}) : null,
|
||||
|
||||
renote: note.renoteId ? this.pack(note.renoteId, meId, {
|
||||
detail: false
|
||||
detail: true
|
||||
}) : null,
|
||||
|
||||
poll: note.hasPoll ? populatePoll() : null,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { EntityRepository, Repository, In } from 'typeorm';
|
||||
import { User, ILocalUser, IRemoteUser } from '../entities/user';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings } from '..';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..';
|
||||
import rap from '@prezzemolo/rap';
|
||||
|
||||
@EntityRepository(User)
|
||||
@ -80,17 +80,21 @@ export class UserRepository extends Repository<User> {
|
||||
|
||||
const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
|
||||
const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : [];
|
||||
const profile = opts.detail ? await UserProfiles.findOne({ userId: user.id }) : null;
|
||||
|
||||
return await rap({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
uri: user.uri,
|
||||
avatarUrl: user.avatarUrl,
|
||||
bannerUrl: user.bannerUrl,
|
||||
avatarColor: user.avatarColor,
|
||||
bannerColor: user.bannerColor,
|
||||
isAdmin: user.isAdmin,
|
||||
isBot: user.isBot,
|
||||
isCat: user.isCat,
|
||||
|
||||
// カスタム絵文字添付
|
||||
emojis: user.emojis.length > 0 ? Emojis.find({
|
||||
@ -113,9 +117,9 @@ export class UserRepository extends Repository<User> {
|
||||
} : {}),
|
||||
|
||||
...(opts.detail ? {
|
||||
description: user.description,
|
||||
location: user.location,
|
||||
birthday: user.birthday,
|
||||
description: profile.description,
|
||||
location: profile.location,
|
||||
birthday: profile.birthday,
|
||||
followersCount: user.followersCount,
|
||||
followingCount: user.followingCount,
|
||||
notesCount: user.notesCount,
|
||||
@ -128,9 +132,9 @@ export class UserRepository extends Repository<User> {
|
||||
...(opts.detail && meId === user.id ? {
|
||||
avatarId: user.avatarId,
|
||||
bannerId: user.bannerId,
|
||||
autoWatch: user.autoWatch,
|
||||
alwaysMarkNsfw: user.alwaysMarkNsfw,
|
||||
carefulBot: user.carefulBot,
|
||||
autoWatch: profile.autoWatch,
|
||||
alwaysMarkNsfw: profile.alwaysMarkNsfw,
|
||||
carefulBot: profile.carefulBot,
|
||||
hasUnreadMessagingMessage: MessagingMessages.count({
|
||||
where: {
|
||||
recipientId: user.id,
|
||||
@ -150,6 +154,12 @@ export class UserRepository extends Repository<User> {
|
||||
}),
|
||||
} : {}),
|
||||
|
||||
...(opts.includeSecrets ? {
|
||||
clientData: profile.clientData,
|
||||
email: profile.email,
|
||||
emailVerified: profile.emailVerified,
|
||||
} : {}),
|
||||
|
||||
...(relation ? {
|
||||
isFollowing: relation.isFollowing,
|
||||
isFollowed: relation.isFollowed,
|
||||
@ -163,7 +173,7 @@ export class UserRepository extends Repository<User> {
|
||||
}
|
||||
|
||||
public isLocalUser(user: User): user is ILocalUser {
|
||||
return user.host === null;
|
||||
return user.host == null;
|
||||
}
|
||||
|
||||
public isRemoteUser(user: User): user is IRemoteUser {
|
||||
|
@ -5,7 +5,7 @@ import follow from '../../../services/following/create';
|
||||
import parseAcct from '../../../misc/acct/parse';
|
||||
import { resolveUser } from '../../../remote/resolve-user';
|
||||
import { downloadTextFile } from '../../../misc/download-text-file';
|
||||
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
|
||||
import { isSelfHost, toPuny } from '../../../misc/convert-host';
|
||||
import { Users, DriveFiles } from '../../../models';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-following');
|
||||
@ -35,7 +35,7 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
|
||||
host: null,
|
||||
usernameLower: username.toLowerCase()
|
||||
}) : await Users.findOne({
|
||||
host: toDbHost(host),
|
||||
host: toPuny(host),
|
||||
usernameLower: username.toLowerCase()
|
||||
});
|
||||
|
||||
|
@ -5,7 +5,7 @@ import parseAcct from '../../../misc/acct/parse';
|
||||
import { resolveUser } from '../../../remote/resolve-user';
|
||||
import { pushUserToUserList } from '../../../services/user-list/push';
|
||||
import { downloadTextFile } from '../../../misc/download-text-file';
|
||||
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
|
||||
import { isSelfHost, toPuny } from '../../../misc/convert-host';
|
||||
import { DriveFiles, Users, UserLists, UserListJoinings } from '../../../models';
|
||||
import { genId } from '../../../misc/gen-id';
|
||||
|
||||
@ -47,7 +47,7 @@ export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
|
||||
host: null,
|
||||
usernameLower: username.toLowerCase()
|
||||
}) : await Users.findOne({
|
||||
host: toDbHost(host),
|
||||
host: toPuny(host),
|
||||
usernameLower: username.toLowerCase()
|
||||
});
|
||||
|
||||
|
@ -4,7 +4,6 @@ import parseAcct from '../../misc/acct/parse';
|
||||
import { IRemoteUser } from '../../models/entities/user';
|
||||
import perform from '../../remote/activitypub/perform';
|
||||
import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
|
||||
import { toUnicode } from 'punycode';
|
||||
import { URL } from 'url';
|
||||
import { publishApLogStream } from '../../services/stream';
|
||||
import Logger from '../../services/logger';
|
||||
@ -13,6 +12,7 @@ import { Instances, Users, UserPublickeys } from '../../models';
|
||||
import { instanceChart } from '../../services/chart';
|
||||
import { UserPublickey } from '../../models/entities/user-publickey';
|
||||
import fetchMeta from '../../misc/fetch-meta';
|
||||
import { toPuny } from '../../misc/convert-host';
|
||||
|
||||
const logger = new Logger('inbox');
|
||||
|
||||
@ -33,7 +33,10 @@ export default async (job: Bull.Job): Promise<void> => {
|
||||
let key: UserPublickey;
|
||||
|
||||
if (keyIdLower.startsWith('acct:')) {
|
||||
const { username, host } = parseAcct(keyIdLower.slice('acct:'.length));
|
||||
const acct = parseAcct(keyIdLower.slice('acct:'.length));
|
||||
const host = toPuny(acct.host);
|
||||
const username = toPuny(acct.username);
|
||||
|
||||
if (host === null) {
|
||||
logger.warn(`request was made by local user: @${username}`);
|
||||
return;
|
||||
@ -50,19 +53,22 @@ export default async (job: Bull.Job): Promise<void> => {
|
||||
// ブロックしてたら中断
|
||||
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
|
||||
const meta = await fetchMeta();
|
||||
if (meta.blockedHosts.includes(host.toLowerCase())) {
|
||||
if (meta.blockedHosts.includes(host)) {
|
||||
logger.info(`Blocked request: ${host}`);
|
||||
return;
|
||||
}
|
||||
|
||||
user = await Users.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
|
||||
user = await Users.findOne({
|
||||
usernameLower: username.toLowerCase(),
|
||||
host: host
|
||||
}) as IRemoteUser;
|
||||
|
||||
key = await UserPublickeys.findOne({
|
||||
userId: user.id
|
||||
});
|
||||
} else {
|
||||
// アクティビティ内のホストの検証
|
||||
const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase());
|
||||
const host = toPuny(new URL(signature.keyId).hostname);
|
||||
try {
|
||||
ValidateActivity(activity, host);
|
||||
} catch (e) {
|
||||
@ -73,7 +79,7 @@ export default async (job: Bull.Job): Promise<void> => {
|
||||
// ブロックしてたら中断
|
||||
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
|
||||
const meta = await fetchMeta();
|
||||
if (meta.blockedHosts.includes(host.toLowerCase())) {
|
||||
if (meta.blockedHosts.includes(host)) {
|
||||
logger.info(`Blocked request: ${host}`);
|
||||
return;
|
||||
}
|
||||
@ -145,7 +151,7 @@ export default async (job: Bull.Job): Promise<void> => {
|
||||
function ValidateActivity(activity: any, host: string) {
|
||||
// id (if exists)
|
||||
if (typeof activity.id === 'string') {
|
||||
const uriHost = toUnicode(new URL(activity.id).hostname.toLowerCase());
|
||||
const uriHost = toPuny(new URL(activity.id).hostname);
|
||||
if (host !== uriHost) {
|
||||
const diag = activity.signature ? '. Has LD-Signature. Forwarded?' : '';
|
||||
throw new Error(`activity.id(${activity.id}) has different host(${host})${diag}`);
|
||||
@ -154,7 +160,7 @@ function ValidateActivity(activity: any, host: string) {
|
||||
|
||||
// actor (if exists)
|
||||
if (typeof activity.actor === 'string') {
|
||||
const uriHost = toUnicode(new URL(activity.actor).hostname.toLowerCase());
|
||||
const uriHost = toPuny(new URL(activity.actor).hostname);
|
||||
if (host !== uriHost) throw new Error('activity.actor has different host');
|
||||
}
|
||||
|
||||
@ -162,13 +168,13 @@ function ValidateActivity(activity: any, host: string) {
|
||||
if (activity.type === 'Create' && activity.object) {
|
||||
// object.id (if exists)
|
||||
if (typeof activity.object.id === 'string') {
|
||||
const uriHost = toUnicode(new URL(activity.object.id).hostname.toLowerCase());
|
||||
const uriHost = toPuny(new URL(activity.object.id).hostname);
|
||||
if (host !== uriHost) throw new Error('activity.object.id has different host');
|
||||
}
|
||||
|
||||
// object.attributedTo (if exists)
|
||||
if (typeof activity.object.attributedTo === 'string') {
|
||||
const uriHost = toUnicode(new URL(activity.object.attributedTo).hostname.toLowerCase());
|
||||
const uriHost = toPuny(new URL(activity.object.attributedTo).hostname);
|
||||
if (host !== uriHost) throw new Error('activity.object.attributedTo has different host');
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (file.isRemote) {
|
||||
if (file.isLink) {
|
||||
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
|
||||
// URLを更新する
|
||||
if (file.url !== image.url) {
|
||||
|
@ -8,14 +8,13 @@ import { resolveImage } from './image';
|
||||
import { IRemoteUser, User } from '../../../models/entities/user';
|
||||
import { fromHtml } from '../../../mfm/fromHtml';
|
||||
import { ITag, extractHashtags } from './tag';
|
||||
import { toUnicode } from 'punycode';
|
||||
import { unique, concat, difference } from '../../../prelude/array';
|
||||
import { extractPollFromQuestion } from './question';
|
||||
import vote from '../../../services/note/polls/vote';
|
||||
import { apLogger } from '../logger';
|
||||
import { DriveFile } from '../../../models/entities/drive-file';
|
||||
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
|
||||
import { extractDbHost } from '../../../misc/convert-host';
|
||||
import { extractDbHost, toPuny } from '../../../misc/convert-host';
|
||||
import { Notes, Emojis, Polls } from '../../../models';
|
||||
import { Note } from '../../../models/entities/note';
|
||||
import { IObject, INote } from '../type';
|
||||
@ -246,8 +245,8 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
|
||||
return await createNote(uri, resolver);
|
||||
}
|
||||
|
||||
export async function extractEmojis(tags: ITag[], host_: string) {
|
||||
const host = toUnicode(host_.toLowerCase());
|
||||
export async function extractEmojis(tags: ITag[], host: string) {
|
||||
host = toPuny(host);
|
||||
|
||||
if (!tags) return [];
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import * as promiseLimit from 'promise-limit';
|
||||
import { toUnicode } from 'punycode';
|
||||
|
||||
import config from '../../../config';
|
||||
import Resolver from '../resolver';
|
||||
@ -15,15 +14,16 @@ import { IIdentifier } from './identifier';
|
||||
import { apLogger } from '../logger';
|
||||
import { Note } from '../../../models/entities/note';
|
||||
import { updateHashtag } from '../../../services/update-hashtag';
|
||||
import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserServiceLinkings, UserPublickeys } from '../../../models';
|
||||
import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '../../../models';
|
||||
import { User, IRemoteUser } from '../../../models/entities/user';
|
||||
import { Emoji } from '../../../models/entities/emoji';
|
||||
import { UserNotePining } from '../../../models/entities/user-note-pinings';
|
||||
import { genId } from '../../../misc/gen-id';
|
||||
import { UserServiceLinking } from '../../../models/entities/user-service-linking';
|
||||
import { instanceChart, usersChart } from '../../../services/chart';
|
||||
import { UserPublickey } from '../../../models/entities/user-publickey';
|
||||
import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
|
||||
import { toPuny } from '../../../misc/convert-host';
|
||||
import { UserProfile } from '../../../models/entities/user-profile';
|
||||
const logger = apLogger;
|
||||
|
||||
/**
|
||||
@ -32,7 +32,7 @@ const logger = apLogger;
|
||||
* @param uri Fetch target URI
|
||||
*/
|
||||
function validatePerson(x: any, uri: string) {
|
||||
const expectHost = toUnicode(new URL(uri).hostname.toLowerCase());
|
||||
const expectHost = toPuny(new URL(uri).hostname);
|
||||
|
||||
if (x == null) {
|
||||
return new Error('invalid person: object is null');
|
||||
@ -62,7 +62,7 @@ function validatePerson(x: any, uri: string) {
|
||||
return new Error('invalid person: id is not a string');
|
||||
}
|
||||
|
||||
const idHost = toUnicode(new URL(x.id).hostname.toLowerCase());
|
||||
const idHost = toPuny(new URL(x.id).hostname);
|
||||
if (idHost !== expectHost) {
|
||||
return new Error('invalid person: id has different host');
|
||||
}
|
||||
@ -71,7 +71,7 @@ function validatePerson(x: any, uri: string) {
|
||||
return new Error('invalid person: publicKey.id is not a string');
|
||||
}
|
||||
|
||||
const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase());
|
||||
const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname);
|
||||
if (publicKeyIdHost !== expectHost) {
|
||||
return new Error('invalid person: publicKey.id has different host');
|
||||
}
|
||||
@ -124,9 +124,9 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
|
||||
logger.info(`Creating the Person: ${person.id}`);
|
||||
|
||||
const host = toUnicode(new URL(object.id).hostname.toLowerCase());
|
||||
const host = toPuny(new URL(object.id).hostname);
|
||||
|
||||
const { fields, services } = analyzeAttachments(person.attachment);
|
||||
const { fields } = analyzeAttachments(person.attachment);
|
||||
|
||||
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase());
|
||||
|
||||
@ -141,7 +141,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
bannerId: null,
|
||||
createdAt: Date.parse(person.published) || new Date(),
|
||||
lastFetchedAt: new Date(),
|
||||
description: fromHtml(person.summary),
|
||||
name: person.name,
|
||||
isLocked: person.manuallyApprovesFollowers,
|
||||
username: person.preferredUsername,
|
||||
@ -153,8 +152,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
endpoints: person.endpoints,
|
||||
uri: person.id,
|
||||
url: person.url,
|
||||
fields,
|
||||
...services,
|
||||
tags,
|
||||
isBot,
|
||||
isCat: (person as any).isCat === true
|
||||
@ -169,18 +166,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
throw e;
|
||||
}
|
||||
|
||||
await UserProfiles.save({
|
||||
userId: user.id,
|
||||
description: fromHtml(person.summary),
|
||||
fields,
|
||||
} as Partial<UserProfile>);
|
||||
|
||||
await UserPublickeys.save({
|
||||
id: genId(),
|
||||
userId: user.id,
|
||||
keyId: person.publicKey.id,
|
||||
keyPem: person.publicKey.publicKeyPem
|
||||
} as UserPublickey);
|
||||
|
||||
await UserServiceLinkings.save({
|
||||
id: genId(),
|
||||
userId: user.id
|
||||
} as UserServiceLinking);
|
||||
|
||||
// Register host
|
||||
registerOrFetchInstanceDoc(host).then(i => {
|
||||
Instances.increment({ id: i.id }, 'usersCount', 1);
|
||||
@ -205,8 +202,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
|
||||
const avatarId = avatar ? avatar.id : null;
|
||||
const bannerId = banner ? banner.id : null;
|
||||
const avatarUrl = DriveFiles.getPublicUrl(avatar);
|
||||
const bannerUrl = DriveFiles.getPublicUrl(banner);
|
||||
const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar) : null;
|
||||
const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
|
||||
const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
|
||||
const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null;
|
||||
|
||||
@ -347,7 +344,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
||||
keyPem: person.publicKey.publicKeyPem
|
||||
});
|
||||
|
||||
await UserServiceLinkings.update({ userId: exist.id }, {
|
||||
await UserProfiles.update({ userId: exist.id }, {
|
||||
twitterUserId: services.twitter.userId,
|
||||
twitterScreenName: services.twitter.screenName,
|
||||
githubId: services.github.id,
|
||||
|
@ -12,7 +12,7 @@ import { Emoji } from '../../../models/entities/emoji';
|
||||
import { Poll } from '../../../models/entities/poll';
|
||||
|
||||
export default async function renderNote(note: Note, dive = true): Promise<any> {
|
||||
const promisedFiles: Promise<DriveFile[]> = note.fileIds.length > 1
|
||||
const promisedFiles: Promise<DriveFile[]> = note.fileIds.length > 0
|
||||
? DriveFiles.find({ id: In(note.fileIds) })
|
||||
: Promise.resolve([]);
|
||||
|
||||
|
@ -8,15 +8,15 @@ import { getEmojis } from './note';
|
||||
import renderEmoji from './emoji';
|
||||
import { IIdentifier } from '../models/identifier';
|
||||
import renderHashtag from './hashtag';
|
||||
import { DriveFiles, UserServiceLinkings, UserKeypairs } from '../../../models';
|
||||
import { DriveFiles, UserProfiles, UserKeypairs } from '../../../models';
|
||||
|
||||
export async function renderPerson(user: ILocalUser) {
|
||||
const id = `${config.url}/users/${user.id}`;
|
||||
|
||||
const [avatar, banner, links] = await Promise.all([
|
||||
const [avatar, banner, profile] = await Promise.all([
|
||||
DriveFiles.findOne(user.avatarId),
|
||||
DriveFiles.findOne(user.bannerId),
|
||||
UserServiceLinkings.findOne({ userId: user.id })
|
||||
UserProfiles.findOne({ userId: user.id })
|
||||
]);
|
||||
|
||||
const attachment: {
|
||||
@ -27,41 +27,41 @@ export async function renderPerson(user: ILocalUser) {
|
||||
identifier?: IIdentifier
|
||||
}[] = [];
|
||||
|
||||
if (links.twitter) {
|
||||
if (profile.twitter) {
|
||||
attachment.push({
|
||||
type: 'PropertyValue',
|
||||
name: 'Twitter',
|
||||
value: `<a href="https://twitter.com/intent/user?user_id=${links.twitterUserId}" rel="me nofollow noopener" target="_blank"><span>@${links.twitterScreenName}</span></a>`,
|
||||
value: `<a href="https://twitter.com/intent/user?user_id=${profile.twitterUserId}" rel="me nofollow noopener" target="_blank"><span>@${profile.twitterScreenName}</span></a>`,
|
||||
identifier: {
|
||||
type: 'PropertyValue',
|
||||
name: 'misskey:authentication:twitter',
|
||||
value: `${links.twitterUserId}@${links.twitterScreenName}`
|
||||
value: `${profile.twitterUserId}@${profile.twitterScreenName}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (links.github) {
|
||||
if (profile.github) {
|
||||
attachment.push({
|
||||
type: 'PropertyValue',
|
||||
name: 'GitHub',
|
||||
value: `<a href="https://github.com/${links.githubLogin}" rel="me nofollow noopener" target="_blank"><span>@${links.githubLogin}</span></a>`,
|
||||
value: `<a href="https://github.com/${profile.githubLogin}" rel="me nofollow noopener" target="_blank"><span>@${profile.githubLogin}</span></a>`,
|
||||
identifier: {
|
||||
type: 'PropertyValue',
|
||||
name: 'misskey:authentication:github',
|
||||
value: `${links.githubId}@${links.githubLogin}`
|
||||
value: `${profile.githubId}@${profile.githubLogin}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (links.discord) {
|
||||
if (profile.discord) {
|
||||
attachment.push({
|
||||
type: 'PropertyValue',
|
||||
name: 'Discord',
|
||||
value: `<a href="https://discordapp.com/users/${links.discordId}" rel="me nofollow noopener" target="_blank"><span>${links.discordUsername}#${links.discordDiscriminator}</span></a>`,
|
||||
value: `<a href="https://discordapp.com/users/${profile.discordId}" rel="me nofollow noopener" target="_blank"><span>${profile.discordUsername}#${profile.discordDiscriminator}</span></a>`,
|
||||
identifier: {
|
||||
type: 'PropertyValue',
|
||||
name: 'misskey:authentication:discord',
|
||||
value: `${links.discordId}@${links.discordUsername}#${links.discordDiscriminator}`
|
||||
value: `${profile.discordId}@${profile.discordUsername}#${profile.discordDiscriminator}`
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -93,7 +93,7 @@ export async function renderPerson(user: ILocalUser) {
|
||||
url: `${config.url}/@${user.username}`,
|
||||
preferredUsername: user.username,
|
||||
name: user.name,
|
||||
summary: toHtml(parse(user.description)),
|
||||
summary: toHtml(parse(profile.description)),
|
||||
icon: user.avatarId && renderImage(avatar),
|
||||
image: user.bannerId && renderImage(banner),
|
||||
tag,
|
||||
|
@ -4,7 +4,6 @@ import { URL } from 'url';
|
||||
import * as crypto from 'crypto';
|
||||
import { lookup, IRunOptions } from 'lookup-dns-cache';
|
||||
import * as promiseAny from 'promise-any';
|
||||
import { toUnicode } from 'punycode';
|
||||
|
||||
import config from '../../config';
|
||||
import { ILocalUser } from '../../models/entities/user';
|
||||
@ -12,6 +11,7 @@ import { publishApLogStream } from '../../services/stream';
|
||||
import { apLogger } from './logger';
|
||||
import { UserKeypairs } from '../../models';
|
||||
import fetchMeta from '../../misc/fetch-meta';
|
||||
import { toPuny } from '../../misc/convert-host';
|
||||
|
||||
export const logger = apLogger.createSubLogger('deliver');
|
||||
|
||||
@ -25,7 +25,7 @@ export default async (user: ILocalUser, url: string, object: any) => {
|
||||
// ブロックしてたら中断
|
||||
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
|
||||
const meta = await fetchMeta();
|
||||
if (meta.blockedHosts.includes(toUnicode(host))) return;
|
||||
if (meta.blockedHosts.includes(toPuny(host))) return;
|
||||
|
||||
const data = JSON.stringify(object);
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { toUnicode, toASCII } from 'punycode';
|
||||
import webFinger from './webfinger';
|
||||
import config from '../config';
|
||||
import { createPerson, updatePerson } from './activitypub/models/person';
|
||||
@ -7,31 +6,27 @@ import { remoteLogger } from './logger';
|
||||
import chalk from 'chalk';
|
||||
import { User, IRemoteUser } from '../models/entities/user';
|
||||
import { Users } from '../models';
|
||||
import { toPuny } from '../misc/convert-host';
|
||||
|
||||
const logger = remoteLogger.createSubLogger('resolve-user');
|
||||
|
||||
export async function resolveUser(username: string, _host: string, option?: any, resync = false): Promise<User> {
|
||||
export async function resolveUser(username: string, host: string, option?: any, resync = false): Promise<User> {
|
||||
const usernameLower = username.toLowerCase();
|
||||
host = toPuny(host);
|
||||
|
||||
if (_host == null) {
|
||||
if (host == null) {
|
||||
logger.info(`return local user: ${usernameLower}`);
|
||||
return await Users.findOne({ usernameLower, host: null });
|
||||
}
|
||||
|
||||
const configHostAscii = toASCII(config.host).toLowerCase();
|
||||
const configHost = toUnicode(configHostAscii);
|
||||
|
||||
const hostAscii = toASCII(_host).toLowerCase();
|
||||
const host = toUnicode(hostAscii);
|
||||
|
||||
if (configHost == host) {
|
||||
if (config.host == host) {
|
||||
logger.info(`return local user: ${usernameLower}`);
|
||||
return await Users.findOne({ usernameLower, host: null });
|
||||
}
|
||||
|
||||
const user = await Users.findOne({ usernameLower, host }, option);
|
||||
|
||||
const acctLower = `${usernameLower}@${hostAscii}`;
|
||||
const acctLower = `${usernameLower}@${host}`;
|
||||
|
||||
if (user == null) {
|
||||
const self = await resolveSelf(acctLower);
|
||||
@ -51,7 +46,7 @@ export async function resolveUser(username: string, _host: string, option?: any,
|
||||
|
||||
// validate uri
|
||||
const uri = new URL(self.href);
|
||||
if (uri.hostname !== hostAscii) {
|
||||
if (uri.hostname !== host) {
|
||||
throw new Error(`Invalied uri`);
|
||||
}
|
||||
|
||||
@ -78,8 +73,8 @@ export async function resolveUser(username: string, _host: string, option?: any,
|
||||
async function resolveSelf(acctLower: string) {
|
||||
logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
|
||||
const finger = await webFinger(acctLower).catch(e => {
|
||||
logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${e.message} (${e.status})`);
|
||||
throw e;
|
||||
logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`);
|
||||
throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
|
||||
});
|
||||
const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
|
||||
if (!self) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { WebFinger } from 'webfinger.js';
|
||||
|
||||
const webFinger = new WebFinger({ });
|
||||
import config from '../config';
|
||||
import * as request from 'request-promise-native';
|
||||
import { URL } from 'url';
|
||||
import { query as urlQuery } from '../prelude/url';
|
||||
|
||||
type ILink = {
|
||||
href: string;
|
||||
@ -12,12 +13,33 @@ type IWebFinger = {
|
||||
subject: string;
|
||||
};
|
||||
|
||||
export default async function resolve(query: any): Promise<IWebFinger> {
|
||||
return await new Promise((res, rej) => webFinger.lookup(query, (error: Error | string, result: any) => {
|
||||
if (error) {
|
||||
return rej(error);
|
||||
}
|
||||
export default async function(query: string): Promise<IWebFinger> {
|
||||
const url = genUrl(query);
|
||||
|
||||
res(result.object);
|
||||
})) as IWebFinger;
|
||||
return await request({
|
||||
url,
|
||||
proxy: config.proxy,
|
||||
timeout: 10 * 1000,
|
||||
forever: true,
|
||||
headers: {
|
||||
'User-Agent': config.userAgent,
|
||||
Accept: 'application/jrd+json, application/json'
|
||||
},
|
||||
json: true
|
||||
});
|
||||
}
|
||||
|
||||
function genUrl(query: string) {
|
||||
if (query.match(/^https?:\/\//)) {
|
||||
const u = new URL(query);
|
||||
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
|
||||
}
|
||||
|
||||
const m = query.match(/^([^@]+)@(.*)/);
|
||||
if (m) {
|
||||
const hostname = m[2];
|
||||
return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` });
|
||||
}
|
||||
|
||||
throw new Error(`Invalied query (${query})`);
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
import rndstr from 'rndstr';
|
||||
|
||||
export default () => `!${rndstr('a-zA-Z0-9', 31)}`;
|
||||
export default () => `0${rndstr('a-zA-Z0-9', 15)}`;
|
||||
|
@ -1,6 +0,0 @@
|
||||
import { toUnicode } from 'punycode';
|
||||
|
||||
export default (host: string) => {
|
||||
if (host == null) return null;
|
||||
return toUnicode(host).toLowerCase();
|
||||
};
|
@ -1 +1 @@
|
||||
export default (token: string) => token.startsWith('!');
|
||||
export default (token: string) => token.startsWith('0');
|
||||
|
@ -1,6 +1,7 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { Emojis } from '../../../../../models';
|
||||
import { toPuny } from '../../../../../misc/convert-host';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -22,7 +23,7 @@ export const meta = {
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
const emojis = await Emojis.find({
|
||||
host: ps.host
|
||||
host: toPuny(ps.host)
|
||||
});
|
||||
|
||||
return emojis.map(e => ({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { Instances } from '../../../../../models';
|
||||
import { toPuny } from '../../../../../misc/convert-host';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@ -20,13 +21,13 @@ export const meta = {
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const instance = await Instances.findOne({ host: ps.host });
|
||||
const instance = await Instances.findOne({ host: toPuny(ps.host) });
|
||||
|
||||
if (instance == null) {
|
||||
throw new Error('instance not found');
|
||||
}
|
||||
|
||||
Instances.update({ host: ps.host }, {
|
||||
Instances.update({ host: toPuny(ps.host) }, {
|
||||
isMarkedAsClosed: ps.isClosed
|
||||
});
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import rndstr from 'rndstr';
|
||||
import { Users } from '../../../../models';
|
||||
import { Users, UserProfiles } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
@ -42,7 +42,9 @@ export default define(meta, async (ps) => {
|
||||
// Generate hash of password
|
||||
const hash = bcrypt.hashSync(passwd);
|
||||
|
||||
await Users.update(user.id, {
|
||||
await UserProfiles.update({
|
||||
userId: user.id
|
||||
}, {
|
||||
password: hash
|
||||
});
|
||||
|
||||
|
@ -38,7 +38,7 @@ export default define(meta, async (ps, user) => {
|
||||
}
|
||||
|
||||
// Generate access token
|
||||
const accessToken = rndstr('a-zA-Z0-9', 32);
|
||||
const accessToken = '1' + rndstr('a-zA-Z0-9', 15);
|
||||
|
||||
// Fetch exist access token
|
||||
const exist = await AccessTokens.findOne({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { Instances } from '../../../../models';
|
||||
import { toPuny } from '../../../../misc/convert-host';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
@ -16,7 +17,7 @@ export const meta = {
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const instance = await Instances
|
||||
.findOne({ host: ps.host });
|
||||
.findOne({ host: toPuny(ps.host) });
|
||||
|
||||
return instance;
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import $ from 'cafy';
|
||||
import * as speakeasy from 'speakeasy';
|
||||
import define from '../../../define';
|
||||
import { Users } from '../../../../../models';
|
||||
import { UserProfiles } from '../../../../../models';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
@ -16,24 +16,26 @@ export const meta = {
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const _token = ps.token.replace(/\s/g, '');
|
||||
const token = ps.token.replace(/\s/g, '');
|
||||
|
||||
if (user.twoFactorTempSecret == null) {
|
||||
const profile = await UserProfiles.findOne({ userId: user.id });
|
||||
|
||||
if (profile.twoFactorTempSecret == null) {
|
||||
throw new Error('二段階認証の設定が開始されていません');
|
||||
}
|
||||
|
||||
const verified = (speakeasy as any).totp.verify({
|
||||
secret: user.twoFactorTempSecret,
|
||||
secret: profile.twoFactorTempSecret,
|
||||
encoding: 'base32',
|
||||
token: _token
|
||||
token: token
|
||||
});
|
||||
|
||||
if (!verified) {
|
||||
throw new Error('not verified');
|
||||
}
|
||||
|
||||
await Users.update(user.id, {
|
||||
twoFactorSecret: user.twoFactorTempSecret,
|
||||
await UserProfiles.update({ userId: user.id }, {
|
||||
twoFactorSecret: profile.twoFactorTempSecret,
|
||||
twoFactorEnabled: true
|
||||
});
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user