Refactor
This commit is contained in:
9
src/remote/activitypub/act/create.ts
Normal file
9
src/remote/activitypub/act/create.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import create from '../create';
|
||||
|
||||
export default (resolver, actor, activity) => {
|
||||
if ('actor' in activity && actor.account.uri !== activity.actor) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return create(resolver, actor, activity.object);
|
||||
};
|
22
src/remote/activitypub/act/index.ts
Normal file
22
src/remote/activitypub/act/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import create from './create';
|
||||
import createObject from '../create';
|
||||
import Resolver from '../resolver';
|
||||
|
||||
export default (actor, value) => {
|
||||
return new Resolver().resolve(value).then(resolved => Promise.all(resolved.map(async promisedResult => {
|
||||
const { resolver, object } = await promisedResult;
|
||||
const created = await (await createObject(resolver, actor, [object]))[0];
|
||||
|
||||
if (created !== null) {
|
||||
return created;
|
||||
}
|
||||
|
||||
switch (object.type) {
|
||||
case 'Create':
|
||||
return create(resolver, actor, object);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})));
|
||||
};
|
87
src/remote/activitypub/create.ts
Normal file
87
src/remote/activitypub/create.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import config from '../../conf';
|
||||
import Post from '../../models/post';
|
||||
import RemoteUserObject, { IRemoteUserObject } from '../../models/remote-user-object';
|
||||
import uploadFromUrl from '../../common/drive/upload-from-url';
|
||||
import Resolver from './resolver';
|
||||
const createDOMPurify = require('dompurify');
|
||||
|
||||
function createRemoteUserObject($ref, $id, { id }) {
|
||||
const object = { $ref, $id };
|
||||
|
||||
if (!id) {
|
||||
return { object };
|
||||
}
|
||||
|
||||
return RemoteUserObject.insert({ uri: id, object });
|
||||
}
|
||||
|
||||
async function createImage(actor, object) {
|
||||
if ('attributedTo' in object && actor.account.uri !== object.attributedTo) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const { _id } = await uploadFromUrl(object.url, actor);
|
||||
return createRemoteUserObject('driveFiles.files', _id, object);
|
||||
}
|
||||
|
||||
async function createNote(resolver, actor, object) {
|
||||
if ('attributedTo' in object && actor.account.uri !== object.attributedTo) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const mediaIds = 'attachment' in object &&
|
||||
(await Promise.all(await create(resolver, actor, object.attachment)))
|
||||
.filter(media => media !== null && media.object.$ref === 'driveFiles.files')
|
||||
.map(({ object }) => object.$id);
|
||||
|
||||
const { window } = new JSDOM(object.content);
|
||||
|
||||
const { _id } = await Post.insert({
|
||||
channelId: undefined,
|
||||
index: undefined,
|
||||
createdAt: new Date(object.published),
|
||||
mediaIds,
|
||||
replyId: undefined,
|
||||
repostId: undefined,
|
||||
poll: undefined,
|
||||
text: window.document.body.textContent,
|
||||
textHtml: object.content && createDOMPurify(window).sanitize(object.content),
|
||||
userId: actor._id,
|
||||
appId: null,
|
||||
viaMobile: false,
|
||||
geo: undefined
|
||||
});
|
||||
|
||||
// Register to search database
|
||||
if (object.content && config.elasticsearch.enable) {
|
||||
const es = require('../../db/elasticsearch');
|
||||
|
||||
es.index({
|
||||
index: 'misskey',
|
||||
type: 'post',
|
||||
id: _id.toString(),
|
||||
body: {
|
||||
text: window.document.body.textContent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return createRemoteUserObject('posts', _id, object);
|
||||
}
|
||||
|
||||
export default async function create(parentResolver: Resolver, actor, value): Promise<Array<Promise<IRemoteUserObject>>> {
|
||||
const results = await parentResolver.resolveRemoteUserObjects(value);
|
||||
|
||||
return results.map(promisedResult => promisedResult.then(({ resolver, object }) => {
|
||||
switch (object.type) {
|
||||
case 'Image':
|
||||
return createImage(actor, object);
|
||||
|
||||
case 'Note':
|
||||
return createNote(resolver, actor, object);
|
||||
}
|
||||
|
||||
return null;
|
||||
}));
|
||||
}
|
5
src/remote/activitypub/renderer/context.ts
Normal file
5
src/remote/activitypub/renderer/context.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{ Hashtag: 'as:Hashtag' }
|
||||
];
|
7
src/remote/activitypub/renderer/document.ts
Normal file
7
src/remote/activitypub/renderer/document.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import config from '../../../conf';
|
||||
|
||||
export default ({ _id, contentType }) => ({
|
||||
type: 'Document',
|
||||
mediaType: contentType,
|
||||
url: `${config.drive_url}/${_id}`
|
||||
});
|
8
src/remote/activitypub/renderer/follow.ts
Normal file
8
src/remote/activitypub/renderer/follow.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import config from '../../../conf';
|
||||
import { IRemoteUser } from '../../../models/user';
|
||||
|
||||
export default ({ username }, followee: IRemoteUser) => ({
|
||||
type: 'Follow',
|
||||
actor: `${config.url}/@${username}`,
|
||||
object: followee.account.uri
|
||||
});
|
7
src/remote/activitypub/renderer/hashtag.ts
Normal file
7
src/remote/activitypub/renderer/hashtag.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import config from '../../../conf';
|
||||
|
||||
export default tag => ({
|
||||
type: 'Hashtag',
|
||||
href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
|
||||
name: '#' + tag
|
||||
});
|
6
src/remote/activitypub/renderer/image.ts
Normal file
6
src/remote/activitypub/renderer/image.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import config from '../../../conf';
|
||||
|
||||
export default ({ _id }) => ({
|
||||
type: 'Image',
|
||||
url: `${config.drive_url}/${_id}`
|
||||
});
|
10
src/remote/activitypub/renderer/key.ts
Normal file
10
src/remote/activitypub/renderer/key.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import config from '../../../conf';
|
||||
import { extractPublic } from '../../../crypto_key';
|
||||
import { ILocalUser } from '../../../models/user';
|
||||
|
||||
export default (user: ILocalUser) => ({
|
||||
id: `${config.url}/@${user.username}/publickey`,
|
||||
type: 'Key',
|
||||
owner: `${config.url}/@${user.username}`,
|
||||
publicKeyPem: extractPublic(user.account.keypair)
|
||||
});
|
44
src/remote/activitypub/renderer/note.ts
Normal file
44
src/remote/activitypub/renderer/note.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import renderDocument from './document';
|
||||
import renderHashtag from './hashtag';
|
||||
import config from '../../../conf';
|
||||
import DriveFile from '../../../models/drive-file';
|
||||
import Post from '../../../models/post';
|
||||
import User from '../../../models/user';
|
||||
|
||||
export default async (user, post) => {
|
||||
const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } });
|
||||
let inReplyTo;
|
||||
|
||||
if (post.replyId) {
|
||||
const inReplyToPost = await Post.findOne({
|
||||
_id: post.replyId,
|
||||
});
|
||||
|
||||
if (inReplyToPost !== null) {
|
||||
const inReplyToUser = await User.findOne({
|
||||
_id: post.userId,
|
||||
});
|
||||
|
||||
if (inReplyToUser !== null) {
|
||||
inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inReplyTo = null;
|
||||
}
|
||||
|
||||
const attributedTo = `${config.url}/@${user.username}`;
|
||||
|
||||
return {
|
||||
id: `${attributedTo}/${post._id}`,
|
||||
type: 'Note',
|
||||
attributedTo,
|
||||
content: post.textHtml,
|
||||
published: post.createdAt.toISOString(),
|
||||
to: 'https://www.w3.org/ns/activitystreams#Public',
|
||||
cc: `${attributedTo}/followers`,
|
||||
inReplyTo,
|
||||
attachment: (await promisedFiles).map(renderDocument),
|
||||
tag: post.tags.map(renderHashtag)
|
||||
};
|
||||
};
|
6
src/remote/activitypub/renderer/ordered-collection.ts
Normal file
6
src/remote/activitypub/renderer/ordered-collection.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default (id, totalItems, orderedItems) => ({
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
totalItems,
|
||||
orderedItems
|
||||
});
|
20
src/remote/activitypub/renderer/person.ts
Normal file
20
src/remote/activitypub/renderer/person.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import renderImage from './image';
|
||||
import renderKey from './key';
|
||||
import config from '../../../conf';
|
||||
|
||||
export default user => {
|
||||
const id = `${config.url}/@${user.username}`;
|
||||
|
||||
return {
|
||||
type: 'Person',
|
||||
id,
|
||||
inbox: `${id}/inbox`,
|
||||
outbox: `${id}/outbox`,
|
||||
preferredUsername: user.username,
|
||||
name: user.name,
|
||||
summary: user.description,
|
||||
icon: user.avatarId && renderImage({ _id: user.avatarId }),
|
||||
image: user.bannerId && renderImage({ _id: user.bannerId }),
|
||||
publicKey: renderKey(user)
|
||||
};
|
||||
};
|
109
src/remote/activitypub/resolve-person.ts
Normal file
109
src/remote/activitypub/resolve-person.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { toUnicode } from 'punycode';
|
||||
import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
|
||||
import queue from '../../queue';
|
||||
import webFinger from '../webfinger';
|
||||
import create from './create';
|
||||
import Resolver from './resolver';
|
||||
|
||||
async function isCollection(collection) {
|
||||
return ['Collection', 'OrderedCollection'].includes(collection.type);
|
||||
}
|
||||
|
||||
export default async (value, usernameLower, hostLower, acctLower) => {
|
||||
if (!validateUsername(usernameLower)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const { resolver, object } = await new Resolver().resolveOne(value);
|
||||
|
||||
if (
|
||||
object === null ||
|
||||
object.type !== 'Person' ||
|
||||
typeof object.preferredUsername !== 'string' ||
|
||||
object.preferredUsername.toLowerCase() !== usernameLower ||
|
||||
!isValidName(object.name) ||
|
||||
!isValidDescription(object.summary)
|
||||
) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
const [followers, following, outbox, finger] = await Promise.all([
|
||||
resolver.resolveOne(object.followers).then(
|
||||
resolved => isCollection(resolved.object) ? resolved.object : null,
|
||||
() => null
|
||||
),
|
||||
resolver.resolveOne(object.following).then(
|
||||
resolved => isCollection(resolved.object) ? resolved.object : null,
|
||||
() => null
|
||||
),
|
||||
resolver.resolveOne(object.outbox).then(
|
||||
resolved => isCollection(resolved.object) ? resolved.object : null,
|
||||
() => null
|
||||
),
|
||||
webFinger(object.id, acctLower),
|
||||
]);
|
||||
|
||||
const summaryDOM = JSDOM.fragment(object.summary);
|
||||
|
||||
// Create user
|
||||
const user = await User.insert({
|
||||
avatarId: null,
|
||||
bannerId: null,
|
||||
createdAt: Date.parse(object.published),
|
||||
description: summaryDOM.textContent,
|
||||
followersCount: followers.totalItem,
|
||||
followingCount: following.totalItem,
|
||||
name: object.name,
|
||||
postsCount: outbox.totalItem,
|
||||
driveCapacity: 1024 * 1024 * 8, // 8MiB
|
||||
username: object.preferredUsername,
|
||||
usernameLower,
|
||||
host: toUnicode(finger.subject.replace(/^.*?@/, '')),
|
||||
hostLower,
|
||||
account: {
|
||||
publicKey: {
|
||||
id: object.publicKey.id,
|
||||
publicKeyPem: object.publicKey.publicKeyPem
|
||||
},
|
||||
inbox: object.inbox,
|
||||
uri: object.id,
|
||||
},
|
||||
});
|
||||
|
||||
queue.create('http', {
|
||||
type: 'performActivityPub',
|
||||
actor: user._id,
|
||||
outbox
|
||||
}).save();
|
||||
|
||||
const [avatarId, bannerId] = await Promise.all([
|
||||
object.icon,
|
||||
object.image
|
||||
].map(async value => {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const created = await create(resolver, user, value);
|
||||
|
||||
await Promise.all(created.map(asyncCreated => asyncCreated.then(created => {
|
||||
if (created !== null && created.object.$ref === 'driveFiles.files') {
|
||||
throw created.object.$id;
|
||||
}
|
||||
}, () => {})));
|
||||
|
||||
return null;
|
||||
} catch (id) {
|
||||
return id;
|
||||
}
|
||||
}));
|
||||
|
||||
User.update({ _id: user._id }, { $set: { avatarId, bannerId } });
|
||||
|
||||
user.avatarId = avatarId;
|
||||
user.bannerId = bannerId;
|
||||
|
||||
return user;
|
||||
};
|
97
src/remote/activitypub/resolver.ts
Normal file
97
src/remote/activitypub/resolver.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import RemoteUserObject from '../../models/remote-user-object';
|
||||
import { IObject } from './type';
|
||||
const request = require('request-promise-native');
|
||||
|
||||
type IResult = {
|
||||
resolver: Resolver;
|
||||
object: IObject;
|
||||
};
|
||||
|
||||
export default class Resolver {
|
||||
private requesting: Set<string>;
|
||||
|
||||
constructor(iterable?: Iterable<string>) {
|
||||
this.requesting = new Set(iterable);
|
||||
}
|
||||
|
||||
private async resolveUnrequestedOne(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return { resolver: this, object: value };
|
||||
}
|
||||
|
||||
const resolver = new Resolver(this.requesting);
|
||||
|
||||
resolver.requesting.add(value);
|
||||
|
||||
const object = await request({
|
||||
url: value,
|
||||
headers: {
|
||||
Accept: 'application/activity+json, application/ld+json'
|
||||
},
|
||||
json: true
|
||||
});
|
||||
|
||||
if (object === null || (
|
||||
Array.isArray(object['@context']) ?
|
||||
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
|
||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||
)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return { resolver, object };
|
||||
}
|
||||
|
||||
private async resolveCollection(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const resolved = typeof value === 'string' ?
|
||||
await this.resolveUnrequestedOne(value) :
|
||||
value;
|
||||
|
||||
switch (resolved.type) {
|
||||
case 'Collection':
|
||||
return resolved.items;
|
||||
|
||||
case 'OrderedCollection':
|
||||
return resolved.orderedItems;
|
||||
|
||||
default:
|
||||
return [resolved];
|
||||
}
|
||||
}
|
||||
|
||||
public async resolve(value): Promise<Array<Promise<IResult>>> {
|
||||
const collection = await this.resolveCollection(value);
|
||||
|
||||
return collection
|
||||
.filter(element => !this.requesting.has(element))
|
||||
.map(this.resolveUnrequestedOne.bind(this));
|
||||
}
|
||||
|
||||
public resolveOne(value) {
|
||||
if (this.requesting.has(value)) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return this.resolveUnrequestedOne(value);
|
||||
}
|
||||
|
||||
public async resolveRemoteUserObjects(value) {
|
||||
const collection = await this.resolveCollection(value);
|
||||
|
||||
return collection.filter(element => !this.requesting.has(element)).map(element => {
|
||||
if (typeof element === 'string') {
|
||||
const object = RemoteUserObject.findOne({ uri: element });
|
||||
|
||||
if (object !== null) {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
|
||||
return this.resolveUnrequestedOne(element);
|
||||
});
|
||||
}
|
||||
}
|
3
src/remote/activitypub/type.ts
Normal file
3
src/remote/activitypub/type.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type IObject = {
|
||||
type: string;
|
||||
};
|
Reference in New Issue
Block a user