wip
This commit is contained in:
		
							parent
							
								
									3ef82ccb62
								
							
						
					
					
						commit
						08beb45935
					
				
					 22 changed files with 332 additions and 241 deletions
				
			
		| 
						 | 
					@ -4,8 +4,8 @@ import * as debug from 'debug';
 | 
				
			||||||
import { verifySignature } from 'http-signature';
 | 
					import { verifySignature } from 'http-signature';
 | 
				
			||||||
import parseAcct from '../../../acct/parse';
 | 
					import parseAcct from '../../../acct/parse';
 | 
				
			||||||
import User, { IRemoteUser } from '../../../models/user';
 | 
					import User, { IRemoteUser } from '../../../models/user';
 | 
				
			||||||
import act from '../../../remote/activitypub/act';
 | 
					import perform from '../../../remote/activitypub/perform';
 | 
				
			||||||
import resolvePerson from '../../../remote/activitypub/resolve-person';
 | 
					import { resolvePerson } from '../../../remote/activitypub/objects/person';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = debug('misskey:queue:inbox');
 | 
					const log = debug('misskey:queue:inbox');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,7 +58,7 @@ export default async (job: kue.Job, done): Promise<void> => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// アクティビティを処理
 | 
						// アクティビティを処理
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		await act(user, activity);
 | 
							await perform(user, activity);
 | 
				
			||||||
		done();
 | 
							done();
 | 
				
			||||||
	} catch (e) {
 | 
						} catch (e) {
 | 
				
			||||||
		done(e);
 | 
							done(e);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,18 +0,0 @@
 | 
				
			||||||
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');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default async function(actor: IRemoteUser, image): Promise<IDriveFile> {
 | 
					 | 
				
			||||||
	if ('attributedTo' in image && actor.uri !== image.attributedTo) {
 | 
					 | 
				
			||||||
		log(`invalid image: ${JSON.stringify(image, null, 2)}`);
 | 
					 | 
				
			||||||
		throw new Error('invalid image');
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	log(`Creating the Image: ${image.url}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return await uploadFromUrl(image.url, actor);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,87 +0,0 @@
 | 
				
			||||||
import { JSDOM } from 'jsdom';
 | 
					 | 
				
			||||||
import * as debug from 'debug';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import Resolver from '../../resolver';
 | 
					 | 
				
			||||||
import Note, { INote } from '../../../../models/note';
 | 
					 | 
				
			||||||
import post from '../../../../services/note/create';
 | 
					 | 
				
			||||||
import { IRemoteUser } from '../../../../models/user';
 | 
					 | 
				
			||||||
import resolvePerson from '../../resolve-person';
 | 
					 | 
				
			||||||
import createImage from './image';
 | 
					 | 
				
			||||||
import config from '../../../../config';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const log = debug('misskey:activitypub');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 投稿作成アクティビティを捌きます
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<INote> {
 | 
					 | 
				
			||||||
	if (typeof note.id !== 'string') {
 | 
					 | 
				
			||||||
		log(`invalid note: ${JSON.stringify(note, null, 2)}`);
 | 
					 | 
				
			||||||
		throw new Error('invalid note');
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 既に同じURIを持つものが登録されていないかチェックし、登録されていたらそれを返す
 | 
					 | 
				
			||||||
	const exist = await Note.findOne({ uri: note.id });
 | 
					 | 
				
			||||||
	if (exist) {
 | 
					 | 
				
			||||||
		return exist;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	log(`Creating the Note: ${note.id}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	//#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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	//#region 添付メディア
 | 
					 | 
				
			||||||
	let media = [];
 | 
					 | 
				
			||||||
	if ('attachment' in note && note.attachment != null) {
 | 
					 | 
				
			||||||
		// TODO: attachmentは必ずしもImageではない
 | 
					 | 
				
			||||||
		// TODO: attachmentは必ずしも配列ではない
 | 
					 | 
				
			||||||
		media = await Promise.all(note.attachment.map(x => {
 | 
					 | 
				
			||||||
			return createImage(actor, x);
 | 
					 | 
				
			||||||
		}));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	//#endregion
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	//#region リプライ
 | 
					 | 
				
			||||||
	let reply = null;
 | 
					 | 
				
			||||||
	if ('inReplyTo' in note && note.inReplyTo != null) {
 | 
					 | 
				
			||||||
		// リプライ先の投稿がMisskeyに登録されているか調べる
 | 
					 | 
				
			||||||
		const uri: string = note.inReplyTo.id || note.inReplyTo;
 | 
					 | 
				
			||||||
		const inReplyToNote = uri.startsWith(config.url + '/')
 | 
					 | 
				
			||||||
			? await Note.findOne({ _id: uri.split('/').pop() })
 | 
					 | 
				
			||||||
			: await Note.findOne({ uri });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (inReplyToNote) {
 | 
					 | 
				
			||||||
			reply = inReplyToNote;
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// 無かったらフェッチ
 | 
					 | 
				
			||||||
			const inReplyTo = await resolver.resolve(note.inReplyTo) as any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// リプライ先の投稿の投稿者をフェッチ
 | 
					 | 
				
			||||||
			const actor = await resolvePerson(inReplyTo.attributedTo) as IRemoteUser;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// TODO: silentを常にtrueにしてはならない
 | 
					 | 
				
			||||||
			reply = await createNote(resolver, actor, inReplyTo);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	//#endregion
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	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
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										29
									
								
								src/remote/activitypub/objects/image.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/remote/activitypub/objects/image.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										110
									
								
								src/remote/activitypub/objects/note.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/remote/activitypub/objects/note.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										142
									
								
								src/remote/activitypub/objects/person.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/remote/activitypub/objects/person.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,10 @@
 | 
				
			||||||
import * as debug from 'debug';
 | 
					import * as debug from 'debug';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Resolver from '../../resolver';
 | 
					import Resolver from '../../resolver';
 | 
				
			||||||
import Note from '../../../../models/note';
 | 
					 | 
				
			||||||
import post from '../../../../services/note/create';
 | 
					import post from '../../../../services/note/create';
 | 
				
			||||||
import { IRemoteUser, isRemoteUser } from '../../../../models/user';
 | 
					import { IRemoteUser } from '../../../../models/user';
 | 
				
			||||||
import { IAnnounce, INote } from '../../type';
 | 
					import { IAnnounce, INote } from '../../type';
 | 
				
			||||||
import createNote from '../create/note';
 | 
					import { fetchNote, resolveNote } from '../../objects/note';
 | 
				
			||||||
import resolvePerson from '../../resolve-person';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = debug('misskey:activitypub');
 | 
					const log = debug('misskey:activitypub');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,17 +19,12 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 既に同じURIを持つものが登録されていないかチェック
 | 
						// 既に同じURIを持つものが登録されていないかチェック
 | 
				
			||||||
	const exist = await Note.findOne({ uri });
 | 
						const exist = await fetchNote(uri);
 | 
				
			||||||
	if (exist) {
 | 
						if (exist) {
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// アナウンス元の投稿の投稿者をフェッチ
 | 
						const renote = await resolveNote(note);
 | 
				
			||||||
	const announcee = await resolvePerson(note.attributedTo);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const renote = isRemoteUser(announcee)
 | 
					 | 
				
			||||||
		? await createNote(resolver, announcee, note, true)
 | 
					 | 
				
			||||||
		: await Note.findOne({ _id: note.id.split('/').pop() });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log(`Creating the (Re)Note: ${uri}`);
 | 
						log(`Creating the (Re)Note: ${uri}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/remote/activitypub/perform/create/image.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/remote/activitypub/perform/create/image.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					import { IRemoteUser } from '../../../../models/user';
 | 
				
			||||||
 | 
					import { createImage } from '../../objects/image';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function(actor: IRemoteUser, image): Promise<void> {
 | 
				
			||||||
 | 
						await createImage(image.url, actor);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/remote/activitypub/perform/create/note.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/remote/activitypub/perform/create/note.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					import Resolver from '../../resolver';
 | 
				
			||||||
 | 
					import { IRemoteUser } from '../../../../models/user';
 | 
				
			||||||
 | 
					import { createNote, fetchNote } from '../../objects/note';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 投稿作成アクティビティを捌きます
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export default async function(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<void> {
 | 
				
			||||||
 | 
						const exist = await fetchNote(note);
 | 
				
			||||||
 | 
						if (exist == null) {
 | 
				
			||||||
 | 
							await createNote(note);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,98 +0,0 @@
 | 
				
			||||||
import { JSDOM } from 'jsdom';
 | 
					 | 
				
			||||||
import { toUnicode } from 'punycode';
 | 
					 | 
				
			||||||
import config from '../../config';
 | 
					 | 
				
			||||||
import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user';
 | 
					 | 
				
			||||||
import webFinger from '../webfinger';
 | 
					 | 
				
			||||||
import Resolver from './resolver';
 | 
					 | 
				
			||||||
import uploadFromUrl from '../../services/drive/upload-from-url';
 | 
					 | 
				
			||||||
import { isCollectionOrOrderedCollection, IObject } from './type';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default async (value: string | IObject, verifier?: string): Promise<IUser> => {
 | 
					 | 
				
			||||||
	const id = typeof value == 'string' ? value : value.id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (id.startsWith(config.url + '/')) {
 | 
					 | 
				
			||||||
		return await User.findOne({ _id: id.split('/').pop() });
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		const exist = await User.findOne({
 | 
					 | 
				
			||||||
			uri: id
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (exist) {
 | 
					 | 
				
			||||||
			return exist;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const 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 [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([
 | 
					 | 
				
			||||||
		resolver.resolve(object.followers).then(
 | 
					 | 
				
			||||||
			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
 | 
					 | 
				
			||||||
			() => undefined
 | 
					 | 
				
			||||||
		),
 | 
					 | 
				
			||||||
		resolver.resolve(object.following).then(
 | 
					 | 
				
			||||||
			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
 | 
					 | 
				
			||||||
			() => undefined
 | 
					 | 
				
			||||||
		),
 | 
					 | 
				
			||||||
		resolver.resolve(object.outbox).then(
 | 
					 | 
				
			||||||
			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
 | 
					 | 
				
			||||||
			() => undefined
 | 
					 | 
				
			||||||
		),
 | 
					 | 
				
			||||||
		webFinger(id, verifier)
 | 
					 | 
				
			||||||
	]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const host = toUnicode(finger.subject.replace(/^.*?@/, ''));
 | 
					 | 
				
			||||||
	const hostLower = host.replace(/[A-Z]+/, matched => matched.toLowerCase());
 | 
					 | 
				
			||||||
	const summaryDOM = JSDOM.fragment(object.summary);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create user
 | 
					 | 
				
			||||||
	const user = await User.insert({
 | 
					 | 
				
			||||||
		avatarId: null,
 | 
					 | 
				
			||||||
		bannerId: null,
 | 
					 | 
				
			||||||
		createdAt: Date.parse(object.published) || null,
 | 
					 | 
				
			||||||
		description: summaryDOM.textContent,
 | 
					 | 
				
			||||||
		followersCount,
 | 
					 | 
				
			||||||
		followingCount,
 | 
					 | 
				
			||||||
		notesCount,
 | 
					 | 
				
			||||||
		name: object.name,
 | 
					 | 
				
			||||||
		driveCapacity: 1024 * 1024 * 8, // 8MiB
 | 
					 | 
				
			||||||
		username: object.preferredUsername,
 | 
					 | 
				
			||||||
		usernameLower: object.preferredUsername.toLowerCase(),
 | 
					 | 
				
			||||||
		host,
 | 
					 | 
				
			||||||
		hostLower,
 | 
					 | 
				
			||||||
		publicKey: {
 | 
					 | 
				
			||||||
			id: object.publicKey.id,
 | 
					 | 
				
			||||||
			publicKeyPem: object.publicKey.publicKeyPem
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		inbox: object.inbox,
 | 
					 | 
				
			||||||
		uri: id
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const [avatarId, bannerId] = (await Promise.all([
 | 
					 | 
				
			||||||
		object.icon,
 | 
					 | 
				
			||||||
		object.image
 | 
					 | 
				
			||||||
	].map(img =>
 | 
					 | 
				
			||||||
		img == null
 | 
					 | 
				
			||||||
			? Promise.resolve(null)
 | 
					 | 
				
			||||||
			: uploadFromUrl(img.url, user)
 | 
					 | 
				
			||||||
	))).map(file => file != null ? file._id : null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	user.avatarId = avatarId;
 | 
					 | 
				
			||||||
	user.bannerId = bannerId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return user;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,11 @@ export interface IObject {
 | 
				
			||||||
	cc?: string[];
 | 
						cc?: string[];
 | 
				
			||||||
	to?: string[];
 | 
						to?: string[];
 | 
				
			||||||
	attributedTo: string;
 | 
						attributedTo: string;
 | 
				
			||||||
 | 
						attachment?: any[];
 | 
				
			||||||
 | 
						inReplyTo?: any;
 | 
				
			||||||
 | 
						content: string;
 | 
				
			||||||
 | 
						icon?: any;
 | 
				
			||||||
 | 
						image?: any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IActivity extends IObject {
 | 
					export interface IActivity extends IObject {
 | 
				
			||||||
| 
						 | 
					@ -34,6 +39,17 @@ export interface INote extends IObject {
 | 
				
			||||||
	type: 'Note';
 | 
						type: 'Note';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IPerson extends IObject {
 | 
				
			||||||
 | 
						type: 'Person';
 | 
				
			||||||
 | 
						name: string;
 | 
				
			||||||
 | 
						preferredUsername: string;
 | 
				
			||||||
 | 
						inbox: string;
 | 
				
			||||||
 | 
						publicKey: any;
 | 
				
			||||||
 | 
						followers: any;
 | 
				
			||||||
 | 
						following: any;
 | 
				
			||||||
 | 
						outbox: any;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const isCollection = (object: IObject): object is ICollection =>
 | 
					export const isCollection = (object: IObject): object is ICollection =>
 | 
				
			||||||
	object.type === 'Collection';
 | 
						object.type === 'Collection';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
import { toUnicode, toASCII } from 'punycode';
 | 
					import { toUnicode, toASCII } from 'punycode';
 | 
				
			||||||
import User from '../models/user';
 | 
					import User from '../models/user';
 | 
				
			||||||
import resolvePerson from './activitypub/resolve-person';
 | 
					 | 
				
			||||||
import webFinger from './webfinger';
 | 
					import webFinger from './webfinger';
 | 
				
			||||||
import config from '../config';
 | 
					import config from '../config';
 | 
				
			||||||
 | 
					import { createPerson } from './activitypub/objects/person';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async (username, host, option) => {
 | 
					export default async (username, host, option) => {
 | 
				
			||||||
	const usernameLower = username.toLowerCase();
 | 
						const usernameLower = username.toLowerCase();
 | 
				
			||||||
| 
						 | 
					@ -18,13 +18,13 @@ export default async (username, host, option) => {
 | 
				
			||||||
	if (user === null) {
 | 
						if (user === null) {
 | 
				
			||||||
		const acctLower = `${usernameLower}@${hostLowerAscii}`;
 | 
							const acctLower = `${usernameLower}@${hostLowerAscii}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const finger = await webFinger(acctLower, acctLower);
 | 
							const finger = await webFinger(acctLower);
 | 
				
			||||||
		const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
 | 
							const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self');
 | 
				
			||||||
		if (!self) {
 | 
							if (!self) {
 | 
				
			||||||
			throw new Error('self link not found');
 | 
								throw new Error('self link not found');
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		user = await resolvePerson(self.href, acctLower);
 | 
							user = await createPerson(self.href);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return user;
 | 
						return user;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,36 +3,21 @@ const WebFinger = require('webfinger.js');
 | 
				
			||||||
const webFinger = new WebFinger({ });
 | 
					const webFinger = new WebFinger({ });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ILink = {
 | 
					type ILink = {
 | 
				
			||||||
  href: string;
 | 
						href: string;
 | 
				
			||||||
  rel: string;
 | 
						rel: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type IWebFinger = {
 | 
					type IWebFinger = {
 | 
				
			||||||
  links: ILink[];
 | 
						links: ILink[];
 | 
				
			||||||
  subject: string;
 | 
						subject: string;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async function resolve(query, verifier?: string): Promise<IWebFinger> {
 | 
					export default async function resolve(query): Promise<IWebFinger> {
 | 
				
			||||||
	const finger = await new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
 | 
						return await new Promise((res, rej) => webFinger.lookup(query, (error, result) => {
 | 
				
			||||||
		if (error) {
 | 
							if (error) {
 | 
				
			||||||
			return rej(error);
 | 
								return rej(error);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		res(result.object);
 | 
							res(result.object);
 | 
				
			||||||
	})) as IWebFinger;
 | 
						})) as IWebFinger;
 | 
				
			||||||
	const subject = finger.subject.toLowerCase().replace(/^acct:/, '');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (typeof verifier === 'string') {
 | 
					 | 
				
			||||||
		if (subject !== verifier) {
 | 
					 | 
				
			||||||
			throw new Error();
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return finger;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (typeof subject === 'string') {
 | 
					 | 
				
			||||||
		return resolve(subject, subject);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	throw new Error();
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue