From 3dfdab9aba8485381ef7af5f5df83e95f14a41dc Mon Sep 17 00:00:00 2001
From: mei23 <m@m544.net>
Date: Sat, 1 Sep 2018 16:39:46 +0900
Subject: [PATCH 1/5] Set Person.updatedAt at first

---
 src/remote/activitypub/models/person.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index 3bd4e1676..212a1c5ee 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -139,6 +139,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,

From fd4566b9e75ba023961c01c2c59ba5df18f72ac5 Mon Sep 17 00:00:00 2001
From: mei23 <m@m544.net>
Date: Sat, 1 Sep 2018 16:46:41 +0900
Subject: [PATCH 2/5] Add missing updatePerson properties

---
 src/remote/activitypub/models/person.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index 212a1c5ee..024679861 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -291,7 +291,9 @@ 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
 		}
 	});
 }

From 22c7d953f40b311591d73ffea5a5cd6b9771be47 Mon Sep 17 00:00:00 2001
From: mei23 <m@m544.net>
Date: Sat, 1 Sep 2018 16:55:11 +0900
Subject: [PATCH 3/5] =?UTF-8?q?updatePerson=E3=81=A7=E5=86=8D=E5=89=B2?=
 =?UTF-8?q?=E3=82=8A=E5=BD=93=E3=81=A6=E3=82=92=E8=80=83=E6=85=AE=E3=81=99?=
 =?UTF-8?q?=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/remote/activitypub/models/person.ts | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index 024679861..9c770141a 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -293,7 +293,12 @@ export async function updatePerson(uri: string, resolver?: Resolver): Promise<vo
 			endpoints: person.endpoints,
 			isBot: object.type == 'Service',
 			isCat: (person as any).isCat === true ? true : false,
-			isLocked: person.manuallyApprovesFollowers
+			isLocked: person.manuallyApprovesFollowers,
+			createdAt: Date.parse(person.published) || null,
+			publicKey: {
+				id: person.publicKey.id,
+				publicKeyPem: person.publicKey.publicKeyPem
+			},
 		}
 	});
 }

From 36e85a02d5f15d737bec6153c9cc04fe61ee7473 Mon Sep 17 00:00:00 2001
From: mei23 <m@m544.net>
Date: Sat, 1 Sep 2018 17:53:38 +0900
Subject: [PATCH 4/5] Receive Update activity

---
 src/queue/processors/http/process-inbox.ts | 28 +++++++++++++++-------
 src/remote/activitypub/models/person.ts    |  8 ++++---
 2 files changed, 24 insertions(+), 12 deletions(-)

diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts
index 7e564dd32..8e6b3769d 100644
--- a/src/queue/processors/http/process-inbox.ts
+++ b/src/queue/processors/http/process-inbox.ts
@@ -5,7 +5,7 @@ const httpSignature = require('http-signature');
 import parseAcct from '../../../misc/acct/parse';
 import User, { IRemoteUser } from '../../../models/user';
 import perform from '../../../remote/activitypub/perform';
-import { resolvePerson } from '../../../remote/activitypub/models/person';
+import { resolvePerson, updatePerson } from '../../../remote/activitypub/models/person';
 import { toUnicode } from 'punycode';
 import { URL } from 'url';
 
@@ -44,11 +44,6 @@ export default async (job: bq.Job, done: any): Promise<void> => {
 		}
 
 		user = await User.findOne({ usernameLower: username, host: host.toLowerCase() }) as IRemoteUser;
-
-		// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
-		if (user === null) {
-			user = await resolvePerson(activity.actor) as IRemoteUser;
-		}
 	} else {
 		// アクティビティ内のホストの検証
 		const host = toUnicode(new URL(signature.keyId).hostname.toLowerCase());
@@ -64,11 +59,26 @@ export default async (job: bq.Job, done: any): Promise<void> => {
 			host: { $ne: null },
 			'publicKey.id': signature.keyId
 		}) as IRemoteUser;
+	}
 
-		// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
-		if (user === null) {
-			user = await resolvePerson(activity.actor) as IRemoteUser;
+	// Update activityの場合は、ここで署名検証/更新処理まで実施して終了
+	if (activity.type === 'Update') {
+		if (activity.object && activity.object.type === 'Person') {
+			if (user == null) {
+				console.warn('Update activity received, but user not registed.');
+			} else if (!httpSignature.verifySignature(signature, user.publicKey.publicKeyPem)) {
+				console.warn('Update activity received, but signature verification failed.');
+			} else {
+				updatePerson(activity.actor, null, activity.object);
+			}
 		}
+		done();
+		return;
+	}
+
+	// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
+	if (user === null) {
+		user = await resolvePerson(activity.actor) as IRemoteUser;
 	}
 
 	if (user === null) {
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index 9c770141a..dff38f546 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -216,10 +216,12 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
 
 /**
  * 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がこのサーバーを指しているならスキップ
@@ -237,7 +239,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);
 

From 036e94c368c71c3217521b0642aad343b42abd00 Mon Sep 17 00:00:00 2001
From: mei23 <m@m544.net>
Date: Sat, 1 Sep 2018 20:17:30 +0900
Subject: [PATCH 5/5] Send Update activity

---
 src/remote/activitypub/renderer/update.ts | 14 +++++++++
 src/server/api/endpoints/i/update.ts      |  4 +++
 src/services/i/update.ts                  | 38 +++++++++++++++++++++++
 3 files changed, 56 insertions(+)
 create mode 100644 src/remote/activitypub/renderer/update.ts
 create mode 100644 src/services/i/update.ts

diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts
new file mode 100644
index 000000000..cf9acc9ac
--- /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/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index cdb4eb3f5..585339e24 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -5,6 +5,7 @@ import DriveFile from '../../../../models/drive-file';
 import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
 import { IApp } from '../../../../models/app';
 import config from '../../../../config';
+import { publishToFollowers } from '../../../../services/i/update';
 
 export const meta = {
 	desc: {
@@ -144,4 +145,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a
 	if (user.isLocked && isLocked === false) {
 		acceptAllFollowRequests(user);
 	}
+
+	// フォロワーにUpdateを配信
+	publishToFollowers(user._id);
 });
diff --git a/src/services/i/update.ts b/src/services/i/update.ts
new file mode 100644
index 000000000..25b55b035
--- /dev/null
+++ b/src/services/i/update.ts
@@ -0,0 +1,38 @@
+import * as mongo from 'mongodb';
+import User, { isLocalUser, isRemoteUser } from '../../models/user';
+import Following from '../../models/following';
+import renderPerson from '../../remote/activitypub/renderer/person';
+import renderUpdate from '../../remote/activitypub/renderer/update';
+import packAp from '../../remote/activitypub/renderer';
+import { deliver } from '../../queue';
+
+export async function publishToFollowers(userId: mongo.ObjectID) {
+	const user = await User.findOne({
+		_id: userId
+	});
+
+	const followers = await Following.find({
+		followeeId: user._id
+	});
+
+	const queue: string[] = [];
+
+	// フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信
+	if (isLocalUser(user)) {
+		followers.map(following => {
+			const follower = following._follower;
+
+			if (isRemoteUser(follower)) {
+				const inbox = follower.sharedInbox || follower.inbox;
+				if (!queue.includes(inbox)) queue.push(inbox);
+			}
+		});
+
+		if (queue.length > 0) {
+			const content = packAp(renderUpdate(await renderPerson(user), user));
+			queue.forEach(inbox => {
+				deliver(user, content, inbox);
+			});
+		}
+	}
+}