Compare commits

...

78 Commits

Author SHA1 Message Date
5d52e9ce6b 11.0.0-beta.3 2019-04-12 01:56:48 +09:00
3c29027ca3 Clean up 2019-04-12 01:54:28 +09:00
2ff3069d23 トランザクションを使うようにしたり 2019-04-12 01:52:25 +09:00
4198246351 トランザクションを使用してアンケートレコードの挿入に失敗した場合に投稿レコードの挿入もなかったことにするように 2019-04-12 01:30:10 +09:00
2b6389b4dc Fix bug 2019-04-12 01:03:40 +09:00
d7df75ae6c Clean up 2019-04-12 01:01:25 +09:00
11c30eccb3 非正規化カラムを削除
非正規化するほどの情報じゃない
2019-04-12 00:42:39 +09:00
ab8c6515b8 Fix error log 2019-04-12 00:33:26 +09:00
d4ad36fa41 Update migrate.ts 2019-04-11 22:49:12 +09:00
4d688be3df Update migrate.ts 2019-04-11 22:44:04 +09:00
d2b75f3501 Update migrate.ts 2019-04-11 19:42:35 +09:00
46b78cb4ff Increase url column length 2019-04-11 19:03:49 +09:00
5d6e0d0f37 Update migrate.ts 2019-04-11 16:15:27 +09:00
e19d0a37bb Update migrate.ts 2019-04-11 16:09:33 +09:00
dea3e2132e Update migrate.ts 2019-04-11 15:53:15 +09:00
91c1ceefbd Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-04-11 12:59:16 +09:00
5c50989970 Fix bug 2019-04-11 12:59:09 +09:00
2a7e3b9c51 Fix: AP actor Service のサポートが不完全 (v11) (#4662) 2019-04-11 03:09:12 +09:00
ab302df0ae Update CHANGELOG.md 2019-04-11 00:18:37 +09:00
21e5809993 Clean up 2019-04-10 23:57:39 +09:00
c58afc67e8 Update migrate.ts 2019-04-10 20:13:14 +09:00
8e344f2deb 11.0.0-beta.2 2019-04-10 20:08:19 +09:00
c28f4ffb3f Clean up 2019-04-10 20:07:36 +09:00
2a40240310 Fix bug 2019-04-10 18:35:51 +09:00
0e9fba9287 11.0.0-beta.1 2019-04-10 18:25:16 +09:00
2e3dd2a30a Fix bug 2019-04-10 18:13:33 +09:00
dda5f6559d Fix bug 2019-04-10 18:10:09 +09:00
4152e59638 Fix bug 2019-04-10 18:05:39 +09:00
9d5a92bce6 11.0.0-alpha.10 2019-04-10 15:08:05 +09:00
30172b92e6 WebFingerリクエストで Proxy, Keep-Alive などをサポート #4658
Co-Authored-By: MeiMei <mei23@users.noreply.github.com>
2019-04-10 15:07:21 +09:00
8468a9d4c7 11.0.0-alpha.9 2019-04-10 15:04:33 +09:00
626cfb61ac テーブル分割 2019-04-10 15:04:27 +09:00
9603f3fa4f Delete get-user-summary.ts 2019-04-10 14:58:45 +09:00
619a1b9e53 Fix bug 2019-04-10 14:10:00 +09:00
62e76ad588 11.0.0-alpha.8 2019-04-10 01:00:00 +09:00
236d72685d More puny 2019-04-10 00:59:41 +09:00
72a5f7b1e2 Fix bug 2019-04-10 00:40:10 +09:00
d1c16a90b4 11.0.0-alpha.7 2019-04-10 00:01:10 +09:00
33a9783ae5 ドメインは常にPunycodeで保存するように 2019-04-09 23:59:32 +09:00
4d64fd665e Update update.ts 2019-04-09 23:40:55 +09:00
96443384fe 11.0.0-alpha.6 2019-04-09 23:35:34 +09:00
69939f1edb :) 2019-04-09 23:31:41 +09:00
e3c0058942 Refactor 2019-04-09 23:29:48 +09:00
3dc2361654 Update migrate.ts 2019-04-09 23:12:11 +09:00
ec2f709018 Update migrate.ts 2019-04-09 23:09:57 +09:00
ea06665c51 isRemote --> isLink 2019-04-09 23:07:08 +09:00
74a4bd704c Update migrate.ts 2019-04-09 21:05:49 +09:00
c62f3e0e45 11.0.0-alpha.5 2019-04-09 20:51:23 +09:00
a56a969433 Update dependencies 🚀 2019-04-09 20:49:56 +09:00
1016f94bbb Fix bug 2019-04-09 20:47:31 +09:00
e98cce9aee Update migrate.ts 2019-04-09 19:20:10 +09:00
d4bdb5d327 Update migrate.ts 2019-04-09 19:14:18 +09:00
db4378415e Update migrate.ts 2019-04-09 19:02:09 +09:00
9b594880c8 Restore crypto_key to migration 2019-04-09 19:02:02 +09:00
d44fc3db2f Update migrate.ts 2019-04-09 18:45:21 +09:00
5133b0a0c0 Update migrate.ts 2019-04-09 18:41:53 +09:00
815469304f typo 2019-04-09 18:37:14 +09:00
c651921c79 Fix bug 2019-04-09 18:36:43 +09:00
d18c835221 Update migrate.ts 2019-04-09 18:35:07 +09:00
e38335077e Update user.ts 2019-04-09 12:36:01 +09:00
34c150cf73 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-04-09 12:31:48 +09:00
24cb3ba091 Update migrate.ts 2019-04-09 12:31:45 +09:00
c07eaef2d1 Update note.ts 2019-04-09 12:25:19 +09:00
5df708ac9f Delete mark-admin.js 2019-04-09 04:09:34 +09:00
38ffbe95e9 Update migrate.ts 2019-04-09 01:42:07 +09:00
8bec4042fc Update migrate.ts 2019-04-09 01:31:54 +09:00
ee7ef89dfb Fix bug 2019-04-09 01:31:48 +09:00
54a5246061 Update migrate.ts 2019-04-09 01:03:58 +09:00
fa6eae2937 Update migrate.ts 2019-04-08 23:24:44 +09:00
795be20ee1 Clean up 2019-04-08 23:06:05 +09:00
87d3a06dcd revert rename 2019-04-08 23:05:41 +09:00
8b5c917195 Update CHANGELOG.md 2019-04-08 22:23:33 +09:00
c7da0e59ff Update CHANGELOG.md 2019-04-08 21:46:11 +09:00
eb14acbe0c Doc: Update installing command (#4655)
Replace the "Checkout to Latest Release" command.
Current setup document will checkout latest alpha version.
Because grep command in the document does not exclude alpha version tags.
2019-04-08 21:40:13 +09:00
56860a6bef wip migration 2019-04-08 20:33:42 +09:00
735687be21 update token generation 2019-04-08 20:29:52 +09:00
fab0cc51b3 Fix 2019-04-08 19:56:42 +09:00
3a26acbdb2 ユーザーリストでフォローボタンを表示するように (#4603) 2019-04-08 17:20:16 +09:00
121 changed files with 1415 additions and 791 deletions

View File

@ -8,11 +8,26 @@ If you encounter any problems with updating, please try the following:
11.0.0
----------
* データベースがMongoDBからPostgreSQLに変更されました
* アカウントを完全に削除できるように
* ミュート/ブロック時にそのユーザーの投稿のウォッチをすべて解除するように
* フォロー申請数が実際より1すくなくなる問題を修正
* リストからアカウント削除したユーザーを削除できない問題を修正
* リストTLでフォローしていないユーザーの非公開投稿が流れる問題を修正
* リストTLでダイレクト投稿が流れない問題を修正
* ミュートしているユーザーの投稿がタイムラインに流れてくることがある問題を修正
### APIの破壊的変更
* v10時点で deprecated だったパラメータなどを削除
* notes/hybrid-timeline が notes/social-timeline にリネーム
* ストリームの hybridTimeline チャンネルが socialTimeline にリネーム
* ユーザーリストの title が name に
10.100.0
----------
* ユーザーリストでフォローボタンを表示するように
* ドライブのファイルのサムネイルを修正
* 投稿ウィジットでローカルのみの公開範囲で投稿できない問題を修正
* TLを遡った時に抜けがある時がある問題を修正
* ユーザータイムラインが投稿日時順ではなくなっているのを修正
* 10.99.0 でチャートのレンダリングがおかしい問題を修正
10.99.0
----------

9
binding.gyp Normal file
View File

@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'crypto_key',
'sources': ['src/crypto_key.cc'],
'include_dirs': ['<!(node -e "require(\'nan\')")']
}
]
}

