summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2018-06-02 16:28:08 +0900
committerGitHub <noreply@github.com>2018-06-02 16:28:08 +0900
commite25e1d88d60c4d427635e51609a6ecbfe7b6049b (patch)
treeb3b9890e83527d0d257c819a2c61981516945e21 /src
parentMerge pull request #1672 from Angristan/patch-1 (diff)
parentwip (diff)
downloadmisskey-e25e1d88d60c4d427635e51609a6ecbfe7b6049b.tar.gz
misskey-e25e1d88d60c4d427635e51609a6ecbfe7b6049b.tar.bz2
misskey-e25e1d88d60c4d427635e51609a6ecbfe7b6049b.zip
Merge pull request #1671 from syuilo/locked-account
Locked account
Diffstat (limited to 'src')
-rw-r--r--src/client/app/common/scripts/streaming/home.ts2
-rw-r--r--src/client/app/desktop/views/components/follow-button.vue76
-rw-r--r--src/client/app/desktop/views/components/notifications.vue21
-rw-r--r--src/client/app/desktop/views/components/received-follow-requests-window.vue72
-rw-r--r--src/client/app/desktop/views/components/settings.profile.vue11
-rw-r--r--src/client/app/desktop/views/components/ui.header.account.vue18
-rw-r--r--src/client/app/desktop/views/components/user-lists-window.vue2
-rw-r--r--src/client/app/mobile/script.ts2
-rw-r--r--src/client/app/mobile/views/components/follow-button.vue105
-rw-r--r--src/client/app/mobile/views/components/notification-preview.vue35
-rw-r--r--src/client/app/mobile/views/components/notification.vue15
-rw-r--r--src/client/app/mobile/views/components/ui.nav.vue1
-rw-r--r--src/client/app/mobile/views/pages/received-follow-requests.vue78
-rw-r--r--src/client/app/mobile/views/pages/user.vue1
-rw-r--r--src/models/follow-request.ts87
-rw-r--r--src/models/notification.ts1
-rw-r--r--src/models/user.ts39
-rw-r--r--src/remote/activitypub/kernel/accept/follow.ts27
-rw-r--r--src/remote/activitypub/kernel/accept/index.ts35
-rw-r--r--src/remote/activitypub/kernel/follow.ts2
-rw-r--r--src/remote/activitypub/kernel/index.ts8
-rw-r--r--src/remote/activitypub/kernel/reject/follow.ts27
-rw-r--r--src/remote/activitypub/kernel/reject/index.ts35
-rw-r--r--src/remote/activitypub/kernel/undo/follow.ts13
-rw-r--r--src/remote/activitypub/models/person.ts1
-rw-r--r--src/remote/activitypub/renderer/follow.ts8
-rw-r--r--src/remote/activitypub/renderer/person.ts4
-rw-r--r--src/remote/activitypub/renderer/reject.ts4
-rw-r--r--src/remote/activitypub/type.ts6
-rw-r--r--src/server/activitypub.ts19
-rw-r--r--src/server/api/endpoints.ts20
-rw-r--r--src/server/api/endpoints/following/create.ts4
-rw-r--r--src/server/api/endpoints/following/delete.ts4
-rw-r--r--src/server/api/endpoints/following/requests/accept.ts26
-rw-r--r--src/server/api/endpoints/following/requests/cancel.ts26
-rw-r--r--src/server/api/endpoints/following/requests/list.ts14
-rw-r--r--src/server/api/endpoints/following/requests/reject.ts26
-rw-r--r--src/server/api/endpoints/i/update.ts58
-rw-r--r--src/server/api/endpoints/users/recommendation.ts1
-rw-r--r--src/server/api/service/twitter.ts4
-rw-r--r--src/services/following/create.ts118
-rw-r--r--src/services/following/delete.ts2
-rw-r--r--src/services/following/requests/accept-all.ts24
-rw-r--r--src/services/following/requests/accept.ts70
-rw-r--r--src/services/following/requests/cancel.ts29
-rw-r--r--src/services/following/requests/create.ts50
-rw-r--r--src/services/following/requests/reject.ts24
47 files changed, 1048 insertions, 207 deletions
diff --git a/src/client/app/common/scripts/streaming/home.ts b/src/client/app/common/scripts/streaming/home.ts
index 50bbb56896..f07d0289f6 100644
--- a/src/client/app/common/scripts/streaming/home.ts
+++ b/src/client/app/common/scripts/streaming/home.ts
@@ -20,7 +20,7 @@ export class HomeStream extends Stream {
}, 1000 * 60);
// 自分の情報が更新されたとき
- this.on('i_updated', i => {
+ this.on('meUpdated', i => {
if (os.debug) {
console.log('I updated:', i);
}
diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue
index dae7604957..c7549c374e 100644
--- a/src/client/app/desktop/views/components/follow-button.vue
+++ b/src/client/app/desktop/views/components/follow-button.vue
@@ -1,19 +1,16 @@
<template>
<button class="mk-follow-button"
- :class="{ wait, follow: !user.isFollowing, unfollow: user.isFollowing, big: size == 'big' }"
+ :class="{ wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou, big: size == 'big' }"
@click="onClick"
:disabled="wait"
- :title="user.isFollowing ? '%i18n:@unfollow%' : '%i18n:@follow%'"
>
- <template v-if="!wait && user.isFollowing">
- <template v-if="size == 'compact'">%fa:minus%</template>
- <template v-if="size == 'big'">%fa:minus%%i18n:@unfollow%</template>
+ <template v-if="!wait">
+ <template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half%<template v-if="size == 'big'"> %i18n:@request-pending%</template></template>
+ <template v-else-if="u.isFollowing">%fa:minus%<template v-if="size == 'big'"> %i18n:@unfollow%</template></template>
+ <template v-else-if="!u.isFollowing && u.isLocked">%fa:plus%<template v-if="size == 'big'"> %i18n:@follow-request%</template></template>
+ <template v-else-if="!u.isFollowing && !u.isLocked">%fa:plus%<template v-if="size == 'big'"> %i18n:@follow%</template></template>
</template>
- <template v-if="!wait && !user.isFollowing">
- <template v-if="size == 'compact'">%fa:plus%</template>
- <template v-if="size == 'big'">%fa:plus%%i18n:@follow%</template>
- </template>
- <template v-if="wait">%fa:spinner .pulse .fw%</template>
+ <template v-else>%fa:spinner .pulse .fw%</template>
</button>
</template>
@@ -34,6 +31,7 @@ export default Vue.extend({
data() {
return {
+ u: this.user,
wait: false,
connection: null,
connectionId: null
@@ -56,39 +54,44 @@ export default Vue.extend({
methods: {
onFollow(user) {
- if (user.id == this.user.id) {
+ if (user.id == this.u.id) {
this.user.isFollowing = user.isFollowing;
}
},
onUnfollow(user) {
- if (user.id == this.user.id) {
+ if (user.id == this.u.id) {
this.user.isFollowing = user.isFollowing;
}
},
- onClick() {
+ async onClick() {
this.wait = true;
- if (this.user.isFollowing) {
- (this as any).api('following/delete', {
- userId: this.user.id
- }).then(() => {
- this.user.isFollowing = false;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.wait = false;
- });
- } else {
- (this as any).api('following/create', {
- userId: this.user.id
- }).then(() => {
- this.user.isFollowing = true;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.wait = false;
- });
+
+ try {
+ if (this.u.isFollowing) {
+ this.u = await (this as any).api('following/delete', {
+ userId: this.u.id
+ });
+ } else {
+ if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
+ this.u = await (this as any).api('following/requests/cancel', {
+ userId: this.u.id
+ });
+ } else if (this.u.isLocked) {
+ this.u = await (this as any).api('following/create', {
+ userId: this.u.id
+ });
+ } else {
+ this.u = await (this as any).api('following/create', {
+ userId: this.user.id
+ });
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.wait = false;
}
}
}
@@ -124,7 +127,7 @@ root(isDark)
border 2px solid rgba($theme-color, 0.3)
border-radius 8px
- &.follow
+ &:not(.active)
color isDark ? #fff : #888
background isDark ? linear-gradient(to bottom, #313543 0%, #282c37 100%) : linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
border solid 1px isDark ? #1c2023 : #e2e2e2
@@ -137,7 +140,7 @@ root(isDark)
background isDark ? #22262f : #ececec
border-color isDark ? #151a1d : #dcdcdc
- &.unfollow
+ &.active
color $theme-color-foreground
background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
border solid 1px lighten($theme-color, 15%)
@@ -162,9 +165,6 @@ root(isDark)
height 38px
line-height 38px
- i
- margin-right 8px
-
.mk-follow-button[data-darkmode]
root(true)
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 5564dad623..f2247a782c 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -5,6 +5,7 @@
<template v-for="(notification, i) in _notifications">
<div class="notification" :class="notification.type" :key="notification.id">
<mk-time :time="notification.createdAt"/>
+
<template v-if="notification.type == 'reaction'">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
@@ -17,6 +18,7 @@
</router-link>
</div>
</template>
+
<template v-if="notification.type == 'renote'">
<mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
@@ -28,6 +30,7 @@
</router-link>
</div>
</template>
+
<template v-if="notification.type == 'quote'">
<mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
@@ -37,6 +40,7 @@
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
</div>
</template>
+
<template v-if="notification.type == 'follow'">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
@@ -45,6 +49,16 @@
</p>
</div>
</template>
+
+ <template v-if="notification.type == 'receiveFollowRequest'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div class="text">
+ <p>%fa:user-clock%
+ <router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
+ </p>
+ </div>
+ </template>
+
<template v-if="notification.type == 'reply'">
<mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
@@ -54,6 +68,7 @@
<router-link class="note-preview" :to="notification.note | notePage">{{ getNoteSummary(notification.note) }}</router-link>
</div>
</template>
+
<template v-if="notification.type == 'mention'">
<mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
@@ -63,6 +78,7 @@
<a class="note-preview" :href="notification.note | notePage">{{ getNoteSummary(notification.note) }}</a>
</div>
</template>
+
<template v-if="notification.type == 'poll_vote'">
<mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
@@ -73,6 +89,7 @@
</div>
</template>
</div>
+
<p class="date" v-if="i != notifications.length - 1 && notification._date != _notifications[i + 1]._date" :key="notification.id + '-time'">
<span>%fa:angle-up%{{ notification._datetext }}</span>
<span>%fa:angle-down%{{ _notifications[i + 1]._datetext }}</span>
@@ -251,6 +268,10 @@ root(isDark)
.text p i
color #53c7ce
+ &.receiveFollowRequest
+ .text p i
+ color #888
+
&.reply, &.mention
.text p i
color #555
diff --git a/src/client/app/desktop/views/components/received-follow-requests-window.vue b/src/client/app/desktop/views/components/received-follow-requests-window.vue
new file mode 100644
index 0000000000..fd37c0a6aa
--- /dev/null
+++ b/src/client/app/desktop/views/components/received-follow-requests-window.vue
@@ -0,0 +1,72 @@
+<template>
+<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
+ <span slot="header">%fa:envelope R% %i18n:@title%</span>
+
+ <div data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4" :data-darkmode="$store.state.device.darkmode">
+ <div v-for="req in requests">
+ <router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
+ <span>
+ <a @click="accept(req.follower)">%i18n:@accept%</a>|<a @click="reject(req.follower)">%i18n:@reject%</a>
+ </span>
+ </div>
+ </div>
+</mk-window>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ requests: []
+ };
+ },
+ mounted() {
+ (this as any).api('following/requests/list').then(requests => {
+ this.fetching = false;
+ this.requests = requests;
+ });
+ },
+ methods: {
+ accept(user) {
+ (this as any).api('following/requests/accept', { userId: user.id }).then(() => {
+ this.requests = this.requests.filter(r => r.follower.id != user.id);
+ });
+ },
+ reject(user) {
+ (this as any).api('following/requests/reject', { userId: user.id }).then(() => {
+ this.requests = this.requests.filter(r => r.follower.id != user.id);
+ });
+ },
+ close() {
+ (this as any).$refs.window.close();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+
+root(isDark)
+ padding 16px
+
+ > button
+ margin-bottom 16px
+
+ > div
+ display flex
+ padding 16px
+ border solid 1px isDark ? #1c2023 : #eee
+ border-radius 4px
+
+ > span
+ margin 0 0 0 auto
+
+[data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4"][data-darkmode]
+ root(true)
+
+[data-id="c1136cec-1278-49b1-9ea7-412c1ef794f4"]:not([data-darkmode])
+ root(false)
+
+</style>
diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue
index 9932cbf7db..0b3a25f389 100644
--- a/src/client/app/desktop/views/components/settings.profile.vue
+++ b/src/client/app/desktop/views/components/settings.profile.vue
@@ -23,7 +23,11 @@
</label>
<button class="ui primary" @click="save">%i18n:@save%</button>
<section>
- <h2>その他</h2>
+ <h2>%i18n:@locked-account%</h2>
+ <mk-switch v-model="$store.state.i.isLocked" @change="onChangeIsLocked" text="%i18n:@is-locked%"/>
+ </section>
+ <section>
+ <h2>%i18n:@other%</h2>
<mk-switch v-model="$store.state.i.isBot" @change="onChangeIsBot" text="%i18n:@is-bot%"/>
<mk-switch v-model="$store.state.i.isCat" @change="onChangeIsCat" text="%i18n:@is-cat%"/>
</section>
@@ -62,6 +66,11 @@ export default Vue.extend({
(this as any).apis.notify('プロフィールを更新しました');
});
},
+ onChangeIsLocked() {
+ (this as any).api('i/update', {
+ isLocked: this.$store.state.i.isLocked
+ });
+ },
onChangeIsBot() {
(this as any).api('i/update', {
isBot: this.$store.state.i.isBot
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index 8d26691f84..4e0fc1cf1a 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -19,6 +19,9 @@
<li @click="list">
<p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p>
</li>
+ <li @click="followRequests" v-if="$store.state.i.isLocked">
+ <p>%fa:envelope R%<span>%i18n:@follow-requests%<i v-if="$store.state.i.pendingReceivedFollowRequestsCount">{{ $store.state.i.pendingReceivedFollowRequestsCount }}</i></span>%fa:angle-right%</p>
+ </li>
</ul>
<ul>
<li>
@@ -46,6 +49,7 @@
<script lang="ts">
import Vue from 'vue';
import MkUserListsWindow from './user-lists-window.vue';
+import MkFollowRequestsWindow from './received-follow-requests-window.vue';
import MkSettingsWindow from './settings-window.vue';
import MkDriveWindow from './drive-window.vue';
import contains from '../../../common/scripts/contains';
@@ -91,6 +95,10 @@ export default Vue.extend({
this.$router.push(`i/lists/${ list.id }`);
});
},
+ followRequests() {
+ this.close();
+ (this as any).os.new(MkFollowRequestsWindow);
+ },
settings() {
this.close();
(this as any).os.new(MkSettingsWindow);
@@ -225,6 +233,16 @@ root(isDark)
> span:first-child
padding-left 22px
+ > span:nth-child(2)
+ > i
+ margin-left 4px
+ padding 2px 8px
+ font-size 90%
+ font-style normal
+ background $theme-color
+ color $theme-color-foreground
+ border-radius 8px
+
> [data-fa]:first-child
margin-right 6px
width 16px
diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue
index 454c725d20..109d1695d8 100644
--- a/src/client/app/desktop/views/components/user-lists-window.vue
+++ b/src/client/app/desktop/views/components/user-lists-window.vue
@@ -1,6 +1,6 @@
<template>
<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy">
- <span slot="header">%fa:list% リスト</span>
+ <span slot="header">%fa:list% %i18n:@title%</span>
<div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="$store.state.device.darkmode">
<button class="ui" @click="add">%i18n:@create-list%</button>
diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts
index 607ff63711..300615ec58 100644
--- a/src/client/app/mobile/script.ts
+++ b/src/client/app/mobile/script.ts
@@ -32,6 +32,7 @@ import MkNotifications from './views/pages/notifications.vue';
import MkWidgets from './views/pages/widgets.vue';
import MkMessaging from './views/pages/messaging.vue';
import MkMessagingRoom from './views/pages/messaging-room.vue';
+import MkReceivedFollowRequests from './views/pages/received-follow-requests.vue';
import MkNote from './views/pages/note.vue';
import MkSearch from './views/pages/search.vue';
import MkFollowers from './views/pages/followers.vue';
@@ -78,6 +79,7 @@ init((launch) => {
{ path: '/i/favorites', name: 'favorites', component: MkFavorites },
{ path: '/i/lists', name: 'user-lists', component: MkUserLists },
{ path: '/i/lists/:list', name: 'user-list', component: MkUserList },
+ { path: '/i/received-follow-requests', name: 'received-follow-requests', component: MkReceivedFollowRequests },
{ path: '/i/widgets', name: 'widgets', component: MkWidgets },
{ path: '/i/messaging', name: 'messaging', component: MkMessaging },
{ path: '/i/messaging/:user', component: MkMessagingRoom },
diff --git a/src/client/app/mobile/views/components/follow-button.vue b/src/client/app/mobile/views/components/follow-button.vue
index a6b5cf0556..d8441a20b9 100644
--- a/src/client/app/mobile/views/components/follow-button.vue
+++ b/src/client/app/mobile/views/components/follow-button.vue
@@ -1,13 +1,16 @@
<template>
<button class="mk-follow-button"
- :class="{ wait: wait, follow: !user.isFollowing, unfollow: user.isFollowing }"
+ :class="{ wait: wait, active: u.isFollowing || u.hasPendingFollowRequestFromYou }"
@click="onClick"
:disabled="wait"
>
- <template v-if="!wait && user.isFollowing">%fa:minus%</template>
- <template v-if="!wait && !user.isFollowing">%fa:plus%</template>
- <template v-if="wait">%fa:spinner .pulse .fw%</template>
- {{ user.isFollowing ? '%i18n:@unfollow%' : '%i18n:@follow%' }}
+ <template v-if="!wait">
+ <template v-if="u.hasPendingFollowRequestFromYou">%fa:hourglass-half% %i18n:@request-pending%</template>
+ <template v-else-if="u.isFollowing">%fa:minus% %i18n:@unfollow%</template>
+ <template v-else-if="!u.isFollowing && u.isLocked">%fa:plus% %i18n:@follow-request%</template>
+ <template v-else-if="!u.isFollowing && !u.isLocked">%fa:plus% %i18n:@follow%</template>
+ </template>
+ <template v-else>%fa:spinner .pulse .fw%</template>
</button>
</template>
@@ -22,6 +25,7 @@ export default Vue.extend({
},
data() {
return {
+ u: this.user,
wait: false,
connection: null,
connectionId: null
@@ -42,39 +46,44 @@ export default Vue.extend({
methods: {
onFollow(user) {
- if (user.id == this.user.id) {
- this.user.isFollowing = user.isFollowing;
+ if (user.id == this.u.id) {
+ this.u.isFollowing = user.isFollowing;
}
},
onUnfollow(user) {
- if (user.id == this.user.id) {
- this.user.isFollowing = user.isFollowing;
+ if (user.id == this.u.id) {
+ this.u.isFollowing = user.isFollowing;
}
},
- onClick() {
+ async onClick() {
this.wait = true;
- if (this.user.isFollowing) {
- (this as any).api('following/delete', {
- userId: this.user.id
- }).then(() => {
- this.user.isFollowing = false;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.wait = false;
- });
- } else {
- (this as any).api('following/create', {
- userId: this.user.id
- }).then(() => {
- this.user.isFollowing = true;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.wait = false;
- });
+
+ try {
+ if (this.u.isFollowing) {
+ this.u = await (this as any).api('following/delete', {
+ userId: this.u.id
+ });
+ } else {
+ if (this.u.isLocked && this.u.hasPendingFollowRequestFromYou) {
+ this.u = await (this as any).api('following/requests/cancel', {
+ userId: this.u.id
+ });
+ } else if (this.u.isLocked) {
+ this.u = await (this as any).api('following/create', {
+ userId: this.u.id
+ });
+ } else {
+ this.u = await (this as any).api('following/create', {
+ userId: this.user.id
+ });
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this.wait = false;
}
}
}
@@ -90,34 +99,38 @@ export default Vue.extend({
cursor pointer
padding 0 16px
margin 0
- height inherit
- font-size 16px
+ min-width 150px
+ line-height 36px
+ font-size 14px
+ color $theme-color
+ background transparent
outline none
border solid 1px $theme-color
- border-radius 4px
+ border-radius 36px
- *
- pointer-events none
+ &:hover
+ background rgba($theme-color, 0.1)
- &.follow
- color $theme-color
- background transparent
+ &:active
+ background rgba($theme-color, 0.2)
+
+ &.active
+ color $theme-color-foreground
+ background $theme-color
&:hover
- background rgba($theme-color, 0.1)
+ background lighten($theme-color, 10%)
+ border-color lighten($theme-color, 10%)
&:active
- background rgba($theme-color, 0.2)
-
- &.unfollow
- color $theme-color-foreground
- background $theme-color
+ background darken($theme-color, 10%)
+ border-color darken($theme-color, 10%)
&.wait
cursor wait !important
opacity 0.7
- > [data-fa]
- margin-right 4px
+ *
+ pointer-events none
</style>
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index d39b2fbf9f..5e2306932b 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -1,7 +1,7 @@
<template>
<div class="mk-notification-preview" :class="notification.type">
<template v-if="notification.type == 'reaction'">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user | userName }}</p>
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
@@ -9,7 +9,7 @@
</template>
<template v-if="notification.type == 'renote'">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
<p>%fa:retweet%{{ notification.note.user | userName }}</p>
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p>
@@ -17,7 +17,7 @@
</template>
<template v-if="notification.type == 'quote'">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
<p>%fa:quote-left%{{ notification.note.user | userName }}</p>
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
@@ -25,14 +25,21 @@
</template>
<template v-if="notification.type == 'follow'">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
<p>%fa:user-plus%{{ notification.user | userName }}</p>
</div>
</template>
+ <template v-if="notification.type == 'receiveFollowRequest'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div class="text">
+ <p>%fa:user-clock%{{ notification.user | userName }}</p>
+ </div>
+ </template>
+
<template v-if="notification.type == 'reply'">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
<p>%fa:reply%{{ notification.note.user | userName }}</p>
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
@@ -40,7 +47,7 @@
</template>
<template v-if="notification.type == 'mention'">
- <img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.note.user"/>
<div class="text">
<p>%fa:at%{{ notification.note.user | userName }}</p>
<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
@@ -48,7 +55,7 @@
</template>
<template v-if="notification.type == 'poll_vote'">
- <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+ <mk-avatar class="avatar" :user="notification.user"/>
<div class="text">
<p>%fa:chart-pie%{{ notification.user | userName }}</p>
<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
@@ -83,16 +90,14 @@ export default Vue.extend({
display block
clear both
- img
+ > .avatar
display block
float left
- min-width 36px
- min-height 36px
- max-width 36px
- max-height 36px
+ width 36px
+ height 36px
border-radius 6px
- .text
+ > .text
float right
width calc(100% - 36px)
padding-left 8px
@@ -120,6 +125,10 @@ export default Vue.extend({
.text p i
color #53c7ce
+ &.receiveFollowRequest
+ .text p i
+ color #888
+
&.reply, &.mention
.text p i
color #fff
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index c1b37563ce..9228950209 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -40,6 +40,17 @@
</div>
</div>
+ <div class="notification followRequest" v-if="notification.type == 'receiveFollowRequest'">
+ <mk-avatar class="avatar" :user="notification.user"/>
+ <div>
+ <header>
+ %fa:user-clock%
+ <router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
+ <mk-time :time="notification.createdAt"/>
+ </header>
+ </div>
+ </div>
+
<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
<mk-avatar class="avatar" :user="notification.user"/>
<div>
@@ -156,6 +167,10 @@ root(isDark)
> div > header i
color #53c7ce
+ &.receiveFollowRequest
+ > div > header i
+ color #888
+
.mk-notification[data-darkmode]
root(true)
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 11a8f7ab97..80f60e4232 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -18,6 +18,7 @@
<li><router-link to="/" :data-active="$route.name == 'index'">%fa:home%%i18n:@timeline%%fa:angle-right%</router-link></li>
<li><router-link to="/i/notifications" :data-active="$route.name == 'notifications'">%fa:R bell%%i18n:@notifications%<template v-if="hasUnreadNotification">%fa:circle%</template>%fa:angle-right%</router-link></li>
<li><router-link to="/i/messaging" :data-active="$route.name == 'messaging'">%fa:R comments%%i18n:@messaging%<template v-if="hasUnreadMessagingMessage">%fa:circle%</template>%fa:angle-right%</router-link></li>
+ <li v-if="$store.getters.isSignedIn && $store.state.i.isLocked"><router-link to="/i/received-follow-requests" :data-active="$route.name == 'received-follow-requests'">%fa:R envelope%%i18n:@follow-requests%<template v-if="$store.getters.isSignedIn && $store.state.i.pendingReceivedFollowRequestsCount">%fa:circle%</template>%fa:angle-right%</router-link></li>
<li><router-link to="/othello" :data-active="$route.name == 'othello'">%fa:gamepad%%i18n:@game%<template v-if="hasGameInvitation">%fa:circle%</template>%fa:angle-right%</router-link></li>
</ul>
<ul>
diff --git a/src/client/app/mobile/views/pages/received-follow-requests.vue b/src/client/app/mobile/views/pages/received-follow-requests.vue
new file mode 100644
index 0000000000..bf26a84ff9
--- /dev/null
+++ b/src/client/app/mobile/views/pages/received-follow-requests.vue
@@ -0,0 +1,78 @@
+<template>
+<mk-ui>
+ <span slot="header">%fa:envelope R%%i18n:@title%</span>
+
+ <main>
+ <div v-for="req in requests">
+ <router-link :key="req.id" :to="req.follower | userPage">{{ req.follower | userName }}</router-link>
+ <span>
+ <a @click="accept(req.follower)">%i18n:@accept%</a>|<a @click="reject(req.follower)">%i18n:@reject%</a>
+ </span>
+ </div>
+ </main>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Progress from '../../../common/scripts/loading';
+
+export default Vue.extend({
+ data() {
+ return {
+ fetching: true,
+ requests: []
+ };
+ },
+ mounted() {
+ document.title = 'Misskey | %i18n:@title%';
+
+ Progress.start();
+
+ (this as any).api('following/requests/list').then(requests => {
+ this.fetching = false;
+ this.requests = requests;
+
+ Progress.done();
+ });
+ },
+ methods: {
+ accept(user) {
+ (this as any).api('following/requests/accept', { userId: user.id }).then(() => {
+ this.requests = this.requests.filter(r => r.follower.id != user.id);
+ });
+ },
+ reject(user) {
+ (this as any).api('following/requests/reject', { userId: user.id }).then(() => {
+ this.requests = this.requests.filter(r => r.follower.id != user.id);
+ });
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+main
+ width 100%
+ max-width 680px
+ margin 0 auto
+ padding 8px
+
+ @media (min-width 500px)
+ padding 16px
+
+ @media (min-width 600px)
+ padding 32px
+
+ > div
+ display flex
+ padding 16px
+ border solid 1px isDark ? #1c2023 : #eee
+ border-radius 4px
+
+ > span
+ margin 0 0 0 auto
+
+</style>
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index b3b820650c..3d37015906 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -184,7 +184,6 @@ root(isDark)
> .mk-follow-button
float right
- height 40px
> .title
margin 8px 0
diff --git a/src/models/follow-request.ts b/src/models/follow-request.ts
new file mode 100644
index 0000000000..eea5d1c535
--- /dev/null
+++ b/src/models/follow-request.ts
@@ -0,0 +1,87 @@
+import * as mongo from 'mongodb';
+import * as deepcopy from 'deepcopy';
+import db from '../db/mongodb';
+import { pack as packUser } from './user';
+
+const FollowRequest = db.get<IFollowRequest>('followRequests');
+FollowRequest.createIndex(['followerId', 'followeeId'], { unique: true });
+export default FollowRequest;
+
+export type IFollowRequest = {
+ _id: mongo.ObjectID;
+ createdAt: Date;
+ followeeId: mongo.ObjectID;
+ followerId: mongo.ObjectID;
+
+ // 非正規化
+ _followee: {
+ host: string;
+ inbox?: string;
+ },
+ _follower: {
+ host: string;
+ inbox?: string;
+ }
+};
+
+/**
+ * FollowRequestを物理削除します
+ */
+export async function deleteFollowRequest(followRequest: string | mongo.ObjectID | IFollowRequest) {
+ let f: IFollowRequest;
+
+ // Populate
+ if (mongo.ObjectID.prototype.isPrototypeOf(followRequest)) {
+ f = await FollowRequest.findOne({
+ _id: followRequest
+ });
+ } else if (typeof followRequest === 'string') {
+ f = await FollowRequest.findOne({
+ _id: new mongo.ObjectID(followRequest)
+ });
+ } else {
+ f = followRequest as IFollowRequest;
+ }
+
+ if (f == null) return;
+
+ // このFollowingを削除
+ await FollowRequest.remove({
+ _id: f._id
+ });
+}
+
+/**
+ * Pack a request for API response
+ */
+export const pack = (
+ request: any,
+ me?: any
+) => new Promise<any>(async (resolve, reject) => {
+ let _request: any;
+
+ // Populate the request if 'request' is ID
+ if (mongo.ObjectID.prototype.isPrototypeOf(request)) {
+ _request = await FollowRequest.findOne({
+ _id: request
+ });
+ } else if (typeof request === 'string') {
+ _request = await FollowRequest.findOne({
+ _id: new mongo.ObjectID(request)
+ });
+ } else {
+ _request = deepcopy(request);
+ }
+
+ // Rename _id to id
+ _request.id = _request._id;
+ delete _request._id;
+
+ // Populate follower
+ _request.follower = await packUser(_request.followerId, me);
+
+ // Populate followee
+ _request.followee = await packUser(_request.followeeId, me);
+
+ resolve(_request);
+});
diff --git a/src/models/notification.ts b/src/models/notification.ts
index c4cf1e4efd..875c6952b5 100644
--- a/src/models/notification.ts
+++ b/src/models/notification.ts
@@ -111,6 +111,7 @@ export const pack = (notification: any) => new Promise<any>(async (resolve, reje
switch (_notification.type) {
case 'follow':
+ case 'receiveFollowRequest':
// nope
break;
case 'mention':
diff --git a/src/models/user.ts b/src/models/user.ts
index 11eafe05ea..0e06512dae 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -22,6 +22,7 @@ import FollowedLog, { deleteFollowedLog } from './followed-log';
import SwSubscription, { deleteSwSubscription } from './sw-subscription';
import Notification, { deleteNotification } from './notification';
import UserList, { deleteUserList } from './user-list';
+import FollowRequest, { deleteFollowRequest } from './follow-request';
const User = db.get<IUser>('users');
@@ -50,7 +51,22 @@ type IUserBase = {
data: any;
description: string;
pinnedNoteId: mongo.ObjectID;
+
+ /**
+ * 凍結されているか否か
+ */
isSuspended: boolean;
+
+ /**
+ * 鍵アカウントか否か
+ */
+ isLocked: boolean;
+
+ /**
+ * このアカウントに届いているフォローリクエストの数
+ */
+ pendingReceivedFollowRequestsCount: number;
+
host: string;
};
@@ -240,6 +256,16 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) {
await Following.find({ followeeId: u._id })
).map(x => deleteFollowing(x)));
+ // このユーザーのFollowRequestをすべて削除
+ await Promise.all((
+ await FollowRequest.find({ followerId: u._id })
+ ).map(x => deleteFollowRequest(x)));
+
+ // このユーザーへのFollowRequestをすべて削除
+ await Promise.all((
+ await FollowRequest.find({ followeeId: u._id })
+ ).map(x => deleteFollowRequest(x)));
+
// このユーザーのFollowingLogをすべて削除
await Promise.all((
await FollowingLog.find({ userId: u._id })
@@ -395,7 +421,7 @@ export const pack = (
}
if (meId && !meId.equals(_user.id)) {
- const [following1, following2, mute] = await Promise.all([
+ const [following1, following2, followReq1, followReq2, mute] = await Promise.all([
Following.findOne({
followerId: meId,
followeeId: _user.id
@@ -404,6 +430,14 @@ export const pack = (
followerId: _user.id,
followeeId: meId
}),
+ _user.isLocked ? FollowRequest.findOne({
+ followerId: meId,
+ followeeId: _user.id
+ }) : Promise.resolve(null),
+ FollowRequest.findOne({
+ followerId: _user.id,
+ followeeId: meId
+ }),
Mute.findOne({
muterId: meId,
muteeId: _user.id
@@ -414,6 +448,9 @@ export const pack = (
_user.isFollowing = following1 !== null;
_user.isStalking = following1 && following1.stalk;
+ _user.hasPendingFollowRequestFromYou = followReq1 !== null;
+ _user.hasPendingFollowRequestToYou = followReq2 !== null;
+
// Whether the user is followed
_user.isFollowed = following2 !== null;
diff --git a/src/remote/activitypub/kernel/accept/follow.ts b/src/remote/activitypub/kernel/accept/follow.ts
new file mode 100644
index 0000000000..0f414ba321
--- /dev/null
+++ b/src/remote/activitypub/kernel/accept/follow.ts
@@ -0,0 +1,27 @@
+import * as mongo from 'mongodb';
+import User, { IRemoteUser } from '../../../../models/user';
+import config from '../../../../config';
+import accept from '../../../../services/following/requests/accept';
+import { IFollow } from '../../type';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
+ const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
+
+ if (!id.startsWith(config.url + '/')) {
+ return null;
+ }
+
+ const follower = await User.findOne({
+ _id: new mongo.ObjectID(id.split('/').pop())
+ });
+
+ if (follower === null) {
+ throw new Error('follower not found');
+ }
+
+ if (follower.host != null) {
+ throw new Error('フォローリクエストしたユーザーはローカルユーザーではありません');
+ }
+
+ await accept(actor, follower);
+};
diff --git a/src/remote/activitypub/kernel/accept/index.ts b/src/remote/activitypub/kernel/accept/index.ts
new file mode 100644
index 0000000000..2f9d646d14
--- /dev/null
+++ b/src/remote/activitypub/kernel/accept/index.ts
@@ -0,0 +1,35 @@
+import * as debug from 'debug';
+
+import Resolver from '../../resolver';
+import { IRemoteUser } from '../../../../models/user';
+import acceptFollow from './follow';
+import { IAccept } from '../../type';
+
+const log = debug('misskey:activitypub');
+
+export default async (actor: IRemoteUser, activity: IAccept): Promise<void> => {
+ const uri = activity.id || activity;
+
+ log(`Accept: ${uri}`);
+
+ const resolver = new Resolver();
+
+ let object;
+
+ try {
+ object = await resolver.resolve(activity.object);
+ } catch (e) {
+ log(`Resolution failed: ${e}`);
+ throw e;
+ }
+
+ switch (object.type) {
+ case 'Follow':
+ acceptFollow(actor, object);
+ break;
+
+ default:
+ console.warn(`Unknown accept type: ${object.type}`);
+ break;
+ }
+};
diff --git a/src/remote/activitypub/kernel/follow.ts b/src/remote/activitypub/kernel/follow.ts
index 7e31eb32ea..464f8582b7 100644
--- a/src/remote/activitypub/kernel/follow.ts
+++ b/src/remote/activitypub/kernel/follow.ts
@@ -23,5 +23,5 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
throw new Error('フォローしようとしているユーザーはローカルユーザーではありません');
}
- await follow(actor, followee, activity);
+ await follow(actor, followee);
};
diff --git a/src/remote/activitypub/kernel/index.ts b/src/remote/activitypub/kernel/index.ts
index 15ea9494ae..752a9bd2e2 100644
--- a/src/remote/activitypub/kernel/index.ts
+++ b/src/remote/activitypub/kernel/index.ts
@@ -6,6 +6,8 @@ import follow from './follow';
import undo from './undo';
import like from './like';
import announce from './announce';
+import accept from './accept';
+import reject from './reject';
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
switch (activity.type) {
@@ -22,7 +24,11 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
break;
case 'Accept':
- // noop
+ await accept(actor, activity);
+ break;
+
+ case 'Reject':
+ await reject(actor, activity);
break;
case 'Announce':
diff --git a/src/remote/activitypub/kernel/reject/follow.ts b/src/remote/activitypub/kernel/reject/follow.ts
new file mode 100644
index 0000000000..c139865d0e
--- /dev/null
+++ b/src/remote/activitypub/kernel/reject/follow.ts
@@ -0,0 +1,27 @@
+import * as mongo from 'mongodb';
+import User, { IRemoteUser } from '../../../../models/user';
+import config from '../../../../config';
+import reject from '../../../../services/following/requests/reject';
+import { IFollow } from '../../type';
+
+export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
+ const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
+
+ if (!id.startsWith(config.url + '/')) {
+ return null;
+ }
+
+ const follower = await User.findOne({
+ _id: new mongo.ObjectID(id.split('/').pop())
+ });
+
+ if (follower === null) {
+ throw new Error('follower not found');
+ }
+
+ if (follower.host != null) {
+ throw new Error('フォローリクエストしたユーザーはローカルユーザーではありません');
+ }
+
+ await reject(actor, follower);
+};
diff --git a/src/remote/activitypub/kernel/reject/index.ts b/src/remote/activitypub/kernel/reject/index.ts
new file mode 100644
index 0000000000..a82c3fd61e
--- /dev/null
+++ b/src/remote/activitypub/kernel/reject/index.ts
@@ -0,0 +1,35 @@
+import * as debug from 'debug';
+
+import Resolver from '../../resolver';
+import { IRemoteUser } from '../../../../models/user';
+import rejectFollow from './follow';
+import { IReject } from '../../type';
+
+const log = debug('misskey:activitypub');
+
+export default async (actor: IRemoteUser, activity: IReject): Promise<void> => {
+ const uri = activity.id || activity;
+
+ log(`Reject: ${uri}`);
+
+ const resolver = new Resolver();
+
+ let object;
+
+ try {
+ object = await resolver.resolve(activity.object);
+ } catch (e) {
+ log(`Resolution failed: ${e}`);
+ throw e;
+ }
+
+ switch (object.type) {
+ case 'Follow':
+ rejectFollow(actor, object);
+ break;
+
+ default:
+ console.warn(`Unknown reject type: ${object.type}`);
+ break;
+ }
+};
diff --git a/src/remote/activitypub/kernel/undo/follow.ts b/src/remote/activitypub/kernel/undo/follow.ts
index c0b10c1898..7377984616 100644
--- a/src/remote/activitypub/kernel/undo/follow.ts
+++ b/src/remote/activitypub/kernel/undo/follow.ts
@@ -2,7 +2,9 @@ import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import unfollow from '../../../../services/following/delete';
+import cancelRequest from '../../../../services/following/requests/cancel';
import { IFollow } from '../../type';
+import FollowRequest from '../../../../models/follow-request';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@@ -23,5 +25,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません');
}
- await unfollow(actor, followee, activity);
+ const req = await FollowRequest.findOne({
+ followerId: actor._id,
+ followeeId: followee._id
+ });
+
+ if (req) {
+ await cancelRequest(actor, followee);
+ } else {
+ await unfollow(actor, followee);
+ }
};
diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts
index 33280f3d89..b720c445c6 100644
--- a/src/remote/activitypub/models/person.ts
+++ b/src/remote/activitypub/models/person.ts
@@ -93,6 +93,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
notesCount,
name: person.name,
driveCapacity: 1024 * 1024 * 8, // 8MiB
+ isLocked: person.manuallyApprovesFollowers,
username: person.preferredUsername,
usernameLower: person.preferredUsername.toLowerCase(),
host,
diff --git a/src/remote/activitypub/renderer/follow.ts b/src/remote/activitypub/renderer/follow.ts
index bf8eeff06b..522422bcff 100644
--- a/src/remote/activitypub/renderer/follow.ts
+++ b/src/remote/activitypub/renderer/follow.ts
@@ -1,8 +1,8 @@
import config from '../../../config';
-import { IRemoteUser, ILocalUser } from '../../../models/user';
+import { IUser, isLocalUser } from '../../../models/user';
-export default (follower: ILocalUser, followee: IRemoteUser) => ({
+export default (follower: IUser, followee: IUser) => ({
type: 'Follow',
- actor: `${config.url}/users/${follower._id}`,
- object: followee.uri
+ actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri,
+ object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri
});
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index 424305f8d3..b2ac885f46 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -1,8 +1,9 @@
import renderImage from './image';
import renderKey from './key';
import config from '../../../config';
+import { ILocalUser } from '../../../models/user';
-export default user => {
+export default (user: ILocalUser) => {
const id = `${config.url}/users/${user._id}`;
return {
@@ -17,6 +18,7 @@ export default user => {
summary: user.description,
icon: user.avatarId && renderImage({ _id: user.avatarId }),
image: user.bannerId && renderImage({ _id: user.bannerId }),
+ manuallyApprovesFollowers: user.isLocked,
publicKey: renderKey(user)
};
};
diff --git a/src/remote/activitypub/renderer/reject.ts b/src/remote/activitypub/renderer/reject.ts
new file mode 100644
index 0000000000..29c998a6b4
--- /dev/null
+++ b/src/remote/activitypub/renderer/reject.ts
@@ -0,0 +1,4 @@
+export default object => ({
+ type: 'Reject',
+ object
+});
diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts
index ca38ec2227..13e1969b4e 100644
--- a/src/remote/activitypub/type.ts
+++ b/src/remote/activitypub/type.ts
@@ -45,6 +45,7 @@ export interface IPerson extends IObject {
type: 'Person';
name: string;
preferredUsername: string;
+ manuallyApprovesFollowers: boolean;
inbox: string;
publicKey: any;
followers: any;
@@ -82,6 +83,10 @@ export interface IAccept extends IActivity {
type: 'Accept';
}
+export interface IReject extends IActivity {
+ type: 'Reject';
+}
+
export interface ILike extends IActivity {
type: 'Like';
_misskey_reaction: string;
@@ -99,5 +104,6 @@ export type Object =
IUndo |
IFollow |
IAccept |
+ IReject |
ILike |
IAnnounce;
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index 3c07a3e2f2..c846e28c07 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -7,7 +7,7 @@ const httpSignature = require('http-signature');
import { createHttp } from '../queue';
import pack from '../remote/activitypub/renderer';
import Note from '../models/note';
-import User, { isLocalUser } from '../models/user';
+import User, { isLocalUser, ILocalUser } from '../models/user';
import renderNote from '../remote/activitypub/renderer/note';
import renderKey from '../remote/activitypub/renderer/key';
import renderPerson from '../remote/activitypub/renderer/person';
@@ -69,7 +69,10 @@ router.get('/notes/:note', async (ctx, next) => {
router.get('/users/:user/outbox', async ctx => {
const userId = new mongo.ObjectID(ctx.params.user);
- const user = await User.findOne({ _id: userId });
+ const user = await User.findOne({
+ _id: userId,
+ host: null
+ });
if (user === null) {
ctx.status = 404;
@@ -91,7 +94,10 @@ router.get('/users/:user/outbox', async ctx => {
router.get('/users/:user/publickey', async ctx => {
const userId = new mongo.ObjectID(ctx.params.user);
- const user = await User.findOne({ _id: userId });
+ const user = await User.findOne({
+ _id: userId,
+ host: null
+ });
if (user === null) {
ctx.status = 404;
@@ -109,14 +115,17 @@ router.get('/users/:user/publickey', async ctx => {
router.get('/users/:user', async ctx => {
const userId = new mongo.ObjectID(ctx.params.user);
- const user = await User.findOne({ _id: userId });
+ const user = await User.findOne({
+ _id: userId,
+ host: null
+ });
if (user === null) {
ctx.status = 404;
return;
}
- ctx.body = pack(renderPerson(user));
+ ctx.body = pack(renderPerson(user as ILocalUser));
});
// follow form
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 196f3adebc..e9392d236b 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -449,6 +449,26 @@ const endpoints: Endpoint[] = [
kind: 'following-write'
},
{
+ name: 'following/requests/accept',
+ withCredential: true,
+ kind: 'following-write'
+ },
+ {
+ name: 'following/requests/reject',
+ withCredential: true,
+ kind: 'following-write'
+ },
+ {
+ name: 'following/requests/cancel',
+ withCredential: true,
+ kind: 'following-write'
+ },
+ {
+ name: 'following/requests/list',
+ withCredential: true,
+ kind: 'following-read'
+ },
+ {
name: 'following/stalk',
withCredential: true,
limit: {
diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts
index 766a8c03d0..48205232e6 100644
--- a/src/server/api/endpoints/following/create.ts
+++ b/src/server/api/endpoints/following/create.ts
@@ -2,7 +2,7 @@
* Module dependencies
*/
import $ from 'cafy'; import ID from '../../../../cafy-id';
-import User from '../../../../models/user';
+import User, { pack } from '../../../../models/user';
import Following from '../../../../models/following';
import create from '../../../../services/following/create';
@@ -49,5 +49,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
create(follower, followee);
// Send response
- res();
+ res(await pack(followee, user));
});
diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts
index 396b19a6f6..f4030c247a 100644
--- a/src/server/api/endpoints/following/delete.ts
+++ b/src/server/api/endpoints/following/delete.ts
@@ -2,7 +2,7 @@
* Module dependencies
*/
import $ from 'cafy'; import ID from '../../../../cafy-id';
-import User from '../../../../models/user';
+import User, { pack } from '../../../../models/user';
import Following from '../../../../models/following';
import deleteFollowing from '../../../../services/following/delete';
@@ -49,5 +49,5 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
deleteFollowing(follower, followee);
// Send response
- res();
+ res(await pack(followee, user));
});
diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts
new file mode 100644
index 0000000000..705d3b161a
--- /dev/null
+++ b/src/server/api/endpoints/following/requests/accept.ts
@@ -0,0 +1,26 @@
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
+import acceptFollowRequest from '../../../../../services/following/requests/accept';
+import User from '../../../../../models/user';
+
+/**
+ * Accept a follow request
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+ // Get 'userId' parameter
+ const [followerId, followerIdErr] = $.type(ID).get(params.userId);
+ if (followerIdErr) return rej('invalid userId param');
+
+ // Fetch follower
+ const follower = await User.findOne({
+ _id: followerId
+ });
+
+ if (follower === null) {
+ return rej('follower not found');
+ }
+
+ await acceptFollowRequest(user, follower);
+
+ // Send response
+ res();
+});
diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts
new file mode 100644
index 0000000000..388a54890b
--- /dev/null
+++ b/src/server/api/endpoints/following/requests/cancel.ts
@@ -0,0 +1,26 @@
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
+import cancelFollowRequest from '../../../../../services/following/requests/cancel';
+import User, { pack } from '../../../../../models/user';
+
+/**
+ * Cancel a follow request
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+ // Get 'userId' parameter
+ const [followeeId, followeeIdErr] = $.type(ID).get(params.userId);
+ if (followeeIdErr) return rej('invalid userId param');
+
+ // Fetch followee
+ const followee = await User.findOne({
+ _id: followeeId
+ });
+
+ if (followee === null) {
+ return rej('followee not found');
+ }
+
+ await cancelFollowRequest(followee, user);
+
+ // Send response
+ res(await pack(followee._id, user));
+});
diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts
new file mode 100644
index 0000000000..e8364335d1
--- /dev/null
+++ b/src/server/api/endpoints/following/requests/list.ts
@@ -0,0 +1,14 @@
+//import $ from 'cafy'; import ID from '../../../../../cafy-id';
+import FollowRequest, { pack } from '../../../../../models/follow-request';
+
+/**
+ * Get all pending received follow requests
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+ const reqs = await FollowRequest.find({
+ followeeId: user._id
+ });
+
+ // Send response
+ res(await Promise.all(reqs.map(req => pack(req))));
+});
diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts
new file mode 100644
index 0000000000..1cfb562b55
--- /dev/null
+++ b/src/server/api/endpoints/following/requests/reject.ts
@@ -0,0 +1,26 @@
+import $ from 'cafy'; import ID from '../../../../../cafy-id';
+import rejectFollowRequest from '../../../../../services/following/requests/reject';
+import User from '../../../../../models/user';
+
+/**
+ * Reject a follow request
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+ // Get 'userId' parameter
+ const [followerId, followerIdErr] = $.type(ID).get(params.userId);
+ if (followerIdErr) return rej('invalid userId param');
+
+ // Fetch follower
+ const follower = await User.findOne({
+ _id: followerId
+ });
+
+ if (follower === null) {
+ return rej('follower not found');
+ }
+
+ await rejectFollowRequest(user, follower);
+
+ // Send response
+ res();
+});
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 6e0c5b8515..b94f073d2c 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -5,6 +5,7 @@ import $ from 'cafy'; import ID from '../../../../cafy-id';
import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user';
import event from '../../../../publishers/stream';
import DriveFile from '../../../../models/drive-file';
+import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
/**
* Update myself
@@ -12,50 +13,57 @@ import DriveFile from '../../../../models/drive-file';
module.exports = async (params, user, app) => new Promise(async (res, rej) => {
const isSecure = user != null && app == null;
+ const updates = {} as any;
+
// Get 'name' parameter
const [name, nameErr] = $.str.optional().nullable().pipe(isValidName).get(params.name);
if (nameErr) return rej('invalid name param');
- if (name) user.name = name;
+ if (name) updates.name = name;
// Get 'description' parameter
const [description, descriptionErr] = $.str.optional().nullable().pipe(isValidDescription).get(params.description);
if (descriptionErr) return rej('invalid description param');
- if (description !== undefined) user.description = description;
+ if (description !== undefined) updates.description = description;
// Get 'location' parameter
const [location, locationErr] = $.str.optional().nullable().pipe(isValidLocation).get(params.location);
if (locationErr) return rej('invalid location param');
- if (location !== undefined) user.profile.location = location;
+ if (location !== undefined) updates['profile.location'] = location;
// Get 'birthday' parameter
const [birthday, birthdayErr] = $.str.optional().nullable().pipe(isValidBirthday).get(params.birthday);
if (birthdayErr) return rej('invalid birthday param');
- if (birthday !== undefined) user.profile.birthday = birthday;
+ if (birthday !== undefined) updates['profile.birthday'] = birthday;
// Get 'avatarId' parameter
- const [avatarId, avatarIdErr] = $.type(ID).optional().get(params.avatarId);
+ const [avatarId, avatarIdErr] = $.type(ID).optional().nullable().get(params.avatarId);
if (avatarIdErr) return rej('invalid avatarId param');
- if (avatarId) user.avatarId = avatarId;
+ if (avatarId !== undefined) updates.avatarId = avatarId;
// Get 'bannerId' parameter
- const [bannerId, bannerIdErr] = $.type(ID).optional().get(params.bannerId);
+ const [bannerId, bannerIdErr] = $.type(ID).optional().nullable().get(params.bannerId);
if (bannerIdErr) return rej('invalid bannerId param');
- if (bannerId) user.bannerId = bannerId;
+ if (bannerId !== undefined) updates.bannerId = bannerId;
+
+ // Get 'isLocked' parameter
+ const [isLocked, isLockedErr] = $.bool.optional().get(params.isLocked);
+ if (isLockedErr) return rej('invalid isLocked param');
+ if (isLocked != null) updates.isLocked = isLocked;
// Get 'isBot' parameter
const [isBot, isBotErr] = $.bool.optional().get(params.isBot);
if (isBotErr) return rej('invalid isBot param');
- if (isBot != null) user.isBot = isBot;
+ if (isBot != null) updates.isBot = isBot;
// Get 'isCat' parameter
const [isCat, isCatErr] = $.bool.optional().get(params.isCat);
if (isCatErr) return rej('invalid isCat param');
- if (isCat != null) user.isCat = isCat;
+ if (isCat != null) updates.isCat = isCat;
// Get 'autoWatch' parameter
const [autoWatch, autoWatchErr] = $.bool.optional().get(params.autoWatch);
if (autoWatchErr) return rej('invalid autoWatch param');
- if (autoWatch != null) user.settings.autoWatch = autoWatch;
+ if (autoWatch != null) updates['settings.autoWatch'] = autoWatch;
if (avatarId) {
const avatar = await DriveFile.findOne({
@@ -63,7 +71,7 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
});
if (avatar != null && avatar.metadata.properties.avgColor) {
- user.avatarColor = avatar.metadata.properties.avgColor;
+ updates.avatarColor = avatar.metadata.properties.avgColor;
}
}
@@ -73,27 +81,16 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
});
if (banner != null && banner.metadata.properties.avgColor) {
- user.bannerColor = banner.metadata.properties.avgColor;
+ updates.bannerColor = banner.metadata.properties.avgColor;
}
}
await User.update(user._id, {
- $set: {
- name: user.name,
- description: user.description,
- avatarId: user.avatarId,
- avatarColor: user.avatarColor,
- bannerId: user.bannerId,
- bannerColor: user.bannerColor,
- profile: user.profile,
- isBot: user.isBot,
- isCat: user.isCat,
- settings: user.settings
- }
+ $set: updates
});
// Serialize
- const iObj = await pack(user, user, {
+ const iObj = await pack(user._id, user, {
detail: true,
includeSecrets: isSecure
});
@@ -101,6 +98,11 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => {
// Send response
res(iObj);
- // Publish i updated event
- event(user._id, 'i_updated', iObj);
+ // Publish meUpdated event
+ event(user._id, 'meUpdated', iObj);
+
+ // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
+ if (user.isLocked && isLocked === false) {
+ acceptAllFollowRequests(user);
+ }
});
diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts
index 620ae17ca2..23821a552f 100644
--- a/src/server/api/endpoints/users/recommendation.ts
+++ b/src/server/api/endpoints/users/recommendation.ts
@@ -36,6 +36,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => {
_id: {
$nin: followingIds.concat(mutedUserIds)
},
+ isLocked: false,
$or: [{
lastUsedAt: {
$gte: new Date(Date.now() - ms('7days'))
diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts
index 284ae7ee22..8c35509cce 100644
--- a/src/server/api/service/twitter.ts
+++ b/src/server/api/service/twitter.ts
@@ -49,7 +49,7 @@ router.get('/disconnect/twitter', async ctx => {
ctx.body = `Twitterの連携を解除しました :v:`;
// Publish i updated event
- event(user._id, 'i_updated', await pack(user, user, {
+ event(user._id, 'meUpdated', await pack(user, user, {
detail: true,
includeSecrets: true
}));
@@ -174,7 +174,7 @@ if (config.twitter == null) {
ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`;
// Publish i updated event
- event(user._id, 'i_updated', await pack(user, user, {
+ event(user._id, 'meUpdated', await pack(user, user, {
detail: true,
includeSecrets: true
}));
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index 3424c55dae..03db72c332 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -8,72 +8,76 @@ import pack from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderAccept from '../../remote/activitypub/renderer/accept';
import { deliver } from '../../queue';
+import createFollowRequest from './requests/create';
-export default async function(follower: IUser, followee: IUser, activity?) {
- const following = await Following.insert({
- createdAt: new Date(),
- followerId: follower._id,
- followeeId: followee._id,
- stalk: true,
+export default async function(follower: IUser, followee: IUser) {
+ if (followee.isLocked) {
+ await createFollowRequest(follower, followee);
+ } else {
+ const following = await Following.insert({
+ createdAt: new Date(),
+ followerId: follower._id,
+ followeeId: followee._id,
- // 非正規化
- _follower: {
- host: follower.host,
- inbox: isRemoteUser(follower) ? follower.inbox : undefined
- },
- _followee: {
- host: followee.host,
- inbox: isRemoteUser(followee) ? followee.inbox : undefined
- }
- });
+ // 非正規化
+ _follower: {
+ host: follower.host,
+ inbox: isRemoteUser(follower) ? follower.inbox : undefined
+ },
+ _followee: {
+ host: followee.host,
+ inbox: isRemoteUser(followee) ? followee.inbox : undefined
+ }
+ });
- //#region Increment following count
- User.update({ _id: follower._id }, {
- $inc: {
- followingCount: 1
- }
- });
+ //#region Increment following count
+ User.update({ _id: follower._id }, {
+ $inc: {
+ followingCount: 1
+ }
+ });
- FollowingLog.insert({
- createdAt: following.createdAt,
- userId: follower._id,
- count: follower.followingCount + 1
- });
- //#endregion
+ FollowingLog.insert({
+ createdAt: following.createdAt,
+ userId: follower._id,
+ count: follower.followingCount + 1
+ });
+ //#endregion
- //#region Increment followers count
- User.update({ _id: followee._id }, {
- $inc: {
- followersCount: 1
- }
- });
- FollowedLog.insert({
- createdAt: following.createdAt,
- userId: followee._id,
- count: followee.followersCount + 1
- });
- //#endregion
+ //#region Increment followers count
+ User.update({ _id: followee._id }, {
+ $inc: {
+ followersCount: 1
+ }
+ });
+ FollowedLog.insert({
+ createdAt: following.createdAt,
+ userId: followee._id,
+ count: followee.followersCount + 1
+ });
+ //#endregion
- // Publish follow event
- if (isLocalUser(follower)) {
- packUser(followee, follower).then(packed => event(follower._id, 'follow', packed));
- }
+ // Publish follow event
+ if (isLocalUser(follower)) {
+ packUser(followee, follower).then(packed => event(follower._id, 'follow', packed));
+ }
- // Publish followed event
- if (isLocalUser(followee)) {
- packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)),
+ // Publish followed event
+ if (isLocalUser(followee)) {
+ packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)),
- // 通知を作成
- notify(followee._id, follower._id, 'follow');
- }
+ // 通知を作成
+ notify(followee._id, follower._id, 'follow');
+ }
- if (isLocalUser(follower) && isRemoteUser(followee)) {
- const content = pack(renderFollow(follower, followee));
- deliver(follower, content, followee.inbox);
- }
+ if (isLocalUser(follower) && isRemoteUser(followee)) {
+ const content = pack(renderFollow(follower, followee));
+ deliver(follower, content, followee.inbox);
+ }
- if (isRemoteUser(follower) && isLocalUser(followee)) {
- const content = pack(renderAccept(activity));
- deliver(followee, content, follower.inbox);
+ if (isRemoteUser(follower) && isLocalUser(followee)) {
+ const content = pack(renderAccept(renderFollow(follower, followee)));
+ deliver(followee, content, follower.inbox);
+ }
}
}
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index c0c99fbed5..4fc5d42476 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -8,7 +8,7 @@ import renderFollow from '../../remote/activitypub/renderer/follow';
import renderUndo from '../../remote/activitypub/renderer/undo';
import { deliver } from '../../queue';
-export default async function(follower: IUser, followee: IUser, activity?) {
+export default async function(follower: IUser, followee: IUser) {
const following = await Following.findOne({
followerId: follower._id,
followeeId: followee._id
diff --git a/src/services/following/requests/accept-all.ts b/src/services/following/requests/accept-all.ts
new file mode 100644
index 0000000000..9708d9e658
--- /dev/null
+++ b/src/services/following/requests/accept-all.ts
@@ -0,0 +1,24 @@
+import User, { IUser } from "../../../models/user";
+import FollowRequest from "../../../models/follow-request";
+import accept from './accept';
+
+/**
+ * 指定したユーザー宛てのフォローリクエストをすべて承認
+ * @param user ユーザー
+ */
+export default async function(user: IUser) {
+ const requests = await FollowRequest.find({
+ followeeId: user._id
+ });
+
+ requests.forEach(async request => {
+ const follower = await User.findOne({ _id: request.followerId });
+ accept(user, follower);
+ });
+
+ User.update({ _id: user._id }, {
+ $set: {
+ pendingReceivedFollowRequestsCount: 0
+ }
+ });
+}
diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts
new file mode 100644
index 0000000000..1b89cdb981
--- /dev/null
+++ b/src/services/following/requests/accept.ts
@@ -0,0 +1,70 @@
+import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from "../../../models/user";
+import FollowRequest from "../../../models/follow-request";
+import pack from '../../../remote/activitypub/renderer';
+import renderFollow from '../../../remote/activitypub/renderer/follow';
+import renderAccept from '../../../remote/activitypub/renderer/accept';
+import { deliver } from '../../../queue';
+import Following from "../../../models/following";
+import FollowingLog from "../../../models/following-log";
+import FollowedLog from "../../../models/followed-log";
+import event from '../../../publishers/stream';
+
+export default async function(followee: IUser, follower: IUser) {
+ const following = await Following.insert({
+ createdAt: new Date(),
+ followerId: follower._id,
+ followeeId: followee._id,
+
+ // 非正規化
+ _follower: {
+ host: follower.host,
+ inbox: isRemoteUser(follower) ? follower.inbox : undefined
+ },
+ _followee: {
+ host: followee.host,
+ inbox: isRemoteUser(followee) ? followee.inbox : undefined
+ }
+ });
+
+ if (isRemoteUser(follower)) {
+ const content = pack(renderAccept(renderFollow(follower, followee)));
+ deliver(followee as ILocalUser, content, follower.inbox);
+ }
+
+ await FollowRequest.remove({
+ followeeId: followee._id,
+ followerId: follower._id
+ });
+
+ //#region Increment following count
+ await User.update({ _id: follower._id }, {
+ $inc: {
+ followingCount: 1
+ }
+ });
+
+ FollowingLog.insert({
+ createdAt: following.createdAt,
+ userId: follower._id,
+ count: follower.followingCount + 1
+ });
+ //#endregion
+
+ //#region Increment followers count
+ await User.update({ _id: followee._id }, {
+ $inc: {
+ followersCount: 1
+ }
+ });
+
+ FollowedLog.insert({
+ createdAt: following.createdAt,
+ userId: followee._id,
+ count: followee.followersCount + 1
+ });
+ //#endregion
+
+ packUser(followee, followee, {
+ detail: true
+ }).then(packed => event(followee._id, 'meUpdated', packed));
+}
diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts
new file mode 100644
index 0000000000..50c95de0eb
--- /dev/null
+++ b/src/services/following/requests/cancel.ts
@@ -0,0 +1,29 @@
+import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from "../../../models/user";
+import FollowRequest from "../../../models/follow-request";
+import pack from '../../../remote/activitypub/renderer';
+import renderFollow from '../../../remote/activitypub/renderer/follow';
+import renderUndo from '../../../remote/activitypub/renderer/undo';
+import { deliver } from '../../../queue';
+import event from '../../../publishers/stream';
+
+export default async function(followee: IUser, follower: IUser) {
+ if (isRemoteUser(followee)) {
+ const content = pack(renderUndo(renderFollow(follower, followee)));
+ deliver(follower as ILocalUser, content, followee.inbox);
+ }
+
+ await FollowRequest.remove({
+ followeeId: followee._id,
+ followerId: follower._id
+ });
+
+ await User.update({ _id: followee._id }, {
+ $inc: {
+ pendingReceivedFollowRequestsCount: -1
+ }
+ });
+
+ packUser(followee, followee, {
+ detail: true
+ }).then(packed => event(followee._id, 'meUpdated', packed));
+}
diff --git a/src/services/following/requests/create.ts b/src/services/following/requests/create.ts
new file mode 100644
index 0000000000..fea82b57d8
--- /dev/null
+++ b/src/services/following/requests/create.ts
@@ -0,0 +1,50 @@
+import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user';
+import event from '../../../publishers/stream';
+import notify from '../../../publishers/notify';
+import pack from '../../../remote/activitypub/renderer';
+import renderFollow from '../../../remote/activitypub/renderer/follow';
+import { deliver } from '../../../queue';
+import FollowRequest from '../../../models/follow-request';
+
+export default async function(follower: IUser, followee: IUser) {
+ if (!followee.isLocked) throw '対象のアカウントは鍵アカウントではありません';
+
+ await FollowRequest.insert({
+ createdAt: new Date(),
+ followerId: follower._id,
+ followeeId: followee._id,
+
+ // 非正規化
+ _follower: {
+ host: follower.host,
+ inbox: isRemoteUser(follower) ? follower.inbox : undefined
+ },
+ _followee: {
+ host: followee.host,
+ inbox: isRemoteUser(followee) ? followee.inbox : undefined
+ }
+ });
+
+ await User.update({ _id: followee._id }, {
+ $inc: {
+ pendingReceivedFollowRequestsCount: 1
+ }
+ });
+
+ // Publish receiveRequest event
+ if (isLocalUser(followee)) {
+ packUser(follower, followee).then(packed => event(followee._id, 'receiveFollowRequest', packed));
+
+ packUser(followee, followee, {
+ detail: true
+ }).then(packed => event(followee._id, 'meUpdated', packed));
+
+ // 通知を作成
+ notify(followee._id, follower._id, 'receiveFollowRequest');
+ }
+
+ if (isLocalUser(follower) && isRemoteUser(followee)) {
+ const content = pack(renderFollow(follower, followee));
+ deliver(follower, content, followee.inbox);
+ }
+}
diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts
new file mode 100644
index 0000000000..acd419d0ee
--- /dev/null
+++ b/src/services/following/requests/reject.ts
@@ -0,0 +1,24 @@
+import User, { IUser, isRemoteUser, ILocalUser } from "../../../models/user";
+import FollowRequest from "../../../models/follow-request";
+import pack from '../../../remote/activitypub/renderer';
+import renderFollow from '../../../remote/activitypub/renderer/follow';
+import renderReject from '../../../remote/activitypub/renderer/reject';
+import { deliver } from '../../../queue';
+
+export default async function(followee: IUser, follower: IUser) {
+ if (isRemoteUser(follower)) {
+ const content = pack(renderReject(renderFollow(follower, followee)));
+ deliver(followee as ILocalUser, content, follower.inbox);
+ }
+
+ await FollowRequest.remove({
+ followeeId: followee._id,
+ followerId: follower._id
+ });
+
+ User.update({ _id: followee._id }, {
+ $inc: {
+ pendingReceivedFollowRequestsCount: -1
+ }
+ });
+}