Fix: AP object / actor type (#5086)
* attributedToがArrayの場合などに対応 * attachment以外で来るDocument系のObjectに対応 * Renote, Reply 対応 * 表示をいい感じに * fix type * revert as const * Fix Note / Question type * attributedToのtypeで複合配列を想定する
This commit is contained in:
		
							parent
							
								
									a8379e3bc9
								
							
						
					
					
						commit
						0141affe05
					
				
					 10 changed files with 86 additions and 87 deletions
				
			
		|  | @ -144,8 +144,8 @@ export class NoteRepository extends Repository<Note> { | |||
| 
 | ||||
| 		let text = note.text; | ||||
| 
 | ||||
| 		if (note.name) { | ||||
| 			text = `【${note.name}】\n${note.text}`; | ||||
| 		if (note.name && note.uri) { | ||||
| 			text = `【${note.name}】\n${(note.text || '').trim()}\n${note.uri}`; | ||||
| 		} | ||||
| 
 | ||||
| 		const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)])); | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| import Resolver from '../../resolver'; | ||||
| import { IRemoteUser } from '../../../../models/entities/user'; | ||||
| import announceNote from './note'; | ||||
| import { IAnnounce, INote } from '../../type'; | ||||
| import { IAnnounce, INote, validPost, getApId } from '../../type'; | ||||
| import { apLogger } from '../../logger'; | ||||
| 
 | ||||
| const logger = apLogger; | ||||
| 
 | ||||
| export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => { | ||||
| 	const uri = activity.id || activity; | ||||
| 	const uri = getApId(activity); | ||||
| 
 | ||||
| 	logger.info(`Announce: ${uri}`); | ||||
| 
 | ||||
|  | @ -22,15 +22,9 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => | |||
| 		throw e; | ||||
| 	} | ||||
| 
 | ||||
| 	switch (object.type) { | ||||
| 	case 'Note': | ||||
| 	case 'Question': | ||||
| 	case 'Article': | ||||
| 	if (validPost.includes(object.type)) { | ||||
| 		announceNote(resolver, actor, activity, object as INote); | ||||
| 		break; | ||||
| 
 | ||||
| 	default: | ||||
| 	} else { | ||||
| 		logger.warn(`Unknown announce type: ${object.type}`); | ||||
| 		break; | ||||
| 	} | ||||
| }; | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import Resolver from '../../resolver'; | ||||
| import post from '../../../../services/note/create'; | ||||
| import { IRemoteUser, User } from '../../../../models/entities/user'; | ||||
| import { IAnnounce, INote } from '../../type'; | ||||
| import { IAnnounce, INote, getApId, getApIds } from '../../type'; | ||||
| import { fetchNote, resolveNote } from '../../models/note'; | ||||
| import { resolvePerson } from '../../models/person'; | ||||
| import { apLogger } from '../../logger'; | ||||
|  | @ -14,17 +14,13 @@ const logger = apLogger; | |||
|  * アナウンスアクティビティを捌きます | ||||
|  */ | ||||
| export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> { | ||||
| 	const uri = activity.id || activity; | ||||
| 	const uri = getApId(activity); | ||||
| 
 | ||||
| 	// アナウンサーが凍結されていたらスキップ
 | ||||
| 	if (actor.isSuspended) { | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	if (typeof uri !== 'string') { | ||||
| 		throw new Error('invalid announce'); | ||||
| 	} | ||||
| 
 | ||||
| 	// アナウンス先をブロックしてたら中断
 | ||||
| 	const meta = await fetchMeta(); | ||||
| 	if (meta.blockedHosts.includes(extractDbHost(uri))) return; | ||||
|  | @ -52,11 +48,14 @@ 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 to = getApIds(activity.to); | ||||
| 	const cc = getApIds(activity.cc); | ||||
| 
 | ||||
| 	const visibility = getVisibility(to, cc, actor); | ||||
| 
 | ||||
| 	let visibleUsers: User[] = []; | ||||
| 	if (visibility == 'specified') { | ||||
| 		visibleUsers = await Promise.all((note.to || []).map(uri => resolvePerson(uri))); | ||||
| 		visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri))); | ||||
| 	} | ||||
| 	//#endergion
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +0,0 @@ | |||
| import { IRemoteUser } from '../../../../models/entities/user'; | ||||
| import { createImage } from '../../models/image'; | ||||
| 
 | ||||