View File

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

View File

@ -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

View File

@ -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

View File

@ -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)でマイグレーション情報を確認する

View File

@ -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

View File

@ -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.

View File

@ -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)でマイグレーション情報を確認する

View File

@ -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/**/*',

View File

@ -1082,13 +1082,10 @@ desktop/views/components/settings.tags.vue:
add: "追加"
save: "保存"
desktop/views/components/taskmanager.vue:
title: "タスクマネージャ"
desktop/views/components/timeline.vue:
home: "ホーム"
local: "ローカル"
social: "ソーシャル"
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "ダイレクト投稿"
@ -1665,7 +1662,7 @@ mobile/views/pages/following.vue:
mobile/views/pages/home.vue:
home: "ホーム"
local: "ローカル"
social: "ソーシャル"
hybrid: "ソーシャル"
global: "グローバル"
mentions: "あなた宛て"
messages: "ダイレクト投稿"
@ -1735,7 +1732,7 @@ deck:
widgets: "ウィジェット"
home: "ホーム"
local: "ローカル"
social: "ソーシャル"
hybrid: "ソーシャル"
hashtag: "ハッシュタグ"
global: "グローバル"
mentions: "あなた宛て"

View File

@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "11.0.0-alpha.4",
"version": "11.0.0-beta.3",
"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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -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">
@ -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

View File

@ -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"/>

View File

@ -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;

View File

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

View File

@ -8,7 +8,7 @@
<div class="is-remote" v-if="user.host != null">
<details>
<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary>
<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a>
<a :href="user.url" target="_blank">{{ $t('@.view-on-remote') }}</a>
</details>
</div>
<header :style="bannerStyle">
@ -88,7 +88,7 @@ export default Vue.extend({
if (this.user == null) return {};
if (this.user.bannerUrl == null) return {};
return {
backgroundColor: this.user.bannerColor && this.user.bannerColor.length == 3 ? `rgb(${ this.user.bannerColor.join(',') })` : null,
backgroundColor: this.user.bannerColor,
backgroundImage: `url(${ this.user.bannerUrl })`
};
},

View File

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

View File

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

View File

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

View File

@ -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';

View File

@ -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';
}
},

View File

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

View File

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

View File

@ -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
} : {};
},

View File

@ -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';

View File

@ -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';
}
},

View File

