diff options
Diffstat (limited to 'src/remote/activitypub/objects')
| -rw-r--r-- | src/remote/activitypub/objects/image.ts | 29 | ||||
| -rw-r--r-- | src/remote/activitypub/objects/note.ts | 110 | ||||
| -rw-r--r-- | src/remote/activitypub/objects/person.ts | 142 |
3 files changed, 281 insertions, 0 deletions
diff --git a/src/remote/activitypub/objects/image.ts b/src/remote/activitypub/objects/image.ts new file mode 100644 index 0000000000..7f79fc5c06 --- /dev/null +++ b/src/remote/activitypub/objects/image.ts @@ -0,0 +1,29 @@ +import * as debug from 'debug'; + +import uploadFromUrl from '../../../services/drive/upload-from-url'; +import { IRemoteUser } from '../../../models/user'; +import { IDriveFile } from '../../../models/drive-file'; + +const log = debug('misskey:activitypub'); + +/** + * Imageを作成します。 + */ +export async function createImage(actor: IRemoteUser, image): Promise<IDriveFile> { + log(`Creating the Image: ${image.url}`); + + return await uploadFromUrl(image.url, actor); +} + +/** + * Imageを解決します。 + * + * Misskeyに対象のImageが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> { + // TODO + + // リモートサーバーからフェッチしてきて登録 + return await createImage(actor, value); +} diff --git a/src/remote/activitypub/objects/note.ts b/src/remote/activitypub/objects/note.ts new file mode 100644 index 0000000000..3edcb8c63f --- /dev/null +++ b/src/remote/activitypub/objects/note.ts @@ -0,0 +1,110 @@ +import { JSDOM } from 'jsdom'; +import * as debug from 'debug'; + +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 } from './person'; +import { resolveImage } from './image'; +import { IRemoteUser } from '../../../models/user'; + +const log = debug('misskey:activitypub'); + +/** + * Noteをフェッチします。 + * + * Misskeyに対象のNoteが登録されていればそれを返します。 + */ +export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(config.url + '/')) { + return await Note.findOne({ _id: uri.split('/').pop() }); + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await Note.findOne({ uri }); + + if (exist) { + return exist; + } + //#endregion + + return null; +} + +/** + * Noteを作成します。 + */ +export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> { + if (resolver == null) resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; + + if (object == null || object.type !== 'Note') { + throw new Error('invalid note'); + } + + const note: INoteActivityStreamsObject = object; + + log(`Creating the Note: ${note.id}`); + + // 投稿者をフェッチ + const actor = await resolvePerson(note.attributedTo) as IRemoteUser; + + //#region Visibility + let visibility = 'public'; + if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (note.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + // 添付メディア + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + const media = note.attachment + ? await Promise.all(note.attachment.map(x => resolveImage(actor, x))) + : []; + + // リプライ + const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; + + const { window } = new JSDOM(note.content); + + return await post(actor, { + createdAt: new Date(note.published), + media, + reply, + renote: undefined, + text: window.document.body.textContent, + viaMobile: false, + geo: undefined, + visibility, + uri: note.id + }, silent); +} + +/** + * Noteを解決します。 + * + * Misskeyに対象のNoteが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> { + const uri = typeof value == 'string' ? value : value.id; + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await fetchNote(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + return await createNote(value, resolver); +} diff --git a/src/remote/activitypub/objects/person.ts b/src/remote/activitypub/objects/person.ts new file mode 100644 index 0000000000..b1e8c9ee0a --- /dev/null +++ b/src/remote/activitypub/objects/person.ts @@ -0,0 +1,142 @@ +import { JSDOM } from 'jsdom'; +import { toUnicode } from 'punycode'; +import * as debug from 'debug'; + +import config from '../../../config'; +import User, { validateUsername, isValidName, isValidDescription, IUser, IRemoteUser } from '../../../models/user'; +import webFinger from '../../webfinger'; +import Resolver from '../resolver'; +import { resolveImage } from './image'; +import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; + +const log = debug('misskey:activitypub'); + +/** + * Personをフェッチします。 + * + * Misskeyに対象のPersonが登録されていればそれを返します。 + */ +export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise<IUser> { + const uri = typeof value == 'string' ? value : value.id; + + // URIがこのサーバーを指しているならデータベースからフェッチ + if (uri.startsWith(config.url + '/')) { + return await User.findOne({ _id: uri.split('/').pop() }); + } + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await User.findOne({ uri }); + + if (exist) { + return exist; + } + //#endregion + + return null; +} + +/** + * Personを作成します。 + */ +export async function createPerson(value: any, resolver?: Resolver): Promise<IUser> { + if (resolver == null) resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; + + if ( + object == null || + object.type !== 'Person' || + typeof object.preferredUsername !== 'string' || + !validateUsername(object.preferredUsername) || + !isValidName(object.name == '' ? null : object.name) || + !isValidDescription(object.summary) + ) { + throw new Error('invalid person'); + } + + const person: IPerson = object; + + log(`Creating the Person: ${person.id}`); + + const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = 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 + ), + webFinger(person.id) + ]); + + const host = toUnicode(finger.subject.replace(/^.*?@/, '')); + const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase()); + const summaryDOM = JSDOM.fragment(person.summary); + + // Create user + const user = await User.insert({ + avatarId: null, + bannerId: null, + createdAt: Date.parse(person.published) || null, + description: summaryDOM.textContent, + followersCount, + followingCount, + notesCount, + name: person.name, + driveCapacity: 1024 * 1024 * 8, // 8MiB + username: person.preferredUsername, + usernameLower: person.preferredUsername.toLowerCase(), + host, + hostLower, + publicKey: { + id: person.publicKey.id, + publicKeyPem: person.publicKey.publicKeyPem + }, + inbox: person.inbox, + uri: person.id + }) as IRemoteUser; + + //#region アイコンとヘッダー画像をフェッチ + const [avatarId, bannerId] = (await Promise.all([ + person.icon, + person.image + ].map(img => + img == null + ? Promise.resolve(null) + : resolveImage(user, img.url) + ))).map(file => file != null ? file._id : null); + + User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); + + user.avatarId = avatarId; + user.bannerId = bannerId; + //#endregion + + return user; +} + +/** + * Personを解決します。 + * + * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ + * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 + */ +export async function resolvePerson(value: string | IObject, verifier?: string): Promise<IUser> { + const uri = typeof value == 'string' ? value : value.id; + + //#region このサーバーに既に登録されていたらそれを返す + const exist = await fetchPerson(uri); + + if (exist) { + return exist; + } + //#endregion + + // リモートサーバーからフェッチしてきて登録 + return await createPerson(value); +} |