diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-10-08 15:37:24 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-10-08 15:37:24 +0900 |
| commit | 9c170c426be01773afb15a9868ff3c278e09409c (patch) | |
| tree | 0229bb52dd9197308d193f4e41bbc11d3dcb95a1 /src/remote/activitypub | |
| parent | New translations ja-JP.yml (Norwegian) (diff) | |
| parent | fix(package): update @types/mongodb to version 3.1.10 (#2849) (diff) | |
| download | misskey-9c170c426be01773afb15a9868ff3c278e09409c.tar.gz misskey-9c170c426be01773afb15a9868ff3c278e09409c.tar.bz2 misskey-9c170c426be01773afb15a9868ff3c278e09409c.zip | |
Merge branch 'develop' into l10n_develop
Diffstat (limited to 'src/remote/activitypub')
| -rw-r--r-- | src/remote/activitypub/kernel/add/index.ts | 22 | ||||
| -rw-r--r-- | src/remote/activitypub/kernel/index.ts | 10 | ||||
| -rw-r--r-- | src/remote/activitypub/kernel/remove/index.ts | 22 | ||||
| -rw-r--r-- | src/remote/activitypub/misc/get-note-html.ts | 15 | ||||
| -rw-r--r-- | src/remote/activitypub/models/note.ts | 12 | ||||
| -rw-r--r-- | src/remote/activitypub/models/person.ts | 64 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/add.ts | 9 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/announce.ts | 2 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/hashtag.ts | 2 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/note.ts | 34 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/ordered-collection.ts | 4 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/person.ts | 1 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/remove.ts | 9 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/tombstone.ts | 4 | ||||
| -rw-r--r-- | src/remote/activitypub/renderer/update.ts | 14 | ||||
| -rw-r--r-- | src/remote/activitypub/request.ts | 28 | ||||
| -rw-r--r-- | src/remote/activitypub/resolver.ts | 11 | ||||
| -rw-r--r-- | src/remote/activitypub/type.ts | 12 |
18 files changed, 234 insertions, 41 deletions
diff --git a/src/remote/activitypub/kernel/add/index.ts b/src/remote/activitypub/kernel/add/index.ts new file mode 100644 index 0000000000..eb2dba5b21 --- /dev/null +++ b/src/remote/activitypub/kernel/add/index.ts @@ -0,0 +1,22 @@ +import { IRemoteUser } from '../../../../models/user'; +import { IAdd } from '../../type'; +import { resolveNote } from '../../models/note'; +import { addPinned } from '../../../../services/i/pin'; + +export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + if (activity.target == null) { + throw new Error('target is null'); + } + + if (activity.target === actor.featured) { + const note = await resolveNote(activity.object); + await addPinned(actor, note._id); + return; + } + + throw new Error(`unknown target: ${activity.target}`); +}; diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts index 752a9bd2e2..52b0efc730 100644 --- a/src/remote/activitypub/kernel/index.ts +++ b/src/remote/activitypub/kernel/index.ts @@ -8,6 +8,8 @@ import like from './like'; import announce from './announce'; import accept from './accept'; import reject from './reject'; +import add from './add'; +import remove from './remove'; const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { switch (activity.type) { @@ -31,6 +33,14 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { await reject(actor, activity); break; + case 'Add': + await add(actor, activity).catch(err => console.log(err)); + break; + + case 'Remove': + await remove(actor, activity).catch(err => console.log(err)); + break; + case 'Announce': await announce(actor, activity); break; diff --git a/src/remote/activitypub/kernel/remove/index.ts b/src/remote/activitypub/kernel/remove/index.ts new file mode 100644 index 0000000000..91b207c80d --- /dev/null +++ b/src/remote/activitypub/kernel/remove/index.ts @@ -0,0 +1,22 @@ +import { IRemoteUser } from '../../../../models/user'; +import { IRemove } from '../../type'; +import { resolveNote } from '../../models/note'; +import { removePinned } from '../../../../services/i/pin'; + +export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => { + if ('actor' in activity && actor.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + if (activity.target == null) { + throw new Error('target is null'); + } + + if (activity.target === actor.featured) { + const note = await resolveNote(activity.object); + await removePinned(actor, note._id); + return; + } + + throw new Error(`unknown target: ${activity.target}`); +}; diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts index 8df440930b..0a607bd48c 100644 --- a/src/remote/activitypub/misc/get-note-html.ts +++ b/src/remote/activitypub/misc/get-note-html.ts @@ -1,23 +1,10 @@ import { INote } from '../../../models/note'; import toHtml from '../../../mfm/html'; import parse from '../../../mfm/parse'; -import config from '../../../config'; export default function(note: INote) { - if (note.text == null) return null; - let html = toHtml(parse(note.text), note.mentionedRemoteUsers); - - if (note.poll != null) { - const url = `${config.url}/notes/${note._id}`; - // TODO: i18n - html += `<p><a href="${url}">【Misskeyで投票を見る】</a></p>`; - } - - if (note.renoteId != null) { - const url = `${config.url}/notes/${note.renoteId}`; - html += `<p>RE: <a href="${url}">${url}</a></p>`; - } + if (html == null) html = ''; return html; } diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 1dfeebfdf7..d49cf53079 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -56,7 +56,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false log(`Creating the Note: ${note.id}`); // 投稿者をフェッチ - const actor = await resolvePerson(note.attributedTo) as IRemoteUser; + const actor = await resolvePerson(note.attributedTo, null, resolver) as IRemoteUser; // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { @@ -73,16 +73,16 @@ export async function createNote(value: any, resolver?: Resolver, silent = false visibility = 'followers'; } else { visibility = 'specified'; - visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri))); + visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, null, resolver))); } } //#endergion - // 添付メディア + // 添付ファイル // TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしも配列ではない // Noteがsensitiveなら添付もsensitiveにする - const media = note.attachment + const files = note.attachment .map(attach => attach.sensitive = note.sensitive) ? await Promise.all(note.attachment.map(x => resolveImage(actor, x))) : []; @@ -91,7 +91,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; // テキストのパース - const text = htmlToMFM(note.content); + const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content); // ユーザーの情報が古かったらついでに更新しておく if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) { @@ -100,7 +100,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false return await post(actor, { createdAt: new Date(note.published), - media, + files: files, reply, renote: undefined, cw: note.summary, diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 3bd4e16763..ee95e43ad3 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -3,15 +3,16 @@ import { toUnicode } from 'punycode'; import * as debug from 'debug'; import config from '../../../config'; -import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; +import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user'; import Resolver from '../resolver'; import { resolveImage } from './image'; -import { isCollectionOrOrderedCollection, IPerson } from '../type'; +import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type'; import { IDriveFile } from '../../../models/drive-file'; import Meta from '../../../models/meta'; import htmlToMFM from '../../../mfm/html-to-mfm'; import { updateUserStats } from '../../../services/update-chart'; import { URL } from 'url'; +import { resolveNote } from './note'; const log = debug('misskey:activitypub'); @@ -139,6 +140,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU avatarId: null, bannerId: null, createdAt: Date.parse(person.published) || null, + updatedAt: new Date(), description: htmlToMFM(person.summary), followersCount, followingCount, @@ -154,6 +156,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU }, inbox: person.inbox, sharedInbox: person.sharedInbox, + featured: person.featured, endpoints: person.endpoints, uri: person.id, url: person.url, @@ -210,15 +213,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU user.bannerUrl = bannerUrl; //#endregion + await updateFeatured(user._id).catch(err => console.log(err)); return user; } /** * Personの情報を更新します。 - * * Misskeyに対象のPersonが登録されていなければ無視します。 + * @param uri URI of Person + * @param resolver Resolver + * @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します) */ -export async function updatePerson(uri: string, resolver?: Resolver): Promise<void> { +export async function updatePerson(uri: string, resolver?: Resolver, hint?: object): Promise<void> { if (typeof uri !== 'string') throw 'uri is not string'; // URIがこのサーバーを指しているならスキップ @@ -236,7 +242,7 @@ export async function updatePerson(uri: string, resolver?: Resolver): Promise<vo if (resolver == null) resolver = new Resolver(); - const object = await resolver.resolve(uri) as any; + const object = hint || await resolver.resolve(uri) as any; const err = validatePerson(object, uri); @@ -279,6 +285,7 @@ export async function updatePerson(uri: string, resolver?: Resolver): Promise<vo updatedAt: new Date(), inbox: person.inbox, sharedInbox: person.sharedInbox, + featured: person.featured, avatarId: avatar ? avatar._id : null, bannerId: banner ? banner._id : null, avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null, @@ -290,9 +297,18 @@ export async function updatePerson(uri: string, resolver?: Resolver): Promise<vo name: person.name, url: person.url, endpoints: person.endpoints, - isCat: (person as any).isCat === true ? true : false + isBot: object.type == 'Service', + isCat: (person as any).isCat === true ? true : false, + isLocked: person.manuallyApprovesFollowers, + createdAt: Date.parse(person.published) || null, + publicKey: { + id: person.publicKey.id, + publicKeyPem: person.publicKey.publicKeyPem + }, } }); + + await updateFeatured(exist._id).catch(err => console.log(err)); } /** @@ -301,7 +317,7 @@ export async function updatePerson(uri: string, resolver?: Resolver): Promise<vo * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> { +export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> { if (typeof uri !== 'string') throw 'uri is not string'; //#region このサーバーに既に登録されていたらそれを返す @@ -313,5 +329,37 @@ export async function resolvePerson(uri: string, verifier?: string): Promise<IUs //#endregion // リモートサーバーからフェッチしてきて登録 - return await createPerson(uri); + if (resolver == null) resolver = new Resolver(); + return await createPerson(uri, resolver); +} + +export async function updateFeatured(userId: mongo.ObjectID) { + const user = await User.findOne({ _id: userId }); + if (!isRemoteUser(user)) return; + if (!user.featured) return; + + log(`Updating the featured: ${user.uri}`); + + const resolver = new Resolver(); + + // Resolve to (Ordered)Collection Object + const collection = await resolver.resolveCollection(user.featured); + if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`); + + // Resolve to Object(may be Note) arrays + const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems; + const items = await resolver.resolve(unresolvedItems); + if (!Array.isArray(items)) throw new Error(`Collection items is not an array`); + + // Resolve and regist Notes + const featuredNotes = await Promise.all(items + .filter(item => item.type === 'Note') + .slice(0, 5) + .map(item => resolveNote(item, resolver))); + + await User.update({ _id: user._id }, { + $set: { + pinnedNoteIds: featuredNotes.map(note => note._id) + } + }); } diff --git a/src/remote/activitypub/renderer/add.ts b/src/remote/activitypub/renderer/add.ts new file mode 100644 index 0000000000..4d6fe392aa --- /dev/null +++ b/src/remote/activitypub/renderer/add.ts @@ -0,0 +1,9 @@ +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default (user: ILocalUser, target: any, object: any) => ({ + type: 'Add', + actor: `${config.url}/users/${user._id}`, + target, + object +}); diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts index f6276ade04..18e23cc336 100644 --- a/src/remote/activitypub/renderer/announce.ts +++ b/src/remote/activitypub/renderer/announce.ts @@ -5,7 +5,7 @@ export default (object: any, note: INote) => { const attributedTo = `${config.url}/users/${note.userId}`; return { - id: `${config.url}/notes/${note._id}`, + id: `${config.url}/notes/${note._id}/activity`, actor: `${config.url}/users/${note.userId}`, type: 'Announce', published: note.createdAt.toISOString(), diff --git a/src/remote/activitypub/renderer/hashtag.ts b/src/remote/activitypub/renderer/hashtag.ts index a37ba63532..36563c2df5 100644 --- a/src/remote/activitypub/renderer/hashtag.ts +++ b/src/remote/activitypub/renderer/hashtag.ts @@ -3,5 +3,5 @@ import config from '../../../config'; export default (tag: string) => ({ type: 'Hashtag', href: `${config.url}/tags/${encodeURIComponent(tag)}`, - name: '#' + tag + name: `#${tag}` }); diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 1d169d3088..b3ce1c03e4 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -6,10 +6,11 @@ import DriveFile, { IDriveFile } from '../../../models/drive-file'; import Note, { INote } from '../../../models/note'; import User from '../../../models/user'; import toHtml from '../misc/get-note-html'; +import parseMfm from '../../../mfm/parse'; export default async function renderNote(note: INote, dive = true): Promise<any> { - const promisedFiles: Promise<IDriveFile[]> = note.mediaIds - ? DriveFile.find({ _id: { $in: note.mediaIds } }) + const promisedFiles: Promise<IDriveFile[]> = note.fileIds + ? DriveFile.find({ _id: { $in: note.fileIds } }) : Promise.resolve([]); let inReplyTo; @@ -81,12 +82,39 @@ export default async function renderNote(note: INote, dive = true): Promise<any> const files = await promisedFiles; + let text = note.text; + + if (note.poll != null) { + if (text == null) text = ''; + const url = `${config.url}/notes/${note._id}`; + // TODO: i18n + text += `\n\n[投票を見る](${url})`; + } + + if (note.renoteId != null) { + if (text == null) text = ''; + const url = `${config.url}/notes/${note.renoteId}`; + text += `\n\nRE: ${url}`; + } + + // 省略されたメンションのホストを復元する + if (text != null) { + text = parseMfm(text).map(x => { + if (x.type == 'mention' && x.host == null) { + return `${x.content}@${config.host}`; + } else { + return x.content; + } + }).join(''); + } + return { id: `${config.url}/notes/${note._id}`, type: 'Note', attributedTo, summary: note.cw, - content: toHtml(note), + content: toHtml(Object.assign({}, note, { text })), + _misskey_content: text, published: note.createdAt.toISOString(), to, cc, diff --git a/src/remote/activitypub/renderer/ordered-collection.ts b/src/remote/activitypub/renderer/ordered-collection.ts index 3c448cf873..5461005983 100644 --- a/src/remote/activitypub/renderer/ordered-collection.ts +++ b/src/remote/activitypub/renderer/ordered-collection.ts @@ -4,8 +4,9 @@ * @param totalItems Total number of items * @param first URL of first page (optional) * @param last URL of last page (optional) + * @param orderedItems attached objects (optional) */ -export default function(id: string, totalItems: any, first: string, last: string) { +export default function(id: string, totalItems: any, first?: string, last?: string, orderedItems?: object) { const page: any = { id, type: 'OrderedCollection', @@ -14,6 +15,7 @@ export default function(id: string, totalItems: any, first: string, last: string if (first) page.first = first; if (last) page.last = last; + if (orderedItems) page.orderedItems = orderedItems; return page; } diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts index 78918af368..52485e6959 100644 --- a/src/remote/activitypub/renderer/person.ts +++ b/src/remote/activitypub/renderer/person.ts @@ -21,6 +21,7 @@ export default async (user: ILocalUser) => { outbox: `${id}/outbox`, followers: `${id}/followers`, following: `${id}/following`, + featured: `${id}/collections/featured`, sharedInbox: `${config.url}/inbox`, url: `${config.url}/@${user.username}`, preferredUsername: user.username, diff --git a/src/remote/activitypub/renderer/remove.ts b/src/remote/activitypub/renderer/remove.ts new file mode 100644 index 0000000000..ed840be751 --- /dev/null +++ b/src/remote/activitypub/renderer/remove.ts @@ -0,0 +1,9 @@ +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default (user: ILocalUser, target: any, object: any) => ({ + type: 'Remove', + actor: `${config.url}/users/${user._id}`, + target, + object +}); diff --git a/src/remote/activitypub/renderer/tombstone.ts b/src/remote/activitypub/renderer/tombstone.ts new file mode 100644 index 0000000000..553406b93b --- /dev/null +++ b/src/remote/activitypub/renderer/tombstone.ts @@ -0,0 +1,4 @@ +export default (id: string) => ({ + id, + type: 'Tombstone' +}); diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts new file mode 100644 index 0000000000..cf9acc9acb --- /dev/null +++ b/src/remote/activitypub/renderer/update.ts @@ -0,0 +1,14 @@ +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default (object: any, user: ILocalUser) => { + const activity = { + id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`, + actor: `${config.url}/users/${user._id}`, + type: 'Update', + to: [ 'https://www.w3.org/ns/activitystreams#Public' ], + object + } as any; + + return activity; +}; diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index 6238d3acb1..177b6f458e 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -2,6 +2,7 @@ import { request } from 'https'; const { sign } = require('http-signature'); import { URL } from 'url'; import * as debug from 'debug'; +const crypto = require('crypto'); import config from '../../config'; import { ILocalUser } from '../../models/user'; @@ -11,22 +12,33 @@ const log = debug('misskey:activitypub:deliver'); export default (user: ILocalUser, url: string, object: any) => new Promise((resolve, reject) => { log(`--> ${url}`); + const timeout = 10 * 1000; + const { protocol, hostname, port, pathname, search } = new URL(url); + const data = JSON.stringify(object); + + const sha256 = crypto.createHash('sha256'); + sha256.update(data); + const hash = sha256.digest('base64'); + const req = request({ protocol, hostname, port, method: 'POST', path: pathname + search, + timeout, headers: { - 'Content-Type': 'application/activity+json' + 'User-Agent': config.user_agent, + 'Content-Type': 'application/activity+json', + 'Digest': `SHA-256=${hash}` } }, res => { log(`${url} --> ${res.statusCode}`); if (res.statusCode >= 400) { - reject(); + reject(res); } else { resolve(); } @@ -35,7 +47,8 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso sign(req, { authorizationHeaderName: 'Signature', key: user.keypair, - keyId: `${config.url}/users/${user._id}/publickey` + keyId: `${config.url}/users/${user._id}/publickey`, + headers: ['date', 'host', 'digest'] }); // Signature: Signature ... => Signature: ... @@ -43,5 +56,12 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso sig = sig.replace(/^Signature /, ''); req.setHeader('Signature', sig); - req.end(JSON.stringify(object)); + req.on('timeout', () => req.abort()); + + req.on('error', e => { + if (req.aborted) reject('timeout'); + reject(e); + }); + + req.end(data); }); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 0b053ca774..ff26971758 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,12 +1,13 @@ import * as request from 'request-promise-native'; import * as debug from 'debug'; import { IObject } from './type'; -//import config from '../../config'; +import config from '../../config'; const log = debug('misskey:activitypub:resolver'); export default class Resolver { private history: Set<string>; + private timeout = 10 * 1000; constructor() { this.history = new Set(); @@ -19,11 +20,11 @@ export default class Resolver { switch (collection.type) { case 'Collection': - collection.objects = collection.object.items; + collection.objects = collection.items; break; case 'OrderedCollection': - collection.objects = collection.object.orderedItems; + collection.objects = collection.orderedItems; break; default: @@ -50,10 +51,14 @@ export default class Resolver { const object = await request({ url: value, + timeout: this.timeout, headers: { + 'User-Agent': config.user_agent, Accept: 'application/activity+json, application/ld+json' }, json: true + }).catch(e => { + throw new Error(`request error: ${e.message}`); }); if (object === null || ( diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 3d40ad48cb..5c06ee4ffe 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -40,6 +40,7 @@ export interface IOrderedCollection extends IObject { export interface INote extends IObject { type: 'Note'; + _misskey_content: string; } export interface IPerson extends IObject { @@ -52,6 +53,7 @@ export interface IPerson extends IObject { publicKey: any; followers: any; following: any; + featured?: any; outbox: any; endpoints: string[]; } @@ -89,6 +91,14 @@ export interface IReject extends IActivity { type: 'Reject'; } +export interface IAdd extends IActivity { + type: 'Add'; +} + +export interface IRemove extends IActivity { + type: 'Remove'; +} + export interface ILike extends IActivity { type: 'Like'; _misskey_reaction: string; @@ -107,5 +117,7 @@ export type Object = IFollow | IAccept | IReject | + IAdd | + IRemove | ILike | IAnnounce; |