@ -5,7 +5,7 @@
</template>
<div class="wwtwuxyh" v-if="!fetching">
<div class="is-suspended" v-if="user.isSuspended"><p><fa icon="exclamation-triangle"/> {{ $t('@.user-suspended') }}</p></div>
<div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div>
<div class="is-remote" v-if="user.host != null"><p><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}<a :href="user.url" target="_blank">{{ $t('@.view-on-remote') }}</a></p></div>
<header>
<div class="banner" :style="style"></div>
<div class="body">
@ -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 })`
};
}

View File

@ -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
View 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
View File

@ -0,0 +1,2 @@
export function extractPublic(keypair: string): string;
export function generate(): string;

View File

@ -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,

View File

@ -339,7 +339,7 @@ Misskeyは投稿のキャプチャと呼ばれる仕組みを提供していま
#### `note`
ローカルタイムラインに新しい投稿が流れてきたときに発生するイベントです。
## `socialTimeline`
## `hybridTimeline`
ソーシャルタイムラインの投稿情報が流れてきます。このチャンネルにパラメータはありません。
### 流れてくるイベント一覧

View File

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

497
src/migrate.ts Normal file
View File

@ -0,0 +1,497 @@
process.env.NODE_ENV = 'production';
import monk from 'monk';
import * as mongo from 'mongodb';
import * as fs from 'fs';
import * as uuid from 'uuid';
import chalk from 'chalk';
import config from './config';
import { initDb } from './db/postgre';
import { User } from './models/entities/user';
import { getRepository } from 'typeorm';
import generateUserToken from './server/api/common/generate-native-user-token';
import { DriveFile } from './models/entities/drive-file';
import { DriveFolder } from './models/entities/drive-folder';
import { InternalStorage } from './services/drive/internal-storage';
import { createTemp } from './misc/create-temp';
import { Note } from './models/entities/note';
import { Following } from './models/entities/following';
import { Poll } from './models/entities/poll';
import { PollVote } from './models/entities/poll-vote';
import { NoteFavorite } from './models/entities/note-favorite';
import { NoteReaction } from './models/entities/note-reaction';
import { UserPublickey } from './models/entities/user-publickey';
import { UserKeypair } from './models/entities/user-keypair';
import { extractPublic } from './crypto_key';
import { Emoji } from './models/entities/emoji';
import { toPuny } 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 test = false;
const limit = 500;
const nativeDbConn = async (): Promise<mongo.Db> => {
if (mdb) return mdb;
const db = await ((): Promise<mongo.Db> => new Promise((resolve, reject) => {
mongo.MongoClient.connect(uri, { useNewUrlParser: true }, (e: Error, client: any) => {
if (e) return reject(e);
resolve(client.db((config as any).mongodb.db));
});
}))();
mdb = db;
return db;
};
const _User = db.get<any>('users');
const _DriveFile = db.get<any>('driveFiles.files');
const _DriveFolder = db.get<any>('driveFolders');
const _Note = db.get<any>('notes');
const _Following = db.get<any>('following');
const _PollVote = db.get<any>('pollVotes');
const _Favorite = db.get<any>('favorites');
const _NoteReaction = db.get<any>('noteReactions');
const _Emoji = db.get<any>('emoji');
const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => {
const db = await nativeDbConn();
const bucket = new mongo.GridFSBucket(db, {
bucketName: 'driveFiles'
});
return bucket;
};
async function main() {
await initDb();
const Users = getRepository(User);
const UserProfiles = getRepository(UserProfile);
const DriveFiles = getRepository(DriveFile);
const DriveFolders = getRepository(DriveFolder);
const Notes = getRepository(Note);
const Followings = getRepository(Following);
const Polls = getRepository(Poll);
const PollVotes = getRepository(PollVote);
const NoteFavorites = getRepository(NoteFavorite);
const NoteReactions = getRepository(NoteReaction);
const UserPublickeys = getRepository(UserPublickey);
const UserKeypairs = getRepository(UserKeypair);
const Emojis = getRepository(Emoji);
async function migrateUser(user: any) {
await Users.save({
id: user._id.toHexString(),
createdAt: user.createdAt || new Date(),
username: user.username,
usernameLower: user.username.toLowerCase(),
host: toPuny(user.host),
token: generateUserToken(),
isAdmin: user.isAdmin || false,
name: user.name,
followersCount: user.followersCount || 0,
followingCount: user.followingCount || 0,
notesCount: user.notesCount || 0,
isBot: user.isBot || false,
isCat: user.isCat || false,
isVerified: user.isVerified || false,
inbox: user.inbox,
sharedInbox: user.sharedInbox,
uri: user.uri,
});
await UserProfiles.save({
userId: user._id.toHexString(),
description: user.description,
userHost: toPuny(user.host),
autoAcceptFollowed: true,
autoWatch: false,
password: user.password,
location: user.profile ? user.profile.location : null,
birthday: user.profile ? user.profile.birthday : null,
});
if (user.publicKey) {
await UserPublickeys.save({
userId: user._id.toHexString(),
keyId: user.publicKey.id,
keyPem: user.publicKey.publicKeyPem
});
}
if (user.keypair) {
await UserKeypairs.save({
userId: user._id.toHexString(),
publicKey: extractPublic(user.keypair),
privateKey: user.keypair,
});
}
}
async function migrateFollowing(following: any) {
await Followings.save({
id: following._id.toHexString(),
createdAt: new Date(),
followerId: following.followerId.toHexString(),
followeeId: following.followeeId.toHexString(),
// 非正規化
followerHost: following._follower ? toPuny(following._follower.host) : null,
followerInbox: following._follower ? following._follower.inbox : null,
followerSharedInbox: following._follower ? following._follower.sharedInbox : null,
followeeHost: following._followee ? toPuny(following._followee.host) : null,
followeeInbox: following._followee ? following._followee.inbox : null,
followeeSharedInbox: following._followee ? following._followee.sharedInbo : null
});
}
async function migrateDriveFolder(folder: any) {
await DriveFolders.save({
id: folder._id.toHexString(),
createdAt: folder.createdAt || new Date(),
name: folder.name,
parentId: folder.parentId ? folder.parentId.toHexString() : null,
});
}
async function migrateDriveFile(file: any) {
const user = await _User.findOne({
_id: file.metadata.userId
});
if (user == null) return;
if (file.metadata.storage && file.metadata.storage.key) { // when object storage
await DriveFiles.save({
id: file._id.toHexString(),
userId: user._id.toHexString(),
userHost: toPuny(user.host),
createdAt: file.uploadDate || new Date(),
md5: file.md5,
name: file.filename,
type: file.contentType,
properties: file.metadata.properties || {},
size: file.length,
url: file.metadata.url,
uri: file.metadata.uri,
accessKey: file.metadata.storage.key,
folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null,
storedInternal: false,
isLink: false
});
} else if (!file.metadata.isLink) {
const [temp, clean] = await createTemp();
await new Promise(async (res, rej) => {
const bucket = await getDriveFileBucket();
const readable = bucket.openDownloadStream(file._id);
const dest = fs.createWriteStream(temp);
readable.pipe(dest);
readable.on('end', () => {
dest.end();
res();
});
});
const key = uuid.v4();
const url = InternalStorage.saveFromPath(key, temp);
await DriveFiles.save({
id: file._id.toHexString(),
userId: user._id.toHexString(),
userHost: toPuny(user.host),
createdAt: file.uploadDate || new Date(),
md5: file.md5,
name: file.filename,
type: file.contentType,
properties: file.metadata.properties,
size: file.length,
url: url,
uri: file.metadata.uri,
accessKey: key,
folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null,
storedInternal: true,
isLink: false
});
clean();
} else {
await DriveFiles.save({
id: file._id.toHexString(),
userId: user._id.toHexString(),
userHost: toPuny(user.host),
createdAt: file.uploadDate || new Date(),
md5: file.md5,
name: file.filename,
type: file.contentType,
properties: file.metadata.properties,
size: file.length,
url: file.metadata.url,
uri: file.metadata.uri,
accessKey: null,
folderId: file.metadata.folderId ? file.metadata.folderId.toHexString() : null,
storedInternal: false,
isLink: true
});
}
}
async function migrateNote(note: any) {
await Notes.save({
id: note._id.toHexString(),
createdAt: note.createdAt || new Date(),
text: note.text,
cw: note.cw || null,
tags: note.tags || [],
userId: note.userId.toHexString(),
viaMobile: note.viaMobile || false,
geo: note.geo,
appId: null,
visibility: note.visibility || 'public',
visibleUserIds: note.visibleUserIds ? note.visibleUserIds.map((id: any) => id.toHexString()) : [],
replyId: note.replyId ? note.replyId.toHexString() : null,
renoteId: note.renoteId ? note.renoteId.toHexString() : null,
userHost: null,
fileIds: note.fileIds ? note.fileIds.map((id: any) => id.toHexString()) : [],
localOnly: note.localOnly || false,
hasPoll: note.poll != null
});
if (note.poll) {
await Polls.save({
noteId: note._id.toHexString(),
choices: note.poll.choices.map((x: any) => x.text),
expiresAt: note.poll.expiresAt,
multiple: note.poll.multiple,
votes: note.poll.choices.map((x: any) => x.votes),
noteVisibility: note.visibility,
userId: note.userId.toHexString(),
userHost: null
});
}
}
async function migratePollVote(vote: any) {
await PollVotes.save({
id: vote._id.toHexString(),
createdAt: vote.createdAt,
noteId: vote.noteId.toHexString(),
userId: vote.userId.toHexString(),
choice: vote.choice
});
}
async function migrateNoteFavorite(favorite: any) {
await NoteFavorites.save({
id: favorite._id.toHexString(),
createdAt: favorite.createdAt,
noteId: favorite.noteId.toHexString(),
userId: favorite.userId.toHexString(),
});
}
async function migrateNoteReaction(reaction: any) {
await NoteReactions.save({
id: reaction._id.toHexString(),
createdAt: reaction.createdAt,
noteId: reaction.noteId.toHexString(),
userId: reaction.userId.toHexString(),
reaction: reaction.reaction
});
}
async function reMigrateUser(user: any) {
const u = await _User.findOne({
_id: new mongo.ObjectId(user.id)
});
const avatar = u.avatarId ? await DriveFiles.findOne(u.avatarId.toHexString()) : null;
const banner = u.bannerId ? await DriveFiles.findOne(u.bannerId.toHexString()) : null;
await Users.update(user.id, {
avatarId: avatar ? avatar.id : null,
bannerId: banner ? banner.id : null,
avatarUrl: avatar ? avatar.url : null,
bannerUrl: banner ? banner.url : null
});
}
async function migrateEmoji(emoji: any) {
await Emojis.save({
id: emoji._id.toHexString(),
updatedAt: emoji.createdAt,
aliases: emoji.aliases,
url: emoji.url,
uri: emoji.uri,
host: toPuny(emoji.host),
name: emoji.name
});
}
let allUsersCount = await _User.count({
deletedAt: { $exists: false }
});
if (test && allUsersCount > limit) allUsersCount = limit;
for (let i = 0; i < allUsersCount; i++) {
const user = await _User.findOne({
deletedAt: { $exists: false }
}, {
skip: i
});
try {
await migrateUser(user);
console.log(`USER (${i + 1}/${allUsersCount}) ${user._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`USER (${i + 1}/${allUsersCount}) ${user._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
let allFollowingsCount = await _Following.count();
if (test && allFollowingsCount > limit) allFollowingsCount = limit;
for (let i = 0; i < allFollowingsCount; i++) {
const following = await _Following.findOne({}, {
skip: i
});
try {
await migrateFollowing(following);
console.log(`FOLLOWING (${i + 1}/${allFollowingsCount}) ${following._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`FOLLOWING (${i + 1}/${allFollowingsCount}) ${following._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
let allDriveFoldersCount = await _DriveFolder.count();
if (test && allDriveFoldersCount > limit) allDriveFoldersCount = limit;
for (let i = 0; i < allDriveFoldersCount; i++) {
const folder = await _DriveFolder.findOne({}, {
skip: i
});
try {
await migrateDriveFolder(folder);
console.log(`FOLDER (${i + 1}/${allDriveFoldersCount}) ${folder._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`FOLDER (${i + 1}/${allDriveFoldersCount}) ${folder._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
let allDriveFilesCount = await _DriveFile.count({
'metadata._user.host': null,
'metadata.deletedAt': { $exists: false }
});
if (test && allDriveFilesCount > limit) allDriveFilesCount = limit;
for (let i = 0; i < allDriveFilesCount; i++) {
const file = await _DriveFile.findOne({
'metadata._user.host': null,
'metadata.deletedAt': { $exists: false }
}, {
skip: i
});
try {
await migrateDriveFile(file);
console.log(`FILE (${i + 1}/${allDriveFilesCount}) ${file._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`FILE (${i + 1}/${allDriveFilesCount}) ${file._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
let allNotesCount = await _Note.count({
'_user.host': null,
'metadata.deletedAt': { $exists: false }
});
if (test && allNotesCount > limit) allNotesCount = limit;
for (let i = 0; i < allNotesCount; i++) {
const note = await _Note.findOne({
'_user.host': null,
'metadata.deletedAt': { $exists: false }
}, {
skip: i
});
try {
await migrateNote(note);
console.log(`NOTE (${i + 1}/${allNotesCount}) ${note._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`NOTE (${i + 1}/${allNotesCount}) ${note._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
let allPollVotesCount = await _PollVote.count();
if (test && allPollVotesCount > limit) allPollVotesCount = limit;
for (let i = 0; i < allPollVotesCount; i++) {
const vote = await _PollVote.findOne({}, {
skip: i
});
try {
await migratePollVote(vote);
console.log(`VOTE (${i + 1}/${allPollVotesCount}) ${vote._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`VOTE (${i + 1}/${allPollVotesCount}) ${vote._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
let allNoteFavoritesCount = await _Favorite.count();
if (test && allNoteFavoritesCount > limit) allNoteFavoritesCount = limit;
for (let i = 0; i < allNoteFavoritesCount; i++) {
const favorite = await _Favorite.findOne({}, {
skip: i
});
try {
await migrateNoteFavorite(favorite);
console.log(`FAVORITE (${i + 1}/${allNoteFavoritesCount}) ${favorite._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`FAVORITE (${i + 1}/${allNoteFavoritesCount}) ${favorite._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
let allNoteReactionsCount = await _NoteReaction.count();
if (test && allNoteReactionsCount > limit) allNoteReactionsCount = limit;
for (let i = 0; i < allNoteReactionsCount; i++) {
const reaction = await _NoteReaction.findOne({}, {
skip: i
});
try {
await migrateNoteReaction(reaction);
console.log(`REACTION (${i + 1}/${allNoteReactionsCount}) ${reaction._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`REACTION (${i + 1}/${allNoteReactionsCount}) ${reaction._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
let allActualUsersCount = await Users.count();
if (test && allActualUsersCount > limit) allActualUsersCount = limit;
for (let i = 0; i < allActualUsersCount; i++) {
const [user] = await Users.find({
take: 1,
skip: i
});
try {
await reMigrateUser(user);
console.log(`RE:USER (${i + 1}/${allActualUsersCount}) ${user.id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`RE:USER (${i + 1}/${allActualUsersCount}) ${user.id} ${chalk.red('ERR')}`);
console.error(e);
}
}
const allEmojisCount = await _Emoji.count();
for (let i = 0; i < allEmojisCount; i++) {
const emoji = await _Emoji.findOne({}, {
skip: i
});
try {
await migrateEmoji(emoji);
console.log(`EMOJI (${i + 1}/${allEmojisCount}) ${emoji._id} ${chalk.green('DONE')}`);
} catch (e) {
console.log(`EMOJI (${i + 1}/${allEmojisCount}) ${emoji._id} ${chalk.red('ERR')}`);
console.error(e);
}
}
console.log('DONE :)');
}
main();

View File

@ -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}`;
};

View File

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

View File

@ -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}`;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,13 +15,6 @@ export class Note {
})
public createdAt: Date;
@Index()
@Column('timestamp with time zone', {
nullable: true,
comment: 'The updated date of the Note.'
})
public updatedAt: Date | null;
@Index()
@Column({
...id(),
@ -126,7 +119,7 @@ export class Note {
@Index({ unique: true })
@Column('varchar', {
length: 256, nullable: true,
length: 512, nullable: true,
comment: 'The URI of a note. it will be null when the note is local.'
})
public uri: string | null;
@ -183,7 +176,7 @@ export class Note {
public hasPoll: boolean;
@Column('jsonb', {
nullable: true, default: {}
nullable: true, default: null
})
public geo: any | null;
@ -195,12 +188,6 @@ export class Note {
})
public userHost: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userInbox: string | null;
@Column({
...id(),
nullable: true,
@ -227,6 +214,14 @@ export class Note {
})
public renoteUserHost: string | null;
//#endregion
constructor(data: Partial<Note>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}
export type IMentionedRemoteUsers = {

View File

@ -6,10 +6,6 @@ import { User } from './user';
@Entity()
export class Poll {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column(id())
public noteId: Note['id'];
@OneToOne(type => Note, {
@ -57,6 +53,14 @@ export class Poll {
})
public userHost: string | null;
//#endregion
constructor(data: Partial<Poll>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}
export type IPoll = {

View File

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

View File

@ -1,14 +1,10 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserKeypair {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column(id())
public userId: User['id'];
@OneToOne(type => User, {
@ -26,4 +22,12 @@ export class UserKeypair {
length: 4096,
})
public privateKey: string;
constructor(data: Partial<UserKeypair>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -0,0 +1,209 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { id } from '../id';
import { User } from './user';
@Entity()
export class UserProfile {
@PrimaryColumn(id())
public userId: User['id'];
@OneToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The location of the User.'
})
public location: string | null;
@Column('char', {
length: 10, nullable: true,
comment: 'The birthday (YYYY-MM-DD) of the User.'
})
public birthday: string | null;
@Column('varchar', {
length: 1024, nullable: true,
comment: 'The description (bio) of the User.'
})
public description: string | null;
@Column('jsonb', {
default: [],
})
public fields: {
name: string;
value: string;
}[];
@Column('varchar', {
length: 512, nullable: true,
comment: 'Remote URL of the user.'
})
public url: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The email address of the User.'
})
public email: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public emailVerifyCode: string | null;
@Column('boolean', {
default: false,
})
public emailVerified: boolean;
@Column('varchar', {
length: 128, nullable: true,
})
public twoFactorTempSecret: string | null;
@Column('varchar', {
length: 128, nullable: true,
})
public twoFactorSecret: string | null;
@Column('boolean', {
default: false,
})
public twoFactorEnabled: boolean;
@Column('varchar', {
length: 128, nullable: true,
comment: 'The password hash of the User. It will be null if the origin of the user is local.'
})
public password: string | null;
@Column('jsonb', {
default: {},
comment: 'The client-specific data of the User.'
})
public clientData: Record<string, any>;
@Column('boolean', {
default: false,
})
public autoWatch: boolean;
@Column('boolean', {
default: false,
})
public autoAcceptFollowed: boolean;
@Column('boolean', {
default: false,
})
public alwaysMarkNsfw: boolean;
@Column('boolean', {
default: false,
})
public carefulBot: boolean;
//#region Linking
@Column('boolean', {
default: false,
})
public twitter: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterAccessToken: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterAccessTokenSecret: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterUserId: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterScreenName: string | null;
@Column('boolean', {
default: false,
})
public github: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public githubAccessToken: string | null;
@Column('integer', {
nullable: true, default: null,
})
public githubId: number | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public githubLogin: string | null;
@Column('boolean', {
default: false,
})
public discord: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordAccessToken: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordRefreshToken: string | null;
@Column('integer', {
nullable: true, default: null,
})
public discordExpiresDate: number | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordId: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordUsername: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordDiscriminator: string | null;
//#endregion
//#region Denormalized fields
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userHost: string | null;
//#endregion
constructor(data: Partial<UserProfile>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -5,10 +5,6 @@ import { id } from '../id';
@Entity()
export class UserPublickey {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column(id())
public userId: User['id'];
@OneToOne(type => User, {
@ -27,4 +23,12 @@ export class UserPublickey {
length: 4096,
})
public keyPem: string;
constructor(data: Partial<UserPublickey>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}

View File

@ -1,108 +0,0 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, OneToOne } from 'typeorm';
import { User } from './user';
import { id } from '../id';
@Entity()
export class UserServiceLinking {
@PrimaryColumn(id())
public id: string;
@Index({ unique: true })
@Column(id())
public userId: User['id'];
@OneToOne(type => User, {
onDelete: 'CASCADE'
})
@JoinColumn()
public user: User | null;
@Column('boolean', {
default: false,
})
public twitter: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterAccessToken: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterAccessTokenSecret: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterUserId: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public twitterScreenName: string | null;
@Column('boolean', {
default: false,
})
public github: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public githubAccessToken: string | null;
@Column('integer', {
nullable: true, default: null,
})
public githubId: number | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public githubLogin: string | null;
@Column('boolean', {
default: false,
})
public discord: boolean;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordAccessToken: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordRefreshToken: string | null;
@Column('integer', {
nullable: true, default: null,
})
public discordExpiresDate: number | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordId: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordUsername: string | null;
@Column('varchar', {
length: 64, nullable: true, default: null,
})
public discordDiscriminator: string | null;
//#region Denormalized fields
@Index()
@Column('varchar', {
length: 128, nullable: true,
comment: '[Denormalized]'
})
public userHost: string | null;
//#endregion
}

View File

@ -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,12 +89,6 @@ 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: '{}'
@ -114,38 +96,12 @@ export class User {
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,
length: 512, nullable: true,
})
public avatarUrl: string | null;
@Column('varchar', {
length: 256, nullable: true,
length: 512, nullable: true,
})
public bannerUrl: string | null;
@ -206,11 +162,6 @@ export class User {
})
public isVerified: boolean;
@Column('boolean', {
default: false,
})
public twoFactorEnabled: boolean;
@Column('varchar', {
length: 128, array: true, default: '{}'
})
@ -224,68 +175,44 @@ export class User {
public host: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: 'The inbox of the User. It will be null if the origin of the user is local.'
length: 512, nullable: true,
comment: 'The inbox URL of the User. It will be null if the origin of the user is local.'
})
public inbox: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: 'The sharedInbox of the User. It will be null if the origin of the user is local.'
length: 512, nullable: true,
comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'
})
public sharedInbox: string | null;
@Column('varchar', {
length: 256, nullable: true,
comment: 'The featured of the User. It will be null if the origin of the user is local.'
length: 512, nullable: true,
comment: 'The featured URL of the User. It will be null if the origin of the user is local.'
})
public featured: string | null;
@Index()
@Column('varchar', {
length: 256, nullable: true,
length: 512, nullable: true,
comment: 'The URI of the User. It will be null if the origin of the user is local.'
})
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>;
constructor(data: Partial<User>) {
if (data == null) return;
@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;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
}
export interface ILocalUser extends User {

View File

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

View File

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

View File

@ -167,6 +167,7 @@ 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,

View File

@ -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,6 +80,7 @@ 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,
@ -91,6 +92,9 @@ export class UserRepository extends Repository<User> {
avatarColor: user.avatarColor,
bannerColor: user.bannerColor,
isAdmin: user.isAdmin,
isBot: user.isBot,
isCat: user.isCat,
isVerified: user.isVerified,
// カスタム絵文字添付
emojis: user.emojis.length > 0 ? Emojis.find({
@ -113,9 +117,12 @@ export class UserRepository extends Repository<User> {
} : {}),
...(opts.detail ? {
description: user.description,
location: user.location,
birthday: user.birthday,
url: profile.url,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
description: profile.description,
location: profile.location,
birthday: profile.birthday,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
@ -128,9 +135,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 +157,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 +176,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 {

View File

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

View File

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

View File

@ -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,8 @@ 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';
import { validActor } from '../../remote/activitypub/type';
const logger = new Logger('inbox');
@ -33,7 +34,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 +54,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 +80,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;
}
@ -87,7 +94,7 @@ export default async (job: Bull.Job): Promise<void> => {
// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') {
if (activity.object && activity.object.type === 'Person') {
if (activity.object && validActor.includes(activity.object.type)) {
if (user == null) {
logger.warn('Update activity received, but user not registed.');
} else if (!httpSignature.verifySignature(signature, key.keyPem)) {
@ -145,7 +152,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 +161,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 +169,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');
}
}

View File

@ -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) {

View File

@ -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 [];

View File

@ -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,18 @@ 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';
import { validActor } from '../../../remote/activitypub/type';
import { getConnection } from 'typeorm';
const logger = apLogger;
/**
@ -32,13 +34,13 @@ const logger = apLogger;
* @param uri Fetch target URI
*/
function validatePerson(x: any, uri: string) {
const expectHost = toUnicode(new URL(uri).hostname.toLowerCase());
const expectHost = toPuny(new URL(uri).hostname);
if (x == null) {
return new Error('invalid person: object is null');
}
if (x.type != 'Person' && x.type != 'Service') {
if (!validActor.includes(x.type)) {
return new Error(`invalid person: object is not a person or service '${x.type}'`);
}
@ -62,7 +64,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 +73,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 +126,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());
@ -135,13 +137,14 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
// Create user
let user: IRemoteUser;
try {
user = await Users.save({
// Start transaction
await getConnection().transaction(async transactionalEntityManager => {
user = await transactionalEntityManager.save(new User({
id: genId(),
avatarId: null,
bannerId: null,
createdAt: Date.parse(person.published) || new Date(),
createdAt: new Date(person.published) || new Date(),
lastFetchedAt: new Date(),
description: fromHtml(person.summary),
name: person.name,
isLocked: person.manuallyApprovesFollowers,
username: person.preferredUsername,
@ -150,15 +153,26 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
inbox: person.inbox,
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
featured: person.featured,
endpoints: person.endpoints,
uri: person.id,
url: person.url,
fields,
...services,
tags,
isBot,
isCat: (person as any).isCat === true
} as Partial<User>) as IRemoteUser;
})) as IRemoteUser;
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: fromHtml(person.summary),
url: person.url,
fields,
userHost: host
}));
await transactionalEntityManager.save(new UserPublickey({
userId: user.id,
keyId: person.publicKey.id,
keyPem: person.publicKey.publicKeyPem
}));
});
} catch (e) {
// duplicate key error
if (isDuplicateKeyValueError(e)) {
@ -169,18 +183,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
throw e;
}
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 +207,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 +349,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,

View File

@ -68,11 +68,7 @@ export async function updateQuestion(value: any) {
}
}
await Notes.update(note.id, {
updatedAt: new Date(),
});
await Polls.update(poll.id, {
await Polls.update({ noteId: note.id }, {
votes: poll.votes
});

View File

@ -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,

View File

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

View File

@ -65,6 +65,8 @@ interface IQuestionChoice {
_misskey_votes?: number;
}
export const validActor = ['Person', 'Service'];
export interface IPerson extends IObject {
type: 'Person';
name: string;

View File

@ -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) {

View File

@ -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);
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 });
}
res(result.object);
})) as IWebFinger;
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})`);
}

View File

@ -80,7 +80,11 @@ export default async (endpoint: string, user: User, app: App, data: any, file?:
apiLogger.error(`Internal error occurred in ${ep.name}`, {
ep: ep.name,
ps: data,
e: e
e: {
message: e.message,
code: e.name,
stack: e.stack
}
});
throw new ApiError(null, {
e: {

View File

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

View File

@ -1,6 +0,0 @@
import { toUnicode } from 'punycode';
export default (host: string) => {
if (host == null) return null;
return toUnicode(host).toLowerCase();
};

View File

@ -1 +1 @@
export default (token: string) => token.startsWith('!');
export default (token: string) => token.startsWith('0');

View File

@ -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 => ({

View File

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

View File

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

View File

@ -10,6 +10,7 @@ import { Users, Notes } from '../../../../models';
import { Note } from '../../../../models/entities/note';
import { User } from '../../../../models/entities/user';
import fetchMeta from '../../../../misc/fetch-meta';
import { validActor } from '../../../../remote/activitypub/type';
export const meta = {
tags: ['federation'],
@ -110,7 +111,7 @@ async function fetchAny(uri: string) {
}
// それでもみつからなければ新規であるため登録
if (object.type === 'Person') {
if (validActor.includes(object.type)) {
const user = await createPerson(object.id);
return {
type: 'User',

View File

@ -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({

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import * as speakeasy from 'speakeasy';
import * as QRCode from 'qrcode';
import config from '../../../../../config';
import define from '../../../define';
import { Users } from '../../../../../models';
import { UserProfiles } from '../../../../../models';
export const meta = {
requireCredential: true,
@ -19,8 +19,10 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id });
// Compare password
const same = await bcrypt.compare(ps.password, user.password);
const same = await bcrypt.compare(ps.password, profile.password);
if (!same) {
throw new Error('incorrect password');
@ -31,7 +33,7 @@ export default define(meta, async (ps, user) => {
length: 32
});
await Users.update(user.id, {
await UserProfiles.update({ userId: user.id }, {
twoFactorTempSecret: secret.base32
});

View File

@ -1,7 +1,7 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import define from '../../../define';
import { Users } from '../../../../../models';
import { UserProfiles } from '../../../../../models';
export const meta = {
requireCredential: true,
@ -16,17 +16,17 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id });
// Compare password
const same = await bcrypt.compare(ps.password, user.password);
const same = await bcrypt.compare(ps.password, profile.password);
if (!same) {
throw new Error('incorrect password');
}
await Users.update(user.id, {
await UserProfiles.update({ userId: user.id }, {
twoFactorSecret: null,
twoFactorEnabled: false
});
return;
});

View File

@ -1,7 +1,7 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import define from '../../define';
import { Users } from '../../../../models';
import { UserProfiles } from '../../../../models';
export const meta = {
requireCredential: true,
@ -20,8 +20,10 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id });
// Compare password
const same = await bcrypt.compare(ps.currentPassword, user.password);
const same = await bcrypt.compare(ps.currentPassword, profile.password);
if (!same) {
throw new Error('incorrect password');
@ -31,7 +33,7 @@ export default define(meta, async (ps, user) => {
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(ps.newPassword, salt);
await Users.update(user.id, {
await UserProfiles.update({ userId: user.id }, {
password: hash
});
});

View File

@ -1,7 +1,7 @@
import $ from 'cafy';
import * as bcrypt from 'bcryptjs';
import define from '../../define';
import { Users } from '../../../../models';
import { Users, UserProfiles } from '../../../../models';
export const meta = {
requireCredential: true,
@ -16,8 +16,10 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id });
// Compare password
const same = await bcrypt.compare(ps.password, user.password);
const same = await bcrypt.compare(ps.password, profile.password);
if (!same) {
throw new Error('incorrect password');

View File

@ -3,7 +3,7 @@ import * as bcrypt from 'bcryptjs';
import { publishMainStream } from '../../../../services/stream';
import generateUserToken from '../../common/generate-native-user-token';
import define from '../../define';
import { Users } from '../../../../models';
import { Users, UserProfiles } from '../../../../models';
export const meta = {
requireCredential: true,
@ -18,8 +18,10 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id });
// Compare password
const same = await bcrypt.compare(ps.password, user.password);
const same = await bcrypt.compare(ps.password, profile.password);
if (!same) {
throw new Error('incorrect password');

View File

@ -1,7 +1,7 @@
import $ from 'cafy';
import { publishMainStream } from '../../../../services/stream';
import define from '../../define';
import { Users } from '../../../../models';
import { UserProfiles } from '../../../../models';
export const meta = {
requireCredential: true,
@ -20,13 +20,13 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
await Users.createQueryBuilder().update()
await UserProfiles.createQueryBuilder().update()
.set({
clientData: {
[ps.name]: ps.value
},
})
.where('id = :id', { id: user.id })
.where('userId = :id', { id: user.id })
.execute();
// Publish event

View File

@ -8,7 +8,7 @@ import config from '../../../../config';
import * as ms from 'ms';
import * as bcrypt from 'bcryptjs';
import { apiLogger } from '../../logger';
import { Users } from '../../../../models';
import { Users, UserProfiles } from '../../../../models';
export const meta = {
requireCredential: true,
@ -32,14 +32,16 @@ export const meta = {
};
export default define(meta, async (ps, user) => {
const profile = await UserProfiles.findOne({ userId: user.id });
// Compare password
const same = await bcrypt.compare(ps.password, user.password);
const same = await bcrypt.compare(ps.password, profile.password);
if (!same) {
throw new Error('incorrect password');
}
await Users.update(user.id, {
await UserProfiles.update({ userId: user.id }, {
email: ps.email,
emailVerified: false,
emailVerifyCode: null
@ -56,7 +58,7 @@ export default define(meta, async (ps, user) => {
if (ps.email != null) {
const code = rndstr('a-z0-9', 16);
await Users.update(user.id, {
await UserProfiles.update({ userId: user.id }, {
emailVerifyCode: code
});

View File

@ -10,7 +10,9 @@ import extractHashtags from '../../../../misc/extract-hashtags';
import * as langmap from 'langmap';
import { updateHashtag } from '../../../../services/update-hashtag';
import { ApiError } from '../../error';
import { Users, DriveFiles } from '../../../../models';
import { Users, DriveFiles, UserProfiles } from '../../../../models';
import { User } from '../../../../models/entities/user';
import { UserProfile } from '../../../../models/entities/user-profile';
export const meta = {
desc: {
@ -154,22 +156,23 @@ export const meta = {
export default define(meta, async (ps, user, app) => {
const isSecure = user != null && app == null;
const updates = {} as any;
const updates = {} as Partial<User>;
const profile = {} as Partial<UserProfile>;
if (ps.name !== undefined) updates.name = ps.name;
if (ps.description !== undefined) updates.description = ps.description;
if (ps.lang !== undefined) updates.lang = ps.lang;
if (ps.location !== undefined) updates.location = ps.location;
if (ps.birthday !== undefined) updates.birthday = ps.birthday;
if (ps.description !== undefined) profile.description = ps.description;
//if (ps.lang !== undefined) updates.lang = ps.lang;
if (ps.location !== undefined) profile.location = ps.location;
if (ps.birthday !== undefined) profile.birthday = ps.birthday;
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot == 'boolean') updates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed == 'boolean') updates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.carefulBot == 'boolean') profile.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed == 'boolean') profile.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
if (typeof ps.autoWatch == 'boolean') updates.autoWatch = ps.autoWatch;
if (typeof ps.alwaysMarkNsfw == 'boolean') updates.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (typeof ps.autoWatch == 'boolean') profile.autoWatch = ps.autoWatch;
if (typeof ps.alwaysMarkNsfw == 'boolean') profile.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (ps.avatarId) {
const avatar = await DriveFiles.findOne(ps.avatarId);
@ -177,7 +180,7 @@ export default define(meta, async (ps, user, app) => {
if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar);
if (!avatar.type.startsWith('image/')) throw new ApiError(meta.errors.avatarNotAnImage);
updates.avatarUrl = avatar.thumbnailUrl;
updates.avatarUrl = DriveFiles.getPublicUrl(avatar, true);
if (avatar.properties.avgColor) {
updates.avatarColor = avatar.properties.avgColor;
@ -190,7 +193,7 @@ export default define(meta, async (ps, user, app) => {
if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner);
if (!banner.type.startsWith('image/')) throw new ApiError(meta.errors.bannerNotAnImage);
updates.bannerUrl = banner.webpublicUrl;
updates.bannerUrl = DriveFiles.getPublicUrl(banner, false);
if (banner.properties.avgColor) {
updates.bannerColor = banner.properties.avgColor;
@ -206,8 +209,8 @@ export default define(meta, async (ps, user, app) => {
emojis = emojis.concat(extractEmojis(tokens));
}
if (updates.description != null) {
const tokens = parse(updates.description);
if (profile.description != null) {
const tokens = parse(profile.description);
emojis = emojis.concat(extractEmojis(tokens));
tags = extractHashtags(tokens).map(tag => tag.toLowerCase());
}
@ -220,7 +223,8 @@ export default define(meta, async (ps, user, app) => {
for (const tag of user.tags.filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
//#endregion
await Users.update(user.id, updates);
if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
if (Object.keys(profile).length > 0) await UserProfiles.update({ userId: user.id }, profile);
const iObj = await Users.pack(user.id, user, {
detail: true,

View File

@ -95,7 +95,7 @@ export const meta = {
errors: {
stlDisabled: {
message: 'Social timeline has been disabled.',
message: 'Hybrid timeline has been disabled.',
code: 'STL_DISABLED',
id: '620763f4-f621-4533-ab33-0577a1a3c342'
},

View File

@ -10,7 +10,7 @@ import { deliver } from '../../../../../queue';
import { renderActivity } from '../../../../../remote/activitypub/renderer';
import renderVote from '../../../../../remote/activitypub/renderer/vote';
import { deliverQuestionUpdate } from '../../../../../services/note/polls/update';
import { PollVotes, NoteWatchings, Users, Polls } from '../../../../../models';
import { PollVotes, NoteWatchings, Users, Polls, UserProfiles } from '../../../../../models';
import { Not } from 'typeorm';
import { IRemoteUser } from '../../../../../models/entities/user';
import { genId } from '../../../../../misc/gen-id';
@ -123,7 +123,7 @@ export default define(meta, async (ps, user) => {
// Increment votes count
const index = ps.choice + 1; // In SQL, array index is 1 based
await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE id = '${poll.id}'`);
await Polls.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE noteId = '${poll.noteId}'`);
publishNoteStream(note.id, 'pollVoted', {
choice: ps.choice,
@ -149,8 +149,10 @@ export default define(meta, async (ps, user) => {
}
});
const profile = await UserProfiles.findOne({ userId: user.id });
// この投稿をWatchする
if (user.autoWatch !== false) {
if (profile.autoWatch !== false) {
watch(user.id, note);
}

View File

@ -4,6 +4,7 @@ import define from '../../define';
import { ApiError } from '../../error';
import { Users, Followings } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { toPuny } from '../../../../misc/convert-host';
export const meta = {
desc: {
@ -65,7 +66,7 @@ export const meta = {
export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId != null
? { id: ps.userId }
: { usernameLower: ps.username.toLowerCase(), host: ps.host });
: { usernameLower: ps.username.toLowerCase(), host: toPuny(ps.host) });
if (user == null) {
throw new ApiError(meta.errors.noSuchUser);

View File

@ -4,6 +4,7 @@ import define from '../../define';
import { ApiError } from '../../error';
import { Users, Followings } from '../../../../models';
import { makePaginationQuery } from '../../common/make-pagination-query';
import { toPuny } from '../../../../misc/convert-host';
export const meta = {
desc: {
@ -65,7 +66,7 @@ export const meta = {
export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId != null
? { id: ps.userId }
: { usernameLower: ps.username.toLowerCase(), host: ps.host });
: { usernameLower: ps.username.toLowerCase(), host: toPuny(ps.host) });
if (user == null) {
throw new ApiError(meta.errors.noSuchUser);

View File

@ -15,7 +15,6 @@ import signin from './private/signin';
import discord from './service/discord';
import github from './service/github';
import twitter from './service/twitter';
import { toASCII } from 'punycode';
import { Instances } from '../../models';
// Init app
@ -71,9 +70,7 @@ router.get('/v1/instance/peers', async ctx => {
select: ['host']
});
const punyCodes = instances.map(instance => toASCII(instance.host));
ctx.body = punyCodes;
ctx.body = instances.map(instance => instance.host);
});
// Return 404 for unknown API

View File

@ -4,7 +4,7 @@ import * as speakeasy from 'speakeasy';
import { publishMainStream } from '../../../services/stream';
import signin from '../common/signin';
import config from '../../../config';
import { Users, Signins } from '../../../models';
import { Users, Signins, UserProfiles } from '../../../models';
import { ILocalUser } from '../../../models/entities/user';
import { genId } from '../../../misc/gen-id';
@ -45,13 +45,15 @@ export default async (ctx: Koa.BaseContext) => {
return;
}
const profile = await UserProfiles.findOne({ userId: user.id });
// Compare password
const same = await bcrypt.compare(password, user.password);
const same = await bcrypt.compare(password, profile.password);
if (same) {
if (user.twoFactorEnabled) {
if (profile.twoFactorEnabled) {
const verified = (speakeasy as any).totp.verify({
secret: user.twoFactorSecret,
secret: profile.twoFactorSecret,
encoding: 'base32',
token: token
});

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