| export default async function(actor: IRemoteUser, image: any): Promise<void> { | ||||
| 	await createImage(image.url, actor); | ||||
| } | ||||
|  | @ -1,14 +1,13 @@ | |||
| import Resolver from '../../resolver'; | ||||
| import { IRemoteUser } from '../../../../models/entities/user'; | ||||
| import createImage from './image'; | ||||
| import createNote from './note'; | ||||
| import { ICreate } from '../../type'; | ||||
| import { ICreate, getApId, validPost } from '../../type'; | ||||
| import { apLogger } from '../../logger'; | ||||
| 
 | ||||
| const logger = apLogger; | ||||
| 
 | ||||
| export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => { | ||||
| 	const uri = activity.id || activity; | ||||
| 	const uri = getApId(activity); | ||||
| 
 | ||||
| 	logger.info(`Create: ${uri}`); | ||||
| 
 | ||||
|  | @ -23,19 +22,9 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => { | |||
| 		throw e; | ||||
| 	} | ||||
| 
 | ||||
| 	switch (object.type) { | ||||
| 	case 'Image': | ||||
| 		createImage(actor, object); | ||||
| 		break; | ||||
| 
 | ||||
| 	case 'Note': | ||||
| 	case 'Question': | ||||
| 	case 'Article': | ||||
| 	if (validPost.includes(object.type)) { | ||||
| 		createNote(resolver, actor, object); | ||||
| 		break; | ||||
| 
 | ||||
| 	default: | ||||
| 	} else { | ||||
| 		logger.warn(`Unknown type: ${object.type}`); | ||||
| 		break; | ||||
| 	} | ||||
| }; | ||||
|  |  | |||
|  | @ -1,9 +1,8 @@ | |||
| import Resolver from '../../resolver'; | ||||
| import deleteNote from './note'; | ||||
| import { IRemoteUser } from '../../../../models/entities/user'; | ||||
| import { IDelete } from '../../type'; | ||||
| import { IDelete, getApId, validPost } from '../../type'; | ||||
| import { apLogger } from '../../logger'; | ||||
| import { Notes } from '../../../../models'; | ||||
| 
 | ||||
| /** | ||||
|  * 削除アクティビティを捌きます | ||||
|  | @ -17,24 +16,11 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => { | |||
| 
 | ||||
| 	const object = await resolver.resolve(activity.object); | ||||
| 
 | ||||
| 	const uri = (object as any).id; | ||||
| 	const uri = getApId(object); | ||||
| 
 | ||||
| 	switch (object.type) { | ||||
| 		case 'Note': | ||||
| 		case 'Question': | ||||
| 		case 'Article': | ||||
| 			deleteNote(actor, uri); | ||||
| 			break; | ||||
| 
 | ||||
| 		case 'Tombstone': | ||||
| 			const note = await Notes.findOne({ uri }); | ||||
| 			if (note != null) { | ||||
| 				deleteNote(actor, uri); | ||||
| 			} | ||||
| 			break; | ||||
| 
 | ||||
| 		default: | ||||
| 			apLogger.warn(`Unknown type: ${object.type}`); | ||||
| 			break; | ||||
| 	if (validPost.includes(object.type) || object.type === 'Tombstone') { | ||||
| 		deleteNote(actor, uri); | ||||
| 	} else { | ||||
| 		apLogger.warn(`Unknown type: ${object.type}`); | ||||
| 	} | ||||
| }; | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update'; | |||
| 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 { IObject, INote, getApIds, getOneApId, getApId, validPost } from '../type'; | ||||
| import { Emoji } from '../../../models/entities/emoji'; | ||||
| import { genId } from '../../../misc/gen-id'; | ||||
| import { fetchMeta } from '../../../misc/fetch-meta'; | ||||
|  | @ -32,7 +32,7 @@ export function validateNote(object: any, uri: string) { | |||
| 		return new Error('invalid Note: object is null'); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!['Note', 'Question', 'Article'].includes(object.type)) { | ||||
| 	if (!validPost.includes(object.type)) { | ||||
| 		return new Error(`invalid Note: invalied object type ${object.type}`); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -40,7 +40,7 @@ export function validateNote(object: any, uri: string) { | |||
| 		return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractDbHost(object.id)}`); | ||||
| 	} | ||||
| 
 | ||||
| 	if (object.attributedTo && extractDbHost(object.attributedTo) !== expectHost) { | ||||
| 	if (object.attributedTo && extractDbHost(getOneApId(object.attributedTo)) !== expectHost) { | ||||
| 		return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractDbHost(object.attributedTo)}`); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -53,8 +53,7 @@ export function validateNote(object: any, uri: string) { | |||
|  * Misskeyに対象のNoteが登録されていればそれを返します。 | ||||
|  */ | ||||
| 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'); | ||||
| 	const uri = getApId(value); | ||||
| 
 | ||||
| 	// URIがこのサーバーを指しているならデータベースからフェッチ
 | ||||
| 	if (uri.startsWith(config.url + '/')) { | ||||
|  | @ -76,12 +75,12 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P | |||
| /** | ||||
|  * Noteを作成します。 | ||||
|  */ | ||||
| export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note | null> { | ||||
| export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> { | ||||
| 	if (resolver == null) resolver = new Resolver(); | ||||
| 
 | ||||
| 	const object: any = await resolver.resolve(value); | ||||
| 
 | ||||
| 	const entryUri = value.id || value; | ||||
| 	const entryUri = getApId(value); | ||||
| 	const err = validateNote(object, entryUri); | ||||
| 	if (err) { | ||||
| 		logger.error(`${err.message}`, { | ||||
|  | @ -101,7 +100,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false | |||
| 	logger.info(`Creating the Note: ${note.id}`); | ||||
| 
 | ||||
| 	// 投稿者をフェッチ
 | ||||
| 	const actor = await resolvePerson(note.attributedTo, resolver) as IRemoteUser; | ||||
| 	const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser; | ||||
| 
 | ||||
| 	// 投稿者が凍結されていたらスキップ
 | ||||
| 	if (actor.isSuspended) { | ||||
|  | @ -109,24 +108,24 @@ export async function createNote(value: any, resolver?: Resolver, silent = false | |||
| 	} | ||||
| 
 | ||||
| 	//#region Visibility
 | ||||
| 	note.to = note.to == null ? [] : typeof note.to == 'string' ? [note.to] : note.to; | ||||
| 	note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc; | ||||
| 	const to = getApIds(note.to); | ||||
| 	const cc = getApIds(note.cc); | ||||
| 
 | ||||
| 	let visibility = 'public'; | ||||
| 	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')) { | ||||
| 	if (!to.includes('https://www.w3.org/ns/activitystreams#Public')) { | ||||
| 		if (cc.includes('https://www.w3.org/ns/activitystreams#Public')) { | ||||
| 			visibility = 'home'; | ||||
| 		} else if (note.to.includes(`${actor.uri}/followers`)) {	// TODO: person.followerと照合するべき?
 | ||||
| 		} else if (to.includes(`${actor.uri}/followers`)) {	// TODO: person.followerと照合するべき?
 | ||||
| 			visibility = 'followers'; | ||||
| 		} else { | ||||
| 			visibility = 'specified'; | ||||
| 			visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, resolver))); | ||||
| 			visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri, resolver))); | ||||
| 		} | ||||
| 	} | ||||
| 	//#endergion
 | ||||
| 
 | ||||
| 	const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver); | ||||
| 	const apMentions = await extractMentionedUsers(actor, to, cc, resolver); | ||||
| 
 | ||||
| 	const apHashtags = await extractHashtags(note.tag); | ||||
| 
 | ||||
|  | @ -217,11 +216,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false | |||
| 	const apEmojis = emojis.map(emoji => emoji.name); | ||||
| 
 | ||||
| 	const questionUri = note._misskey_question; | ||||
| 	const poll = await extractPollFromQuestion(note._misskey_question || note).catch(() => undefined); | ||||
| 	const poll = await extractPollFromQuestion(note._misskey_question || note, resolver).catch(() => undefined); | ||||
| 
 | ||||
| 	// ユーザーの情報が古かったらついでに更新しておく
 | ||||
| 	if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { | ||||
| 		updatePerson(note.attributedTo); | ||||
| 		if (actor.uri) updatePerson(actor.uri); | ||||
| 	} | ||||
| 
 | ||||
| 	return await post(actor, { | ||||
|  |  | |||
|  | @ -1,12 +1,19 @@ | |||
| import config from '../../../config'; | ||||
| import Resolver from '../resolver'; | ||||
| import { IQuestion } from '../type'; | ||||
| import { IObject, IQuestion, isQuestion,  } 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; | ||||
| export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> { | ||||
| 	if (resolver == null) resolver = new Resolver(); | ||||
| 
 | ||||
| 	const question = await resolver.resolve(source); | ||||
| 
 | ||||
| 	if (!isQuestion(question)) { | ||||
| 		throw new Error('invalid type'); | ||||
| 	} | ||||
| 
 | ||||
| 	const multiple = !question.oneOf; | ||||
| 	const expiresAt = question.endTime ? new Date(question.endTime) : null; | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,9 +6,9 @@ export interface IObject { | |||
| 	id?: string; | ||||
| 	summary?: string; | ||||
| 	published?: string; | ||||
| 	cc?: string[]; | ||||
| 	to?: string[]; | ||||
| 	attributedTo: string; | ||||
| 	cc?: IObject | string | (IObject | string)[]; | ||||
| 	to?: IObject | string | (IObject | string)[]; | ||||
| 	attributedTo: IObject | string | (IObject | string)[]; | ||||
| 	attachment?: any[]; | ||||
| 	inReplyTo?: any; | ||||
| 	replies?: ICollection; | ||||
|  | @ -23,6 +23,32 @@ export interface IObject { | |||
| 	sensitive?: boolean; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Get array of ActivityStreams Objects id | ||||
|  */ | ||||
| export function getApIds(value: IObject | string | (IObject | string)[] | undefined): string[] { | ||||
| 	if (value == null) return []; | ||||
| 	const array = Array.isArray(value) ? value : [value]; | ||||
| 	return array.map(x => getApId(x)); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Get first ActivityStreams Object id | ||||
|  */ | ||||
| export function getOneApId(value: IObject | string | (IObject | string)[]): string { | ||||
| 	const firstOne = Array.isArray(value) ? value[0] : value; | ||||
| 	return getApId(firstOne); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Get ActivityStreams Object id | ||||
|  */ | ||||
| export function getApId(value: string | IObject): string { | ||||
| 	if (typeof value === 'string') return value; | ||||
| 	if (typeof value.id === 'string') return value.id; | ||||
| 	throw new Error(`cannot detemine id`); | ||||
| } | ||||
| 
 | ||||
| export interface IActivity extends IObject { | ||||
| 	//type: 'Activity';
 | ||||
| 	actor: IObject | string; | ||||
|  | @ -42,8 +68,10 @@ export interface IOrderedCollection extends IObject { | |||
| 	orderedItems: IObject | string | IObject[] | string[]; | ||||
| } | ||||
| 
 | ||||
| export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video']; | ||||
| 
 | ||||
| export interface INote extends IObject { | ||||
| 	type: 'Note' | 'Question'; | ||||
| 	type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video'; | ||||
| 	_misskey_content?: string; | ||||
| 	_misskey_quote?: string; | ||||
| 	_misskey_question?: string; | ||||
|  | @ -59,6 +87,9 @@ export interface IQuestion extends IObject { | |||
| 	endTime?: Date; | ||||
| } | ||||
| 
 | ||||
| export const isQuestion = (object: IObject): object is IQuestion => | ||||
| 	object.type === 'Note' || object.type === 'Question'; | ||||
| 
 | ||||
| interface IQuestionChoice { | ||||
| 	name?: string; | ||||
| 	replies?: ICollection; | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ import { Users, Notes } from '../../../../models'; | |||
| import { Note } from '../../../../models/entities/note'; | ||||
| import { User } from '../../../../models/entities/user'; | ||||
| import { fetchMeta } from '../../../../misc/fetch-meta'; | ||||
| import { validActor } from '../../../../remote/activitypub/type'; | ||||
| import { validActor, validPost } from '../../../../remote/activitypub/type'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['federation'], | ||||
|  | @ -145,7 +145,7 @@ async function fetchAny(uri: string) { | |||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	if (['Note', 'Question', 'Article'].includes(object.type)) { | ||||
| 	if (validPost.includes(object.type)) { | ||||
| 		const note = await createNote(object.id, undefined, true); | ||||
| 		return { | ||||
| 			type: 'Note', | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue