From dc529711ced031155f53fa321159ec2830ef8b05 Mon Sep 17 00:00:00 2001
From: Akihiko Odaki <nekomanma@pixiv.co.jp>
Date: Sun, 1 Apr 2018 19:43:26 +0900
Subject: [PATCH] Implement remote follow

---
 src/{server/api => }/common/notify.ts         |  8 +-
 .../remote/activitypub/renderer/follow.ts     |  8 ++
 .../remote/activitypub/resolve-person.ts      |  1 +
 src/common/remote/webfinger.ts                |  2 +-
 src/models/user.ts                            |  1 +
 src/processor/http/follow.ts                  | 89 +++++++++++++++++++
 src/processor/http/index.ts                   |  2 +
 src/server/api/endpoints/following/create.ts  | 29 ++----
 src/server/api/endpoints/posts/create.ts      |  2 +-
 src/server/api/endpoints/posts/polls/vote.ts  |  2 +-
 .../api/endpoints/posts/reactions/create.ts   |  2 +-
 11 files changed, 114 insertions(+), 32 deletions(-)
 rename src/{server/api => }/common/notify.ts (86%)
 create mode 100644 src/common/remote/activitypub/renderer/follow.ts
 create mode 100644 src/processor/http/follow.ts

diff --git a/src/server/api/common/notify.ts b/src/common/notify.ts
similarity index 86%
rename from src/server/api/common/notify.ts
rename to 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/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/resolve-person.ts b/src/common/remote/activitypub/resolve-person.ts
index 999a37eea1..c44911a571 100644
--- a/src/common/remote/activitypub/resolve-person.ts
+++ b/src/common/remote/activitypub/resolve-person.ts
@@ -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/webfinger.ts b/src/common/remote/webfinger.ts
index 23f0aaa55f..f5e3d89e1e 100644
--- a/src/common/remote/webfinger.ts
+++ b/src/common/remote/webfinger.ts
@@ -1,6 +1,6 @@
 const WebFinger = require('webfinger.js');
 
-const webFinger = new WebFinger({});
+const webFinger = new WebFinger({ tls_only: false });
 
 type ILink = {
   href: string;
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/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