Compare commits

...

24 Commits

Author SHA1 Message Date
5d52e9ce6b 11.0.0-beta.3 2019-04-12 01:56:48 +09:00
3c29027ca3 Clean up 2019-04-12 01:54:28 +09:00
2ff3069d23 トランザクションを使うようにしたり 2019-04-12 01:52:25 +09:00
4198246351 トランザクションを使用してアンケートレコードの挿入に失敗した場合に投稿レコードの挿入もなかったことにするように 2019-04-12 01:30:10 +09:00
2b6389b4dc Fix bug 2019-04-12 01:03:40 +09:00
d7df75ae6c Clean up 2019-04-12 01:01:25 +09:00
11c30eccb3 非正規化カラムを削除
非正規化するほどの情報じゃない
2019-04-12 00:42:39 +09:00
ab8c6515b8 Fix error log 2019-04-12 00:33:26 +09:00
d4ad36fa41 Update migrate.ts 2019-04-11 22:49:12 +09:00
4d688be3df Update migrate.ts 2019-04-11 22:44:04 +09:00
d2b75f3501 Update migrate.ts 2019-04-11 19:42:35 +09:00
46b78cb4ff Increase url column length 2019-04-11 19:03:49 +09:00
5d6e0d0f37 Update migrate.ts 2019-04-11 16:15:27 +09:00
e19d0a37bb Update migrate.ts 2019-04-11 16:09:33 +09:00
dea3e2132e Update migrate.ts 2019-04-11 15:53:15 +09:00
91c1ceefbd Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-04-11 12:59:16 +09:00
5c50989970 Fix bug 2019-04-11 12:59:09 +09:00
2a7e3b9c51 Fix: AP actor Service のサポートが不完全 (v11) (#4662) 2019-04-11 03:09:12 +09:00
ab302df0ae Update CHANGELOG.md 2019-04-11 00:18:37 +09:00
21e5809993 Clean up 2019-04-10 23:57:39 +09:00
c58afc67e8 Update migrate.ts 2019-04-10 20:13:14 +09:00
8e344f2deb 11.0.0-beta.2 2019-04-10 20:08:19 +09:00
c28f4ffb3f Clean up 2019-04-10 20:07:36 +09:00
2a40240310 Fix bug 2019-04-10 18:35:51 +09:00
34 changed files with 286 additions and 196 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-beta.1", "version": "11.0.0-beta.3",
"codename": "daybreak", "codename": "daybreak",
"repository": { "repository": {
"type": "git", "type": "git",

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

@ -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';
@ -36,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;
@ -92,14 +94,14 @@ async function main() {
usernameLower: user.username.toLowerCase(), usernameLower: user.username.toLowerCase(),
host: toPuny(user.host), host: toPuny(user.host),
token: generateUserToken(), token: generateUserToken(),
isAdmin: user.isAdmin, isAdmin: user.isAdmin || false,
name: user.name, name: user.name,
followersCount: user.followersCount, followersCount: user.followersCount || 0,
followingCount: user.followingCount, followingCount: user.followingCount || 0,
notesCount: user.notesCount, notesCount: user.notesCount || 0,
isBot: user.isBot, isBot: user.isBot || false,
isCat: user.isCat, isCat: user.isCat || false,
isVerified: user.isVerified, isVerified: user.isVerified || false,
inbox: user.inbox, inbox: user.inbox,
sharedInbox: user.sharedInbox, sharedInbox: user.sharedInbox,
uri: user.uri, uri: user.uri,
@ -133,7 +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(),
@ -160,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(),
@ -169,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,
@ -255,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,
@ -301,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
}); });
} }
@ -323,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 {
@ -337,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
@ -351,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
@ -365,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 {
@ -379,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
}); });
@ -397,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
@ -411,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
@ -425,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
@ -439,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

