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
8 changed files with 128 additions and 64 deletions
|
@ -1,7 +1,7 @@
|
||||||
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
|
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
|
||||||
import { urlRegex } from './prelude';
|
import { urlRegex } from './prelude';
|
||||||
|
|
||||||
export function fromHtml(html: string): string {
|
export function fromHtml(html: string, hashtagNames?: string[]): string {
|
||||||
const dom = parseFragment(html) as DefaultTreeDocumentFragment;
|
const dom = parseFragment(html) as DefaultTreeDocumentFragment;
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
|
@ -36,12 +36,10 @@ export function fromHtml(html: string): string {
|
||||||
const txt = getText(node);
|
const txt = getText(node);
|
||||||
const rel = node.attrs.find((x: any) => x.name == 'rel');
|
const rel = node.attrs.find((x: any) => x.name == 'rel');
|
||||||
const href = node.attrs.find((x: any) => x.name == 'href');
|
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) {
|
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||||
text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`;
|
text += txt;
|
||||||
// メンション
|
// メンション
|
||||||
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
||||||
const part = txt.split('@');
|
const part = txt.split('@');
|
||||||
|
@ -56,7 +54,7 @@ export function fromHtml(html: string): string {
|
||||||
}
|
}
|
||||||
// その他
|
// その他
|
||||||
} else {
|
} else {
|
||||||
text += `[${txt}](${href.value})`;
|
text += (!href || (txt === href.value && txt.match(urlRegex))) ? txt : `[${txt}](${href.value})`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
9
src/remote/activitypub/misc/html-to-mfm.ts
Normal file
9
src/remote/activitypub/misc/html-to-mfm.ts
Normal file
|
@ -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);
|
||||||
|
}
|
24
src/remote/activitypub/models/mention.ts
Normal file
24
src/remote/activitypub/models/mention.ts
Normal file
|
@ -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 { resolvePerson, updatePerson } from './person';
|
||||||
import { resolveImage } from './image';
|
import { resolveImage } from './image';
|
||||||
import { IRemoteUser } from '../../../models/entities/user';
|
import { IRemoteUser } from '../../../models/entities/user';
|
||||||
import { fromHtml } from '../../../mfm/fromHtml';
|
import { htmlToMfm } from '../misc/html-to-mfm';
|
||||||
import { ITag, extractHashtags } from './tag';
|
import { extractApHashtags } from './tag';
|
||||||
import { unique } from '../../../prelude/array';
|
import { unique, toArray, toSingle } from '../../../prelude/array';
|
||||||
import { extractPollFromQuestion } from './question';
|
import { extractPollFromQuestion } from './question';
|
||||||
import vote from '../../../services/note/polls/vote';
|
import vote from '../../../services/note/polls/vote';
|
||||||
import { apLogger } from '../logger';
|
import { apLogger } from '../logger';
|
||||||
|
@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update';
|
||||||
import { extractDbHost, toPuny } from '../../../misc/convert-host';
|
import { extractDbHost, toPuny } from '../../../misc/convert-host';
|
||||||
import { Notes, Emojis, Polls, MessagingMessages } from '../../../models';
|
import { Notes, Emojis, Polls, MessagingMessages } from '../../../models';
|
||||||
import { Note } from '../../../models/entities/note';
|
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 { Emoji } from '../../../models/entities/emoji';
|
||||||
import { genId } from '../../../misc/gen-id';
|
import { genId } from '../../../misc/gen-id';
|
||||||
import { fetchMeta } from '../../../misc/fetch-meta';
|
import { fetchMeta } from '../../../misc/fetch-meta';
|
||||||
|
@ -25,6 +25,7 @@ import { ensure } from '../../../prelude/ensure';
|
||||||
import { getApLock } from '../../../misc/app-lock';
|
import { getApLock } from '../../../misc/app-lock';
|
||||||
import { createMessage } from '../../../services/messages/create';
|
import { createMessage } from '../../../services/messages/create';
|
||||||
import { parseAudience } from '../audience';
|
import { parseAudience } from '../audience';
|
||||||
|
import { extractApMentions } from './mention';
|
||||||
|
|
||||||
const logger = apLogger;
|
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);
|
const noteAudience = await parseAudience(actor, note.to, note.cc);
|
||||||
let visibility = noteAudience.visibility;
|
let visibility = noteAudience.visibility;
|
||||||
const visibleUsers = noteAudience.visibleUsers;
|
const visibleUsers = noteAudience.visibleUsers;
|
||||||
const apMentions = noteAudience.mentionedUsers;
|
|
||||||
|
|
||||||
// Audience (to, cc) が指定されてなかった場合
|
// Audience (to, cc) が指定されてなかった場合
|
||||||
if (visibility === 'specified' && visibleUsers.length === 0) {
|
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';
|
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ではない
|
// 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 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
|
// vote
|
||||||
if (reply && reply.hasPoll) {
|
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);
|
host = toPuny(host);
|
||||||
|
|
||||||
if (!tags) return [];
|
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 => {
|
return await Promise.all(eomjiTags.map(async tag => {
|
||||||
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
|
const name = tag.name!.replace(/^:/, '').replace(/:$/, '');
|
||||||
|
tag.icon = toSingle(tag.icon);
|
||||||
|
|
||||||
const exists = await Emojis.findOne({
|
const exists = await Emojis.findOne({
|
||||||
host,
|
host,
|
||||||
|
|
|
@ -3,12 +3,12 @@ import * as promiseLimit from 'promise-limit';
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import Resolver from '../resolver';
|
import Resolver from '../resolver';
|
||||||
import { resolveImage } from './image';
|
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 { fromHtml } from '../../../mfm/fromHtml';
|
||||||
|
import { htmlToMfm } from '../misc/html-to-mfm';
|
||||||
import { resolveNote, extractEmojis } from './note';
|
import { resolveNote, extractEmojis } from './note';
|
||||||
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
|
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
|
||||||
import { ITag, extractHashtags } from './tag';
|
import { extractApHashtags } from './tag';
|
||||||
import { IIdentifier } from './identifier';
|
|
||||||
import { apLogger } from '../logger';
|
import { apLogger } from '../logger';
|
||||||
import { Note } from '../../../models/entities/note';
|
import { Note } from '../../../models/entities/note';
|
||||||
import { updateUsertags } from '../../../services/update-hashtag';
|
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 { 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';
|
const isBot = object.type == 'Service';
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||||
|
|
||||||
await transactionalEntityManager.save(new UserProfile({
|
await transactionalEntityManager.save(new UserProfile({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
description: person.summary ? fromHtml(person.summary) : null,
|
description: person.summary ? htmlToMfm(person.summary, person.tag) : null,
|
||||||
url: person.url,
|
url: person.url,
|
||||||
fields,
|
fields,
|
||||||
userHost: host
|
userHost: host
|
||||||
|
@ -180,11 +180,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// duplicate key error
|
// duplicate key error
|
||||||
if (isDuplicateKeyValueError(e)) {
|
if (isDuplicateKeyValueError(e)) {
|
||||||
throw new Error('already registered');
|
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||||
}
|
const u = await Users.findOne({
|
||||||
|
uri: person.id
|
||||||
|
});
|
||||||
|
|
||||||
logger.error(e);
|
if (u) {
|
||||||
throw e;
|
user = u as IRemoteUser;
|
||||||
|
} else {
|
||||||
|
throw new Error('already registered');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register host
|
// Register host
|
||||||
|
@ -308,7 +317,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
|
|
||||||
const { fields } = analyzeAttachments(person.attachment || []);
|
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 = {
|
const updates = {
|
||||||
lastFetchedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
|
@ -346,7 +355,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||||
await UserProfiles.update({ userId: exist.id }, {
|
await UserProfiles.update({ userId: exist.id }, {
|
||||||
url: person.url,
|
url: person.url,
|
||||||
fields,
|
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);
|
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: {
|
const services: {
|
||||||
[x: string]: (id: string, username: string) => any
|
[x: string]: (id: string, username: string) => any
|
||||||
} = {
|
} = {
|
||||||
|
@ -409,7 +408,7 @@ const $discord = (id: string, name: string) => {
|
||||||
return { id, username, discriminator };
|
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];
|
const service = services[source.name];
|
||||||
|
|
||||||
if (typeof source.value !== 'string')
|
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);
|
target[source.name.split(':')[2]] = service(id, username);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function analyzeAttachments(attachments: ITag[]) {
|
export function analyzeAttachments(attachments: IObject | IObject[] | undefined) {
|
||||||
const fields: {
|
const fields: {
|
||||||
name: string,
|
name: string,
|
||||||
value: string
|
value: string
|
||||||
|
@ -430,12 +429,12 @@ export function analyzeAttachments(attachments: ITag[]) {
|
||||||
|
|
||||||
if (Array.isArray(attachments)) {
|
if (Array.isArray(attachments)) {
|
||||||
for (const attachment of attachments.filter(isPropertyValue)) {
|
for (const attachment of attachments.filter(isPropertyValue)) {
|
||||||
if (isPropertyValue(attachment.identifier!)) {
|
if (isPropertyValue(attachment.identifier)) {
|
||||||
addService(services, attachment.identifier!);
|
addService(services, attachment.identifier);
|
||||||
} else {
|
} else {
|
||||||
fields.push({
|
fields.push({
|
||||||
name: attachment.name!,
|
name: attachment.name,
|
||||||
value: fromHtml(attachment.value!)
|
value: fromHtml(attachment.value)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,18 @@
|
||||||
import { IIcon } from './icon';
|
import { toArray } from '../../../prelude/array';
|
||||||
import { IIdentifier } from './identifier';
|
import { IObject, isHashtag, IApHashtag } from '../type';
|
||||||
|
|
||||||
/***
|
export function extractApHashtags(tags: IObject | IObject[] | null | undefined) {
|
||||||
* 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[] {
|
|
||||||
if (tags == null) return [];
|
if (tags == null) return [];
|
||||||
|
|
||||||
const hashtags = tags.filter(tag => tag.type === 'Hashtag' && typeof tag.name == 'string');
|
const hashtags = extractApHashtagObjects(tags);
|
||||||
|
|
||||||
return hashtags.map(tag => {
|
return hashtags.map(tag => {
|
||||||
const m = tag.name ? tag.name.match(/^#(.+)/) : null;
|
const m = tag.name.match(/^#(.+)/);
|
||||||
return m ? m[1] : null;
|
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) => ({
|
export default (mention: User) => ({
|
||||||
type: 'Mention',
|
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}`,
|
name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,8 @@ export interface IObject {
|
||||||
icon?: any;
|
icon?: any;
|
||||||
image?: any;
|
image?: any;
|
||||||
url?: string;
|
url?: string;
|
||||||
tag?: any[];
|
href?: string;
|
||||||
|
tag?: IObject | IObject[];
|
||||||
sensitive?: boolean;
|
sensitive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +131,45 @@ export const isOrderedCollection = (object: IObject): object is IOrderedCollecti
|
||||||
export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
|
export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection =>
|
||||||
isCollection(object) || isOrderedCollection(object);
|
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 {
|
export interface ICreate extends IActivity {
|
||||||
type: 'Create';
|
type: 'Create';
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue