From ef44eda69eefbdeeb1efee1c8351be081938cae5 Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Thu, 18 Jul 2019 00:11:39 +0900
Subject: [PATCH] =?UTF-8?q?Mastodon=E3=81=AE=E3=83=AA=E3=83=B3=E3=82=AF?=
 =?UTF-8?q?=E3=81=AE=E6=89=80=E6=9C=89=E8=80=85=E8=AA=8D=E8=A8=BC=E3=81=AB?=
 =?UTF-8?q?=E5=AF=BE=E5=BF=9C=20(#5161)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Profile metadata を設定できるように

* API desc
---
 locales/ja-JP.yml                             |  3 ++
 .../views/components/settings/profile.vue     | 46 +++++++++++++++++++
 src/models/repositories/user.ts               |  1 +
 src/remote/activitypub/renderer/person.ts     | 15 +++++-
 src/server/api/endpoints/i/update.ts          | 15 ++++++
 src/server/web/index.ts                       | 10 +++-
 src/server/web/views/base.pug                 |  1 +
 src/server/web/views/user.pug                 |  5 ++
 8 files changed, 92 insertions(+), 4 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 76fc26381..b6bbb7e96 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -804,6 +804,9 @@ common/views/components/profile-editor.vue:
   danger-zone: "危険な設定"
   delete-account: "アカウントを削除"
   account-deleted: "アカウントが削除されました。データが消えるまで時間がかかる場合があります。"
+  profile-metadata: "プロフィール補足情報"
+  metadata-label: "ラベル"
+  metadata-content: "内容"
 
 common/views/components/user-list-editor.vue:
   users: "ユーザー"
diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue
index 52ec8ceda..edfc5a9ed 100644
--- a/src/client/app/common/views/components/settings/profile.vue
+++ b/src/client/app/common/views/components/settings/profile.vue
@@ -51,6 +51,26 @@
 				<template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
 			</ui-input>
 
+			<div class="fields">
+				<header>{{ $t('profile-metadata') }}</header>
+				<ui-horizon-group>
+					<ui-input v-model="fieldName0">{{ $t('metadata-label') }}</ui-input>
+					<ui-input v-model="fieldValue0">{{ $t('metadata-content') }}</ui-input>
+				</ui-horizon-group>
+				<ui-horizon-group>
+					<ui-input v-model="fieldName1">{{ $t('metadata-label') }}</ui-input>
+					<ui-input v-model="fieldValue1">{{ $t('metadata-content') }}</ui-input>
+				</ui-horizon-group>
+				<ui-horizon-group>
+					<ui-input v-model="fieldName2">{{ $t('metadata-label') }}</ui-input>
+					<ui-input v-model="fieldValue2">{{ $t('metadata-content') }}</ui-input>
+				</ui-horizon-group>
+				<ui-horizon-group>
+					<ui-input v-model="fieldName3">{{ $t('metadata-label') }}</ui-input>
+					<ui-input v-model="fieldValue3">{{ $t('metadata-content') }}</ui-input>
+				</ui-horizon-group>
+			</div>
+
 			<ui-button @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
 		</ui-form>
 	</section>
@@ -189,6 +209,17 @@ export default Vue.extend({
 		this.isLocked = this.$store.state.i.isLocked;
 		this.carefulBot = this.$store.state.i.carefulBot;
 		this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
+
+		if (this.$store.state.i.fields) {
+			this.fieldName0 = this.$store.state.i.fields[0].name;
+			this.fieldValue0 = this.$store.state.i.fields[0].value;
+			this.fieldName1 = this.$store.state.i.fields[1].name;
+			this.fieldValue1 = this.$store.state.i.fields[1].value;
+			this.fieldName2 = this.$store.state.i.fields[2].name;
+			this.fieldValue2 = this.$store.state.i.fields[2].value;
+			this.fieldName3 = this.$store.state.i.fields[3].name;
+			this.fieldValue3 = this.$store.state.i.fields[3].value;
+		}
 	},
 
 	methods: {
@@ -237,6 +268,13 @@ export default Vue.extend({
 		},
 
 		save(notify) {
+			const fields = [
+				{ name: this.fieldName0, value: this.fieldValue0 },
+				{ name: this.fieldName1, value: this.fieldValue1 },
+				{ name: this.fieldName2, value: this.fieldValue2 },
+				{ name: this.fieldName3, value: this.fieldValue3 },
+			];
+
 			this.saving = true;
 
 			this.$root.api('i/update', {
@@ -247,6 +285,7 @@ export default Vue.extend({
 				birthday: this.birthday || null,
 				avatarId: this.avatarId || undefined,
 				bannerId: this.bannerId || undefined,
+				fields,
 				isCat: !!this.isCat,
 				isBot: !!this.isBot,
 				isLocked: !!this.isLocked,
@@ -389,4 +428,11 @@ export default Vue.extend({
 			height 72px
 			margin auto
 
+.fields
+	> header
+		padding 8px 0px
+		font-weight bold
+	> div
+		padding-left 16px
+
 </style>
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 4e85fd7b9..a04b87f77 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -148,6 +148,7 @@ export class UserRepository extends Repository<User> {
 				description: profile!.description,
 				location: profile!.location,
 				birthday: profile!.birthday,
+				fields: profile!.fields,
 				followersCount: user.followersCount,
 				followingCount: user.followingCount,
 				notesCount: user.notesCount,
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index efe52cdef..d4c018fb7 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -21,13 +21,24 @@ export async function renderPerson(user: ILocalUser) {
 	]);
 
 	const attachment: {
-		type: string,
+		type: 'PropertyValue',
 		name: string,
 		value: string,
-		verified_at?: string,
 		identifier?: IIdentifier
 	}[] = [];
 
+	if (profile.fields) {
+		for (const field of profile.fields) {
+			attachment.push({
+				type: 'PropertyValue',
+				name: field.name,
+				value: (field.value != null && field.value.match(/^https?:/))
+					? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
+					: field.value
+			});
+		}
+	}
+
 	if (profile.twitter) {
 		attachment.push({
 			type: 'PropertyValue',
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index a454cdb94..149081e50 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -77,6 +77,13 @@ export const meta = {
 			}
 		},
 
+		fields: {
+			validator: $.optional.arr($.object()).range(1, 4),
+			desc: {
+				'ja-JP': 'プロフィール補足情報'
+			}
+		},
+
 		isLocked: {
 			validator: $.optional.bool,
 			desc: {
@@ -226,6 +233,14 @@ export default define(meta, async (ps, user, app) => {
 		profileUpdates.pinnedPageId = null;
 	}
 
+	if (ps.fields) {
+		profileUpdates.fields = ps.fields
+			.filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
+			.map(x => {
+				return { name: x.name, value: x.value };
+			});
+	}
+
 	//#region emojis/tags
 
 	let emojis = [] as string[];
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 8cf6a7520..6c41bbde4 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -156,11 +156,17 @@ router.get('/@:user', async (ctx, next) => {
 	if (user != null) {
 		const profile = await UserProfiles.findOne(user.id).then(ensure);
 		const meta = await fetchMeta();
+		const me = profile.fields
+			? profile.fields
+				.filter(filed => filed.value != null && filed.value.match(/^https?:/))
+				.map(field => field.value)
+			: [];
+
 		await ctx.render('user', {
-			user, profile,
+			user, profile, me,
 			instanceName: meta.name || 'Misskey'
 		});
-		ctx.set('Cache-Control', 'public, max-age=180');
+		ctx.set('Cache-Control', 'public, max-age=30');
 	} else {
 		// リモートユーザーなので
 		await next();
diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug
index 733a306d5..16bea853e 100644
--- a/src/server/web/views/base.pug
+++ b/src/server/web/views/base.pug
@@ -44,3 +44,4 @@ html
 			<svg viewBox="0 0 50 50">
 				<path fill=#fb4e4e d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z" />
 			</svg>
+		block content
diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug
index 9b257afb7..6ff86b09b 100644
--- a/src/server/web/views/user.pug
+++ b/src/server/web/views/user.pug
@@ -36,3 +36,8 @@ block meta
 		link(rel='alternate' href=user.uri type='application/activity+json')
 	if profile.url
 		link(rel='alternate' href=profile.url type='text/html')
+
+block content
+	div#me
+		each m in me
+			a(rel='me' href=`${m}`) #{m}