wip
This commit is contained in:
		| @@ -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(); |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo