Compare commits

...

40 Commits

Author SHA1 Message Date
syuilo
64e10e9619 11.0.0-beta.4 2019-04-12 02:25:56 +09:00
syuilo
b89cffe98d Fix bug 2019-04-12 02:22:22 +09:00
syuilo
bd76ba702f 🎨 2019-04-12 02:15:22 +09:00
syuilo
5d52e9ce6b 11.0.0-beta.3 2019-04-12 01:56:48 +09:00
syuilo
3c29027ca3 Clean up 2019-04-12 01:54:28 +09:00
syuilo
2ff3069d23 トランザクションを使うようにしたり 2019-04-12 01:52:25 +09:00
syuilo
4198246351 トランザクションを使用してアンケートレコードの挿入に失敗した場合に投稿レコードの挿入もなかったことにするように 2019-04-12 01:30:10 +09:00
syuilo
2b6389b4dc Fix bug 2019-04-12 01:03:40 +09:00
syuilo
d7df75ae6c Clean up 2019-04-12 01:01:25 +09:00
syuilo
11c30eccb3 非正規化カラムを削除
非正規化するほどの情報じゃない
2019-04-12 00:42:39 +09:00
syuilo
ab8c6515b8 Fix error log 2019-04-12 00:33:26 +09:00
syuilo
d4ad36fa41 Update migrate.ts 2019-04-11 22:49:12 +09:00
syuilo
4d688be3df Update migrate.ts 2019-04-11 22:44:04 +09:00
syuilo
d2b75f3501 Update migrate.ts 2019-04-11 19:42:35 +09:00
syuilo
46b78cb4ff Increase url column length 2019-04-11 19:03:49 +09:00
syuilo
5d6e0d0f37 Update migrate.ts 2019-04-11 16:15:27 +09:00
syuilo
e19d0a37bb Update migrate.ts 2019-04-11 16:09:33 +09:00
syuilo
dea3e2132e Update migrate.ts 2019-04-11 15:53:15 +09:00
syuilo
91c1ceefbd Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-04-11 12:59:16 +09:00
syuilo
5c50989970 Fix bug 2019-04-11 12:59:09 +09:00
MeiMei
2a7e3b9c51 Fix: AP actor Service のサポートが不完全 (v11) (#4662) 2019-04-11 03:09:12 +09:00
syuilo
ab302df0ae Update CHANGELOG.md 2019-04-11 00:18:37 +09:00
syuilo
21e5809993 Clean up 2019-04-10 23:57:39 +09:00
syuilo
c58afc67e8 Update migrate.ts 2019-04-10 20:13:14 +09:00
syuilo
8e344f2deb 11.0.0-beta.2 2019-04-10 20:08:19 +09:00
syuilo
c28f4ffb3f Clean up 2019-04-10 20:07:36 +09:00
syuilo
2a40240310 Fix bug 2019-04-10 18:35:51 +09:00
syuilo
0e9fba9287 11.0.0-beta.1 2019-04-10 18:25:16 +09:00
syuilo
2e3dd2a30a Fix bug 2019-04-10 18:13:33 +09:00
syuilo
dda5f6559d Fix bug 2019-04-10 18:10:09 +09:00
syuilo
4152e59638 Fix bug 2019-04-10 18:05:39 +09:00
syuilo
9d5a92bce6 11.0.0-alpha.10 2019-04-10 15:08:05 +09:00
syuilo
30172b92e6 WebFingerリクエストで Proxy, Keep-Alive などをサポート #4658
Co-Authored-By: MeiMei <mei23@users.noreply.github.com>
2019-04-10 15:07:21 +09:00
syuilo
8468a9d4c7 11.0.0-alpha.9 2019-04-10 15:04:33 +09:00
syuilo
626cfb61ac テーブル分割 2019-04-10 15:04:27 +09:00
syuilo
9603f3fa4f Delete get-user-summary.ts 2019-04-10 14:58:45 +09:00
syuilo
619a1b9e53 Fix bug 2019-04-10 14:10:00 +09:00
syuilo
62e76ad588 11.0.0-alpha.8 2019-04-10 01:00:00 +09:00
syuilo
236d72685d More puny 2019-04-10 00:59:41 +09:00
syuilo
72a5f7b1e2 Fix bug 2019-04-10 00:40:10 +09:00
73 changed files with 736 additions and 683 deletions

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "11.0.0-alpha.7", "version": "11.0.0-beta.4",
"codename": "daybreak", "codename": "daybreak",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -258,7 +258,6 @@
"vuex": "3.1.0", "vuex": "3.1.0",
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
"web-push": "3.3.3", "web-push": "3.3.3",
"webfinger.js": "2.7.0",
"webpack": "4.28.4", "webpack": "4.28.4",
"webpack-cli": "3.2.3", "webpack-cli": "3.2.3",
"websocket": "1.0.28", "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

@@ -8,7 +8,7 @@
<div class="is-remote" v-if="user.host != null"> <div class="is-remote" v-if="user.host != null">
<details> <details>
<summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary> <summary><fa icon="exclamation-triangle"/> {{ $t('@.is-remote-user') }}</summary>
<a :href="user.url || user.uri" target="_blank">{{ $t('@.view-on-remote') }}</a> <a :href="user.url" target="_blank">{{ $t('@.view-on-remote') }}</a>
</details> </details>
</div> </div>
<header :style="bannerStyle"> <header :style="bannerStyle">

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import { URL } from 'url';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { Source, Mixin } from './types'; import { Source, Mixin } from './types';
import * as pkg from '../../package.json'; import * as pkg from '../../package.json';
import { toPuny } from '../misc/convert-host';
/** /**
* Path of configuration directory * Path of configuration directory
@@ -26,14 +25,14 @@ export default function load() {
const mixin = {} as Mixin; const mixin = {} as Mixin;
const url = validateUrl(config.url); const url = tryCreateUrl(config.url);
config.url = toPuny(normalizeUrl(config.url)); config.url = url.origin;
config.port = config.port || parseInt(process.env.PORT, 10); config.port = config.port || parseInt(process.env.PORT, 10);
mixin.host = toPuny(url.host); mixin.host = url.host;
mixin.hostname = toPuny(url.hostname); mixin.hostname = url.hostname;
mixin.scheme = url.protocol.replace(/:$/, ''); mixin.scheme = url.protocol.replace(/:$/, '');
mixin.wsScheme = mixin.scheme.replace('http', 'ws'); mixin.wsScheme = mixin.scheme.replace('http', 'ws');
mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`; mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
@@ -54,14 +53,3 @@ function tryCreateUrl(url: string) {
throw `url="${url}" is not a valid URL.`; 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;
}

View File

@@ -36,10 +36,10 @@ import { Emoji } from '../models/entities/emoji';
import { ReversiGame } from '../models/entities/games/reversi/game'; import { ReversiGame } from '../models/entities/games/reversi/game';
import { ReversiMatching } from '../models/entities/games/reversi/matching'; import { ReversiMatching } from '../models/entities/games/reversi/matching';
import { UserNotePining } from '../models/entities/user-note-pinings'; import { UserNotePining } from '../models/entities/user-note-pinings';
import { UserServiceLinking } from '../models/entities/user-service-linking';
import { Poll } from '../models/entities/poll'; import { Poll } from '../models/entities/poll';
import { UserKeypair } from '../models/entities/user-keypair'; import { UserKeypair } from '../models/entities/user-keypair';
import { UserPublickey } from '../models/entities/user-publickey'; import { UserPublickey } from '../models/entities/user-publickey';
import { UserProfile } from '../models/entities/user-profile';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@@ -101,12 +101,12 @@ export function initDb(justBorrow = false, sync = false, log = false) {
AuthSession, AuthSession,
AccessToken, AccessToken,
User, User,
UserProfile,
UserKeypair, UserKeypair,
UserPublickey, UserPublickey,
UserList, UserList,
UserListJoining, UserListJoining,
UserNotePining, UserNotePining,
UserServiceLinking,
Following, Following,
FollowRequest, FollowRequest,
Muting, Muting,

View File

@@ -16,7 +16,6 @@ import { InternalStorage } from './services/drive/internal-storage';
import { createTemp } from './misc/create-temp'; import { createTemp } from './misc/create-temp';
import { Note } from './models/entities/note'; import { Note } from './models/entities/note';
import { Following } from './models/entities/following'; import { Following } from './models/entities/following';
import { genId } from './misc/gen-id';
import { Poll } from './models/entities/poll'; import { Poll } from './models/entities/poll';
import { PollVote } from './models/entities/poll-vote'; import { PollVote } from './models/entities/poll-vote';
import { NoteFavorite } from './models/entities/note-favorite'; import { NoteFavorite } from './models/entities/note-favorite';
@@ -26,6 +25,7 @@ import { UserKeypair } from './models/entities/user-keypair';
import { extractPublic } from './crypto_key'; import { extractPublic } from './crypto_key';
import { Emoji } from './models/entities/emoji'; import { Emoji } from './models/entities/emoji';
import { toPuny } from './misc/convert-host'; import { toPuny } 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 u = (config as any).mongodb.user ? encodeURIComponent((config as any).mongodb.user) : null;
const p = (config as any).mongodb.pass ? encodeURIComponent((config as any).mongodb.pass) : null; const p = (config as any).mongodb.pass ? encodeURIComponent((config as any).mongodb.pass) : null;
@@ -35,6 +35,9 @@ const uri = `mongodb://${u && p ? `${u}:${p}@` : ''}${(config as any).mongodb.ho
const db = monk(uri); const db = monk(uri);
let mdb: mongo.Db; let mdb: mongo.Db;
const test = false;
const limit = 500;
const nativeDbConn = async (): Promise<mongo.Db> => { const nativeDbConn = async (): Promise<mongo.Db> => {
if (mdb) return mdb; if (mdb) return mdb;
@@ -70,6 +73,7 @@ const getDriveFileBucket = async (): Promise<mongo.GridFSBucket> => {
async function main() { async function main() {
await initDb(); await initDb();
const Users = getRepository(User); const Users = getRepository(User);
const UserProfiles = getRepository(UserProfile);
const DriveFiles = getRepository(DriveFile); const DriveFiles = getRepository(DriveFile);
const DriveFolders = getRepository(DriveFolder); const DriveFolders = getRepository(DriveFolder);
const Notes = getRepository(Note); const Notes = getRepository(Note);
@@ -90,27 +94,30 @@ async function main() {
usernameLower: user.username.toLowerCase(), usernameLower: user.username.toLowerCase(),
host: toPuny(user.host), host: toPuny(user.host),
token: generateUserToken(), token: generateUserToken(),
password: user.password, isAdmin: user.isAdmin || false,
isAdmin: user.isAdmin,
autoAcceptFollowed: true,
autoWatch: false,
name: user.name, name: user.name,
location: user.profile ? user.profile.location : null, followersCount: user.followersCount || 0,
birthday: user.profile ? user.profile.birthday : null, followingCount: user.followingCount || 0,
followersCount: user.followersCount, notesCount: user.notesCount || 0,
followingCount: user.followingCount, isBot: user.isBot || false,
notesCount: user.notesCount, isCat: user.isCat || false,
description: user.description, isVerified: user.isVerified || false,
isBot: user.isBot,
isCat: user.isCat,
isVerified: user.isVerified,
inbox: user.inbox, inbox: user.inbox,
sharedInbox: user.sharedInbox, sharedInbox: user.sharedInbox,
uri: user.uri, 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) { if (user.publicKey) {
await UserPublickeys.save({ await UserPublickeys.save({
id: genId(),
userId: user._id.toHexString(), userId: user._id.toHexString(),
keyId: user.publicKey.id, keyId: user.publicKey.id,
keyPem: user.publicKey.publicKeyPem keyPem: user.publicKey.publicKeyPem
@@ -118,7 +125,6 @@ async function main() {
} }
if (user.keypair) { if (user.keypair) {
await UserKeypairs.save({ await UserKeypairs.save({
id: genId(),
userId: user._id.toHexString(), userId: user._id.toHexString(),
publicKey: extractPublic(user.keypair), publicKey: extractPublic(user.keypair),
privateKey: user.keypair, privateKey: user.keypair,
@@ -129,7 +135,7 @@ async function main() {
async function migrateFollowing(following: any) { async function migrateFollowing(following: any) {
await Followings.save({ await Followings.save({
id: following._id.toHexString(), id: following._id.toHexString(),
createdAt: following.createdAt || new Date(), createdAt: new Date(),
followerId: following.followerId.toHexString(), followerId: following.followerId.toHexString(),
followeeId: following.followeeId.toHexString(), followeeId: following.followeeId.toHexString(),
@@ -156,6 +162,7 @@ async function main() {
const user = await _User.findOne({ const user = await _User.findOne({
_id: file.metadata.userId _id: file.metadata.userId
}); });
if (user == null) return;
if (file.metadata.storage && file.metadata.storage.key) { // when object storage if (file.metadata.storage && file.metadata.storage.key) { // when object storage
await DriveFiles.save({ await DriveFiles.save({
id: file._id.toHexString(), id: file._id.toHexString(),
@@ -165,7 +172,7 @@ async function main() {
md5: file.md5, md5: file.md5,
name: file.filename, name: file.filename,
type: file.contentType, type: file.contentType,
properties: file.metadata.properties, properties: file.metadata.properties || {},
size: file.length, size: file.length,
url: file.metadata.url, url: file.metadata.url,
uri: file.metadata.uri, uri: file.metadata.uri,
@@ -251,7 +258,6 @@ async function main() {
if (note.poll) { if (note.poll) {
await Polls.save({ await Polls.save({
id: genId(),
noteId: note._id.toHexString(), noteId: note._id.toHexString(),
choices: note.poll.choices.map((x: any) => x.text), choices: note.poll.choices.map((x: any) => x.text),
expiresAt: note.poll.expiresAt, expiresAt: note.poll.expiresAt,
@@ -297,13 +303,13 @@ async function main() {
const u = await _User.findOne({ const u = await _User.findOne({
_id: new mongo.ObjectId(user.id) _id: new mongo.ObjectId(user.id)
}); });
const avatar = await DriveFiles.findOne(u.avatarId.toHexString()); const avatar = u.avatarId ? await DriveFiles.findOne(u.avatarId.toHexString()) : null;
const banner = await DriveFiles.findOne(u.bannerId.toHexString()); const banner = u.bannerId ? await DriveFiles.findOne(u.bannerId.toHexString()) : null;
await Users.update(user.id, { await Users.update(user.id, {
avatarId: avatar.id, avatarId: avatar ? avatar.id : null,
bannerId: banner.id, bannerId: banner ? banner.id : null,
avatarUrl: avatar.url, avatarUrl: avatar ? avatar.url : null,
bannerUrl: banner.url bannerUrl: banner ? banner.url : null
}); });
} }
@@ -319,9 +325,14 @@ async function main() {
}); });
} }
const allUsersCount = await _User.count(); let allUsersCount = await _User.count({
deletedAt: { $exists: false }
});
if (test && allUsersCount > limit) allUsersCount = limit;
for (let i = 0; i < allUsersCount; i++) { for (let i = 0; i < allUsersCount; i++) {
const user = await _User.findOne({}, { const user = await _User.findOne({
deletedAt: { $exists: false }
}, {
skip: i skip: i
}); });
try { try {
@@ -333,7 +344,8 @@ async function main() {
} }
} }
const allFollowingsCount = await _Following.count(); let allFollowingsCount = await _Following.count();
if (test && allFollowingsCount > limit) allFollowingsCount = limit;
for (let i = 0; i < allFollowingsCount; i++) { for (let i = 0; i < allFollowingsCount; i++) {
const following = await _Following.findOne({}, { const following = await _Following.findOne({}, {
skip: i skip: i
@@ -347,7 +359,8 @@ async function main() {
} }
} }
const allDriveFoldersCount = await _DriveFolder.count(); let allDriveFoldersCount = await _DriveFolder.count();
if (test && allDriveFoldersCount > limit) allDriveFoldersCount = limit;
for (let i = 0; i < allDriveFoldersCount; i++) { for (let i = 0; i < allDriveFoldersCount; i++) {
const folder = await _DriveFolder.findOne({}, { const folder = await _DriveFolder.findOne({}, {
skip: i skip: i
@@ -361,9 +374,16 @@ async function main() {
} }
} }
const allDriveFilesCount = await _DriveFile.count(); let allDriveFilesCount = await _DriveFile.count({
'metadata._user.host': null,
'metadata.deletedAt': { $exists: false }
});
if (test && allDriveFilesCount > limit) allDriveFilesCount = limit;
for (let i = 0; i < allDriveFilesCount; i++) { for (let i = 0; i < allDriveFilesCount; i++) {
const file = await _DriveFile.findOne({}, { const file = await _DriveFile.findOne({
'metadata._user.host': null,
'metadata.deletedAt': { $exists: false }
}, {
skip: i skip: i
}); });
try { try {
@@ -375,12 +395,15 @@ async function main() {
} }
} }
const allNotesCount = await _Note.count({ let allNotesCount = await _Note.count({
'_user.host': null '_user.host': null,
'metadata.deletedAt': { $exists: false }
}); });
if (test && allNotesCount > limit) allNotesCount = limit;
for (let i = 0; i < allNotesCount; i++) { for (let i = 0; i < allNotesCount; i++) {
const note = await _Note.findOne({ const note = await _Note.findOne({
'_user.host': null '_user.host': null,
'metadata.deletedAt': { $exists: false }
}, { }, {
skip: i skip: i
}); });
@@ -393,7 +416,8 @@ async function main() {
} }
} }
const allPollVotesCount = await _PollVote.count(); let allPollVotesCount = await _PollVote.count();
if (test && allPollVotesCount > limit) allPollVotesCount = limit;
for (let i = 0; i < allPollVotesCount; i++) { for (let i = 0; i < allPollVotesCount; i++) {
const vote = await _PollVote.findOne({}, { const vote = await _PollVote.findOne({}, {
skip: i skip: i
@@ -407,7 +431,8 @@ async function main() {
} }
} }
const allNoteFavoritesCount = await _Favorite.count(); let allNoteFavoritesCount = await _Favorite.count();
if (test && allNoteFavoritesCount > limit) allNoteFavoritesCount = limit;
for (let i = 0; i < allNoteFavoritesCount; i++) { for (let i = 0; i < allNoteFavoritesCount; i++) {
const favorite = await _Favorite.findOne({}, { const favorite = await _Favorite.findOne({}, {
skip: i skip: i
@@ -421,7 +446,8 @@ async function main() {
} }
} }
const allNoteReactionsCount = await _NoteReaction.count(); let allNoteReactionsCount = await _NoteReaction.count();
if (test && allNoteReactionsCount > limit) allNoteReactionsCount = limit;
for (let i = 0; i < allNoteReactionsCount; i++) { for (let i = 0; i < allNoteReactionsCount; i++) {
const reaction = await _NoteReaction.findOne({}, { const reaction = await _NoteReaction.findOne({}, {
skip: i skip: i
@@ -435,7 +461,8 @@ async function main() {
} }
} }
const allActualUsersCount = await Users.count(); let allActualUsersCount = await Users.count();
if (test && allActualUsersCount > limit) allActualUsersCount = limit;
for (let i = 0; i < allActualUsersCount; i++) { for (let i = 0; i < allActualUsersCount; i++) {
const [user] = await Users.find({ const [user] = await Users.find({
take: 1, take: 1,

View File

@@ -1,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[]; public permission: string[];
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The callbackUrl of the App.' comment: 'The callbackUrl of the App.'
}) })
public callbackUrl: string | null; public callbackUrl: string | null;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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; 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', { @Column('integer', {
default: 0, default: 0,
comment: 'The count of followers.' comment: 'The count of followers.'
@@ -101,12 +89,6 @@ export class User {
@JoinColumn() @JoinColumn()
public banner: DriveFile | null; public banner: DriveFile | null;
@Column('varchar', {
length: 1024, nullable: true,
comment: 'The description (bio) of the User.'
})
public description: string | null;
@Index() @Index()
@Column('varchar', { @Column('varchar', {
length: 128, array: true, default: '{}' length: 128, array: true, default: '{}'
@@ -114,38 +96,12 @@ export class User {
public tags: string[]; public tags: string[];
@Column('varchar', { @Column('varchar', {
length: 128, nullable: true, length: 512, 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,
}) })
public avatarUrl: string | null; public avatarUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
}) })
public bannerUrl: string | null; public bannerUrl: string | null;
@@ -206,11 +162,6 @@ export class User {
}) })
public isVerified: boolean; public isVerified: boolean;
@Column('boolean', {
default: false,
})
public twoFactorEnabled: boolean;
@Column('varchar', { @Column('varchar', {
length: 128, array: true, default: '{}' length: 128, array: true, default: '{}'
}) })
@@ -224,36 +175,30 @@ export class User {
public host: string | null; public host: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The inbox of the User. It will be null if the origin of the user is local.' comment: 'The inbox URL of the User. It will be null if the origin of the user is local.'
}) })
public inbox: string | null; public inbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The sharedInbox of the User. It will be null if the origin of the user is local.' comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'
}) })
public sharedInbox: string | null; public sharedInbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The featured of the User. It will be null if the origin of the user is local.' comment: 'The featured URL of the User. It will be null if the origin of the user is local.'
}) })
public featured: string | null; public featured: string | null;
@Index() @Index()
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The URI of the User. It will be null if the origin of the user is local.' comment: 'The URI of the User. It will be null if the origin of the user is local.'
}) })
public uri: string | null; public uri: string | null;
@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 }) @Index({ unique: true })
@Column('char', { @Column('char', {
length: 16, nullable: true, unique: true, length: 16, nullable: true, unique: true,
@@ -261,31 +206,13 @@ export class User {
}) })
public token: string | null; public token: string | null;
@Column('jsonb', { constructor(data: Partial<User>) {
default: {}, if (data == null) return;
comment: 'The client-specific data of the User.'
})
public clientData: Record<string, any>;
@Column('boolean', { for (const [k, v] of Object.entries(data)) {
default: false, (this as any)[k] = v;
}) }
public autoWatch: boolean; }
@Column('boolean', {
default: false,
})
public autoAcceptFollowed: boolean;
@Column('boolean', {
default: false,
})
public alwaysMarkNsfw: boolean;
@Column('boolean', {
default: false,
})
public carefulBot: boolean;
} }
export interface ILocalUser extends User { export interface ILocalUser extends User {

View File

@@ -25,7 +25,6 @@ import { FollowRequestRepository } from './repositories/follow-request';
import { MutingRepository } from './repositories/muting'; import { MutingRepository } from './repositories/muting';
import { BlockingRepository } from './repositories/blocking'; import { BlockingRepository } from './repositories/blocking';
import { NoteReactionRepository } from './repositories/note-reaction'; import { NoteReactionRepository } from './repositories/note-reaction';
import { UserServiceLinking } from './entities/user-service-linking';
import { NotificationRepository } from './repositories/notification'; import { NotificationRepository } from './repositories/notification';
import { NoteFavoriteRepository } from './repositories/note-favorite'; import { NoteFavoriteRepository } from './repositories/note-favorite';
import { ReversiMatchingRepository } from './repositories/games/reversi/matching'; import { ReversiMatchingRepository } from './repositories/games/reversi/matching';
@@ -35,6 +34,7 @@ import { AppRepository } from './repositories/app';
import { FollowingRepository } from './repositories/following'; import { FollowingRepository } from './repositories/following';
import { AbuseUserReportRepository } from './repositories/abuse-user-report'; import { AbuseUserReportRepository } from './repositories/abuse-user-report';
import { AuthSessionRepository } from './repositories/auth-session'; import { AuthSessionRepository } from './repositories/auth-session';
import { UserProfile } from './entities/user-profile';
export const Apps = getCustomRepository(AppRepository); export const Apps = getCustomRepository(AppRepository);
export const Notes = getCustomRepository(NoteRepository); export const Notes = getCustomRepository(NoteRepository);
@@ -45,12 +45,12 @@ export const NoteUnreads = getRepository(NoteUnread);
export const Polls = getRepository(Poll); export const Polls = getRepository(Poll);
export const PollVotes = getRepository(PollVote); export const PollVotes = getRepository(PollVote);
export const Users = getCustomRepository(UserRepository); export const Users = getCustomRepository(UserRepository);
export const UserProfiles = getRepository(UserProfile);
export const UserKeypairs = getRepository(UserKeypair); export const UserKeypairs = getRepository(UserKeypair);
export const UserPublickeys = getRepository(UserPublickey); export const UserPublickeys = getRepository(UserPublickey);
export const UserLists = getCustomRepository(UserListRepository); export const UserLists = getCustomRepository(UserListRepository);
export const UserListJoinings = getRepository(UserListJoining); export const UserListJoinings = getRepository(UserListJoining);
export const UserNotePinings = getRepository(UserNotePining); export const UserNotePinings = getRepository(UserNotePining);
export const UserServiceLinkings = getRepository(UserServiceLinking);
export const Followings = getCustomRepository(FollowingRepository); export const Followings = getCustomRepository(FollowingRepository);
export const FollowRequests = getCustomRepository(FollowRequestRepository); export const FollowRequests = getCustomRepository(FollowRequestRepository);
export const Instances = getRepository(Instance); export const Instances = getRepository(Instance);

View File

@@ -3,6 +3,7 @@ import { DriveFile } from '../entities/drive-file';
import { Users, DriveFolders } from '..'; import { Users, DriveFolders } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
import { User } from '../entities/user'; import { User } from '../entities/user';
import { toPuny } from '../../misc/convert-host';
@EntityRepository(DriveFile) @EntityRepository(DriveFile)
export class DriveFileRepository extends Repository<DriveFile> { export class DriveFileRepository extends Repository<DriveFile> {
@@ -39,7 +40,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
public async clacDriveUsageOfHost(host: string): Promise<number> { public async clacDriveUsageOfHost(host: string): Promise<number> {
const { sum } = await this const { sum } = await this
.createQueryBuilder('file') .createQueryBuilder('file')
.where('file.userHost = :host', { host: host }) .where('file.userHost = :host', { host: toPuny(host) })
.select('SUM(file.size)', 'sum') .select('SUM(file.size)', 'sum')
.getRawOne(); .getRawOne();

View File

@@ -1,6 +1,6 @@
import { EntityRepository, Repository, In } from 'typeorm'; import { EntityRepository, Repository, In } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user'; import { User, ILocalUser, IRemoteUser } from '../entities/user';
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings } from '..'; import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles } from '..';
import rap from '@prezzemolo/rap'; import rap from '@prezzemolo/rap';
@EntityRepository(User) @EntityRepository(User)
@@ -80,13 +80,13 @@ export class UserRepository extends Repository<User> {
const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null; const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : []; const pins = opts.detail ? await UserNotePinings.find({ userId: user.id }) : [];
const profile = opts.detail ? await UserProfiles.findOne({ userId: user.id }) : null;
return await rap({ return await rap({
id: user.id, id: user.id,
name: user.name, name: user.name,
username: user.username, username: user.username,
host: user.host, host: user.host,
uri: user.uri,
avatarUrl: user.avatarUrl, avatarUrl: user.avatarUrl,
bannerUrl: user.bannerUrl, bannerUrl: user.bannerUrl,
avatarColor: user.avatarColor, avatarColor: user.avatarColor,
@@ -94,6 +94,7 @@ export class UserRepository extends Repository<User> {
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
isBot: user.isBot, isBot: user.isBot,
isCat: user.isCat, isCat: user.isCat,
isVerified: user.isVerified,
// カスタム絵文字添付 // カスタム絵文字添付
emojis: user.emojis.length > 0 ? Emojis.find({ emojis: user.emojis.length > 0 ? Emojis.find({
@@ -116,9 +117,12 @@ export class UserRepository extends Repository<User> {
} : {}), } : {}),
...(opts.detail ? { ...(opts.detail ? {
description: user.description, url: profile.url,
location: user.location, createdAt: user.createdAt,
birthday: user.birthday, updatedAt: user.updatedAt,
description: profile.description,
location: profile.location,
birthday: profile.birthday,
followersCount: user.followersCount, followersCount: user.followersCount,
followingCount: user.followingCount, followingCount: user.followingCount,
notesCount: user.notesCount, notesCount: user.notesCount,
@@ -131,9 +135,9 @@ export class UserRepository extends Repository<User> {
...(opts.detail && meId === user.id ? { ...(opts.detail && meId === user.id ? {
avatarId: user.avatarId, avatarId: user.avatarId,
bannerId: user.bannerId, bannerId: user.bannerId,
autoWatch: user.autoWatch, autoWatch: profile.autoWatch,
alwaysMarkNsfw: user.alwaysMarkNsfw, alwaysMarkNsfw: profile.alwaysMarkNsfw,
carefulBot: user.carefulBot, carefulBot: profile.carefulBot,
hasUnreadMessagingMessage: MessagingMessages.count({ hasUnreadMessagingMessage: MessagingMessages.count({
where: { where: {
recipientId: user.id, recipientId: user.id,
@@ -153,6 +157,12 @@ export class UserRepository extends Repository<User> {
}), }),
} : {}), } : {}),
...(opts.includeSecrets ? {
clientData: profile.clientData,
email: profile.email,
emailVerified: profile.emailVerified,
} : {}),
...(relation ? { ...(relation ? {
isFollowing: relation.isFollowing, isFollowing: relation.isFollowing,
isFollowed: relation.isFollowed, isFollowed: relation.isFollowed,

View File

@@ -4,7 +4,6 @@ import parseAcct from '../../misc/acct/parse';
import { IRemoteUser } from '../../models/entities/user'; import { IRemoteUser } from '../../models/entities/user';
import perform from '../../remote/activitypub/perform'; import perform from '../../remote/activitypub/perform';
import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person'; import { resolvePerson, updatePerson } from '../../remote/activitypub/models/person';
import { toUnicode } from 'punycode';
import { URL } from 'url'; import { URL } from 'url';
import { publishApLogStream } from '../../services/stream'; import { publishApLogStream } from '../../services/stream';
import Logger from '../../services/logger'; import Logger from '../../services/logger';
@@ -13,6 +12,8 @@ import { Instances, Users, UserPublickeys } from '../../models';
import { instanceChart } from '../../services/chart'; import { instanceChart } from '../../services/chart';
import { UserPublickey } from '../../models/entities/user-publickey'; import { UserPublickey } from '../../models/entities/user-publickey';
import fetchMeta from '../../misc/fetch-meta'; import fetchMeta from '../../misc/fetch-meta';
import { toPuny } from '../../misc/convert-host';
import { validActor } from '../../remote/activitypub/type';
const logger = new Logger('inbox'); const logger = new Logger('inbox');
@@ -33,7 +34,10 @@ export default async (job: Bull.Job): Promise<void> => {
let key: UserPublickey; let key: UserPublickey;
if (keyIdLower.startsWith('acct:')) { 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) { if (host === null) {
logger.warn(`request was made by local user: @${username}`); logger.warn(`request was made by local user: @${username}`);
return; return;
@@ -50,19 +54,22 @@ export default async (job: Bull.Job): Promise<void> => {
// ブロックしてたら中断 // ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.blockedHosts.includes(host.toLowerCase())) { if (meta.blockedHosts.includes(host)) {
logger.info(`Blocked request: ${host}`); logger.info(`Blocked request: ${host}`);
return; 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({ key = await UserPublickeys.findOne({
userId: user.id userId: user.id
}); });
} else { } else {
// アクティビティ内のホストの検証 // アクティビティ内のホストの検証
const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase()); const host = toPuny(new URL(signature.keyId).hostname);
try { try {
ValidateActivity(activity, host); ValidateActivity(activity, host);
} catch (e) { } catch (e) {
@@ -73,7 +80,7 @@ export default async (job: Bull.Job): Promise<void> => {
// ブロックしてたら中断 // ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.blockedHosts.includes(host.toLowerCase())) { if (meta.blockedHosts.includes(host)) {
logger.info(`Blocked request: ${host}`); logger.info(`Blocked request: ${host}`);
return; return;
} }
@@ -87,7 +94,7 @@ export default async (job: Bull.Job): Promise<void> => {
// Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了 // Update Person activityの場合は、ここで署名検証/更新処理まで実施して終了
if (activity.type === 'Update') { if (activity.type === 'Update') {
if (activity.object && activity.object.type === 'Person') { if (activity.object && validActor.includes(activity.object.type)) {
if (user == null) { if (user == null) {
logger.warn('Update activity received, but user not registed.'); logger.warn('Update activity received, but user not registed.');
} else if (!httpSignature.verifySignature(signature, key.keyPem)) { } else if (!httpSignature.verifySignature(signature, key.keyPem)) {
@@ -145,7 +152,7 @@ export default async (job: Bull.Job): Promise<void> => {
function ValidateActivity(activity: any, host: string) { function ValidateActivity(activity: any, host: string) {
// id (if exists) // id (if exists)
if (typeof activity.id === 'string') { 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) { if (host !== uriHost) {
const diag = activity.signature ? '. Has LD-Signature. Forwarded?' : ''; const diag = activity.signature ? '. Has LD-Signature. Forwarded?' : '';
throw new Error(`activity.id(${activity.id}) has different host(${host})${diag}`); 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) // actor (if exists)
if (typeof activity.actor === 'string') { 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'); 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) { if (activity.type === 'Create' && activity.object) {
// object.id (if exists) // object.id (if exists)
if (typeof activity.object.id === 'string') { 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'); if (host !== uriHost) throw new Error('activity.object.id has different host');
} }
// object.attributedTo (if exists) // object.attributedTo (if exists)
if (typeof activity.object.attributedTo === 'string') { 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'); if (host !== uriHost) throw new Error('activity.object.attributedTo has different host');
} }
} }

View File

@@ -8,14 +8,13 @@ import { resolveImage } from './image';
import { IRemoteUser, User } from '../../../models/entities/user'; import { IRemoteUser, User } from '../../../models/entities/user';
import { fromHtml } from '../../../mfm/fromHtml'; import { fromHtml } from '../../../mfm/fromHtml';
import { ITag, extractHashtags } from './tag'; import { ITag, extractHashtags } from './tag';
import { toUnicode } from 'punycode';
import { unique, concat, difference } from '../../../prelude/array'; import { unique, concat, difference } from '../../../prelude/array';
import { extractPollFromQuestion } from './question'; import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote'; import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger'; import { apLogger } from '../logger';
import { DriveFile } from '../../../models/entities/drive-file'; import { DriveFile } from '../../../models/entities/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update'; 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 { Notes, Emojis, Polls } from '../../../models';
import { Note } from '../../../models/entities/note'; import { Note } from '../../../models/entities/note';
import { IObject, INote } from '../type'; import { IObject, INote } from '../type';
@@ -246,8 +245,8 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
return await createNote(uri, resolver); return await createNote(uri, resolver);
} }
export async function extractEmojis(tags: ITag[], host_: string) { export async function extractEmojis(tags: ITag[], host: string) {
const host = toUnicode(host_.toLowerCase()); host = toPuny(host);
if (!tags) return []; if (!tags) return [];

View File

@@ -1,5 +1,4 @@
import * as promiseLimit from 'promise-limit'; import * as promiseLimit from 'promise-limit';
import { toUnicode } from 'punycode';
import config from '../../../config'; import config from '../../../config';
import Resolver from '../resolver'; import Resolver from '../resolver';
@@ -15,16 +14,18 @@ import { IIdentifier } from './identifier';
import { apLogger } from '../logger'; import { apLogger } from '../logger';
import { Note } from '../../../models/entities/note'; import { Note } from '../../../models/entities/note';
import { updateHashtag } from '../../../services/update-hashtag'; 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 { User, IRemoteUser } from '../../../models/entities/user';
import { Emoji } from '../../../models/entities/emoji'; import { Emoji } from '../../../models/entities/emoji';
import { UserNotePining } from '../../../models/entities/user-note-pinings'; import { UserNotePining } from '../../../models/entities/user-note-pinings';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
import { UserServiceLinking } from '../../../models/entities/user-service-linking';
import { instanceChart, usersChart } from '../../../services/chart'; import { instanceChart, usersChart } from '../../../services/chart';
import { UserPublickey } from '../../../models/entities/user-publickey'; import { UserPublickey } from '../../../models/entities/user-publickey';
import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
import { toPuny } from '../../../misc/convert-host'; import { toPuny } from '../../../misc/convert-host';
import { UserProfile } from '../../../models/entities/user-profile';
import { validActor } from '../../../remote/activitypub/type';
import { getConnection } from 'typeorm';
const logger = apLogger; const logger = apLogger;
/** /**
@@ -33,13 +34,13 @@ const logger = apLogger;
* @param uri Fetch target URI * @param uri Fetch target URI
*/ */
function validatePerson(x: any, uri: string) { function validatePerson(x: any, uri: string) {
const expectHost = toUnicode(new URL(uri).hostname.toLowerCase()); const expectHost = toPuny(new URL(uri).hostname);
if (x == null) { if (x == null) {
return new Error('invalid person: object is null'); return new Error('invalid person: object is null');
} }
if (x.type != 'Person' && x.type != 'Service') { if (!validActor.includes(x.type)) {
return new Error(`invalid person: object is not a person or service '${x.type}'`); return new Error(`invalid person: object is not a person or service '${x.type}'`);
} }
@@ -63,7 +64,7 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: id is not a 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) { if (idHost !== expectHost) {
return new Error('invalid person: id has different host'); return new Error('invalid person: id has different host');
} }
@@ -72,7 +73,7 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: publicKey.id is not a 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) { if (publicKeyIdHost !== expectHost) {
return new Error('invalid person: publicKey.id has different host'); return new Error('invalid person: publicKey.id has different host');
} }
@@ -127,7 +128,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const host = toPuny(new URL(object.id).hostname); const host = toPuny(new URL(object.id).hostname);
const { fields, services } = analyzeAttachments(person.attachment); const { fields } = analyzeAttachments(person.attachment);
const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()); const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase());
@@ -136,13 +137,14 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
// Create user // Create user
let user: IRemoteUser; let user: IRemoteUser;
try { try {
user = await Users.save({ // Start transaction
await getConnection().transaction(async transactionalEntityManager => {
user = await transactionalEntityManager.save(new User({
id: genId(), id: genId(),
avatarId: null, avatarId: null,
bannerId: null, bannerId: null,
createdAt: Date.parse(person.published) || new Date(), createdAt: new Date(),
lastFetchedAt: new Date(), lastFetchedAt: new Date(),
description: fromHtml(person.summary),
name: person.name, name: person.name,
isLocked: person.manuallyApprovesFollowers, isLocked: person.manuallyApprovesFollowers,
username: person.preferredUsername, username: person.preferredUsername,
@@ -151,15 +153,26 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
inbox: person.inbox, inbox: person.inbox,
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
featured: person.featured, featured: person.featured,
endpoints: person.endpoints,
uri: person.id, uri: person.id,
url: person.url,
fields,
...services,
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true isCat: (person as any).isCat === true
} as Partial<User>) as IRemoteUser; })) as IRemoteUser;
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: 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) { } catch (e) {
// duplicate key error // duplicate key error
if (isDuplicateKeyValueError(e)) { if (isDuplicateKeyValueError(e)) {
@@ -170,18 +183,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
throw e; 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 // Register host
registerOrFetchInstanceDoc(host).then(i => { registerOrFetchInstanceDoc(host).then(i => {
Instances.increment({ id: i.id }, 'usersCount', 1); Instances.increment({ id: i.id }, 'usersCount', 1);
@@ -206,8 +207,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
const avatarId = avatar ? avatar.id : null; const avatarId = avatar ? avatar.id : null;
const bannerId = banner ? banner.id : null; const bannerId = banner ? banner.id : null;
const avatarUrl = DriveFiles.getPublicUrl(avatar); const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar) : null;
const bannerUrl = DriveFiles.getPublicUrl(banner); const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null;
const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null; const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null; const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null;
@@ -348,7 +349,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
keyPem: person.publicKey.publicKeyPem keyPem: person.publicKey.publicKeyPem
}); });
await UserServiceLinkings.update({ userId: exist.id }, { await UserProfiles.update({ userId: exist.id }, {
twitterUserId: services.twitter.userId, twitterUserId: services.twitter.userId,
twitterScreenName: services.twitter.screenName, twitterScreenName: services.twitter.screenName,
githubId: services.github.id, githubId: services.github.id,

View File

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

View File

@@ -8,15 +8,15 @@ import { getEmojis } from './note';
import renderEmoji from './emoji'; import renderEmoji from './emoji';
import { IIdentifier } from '../models/identifier'; import { IIdentifier } from '../models/identifier';
import renderHashtag from './hashtag'; import renderHashtag from './hashtag';
import { DriveFiles, UserServiceLinkings, UserKeypairs } from '../../../models'; import { DriveFiles, UserProfiles, UserKeypairs } from '../../../models';
export async function renderPerson(user: ILocalUser) { export async function renderPerson(user: ILocalUser) {
const id = `${config.url}/users/${user.id}`; 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.avatarId),
DriveFiles.findOne(user.bannerId), DriveFiles.findOne(user.bannerId),
UserServiceLinkings.findOne({ userId: user.id }) UserProfiles.findOne({ userId: user.id })
]); ]);
const attachment: { const attachment: {
@@ -27,41 +27,41 @@ export async function renderPerson(user: ILocalUser) {
identifier?: IIdentifier identifier?: IIdentifier
}[] = []; }[] = [];
if (links.twitter) { if (profile.twitter) {
attachment.push({ attachment.push({
type: 'PropertyValue', type: 'PropertyValue',
name: 'Twitter', 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: { identifier: {
type: 'PropertyValue', type: 'PropertyValue',
name: 'misskey:authentication:twitter', name: 'misskey:authentication:twitter',
value: `${links.twitterUserId}@${links.twitterScreenName}` value: `${profile.twitterUserId}@${profile.twitterScreenName}`
} }
}); });
} }
if (links.github) { if (profile.github) {
attachment.push({ attachment.push({
type: 'PropertyValue', type: 'PropertyValue',
name: 'GitHub', 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: { identifier: {
type: 'PropertyValue', type: 'PropertyValue',
name: 'misskey:authentication:github', name: 'misskey:authentication:github',
value: `${links.githubId}@${links.githubLogin}` value: `${profile.githubId}@${profile.githubLogin}`
} }
}); });
} }
if (links.discord) { if (profile.discord) {
attachment.push({ attachment.push({
type: 'PropertyValue', type: 'PropertyValue',
name: 'Discord', 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: { identifier: {
type: 'PropertyValue', type: 'PropertyValue',
name: 'misskey:authentication:discord', 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}`, url: `${config.url}/@${user.username}`,
preferredUsername: user.username, preferredUsername: user.username,
name: user.name, name: user.name,
summary: toHtml(parse(user.description)), summary: toHtml(parse(profile.description)),
icon: user.avatarId && renderImage(avatar), icon: user.avatarId && renderImage(avatar),
image: user.bannerId && renderImage(banner), image: user.bannerId && renderImage(banner),
tag, tag,

View File

@@ -4,7 +4,6 @@ import { URL } from 'url';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { lookup, IRunOptions } from 'lookup-dns-cache'; import { lookup, IRunOptions } from 'lookup-dns-cache';
import * as promiseAny from 'promise-any'; import * as promiseAny from 'promise-any';
import { toUnicode } from 'punycode';
import config from '../../config'; import config from '../../config';
import { ILocalUser } from '../../models/entities/user'; import { ILocalUser } from '../../models/entities/user';
@@ -12,6 +11,7 @@ import { publishApLogStream } from '../../services/stream';
import { apLogger } from './logger'; import { apLogger } from './logger';
import { UserKeypairs } from '../../models'; import { UserKeypairs } from '../../models';
import fetchMeta from '../../misc/fetch-meta'; import fetchMeta from '../../misc/fetch-meta';
import { toPuny } from '../../misc/convert-host';
export const logger = apLogger.createSubLogger('deliver'); export const logger = apLogger.createSubLogger('deliver');
@@ -25,7 +25,7 @@ export default async (user: ILocalUser, url: string, object: any) => {
// ブロックしてたら中断 // ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.blockedHosts.includes(toUnicode(host))) return; if (meta.blockedHosts.includes(toPuny(host))) return;
const data = JSON.stringify(object); const data = JSON.stringify(object);

View File

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

View File

@@ -1,4 +1,3 @@
import { toUnicode, toASCII } from 'punycode';
import webFinger from './webfinger'; import webFinger from './webfinger';
import config from '../config'; import config from '../config';
import { createPerson, updatePerson } from './activitypub/models/person'; import { createPerson, updatePerson } from './activitypub/models/person';
@@ -7,31 +6,27 @@ import { remoteLogger } from './logger';
import chalk from 'chalk'; import chalk from 'chalk';
import { User, IRemoteUser } from '../models/entities/user'; import { User, IRemoteUser } from '../models/entities/user';
import { Users } from '../models'; import { Users } from '../models';
import { toPuny } from '../misc/convert-host';
const logger = remoteLogger.createSubLogger('resolve-user'); 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(); const usernameLower = username.toLowerCase();
host = toPuny(host);
if (_host == null) { if (host == null) {
logger.info(`return local user: ${usernameLower}`); logger.info(`return local user: ${usernameLower}`);
return await Users.findOne({ usernameLower, host: null }); return await Users.findOne({ usernameLower, host: null });
} }
const configHostAscii = toASCII(config.host).toLowerCase(); if (config.host == host) {
const configHost = toUnicode(configHostAscii);
const hostAscii = toASCII(_host).toLowerCase();
const host = toUnicode(hostAscii);
if (configHost == host) {
logger.info(`return local user: ${usernameLower}`); logger.info(`return local user: ${usernameLower}`);
return await Users.findOne({ usernameLower, host: null }); return await Users.findOne({ usernameLower, host: null });
} }
const user = await Users.findOne({ usernameLower, host }, option); const user = await Users.findOne({ usernameLower, host }, option);
const acctLower = `${usernameLower}@${hostAscii}`; const acctLower = `${usernameLower}@${host}`;
if (user == null) { if (user == null) {
const self = await resolveSelf(acctLower); const self = await resolveSelf(acctLower);
@@ -51,7 +46,7 @@ export async function resolveUser(username: string, _host: string, option?: any,
// validate uri // validate uri
const uri = new URL(self.href); const uri = new URL(self.href);
if (uri.hostname !== hostAscii) { if (uri.hostname !== host) {
throw new Error(`Invalied uri`); throw new Error(`Invalied uri`);
} }
@@ -78,8 +73,8 @@ export async function resolveUser(username: string, _host: string, option?: any,
async function resolveSelf(acctLower: string) { async function resolveSelf(acctLower: string) {
logger.info(`WebFinger for ${chalk.yellow(acctLower)}`); logger.info(`WebFinger for ${chalk.yellow(acctLower)}`);
const finger = await webFinger(acctLower).catch(e => { const finger = await webFinger(acctLower).catch(e => {
logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${e.message} (${e.status})`); logger.error(`Failed to WebFinger for ${chalk.yellow(acctLower)}: ${ e.statusCode || e.message }`);
throw e; throw new Error(`Failed to WebFinger for ${acctLower}: ${ e.statusCode || e.message }`);
}); });
const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
if (!self) { if (!self) {

View File

@@ -1,6 +1,7 @@
import { WebFinger } from 'webfinger.js'; import config from '../config';
import * as request from 'request-promise-native';
const webFinger = new WebFinger({ }); import { URL } from 'url';
import { query as urlQuery } from '../prelude/url';
type ILink = { type ILink = {
href: string; href: string;
@@ -12,12 +13,33 @@ type IWebFinger = {
subject: string; subject: string;
}; };
export default async function resolve(query: any): Promise<IWebFinger> { export default async function(query: string): Promise<IWebFinger> {
return await new Promise((res, rej) => webFinger.lookup(query, (error: Error | string, result: any) => { const url = genUrl(query);
if (error) {
return rej(error); 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
});
} }
res(result.object); function genUrl(query: string) {
})) as IWebFinger; if (query.match(/^https?:\/\//)) {
const u = new URL(query);
return `${u.protocol}//${u.hostname}/.well-known/webfinger?` + urlQuery({ resource: query });
}
const m = query.match(/^([^@]+)@(.*)/);
if (m) {
const hostname = m[2];
return `https://${hostname}/.well-known/webfinger?` + urlQuery({ resource: `acct:${query}` });
}
throw new Error(`Invalied query (${query})`);
} }

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}`, { apiLogger.error(`Internal error occurred in ${ep.name}`, {
ep: ep.name, ep: ep.name,
ps: data, ps: data,
e: e e: {
message: e.message,
code: e.name,
stack: e.stack
}
}); });
throw new ApiError(null, { throw new ApiError(null, {
e: { e: {

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,6 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../../../define'; import define from '../../../define';
import { Emojis } from '../../../../../models'; import { Emojis } from '../../../../../models';
import { toPuny } from '../../../../../misc/convert-host';
export const meta = { export const meta = {
desc: { desc: {
@@ -22,7 +23,7 @@ export const meta = {
export default define(meta, async (ps) => { export default define(meta, async (ps) => {
const emojis = await Emojis.find({ const emojis = await Emojis.find({
host: ps.host host: toPuny(ps.host)
}); });
return emojis.map(e => ({ return emojis.map(e => ({

View File

@@ -1,6 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../../../define'; import define from '../../../define';
import { Instances } from '../../../../../models'; import { Instances } from '../../../../../models';
import { toPuny } from '../../../../../misc/convert-host';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@@ -20,13 +21,13 @@ export const meta = {
}; };
export default define(meta, async (ps, me) => { 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) { if (instance == null) {
throw new Error('instance not found'); throw new Error('instance not found');
} }
Instances.update({ host: ps.host }, { Instances.update({ host: toPuny(ps.host) }, {
isMarkedAsClosed: ps.isClosed isMarkedAsClosed: ps.isClosed
}); });
}); });

View File

@@ -3,7 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import * as bcrypt from 'bcryptjs'; import * as bcrypt from 'bcryptjs';
import rndstr from 'rndstr'; import rndstr from 'rndstr';
import { Users } from '../../../../models'; import { Users, UserProfiles } from '../../../../models';
export const meta = { export const meta = {
desc: { desc: {
@@ -42,7 +42,9 @@ export default define(meta, async (ps) => {
// Generate hash of password // Generate hash of password
const hash = bcrypt.hashSync(passwd); const hash = bcrypt.hashSync(passwd);
await Users.update(user.id, { await UserProfiles.update({
userId: user.id
}, {
password: hash password: hash
}); });

View File

@@ -10,6 +10,7 @@ import { Users, Notes } from '../../../../models';
import { Note } from '../../../../models/entities/note'; import { Note } from '../../../../models/entities/note';
import { User } from '../../../../models/entities/user'; import { User } from '../../../../models/entities/user';
import fetchMeta from '../../../../misc/fetch-meta'; import fetchMeta from '../../../../misc/fetch-meta';
import { validActor } from '../../../../remote/activitypub/type';
export const meta = { export const meta = {
tags: ['federation'], 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); const user = await createPerson(object.id);
return { return {
type: 'User', type: 'User',

View File

@@ -1,6 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import define from '../../define'; import define from '../../define';
import { Instances } from '../../../../models'; import { Instances } from '../../../../models';
import { toPuny } from '../../../../misc/convert-host';
export const meta = { export const meta = {
tags: ['federation'], tags: ['federation'],
@@ -16,7 +17,7 @@ export const meta = {
export default define(meta, async (ps, me) => { export default define(meta, async (ps, me) => {
const instance = await Instances const instance = await Instances
.findOne({ host: ps.host }); .findOne({ host: toPuny(ps.host) });
return instance; return instance;
}); });

View File

@@ -1,7 +1,7 @@
import $ from 'cafy'; import $ from 'cafy';
import * as speakeasy from 'speakeasy'; import * as speakeasy from 'speakeasy';
import define from '../../../define'; import define from '../../../define';
import { Users } from '../../../../../models'; import { UserProfiles } from '../../../../../models';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@@ -16,24 +16,26 @@ export const meta = {
}; };
export default define(meta, async (ps, user) => { 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('二段階認証の設定が開始されていません'); throw new Error('二段階認証の設定が開始されていません');
} }
const verified = (speakeasy as any).totp.verify({ const verified = (speakeasy as any).totp.verify({
secret: user.twoFactorTempSecret, secret: profile.twoFactorTempSecret,
encoding: 'base32', encoding: 'base32',
token: _token token: token
}); });
if (!verified) { if (!verified) {
throw new Error('not verified'); throw new Error('not verified');
} }
await Users.update(user.id, { await UserProfiles.update({ userId: user.id }, {
twoFactorSecret: user.twoFactorTempSecret, twoFactorSecret: profile.twoFactorTempSecret,
twoFactorEnabled: true twoFactorEnabled: true
}); });
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,9 @@ import extractHashtags from '../../../../misc/extract-hashtags';
import * as langmap from 'langmap'; import * as langmap from 'langmap';
import { updateHashtag } from '../../../../services/update-hashtag'; import { updateHashtag } from '../../../../services/update-hashtag';
import { ApiError } from '../../error'; 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 = { export const meta = {
desc: { desc: {
@@ -154,22 +156,23 @@ export const meta = {
export default define(meta, async (ps, user, app) => { export default define(meta, async (ps, user, app) => {
const isSecure = user != null && app == null; 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.name !== undefined) updates.name = ps.name;
if (ps.description !== undefined) updates.description = ps.description; if (ps.description !== undefined) profile.description = ps.description;
if (ps.lang !== undefined) updates.lang = ps.lang; //if (ps.lang !== undefined) updates.lang = ps.lang;
if (ps.location !== undefined) updates.location = ps.location; if (ps.location !== undefined) profile.location = ps.location;
if (ps.birthday !== undefined) updates.birthday = ps.birthday; if (ps.birthday !== undefined) profile.birthday = ps.birthday;
if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId;
if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId;
if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot; if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot == 'boolean') updates.carefulBot = ps.carefulBot; if (typeof ps.carefulBot == 'boolean') profile.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed == 'boolean') updates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.autoAcceptFollowed == 'boolean') profile.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat; if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat;
if (typeof ps.autoWatch == 'boolean') updates.autoWatch = ps.autoWatch; if (typeof ps.autoWatch == 'boolean') profile.autoWatch = ps.autoWatch;
if (typeof ps.alwaysMarkNsfw == 'boolean') updates.alwaysMarkNsfw = ps.alwaysMarkNsfw; if (typeof ps.alwaysMarkNsfw == 'boolean') profile.alwaysMarkNsfw = ps.alwaysMarkNsfw;
if (ps.avatarId) { if (ps.avatarId) {
const avatar = await DriveFiles.findOne(ps.avatarId); const avatar = await DriveFiles.findOne(ps.avatarId);
@@ -206,8 +209,8 @@ export default define(meta, async (ps, user, app) => {
emojis = emojis.concat(extractEmojis(tokens)); emojis = emojis.concat(extractEmojis(tokens));
} }
if (updates.description != null) { if (profile.description != null) {
const tokens = parse(updates.description); const tokens = parse(profile.description);
emojis = emojis.concat(extractEmojis(tokens)); emojis = emojis.concat(extractEmojis(tokens));
tags = extractHashtags(tokens).map(tag => tag.toLowerCase()); 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); for (const tag of user.tags.filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
//#endregion //#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, { const iObj = await Users.pack(user.id, user, {
detail: true, detail: true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,13 +5,14 @@ import generateUserToken from '../common/generate-native-user-token';
import config from '../../../config'; import config from '../../../config';
import fetchMeta from '../../../misc/fetch-meta'; import fetchMeta from '../../../misc/fetch-meta';
import * as recaptcha from 'recaptcha-promise'; import * as recaptcha from 'recaptcha-promise';
import { Users, RegistrationTickets, UserServiceLinkings, UserKeypairs } from '../../../models'; import { Users, RegistrationTickets } from '../../../models';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
import { usersChart } from '../../../services/chart'; import { usersChart } from '../../../services/chart';
import { UserServiceLinking } from '../../../models/entities/user-service-linking';
import { User } from '../../../models/entities/user'; import { User } from '../../../models/entities/user';
import { UserKeypair } from '../../../models/entities/user-keypair'; import { UserKeypair } from '../../../models/entities/user-keypair';
import { toPuny } from '../../../misc/convert-host'; import { toPuny } from '../../../misc/convert-host';
import { UserProfile } from '../../../models/entities/user-profile';
import { getConnection } from 'typeorm';
export default async (ctx: Koa.BaseContext) => { export default async (ctx: Koa.BaseContext) => {
const body = ctx.request.body as any; const body = ctx.request.body as any;
@@ -99,30 +100,33 @@ export default async (ctx: Koa.BaseContext) => {
e ? j(e) : s([publicKey, privateKey]) e ? j(e) : s([publicKey, privateKey])
)); ));
const account = await Users.save({ let account: User;
// Start transaction
await getConnection().transaction(async transactionalEntityManager => {
account = await transactionalEntityManager.save(new User({
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
username: username, username: username,
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host: toPuny(host), host: toPuny(host),
token: secret, token: secret,
password: hash,
isAdmin: config.autoAdmin && usersCount === 0, isAdmin: config.autoAdmin && usersCount === 0,
autoAcceptFollowed: true, }));
autoWatch: false
} as User);
await UserKeypairs.save({ await transactionalEntityManager.save(new UserKeypair({
id: genId(),
publicKey: keyPair[0], publicKey: keyPair[0],
privateKey: keyPair[1], privateKey: keyPair[1],
userId: account.id userId: account.id
} as UserKeypair); }));
await UserServiceLinkings.save({ await transactionalEntityManager.save(new UserProfile({
id: genId(), userId: account.id,
userId: account.id autoAcceptFollowed: true,
} as UserServiceLinking); autoWatch: false,
password: hash,
}));
});
usersChart.update(account, true); usersChart.update(account, true);

View File

@@ -8,7 +8,7 @@ import redis from '../../../db/redis';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import signin from '../common/signin'; import signin from '../common/signin';
import fetchMeta from '../../../misc/fetch-meta'; import fetchMeta from '../../../misc/fetch-meta';
import { Users, UserServiceLinkings } from '../../../models'; import { Users, UserProfiles } from '../../../models';
import { ILocalUser } from '../../../models/entities/user'; import { ILocalUser } from '../../../models/entities/user';
function getUserToken(ctx: Koa.BaseContext) { function getUserToken(ctx: Koa.BaseContext) {
@@ -45,7 +45,7 @@ router.get('/disconnect/discord', async ctx => {
token: userToken token: userToken
}); });
await UserServiceLinkings.update({ await UserProfiles.update({
userId: user.id userId: user.id
}, { }, {
discord: false, discord: false,
@@ -202,7 +202,7 @@ router.get('/dc/cb', async ctx => {
return; return;
} }
const link = await UserServiceLinkings.createQueryBuilder() const profile = await UserProfiles.createQueryBuilder()
.where('discord @> :discord', { .where('discord @> :discord', {
discord: { discord: {
id: id, id: id,
@@ -211,12 +211,12 @@ router.get('/dc/cb', async ctx => {
.andWhere('userHost IS NULL') .andWhere('userHost IS NULL')
.getOne(); .getOne();
if (link == null) { if (profile == null) {
ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`); ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`);
return; return;
} }
await UserServiceLinkings.update(link.id, { await UserProfiles.update({ userId: profile.userId }, {
discord: true, discord: true,
discordAccessToken: accessToken, discordAccessToken: accessToken,
discordRefreshToken: refreshToken, discordRefreshToken: refreshToken,
@@ -225,7 +225,7 @@ router.get('/dc/cb', async ctx => {
discordDiscriminator: discriminator discordDiscriminator: discriminator
}); });
signin(ctx, await Users.findOne(link.userId) as ILocalUser, true); signin(ctx, await Users.findOne(profile.userId) as ILocalUser, true);
} else { } else {
const code = ctx.query.code; const code = ctx.query.code;
@@ -289,7 +289,7 @@ router.get('/dc/cb', async ctx => {
token: userToken token: userToken
}); });
await UserServiceLinkings.update({ userId: user.id }, { await UserProfiles.update({ userId: user.id }, {
discord: true, discord: true,
discordAccessToken: accessToken, discordAccessToken: accessToken,
discordRefreshToken: refreshToken, discordRefreshToken: refreshToken,

View File

@@ -8,7 +8,7 @@ import redis from '../../../db/redis';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import signin from '../common/signin'; import signin from '../common/signin';
import fetchMeta from '../../../misc/fetch-meta'; import fetchMeta from '../../../misc/fetch-meta';
import { Users, UserServiceLinkings } from '../../../models'; import { Users, UserProfiles } from '../../../models';
import { ILocalUser } from '../../../models/entities/user'; import { ILocalUser } from '../../../models/entities/user';
function getUserToken(ctx: Koa.BaseContext) { function getUserToken(ctx: Koa.BaseContext) {
@@ -45,7 +45,7 @@ router.get('/disconnect/github', async ctx => {
token: userToken token: userToken
}); });
await UserServiceLinkings.update({ await UserProfiles.update({
userId: user.id userId: user.id
}, { }, {
github: false, github: false,
@@ -191,7 +191,7 @@ router.get('/gh/cb', async ctx => {
return; return;
} }
const link = await UserServiceLinkings.createQueryBuilder() const link = await UserProfiles.createQueryBuilder()
.where('github @> :github', { .where('github @> :github', {
github: { github: {
id: id, id: id,
@@ -263,7 +263,7 @@ router.get('/gh/cb', async ctx => {
token: userToken token: userToken
}); });
await UserServiceLinkings.update({ userId: user.id }, { await UserProfiles.update({ userId: user.id }, {
github: true, github: true,
githubAccessToken: accessToken, githubAccessToken: accessToken,
githubId: id, githubId: id,

View File

@@ -7,7 +7,7 @@ import { publishMainStream } from '../../../services/stream';
import config from '../../../config'; import config from '../../../config';
import signin from '../common/signin'; import signin from '../common/signin';
import fetchMeta from '../../../misc/fetch-meta'; import fetchMeta from '../../../misc/fetch-meta';
import { Users, UserServiceLinkings } from '../../../models'; import { Users, UserProfiles } from '../../../models';
import { ILocalUser } from '../../../models/entities/user'; import { ILocalUser } from '../../../models/entities/user';
function getUserToken(ctx: Koa.BaseContext) { function getUserToken(ctx: Koa.BaseContext) {
@@ -44,7 +44,7 @@ router.get('/disconnect/twitter', async ctx => {
token: userToken token: userToken
}); });
await UserServiceLinkings.update({ await UserProfiles.update({
userId: user.id userId: user.id
}, { }, {
twitter: false, twitter: false,
@@ -139,7 +139,7 @@ router.get('/tw/cb', async ctx => {
const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier); const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
const link = await UserServiceLinkings.createQueryBuilder() const link = await UserProfiles.createQueryBuilder()
.where('twitter @> :twitter', { .where('twitter @> :twitter', {
twitter: { twitter: {
userId: result.userId, userId: result.userId,
@@ -177,7 +177,7 @@ router.get('/tw/cb', async ctx => {
token: userToken token: userToken
}); });
await UserServiceLinkings.update({ userId: user.id }, { await UserProfiles.update({ userId: user.id }, {
twitter: true, twitter: true,
twitterAccessToken: result.accessToken, twitterAccessToken: result.accessToken,
twitterAccessTokenSecret: result.accessTokenSecret, twitterAccessTokenSecret: result.accessTokenSecret,

View File

@@ -23,7 +23,7 @@ import apiServer from './api';
import { sum } from '../prelude/array'; import { sum } from '../prelude/array';
import Logger from '../services/logger'; import Logger from '../services/logger';
import { program } from '../argv'; import { program } from '../argv';
import { Users } from '../models'; import { UserProfiles } from '../models';
import { networkChart } from '../services/chart'; import { networkChart } from '../services/chart';
export const serverLogger = new Logger('server', 'gray', false); export const serverLogger = new Logger('server', 'gray', false);
@@ -73,15 +73,15 @@ router.use(nodeinfo.routes());
router.use(wellKnown.routes()); router.use(wellKnown.routes());
router.get('/verify-email/:code', async ctx => { router.get('/verify-email/:code', async ctx => {
const user = await Users.findOne({ const profile = await UserProfiles.findOne({
emailVerifyCode: ctx.params.code emailVerifyCode: ctx.params.code
}); });
if (user != null) { if (profile != null) {
ctx.body = 'Verify succeeded!'; ctx.body = 'Verify succeeded!';
ctx.status = 200; ctx.status = 200;
Users.update(user.id, { UserProfiles.update({ userId: profile.userId }, {
emailVerified: true, emailVerified: true,
emailVerifyCode: null emailVerifyCode: null
}); });

View File

@@ -1,7 +1,7 @@
import { Feed } from 'feed'; import { Feed } from 'feed';
import config from '../../config'; import config from '../../config';
import { User } from '../../models/entities/user'; import { User } from '../../models/entities/user';
import { Notes, DriveFiles } from '../../models'; import { Notes, DriveFiles, UserProfiles } from '../../models';
import { In } from 'typeorm'; import { In } from 'typeorm';
export default async function(user: User) { export default async function(user: User) {
@@ -10,6 +10,8 @@ export default async function(user: User) {
name: user.name || user.username name: user.name || user.username
}; };
const profile = await UserProfiles.findOne({ userId: user.id });
const notes = await Notes.find({ const notes = await Notes.find({
where: { where: {
userId: user.id, userId: user.id,
@@ -25,7 +27,7 @@ export default async function(user: User) {
title: `${author.name} (@${user.username}@${config.host})`, title: `${author.name} (@${user.username}@${config.host})`,
updated: notes[0].createdAt, updated: notes[0].createdAt,
generator: 'Misskey', generator: 'Misskey',
description: `${user.notesCount} Notes, ${user.followingCount} Following, ${user.followersCount} Followers${user.description ? ` · ${user.description}` : ''}`, description: `${user.notesCount} Notes, ${user.followingCount} Following, ${user.followersCount} Followers${profile.description ? ` · ${profile.description}` : ''}`,
link: author.link, link: author.link,
image: user.avatarUrl, image: user.avatarUrl,
feedLinks: { feedLinks: {

View File

@@ -16,7 +16,7 @@ import fetchMeta from '../../misc/fetch-meta';
import * as pkg from '../../../package.json'; import * as pkg from '../../../package.json';
import { genOpenapiSpec } from '../api/openapi/gen-spec'; import { genOpenapiSpec } from '../api/openapi/gen-spec';
import config from '../../config'; import config from '../../config';
import { Users, Notes, Emojis } from '../../models'; import { Users, Notes, Emojis, UserProfiles } from '../../models';
import parseAcct from '../../misc/acct/parse'; import parseAcct from '../../misc/acct/parse';
import getNoteSummary from '../../misc/get-note-summary'; import getNoteSummary from '../../misc/get-note-summary';
@@ -149,11 +149,14 @@ router.get('/@:user', async (ctx, next) => {
usernameLower: username.toLowerCase(), usernameLower: username.toLowerCase(),
host host
}); });
const profile = await UserProfiles.findOne({
userId: user.id
});
if (user != null) { if (user != null) {
const meta = await fetchMeta(); const meta = await fetchMeta();
await ctx.render('user', { await ctx.render('user', {
user, user, profile,
instanceName: meta.name || 'Misskey' instanceName: meta.name || 'Misskey'
}); });
ctx.set('Cache-Control', 'public, max-age=180'); ctx.set('Cache-Control', 'public, max-age=180');

View File

@@ -9,12 +9,12 @@ block title
= `${title} | ${instanceName}` = `${title} | ${instanceName}`
block desc block desc
meta(name='description' content= user.description) meta(name='description' content= profile.description)
block og block og
meta(property='og:type' content='blog') meta(property='og:type' content='blog')
meta(property='og:title' content= title) meta(property='og:title' content= title)
meta(property='og:description' content= user.description) meta(property='og:description' content= profile.description)
meta(property='og:url' content= url) meta(property='og:url' content= url)
meta(property='og:image' content= img) meta(property='og:image' content= img)
@@ -24,12 +24,12 @@ block meta
meta(name='twitter:card' content='summary') meta(name='twitter:card' content='summary')
if user.twitter if profile.twitter
meta(name='twitter:creator' content=`@${user.twitter.screenName}`) meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
if !user.host if !user.host
link(rel='alternate' href=`${config.url}/users/${user._id}` type='application/activity+json') link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
if user.uri if user.uri
link(rel='alternate' href=user.uri type='application/activity+json') link(rel='alternate' href=user.uri type='application/activity+json')
if user.url if profile.url
link(rel='alternate' href=user.url type='text/html') link(rel='alternate' href=profile.url type='text/html')

View File

@@ -5,6 +5,7 @@ import { DriveFiles, Followings, Users, Notes } from '../../../../models';
import { DriveFile } from '../../../../models/entities/drive-file'; import { DriveFile } from '../../../../models/entities/drive-file';
import { name, schema } from '../schemas/instance'; import { name, schema } from '../schemas/instance';
import { Note } from '../../../../models/entities/note'; import { Note } from '../../../../models/entities/note';
import { toPuny } from '../../../../misc/convert-host';
type InstanceLog = SchemaType<typeof schema>; type InstanceLog = SchemaType<typeof schema>;
@@ -79,7 +80,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
requests: { requests: {
received: 1 received: 1
} }
}, host); }, toPuny(host));
} }
@autobind @autobind
@@ -94,7 +95,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
await this.inc({ await this.inc({
requests: update requests: update
}, host); }, toPuny(host));
} }
@autobind @autobind
@@ -104,7 +105,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
total: 1, total: 1,
inc: 1 inc: 1
} }
}, host); }, toPuny(host));
} }
@autobind @autobind
@@ -126,7 +127,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
dec: isAdditional ? 0 : 1, dec: isAdditional ? 0 : 1,
diffs: diffs diffs: diffs
} }
}, host); }, toPuny(host));
} }
@autobind @autobind
@@ -137,7 +138,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
inc: isAdditional ? 1 : 0, inc: isAdditional ? 1 : 0,
dec: isAdditional ? 0 : 1, dec: isAdditional ? 0 : 1,
} }
}, host); }, toPuny(host));
} }
@autobind @autobind
@@ -148,7 +149,7 @@ export default class InstanceChart extends Chart<InstanceLog> {
inc: isAdditional ? 1 : 0, inc: isAdditional ? 1 : 0,
dec: isAdditional ? 0 : 1, dec: isAdditional ? 0 : 1,
} }
}, host); }, toPuny(host));
} }
@autobind @autobind

View File

@@ -15,7 +15,7 @@ import { driveLogger } from './logger';
import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor'; import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor';
import { contentDisposition } from '../../misc/content-disposition'; import { contentDisposition } from '../../misc/content-disposition';
import { detectMine } from '../../misc/detect-mine'; import { detectMine } from '../../misc/detect-mine';
import { DriveFiles, DriveFolders, Users, Instances } from '../../models'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models';
import { InternalStorage } from './internal-storage'; import { InternalStorage } from './internal-storage';
import { DriveFile } from '../../models/entities/drive-file'; import { DriveFile } from '../../models/entities/drive-file';
import { IRemoteUser, User } from '../../models/entities/user'; import { IRemoteUser, User } from '../../models/entities/user';
@@ -365,6 +365,8 @@ export default async function(
propPromises = [calcWh(), calcAvg()]; propPromises = [calcWh(), calcAvg()];
} }
const profile = await UserProfiles.findOne({ userId: user.id });
const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]); const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]);
let file = new DriveFile(); let file = new DriveFile();
@@ -376,7 +378,7 @@ export default async function(
file.comment = comment; file.comment = comment;
file.properties = properties; file.properties = properties;
file.isLink = isLink; file.isLink = isLink;
file.isSensitive = Users.isLocalUser(user) && user.alwaysMarkNsfw ? true : file.isSensitive = Users.isLocalUser(user) && profile.alwaysMarkNsfw ? true :
(sensitive !== null && sensitive !== undefined) (sensitive !== null && sensitive !== undefined)
? sensitive ? sensitive
: false; : false;

View File

@@ -9,7 +9,7 @@ import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
import Logger from '../logger'; import Logger from '../logger';
import { IdentifiableError } from '../../misc/identifiable-error'; import { IdentifiableError } from '../../misc/identifiable-error';
import { User } from '../../models/entities/user'; import { User } from '../../models/entities/user';
import { Followings, Users, FollowRequests, Blockings, Instances } from '../../models'; import { Followings, Users, FollowRequests, Blockings, Instances, UserProfiles } from '../../models';
import { instanceChart, perUserFollowingChart } from '../chart'; import { instanceChart, perUserFollowingChart } from '../chart';
import { genId } from '../../misc/gen-id'; import { genId } from '../../misc/gen-id';
import { createNotification } from '../create-notification'; import { createNotification } from '../create-notification';
@@ -115,11 +115,13 @@ export default async function(follower: User, followee: User, requestId?: string
if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked'); if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
} }
const followeeProfile = await UserProfiles.findOne({ userId: followee.id });
// フォロー対象が鍵アカウントである or // フォロー対象が鍵アカウントである or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである // フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく // 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if (followee.isLocked || (followee.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) { if (followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (Users.isLocalUser(follower) && Users.isRemoteUser(followee))) {
let autoAccept = false; let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー // 鍵アカウントであっても、既にフォローされていた場合はスルー
@@ -132,7 +134,7 @@ export default async function(follower: User, followee: User, requestId?: string
} }
// フォローしているユーザーは自動承認オプション // フォローしているユーザーは自動承認オプション
if (!autoAccept && (Users.isLocalUser(followee) && followee.autoAcceptFollowed)) { if (!autoAccept && (Users.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
const followed = await Followings.findOne({ const followed = await Followings.findOne({
followerId: followee.id, followerId: followee.id,
followeeId: follower.id followeeId: follower.id

View File

@@ -17,10 +17,10 @@ import extractMentions from '../../misc/extract-mentions';
import extractEmojis from '../../misc/extract-emojis'; import extractEmojis from '../../misc/extract-emojis';
import extractHashtags from '../../misc/extract-hashtags'; import extractHashtags from '../../misc/extract-hashtags';
import { Note } from '../../models/entities/note'; import { Note } from '../../models/entities/note';
import { Mutings, Users, NoteWatchings, Followings, Notes, Instances, Polls } from '../../models'; import { Mutings, Users, NoteWatchings, Followings, Notes, Instances, UserProfiles } from '../../models';
import { DriveFile } from '../../models/entities/drive-file'; import { DriveFile } from '../../models/entities/drive-file';
import { App } from '../../models/entities/app'; import { App } from '../../models/entities/app';
import { Not } from 'typeorm'; import { Not, getConnection } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../../models/entities/user'; import { User, ILocalUser, IRemoteUser } from '../../models/entities/user';
import { genId } from '../../misc/gen-id'; import { genId } from '../../misc/gen-id';
import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '../chart'; import { notesChart, perUserNotesChart, activeUsersChart, instanceChart } from '../chart';
@@ -256,13 +256,15 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity); deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity);
} }
const profile = await UserProfiles.findOne({ userId: user.id });
// If has in reply to note // If has in reply to note
if (data.reply) { if (data.reply) {
// Fetch watchers // Fetch watchers
nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm)); nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
// この投稿をWatchする // この投稿をWatchする
if (Users.isLocalUser(user) && user.autoWatch !== false) { if (Users.isLocalUser(user) && profile.autoWatch) {
watch(user.id, data.reply); watch(user.id, data.reply);
} }
@@ -286,7 +288,7 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type)); nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type));
// この投稿をWatchする // この投稿をWatchする
if (Users.isLocalUser(user) && user.autoWatch !== false) { if (Users.isLocalUser(user) && profile.autoWatch) {
watch(user.id, data.renote); watch(user.id, data.renote);
} }
@@ -327,12 +329,16 @@ async function publish(user: User, note: Note, reply: Note, renote: Note, noteAc
if (Users.isLocalUser(user)) { if (Users.isLocalUser(user)) {
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
if (reply && reply.userHost !== null) { if (reply && reply.userHost !== null) {
deliver(user, noteActivity, reply.userInbox); Users.findOne(reply.userId).then(u => {
deliver(user, noteActivity, u.inbox);
});
} }
// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送 // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
if (renote && renote.userHost !== null) { if (renote && renote.userHost !== null) {
deliver(user, noteActivity, renote.userInbox); Users.findOne(renote.userId).then(u => {
deliver(user, noteActivity, u.inbox);
});
} }
} }
@@ -343,7 +349,7 @@ async function publish(user: User, note: Note, reply: Note, renote: Note, noteAc
} }
async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) { async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) {
const insert: Partial<Note> = { const insert = new Note({
id: genId(data.createdAt), id: genId(data.createdAt),
createdAt: data.createdAt, createdAt: data.createdAt,
fileIds: data.files ? data.files.map(file => file.id) : [], fileIds: data.files ? data.files.map(file => file.id) : [],
@@ -375,8 +381,7 @@ async function insertNote(user: User, data: Option, tags: string[], emojis: stri
renoteUserId: data.renote ? data.renote.userId : null, renoteUserId: data.renote ? data.renote.userId : null,
renoteUserHost: data.renote ? data.renote.userHost : null, renoteUserHost: data.renote ? data.renote.userHost : null,
userHost: user.host, userHost: user.host,
userInbox: user.inbox, });
};
if (data.uri != null) insert.uri = data.uri; if (data.uri != null) insert.uri = data.uri;
@@ -392,11 +397,13 @@ async function insertNote(user: User, data: Option, tags: string[], emojis: stri
// 投稿を作成 // 投稿を作成
try { try {
const note = await Notes.save(insert); let note: Note;
if (insert.hasPoll) {
// Start transaction
await getConnection().transaction(async transactionalEntityManager => {
note = await transactionalEntityManager.save(insert);
if (note.hasPoll) { const poll = new Poll({
await Polls.save({
id: genId(),
noteId: note.id, noteId: note.id,
choices: data.poll.choices, choices: data.poll.choices,
expiresAt: data.poll.expiresAt, expiresAt: data.poll.expiresAt,
@@ -405,7 +412,12 @@ async function insertNote(user: User, data: Option, tags: string[], emojis: stri
noteVisibility: note.visibility, noteVisibility: note.visibility,
userId: user.id, userId: user.id,
userHost: user.host userHost: user.host
} as Poll); });
await transactionalEntityManager.save(poll);
});
} else {
note = await Notes.save(insert);
} }
return note; return note;

View File

@@ -1,6 +1,3 @@
import { updateQuestion } from '../../../remote/activitypub/models/question';
import ms = require('ms');
import Logger from '../../logger';
import renderUpdate from '../../../remote/activitypub/renderer/update'; import renderUpdate from '../../../remote/activitypub/renderer/update';
import { renderActivity } from '../../../remote/activitypub/renderer'; import { renderActivity } from '../../../remote/activitypub/renderer';
import { deliver } from '../../../queue'; import { deliver } from '../../../queue';
@@ -8,21 +5,6 @@ import renderNote from '../../../remote/activitypub/renderer/note';
import { Users, Notes, Followings } from '../../../models'; import { Users, Notes, Followings } from '../../../models';
import { Note } from '../../../models/entities/note'; import { Note } from '../../../models/entities/note';
const logger = new Logger('pollsUpdate');
export async function triggerUpdate(note: Note) {
if (!note.updatedAt || Date.now() - new Date(note.updatedAt).getTime() > ms('1min')) {
logger.info(`Updating ${note.id}`);
try {
const updated = await updateQuestion(note.uri);
logger.info(`Updated ${note.id} ${updated ? 'changed' : 'nochange'}`);
} catch (e) {
logger.error(e);
}
}
}
export async function deliverQuestionUpdate(noteId: Note['id']) { export async function deliverQuestionUpdate(noteId: Note['id']) {
const note = await Notes.findOne(noteId); const note = await Notes.findOne(noteId);

View File

@@ -2,7 +2,7 @@ import watch from '../../../services/note/watch';
import { publishNoteStream } from '../../stream'; import { publishNoteStream } from '../../stream';
import { User } from '../../../models/entities/user'; import { User } from '../../../models/entities/user';
import { Note } from '../../../models/entities/note'; import { Note } from '../../../models/entities/note';
import { PollVotes, Users, NoteWatchings, Polls } from '../../../models'; import { PollVotes, Users, NoteWatchings, Polls, UserProfiles } from '../../../models';
import { Not } from 'typeorm'; import { Not } from 'typeorm';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
import { createNotification } from '../../create-notification'; import { createNotification } from '../../create-notification';
@@ -40,7 +40,7 @@ export default (user: User, note: Note, choice: number) => new Promise(async (re
// Increment votes count // Increment votes count
const index = choice + 1; // In SQL, array index is 1 based const index = 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', { publishNoteStream(note.id, 'pollVoted', {
choice: choice, choice: choice,
@@ -67,8 +67,10 @@ export default (user: User, note: Note, choice: number) => new Promise(async (re
} }
}); });
const profile = await UserProfiles.findOne({ userId: user.id });
// ローカルユーザーが投票した場合この投稿をWatchする // ローカルユーザーが投票した場合この投稿をWatchする
if (Users.isLocalUser(user) && user.autoWatch) { if (Users.isLocalUser(user) && profile.autoWatch) {
watch(user.id, note); watch(user.id, note);
} }
}); });

View File

@@ -8,7 +8,7 @@ import { toDbReaction } from '../../../misc/reaction-lib';
import fetchMeta from '../../../misc/fetch-meta'; import fetchMeta from '../../../misc/fetch-meta';
import { User } from '../../../models/entities/user'; import { User } from '../../../models/entities/user';
import { Note } from '../../../models/entities/note'; import { Note } from '../../../models/entities/note';
import { NoteReactions, Users, NoteWatchings, Notes } from '../../../models'; import { NoteReactions, Users, NoteWatchings, Notes, UserProfiles } from '../../../models';
import { Not } from 'typeorm'; import { Not } from 'typeorm';
import { perUserReactionsChart } from '../../chart'; import { perUserReactionsChart } from '../../chart';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
@@ -79,8 +79,10 @@ export default async (user: User, note: Note, reaction: string) => {
} }
}); });
const profile = await UserProfiles.findOne({ userId: user.id });
// ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする // ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする
if (Users.isLocalUser(user) && user.autoWatch !== false) { if (Users.isLocalUser(user) && profile.autoWatch) {
watch(user.id, note); watch(user.id, note);
} }
@@ -88,7 +90,9 @@ export default async (user: User, note: Note, reaction: string) => {
// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
if (Users.isLocalUser(user) && note.userHost !== null) { if (Users.isLocalUser(user) && note.userHost !== null) {
const content = renderActivity(renderLike(user, note, reaction)); const content = renderActivity(renderLike(user, note, reaction));
deliver(user, content, note.userInbox); Users.findOne(note.userId).then(u => {
deliver(user, content, u.inbox);
});
} }
//#endregion //#endregion
}; };

View File

@@ -41,7 +41,9 @@ export default async (user: User, note: Note) => {
// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
if (Users.isLocalUser(user) && (note.userHost !== null)) { if (Users.isLocalUser(user) && (note.userHost !== null)) {
const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user)); const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user));
deliver(user, content, note.userInbox); Users.findOne(note.userId).then(u => {
deliver(user, content, u.inbox);
});
} }
//#endregion //#endregion
}; };