APメンションはaudienceじゃなくてtagを参照するなど (#6128)
* APメンションはaudienceじゃなくてtagを参照するなど * AP/tag/Mentionではurlじゃなくてuriを提示する * createPersonでaliasが入力された場合に対応 * AP HTMLパースでMention/Hashtag判定にtagを使うように * fix * indent * use hashtag name * fix * URLエンコード不要だったら<>を使わないの条件が消えたたのを修正
This commit is contained in:
		
							parent
							
								
									8bb311df51
								
							
						
					
					
						commit
						99fc77b678
					
				|  | @ -1,7 +1,7 @@ | |||
| import { parseFragment, DefaultTreeDocumentFragment } from 'parse5'; | ||||
| import { urlRegex } from './prelude'; | ||||
| 
 | ||||
| export function fromHtml(html: string): string { | ||||
| export function fromHtml(html: string, hashtagNames?: string[]): string { | ||||
| 	const dom = parseFragment(html) as DefaultTreeDocumentFragment; | ||||
| 
 | ||||
| 	let text = ''; | ||||
|  | @ -36,12 +36,10 @@ export function fromHtml(html: string): string { | |||
| 				const txt = getText(node); | ||||
| 				const rel = node.attrs.find((x: any) => x.name == 'rel'); | ||||
| 				const href = node.attrs.find((x: any) => x.name == 'href'); | ||||
| 				const _class = node.attrs.find((x: any) => x.name == 'class'); | ||||
| 				const isHashtag = rel?.value?.match('tag') || _class?.value?.match('hashtag'); | ||||
| 
 | ||||
| 				// ハッシュタグ / hrefがない / txtがURL
 | ||||
| 				if (isHashtag || !href || href.value == txt) { | ||||
| 					text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`; | ||||
| 				// ハッシュタグ
 | ||||
| 				if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) { | ||||
| 					text += txt; | ||||
| 				// メンション
 | ||||
| 				} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { | ||||
| 					const part = txt.split('@'); | ||||
|  | @ -56,7 +54,7 @@ export function fromHtml(html: string): string { | |||
| 					} | ||||
| 				// その他
 | ||||
| 				} else { | ||||
| 					text += `[${txt}](${href.value})`; | ||||
| 					text += (!href || (txt === href.value && txt.match(urlRegex))) ? txt : `[${txt}](${href.value})`; | ||||
| 				} | ||||
| 				break; | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,9 @@ | |||
| import { IObject } from '../type'; | ||||
| import { extractApHashtagObjects } from '../models/tag'; | ||||
| import { fromHtml } from '../../../mfm/fromHtml'; | ||||
| 
 | ||||
| export function htmlToMfm(html: string, tag?: IObject | IObject[]) { | ||||
| 	const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); | ||||
| 
 | ||||
| 	return fromHtml(html, hashtagNames); | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| import { toArray, unique } from '../../../prelude/array'; | ||||
| import { IObject, isMention, IApMention } from '../type'; | ||||
| import { resolvePerson } from './person'; | ||||
| import * as promiseLimit from 'promise-limit'; | ||||
| import Resolver from '../resolver'; | ||||
| import { User } from '../../../models/entities/user'; | ||||
| 
 | ||||
| export async function extractApMentions(tags: IObject | IObject[] | null | undefined) { | ||||
| 	const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string)); | ||||
| 
 | ||||
| 	const resolver = new Resolver(); | ||||
| 
 | ||||
| 	const limit = promiseLimit<User | null>(2); | ||||
| 	const mentionedUsers = (await Promise.all( | ||||
| 		hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))) | ||||
| 	)).filter((x): x is User => x != null); | ||||
| 
 | ||||
| 	return mentionedUsers; | ||||
| } | ||||
| 
 | ||||
| export function extractApMentionObjects(tags: IObject | IObject[] | null | undefined): IApMention[] { | ||||
| 	if (tags == null) return []; | ||||
| 	return toArray(tags).filter(isMention); | ||||
| } | ||||
|  | @ -6,9 +6,9 @@ import post from '../../../services/note/create'; | |||
| import { resolvePerson, updatePerson } from './person'; | ||||
| import { resolveImage } from './image'; | ||||
| import { IRemoteUser } from '../../../models/entities/user'; | ||||
| import { fromHtml } from '../../../mfm/fromHtml'; | ||||
| import { ITag, extractHashtags } from './tag'; | ||||
| import { unique } from '../../../prelude/array'; | ||||
| import { htmlToMfm } from '../misc/html-to-mfm'; | ||||
| import { extractApHashtags } from './tag'; | ||||
| import { unique, toArray, toSingle } from '../../../prelude/array'; | ||||
| import { extractPollFromQuestion } from './question'; | ||||
| import vote from '../../../services/note/polls/vote'; | ||||
| import { apLogger } from '../logger'; | ||||
|  | @ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update'; | |||
| import { extractDbHost, toPuny } from '../../../misc/convert-host'; | ||||
| import { Notes, Emojis, Polls, MessagingMessages } from '../../../models'; | ||||
| import { Note } from '../../../models/entities/note'; | ||||
| import { IObject, getOneApId, getApId, validPost, IPost } from '../type'; | ||||
| import { IObject, getOneApId, getApId, validPost, IPost, isEmoji } from '../type'; | ||||
| import { Emoji } from '../../../models/entities/emoji'; | ||||
| import { genId } from '../../../misc/gen-id'; | ||||
| import { fetchMeta } from '../../../misc/fetch-meta'; | ||||
|  | @ -25,6 +25,7 @@ import { ensure } from '../../../prelude/ensure'; | |||
| import { getApLock } from '../../../misc/app-lock'; | ||||
| import { createMessage } from '../../../services/messages/create'; | ||||
| import { parseAudience } from '../audience'; | ||||
| import { extractApMentions } from './mention'; | ||||
| 
 | ||||
| const logger = apLogger; | ||||
| 
 | ||||
|  | @ -113,7 +114,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s | |||
| 	const noteAudience = await parseAudience(actor, note.to, note.cc); | ||||
| 	let visibility = noteAudience.visibility; | ||||
| 	const visibleUsers = noteAudience.visibleUsers; | ||||
| 	const apMentions = noteAudience.mentionedUsers; | ||||
| 
 | ||||
| 	// Audience (to, cc) が指定されてなかった場合
 | ||||
| 	if (visibility === 'specified' && visibleUsers.length === 0) { | ||||
|  | @ -125,7 +125,8 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s | |||
| 
 | ||||
| 	let isTalk = note._misskey_talk && visibility === 'specified'; | ||||
| 
 | ||||
| 	const apHashtags = await extractHashtags(note.tag); | ||||
| 	const apMentions = await extractApMentions(note.tag); | ||||
| 	const apHashtags = await extractApHashtags(note.tag); | ||||
| 
 | ||||
| 	// 添付ファイル
 | ||||
| 	// TODO: attachmentは必ずしもImageではない
 | ||||
|  | @ -210,7 +211,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s | |||
| 	const cw = note.summary === '' ? null : note.summary; | ||||
| 
 | ||||
| 	// テキストのパース
 | ||||
| 	const text = note._misskey_content || (note.content ? fromHtml(note.content) : null); | ||||
| 	const text = note._misskey_content || (note.content ? htmlToMfm(note.content, note.tag) : null); | ||||
| 
 | ||||
| 	// vote
 | ||||
| 	if (reply && reply.hasPoll) { | ||||
|  | @ -319,15 +320,16 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver): | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| export async function extractEmojis(tags: ITag[], host: string): Promise<Emoji[]> { | ||||
| export async function extractEmojis(tags: IObject | IObject[], host: string): Promise<Emoji[]> { | ||||
| 	host = toPuny(host); | ||||
| 
 | ||||
| 	if (!tags) return []; | ||||
| 
 | ||||
| 	const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url && tag.name); | ||||
| 	const eomjiTags = toArray(tags).filter(isEmoji); | ||||
| 
 | ||||
| 	return await Promise.all(eomjiTags.map(async tag => { | ||||
| 		const name = tag.name!.replace(/^:/, '').replace(/:$/, ''); | ||||
| 		tag.icon = toSingle(tag.icon); | ||||
| 
 | ||||
| 		const exists = await Emojis.findOne({ | ||||
| 			host, | ||||
|  |  | |||
|  | @ -3,12 +3,12 @@ import * as promiseLimit from 'promise-limit'; | |||
| import config from '../../../config'; | ||||
| import Resolver from '../resolver'; | ||||
| import { resolveImage } from './image'; | ||||
| import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId } from '../type'; | ||||
| import { isCollectionOrOrderedCollection, isCollection, IPerson, getApId, IObject, isPropertyValue, IApPropertyValue } from '../type'; | ||||
| import { fromHtml } from '../../../mfm/fromHtml'; | ||||
| import { htmlToMfm } from '../misc/html-to-mfm'; | ||||
| import { resolveNote, extractEmojis } from './note'; | ||||
| import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc'; | ||||
| import { ITag, extractHashtags } from './tag'; | ||||
| import { IIdentifier } from './identifier'; | ||||
| import { extractApHashtags } from './tag'; | ||||
| import { apLogger } from '../logger'; | ||||
| import { Note } from '../../../models/entities/note'; | ||||
| import { updateUsertags } from '../../../services/update-hashtag'; | ||||
|  | @ -134,7 +134,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us | |||
| 
 | ||||
| 	const { fields } = analyzeAttachments(person.attachment || []); | ||||
| 
 | ||||
| 	const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32); | ||||
| 	const tags = extractApHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32); | ||||
| 
 | ||||
| 	const isBot = object.type == 'Service'; | ||||
| 
 | ||||
|  | @ -165,7 +165,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us | |||
| 
 | ||||
| 			await transactionalEntityManager.save(new UserProfile({ | ||||
| 				userId: user.id, | ||||
| 				description: person.summary ? fromHtml(person.summary) : null, | ||||
| 				description: person.summary ? htmlToMfm(person.summary, person.tag) : null, | ||||
| 				url: person.url, | ||||
| 				fields, | ||||
| 				userHost: host | ||||
|  | @ -180,11 +180,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us | |||
| 	} catch (e) { | ||||
| 		// duplicate key error
 | ||||
| 		if (isDuplicateKeyValueError(e)) { | ||||
| 			throw new Error('already registered'); | ||||
| 		} | ||||
| 			// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
 | ||||
| 			const u = await Users.findOne({ | ||||
| 				uri: person.id | ||||
| 			}); | ||||
| 
 | ||||
| 		logger.error(e); | ||||
| 		throw e; | ||||
| 			if (u) { | ||||
| 				user = u as IRemoteUser; | ||||
| 			} else { | ||||
| 				throw new Error('already registered'); | ||||
| 			} | ||||
| 		} else { | ||||
| 			logger.error(e); | ||||
| 			throw e; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Register host
 | ||||
|  | @ -308,7 +317,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint | |||
| 
 | ||||
| 	const { fields } = analyzeAttachments(person.attachment || []); | ||||
| 
 | ||||
| 	const tags = extractHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32); | ||||
| 	const tags = extractApHashtags(person.tag).map(tag => tag.toLowerCase()).splice(0, 32); | ||||
| 
 | ||||
| 	const updates = { | ||||
| 		lastFetchedAt: new Date(), | ||||
|  | @ -346,7 +355,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint | |||
| 	await UserProfiles.update({ userId: exist.id }, { | ||||
| 		url: person.url, | ||||
| 		fields, | ||||
| 		description: person.summary ? fromHtml(person.summary) : null, | ||||
| 		description: person.summary ? htmlToMfm(person.summary, person.tag) : null, | ||||
| 	}); | ||||
| 
 | ||||
| 	// ハッシュタグ更新
 | ||||
|  | @ -384,16 +393,6 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<U | |||
| 	return await createPerson(uri, resolver); | ||||
| } | ||||
| 
 | ||||
| const isPropertyValue = (x: { | ||||
| 		type: string, | ||||
| 		name?: string, | ||||
| 		value?: string | ||||
| 	}) => | ||||
| 		x && | ||||
| 		x.type === 'PropertyValue' && | ||||
| 		typeof x.name === 'string' && | ||||
| 		typeof x.value === 'string'; | ||||
| 
 | ||||
| const services: { | ||||
| 		[x: string]: (id: string, username: string) => any | ||||
| 	} = { | ||||
|  | @ -409,7 +408,7 @@ const $discord = (id: string, name: string) => { | |||
| 	return { id, username, discriminator }; | ||||
| }; | ||||
| 
 | ||||
| function addService(target: { [x: string]: any }, source: IIdentifier) { | ||||
| function addService(target: { [x: string]: any }, source: IApPropertyValue) { | ||||
| 	const service = services[source.name]; | ||||
| 
 | ||||
| 	if (typeof source.value !== 'string') | ||||
|  | @ -421,7 +420,7 @@ function addService(target: { [x: string]: any }, source: IIdentifier) { | |||
| 		target[source.name.split(':')[2]] = service(id, username); | ||||
| } | ||||
| 
 | ||||
| export function analyzeAttachments(attachments: ITag[]) { | ||||
| export function analyzeAttachments(attachments: IObject | IObject[] | undefined) { | ||||
| 	const fields: { | ||||
| 		name: string, | ||||
| 		value: string | ||||
|  | @ -430,12 +429,12 @@ export function analyzeAttachments(attachments: ITag[]) { | |||
| 
 | ||||
| 	if (Array.isArray(attachments)) { | ||||
| 		for (const attachment of attachments.filter(isPropertyValue)) { | ||||
| 			if (isPropertyValue(attachment.identifier!)) { | ||||
| 				addService(services, attachment.identifier!); | ||||
| 			if (isPropertyValue(attachment.identifier)) { | ||||
| 				addService(services, attachment.identifier); | ||||
| 			} else { | ||||
| 				fields.push({ | ||||
| 					name: attachment.name!, | ||||
| 					value: fromHtml(attachment.value!) | ||||
| 					name: attachment.name, | ||||
| 					value: fromHtml(attachment.value) | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
|  |  | |||
|  | @ -1,26 +1,18 @@ | |||
| import { IIcon } from './icon'; | ||||
| import { IIdentifier } from './identifier'; | ||||
| import { toArray } from '../../../prelude/array'; | ||||
| import { IObject, isHashtag, IApHashtag } from '../type'; | ||||
| 
 | ||||
| /*** | ||||
|  * tag (ActivityPub) | ||||
|  */ | ||||
| export type ITag = { | ||||
| 	id: string; | ||||
| 	type: string; | ||||
| 	name?: string; | ||||
| 	value?: string; | ||||
| 	updated?: Date; | ||||
| 	icon?: IIcon; | ||||
| 	identifier?: IIdentifier; | ||||
| }; | ||||
| 
 | ||||
| export function extractHashtags(tags: ITag[] | null | undefined): string[] { | ||||
| export function extractApHashtags(tags: IObject | IObject[] | null | undefined) { | ||||
| 	if (tags == null) return []; | ||||
| 
 | ||||
| 	const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string'); | ||||
| 	const hashtags = extractApHashtagObjects(tags); | ||||
| 
 | ||||
| 	return hashtags.map(tag => { | ||||
| 		const m = tag.name ? tag.name.match(/^#(.+)/) : null; | ||||
| 		const m = tag.name.match(/^#(.+)/); | ||||
| 		return m ? m[1] : null; | ||||
| 	}).filter(x => x != null) as string[]; | ||||
| 	}).filter((x): x is string => x != null); | ||||
| } | ||||
| 
 | ||||
| export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { | ||||
| 	if (tags == null) return []; | ||||
| 	return toArray(tags).filter(isHashtag); | ||||
| } | ||||
|  |  | |||
|  | @ -4,6 +4,6 @@ import { Users } from '../../../models'; | |||
| 
 | ||||
| export default (mention: User) => ({ | ||||
| 	type: 'Mention', | ||||
| 	href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`, | ||||
| 	href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${(mention as ILocalUser).id}`, | ||||
| 	name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`, | ||||
| }); | ||||
|  |  | |||
|  | @ -20,7 +20,8 @@ export interface IObject { | |||
| 	icon?: any; | ||||
| 	image?: any; | ||||
| 	url?: string; | ||||
| 	tag?: any[]; | ||||
| 	href?: string; | ||||
| 	tag?: IObject | IObject[]; | ||||
| 	sensitive?: boolean; | ||||
| } | ||||
| 
 | ||||
|  | @ -130,6 +131,45 @@ export const isOrderedCollection = (object: IObject): object is IOrderedCollecti | |||
| export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => | ||||
| 	isCollection(object) || isOrderedCollection(object); | ||||
| 
 | ||||
| export interface IApPropertyValue extends IObject { | ||||
| 	type: 'PropertyValue'; | ||||
| 	identifier: IApPropertyValue; | ||||
| 	name: string; | ||||
| 	value: string; | ||||
| } | ||||
| 
 | ||||
| export const isPropertyValue = (object: IObject): object is IApPropertyValue => | ||||
| 	object && | ||||
| 	object.type === 'PropertyValue' && | ||||
| 	typeof object.name === 'string' && | ||||
| 	typeof (object as any).value === 'string'; | ||||
| 
 | ||||
| export interface IApMention extends IObject { | ||||
| 	type: 'Mention'; | ||||
| 	href: string; | ||||
| } | ||||
| 
 | ||||
| export const isMention = (object: IObject): object is IApMention=> | ||||
| 	object.type === 'Mention' && | ||||
| 	typeof object.href === 'string'; | ||||
| 
 | ||||
| export interface IApHashtag extends IObject { | ||||
| 	type: 'Hashtag'; | ||||
| 	name: string; | ||||
| } | ||||
| 
 | ||||
| export const isHashtag = (object: IObject): object is IApHashtag => | ||||
| 	object.type === 'Hashtag' && | ||||
| 	typeof object.name === 'string'; | ||||
| 
 | ||||
| export interface IApEmoji extends IObject { | ||||
| 	type: 'Emoji'; | ||||
| 	updated: Date; | ||||
| } | ||||
| 
 | ||||
| export const isEmoji = (object: IObject): object is IApEmoji => | ||||
| 	object.type === 'Emoji' && !Array.isArray(object.icon) && object.icon.url != null; | ||||
| 
 | ||||
| export interface ICreate extends IActivity { | ||||
| 	type: 'Create'; | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue