diff options
Diffstat (limited to 'src')
24 files changed, 247 insertions, 150 deletions
diff --git a/src/server/api/common/notify.ts b/src/common/notify.ts index 69bf8480b0..fc65820d3b 100644 --- a/src/server/api/common/notify.ts +++ b/src/common/notify.ts @@ -1,8 +1,8 @@ import * as mongo from 'mongodb'; -import Notification from '../../../models/notification'; -import Mute from '../../../models/mute'; -import event from '../../../common/event'; -import { pack } from '../../../models/notification'; +import Notification from '../models/notification'; +import Mute from '../models/mute'; +import event from './event'; +import { pack } from '../models/notification'; export default ( notifiee: mongo.ObjectID, diff --git a/src/common/remote/activitypub/act/create.ts b/src/common/remote/activitypub/act/create.ts index 6c62f7ab9e..9eb74800ea 100644 --- a/src/common/remote/activitypub/act/create.ts +++ b/src/common/remote/activitypub/act/create.ts @@ -2,7 +2,7 @@ import create from '../create'; export default (resolver, actor, activity) => { if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error; + throw new Error(); } return create(resolver, actor, activity.object); diff --git a/src/common/remote/activitypub/act/index.ts b/src/common/remote/activitypub/act/index.ts index 0f4084a61e..a76983638f 100644 --- a/src/common/remote/activitypub/act/index.ts +++ b/src/common/remote/activitypub/act/index.ts @@ -3,8 +3,8 @@ import createObject from '../create'; import Resolver from '../resolver'; export default (actor, value) => { - return (new Resolver).resolve(value).then(resolved => Promise.all(resolved.map(async asyncResult => { - const { resolver, object } = await asyncResult; + 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) { @@ -19,4 +19,4 @@ export default (actor, value) => { return null; } }))); -} +}; diff --git a/src/common/remote/activitypub/create.ts b/src/common/remote/activitypub/create.ts index 4aaaeb3060..ea780f01ea 100644 --- a/src/common/remote/activitypub/create.ts +++ b/src/common/remote/activitypub/create.ts @@ -3,6 +3,7 @@ import config from '../../../conf'; import Post from '../../../models/post'; import RemoteUserObject, { IRemoteUserObject } from '../../../models/remote-user-object'; import uploadFromUrl from '../../drive/upload_from_url'; +import Resolver from './resolver'; const createDOMPurify = require('dompurify'); function createRemoteUserObject($ref, $id, { id }) { @@ -17,7 +18,7 @@ function createRemoteUserObject($ref, $id, { id }) { async function createImage(actor, object) { if ('attributedTo' in object && actor.account.uri !== object.attributedTo) { - throw new Error; + throw new Error(); } const { _id } = await uploadFromUrl(object.url, actor); @@ -26,7 +27,7 @@ async function createImage(actor, object) { async function createNote(resolver, actor, object) { if ('attributedTo' in object && actor.account.uri !== object.attributedTo) { - throw new Error; + throw new Error(); } const mediaIds = 'attachment' in object && @@ -69,10 +70,10 @@ async function createNote(resolver, actor, object) { return createRemoteUserObject('posts', _id, object); } -export default async function create(parentResolver, actor, value): Promise<Promise<IRemoteUserObject>[]> { +export default async function create(parentResolver: Resolver, actor, value): Promise<Array<Promise<IRemoteUserObject>>> { const results = await parentResolver.resolveRemoteUserObjects(value); - return results.map(asyncResult => asyncResult.then(({ resolver, object }) => { + return results.map(promisedResult => promisedResult.then(({ resolver, object }) => { switch (object.type) { case 'Image': return createImage(actor, object); @@ -83,4 +84,4 @@ export default async function create(parentResolver, actor, value): Promise<Prom return null; })); -}; +} diff --git a/src/common/remote/activitypub/renderer/follow.ts b/src/common/remote/activitypub/renderer/follow.ts new file mode 100644 index 0000000000..05c0ecca06 --- /dev/null +++ b/src/common/remote/activitypub/renderer/follow.ts @@ -0,0 +1,8 @@ +import config from '../../../../conf'; +import { IRemoteAccount } from '../../../../models/user'; + +export default ({ username }, { account }) => ({ + type: 'Follow', + actor: `${config.url}/@${username}`, + object: (account as IRemoteAccount).uri +}); diff --git a/src/common/remote/activitypub/renderer/key.ts b/src/common/remote/activitypub/renderer/key.ts index 7148c59745..692c71f88e 100644 --- a/src/common/remote/activitypub/renderer/key.ts +++ b/src/common/remote/activitypub/renderer/key.ts @@ -3,6 +3,7 @@ import { extractPublic } from '../../../../crypto_key'; import { ILocalAccount } from '../../../../models/user'; export default ({ username, account }) => ({ + id: `${config.url}/@${username}/publickey`, type: 'Key', owner: `${config.url}/@${username}`, publicKeyPem: extractPublic((account as ILocalAccount).keypair) diff --git a/src/common/remote/activitypub/resolve-person.ts b/src/common/remote/activitypub/resolve-person.ts index 999a37eea1..73584946e5 100644 --- a/src/common/remote/activitypub/resolve-person.ts +++ b/src/common/remote/activitypub/resolve-person.ts @@ -12,10 +12,10 @@ async function isCollection(collection) { export default async (value, usernameLower, hostLower, acctLower) => { if (!validateUsername(usernameLower)) { - throw new Error; + throw new Error(); } - const { resolver, object } = await (new Resolver).resolveOne(value); + const { resolver, object } = await new Resolver().resolveOne(value); if ( object === null || @@ -25,7 +25,7 @@ export default async (value, usernameLower, hostLower, acctLower) => { !isValidName(object.name) || !isValidDescription(object.summary) ) { - throw new Error; + throw new Error(); } const [followers, following, outbox, finger] = await Promise.all([ @@ -66,6 +66,7 @@ export default async (value, usernameLower, hostLower, acctLower) => { id: object.publicKey.id, publicKeyPem: object.publicKey.publicKeyPem }, + inbox: object.inbox, uri: object.id, }, }); diff --git a/src/common/remote/activitypub/resolver.ts b/src/common/remote/activitypub/resolver.ts index 50ac1b0b19..a167fa1339 100644 --- a/src/common/remote/activitypub/resolver.ts +++ b/src/common/remote/activitypub/resolver.ts @@ -7,80 +7,80 @@ type IResult = { object: IObject; }; -async function resolveUnrequestedOne(this: Resolver, value) { - if (typeof value !== 'string') { - return { resolver: this, object: value }; +export default class Resolver { + private requesting: Set<string>; + + constructor(iterable?: Iterable<string>) { + this.requesting = new Set(iterable); } - const resolver = new Resolver(this.requesting); + private async resolveUnrequestedOne(value) { + if (typeof value !== 'string') { + return { resolver: this, object: value }; + } - resolver.requesting.add(value); + const resolver = new Resolver(this.requesting); - const object = await request({ - url: value, - headers: { - Accept: 'application/activity+json, application/ld+json' - }, - json: true - }); + resolver.requesting.add(value); - 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; - } + const object = await request({ + url: value, + headers: { + Accept: 'application/activity+json, application/ld+json' + }, + json: true + }); - return { resolver, object }; -} + 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(); + } -async function resolveCollection(this: Resolver, value) { - if (Array.isArray(value)) { - return value; + return { resolver, object }; } - const resolved = typeof value === 'string' ? - await resolveUnrequestedOne.call(this, value) : - value; - - switch (resolved.type) { - case 'Collection': - return resolved.items; + private async resolveCollection(value) { + if (Array.isArray(value)) { + return value; + } - case 'OrderedCollection': - return resolved.orderedItems; + const resolved = typeof value === 'string' ? + await this.resolveUnrequestedOne(value) : + value; - default: - return [resolved]; - } -} + switch (resolved.type) { + case 'Collection': + return resolved.items; -export default class Resolver { - requesting: Set<string>; + case 'OrderedCollection': + return resolved.orderedItems; - constructor(iterable?: Iterable<string>) { - this.requesting = new Set(iterable); + default: + return [resolved]; + } } - async resolve(value): Promise<Promise<IResult>[]> { - const collection = await resolveCollection.call(this, value); + public async resolve(value): Promise<Array<Promise<IResult>>> { + const collection = await this.resolveCollection(value); return collection .filter(element => !this.requesting.has(element)) - .map(resolveUnrequestedOne.bind(this)); + .map(this.resolveUnrequestedOne.bind(this)); } - resolveOne(value) { + public resolveOne(value) { if (this.requesting.has(value)) { - throw new Error; + throw new Error(); } - return resolveUnrequestedOne.call(this, value); + return this.resolveUnrequestedOne(value); } - async resolveRemoteUserObjects(value) { - const collection = await resolveCollection.call(this, 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') { @@ -91,7 +91,7 @@ export default class Resolver { } } - return resolveUnrequestedOne.call(this, element); + return this.resolveUnrequestedOne(element); }); } } diff --git a/src/common/remote/activitypub/type.ts b/src/common/remote/activitypub/type.ts index 5c4750e140..94e2c350a2 100644 --- a/src/common/remote/activitypub/type.ts +++ b/src/common/remote/activitypub/type.ts @@ -1,3 +1,3 @@ export type IObject = { type: string; -} +}; diff --git a/src/common/remote/resolve-user.ts b/src/common/remote/resolve-user.ts index 13d155830e..4959539da6 100644 --- a/src/common/remote/resolve-user.ts +++ b/src/common/remote/resolve-user.ts @@ -16,7 +16,7 @@ export default async (username, host, option) => { const finger = await webFinger(acctLower, acctLower); const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); if (!self) { - throw new Error; + throw new Error(); } user = await resolvePerson(self.href, usernameLower, hostLower, acctLower); diff --git a/src/common/remote/webfinger.ts b/src/common/remote/webfinger.ts index 23f0aaa55f..fec5da689c 100644 --- a/src/common/remote/webfinger.ts +++ b/src/common/remote/webfinger.ts @@ -1,16 +1,16 @@ const WebFinger = require('webfinger.js'); -const webFinger = new WebFinger({}); +const webFinger = new WebFinger({ }); type ILink = { href: string; rel: string; -} +}; type IWebFinger = { - links: Array<ILink>; + links: ILink[]; subject: string; -} +}; export default (query, verifier): Promise<IWebFinger> => new Promise((res, rej) => webFinger.lookup(query, (error, result) => { if (error) { diff --git a/src/models/drive-folder.ts b/src/models/drive-folder.ts index ad27b151b1..45cc9c9649 100644 --- a/src/models/drive-folder.ts +++ b/src/models/drive-folder.ts @@ -3,7 +3,7 @@ import deepcopy = require('deepcopy'); import db from '../db/mongodb'; import DriveFile from './drive-file'; -const DriveFolder = db.get<IDriveFolder>('drive_folders'); +const DriveFolder = db.get<IDriveFolder>('driveFolders'); export default DriveFolder; export type IDriveFolder = { diff --git a/src/models/user.ts b/src/models/user.ts index 9588c45153..d9ac72b88f 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -70,6 +70,7 @@ export type ILocalAccount = { }; export type IRemoteAccount = { + inbox: string; uri: string; publicKey: { id: string; diff --git a/src/processor/http/follow.ts b/src/processor/http/follow.ts new file mode 100644 index 0000000000..adaa2f3f65 --- /dev/null +++ b/src/processor/http/follow.ts @@ -0,0 +1,89 @@ +import { request } from 'https'; +import { sign } from 'http-signature'; +import { URL } from 'url'; +import User, { ILocalAccount, IRemoteAccount, pack as packUser } from '../../models/user'; +import Following from '../../models/following'; +import event from '../../common/event'; +import notify from '../../common/notify'; +import context from '../../common/remote/activitypub/renderer/context'; +import render from '../../common/remote/activitypub/renderer/follow'; +import config from '../../conf'; + +export default ({ data }, done) => Following.findOne({ _id: data.following }).then(({ followerId, followeeId }) => { + const promisedFollower = User.findOne({ _id: followerId }); + const promisedFollowee = User.findOne({ _id: followeeId }); + + return Promise.all([ + // Increment following count + User.update(followerId, { + $inc: { + followingCount: 1 + } + }), + + // Increment followers count + User.update({ _id: followeeId }, { + $inc: { + followersCount: 1 + } + }), + + // Notify + promisedFollowee.then(followee => followee.host === null ? + notify(followeeId, followerId, 'follow') : null), + + // Publish follow event + Promise.all([promisedFollower, promisedFollowee]).then(([follower, followee]) => { + const followerEvent = packUser(followee, follower) + .then(packed => event(follower._id, 'follow', packed)); + let followeeEvent; + + if (followee.host === null) { + followeeEvent = packUser(follower, followee) + .then(packed => event(followee._id, 'followed', packed)); + } else { + followeeEvent = new Promise((resolve, reject) => { + const { + protocol, + hostname, + port, + pathname, + search + } = new URL(followee.account as IRemoteAccount).inbox); + + const req = request({ + protocol, + hostname, + port, + method: 'POST', + path: pathname + search, + }, res => { + res.on('close', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(); + } else { + reject(res); + } + }); + + res.on('data', () => {}); + res.on('error', reject); + }); + + sign(req, { + authorizationHeaderName: 'Signature', + key: (follower.account as ILocalAccount).keypair, + keyId: `acct:${follower.username}@${config.host}` + }); + + const rendered = render(follower, followee); + rendered['@context'] = context; + + req.end(JSON.stringify(rendered)); + }); + } + + return Promise.all([followerEvent, followeeEvent]); + }) + ]); +}).then(done, done); diff --git a/src/processor/http/index.ts b/src/processor/http/index.ts index da942ad2a1..a001cf11f7 100644 --- a/src/processor/http/index.ts +++ b/src/processor/http/index.ts @@ -1,7 +1,9 @@ +import follow from './follow'; import performActivityPub from './perform-activitypub'; import reportGitHubFailure from './report-github-failure'; const handlers = { + follow, performActivityPub, reportGitHubFailure, }; diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts index c81024d15f..ac7a184f2a 100644 --- a/src/server/activitypub/index.ts +++ b/src/server/activitypub/index.ts @@ -3,6 +3,7 @@ import * as express from 'express'; import user from './user'; import inbox from './inbox'; import outbox from './outbox'; +import publicKey from './publickey'; import post from './post'; const app = express(); @@ -11,6 +12,7 @@ app.disable('x-powered-by'); app.use(user); app.use(inbox); app.use(outbox); +app.use(publicKey); app.use(post); export default app; diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index c5a42ae0a9..c26c4df75d 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -2,44 +2,26 @@ import * as express from 'express'; import context from '../../common/remote/activitypub/renderer/context'; import renderNote from '../../common/remote/activitypub/renderer/note'; import renderOrderedCollection from '../../common/remote/activitypub/renderer/ordered-collection'; -import parseAcct from '../../common/user/parse-acct'; import config from '../../conf'; import Post from '../../models/post'; -import User from '../../models/user'; +import withUser from './with-user'; const app = express(); app.disable('x-powered-by'); -app.get('/@:user/outbox', async (req, res) => { - const { username, host } = parseAcct(req.params.user); - if (host !== null) { - return res.sendStatus(422); - } - - const user = await User.findOne({ - usernameLower: username.toLowerCase(), - host: null - }); - if (user === null) { - return res.sendStatus(404); - } - - const id = `${config.url}/@${user.username}/inbox`; - - if (username !== user.username) { - return res.redirect(id); - } - +app.get('/@:user/outbox', withUser(username => { + return `${config.url}/@${username}/inbox`; +}, async (user, req, res) => { const posts = await Post.find({ userId: user._id }, { limit: 20, sort: { _id: -1 } }); const renderedPosts = await Promise.all(posts.map(post => renderNote(user, post))); - const rendered = renderOrderedCollection(id, user.postsCount, renderedPosts); + const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.postsCount, renderedPosts); rendered['@context'] = context; res.json(rendered); -}); +})); export default app; diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts new file mode 100644 index 0000000000..e380309dc4 --- /dev/null +++ b/src/server/activitypub/publickey.ts @@ -0,0 +1,19 @@ +import * as express from 'express'; +import context from '../../common/remote/activitypub/renderer/context'; +import render from '../../common/remote/activitypub/renderer/key'; +import config from '../../conf'; +import withUser from './with-user'; + +const app = express(); +app.disable('x-powered-by'); + +app.get('/@:user/publickey', withUser(username => { + return `${config.url}/@${username}/publickey`; +}, (user, req, res) => { + const rendered = render(user); + rendered['@context'] = context; + + res.json(rendered); +})); + +export default app; diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts index d43a9793d4..cfda409e2c 100644 --- a/src/server/activitypub/user.ts +++ b/src/server/activitypub/user.ts @@ -2,39 +2,26 @@ import * as express from 'express'; import config from '../../conf'; import context from '../../common/remote/activitypub/renderer/context'; import render from '../../common/remote/activitypub/renderer/person'; -import parseAcct from '../../common/user/parse-acct'; -import User from '../../models/user'; +import withUser from './with-user'; + +const respond = withUser(username => `${config.url}/@${username}`, (user, req, res) => { + const rendered = render(user); + rendered['@context'] = context; + + res.json(rendered); +}); const app = express(); app.disable('x-powered-by'); -app.get('/@:user', async (req, res, next) => { +app.get('/@:user', (req, res, next) => { const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); - if (!(['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) { - return next(); - } - - const { username, host } = parseAcct(req.params.user); - if (host !== null) { - return res.sendStatus(422); - } - - const user = await User.findOne({ - usernameLower: username.toLowerCase(), - host: null - }); - if (user === null) { - return res.sendStatus(404); - } - if (username !== user.username) { - return res.redirect(`${config.url}/@${user.username}`); + if ((['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) { + respond(req, res, next); + } else { + next(); } - - const rendered = render(user); - rendered['@context'] = context; - - res.json(rendered); }); export default app; diff --git a/src/server/activitypub/with-user.ts b/src/server/activitypub/with-user.ts new file mode 100644 index 0000000000..ed289b641b --- /dev/null +++ b/src/server/activitypub/with-user.ts @@ -0,0 +1,23 @@ +import parseAcct from '../../common/user/parse-acct'; +import User from '../../models/user'; + +export default (redirect, respond) => async (req, res, next) => { + const { username, host } = parseAcct(req.params.user); + if (host !== null) { + return res.sendStatus(422); + } + + const user = await User.findOne({ + usernameLower: username.toLowerCase(), + host: null + }); + if (user === null) { + return res.sendStatus(404); + } + + if (username !== user.username) { + return res.redirect(redirect(user.username)); + } + + return respond(user, req, res, next); +}; diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index a689250e35..03c13ab7fc 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -2,10 +2,9 @@ * Module dependencies */ import $ from 'cafy'; -import User, { pack as packUser } from '../../../../models/user'; +import User from '../../../../models/user'; import Following from '../../../../models/following'; -import notify from '../../common/notify'; -import event from '../../../../common/event'; +import queue from '../../../../queue'; /** * Follow a user @@ -52,33 +51,15 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Create following - await Following.insert({ + const { _id } = await Following.insert({ createdAt: new Date(), followerId: follower._id, followeeId: followee._id }); + queue.create('http', { type: 'follow', following: _id }).save(); + // Send response res(); - // Increment following count - User.update(follower._id, { - $inc: { - followingCount: 1 - } - }); - - // Increment followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: 1 - } - }); - - // Publish follow event - event(follower._id, 'follow', await packUser(followee, follower)); - event(followee._id, 'followed', await packUser(follower, followee)); - - // Notify - notify(followee._id, follower._id, 'follow'); }); diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts index 42901ebcbf..6e7d2329a7 100644 --- a/src/server/api/endpoints/posts/create.ts +++ b/src/server/api/endpoints/posts/create.ts @@ -14,9 +14,9 @@ import DriveFile from '../../../../models/drive-file'; import Watching from '../../../../models/post-watching'; import ChannelWatching from '../../../../models/channel-watching'; import { pack } from '../../../../models/post'; -import notify from '../../common/notify'; import watch from '../../common/watch-post'; import event, { pushSw, publishChannelStream } from '../../../../common/event'; +import notify from '../../../../common/notify'; import getAcct from '../../../../common/user/get-acct'; import parseAcct from '../../../../common/user/parse-acct'; import config from '../../../../conf'; diff --git a/src/server/api/endpoints/posts/polls/vote.ts b/src/server/api/endpoints/posts/polls/vote.ts index 98df074e5d..59b1f099fb 100644 --- a/src/server/api/endpoints/posts/polls/vote.ts +++ b/src/server/api/endpoints/posts/polls/vote.ts @@ -5,9 +5,9 @@ import $ from 'cafy'; import Vote from '../../../../../models/poll-vote'; import Post from '../../../../../models/post'; import Watching from '../../../../../models/post-watching'; -import notify from '../../../common/notify'; import watch from '../../../common/watch-post'; import { publishPostStream } from '../../../../../common/event'; +import notify from '../../../../../common/notify'; /** * Vote poll of a post diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts index 8db76d6436..441d563835 100644 --- a/src/server/api/endpoints/posts/reactions/create.ts +++ b/src/server/api/endpoints/posts/reactions/create.ts @@ -6,9 +6,9 @@ import Reaction from '../../../../../models/post-reaction'; import Post, { pack as packPost } from '../../../../../models/post'; import { pack as packUser } from '../../../../../models/user'; import Watching from '../../../../../models/post-watching'; -import notify from '../../../common/notify'; import watch from '../../../common/watch-post'; import { publishPostStream, pushSw } from '../../../../../common/event'; +import notify from '../../../../../common/notify'; /** * React to a post |