From 8e13f6a7e0cadfccab34e94cd81e6a136ba90806 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Fri, 3 Apr 2020 22:51:38 +0900 Subject: [PATCH] =?UTF-8?q?AP=E3=83=A1=E3=83=B3=E3=82=B7=E3=83=A7=E3=83=B3?= =?UTF-8?q?=E3=81=AFaudience=E3=81=98=E3=82=83=E3=81=AA=E3=81=8F=E3=81=A6t?= =?UTF-8?q?ag=E3=82=92=E5=8F=82=E7=85=A7=E3=81=99=E3=82=8B=E3=81=AA?= =?UTF-8?q?=E3=81=A9=20(#6128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * APメンションはaudienceじゃなくてtagを参照するなど * AP/tag/Mentionではurlじゃなくてuriを提示する * createPersonでaliasが入力された場合に対応 * AP HTMLパースでMention/Hashtag判定にtagを使うように * fix * indent * use hashtag name * fix * URLエンコード不要だったら<>を使わないの条件が消えたたのを修正 --- src/mfm/fromHtml.ts | 12 ++--- src/remote/activitypub/misc/html-to-mfm.ts | 9 ++++ src/remote/activitypub/models/mention.ts | 24 ++++++++++ src/remote/activitypub/models/note.ts | 20 ++++---- src/remote/activitypub/models/person.ts | 53 +++++++++++----------- src/remote/activitypub/models/tag.ts | 30 +++++------- src/remote/activitypub/renderer/mention.ts | 2 +- src/remote/activitypub/type.ts | 42 ++++++++++++++++- 8 files changed, 128 insertions(+), 64 deletions(-) create mode 100644 src/remote/activitypub/misc/html-to-mfm.ts create mode 100644 src/remote/activitypub/models/mention.ts diff --git a/src/mfm/fromHtml.ts b/src/mfm/fromHtml.ts index 0ffae014a..8c6ca337e 100644 --- a/src/mfm/fromHtml.ts +++ b/src/mfm/fromHtml.ts @@ -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; diff --git a/src/remote/activitypub/misc/html-to-mfm.ts b/src/remote/activitypub/misc/html-to-mfm.ts new file mode 100644 index 000000000..5d3cf0b77 --- /dev/null +++ b/src/remote/activitypub/misc/html-to-mfm.ts @@ -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); +} diff --git a/src/remote/activitypub/models/mention.ts b/src/remote/activitypub/models/mention.ts new file mode 100644 index 000000000..5d10328ef --- /dev/null +++ b/src/remote/activitypub/models/mention.ts @@ -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(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); +} diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 55b5440e6..1e633704d 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -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 { +export async function extractEmojis(tags: IObject | IObject[], host: string): Promise { 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, diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index f9ffa4d77..5936cee32 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -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 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 /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 - 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) }); } } diff --git a/src/remote/activitypub/models/tag.ts b/src/remote/activitypub/models/tag.ts index 8d2008d1d..d25cb463f 100644 --- a/src/remote/activitypub/models/tag.ts +++ b/src/remote/activitypub/models/tag.ts @@ -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); } diff --git a/src/remote/activitypub/renderer/mention.ts b/src/remote/activitypub/renderer/mention.ts index 889be5d85..3b5e8f27a 100644 --- a/src/remote/activitypub/renderer/mention.ts +++ b/src/remote/activitypub/renderer/mention.ts @@ -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}`, }); diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 35a070516..5cae8ee72 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -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'; }