@ -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,10 +1,9 @@
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 {
@Index({ unique: true })
@PrimaryColumn(id()) @PrimaryColumn(id())
public userId: User['id']; public userId: User['id'];
@ -23,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

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

View File

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

View File

@ -96,12 +96,12 @@ export class User {
public tags: string[]; public tags: string[];
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
}) })
public avatarUrl: string | null; public avatarUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
}) })
public bannerUrl: string | null; public bannerUrl: string | null;
@ -175,26 +175,26 @@ export class User {
public host: string | null; public host: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The inbox of the User. It will be null if the origin of the user is local.' comment: 'The inbox URL of the User. It will be null if the origin of the user is local.'
}) })
public inbox: string | null; public inbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The sharedInbox of the User. It will be null if the origin of the user is local.' comment: 'The sharedInbox URL of the User. It will be null if the origin of the user is local.'
}) })
public sharedInbox: string | null; public sharedInbox: string | null;
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The featured of the User. It will be null if the origin of the user is local.' comment: 'The featured URL of the User. It will be null if the origin of the user is local.'
}) })
public featured: string | null; public featured: string | null;
@Index() @Index()
@Column('varchar', { @Column('varchar', {
length: 256, nullable: true, length: 512, nullable: true,
comment: 'The URI of the User. It will be null if the origin of the user is local.' comment: 'The URI of the User. It will be null if the origin of the user is local.'
}) })
public uri: string | null; public uri: string | null;
@ -205,6 +205,14 @@ export class User {
comment: 'The native access token of the User. It will be null if the origin of the user is local.' comment: 'The native access token of the User. It will be null if the origin of the user is local.'
}) })
public token: string | null; public token: string | null;
constructor(data: Partial<User>) {
if (data == null) return;
for (const [k, v] of Object.entries(data)) {
(this as any)[k] = v;
}
}
} }
export interface ILocalUser extends User { export interface ILocalUser extends User {

View File

@ -87,7 +87,6 @@ export class UserRepository extends Repository<User> {
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,
@ -95,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({
@ -117,6 +117,9 @@ export class UserRepository extends Repository<User> {
} : {}), } : {}),
...(opts.detail ? { ...(opts.detail ? {
url: profile.url,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
description: profile.description, description: profile.description,
location: profile.location, location: profile.location,
birthday: profile.birthday, birthday: profile.birthday,

View File

@ -13,6 +13,7 @@ import { instanceChart } from '../../services/chart';
import { UserPublickey } from '../../models/entities/user-publickey'; import { UserPublickey } from '../../models/entities/user-publickey';
import fetchMeta from '../../misc/fetch-meta'; import fetchMeta from '../../misc/fetch-meta';
import { toPuny } from '../../misc/convert-host'; import { toPuny } from '../../misc/convert-host';
import { validActor } from '../../remote/activitypub/type';
const logger = new Logger('inbox'); const logger = new Logger('inbox');
@ -93,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)) {

View File

@ -24,6 +24,8 @@ import { UserPublickey } from '../../../models/entities/user-publickey';
import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
import { toPuny } from '../../../misc/convert-host'; import { toPuny } from '../../../misc/convert-host';
import { UserProfile } from '../../../models/entities/user-profile'; import { UserProfile } from '../../../models/entities/user-profile';
import { validActor } from '../../../remote/activitypub/type';
import { getConnection } from 'typeorm';
const logger = apLogger; const logger = apLogger;
/** /**
@ -38,7 +40,7 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: object is null'); return new Error('invalid person: object is null');
} }
if (x.type != 'Person' && x.type != 'Service') { if (!validActor.includes(x.type)) {
return new Error(`invalid person: object is not a person or service '${x.type}'`); return new Error(`invalid person: object is not a person or service '${x.type}'`);
} }
@ -135,27 +137,42 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
// Create user // Create user
let user: IRemoteUser; let user: IRemoteUser;
try { try {
user = await Users.save({ // Start transaction
id: genId(), await getConnection().transaction(async transactionalEntityManager => {
avatarId: null, user = await transactionalEntityManager.save(new User({
bannerId: null, id: genId(),
createdAt: Date.parse(person.published) || new Date(), avatarId: null,
lastFetchedAt: new Date(), bannerId: null,
name: person.name, createdAt: new Date(person.published) || new Date(),
isLocked: person.manuallyApprovesFollowers, lastFetchedAt: new Date(),
username: person.preferredUsername, name: person.name,
usernameLower: person.preferredUsername.toLowerCase(), isLocked: person.manuallyApprovesFollowers,
host, username: person.preferredUsername,
inbox: person.inbox, usernameLower: person.preferredUsername.toLowerCase(),
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), host,
featured: person.featured, inbox: person.inbox,
endpoints: person.endpoints, sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
uri: person.id, featured: person.featured,
url: person.url, uri: person.id,
tags, tags,
isBot, isBot,
isCat: (person as any).isCat === true isCat: (person as any).isCat === true
} as Partial<User>) as IRemoteUser; })) as IRemoteUser;
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: 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)) {
@ -166,18 +183,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
throw e; throw e;
} }
await UserProfiles.save({
userId: user.id,
description: fromHtml(person.summary),
fields,
} as Partial<UserProfile>);
await UserPublickeys.save({
userId: user.id,
keyId: person.publicKey.id,
keyPem: person.publicKey.publicKeyPem
} as UserPublickey);
// Register host // Register host
registerOrFetchInstanceDoc(host).then(i => { registerOrFetchInstanceDoc(host).then(i => {
Instances.increment({ id: i.id }, 'usersCount', 1); Instances.increment({ id: i.id }, 'usersCount', 1);

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

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

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

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

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

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, UserProfiles, 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 { 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 { 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,28 +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;
id: genId(),
createdAt: new Date(),
username: username,
usernameLower: username.toLowerCase(),
host: toPuny(host),
token: secret,
isAdmin: config.autoAdmin && usersCount === 0,
} as User);
await UserKeypairs.save({ // Start transaction
publicKey: keyPair[0], await getConnection().transaction(async transactionalEntityManager => {
privateKey: keyPair[1], account = await transactionalEntityManager.save(new User({
userId: account.id id: genId(),
} as UserKeypair); createdAt: new Date(),
username: username,
usernameLower: username.toLowerCase(),
host: toPuny(host),
token: secret,
isAdmin: config.autoAdmin && usersCount === 0,
}));
await UserProfiles.save({ await transactionalEntityManager.save(new UserKeypair({
userId: account.id, publicKey: keyPair[0],
autoAcceptFollowed: true, privateKey: keyPair[1],
autoWatch: false, userId: account.id
password: hash, }));
} as Partial<UserProfile>);
await transactionalEntityManager.save(new UserProfile({
userId: account.id,
autoAcceptFollowed: true,
autoWatch: false,
password: hash,
}));
});
usersChart.update(account, true); usersChart.update(account, true);

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

@ -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, UserProfiles } 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';
@ -329,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);
});
} }
} }
@ -345,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) : [],
@ -377,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;
@ -394,20 +397,27 @@ 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({ noteId: note.id,
id: genId(), choices: data.poll.choices,
noteId: note.id, expiresAt: data.poll.expiresAt,
choices: data.poll.choices, multiple: data.poll.multiple,
expiresAt: data.poll.expiresAt, votes: new Array(data.poll.choices.length).fill(0),
multiple: data.poll.multiple, noteVisibility: note.visibility,
votes: new Array(data.poll.choices.length).fill(0), userId: user.id,
noteVisibility: note.visibility, userHost: user.host
userId: user.id, });
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

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

View File

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