diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2019-04-14 20:38:55 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2019-04-14 20:38:55 +0900 |
| commit | d66e4b7ff97d512e2a2523815e2eef170456b37f (patch) | |
| tree | 59ae1a102d88b5c2c2236b734ea4a584b4f9ba46 /src/remote/activitypub | |
| parent | 10.100.0 (diff) | |
| parent | 11.0.0 (diff) | |
| download | misskey-d66e4b7ff97d512e2a2523815e2eef170456b37f.tar.gz misskey-d66e4b7ff97d512e2a2523815e2eef170456b37f.tar.bz2 misskey-d66e4b7ff97d512e2a2523815e2eef170456b37f.zip | |
Merge branch 'develop'
Diffstat (limited to 'src/remote/activitypub')
55 files changed, 577 insertions, 609 deletions
diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts index 07c820c28a..377b8dac42 100644 --- a/src/remote/activitypub/kernel/accept/follow.ts +++ b/src/remote/activitypub/kernel/accept/follow.ts @@ -1,21 +1,22 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import config from '../../../../config'; import accept from '../../../../services/following/requests/accept'; import { IFollow } from '../../type'; +import { Users } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id; + if (id == null) throw new Error('missing id'); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const follower = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) + const follower = await Users.findOne({ + id: id.split('/').pop() }); - if (follower === null) { + if (follower == null) { throw new Error('follower not found'); } diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts index 443c1935d6..5a27ce1d4d 100644 --- a/src/remote/activitypub/kernel/accept/index.ts +++ b/src/remote/activitypub/kernel/accept/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import acceptFollow from './follow'; import { IAccept, IFollow } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts index eb2dba5b21..a5b2687416 100644 --- a/src/remote/activitypub/kernel/add/index.ts +++ b/src/remote/activitypub/kernel/add/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IAdd } from '../../type'; import { resolveNote } from '../../models/note'; import { addPinned } from '../../../../services/i/pin'; @@ -14,7 +14,8 @@ export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => { if (activity.target === actor.featured) { const note = await resolveNote(activity.object); - await addPinned(actor, note._id); + if (note == null) throw new Error('note not found'); + await addPinned(actor, note.id); return; } diff --git a/src/remote/activitypub/kernel/announce/index.ts b/src/remote/activitypub/kernel/announce/index.ts index 5f738da6c7..ebd5a27b92 100644 --- a/src/remote/activitypub/kernel/announce/index.ts +++ b/src/remote/activitypub/kernel/announce/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import announceNote from './note'; import { IAnnounce, INote } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/announce/note.ts b/src/remote/activitypub/kernel/announce/note.ts index 912936bef8..f9822c5187 100644 --- a/src/remote/activitypub/kernel/announce/note.ts +++ b/src/remote/activitypub/kernel/announce/note.ts @@ -1,12 +1,12 @@ import Resolver from '../../resolver'; import post from '../../../../services/note/create'; -import { IRemoteUser, IUser } from '../../../../models/user'; +import { IRemoteUser, User } from '../../../../models/entities/user'; import { IAnnounce, INote } from '../../type'; import { fetchNote, resolveNote } from '../../models/note'; import { resolvePerson } from '../../models/person'; import { apLogger } from '../../logger'; import { extractDbHost } from '../../../../misc/convert-host'; -import Instance from '../../../../models/instance'; +import fetchMeta from '../../../../misc/fetch-meta'; const logger = apLogger; @@ -27,8 +27,8 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity: // アナウンス先をブロックしてたら中断 // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく - const instance = await Instance.findOne({ host: extractDbHost(uri) }); - if (instance && instance.isBlocked) return; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(extractDbHost(uri))) return; // 既に同じURIを持つものが登録されていないかチェック const exist = await fetchNote(uri); @@ -53,16 +53,16 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity: logger.info(`Creating the (Re)Note: ${uri}`); //#region Visibility - const visibility = getVisibility(activity.to, activity.cc, actor); + const visibility = getVisibility(activity.to || [], activity.cc || [], actor); - let visibleUsers: IUser[] = []; + let visibleUsers: User[] = []; if (visibility == 'specified') { - visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri))); + visibleUsers = await Promise.all((note.to || []).map(uri => resolvePerson(uri))); } //#endergion await post(actor, { - createdAt: new Date(activity.published), + createdAt: activity.published ? new Date(activity.published) : null, renote, visibility, visibleUsers, @@ -75,9 +75,6 @@ type visibility = 'public' | 'home' | 'followers' | 'specified'; function getVisibility(to: string[], cc: string[], actor: IRemoteUser): visibility { const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; - to = to || []; - cc = cc || []; - if (to.includes(PUBLIC)) { return 'public'; } else if (cc.includes(PUBLIC)) { diff --git a/src/remote/activitypub/kernel/block/index.ts b/src/remote/activitypub/kernel/block/index.ts index a10163016c..5c247326cb 100644 --- a/src/remote/activitypub/kernel/block/index.ts +++ b/src/remote/activitypub/kernel/block/index.ts @@ -1,28 +1,27 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import { IBlock } from '../../type'; import block from '../../../../services/blocking/create'; import { apLogger } from '../../logger'; +import { Users } from '../../../../models'; +import { IRemoteUser } from '../../../../models/entities/user'; const logger = apLogger; export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); const uri = activity.id || activity; logger.info(`Block: ${uri}`); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const blockee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const blockee = await Users.findOne(id.split('/').pop()); - if (blockee === null) { + if (blockee == null) { throw new Error('blockee not found'); } diff --git a/src/remote/activitypub/kernel/create/image.ts b/src/remote/activitypub/kernel/create/image.ts index 9c19abbcc4..7720e8f1bd 100644 --- a/src/remote/activitypub/kernel/create/image.ts +++ b/src/remote/activitypub/kernel/create/image.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { createImage } from '../../models/image'; export default async function(actor: IRemoteUser, image: any): Promise<void> { diff --git a/src/remote/activitypub/kernel/create/index.ts b/src/remote/activitypub/kernel/create/index.ts index 6e314d0b82..0326b591f8 100644 --- a/src/remote/activitypub/kernel/create/index.ts +++ b/src/remote/activitypub/kernel/create/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import createImage from './image'; import createNote from './note'; import { ICreate } from '../../type'; diff --git a/src/remote/activitypub/kernel/create/note.ts b/src/remote/activitypub/kernel/create/note.ts index 0f874b9fbf..70e61bdf1b 100644 --- a/src/remote/activitypub/kernel/create/note.ts +++ b/src/remote/activitypub/kernel/create/note.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { createNote, fetchNote } from '../../models/note'; /** diff --git a/src/remote/activitypub/kernel/delete/index.ts b/src/remote/activitypub/kernel/delete/index.ts index c9c385b1fa..fab5e7ab64 100644 --- a/src/remote/activitypub/kernel/delete/index.ts +++ b/src/remote/activitypub/kernel/delete/index.ts @@ -1,9 +1,9 @@ import Resolver from '../../resolver'; import deleteNote from './note'; -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IDelete } from '../../type'; import { apLogger } from '../../logger'; +import { Notes } from '../../../../models'; /** * 削除アクティビティを捌きます @@ -27,7 +27,7 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => { break; case 'Tombstone': - const note = await Note.findOne({ uri }); + const note = await Notes.findOne({ uri }); if (note != null) { deleteNote(actor, uri); } diff --git a/src/remote/activitypub/kernel/delete/note.ts b/src/remote/activitypub/kernel/delete/note.ts index f67919c56b..b146e68a07 100644 --- a/src/remote/activitypub/kernel/delete/note.ts +++ b/src/remote/activitypub/kernel/delete/note.ts @@ -1,20 +1,20 @@ -import Note from '../../../../models/note'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import deleteNode from '../../../../services/note/delete'; import { apLogger } from '../../logger'; +import { Notes } from '../../../../models'; const logger = apLogger; export default async function(actor: IRemoteUser, uri: string): Promise<void> { logger.info(`Deleting the Note: ${uri}`); - const note = await Note.findOne({ uri }); + const note = await Notes.findOne({ uri }); if (note == null) { throw new Error('note not found'); } - if (!note.userId.equals(actor._id)) { + if (note.userId !== actor.id) { throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); } diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts index e2db70b20d..c255067bfd 100644 --- a/src/remote/activitypub/kernel/follow.ts +++ b/src/remote/activitypub/kernel/follow.ts @@ -1,21 +1,20 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import config from '../../../config'; import follow from '../../../services/following/create'; import { IFollow } from '../type'; +import { Users } from '../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const followee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const followee = await Users.findOne(id.split('/').pop()); - if (followee === null) { + if (followee == null) { throw new Error('followee not found'); } diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts index 4f7a5c91fd..d1251817fa 100644 --- a/src/remote/activitypub/kernel/index.ts +++ b/src/remote/activitypub/kernel/index.ts @@ -1,5 +1,5 @@ import { Object } from '../type'; -import { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import create from './create'; import performDeleteActivity from './delete'; import performUpdateActivity from './update'; @@ -71,7 +71,7 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { default: apLogger.warn(`unknown activity type: ${(activity as any).type}`); - return null; + return; } }; diff --git a/src/remote/activitypub/kernel/like.ts b/src/remote/activitypub/kernel/like.ts index ed35da8133..a08b453a89 100644 --- a/src/remote/activitypub/kernel/like.ts +++ b/src/remote/activitypub/kernel/like.ts @@ -1,19 +1,19 @@ -import * as mongo from 'mongodb'; -import Note from '../../../models/note'; -import { IRemoteUser } from '../../../models/user'; +import { IRemoteUser } from '../../../models/entities/user'; import { ILike } from '../type'; import create from '../../../services/note/reaction/create'; +import { Notes } from '../../../models'; export default async (actor: IRemoteUser, activity: ILike) => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); // Transform: // https://misskey.ex/notes/xxxx to // xxxx - const noteId = new mongo.ObjectID(id.split('/').pop()); + const noteId = id.split('/').pop(); - const note = await Note.findOne({ _id: noteId }); - if (note === null) { + const note = await Notes.findOne(noteId); + if (note == null) { throw new Error(); } diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts index 35cd2ec0c9..d8b5a4b9b9 100644 --- a/src/remote/activitypub/kernel/reject/follow.ts +++ b/src/remote/activitypub/kernel/reject/follow.ts @@ -1,21 +1,20 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import config from '../../../../config'; import reject from '../../../../services/following/requests/reject'; import { IFollow } from '../../type'; +import { Users } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id; + if (id == null) throw new Error('missing id'); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const follower = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const follower = await Users.findOne(id.split('/').pop()); - if (follower === null) { + if (follower == null) { throw new Error('follower not found'); } diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts index c3585abbb6..8ece5cf174 100644 --- a/src/remote/activitypub/kernel/reject/index.ts +++ b/src/remote/activitypub/kernel/reject/index.ts @@ -1,5 +1,5 @@ import Resolver from '../../resolver'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import rejectFollow from './follow'; import { IReject, IFollow } from '../../type'; import { apLogger } from '../../logger'; diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts index 91b207c80d..32b8d66471 100644 --- a/src/remote/activitypub/kernel/remove/index.ts +++ b/src/remote/activitypub/kernel/remove/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IRemove } from '../../type'; import { resolveNote } from '../../models/note'; import { removePinned } from '../../../../services/i/pin'; @@ -14,7 +14,8 @@ export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => { if (activity.target === actor.featured) { const note = await resolveNote(activity.object); - await removePinned(actor, note._id); + if (note == null) throw new Error('note not found'); + await removePinned(actor, note.id); return; } diff --git a/src/remote/activitypub/kernel/undo/block.ts b/src/remote/activitypub/kernel/undo/block.ts index 4a22ac7924..8ef70a9bef 100644 --- a/src/remote/activitypub/kernel/undo/block.ts +++ b/src/remote/activitypub/kernel/undo/block.ts @@ -1,28 +1,27 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import { IBlock } from '../../type'; import unblock from '../../../../services/blocking/delete'; import { apLogger } from '../../logger'; +import { IRemoteUser } from '../../../../models/entities/user'; +import { Users } from '../../../../models'; const logger = apLogger; export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); const uri = activity.id || activity; logger.info(`UnBlock: ${uri}`); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const blockee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const blockee = await Users.findOne(id.split('/').pop()); - if (blockee === null) { + if (blockee == null) { throw new Error('blockee not found'); } diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts index af06aa5b31..d75f055640 100644 --- a/src/remote/activitypub/kernel/undo/follow.ts +++ b/src/remote/activitypub/kernel/undo/follow.ts @@ -1,24 +1,21 @@ -import * as mongo from 'mongodb'; -import User, { IRemoteUser } from '../../../../models/user'; import config from '../../../../config'; import unfollow from '../../../../services/following/delete'; import cancelRequest from '../../../../services/following/requests/cancel'; import { IFollow } from '../../type'; -import FollowRequest from '../../../../models/follow-request'; -import Following from '../../../../models/following'; +import { IRemoteUser } from '../../../../models/entities/user'; +import { Users, FollowRequests, Followings } from '../../../../models'; export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); if (!id.startsWith(config.url + '/')) { - return null; + return; } - const followee = await User.findOne({ - _id: new mongo.ObjectID(id.split('/').pop()) - }); + const followee = await Users.findOne(id.split('/').pop()); - if (followee === null) { + if (followee == null) { throw new Error('followee not found'); } @@ -26,14 +23,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません'); } - const req = await FollowRequest.findOne({ - followerId: actor._id, - followeeId: followee._id + const req = await FollowRequests.findOne({ + followerId: actor.id, + followeeId: followee.id }); - const following = await Following.findOne({ - followerId: actor._id, - followeeId: followee._id + const following = await Followings.findOne({ + followerId: actor.id, + followeeId: followee.id }); if (req) { diff --git a/src/remote/activitypub/kernel/undo/index.ts b/src/remote/activitypub/kernel/undo/index.ts index 80b44fae04..5f2e58c3bf 100644 --- a/src/remote/activitypub/kernel/undo/index.ts +++ b/src/remote/activitypub/kernel/undo/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IUndo, IFollow, IBlock, ILike } from '../../type'; import unfollow from './follow'; import unblock from './block'; @@ -39,6 +39,4 @@ export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => { undoLike(actor, object as ILike); break; } - - return null; }; diff --git a/src/remote/activitypub/kernel/undo/like.ts b/src/remote/activitypub/kernel/undo/like.ts index b324ec854c..2678828a9a 100644 --- a/src/remote/activitypub/kernel/undo/like.ts +++ b/src/remote/activitypub/kernel/undo/like.ts @@ -1,20 +1,20 @@ -import * as mongo from 'mongodb'; -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { ILike } from '../../type'; -import Note from '../../../../models/note'; import deleteReaction from '../../../../services/note/reaction/delete'; +import { Notes } from '../../../../models'; /** * Process Undo.Like activity */ export default async (actor: IRemoteUser, activity: ILike): Promise<void> => { const id = typeof activity.object == 'string' ? activity.object : activity.object.id; + if (id == null) throw new Error('missing id'); - const noteId = new mongo.ObjectID(id.split('/').pop()); + const noteId = id.split('/').pop(); - const note = await Note.findOne({ _id: noteId }); - if (note === null) { - throw 'note not found'; + const note = await Notes.findOne(noteId); + if (note == null) { + throw new Error('note not found'); } await deleteReaction(actor, note); diff --git a/src/remote/activitypub/kernel/update/index.ts b/src/remote/activitypub/kernel/update/index.ts index 49b730391a..b8dff73395 100644 --- a/src/remote/activitypub/kernel/update/index.ts +++ b/src/remote/activitypub/kernel/update/index.ts @@ -1,4 +1,4 @@ -import { IRemoteUser } from '../../../../models/user'; +import { IRemoteUser } from '../../../../models/entities/user'; import { IUpdate, IObject } from '../../type'; import { apLogger } from '../../logger'; import { updateQuestion } from '../../models/question'; diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts index 967ee65544..dba915fee9 100644 --- a/src/remote/activitypub/misc/get-note-html.ts +++ b/src/remote/activitypub/misc/get-note-html.ts @@ -1,9 +1,9 @@ -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; import { toHtml } from '../../../mfm/toHtml'; import { parse } from '../../../mfm/parse'; -export default function(note: INote) { - let html = toHtml(parse(note.text), note.mentionedRemoteUsers); +export default function(note: Note) { + let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers)); if (html == null) html = '<p>.</p>'; return html; diff --git a/src/remote/activitypub/models/image.ts b/src/remote/activitypub/models/image.ts index bd97d13d27..f8b35ea21c 100644 --- a/src/remote/activitypub/models/image.ts +++ b/src/remote/activitypub/models/image.ts @@ -1,19 +1,21 @@ import uploadFromUrl from '../../../services/drive/upload-from-url'; -import { IRemoteUser } from '../../../models/user'; -import DriveFile, { IDriveFile } from '../../../models/drive-file'; +import { IRemoteUser } from '../../../models/entities/user'; import Resolver from '../resolver'; import fetchMeta from '../../../misc/fetch-meta'; import { apLogger } from '../logger'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; +import { ensure } from '../../../prelude/ensure'; const logger = apLogger; /** * Imageを作成します。 */ -export async function createImage(actor: IRemoteUser, value: any): Promise<IDriveFile> { +export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> { // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - return null; + throw new Error('actor has been suspended'); } const image = await new Resolver().resolve(value) as any; @@ -27,30 +29,18 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv const instance = await fetchMeta(); const cache = instance.cacheRemoteFiles; - let file; - try { - file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache); - } catch (e) { - // 4xxの場合は添付されてなかったことにする - if (e >= 400 && e < 500) { - logger.warn(`Ignored image: ${image.url} - ${e}`); - return null; - } - throw e; - } + let file = await uploadFromUrl(image.url, actor, null, image.url, image.sensitive, false, !cache); - if (file.metadata.isRemote) { + if (file.isLink) { // URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、 // URLを更新する - if (file.metadata.url !== image.url) { - file = await DriveFile.findOneAndUpdate({ _id: file._id }, { - $set: { - 'metadata.url': image.url, - 'metadata.uri': image.url - } - }, { - returnNewDocument: true + if (file.url !== image.url) { + await DriveFiles.update({ id: file.id }, { + url: image.url, + uri: image.url }); + + file = await DriveFiles.findOne(file.id).then(ensure); } } @@ -63,7 +53,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> { +export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> { // TODO // リモートサーバーからフェッチしてきて登録 diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 6251621527..8842342342 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -1,26 +1,27 @@ -import * as mongo from 'mongodb'; import * as promiseLimit from 'promise-limit'; import config from '../../../config'; import Resolver from '../resolver'; -import Note, { INote } from '../../../models/note'; import post from '../../../services/note/create'; -import { INote as INoteActivityStreamsObject, IObject } from '../type'; import { resolvePerson, updatePerson } from './person'; import { resolveImage } from './image'; -import { IRemoteUser, IUser } from '../../../models/user'; +import { IRemoteUser, User } from '../../../models/entities/user'; import { fromHtml } from '../../../mfm/fromHtml'; -import Emoji, { IEmoji } from '../../../models/emoji'; import { ITag, extractHashtags } from './tag'; -import { toUnicode } from 'punycode'; import { unique, concat, difference } from '../../../prelude/array'; import { extractPollFromQuestion } from './question'; import vote from '../../../services/note/polls/vote'; import { apLogger } from '../logger'; -import { IDriveFile } from '../../../models/drive-file'; +import { DriveFile } from '../../../models/entities/drive-file'; import { deliverQuestionUpdate } from '../../../services/note/polls/update'; -import Instance from '../../../models/instance'; -import { extractDbHost } from '../../../misc/convert-host'; +import { extractDbHost, toPuny } from '../../../misc/convert-host'; +import { Notes, Emojis, Polls } from '../../../models'; +import { Note } from '../../../models/entities/note'; +import { IObject, INote } from '../type'; +import { Emoji } from '../../../models/entities/emoji'; +import { genId } from '../../../misc/gen-id'; +import fetchMeta from '../../../misc/fetch-meta'; +import { ensure } from '../../../prelude/ensure'; const logger = apLogger; @@ -29,17 +30,18 @@ const logger = apLogger; * * Misskeyに対象のNoteが登録されていればそれを返します。 */ -export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> { +export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { const uri = typeof value == 'string' ? value : value.id; + if (uri == null) throw new Error('missing uri'); // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - return await Note.findOne({ _id: id }); + const id = uri.split('/').pop(); + return await Notes.findOne(id).then(x => x || null); } //#region このサーバーに既に登録されていたらそれを返す - const exist = await Note.findOne({ uri }); + const exist = await Notes.findOne({ uri }); if (exist) { return exist; @@ -52,7 +54,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P /** * Noteを作成します。 */ -export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> { +export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note | null> { if (resolver == null) resolver = new Resolver(); const object: any = await resolver.resolve(value); @@ -65,21 +67,21 @@ export async function createNote(value: any, resolver?: Resolver, silent = false value: value, object: object }); - return null; + throw new Error('invalid note'); } - const note: INoteActivityStreamsObject = object; + const note: INote = object; logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); logger.info(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await resolvePerson(note.attributedTo, null, resolver) as IRemoteUser; + const actor = await resolvePerson(note.attributedTo, resolver) as IRemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { - return null; + throw new Error('actor has been suspended'); } //#region Visibility @@ -87,7 +89,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc; let visibility = 'public'; - let visibleUsers: IUser[] = []; + let visibleUsers: User[] = []; if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) { if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) { visibility = 'home'; @@ -95,9 +97,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false visibility = 'followers'; } else { visibility = 'specified'; - visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver))); + visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, resolver))); } -} + } //#endergion const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver); @@ -113,25 +115,27 @@ export async function createNote(value: any, resolver?: Resolver, silent = false note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; const files = note.attachment .map(attach => attach.sensitive = note.sensitive) - ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>))) + ? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>))) .filter(image => image != null) : []; // リプライ - const reply: INote = note.inReplyTo - ? await resolveNote(note.inReplyTo, resolver).catch(e => { - // 4xxの場合はリプライしてないことにする - if (e.statusCode >= 400 && e.statusCode < 500) { - logger.warn(`Ignored inReplyTo ${note.inReplyTo} - ${e.statusCode} `); - return null; + const reply: Note | null = note.inReplyTo + ? await resolveNote(note.inReplyTo, resolver).then(x => { + if (x == null) { + logger.warn(`Specified inReplyTo, but nout found`); + throw new Error('inReplyTo not found'); + } else { + return x; } + }).catch(e => { logger.warn(`Error in inReplyTo ${note.inReplyTo} - ${e.statusCode || e}`); throw e; }) : null; // 引用 - let quote: INote; + let quote: Note | undefined | null; if (note._misskey_quote && typeof note._misskey_quote == 'string') { quote = await resolveNote(note._misskey_quote).catch(e => { @@ -148,25 +152,27 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const cw = note.summary === '' ? null : note.summary; // テキストのパース - const text = note._misskey_content || fromHtml(note.content); + const text = note._misskey_content || (note.content ? fromHtml(note.content) : null); // vote - if (reply && reply.poll) { + if (reply && reply.hasPoll) { + const poll = await Polls.findOne(reply.id).then(ensure); + const tryCreateVote = async (name: string, index: number): Promise<null> => { - if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) { + if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); } else if (index >= 0) { logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); await vote(actor, reply, index); // リモートフォロワーにUpdate配信 - deliverQuestionUpdate(reply._id); + deliverQuestionUpdate(reply.id); } return null; }; if (note.name) { - return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name)); + return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); } // 後方互換性のため @@ -179,9 +185,9 @@ export async function createNote(value: any, resolver?: Resolver, silent = false } } - const emojis = await extractEmojis(note.tag, actor.host).catch(e => { + const emojis = await extractEmojis(note.tag || [], actor.host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const apEmojis = emojis.map(emoji => emoji.name); @@ -195,7 +201,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false } return await post(actor, { - createdAt: new Date(note.published), + createdAt: note.published ? new Date(note.published) : null, files, reply, renote: quote, @@ -222,13 +228,14 @@ export async function createNote(value: any, resolver?: Resolver, silent = false * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> { +export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> { const uri = typeof value == 'string' ? value : value.id; + if (uri == null) throw new Error('missing uri'); // ブロックしてたら中断 // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく - const instance = await Instance.findOne({ host: extractDbHost(uri) }); - if (instance && instance.isBlocked) throw { statusCode: 451 }; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 }; //#region このサーバーに既に登録されていたらそれを返す const exist = await fetchNote(uri); @@ -241,65 +248,81 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver): // リモートサーバーからフェッチしてきて登録 // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 - return await createNote(uri, resolver); + return await createNote(uri, resolver).catch(e => { + if (e.name === 'duplicated') { + return fetchNote(uri).then(note => { + if (note == null) { + throw new Error('something happened'); + } else { + return note; + } + }); + } else { + throw e; + } + }); } -export async function extractEmojis(tags: ITag[], host_: string) { - const host = toUnicode(host_.toLowerCase()); +export async function extractEmojis(tags: ITag[], host: string): Promise<Emoji[]> { + host = toPuny(host); if (!tags) return []; - const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url); + const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url && tag.name); - return await Promise.all( - eomjiTags.map(async tag => { - const name = tag.name.replace(/^:/, '').replace(/:$/, ''); + return await Promise.all(eomjiTags.map(async tag => { + const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); - const exists = await Emoji.findOne({ - host, - name - }); + const exists = await Emojis.findOne({ + host, + name + }); - if (exists) { - if ((tag.updated != null && exists.updatedAt == null) - || (tag.id != null && exists.uri == null) - || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) { - return await Emoji.findOneAndUpdate({ - host, - name, - }, { - $set: { - uri: tag.id, - url: tag.icon.url, - updatedAt: new Date(tag.updated), - } - }); - } - return exists; + if (exists) { + if ((tag.updated != null && exists.updatedAt == null) + || (tag.id != null && exists.uri == null) + || (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt) + ) { + await Emojis.update({ + host, + name, + }, { + uri: tag.id, + url: tag.icon!.url, + updatedAt: new Date(tag.updated!), + }); + + return await Emojis.findOne({ + host, + name + }) as Emoji; } - logger.info(`register emoji host=${host}, name=${name}`); + return exists; + } + + logger.info(`register emoji host=${host}, name=${name}`); - return await Emoji.insert({ - host, - name, - uri: tag.id, - url: tag.icon.url, - updatedAt: tag.updated ? new Date(tag.updated) : undefined, - aliases: [] - }); - }) - ); + return await Emojis.save({ + id: genId(), + host, + name, + uri: tag.id, + url: tag.icon!.url, + updatedAt: tag.updated ? new Date(tag.updated) : undefined, + aliases: [] + } as Partial<Emoji>); + })); } async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: string[], resolver: Resolver) { const ignoreUris = ['https://www.w3.org/ns/activitystreams#Public', `${actor.uri}/followers`]; const uris = difference(unique(concat([to || [], cc || []])), ignoreUris); - const limit = promiseLimit(2); + const limit = promiseLimit<User | null>(2); const users = await Promise.all( - uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<IUser>) + uris.map(uri => limit(() => resolvePerson(uri, resolver).catch(() => null)) as Promise<User | null>) ); - return users.filter(x => x != null); + return users.filter(x => x != null) as User[]; } diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index d27c937988..c1c07c7bbf 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -1,29 +1,32 @@ -import * as mongo from 'mongodb'; import * as promiseLimit from 'promise-limit'; -import { toUnicode } from 'punycode'; import config from '../../../config'; -import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user'; import Resolver from '../resolver'; import { resolveImage } from './image'; import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'; -import { IDriveFile } from '../../../models/drive-file'; -import Meta from '../../../models/meta'; +import { DriveFile } from '../../../models/entities/drive-file'; import { fromHtml } from '../../../mfm/fromHtml'; -import usersChart from '../../../services/chart/users'; -import instanceChart from '../../../services/chart/instance'; import { URL } from 'url'; import { resolveNote, extractEmojis } from './note'; import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; -import Instance from '../../../models/instance'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; -import { IEmoji } from '../../../models/emoji'; import { ITag, extractHashtags } from './tag'; -import Following from '../../../models/following'; import { IIdentifier } from './identifier'; import { apLogger } from '../logger'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; import { updateHashtag } from '../../../services/update-hashtag'; +import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '../../../models'; +import { User, IRemoteUser } from '../../../models/entities/user'; +import { Emoji } from '../../../models/entities/emoji'; +import { UserNotePining } from '../../../models/entities/user-note-pinings'; +import { genId } from '../../../misc/gen-id'; +import { instanceChart, usersChart } from '../../../services/chart'; +import { UserPublickey } from '../../../models/entities/user-publickey'; +import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error'; +import { toPuny } from '../../../misc/convert-host'; +import { UserProfile } from '../../../models/entities/user-profile'; +import { validActor } from '../../../remote/activitypub/type'; +import { getConnection } from 'typeorm'; +import { ensure } from '../../../prelude/ensure'; const logger = apLogger; /** @@ -32,13 +35,13 @@ const logger = apLogger; * @param uri Fetch target URI */ function validatePerson(x: any, uri: string) { - const expectHost = toUnicode(new URL(uri).hostname.toLowerCase()); + const expectHost = toPuny(new URL(uri).hostname); if (x == null) { return new Error('invalid person: object is null'); } - if (x.type != 'Person' && x.type != 'Service') { + if (!validActor.includes(x.type)) { return new Error(`invalid person: object is not a person or service '${x.type}'`); } @@ -50,11 +53,11 @@ function validatePerson(x: any, uri: string) { return new Error('invalid person: inbox is not a string'); } - if (!validateUsername(x.preferredUsername, true)) { + if (!Users.validateUsername(x.preferredUsername, true)) { return new Error('invalid person: invalid username'); } - if (!isValidName(x.name == '' ? null : x.name)) { + if (!Users.isValidName(x.name == '' ? null : x.name)) { return new Error('invalid person: invalid name'); } @@ -62,7 +65,7 @@ function validatePerson(x: any, uri: string) { return new Error('invalid person: id is not a string'); } - const idHost = toUnicode(new URL(x.id).hostname.toLowerCase()); + const idHost = toPuny(new URL(x.id).hostname); if (idHost !== expectHost) { return new Error('invalid person: id has different host'); } @@ -71,7 +74,7 @@ function validatePerson(x: any, uri: string) { return new Error('invalid person: publicKey.id is not a string'); } - const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase()); + const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname); if (publicKeyIdHost !== expectHost) { return new Error('invalid person: publicKey.id has different host'); } @@ -84,17 +87,17 @@ function validatePerson(x: any, uri: string) { * * Misskeyに対象のPersonが登録されていればそれを返します。 */ -export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> { - if (typeof uri !== 'string') throw 'uri is not string'; +export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> { + if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(config.url + '/')) { - const id = new mongo.ObjectID(uri.split('/').pop()); - return await User.findOne({ _id: id }); + const id = uri.split('/').pop(); + return await Users.findOne(id).then(x => x || null); } //#region このサーバーに既に登録されていたらそれを返す - const exist = await User.findOne({ uri }); + const exist = await Users.findOne({ uri }); if (exist) { return exist; @@ -107,8 +110,8 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUs /** * Personを作成します。 */ -export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> { - if (typeof uri !== 'string') throw 'uri is not string'; +export async function createPerson(uri: string, resolver?: Resolver): Promise<User> { + if (typeof uri !== 'string') throw new Error('uri is not string'); if (resolver == null) resolver = new Resolver(); @@ -124,24 +127,9 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU logger.info(`Creating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ - resolver.resolve(person.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ) - ]); + const host = toPuny(new URL(object.id).hostname); - const host = toUnicode(new URL(object.id).hostname.toLowerCase()); - - const { fields, services } = analyzeAttachments(person.attachment); + const { fields } = analyzeAttachments(person.attachment || []); const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()); @@ -150,39 +138,45 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU // Create user let user: IRemoteUser; try { - user = await User.insert({ - avatarId: null, - bannerId: null, - createdAt: Date.parse(person.published) || null, - lastFetchedAt: new Date(), - description: fromHtml(person.summary), - followersCount, - followingCount, - notesCount, - name: person.name, - isLocked: person.manuallyApprovesFollowers, - username: person.preferredUsername, - usernameLower: person.preferredUsername.toLowerCase(), - host, - publicKey: { - id: person.publicKey.id, - publicKeyPem: person.publicKey.publicKeyPem - }, - inbox: person.inbox, - sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), - featured: person.featured, - endpoints: person.endpoints, - uri: person.id, - url: person.url, - fields, - ...services, - tags, - isBot, - isCat: (person as any).isCat === true - }) as IRemoteUser; + // Start transaction + await getConnection().transaction(async transactionalEntityManager => { + user = await transactionalEntityManager.save(new User({ + id: genId(), + avatarId: null, + bannerId: null, + createdAt: new Date(), + lastFetchedAt: new Date(), + name: person.name, + isLocked: person.manuallyApprovesFollowers, + username: person.preferredUsername, + usernameLower: person.preferredUsername.toLowerCase(), + host, + inbox: person.inbox, + sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), + featured: person.featured, + uri: person.id, + tags, + isBot, + isCat: (person as any).isCat === true + })) as IRemoteUser; + + await transactionalEntityManager.save(new UserProfile({ + userId: user.id, + description: person.summary ? fromHtml(person.summary) : null, + url: person.url, + fields, + userHost: host + })); + + await transactionalEntityManager.save(new UserPublickey({ + userId: user.id, + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem + })); + }); } catch (e) { // duplicate key error - if (e.code === 11000) { + if (isDuplicateKeyValueError(e)) { throw new Error('already registered'); } @@ -192,83 +186,66 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU // Register host registerOrFetchInstanceDoc(host).then(i => { - Instance.update({ _id: i._id }, { - $inc: { - usersCount: 1 - } - }); - + Instances.increment({ id: i.id }, 'usersCount', 1); instanceChart.newUser(i.host); }); - //#region Increment users count - Meta.update({}, { - $inc: { - 'stats.usersCount': 1 - } - }, { upsert: true }); - - usersChart.update(user, true); - //#endregion + usersChart.update(user!, true); // ハッシュタグ更新 - for (const tag of tags) updateHashtag(user, tag, true, true); - for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false); + for (const tag of tags) updateHashtag(user!, tag, true, true); + for (const tag of (user!.tags || []).filter(x => !tags.includes(x))) updateHashtag(user!, tag, true, false); //#region アイコンとヘッダー画像をフェッチ - const [avatar, banner] = (await Promise.all<IDriveFile>([ + const [avatar, banner] = (await Promise.all<DriveFile | null>([ person.icon, person.image ].map(img => img == null ? Promise.resolve(null) - : resolveImage(user, img).catch(() => null) + : resolveImage(user!, img).catch(() => null) ))); - const avatarId = avatar ? avatar._id : null; - const bannerId = banner ? banner._id : null; - const avatarUrl = getDriveFileUrl(avatar, true); - const bannerUrl = getDriveFileUrl(banner, false); - const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null; - const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null; + const avatarId = avatar ? avatar.id : null; + const bannerId = banner ? banner.id : null; + const avatarUrl = avatar ? DriveFiles.getPublicUrl(avatar) : null; + const bannerUrl = banner ? DriveFiles.getPublicUrl(banner) : null; + const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null; + const bannerColor = banner && banner.properties.avgColor ? banner.properties.avgColor : null; - await User.update({ _id: user._id }, { - $set: { - avatarId, - bannerId, - avatarUrl, - bannerUrl, - avatarColor, - bannerColor - } + await Users.update(user!.id, { + avatarId, + bannerId, + avatarUrl, + bannerUrl, + avatarColor, + bannerColor }); - user.avatarId = avatarId; - user.bannerId = bannerId; - user.avatarUrl = avatarUrl; - user.bannerUrl = bannerUrl; - user.avatarColor = avatarColor; - user.bannerColor = bannerColor; + user!.avatarId = avatarId; + user!.bannerId = bannerId; + user!.avatarUrl = avatarUrl; + user!.bannerUrl = bannerUrl; + user!.avatarColor = avatarColor; + user!.bannerColor = bannerColor; //#endregion //#region カスタム絵文字取得 - const emojis = await extractEmojis(person.tag, host).catch(e => { + const emojis = await extractEmojis(person.tag || [], host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); - await User.update({ _id: user._id }, { - $set: { - emojis: emojiNames - } + await Users.update(user!.id, { + emojis: emojiNames }); //#endregion - await updateFeatured(user._id).catch(err => logger.error(err)); + await updateFeatured(user!.id).catch(err => logger.error(err)); - return user; + return user!; } /** @@ -278,8 +255,8 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU * @param resolver Resolver * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) */ -export async function updatePerson(uri: string, resolver?: Resolver, hint?: object): Promise<void> { - if (typeof uri !== 'string') throw 'uri is not string'; +export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: object): Promise<void> { + if (typeof uri !== 'string') throw new Error('uri is not string'); // URIがこのサーバーを指しているならスキップ if (uri.startsWith(config.url + '/')) { @@ -287,7 +264,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje } //#region このサーバーに既に登録されているか - const exist = await User.findOne({ uri }) as IRemoteUser; + const exist = await Users.findOne({ uri }) as IRemoteUser; if (exist == null) { return; @@ -295,10 +272,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje //#endregion // 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する - await User.update({ _id: exist._id }, { - $set: { - lastFetchedAt: new Date(), - }, + await Users.update(exist.id, { + lastFetchedAt: new Date(), }); if (resolver == null) resolver = new Resolver(); @@ -315,23 +290,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje logger.info(`Updating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ - resolver.resolve(person.followers).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.following).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ), - resolver.resolve(person.outbox).then( - resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, - () => undefined - ) - ]); - // アイコンとヘッダー画像をフェッチ - const [avatar, banner] = (await Promise.all<IDriveFile>([ + const [avatar, banner] = (await Promise.all<DriveFile | null>([ person.icon, person.image ].map(img => @@ -341,14 +301,14 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje ))); // カスタム絵文字取得 - const emojis = await extractEmojis(person.tag, exist.host).catch(e => { + const emojis = await extractEmojis(person.tag || [], exist.host).catch(e => { logger.info(`extractEmojis: ${e}`); - return [] as IEmoji[]; + return [] as Emoji[]; }); const emojiNames = emojis.map(emoji => emoji.name); - const { fields, services } = analyzeAttachments(person.attachment); + const { fields, services } = analyzeAttachments(person.attachment || []); const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()); @@ -358,41 +318,45 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), featured: person.featured, emojis: emojiNames, - description: fromHtml(person.summary), - followersCount, - followingCount, - notesCount, + description: person.summary ? fromHtml(person.summary) : null, name: person.name, url: person.url, endpoints: person.endpoints, fields, - ...services, tags, isBot: object.type == 'Service', isCat: (person as any).isCat === true, isLocked: person.manuallyApprovesFollowers, - createdAt: Date.parse(person.published) || null, - publicKey: { - id: person.publicKey.id, - publicKeyPem: person.publicKey.publicKeyPem - }, - } as any; + } as Partial<User>; if (avatar) { - updates.avatarId = avatar._id; - updates.avatarUrl = getDriveFileUrl(avatar, true); - updates.avatarColor = avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null; + updates.avatarId = avatar.id; + updates.avatarUrl = DriveFiles.getPublicUrl(avatar); + updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null; } if (banner) { - updates.bannerId = banner._id; - updates.bannerUrl = getDriveFileUrl(banner, true); - updates.bannerColor = banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null; + updates.bannerId = banner.id; + updates.bannerUrl = DriveFiles.getPublicUrl(banner); + updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null; } // Update user - await User.update({ _id: exist._id }, { - $set: updates + await Users.update(exist.id, updates); + + await UserPublickeys.update({ userId: exist.id }, { + keyId: person.publicKey.id, + keyPem: person.publicKey.publicKeyPem + }); + + await UserProfiles.update({ userId: exist.id }, { + twitterUserId: services.twitter.userId, + twitterScreenName: services.twitter.screenName, + githubId: services.github.id, + githubLogin: services.github.login, + discordId: services.discord.id, + discordUsername: services.discord.username, + discordDiscriminator: services.discord.discriminator, }); // ハッシュタグ更新 @@ -400,17 +364,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje for (const tag of (exist.tags || []).filter(x => !tags.includes(x))) updateHashtag(exist, tag, true, false); // 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする - await Following.update({ - followerId: exist._id - }, { - $set: { - '_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined) - } + await Followings.update({ + followerId: exist.id }, { - multi: true + followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined) }); - await updateFeatured(exist._id).catch(err => logger.error(err)); + await updateFeatured(exist.id).catch(err => logger.error(err)); } /** @@ -419,8 +379,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> { - if (typeof uri !== 'string') throw 'uri is not string'; +export async function resolvePerson(uri: string, resolver?: Resolver): Promise<User> { + if (typeof uri !== 'string') throw new Error('uri is not string'); //#region このサーバーに既に登録されていたらそれを返す const exist = await fetchPerson(uri); @@ -479,22 +439,25 @@ export function analyzeAttachments(attachments: ITag[]) { }[] = []; const services: { [x: string]: any } = {}; - if (Array.isArray(attachments)) - for (const attachment of attachments.filter(isPropertyValue)) - if (isPropertyValue(attachment.identifier)) - addService(services, attachment.identifier); - else + if (Array.isArray(attachments)) { + for (const attachment of attachments.filter(isPropertyValue)) { + if (isPropertyValue(attachment.identifier!)) { + addService(services, attachment.identifier!); + } else { fields.push({ - name: attachment.name, - value: fromHtml(attachment.value) + name: attachment.name!, + value: fromHtml(attachment.value!) }); + } + } + } return { fields, services }; } -export async function updateFeatured(userId: mongo.ObjectID) { - const user = await User.findOne({ _id: userId }); - if (!isRemoteUser(user)) return; +export async function updateFeatured(userId: User['id']) { + const user = await Users.findOne(userId).then(ensure); + if (!Users.isRemoteUser(user)) return; if (!user.featured) return; logger.info(`Updating the featured: ${user.uri}`); @@ -511,15 +474,18 @@ export async function updateFeatured(userId: mongo.ObjectID) { if (!Array.isArray(items)) throw new Error(`Collection items is not an array`); // Resolve and regist Notes - const limit = promiseLimit(2); + const limit = promiseLimit<Note | null>(2); const featuredNotes = await Promise.all(items .filter(item => item.type === 'Note') .slice(0, 5) - .map(item => limit(() => resolveNote(item, resolver)) as Promise<INote>)); + .map(item => limit(() => resolveNote(item, resolver)))); - await User.update({ _id: user._id }, { - $set: { - pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id) - } - }); + for (const note of featuredNotes.filter(note => note != null)) { + UserNotePinings.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + noteId: note!.id + } as UserNotePining); + } } diff --git a/src/remote/activitypub/models/question.ts b/src/remote/activitypub/models/question.ts index c073684349..5c126c3a56 100644 --- a/src/remote/activitypub/models/question.ts +++ b/src/remote/activitypub/models/question.ts @@ -1,8 +1,9 @@ import config from '../../../config'; -import Note, { IChoice, IPoll } from '../../../models/note'; import Resolver from '../resolver'; import { IQuestion } from '../type'; import { apLogger } from '../logger'; +import { Notes, Polls } from '../../../models'; +import { IPoll } from '../../../models/entities/poll'; export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> { const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source; @@ -10,18 +11,18 @@ export async function extractPollFromQuestion(source: string | IQuestion): Promi const expiresAt = question.endTime ? new Date(question.endTime) : null; if (multiple && !question.anyOf) { - throw 'invalid question'; + throw new Error('invalid question'); } - const choices = question[multiple ? 'anyOf' : 'oneOf'] - .map((x, i) => ({ - id: i, - text: x.name, - votes: x.replies && x.replies.totalItems || x._misskey_votes || 0, - } as IChoice)); + const choices = question[multiple ? 'anyOf' : 'oneOf']! + .map((x, i) => x.name!); + + const votes = question[multiple ? 'anyOf' : 'oneOf']! + .map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0); return { choices, + votes, multiple, expiresAt }; @@ -36,12 +37,14 @@ export async function updateQuestion(value: any) { const uri = typeof value == 'string' ? value : value.id; // URIがこのサーバーを指しているならスキップ - if (uri.startsWith(config.url + '/')) throw 'uri points local'; + if (uri.startsWith(config.url + '/')) throw new Error('uri points local'); //#region このサーバーに既に登録されているか - const note = await Note.findOne({ uri }); + const note = await Notes.findOne({ uri }); + if (note == null) throw new Error('Question is not registed'); - if (note == null) throw 'Question is not registed'; + const poll = await Polls.findOne({ noteId: note.id }); + if (poll == null) throw new Error('Question is not registed'); //#endregion // resolve new Question object @@ -49,30 +52,24 @@ export async function updateQuestion(value: any) { const question = await resolver.resolve(value) as IQuestion; apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); - if (question.type !== 'Question') throw 'object is not a Question'; + if (question.type !== 'Question') throw new Error('object is not a Question'); const apChoices = question.oneOf || question.anyOf; - const dbChoices = note.poll.choices; let changed = false; - for (const db of dbChoices) { - const oldCount = db.votes; - const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems; + for (const choice of poll.choices) { + const oldCount = poll.votes[poll.choices.indexOf(choice)]; + const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems; if (oldCount != newCount) { changed = true; - db.votes = newCount; + poll.votes[poll.choices.indexOf(choice)] = newCount; } } - await Note.update({ - _id: note._id - }, { - $set: { - 'poll.choices': dbChoices, - updatedAt: new Date(), - } + await Polls.update({ noteId: note.id }, { + votes: poll.votes }); return changed; diff --git a/src/remote/activitypub/models/tag.ts b/src/remote/activitypub/models/tag.ts index 0a1e6e29f9..8d2008d1d9 100644 --- a/src/remote/activitypub/models/tag.ts +++ b/src/remote/activitypub/models/tag.ts @@ -14,13 +14,13 @@ export type ITag = { identifier?: IIdentifier; }; -export function extractHashtags(tags: ITag[]) { - if (!tags) return []; +export function extractHashtags(tags: ITag[] | null | undefined): string[] { + if (tags == null) return []; const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string'); return hashtags.map(tag => { - const m = tag.name.match(/^#(.+)/); + const m = tag.name ? tag.name.match(/^#(.+)/) : null; return m ? m[1] : null; - }).filter(x => x != null); + }).filter(x => x != null) as string[]; } diff --git a/src/remote/activitypub/perform.ts b/src/remote/activitypub/perform.ts index 2e4f53adf5..425adaec96 100644 --- a/src/remote/activitypub/perform.ts +++ b/src/remote/activitypub/perform.ts @@ -1,5 +1,5 @@ import { Object } from './type'; -import { IRemoteUser } from '../../models/user'; +import { IRemoteUser } from '../../models/entities/user'; import kernel from './kernel'; export default async (actor: IRemoteUser, activity: Object): Promise<void> => { diff --git a/src/remote/activitypub/renderer/accept.ts b/src/remote/activitypub/renderer/accept.ts index fdbdff3f12..21b4629074 100644 --- a/src/remote/activitypub/renderer/accept.ts +++ b/src/remote/activitypub/renderer/accept.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Accept', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts index 4d6fe392aa..46f937f61d 100644 --- a/src/remote/activitypub/renderer/add.ts +++ b/src/remote/activitypub/renderer/add.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (user: ILocalUser, target: any, object: any) => ({ type: 'Add', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, target, object }); diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts index f6f2f9bdcd..11e7be449b 100644 --- a/src/remote/activitypub/renderer/announce.ts +++ b/src/remote/activitypub/renderer/announce.ts @@ -1,7 +1,7 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; -export default (object: any, note: INote) => { +export default (object: any, note: Note) => { const attributedTo = `${config.url}/users/${note.userId}`; let to: string[] = []; @@ -18,7 +18,7 @@ export default (object: any, note: INote) => { } return { - id: `${config.url}/notes/${note._id}/activity`, + id: `${config.url}/notes/${note.id}/activity`, actor: `${config.url}/users/${note.userId}`, type: 'Announce', published: note.createdAt.toISOString(), diff --git a/src/remote/activitypub/renderer/block.ts b/src/remote/activitypub/renderer/block.ts index 694f3a1418..c29a9aea82 100644 --- a/src/remote/activitypub/renderer/block.ts +++ b/src/remote/activitypub/renderer/block.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser, IRemoteUser } from '../../../models/user'; +import { ILocalUser, IRemoteUser } from '../../../models/entities/user'; -export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({ +export default (blocker: ILocalUser, blockee: IRemoteUser) => ({ type: 'Block', - actor: `${config.url}/users/${blocker._id}`, + actor: `${config.url}/users/${blocker.id}`, object: blockee.uri }); diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts index 1ee1418fce..e1fc0515c8 100644 --- a/src/remote/activitypub/renderer/create.ts +++ b/src/remote/activitypub/renderer/create.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; +import { Note } from '../../../models/entities/note'; -export default (object: any, note: INote) => { +export default (object: any, note: Note) => { const activity = { - id: `${config.url}/notes/${note._id}/activity`, + id: `${config.url}/notes/${note.id}/activity`, actor: `${config.url}/users/${note.userId}`, type: 'Create', published: note.createdAt.toISOString(), diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts index e090e1c886..a98c97e6e9 100644 --- a/src/remote/activitypub/renderer/delete.ts +++ b/src/remote/activitypub/renderer/delete.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Delete', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/document.ts b/src/remote/activitypub/renderer/document.ts index 17721e9417..4f6ea8c4ee 100644 --- a/src/remote/activitypub/renderer/document.ts +++ b/src/remote/activitypub/renderer/document.ts @@ -1,8 +1,8 @@ -import { IDriveFile } from '../../../models/drive-file'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; -export default (file: IDriveFile) => ({ +export default (file: DriveFile) => ({ type: 'Document', - mediaType: file.contentType, - url: getDriveFileUrl(file) + mediaType: file.type, + url: DriveFiles.getPublicUrl(file) }); diff --git a/src/remote/activitypub/renderer/emoji.ts b/src/remote/activitypub/renderer/emoji.ts index 1a05b4e89e..947a96df37 100644 --- a/src/remote/activitypub/renderer/emoji.ts +++ b/src/remote/activitypub/renderer/emoji.ts @@ -1,7 +1,7 @@ -import { IEmoji } from '../../../models/emoji'; import config from '../../../config'; +import { Emoji } from '../../../models/entities/emoji'; -export default (emoji: IEmoji) => ({ +export default (emoji: Emoji) => ({ id: `${config.url}/emojis/${emoji.name}`, type: 'Emoji', name: `:${emoji.name}:`, diff --git a/src/remote/activitypub/renderer/follow-user.ts b/src/remote/activitypub/renderer/follow-user.ts index 9a488d392b..6d354803e5 100644 --- a/src/remote/activitypub/renderer/follow-user.ts +++ b/src/remote/activitypub/renderer/follow-user.ts @@ -1,16 +1,13 @@ import config from '../../../config'; -import * as mongo from 'mongodb'; -import User, { isLocalUser } from '../../../models/user'; +import { Users } from '../../../models'; +import { User } from '../../../models/entities/user'; +import { ensure } from '../../../prelude/ensure'; /** * Convert (local|remote)(Follower|Followee)ID to URL * @param id Follower|Followee ID */ -export default async function renderFollowUser(id: mongo.ObjectID): Promise<any> { - - const user = await User.findOne({ - _id: id - }); - - return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri; +export default async function renderFollowUser(id: User['id']): Promise<any> { + const user = await Users.findOne(id).then(ensure); + return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri; } diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts index 98d4cdd020..400b15ec7b 100644 --- a/src/remote/activitypub/renderer/follow.ts +++ b/src/remote/activitypub/renderer/follow.ts @@ -1,11 +1,12 @@ import config from '../../../config'; -import { IUser, isLocalUser } from '../../../models/user'; +import { User } from '../../../models/entities/user'; +import { Users } from '../../../models'; -export default (follower: IUser, followee: IUser, requestId?: string) => { +export default (follower: User, followee: User, requestId?: string) => { const follow = { type: 'Follow', - actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri, - object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri + actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, + object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri } as any; if (requestId) follow.id = requestId; diff --git a/src/remote/activitypub/renderer/image.ts b/src/remote/activitypub/renderer/image.ts index ec637b9521..ce98f98c62 100644 --- a/src/remote/activitypub/renderer/image.ts +++ b/src/remote/activitypub/renderer/image.ts @@ -1,8 +1,8 @@ -import { IDriveFile } from '../../../models/drive-file'; -import getDriveFileUrl from '../../../misc/get-drive-file-url'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles } from '../../../models'; -export default (file: IDriveFile) => ({ +export default (file: DriveFile) => ({ type: 'Image', - url: getDriveFileUrl(file), - sensitive: file.metadata.isSensitive + url: DriveFiles.getPublicUrl(file), + sensitive: file.isSensitive }); diff --git a/src/remote/activitypub/renderer/key.ts b/src/remote/activitypub/renderer/key.ts index 0d5e52557c..334e5e00cd 100644 --- a/src/remote/activitypub/renderer/key.ts +++ b/src/remote/activitypub/renderer/key.ts @@ -1,10 +1,10 @@ import config from '../../../config'; -import { extractPublic } from '../../../crypto_key'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; +import { UserKeypair } from '../../../models/entities/user-keypair'; -export default (user: ILocalUser) => ({ - id: `${config.url}/users/${user._id}/publickey`, +export default (user: ILocalUser, key: UserKeypair) => ({ + id: `${config.url}/users/${user.id}/publickey`, type: 'Key', - owner: `${config.url}/users/${user._id}`, - publicKeyPem: extractPublic(user.keypair) + owner: `${config.url}/users/${user.id}`, + publicKeyPem: key.publicKey }); diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts index 523cb4f1ad..01f10ec0a9 100644 --- a/src/remote/activitypub/renderer/like.ts +++ b/src/remote/activitypub/renderer/like.ts @@ -1,10 +1,10 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; -import { INote } from '../../../models/note'; +import { ILocalUser } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; -export default (user: ILocalUser, note: INote, reaction: string) => ({ +export default (user: ILocalUser, note: Note, reaction: string) => ({ type: 'Like', - actor: `${config.url}/users/${user._id}`, - object: note.uri ? note.uri : `${config.url}/notes/${note._id}`, + actor: `${config.url}/users/${user.id}`, + object: note.uri ? note.uri : `${config.url}/notes/${note.id}`, _misskey_reaction: reaction }); diff --git a/src/remote/activitypub/renderer/mention.ts b/src/remote/activitypub/renderer/mention.ts index 8d12e6d8bf..889be5d85d 100644 --- a/src/remote/activitypub/renderer/mention.ts +++ b/src/remote/activitypub/renderer/mention.ts @@ -1,8 +1,9 @@ -import { IUser, isRemoteUser } from '../../../models/user'; import config from '../../../config'; +import { User, ILocalUser } from '../../../models/entities/user'; +import { Users } from '../../../models'; -export default (mention: IUser) => ({ +export default (mention: User) => ({ type: 'Mention', - href: isRemoteUser(mention) ? mention.uri : `${config.url}/@${mention.username}`, - name: isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${mention.username}`, + href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`, + name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, }); diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 8b349526e1..c66af2667b 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -3,38 +3,37 @@ import renderHashtag from './hashtag'; import renderMention from './mention'; import renderEmoji from './emoji'; import config from '../../../config'; -import DriveFile, { IDriveFile } from '../../../models/drive-file'; -import Note, { INote } from '../../../models/note'; -import User from '../../../models/user'; import toHtml from '../misc/get-note-html'; -import Emoji, { IEmoji } from '../../../models/emoji'; +import { Note, IMentionedRemoteUsers } from '../../../models/entities/note'; +import { DriveFile } from '../../../models/entities/drive-file'; +import { DriveFiles, Notes, Users, Emojis, Polls } from '../../../models'; +import { In } from 'typeorm'; +import { Emoji } from '../../../models/entities/emoji'; +import { Poll } from '../../../models/entities/poll'; +import { ensure } from '../../../prelude/ensure'; -export default async function renderNote(note: INote, dive = true): Promise<any> { - const promisedFiles: Promise<IDriveFile[]> = note.fileIds - ? DriveFile.find({ _id: { $in: note.fileIds } }) +export default async function renderNote(note: Note, dive = true): Promise<any> { + const promisedFiles: Promise<DriveFile[]> = note.fileIds.length > 0 + ? DriveFiles.find({ id: In(note.fileIds) }) : Promise.resolve([]); let inReplyTo; - let inReplyToNote: INote; + let inReplyToNote: Note | undefined; if (note.replyId) { - inReplyToNote = await Note.findOne({ - _id: note.replyId, - }); + inReplyToNote = await Notes.findOne(note.replyId); - if (inReplyToNote !== null) { - const inReplyToUser = await User.findOne({ - _id: inReplyToNote.userId, - }); + if (inReplyToNote != null) { + const inReplyToUser = await Users.findOne(inReplyToNote.userId); - if (inReplyToUser !== null) { + if (inReplyToUser != null) { if (inReplyToNote.uri) { inReplyTo = inReplyToNote.uri; } else { if (dive) { inReplyTo = await renderNote(inReplyToNote, false); } else { - inReplyTo = `${config.url}/notes/${inReplyToNote._id}`; + inReplyTo = `${config.url}/notes/${inReplyToNote.id}`; } } } @@ -46,24 +45,18 @@ export default async function renderNote(note: INote, dive = true): Promise<any> let quote; if (note.renoteId) { - const renote = await Note.findOne({ - _id: note.renoteId, - }); + const renote = await Notes.findOne(note.renoteId); if (renote) { - quote = renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`; + quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`; } } - const user = await User.findOne({ - _id: note.userId - }); + const user = await Users.findOne(note.userId).then(ensure); - const attributedTo = `${config.url}/users/${user._id}`; + const attributedTo = `${config.url}/users/${user.id}`; - const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0 - ? note.mentionedRemoteUsers.map(x => x.uri) - : []; + const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); let to: string[] = []; let cc: string[] = []; @@ -81,10 +74,8 @@ export default async function renderNote(note: INote, dive = true): Promise<any> to = mentions; } - const mentionedUsers = note.mentions ? await User.find({ - _id: { - $in: note.mentions - } + const mentionedUsers = note.mentions.length > 0 ? await Users.find({ + id: In(note.mentions) }) : []; const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag)); @@ -93,23 +84,28 @@ export default async function renderNote(note: INote, dive = true): Promise<any> const files = await promisedFiles; let text = note.text; + let poll: Poll | undefined; + + if (note.hasPoll) { + poll = await Polls.findOne({ noteId: note.id }); + } - let question: string; - if (note.poll != null) { + let question: string | undefined; + if (poll) { if (text == null) text = ''; - const url = `${config.url}/notes/${note._id}`; + const url = `${config.url}/notes/${note.id}`; // TODO: i18n text += `\n[リモートで結果を表示](${url})`; - question = `${config.url}/questions/${note._id}`; + question = `${config.url}/questions/${note.id}`; } let apText = text; if (apText == null) apText = ''; // Provides choices as text for AP - if (note.poll != null) { - const cs = note.poll.choices.map(c => `${c.id}: ${c.text}`); + if (poll) { + const cs = poll.choices.map((c, i) => `${i}: ${c}`); apText += '\n----------------------------------------\n'; apText += cs.join('\n'); apText += '\n----------------------------------------\n'; @@ -135,31 +131,25 @@ export default async function renderNote(note: INote, dive = true): Promise<any> ...apemojis, ]; - const { - choices = [], - expiresAt = null, - multiple = false - } = note.poll || {}; - - const asPoll = note.poll ? { + const asPoll = poll ? { type: 'Question', content: toHtml(Object.assign({}, note, { text: text })), _misskey_fallback_content: content, - [expiresAt && expiresAt < new Date() ? 'closed' : 'endTime']: expiresAt, - [multiple ? 'anyOf' : 'oneOf']: choices.map(({ text, votes }) => ({ + [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, + [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ type: 'Note', name: text, replies: { type: 'Collection', - totalItems: votes + totalItems: poll!.votes[i] } })) } : {}; return { - id: `${config.url}/notes/${note._id}`, + id: `${config.url}/notes/${note.id}`, type: 'Note', attributedTo, summary, @@ -172,21 +162,21 @@ export default async function renderNote(note: INote, dive = true): Promise<any> cc, inReplyTo, attachment: files.map(renderDocument), - sensitive: files.some(file => file.metadata.isSensitive), + sensitive: files.some(file => file.isSensitive), tag, ...asPoll }; } -export async function getEmojis(names: string[]): Promise<IEmoji[]> { - if (names == null || names.length < 1) return []; +export async function getEmojis(names: string[]): Promise<Emoji[]> { + if (names == null || names.length === 0) return []; const emojis = await Promise.all( - names.map(name => Emoji.findOne({ + names.map(name => Emojis.findOne({ name, host: null })) ); - return emojis.filter(emoji => emoji != null); + return emojis.filter(emoji => emoji != null) as Emoji[]; } diff --git a/src/remote/activitypub/renderer/ordered-collection-page.ts b/src/remote/activitypub/renderer/ordered-collection-page.ts index 83af07870e..2433358646 100644 --- a/src/remote/activitypub/renderer/ordered-collection-page.ts +++ b/src/remote/activitypub/renderer/ordered-collection-page.ts @@ -7,7 +7,7 @@ * @param prev URL of prev page (optional) * @param next URL of next page (optional) */ -export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev: string, next: string) { +export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { const page = { id, partOf, diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 77e60cd61a..3fb164ef4e 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -1,21 +1,23 @@ import renderImage from './image'; import renderKey from './key'; import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; import { toHtml } from '../../../mfm/toHtml'; import { parse } from '../../../mfm/parse'; -import DriveFile from '../../../models/drive-file'; import { getEmojis } from './note'; import renderEmoji from './emoji'; import { IIdentifier } from '../models/identifier'; import renderHashtag from './hashtag'; +import { DriveFiles, UserProfiles, UserKeypairs } from '../../../models'; +import { ensure } from '../../../prelude/ensure'; -export default async (user: ILocalUser) => { - const id = `${config.url}/users/${user._id}`; +export async function renderPerson(user: ILocalUser) { + const id = `${config.url}/users/${user.id}`; - const [avatar, banner] = await Promise.all([ - DriveFile.findOne({ _id: user.avatarId }), - DriveFile.findOne({ _id: user.bannerId }) + const [avatar, banner, profile] = await Promise.all([ + user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined), + user.bannerId ? DriveFiles.findOne(user.bannerId) : Promise.resolve(undefined), + UserProfiles.findOne({ userId: user.id }).then(ensure) ]); const attachment: { @@ -26,41 +28,41 @@ export default async (user: ILocalUser) => { identifier?: IIdentifier }[] = []; - if (user.twitter) { + if (profile.twitter) { attachment.push({ type: 'PropertyValue', name: 'Twitter', - value: `<a href="https://twitter.com/intent/user?user_id=${user.twitter.userId}" rel="me nofollow noopener" target="_blank"><span>@${user.twitter.screenName}</span></a>`, + value: `<a href="https://twitter.com/intent/user?user_id=${profile.twitterUserId}" rel="me nofollow noopener" target="_blank"><span>@${profile.twitterScreenName}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:twitter', - value: `${user.twitter.userId}@${user.twitter.screenName}` + value: `${profile.twitterUserId}@${profile.twitterScreenName}` } }); } - if (user.github) { + if (profile.github) { attachment.push({ type: 'PropertyValue', name: 'GitHub', - value: `<a href="https://github.com/${user.github.login}" rel="me nofollow noopener" target="_blank"><span>@${user.github.login}</span></a>`, + value: `<a href="https://github.com/${profile.githubLogin}" rel="me nofollow noopener" target="_blank"><span>@${profile.githubLogin}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:github', - value: `${user.github.id}@${user.github.login}` + value: `${profile.githubId}@${profile.githubLogin}` } }); } - if (user.discord) { + if (profile.discord) { attachment.push({ type: 'PropertyValue', name: 'Discord', - value: `<a href="https://discordapp.com/users/${user.discord.id}" rel="me nofollow noopener" target="_blank"><span>${user.discord.username}#${user.discord.discriminator}</span></a>`, + value: `<a href="https://discordapp.com/users/${profile.discordId}" rel="me nofollow noopener" target="_blank"><span>${profile.discordUsername}#${profile.discordDiscriminator}</span></a>`, identifier: { type: 'PropertyValue', name: 'misskey:authentication:discord', - value: `${user.discord.id}@${user.discord.username}#${user.discord.discriminator}` + value: `${profile.discordId}@${profile.discordUsername}#${profile.discordDiscriminator}` } }); } @@ -75,6 +77,8 @@ export default async (user: ILocalUser) => { ...hashtagTags, ]; + const keypair = await UserKeypairs.findOne(user.id).then(ensure); + return { type: user.isBot ? 'Service' : 'Person', id, @@ -88,13 +92,13 @@ export default async (user: ILocalUser) => { url: `${config.url}/@${user.username}`, preferredUsername: user.username, name: user.name, - summary: toHtml(parse(user.description)), - icon: user.avatarId && renderImage(avatar), - image: user.bannerId && renderImage(banner), + summary: toHtml(parse(profile.description)), + icon: avatar ? renderImage(avatar) : null, + image: banner ? renderImage(banner) : null, tag, manuallyApprovesFollowers: user.isLocked, - publicKey: renderKey(user), + publicKey: renderKey(user, keypair), isCat: user.isCat, attachment: attachment.length ? attachment : undefined }; -}; +} diff --git a/src/remote/activitypub/renderer/question.ts b/src/remote/activitypub/renderer/question.ts index cf0bf387c8..6ade10d1bf 100644 --- a/src/remote/activitypub/renderer/question.ts +++ b/src/remote/activitypub/renderer/question.ts @@ -1,19 +1,20 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; -import { INote } from '../../../models/note'; +import { ILocalUser } from '../../../models/entities/user'; +import { Note } from '../../../models/entities/note'; +import { Poll } from '../../../models/entities/poll'; -export default async function renderQuestion(user: ILocalUser, note: INote) { +export default async function renderQuestion(user: ILocalUser, note: Note, poll: Poll) { const question = { type: 'Question', - id: `${config.url}/questions/${note._id}`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/questions/${note.id}`, + actor: `${config.url}/users/${user.id}`, content: note.text || '', - [note.poll.multiple ? 'anyOf' : 'oneOf']: note.poll.choices.map(c => ({ - name: c.text, - _misskey_votes: c.votes, + [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ + name: text, + _misskey_votes: poll.votes[i], replies: { type: 'Collection', - totalItems: c.votes + totalItems: poll.votes[i] } })) }; diff --git a/src/remote/activitypub/renderer/reject.ts b/src/remote/activitypub/renderer/reject.ts index 6d7d23708a..c4e0ba0d0a 100644 --- a/src/remote/activitypub/renderer/reject.ts +++ b/src/remote/activitypub/renderer/reject.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => ({ type: 'Reject', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts index ed840be751..1b9a6b8c05 100644 --- a/src/remote/activitypub/renderer/remove.ts +++ b/src/remote/activitypub/renderer/remove.ts @@ -1,9 +1,9 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (user: ILocalUser, target: any, object: any) => ({ type: 'Remove', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, target, object }); diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts index dbcf5732be..2ff6b61b90 100644 --- a/src/remote/activitypub/renderer/undo.ts +++ b/src/remote/activitypub/renderer/undo.ts @@ -1,8 +1,8 @@ import config from '../../../config'; -import { ILocalUser, IUser } from '../../../models/user'; +import { ILocalUser, User } from '../../../models/entities/user'; -export default (object: any, user: ILocalUser | IUser) => ({ +export default (object: any, user: ILocalUser | User) => ({ type: 'Undo', - actor: `${config.url}/users/${user._id}`, + actor: `${config.url}/users/${user.id}`, object }); diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts index cf9acc9acb..c1d5ba29b2 100644 --- a/src/remote/activitypub/renderer/update.ts +++ b/src/remote/activitypub/renderer/update.ts @@ -1,10 +1,10 @@ import config from '../../../config'; -import { ILocalUser } from '../../../models/user'; +import { ILocalUser } from '../../../models/entities/user'; export default (object: any, user: ILocalUser) => { const activity = { - id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`, + actor: `${config.url}/users/${user.id}`, type: 'Update', to: [ 'https://www.w3.org/ns/activitystreams#Public' ], object diff --git a/src/remote/activitypub/renderer/vote.ts b/src/remote/activitypub/renderer/vote.ts index 014b76765b..8929c03460 100644 --- a/src/remote/activitypub/renderer/vote.ts +++ b/src/remote/activitypub/renderer/vote.ts @@ -1,22 +1,23 @@ import config from '../../../config'; -import { INote } from '../../../models/note'; -import { IRemoteUser, ILocalUser } from '../../../models/user'; -import { IPollVote } from '../../../models/poll-vote'; +import { Note } from '../../../models/entities/note'; +import { IRemoteUser, ILocalUser } from '../../../models/entities/user'; +import { PollVote } from '../../../models/entities/poll-vote'; +import { Poll } from '../../../models/entities/poll'; -export default async function renderVote(user: ILocalUser, vote: IPollVote, pollNote: INote, pollOwner: IRemoteUser): Promise<any> { +export default async function renderVote(user: ILocalUser, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> { return { - id: `${config.url}/users/${user._id}#votes/${vote._id}/activity`, - actor: `${config.url}/users/${user._id}`, + id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`, + actor: `${config.url}/users/${user.id}`, type: 'Create', to: [pollOwner.uri], published: new Date().toISOString(), object: { - id: `${config.url}/users/${user._id}#votes/${vote._id}`, + id: `${config.url}/users/${user.id}#votes/${vote.id}`, type: 'Note', - attributedTo: `${config.url}/users/${user._id}`, + attributedTo: `${config.url}/users/${user.id}`, to: [pollOwner.uri], - inReplyTo: pollNote.uri, - name: pollNote.poll.choices.find(x => x.id === vote.choice).text + inReplyTo: note.uri, + name: poll.choices[vote.choice] } }; } diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index 08dd7a6ba9..897dd9acac 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -4,13 +4,15 @@ import { URL } from 'url'; import * as crypto from 'crypto'; import { lookup, IRunOptions } from 'lookup-dns-cache'; import * as promiseAny from 'promise-any'; -import { toUnicode } from 'punycode'; import config from '../../config'; -import { ILocalUser } from '../../models/user'; +import { ILocalUser } from '../../models/entities/user'; import { publishApLogStream } from '../../services/stream'; import { apLogger } from './logger'; -import Instance from '../../models/instance'; +import { UserKeypairs } from '../../models'; +import fetchMeta from '../../misc/fetch-meta'; +import { toPuny } from '../../misc/convert-host'; +import { ensure } from '../../prelude/ensure'; export const logger = apLogger.createSubLogger('deliver'); @@ -23,8 +25,8 @@ export default async (user: ILocalUser, url: string, object: any) => { // ブロックしてたら中断 // TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく - const instance = await Instance.findOne({ host: toUnicode(host) }); - if (instance && instance.isBlocked) return; + const meta = await fetchMeta(); + if (meta.blockedHosts.includes(toPuny(host))) return; const data = JSON.stringify(object); @@ -35,7 +37,11 @@ export default async (user: ILocalUser, url: string, object: any) => { const addr = await resolveAddr(hostname); if (!addr) return; - const _ = new Promise((resolve, reject) => { + const keypair = await UserKeypairs.findOne({ + userId: user.id + }).then(ensure); + + await new Promise((resolve, reject) => { const req = request({ protocol, hostname: addr, @@ -51,7 +57,7 @@ export default async (user: ILocalUser, url: string, object: any) => { 'Digest': `SHA-256=${hash}` } }, res => { - if (res.statusCode >= 400) { + if (res.statusCode! >= 400) { logger.warn(`${url} --> ${res.statusCode}`); reject(res); } else { @@ -62,13 +68,13 @@ export default async (user: ILocalUser, url: string, object: any) => { sign(req, { authorizationHeaderName: 'Signature', - key: user.keypair, - keyId: `${config.url}/users/${user._id}/publickey`, + key: keypair.privateKey, + keyId: `${config.url}/users/${user.id}/publickey`, headers: ['date', 'host', 'digest'] }); // Signature: Signature ... => Signature: ... - let sig = req.getHeader('Signature').toString(); + let sig = req.getHeader('Signature')!.toString(); sig = sig.replace(/^Signature /, ''); req.setHeader('Signature', sig); @@ -82,8 +88,6 @@ export default async (user: ILocalUser, url: string, object: any) => { req.end(data); }); - await _; - //#region Log publishApLogStream({ direction: 'out', @@ -107,7 +111,7 @@ async function resolveAddr(domain: string) { function resolveAddrInner(domain: string, options: IRunOptions = {}): Promise<string> { return new Promise((res, rej) => { - lookup(domain, options, (error: any, address: string | string[]) => { + lookup(domain, options, (error, address) => { if (error) return rej(error); return res(Array.isArray(address) ? address[0] : address); }); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 05152993e4..e8d0be638a 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -64,7 +64,7 @@ export default class Resolver { json: true }); - if (object === null || ( + if (object == null || ( Array.isArray(object['@context']) ? !object['@context'].includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index c381e63507..95c69fb8ac 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -12,7 +12,7 @@ export interface IObject { attachment?: any[]; inReplyTo?: any; replies?: ICollection; - content: string; + content?: string; name?: string; startTime?: Date; endTime?: Date; @@ -44,16 +44,16 @@ export interface IOrderedCollection extends IObject { export interface INote extends IObject { type: 'Note' | 'Question'; - _misskey_content: string; - _misskey_quote: string; - _misskey_question: string; + _misskey_content?: string; + _misskey_quote?: string; + _misskey_question?: string; } export interface IQuestion extends IObject { type: 'Note' | 'Question'; - _misskey_content: string; - _misskey_quote: string; - _misskey_question: string; + _misskey_content?: string; + _misskey_quote?: string; + _misskey_question?: string; oneOf?: IQuestionChoice[]; anyOf?: IQuestionChoice[]; endTime?: Date; @@ -65,6 +65,8 @@ interface IQuestionChoice { _misskey_votes?: number; } +export const validActor = ['Person', 'Service']; + export interface IPerson extends IObject { type: 'Person'; name: string; @@ -127,7 +129,7 @@ export interface IRemove extends IActivity { export interface ILike extends IActivity { type: 'Like'; - _misskey_reaction: string; + _misskey_reaction?: string; } export interface IAnnounce extends IActivity { |