diff --git a/package.json b/package.json index b721682d41..5018fadf60 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@types/license-checker": "15.0.0", "@types/mkdirp": "0.5.2", "@types/mocha": "5.0.0", - "@types/mongodb": "3.0.9", + "@types/mongodb": "3.0.10", "@types/monk": "6.0.0", "@types/morgan": "1.7.35", "@types/ms": "0.7.30", @@ -203,13 +203,13 @@ "vue-cropperjs": "2.2.0", "vue-js-modal": "1.3.12", "vue-json-tree-view": "2.1.3", - "vue-loader": "14.2.2", + "vue-loader": "15.0.0-rc.1", "vue-router": "3.0.1", "vue-template-compiler": "2.5.16", "vuedraggable": "2.16.0", "web-push": "3.3.0", "webfinger.js": "2.6.6", - "webpack": "4.4.1", + "webpack": "4.5.0", "webpack-cli": "2.0.14", "webpack-replace-loader": "1.3.0", "websocket": "1.0.25", diff --git a/src/client/app/ch/tags/channel.tag b/src/client/app/ch/tags/channel.tag index 1ebc3ccebc..4856728dec 100644 --- a/src/client/app/ch/tags/channel.tag +++ b/src/client/app/ch/tags/channel.tag @@ -165,7 +165,7 @@ <mk-channel-post> <header> <a class="index" @click="reply">{ post.index }:</a> - <a class="name" href={ _URL_ + '/@' + acct }><b>{ post.user.name }</b></a> + <a class="name" href={ _URL_ + '/@' + acct }><b>{ getUserName(post.user) }</b></a> <mk-time time={ post.createdAt }/> <mk-time time={ post.createdAt } mode="detail"/> <span>ID:<i>{ acct }</i></span> @@ -230,10 +230,12 @@ </style> <script lang="typescript"> import getAcct from '../../../../acct/render'; + import getUserName from '../../../../renderers/get-user-name'; this.post = this.opts.post; this.form = this.opts.form; this.acct = getAcct(this.post.user); + this.name = getUserName(this.post.user); this.reply = () => { this.form.update({ @@ -244,7 +246,7 @@ </mk-channel-post> <mk-channel-form> - <p v-if="reply"><b>>>{ reply.index }</b> ({ reply.user.name }): <a @click="clearReply">[x]</a></p> + <p v-if="reply"><b>>>{ reply.index }</b> ({ getUserName(reply.user) }): <a @click="clearReply">[x]</a></p> <textarea ref="text" disabled={ wait } oninput={ update } onkeydown={ onkeydown } onpaste={ onpaste } placeholder="%i18n:ch.tags.mk-channel-form.textarea%"></textarea> <div class="actions"> <button @click="selectFile">%fa:upload%%i18n:ch.tags.mk-channel-form.upload%</button> @@ -286,6 +288,8 @@ </style> <script lang="typescript"> + import getUserName from '../../../../renderers/get-user-name'; + this.mixin('api'); this.channel = this.opts.channel; @@ -373,6 +377,8 @@ } }); }; + + this.getUserName = getUserName; </script> </mk-channel-form> diff --git a/src/client/app/common/scripts/compose-notification.ts b/src/client/app/common/scripts/compose-notification.ts index ebc15952f6..e99d502960 100644 --- a/src/client/app/common/scripts/compose-notification.ts +++ b/src/client/app/common/scripts/compose-notification.ts @@ -1,5 +1,6 @@ import getPostSummary from '../../../../renderers/get-post-summary'; import getReactionEmoji from '../../../../renderers/get-reaction-emoji'; +import getUserName from '../../../../renderers/get-user-name'; type Notification = { title: string; @@ -21,35 +22,35 @@ export default function(type, data): Notification { case 'mention': return { - title: `${data.user.name}さんから:`, + title: `${getUserName(data.user)}さんから:`, body: getPostSummary(data), icon: data.user.avatarUrl + '?thumbnail&size=64' }; case 'reply': return { - title: `${data.user.name}さんから返信:`, + title: `${getUserName(data.user)}さんから返信:`, body: getPostSummary(data), icon: data.user.avatarUrl + '?thumbnail&size=64' }; case 'quote': return { - title: `${data.user.name}さんが引用:`, + title: `${getUserName(data.user)}さんが引用:`, body: getPostSummary(data), icon: data.user.avatarUrl + '?thumbnail&size=64' }; case 'reaction': return { - title: `${data.user.name}: ${getReactionEmoji(data.reaction)}:`, + title: `${getUserName(data.user)}: ${getReactionEmoji(data.reaction)}:`, body: getPostSummary(data.post), icon: data.user.avatarUrl + '?thumbnail&size=64' }; case 'unread_messaging_message': return { - title: `${data.user.name}さんからメッセージ:`, + title: `${getUserName(data.user)}さんからメッセージ:`, body: data.text, // TODO: getMessagingMessageSummary(data), icon: data.user.avatarUrl + '?thumbnail&size=64' }; @@ -57,7 +58,7 @@ export default function(type, data): Notification { case 'othello_invited': return { title: '対局への招待があります', - body: `${data.parent.name}さんから`, + body: `${getUserName(data.parent)}さんから`, icon: data.parent.avatarUrl + '?thumbnail&size=64' }; diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue index 38eaf86508..8837fde6be 100644 --- a/src/client/app/common/views/components/autocomplete.vue +++ b/src/client/app/common/views/components/autocomplete.vue @@ -3,7 +3,7 @@ <ol class="users" ref="suggests" v-if="users.length > 0"> <li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1"> <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> - <span class="name">{{ user.name }}</span> + <span class="name">{{ getUserName(user) }}</span> <span class="username">@{{ getAcct(user) }}</span> </li> </ol> @@ -22,6 +22,7 @@ import Vue from 'vue'; import * as emojilib from 'emojilib'; import contains from '../../../common/scripts/contains'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; const lib = Object.entries(emojilib.lib).filter((x: any) => { return x[1].category != 'flags'; @@ -107,6 +108,7 @@ export default Vue.extend({ }, methods: { getAcct, + getUserName, exec() { this.select = -1; if (this.$refs.suggests) { diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index 4ab3e46e89..9b1449daa5 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -14,7 +14,7 @@ tabindex="-1" > <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/> - <span class="name">{{ user.name }}</span> + <span class="name">{{ getUserName(user) }}</span> <span class="username">@{{ getAcct(user) }}</span> </li> </ol> @@ -33,7 +33,7 @@ <div> <img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/> <header> - <span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span> + <span class="name">{{ getUserName(isMe(message) ? message.recipient : message.user) }}</span> <span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span> <mk-time :time="message.createdAt"/> </header> @@ -52,6 +52,7 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: { @@ -94,6 +95,7 @@ export default Vue.extend({ }, methods: { getAcct, + getUserName, isMe(message) { return message.userId == (this as any).os.i.id; }, diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue index ef0c7beaad..bfb4b2bbe0 100644 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -6,7 +6,7 @@ </router-link> <div class="body"> <header> - <router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ post.user.name }}</router-link> + <router-link class="name" :to="`/@${getAcct(post.user)}`" v-user-preview="post.user.id">{{ getUserName(post.user) }}</router-link> <span class="username">@{{ getAcct(post.user) }}</span> <div class="info"> <router-link class="created-at" :to="`/@${getAcct(post.user)}/${post.id}`"> @@ -25,6 +25,7 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -38,6 +39,7 @@ export default Vue.extend({ }, methods: { getAcct, + getUserName, fetch(cb?) { this.fetching = true; (this as any).api('posts', { diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue index 623971fa33..d37ca745af 100644 --- a/src/client/app/desktop/views/components/followers-window.vue +++ b/src/client/app/desktop/views/components/followers-window.vue @@ -1,7 +1,7 @@ <template> <mk-window width="400px" height="550px" @closed="$destroy"> <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロワー </span> <mk-followers :user="user"/> </mk-window> @@ -9,8 +9,15 @@ <script lang="ts"> import Vue from 'vue'; +import getUserName from '../../../../../renderers/get-user-name'; + export default Vue.extend({ - props: ['user'] + props: ['user'], + computed { + name() { + return getUserName(this.user); + } + } }); </script> diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue index 612847b386..cbd8ec5f94 100644 --- a/src/client/app/desktop/views/components/following-window.vue +++ b/src/client/app/desktop/views/components/following-window.vue @@ -1,7 +1,7 @@ <template> <mk-window width="400px" height="550px" @closed="$destroy"> <span slot="header" :class="$style.header"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー + <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロー </span> <mk-following :user="user"/> </mk-window> @@ -9,8 +9,15 @@ <script lang="ts"> import Vue from 'vue'; +import getUserName from '../../../../../renderers/get-user-name'; + export default Vue.extend({ - props: ['user'] + props: ['user'], + computed: { + name() { + return getUserName(this.user); + } + } }); </script> diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue index 351e9e1c5c..acc4542d95 100644 --- a/src/client/app/desktop/views/components/friends-maker.vue +++ b/src/client/app/desktop/views/components/friends-maker.vue @@ -7,7 +7,7 @@ <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/> </router-link> <div class="body"> - <router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ user.name }}</router-link> + <router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ getUserName(user) }}</router-link> <p class="username">@{{ getAcct(user) }}</p> </div> <mk-follow-button :user="user"/> @@ -23,6 +23,7 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -38,6 +39,7 @@ export default Vue.extend({ }, methods: { getAcct, + getUserName, fetch() { this.fetching = true; this.users = []; diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue index f29f9b74e7..7f8c35c2f8 100644 --- a/src/client/app/desktop/views/components/messaging-room-window.vue +++ b/src/client/app/desktop/views/components/messaging-room-window.vue @@ -1,6 +1,6 @@ <template> <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy"> - <span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user.name }}</span> + <span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ name }}</span> <mk-messaging-room :user="user" :class="$style.content"/> </mk-window> </template> @@ -9,10 +9,14 @@ import Vue from 'vue'; import { url } from '../../../config'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], computed: { + name(): string { + return getUserName(this.user); + }, popout(): string { return `${url}/i/messaging/${getAcct(this.user)}`; } diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue index c5ab284df9..d8b8eab212 100644 --- a/src/client/app/desktop/views/components/notifications.vue +++ b/src/client/app/desktop/views/components/notifications.vue @@ -11,7 +11,7 @@ <div class="text"> <p> <mk-reaction-icon :reaction="notification.reaction"/> - <router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link> + <router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link> </p> <router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`"> %fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right% @@ -24,7 +24,7 @@ </router-link> <div class="text"> <p>%fa:retweet% - <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link> + <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link> </p> <router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`"> %fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right% @@ -37,7 +37,7 @@ </router-link> <div class="text"> <p>%fa:quote-left% - <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link> + <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link> </p> <router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link> </div> @@ -48,7 +48,7 @@ </router-link> <div class="text"> <p>%fa:user-plus% - <router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</router-link> + <router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link> </p> </div> </template> @@ -58,7 +58,7 @@ </router-link> <div class="text"> <p>%fa:reply% - <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link> + <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link> </p> <router-link class="post-preview" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</router-link> </div> @@ -69,7 +69,7 @@ </router-link> <div class="text"> <p>%fa:at% - <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ notification.post.user.name }}</router-link> + <router-link :to="`/@${getAcct(notification.post.user)}`" v-user-preview="notification.post.userId">{{ getUserName(notification.post.user) }}</router-link> </p> <a class="post-preview" :href="`/@${getAcct(notification.post.user)}/${notification.post.id}`">{{ getPostSummary(notification.post) }}</a> </div> @@ -79,7 +79,7 @@ <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/> </router-link> <div class="text"> - <p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ notification.user.name }}</a></p> + <p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</a></p> <router-link class="post-ref" :to="`/@${getAcct(notification.post.user)}/${notification.post.id}`"> %fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right% </router-link> @@ -104,6 +104,7 @@ import Vue from 'vue'; import getAcct from '../../../../../acct/render'; import getPostSummary from '../../../../../renderers/get-post-summary'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -154,6 +155,7 @@ export default Vue.extend({ }, methods: { getAcct, + getUserName, fetchMoreNotifications() { this.fetchingMoreNotifications = true; diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue index 59bc9ce0cf..496003eb8b 100644 --- a/src/client/app/desktop/views/components/post-detail.sub.vue +++ b/src/client/app/desktop/views/components/post-detail.sub.vue @@ -6,7 +6,7 @@ <div class="main"> <header> <div class="left"> - <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</router-link> <span class="username">@{{ acct }}</span> </div> <div class="right"> @@ -29,6 +29,7 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['post'], @@ -36,6 +37,9 @@ export default Vue.extend({ acct() { return getAcct(this.post.user); }, + name() { + return getUserName(this.post.user); + }, title(): string { return dateStringify(this.post.createdAt); } diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue index 8000ce2e6f..1a3c0d1b68 100644 --- a/src/client/app/desktop/views/components/post-detail.vue +++ b/src/client/app/desktop/views/components/post-detail.vue @@ -22,7 +22,7 @@ <img class="avatar" :src="`${post.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/> </router-link> %fa:retweet% - <router-link class="name" :href="`/@${acct}`">{{ post.user.name }}</router-link> + <router-link class="name" :href="`/@${acct}`">{{ getUserName(post.user) }}</router-link> がRepost </p> </div> @@ -31,7 +31,7 @@ <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/> </router-link> <header> - <router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link> + <router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link> <span class="username">@{{ pAcct }}</span> <router-link class="time" :to="`/@${pAcct}/${p.id}`"> <mk-time :time="p.createdAt"/> @@ -79,6 +79,7 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; import MkPostFormWindow from './post-form-window.vue'; @@ -133,9 +134,15 @@ export default Vue.extend({ acct(): string { return getAcct(this.post.user); }, + name(): string { + return getUserName(this.post.user); + }, pAcct(): string { return getAcct(this.p.user); }, + pName(): string { + return getUserName(this.p.user); + }, urls(): string[] { if (this.p.text) { const ast = parse(this.p.text); diff --git a/src/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue index 7129f67b39..99d9442d93 100644 --- a/src/client/app/desktop/views/components/post-preview.vue +++ b/src/client/app/desktop/views/components/post-preview.vue @@ -5,7 +5,7 @@ </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link> <span class="username">@{{ acct }}</span> <router-link class="time" :to="`/@${acct}/${post.id}`"> <mk-time :time="post.createdAt"/> @@ -22,6 +22,7 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['post'], @@ -29,6 +30,9 @@ export default Vue.extend({ acct() { return getAcct(this.post.user); }, + name() { + return getUserName(this.post.user); + }, title(): string { return dateStringify(this.post.createdAt); } diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue index dffecb89cc..a9cd0a9279 100644 --- a/src/client/app/desktop/views/components/posts.post.sub.vue +++ b/src/client/app/desktop/views/components/posts.post.sub.vue @@ -5,7 +5,7 @@ </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`" v-user-preview="post.userId">{{ name }}</router-link> <span class="username">@{{ acct }}</span> <router-link class="created-at" :to="`/@${acct}/${post.id}`"> <mk-time :time="post.createdAt"/> @@ -22,6 +22,7 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['post'], @@ -29,6 +30,9 @@ export default Vue.extend({ acct() { return getAcct(this.post.user); }, + name(): string { + return getUserName(this.post.user); + }, title(): string { return dateStringify(this.post.createdAt); } diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue index 9a13dd6872..17fe330420 100644 --- a/src/client/app/desktop/views/components/posts.post.vue +++ b/src/client/app/desktop/views/components/posts.post.vue @@ -10,7 +10,7 @@ </router-link> %fa:retweet% <span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span> - <a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ post.user.name }}</a> + <a class="name" :href="`/@${acct}`" v-user-preview="post.userId">{{ getUserName(post.user) }}</a> <span>{{ '%i18n:desktop.tags.mk-timeline-post.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span> </p> <mk-time :time="post.createdAt"/> @@ -86,6 +86,7 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; import MkPostFormWindow from './post-form-window.vue'; @@ -124,6 +125,9 @@ export default Vue.extend({ acct(): string { return getAcct(this.p.user); }, + name(): string { + return getUserName(this.p.user); + }, isRepost(): boolean { return (this.post.repost && this.post.text == null && diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue index c87f973faf..6bdc766538 100644 --- a/src/client/app/desktop/views/components/settings.mute.vue +++ b/src/client/app/desktop/views/components/settings.mute.vue @@ -5,7 +5,7 @@ </div> <div class="users" v-if="users.length != 0"> <div v-for="user in users" :key="user.id"> - <p><b>{{ user.name }}</b> @{{ getAcct(user) }}</p> + <p><b>{{ getUserName(user) }}</b> @{{ getAcct(user) }}</p> </div> </div> </div> @@ -14,6 +14,7 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -23,7 +24,8 @@ export default Vue.extend({ }; }, methods: { - getAcct + getAcct, + getUserName }, mounted() { (this as any).api('mute/list').then(x => { diff --git a/src/client/app/desktop/views/components/settings.profile.vue b/src/client/app/desktop/views/components/settings.profile.vue index ba86286f87..28be48e0a8 100644 --- a/src/client/app/desktop/views/components/settings.profile.vue +++ b/src/client/app/desktop/views/components/settings.profile.vue @@ -42,7 +42,7 @@ export default Vue.extend({ }; }, created() { - this.name = (this as any).os.i.name; + this.name = (this as any).os.i.name || ''; this.location = (this as any).os.i.account.profile.location; this.description = (this as any).os.i.description; this.birthday = (this as any).os.i.account.profile.birthday; @@ -53,7 +53,7 @@ export default Vue.extend({ }, save() { (this as any).api('i/update', { - name: this.name, + name: this.name || null, location: this.location || null, description: this.description || null, birthday: this.birthday || null diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index 448d04d261..7d93847fab 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -4,7 +4,7 @@ <div class="main" ref="main"> <div class="backdrop"></div> <div class="main"> - <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p> + <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p> <div class="container" ref="mainContainer"> <div class="left"> <x-nav/> @@ -33,7 +33,14 @@ import XNotifications from './ui.header.notifications.vue'; import XPost from './ui.header.post.vue'; import XClock from './ui.header.clock.vue'; +import getUserName from '../../../../../renderers/get-user-name'; + export default Vue.extend({ + computed: { + name() { + return getUserName(this.os.i); + } + }, components: { XNav, XSearch, diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue index 2d7d4dc72d..c7a132ecfb 100644 --- a/src/client/app/desktop/views/components/users-list.item.vue +++ b/src/client/app/desktop/views/components/users-list.item.vue @@ -5,7 +5,7 @@ </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ name }}</router-link> <span class="username">@{{ acct }}</span> </header> <div class="body"> @@ -20,12 +20,16 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], computed: { acct() { return getAcct(this.user); + }, + name() { + return getUserName(this.user); } } }); diff --git a/src/client/app/desktop/views/pages/messaging-room.vue b/src/client/app/desktop/views/pages/messaging-room.vue index 1e61f3ce17..1cc8d8a778 100644 --- a/src/client/app/desktop/views/pages/messaging-room.vue +++ b/src/client/app/desktop/views/pages/messaging-room.vue @@ -8,6 +8,7 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import parseAcct from '../../../../../acct/parse'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -34,7 +35,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = 'メッセージ: ' + this.user.name; + document.title = 'メッセージ: ' + getUserName(this.user); Progress.done(); }); diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue index 7497acd0e0..16625b6899 100644 --- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue +++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue @@ -4,7 +4,7 @@ <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p> <div v-if="!fetching && users.length > 0"> <router-link v-for="user in users" :to="`/@${getAcct(user)}`" :key="user.id"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name" v-user-preview="user.id"/> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)" v-user-preview="user.id"/> </router-link> </div> <p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p> @@ -14,6 +14,7 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../../acct/render'; +import getUserName from '../../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], @@ -24,7 +25,8 @@ export default Vue.extend({ }; }, method() { - getAcct + getAcct, + getUserName }, mounted() { (this as any).api('users/followers', { diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue index d30f423d53..5c6746d5dc 100644 --- a/src/client/app/desktop/views/pages/user/user.header.vue +++ b/src/client/app/desktop/views/pages/user/user.header.vue @@ -7,7 +7,7 @@ <div class="container"> <img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/> <div class="title"> - <p class="name">{{ user.name }}</p> + <p class="name">{{ name }}</p> <p class="username">@{{ acct }}</p> <p class="location" v-if="user.host === null && user.account.profile.location">%fa:map-marker%{{ user.account.profile.location }}</p> </div> @@ -23,12 +23,16 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../../acct/render'; +import getUserName from '../../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], computed: { acct() { return getAcct(this.user); + }, + name() { + return getUserName(this.user); } }, mounted() { diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index 02ddc2421d..d07b462b59 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -10,6 +10,7 @@ <script lang="ts"> import Vue from 'vue'; import parseAcct from '../../../../../../acct/parse'; +import getUserName from '../../../../../../renderers/get-user-name'; import Progress from '../../../../common/scripts/loading'; import XHeader from './user.header.vue'; import XHome from './user.home.vue'; @@ -44,7 +45,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; Progress.done(); - document.title = user.name + ' | Misskey'; + document.title = getUserName(user) + ' | Misskey'; }); } } diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue index e10e9c4f76..fa6d8c34a5 100644 --- a/src/client/app/desktop/views/widgets/channel.channel.post.vue +++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue @@ -2,7 +2,7 @@ <div class="post"> <header> <a class="index" @click="reply">{{ post.index }}:</a> - <router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ post.user.name }}</b></router-link> + <router-link class="name" :to="`/@${acct}`" v-user-preview="post.user.id"><b>{{ name }}</b></router-link> <span>ID:<i>{{ acct }}</i></span> </header> <div> @@ -20,12 +20,16 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['post'], computed: { acct() { return getAcct(this.post.user); + }, + name() { + return getUserName(this.post.user); } }, methods: { diff --git a/src/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue index 83cd67b50c..98e42222ec 100644 --- a/src/client/app/desktop/views/widgets/profile.vue +++ b/src/client/app/desktop/views/widgets/profile.vue @@ -15,19 +15,26 @@ title="クリックでアバター編集" v-user-preview="os.i.id" /> - <router-link class="name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link> + <router-link class="name" :to="`/@${os.i.username}`">{{ name }}</router-link> <p class="username">@{{ os.i.username }}</p> </div> </template> <script lang="ts"> import define from '../../../common/define-widget'; +import getUserName from '../../../../../renderers/get-user-name'; + export default define({ name: 'profile', props: () => ({ design: 0 }) }).extend({ + computed: { + name() { + return getUserName(this.os.i); + } + }, methods: { func() { if (this.props.design == 2) { diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue index 6f6a101577..a5dabb68fc 100644 --- a/src/client/app/desktop/views/widgets/users.vue +++ b/src/client/app/desktop/views/widgets/users.vue @@ -11,7 +11,7 @@ <img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/> </router-link> <div class="body"> - <router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ _user.name }}</router-link> + <router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ getUserName(_user) }}</router-link> <p class="username">@{{ getAcct(_user) }}</p> </div> <mk-follow-button :user="_user"/> @@ -24,6 +24,7 @@ <script lang="ts"> import define from '../../../common/define-widget'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; const limit = 3; @@ -45,6 +46,7 @@ export default define({ }, methods: { getAcct, + getUserName, func() { this.props.compact = !this.props.compact; }, diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 3e5c38961f..2fb8f15cf3 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -14,7 +14,7 @@ import ElementLocaleJa from 'element-ui/lib/locale/lang/ja'; import App from './app.vue'; import checkForUpdate from './common/scripts/check-for-update'; import MiOS, { API } from './common/mios'; -import { version, codename, hostname, lang } from './config'; +import { version, codename, lang } from './config'; let elementLocale; switch (lang) { @@ -60,10 +60,6 @@ console.info( window.clearTimeout((window as any).mkBootTimer); delete (window as any).mkBootTimer; -if (hostname != 'localhost') { - document.domain = hostname; -} - //#region Set lang attr const html = document.documentElement; html.setAttribute('lang', lang); diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue index 77abd3c0ea..0492c5d86c 100644 --- a/src/client/app/mobile/views/components/notification-preview.vue +++ b/src/client/app/mobile/views/components/notification-preview.vue @@ -3,7 +3,7 @@ <template v-if="notification.type == 'reaction'"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user.name }}</p> + <p><mk-reaction-icon :reaction="notification.reaction"/>{{ name }}</p> <p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p> </div> </template> @@ -11,7 +11,7 @@ <template v-if="notification.type == 'repost'"> <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:retweet%{{ notification.post.user.name }}</p> + <p>%fa:retweet%{{ posterName }}</p> <p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right%</p> </div> </template> @@ -19,7 +19,7 @@ <template v-if="notification.type == 'quote'"> <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:quote-left%{{ notification.post.user.name }}</p> + <p>%fa:quote-left%{{ posterName }}</p> <p class="post-preview">{{ getPostSummary(notification.post) }}</p> </div> </template> @@ -27,14 +27,14 @@ <template v-if="notification.type == 'follow'"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:user-plus%{{ notification.user.name }}</p> + <p>%fa:user-plus%{{ name }}</p> </div> </template> <template v-if="notification.type == 'reply'"> <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:reply%{{ notification.post.user.name }}</p> + <p>%fa:reply%{{ posterName }}</p> <p class="post-preview">{{ getPostSummary(notification.post) }}</p> </div> </template> @@ -42,7 +42,7 @@ <template v-if="notification.type == 'mention'"> <img class="avatar" :src="`${notification.post.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:at%{{ notification.post.user.name }}</p> + <p>%fa:at%{{ posterName }}</p> <p class="post-preview">{{ getPostSummary(notification.post) }}</p> </div> </template> @@ -50,7 +50,7 @@ <template v-if="notification.type == 'poll_vote'"> <img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> <div class="text"> - <p>%fa:chart-pie%{{ notification.user.name }}</p> + <p>%fa:chart-pie%{{ name }}</p> <p class="post-ref">%fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right%</p> </div> </template> @@ -60,9 +60,18 @@ <script lang="ts"> import Vue from 'vue'; import getPostSummary from '../../../../../renderers/get-post-summary'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['notification'], + computed: { + name() { + return getUserName(this.notification.user); + }, + posterName() { + return getUserName(this.notification.post.user); + } + }, data() { return { getPostSummary diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index 189d7195fb..62a0e6425e 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -8,7 +8,7 @@ <div class="text"> <p> <mk-reaction-icon :reaction="notification.reaction"/> - <router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link> + <router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link> </p> <router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`"> %fa:quote-left%{{ getPostSummary(notification.post) }} @@ -25,7 +25,7 @@ <div class="text"> <p> %fa:retweet% - <router-link :to="`/@${acct}`">{{ notification.post.user.name }}</router-link> + <router-link :to="`/@${acct}`">{{ getUserName(notification.post.user) }}</router-link> </p> <router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`"> %fa:quote-left%{{ getPostSummary(notification.post.repost) }}%fa:quote-right% @@ -45,7 +45,7 @@ <div class="text"> <p> %fa:user-plus% - <router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link> + <router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link> </p> </div> </div> @@ -66,7 +66,7 @@ <div class="text"> <p> %fa:chart-pie% - <router-link :to="`/@${acct}`">{{ notification.user.name }}</router-link> + <router-link :to="`/@${acct}`">{{ getUserName(notification.user) }}</router-link> </p> <router-link class="post-ref" :to="`/@${acct}/${notification.post.id}`"> %fa:quote-left%{{ getPostSummary(notification.post) }}%fa:quote-right% @@ -80,12 +80,19 @@ import Vue from 'vue'; import getPostSummary from '../../../../../renderers/get-post-summary'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['notification'], computed: { acct() { return getAcct(this.notification.user); + }, + name() { + return getUserName(this.notification.user); + }, + posterName() { + return getUserName(this.notification.post.user); } }, data() { diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue index 38d4522d26..68a42ef249 100644 --- a/src/client/app/mobile/views/components/post-card.vue +++ b/src/client/app/mobile/views/components/post-card.vue @@ -2,7 +2,7 @@ <div class="mk-post-card"> <a :href="`/@${acct}/${post.id}`"> <header> - <img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ post.user.name }}</h3> + <img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ name }}</h3> </header> <div> {{ text }} @@ -16,6 +16,7 @@ import Vue from 'vue'; import summary from '../../../../../renderers/get-post-summary'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['post'], @@ -23,6 +24,9 @@ export default Vue.extend({ acct() { return getAcct(this.post.user); }, + name() { + return getUserName(this.post.user); + }, text(): string { return summary(this.post); } diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue index 7ff9f1aabd..98d6a14cac 100644 --- a/src/client/app/mobile/views/components/post-detail.sub.vue +++ b/src/client/app/mobile/views/components/post-detail.sub.vue @@ -5,7 +5,7 @@ </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`">{{ getUserName(post.user) }}</router-link> <span class="username">@{{ acct }}</span> <router-link class="time" :to="`/@${acct}/${post.id}`"> <mk-time :time="post.createdAt"/> @@ -21,12 +21,16 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['post'], computed: { acct() { return getAcct(this.post.user); + }, + name() { + return getUserName(this.post.user); } } }); diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue index ed394acd36..0226ce081a 100644 --- a/src/client/app/mobile/views/components/post-detail.vue +++ b/src/client/app/mobile/views/components/post-detail.vue @@ -22,7 +22,7 @@ </router-link> %fa:retweet% <router-link class="name" :to="`/@${acct}`"> - {{ post.user.name }} + {{ name }} </router-link> がRepost </p> @@ -33,7 +33,7 @@ <img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/> </router-link> <div> - <router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link> + <router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link> <span class="username">@{{ pAcct }}</span> </div> </header> @@ -81,6 +81,7 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; import MkPostMenu from '../../../common/views/components/post-menu.vue'; @@ -114,9 +115,15 @@ export default Vue.extend({ acct(): string { return getAcct(this.post.user); }, + name(): string { + return getUserName(this.post.user); + }, pAcct(): string { return getAcct(this.p.user); }, + pName(): string { + return getUserName(this.p.user); + }, isRepost(): boolean { return (this.post.repost && this.post.text == null && diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue index 81b0c22bfb..96bd4d5c15 100644 --- a/src/client/app/mobile/views/components/post-preview.vue +++ b/src/client/app/mobile/views/components/post-preview.vue @@ -5,7 +5,7 @@ </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`">{{ name }}</router-link> <span class="username">@{{ acct }}</span> <router-link class="time" :to="`/@${acct}/${post.id}`"> <mk-time :time="post.createdAt"/> @@ -21,12 +21,16 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['post'], computed: { acct() { return getAcct(this.post.user); + }, + name() { + return getUserName(this.post.user); } } }); diff --git a/src/client/app/mobile/views/components/post.sub.vue b/src/client/app/mobile/views/components/post.sub.vue index 85ddb91880..909d5cb597 100644 --- a/src/client/app/mobile/views/components/post.sub.vue +++ b/src/client/app/mobile/views/components/post.sub.vue @@ -5,7 +5,7 @@ </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`">{{ getUserName(post.user) }}</router-link> <span class="username">@{{ acct }}</span> <router-link class="created-at" :to="`/@${acct}/${post.id}`"> <mk-time :time="post.createdAt"/> @@ -21,12 +21,16 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['post'], computed: { acct() { return getAcct(this.post.user); + }, + name() { + return getUserName(this.post.user); } } }); diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue index 1454bc939f..eee1e80fd3 100644 --- a/src/client/app/mobile/views/components/post.vue +++ b/src/client/app/mobile/views/components/post.vue @@ -10,7 +10,7 @@ </router-link> %fa:retweet% <span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('{')) }}</span> - <router-link class="name" :to="`/@${acct}`">{{ post.user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`">{{ name }}</router-link> <span>{{ '%i18n:mobile.tags.mk-timeline-post.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-post.reposted-by%'.indexOf('}') + 1) }}</span> </p> <mk-time :time="post.createdAt"/> @@ -21,7 +21,7 @@ </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${pAcct}`">{{ p.user.name }}</router-link> + <router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link> <span class="is-bot" v-if="p.user.host === null && p.user.account.isBot">bot</span> <span class="username">@{{ pAcct }}</span> <div class="info"> @@ -78,6 +78,7 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; import parse from '../../../../../text/parse'; import MkPostMenu from '../../../common/views/components/post-menu.vue'; @@ -102,9 +103,15 @@ export default Vue.extend({ acct(): string { return getAcct(this.post.user); }, + name(): string { + return getUserName(this.post.user); + }, pAcct(): string { return getAcct(this.p.user); }, + pName(): string { + return getUserName(this.p.user); + }, isRepost(): boolean { return (this.post.repost && this.post.text == null && diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue index 2bf47a90a9..fd4f31fd98 100644 --- a/src/client/app/mobile/views/components/ui.header.vue +++ b/src/client/app/mobile/views/components/ui.header.vue @@ -3,7 +3,7 @@ <mk-special-message/> <div class="main" ref="main"> <div class="backdrop"></div> - <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i.name }}</b>さん</p> + <p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p> <div class="content" ref="mainContainer"> <button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button> <template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template> @@ -19,9 +19,15 @@ <script lang="ts"> import Vue from 'vue'; import * as anime from 'animejs'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['func'], + computed: { + name() { + return getUserName(this.os.i); + } + }, data() { return { hasUnreadNotifications: false, diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index a923774a73..61dd8ca9de 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -11,7 +11,7 @@ <div class="body" v-if="isOpen"> <router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`"> <img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/> - <p class="name">{{ os.i.name }}</p> + <p class="name">{{ name }}</p> </router-link> <div class="links"> <ul> @@ -40,9 +40,15 @@ <script lang="ts"> import Vue from 'vue'; import { docsUrl, chUrl, lang } from '../../../config'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['isOpen'], + computed: { + name() { + return getUserName(this.os.i); + } + }, data() { return { hasUnreadNotifications: false, diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue index 46fa3b4732..e8698a62f0 100644 --- a/src/client/app/mobile/views/components/user-card.vue +++ b/src/client/app/mobile/views/components/user-card.vue @@ -5,7 +5,7 @@ <img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/> </a> </header> - <a class="name" :href="`/@${acct}`" target="_blank">{{ user.name }}</a> + <a class="name" :href="`/@${acct}`" target="_blank">{{ name }}</a> <p class="username">@{{ acct }}</p> <mk-follow-button :user="user"/> </div> @@ -14,12 +14,16 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], computed: { acct() { return getAcct(this.user); + }, + name() { + return getUserName(this.user); } } }); diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue index 00f87e5549..72a6bcf8ad 100644 --- a/src/client/app/mobile/views/components/user-preview.vue +++ b/src/client/app/mobile/views/components/user-preview.vue @@ -5,7 +5,7 @@ </router-link> <div class="main"> <header> - <router-link class="name" :to="`/@${acct}`">{{ user.name }}</router-link> + <router-link class="name" :to="`/@${acct}`">{{ name }}</router-link> <span class="username">@{{ acct }}</span> </header> <div class="body"> @@ -18,12 +18,16 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../acct/render'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], computed: { acct() { return getAcct(this.user); + }, + name() { + return getUserName(this.user); } } }); diff --git a/src/client/app/mobile/views/pages/followers.vue b/src/client/app/mobile/views/pages/followers.vue index d2e6a9aea5..f4225d556d 100644 --- a/src/client/app/mobile/views/pages/followers.vue +++ b/src/client/app/mobile/views/pages/followers.vue @@ -2,7 +2,7 @@ <mk-ui> <template slot="header" v-if="!fetching"> <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> - {{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) }} + {{ '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', name) }} </template> <mk-users-list v-if="!fetching" @@ -20,6 +20,7 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import parseAcct from '../../../../../acct/parse'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -28,6 +29,11 @@ export default Vue.extend({ user: null }; }, + computed: { + name() { + return getUserName(this.user); + } + }, watch: { $route: 'fetch' }, @@ -46,7 +52,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey'; + document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', this.name) + ' | Misskey'; }); }, onLoaded() { diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue index 3690536cf5..cc2442e476 100644 --- a/src/client/app/mobile/views/pages/following.vue +++ b/src/client/app/mobile/views/pages/following.vue @@ -2,7 +2,7 @@ <mk-ui> <template slot="header" v-if="!fetching"> <img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""> - {{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', user.name) }} + {{ '%i18n:mobile.tags.mk-user-following-page.following-of%'.replace('{}', name) }} </template> <mk-users-list v-if="!fetching" @@ -20,6 +20,7 @@ import Vue from 'vue'; import Progress from '../../../common/scripts/loading'; import parseAcct from '../../../../../acct/parse'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -28,6 +29,11 @@ export default Vue.extend({ user: null }; }, + computed: { + name() { + return getUserName(this.user); + } + }, watch: { $route: 'fetch' }, @@ -46,7 +52,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey'; + document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', this.name) + ' | Misskey'; }); }, onLoaded() { diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue index 172666eea5..ae6662dc04 100644 --- a/src/client/app/mobile/views/pages/messaging-room.vue +++ b/src/client/app/mobile/views/pages/messaging-room.vue @@ -1,7 +1,7 @@ <template> <mk-ui> <span slot="header"> - <template v-if="user">%fa:R comments%{{ user.name }}</template> + <template v-if="user">%fa:R comments%{{ name }}</template> <template v-else><mk-ellipsis/></template> </span> <mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/> @@ -11,6 +11,7 @@ <script lang="ts"> import Vue from 'vue'; import parseAcct from '../../../../../acct/parse'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -19,6 +20,11 @@ export default Vue.extend({ user: null }; }, + computed: { + name() { + return getUserName(this.user); + } + }, watch: { $route: 'fetch' }, @@ -33,7 +39,7 @@ export default Vue.extend({ this.user = user; this.fetching = false; - document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${user.name} | Misskey`; + document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${this.name} | Misskey`; }); } } diff --git a/src/client/app/mobile/views/pages/profile-setting.vue b/src/client/app/mobile/views/pages/profile-setting.vue index 15f9bc9b68..4a560c0272 100644 --- a/src/client/app/mobile/views/pages/profile-setting.vue +++ b/src/client/app/mobile/views/pages/profile-setting.vue @@ -52,7 +52,7 @@ export default Vue.extend({ }; }, created() { - this.name = (this as any).os.i.name; + this.name = (this as any).os.i.name || ''; this.location = (this as any).os.i.account.profile.location; this.description = (this as any).os.i.description; this.birthday = (this as any).os.i.account.profile.birthday; @@ -94,7 +94,7 @@ export default Vue.extend({ this.saving = true; (this as any).api('i/update', { - name: this.name, + name: this.name || null, location: this.location || null, description: this.description || null, birthday: this.birthday || null diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index a945a21c5c..58a9d4e37e 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -2,7 +2,7 @@ <mk-ui> <span slot="header">%fa:cog%%i18n:mobile.tags.mk-settings-page.settings%</span> <div :class="$style.content"> - <p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + os.i.name + '</b>')"></p> + <p v-html="'%i18n:mobile.tags.mk-settings.signed-in-as%'.replace('{}', '<b>' + name + '</b>')"></p> <ul> <li><router-link to="./settings/profile">%fa:user%%i18n:mobile.tags.mk-settings-page.profile%%fa:angle-right%</router-link></li> <li><router-link to="./settings/authorized-apps">%fa:puzzle-piece%%i18n:mobile.tags.mk-settings-page.applications%%fa:angle-right%</router-link></li> @@ -20,6 +20,7 @@ <script lang="ts"> import Vue from 'vue'; import { version, codename } from '../../../config'; +import getUserName from '../../../../../renderers/get-user-name'; export default Vue.extend({ data() { @@ -28,6 +29,11 @@ export default Vue.extend({ codename }; }, + computed: { + name() { + return getUserName(this.os.i); + } + }, mounted() { document.title = 'Misskey | %i18n:mobile.tags.mk-settings-page.settings%'; document.documentElement.style.background = '#313a42'; diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index be696484b6..3b7b4d6c28 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -1,6 +1,6 @@ <template> <mk-ui> - <span slot="header" v-if="!fetching">%fa:user% {{ user.name }}</span> + <span slot="header" v-if="!fetching">%fa:user% {{ user }}</span> <main v-if="!fetching"> <header> <div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div> @@ -12,7 +12,7 @@ <mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/> </div> <div class="title"> - <h1>{{ user.name }}</h1> + <h1>{{ user }}</h1> <span class="username">@{{ acct }}</span> <span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span> </div> @@ -61,7 +61,7 @@ import Vue from 'vue'; import * as age from 's-age'; import getAcct from '../../../../../acct/render'; -import getAcct from '../../../../../acct/parse'; +import getUserName from '../../../../../renderers/get-user-name'; import Progress from '../../../common/scripts/loading'; import XHome from './user/home.vue'; @@ -82,6 +82,9 @@ export default Vue.extend({ }, age(): number { return age(this.user.account.profile.birthday); + }, + name() { + return getUserName(this.user); } }, watch: { @@ -102,7 +105,7 @@ export default Vue.extend({ this.fetching = false; Progress.done(); - document.title = user.name + ' | Misskey'; + document.title = this.name + ' | Misskey'; }); } } diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue index ffdd9f178d..1b128e2f24 100644 --- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue +++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue @@ -3,7 +3,7 @@ <p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p> <div v-if="!fetching && users.length > 0"> <a v-for="user in users" :key="user.id" :href="`/@${getAcct(user)}`"> - <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user.name"/> + <img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)"/> </a> </div> <p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p> @@ -13,6 +13,7 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../../acct/render'; +import getUserName from '../../../../../../renderers/get-user-name'; export default Vue.extend({ props: ['user'], @@ -22,6 +23,11 @@ export default Vue.extend({ users: [] }; }, + computed: { + name() { + return getUserName(this.user); + } + }, methods: { getAcct }, diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue index f1d283e45a..bd257a3ff3 100644 --- a/src/client/app/mobile/views/widgets/profile.vue +++ b/src/client/app/mobile/views/widgets/profile.vue @@ -8,15 +8,23 @@ :src="`${os.i.avatarUrl}?thumbnail&size=96`" alt="avatar" /> - <router-link :class="$style.name" :to="`/@${os.i.username}`">{{ os.i.name }}</router-link> + <router-link :class="$style.name" :to="`/@${os.i.username}`">{{ name }}</router-link> </mk-widget-container> </div> </template> <script lang="ts"> import define from '../../../common/define-widget'; +import getUserName from '../../../../../renderers/get-user-name'; + export default define({ name: 'profile' +}).extend({ + computed: { + name() { + return getUserName(this.os.i); + } + } }); </script> diff --git a/src/following/distribute.ts b/src/following/distribute.ts deleted file mode 100644 index 10ff988814..0000000000 --- a/src/following/distribute.ts +++ /dev/null @@ -1,42 +0,0 @@ -import User, { pack as packUser } from '../models/user'; -import FollowingLog from '../models/following-log'; -import FollowedLog from '../models/followed-log'; -import event from '../publishers/stream'; -import notify from '../publishers/notify'; - -export default async (follower, followee) => Promise.all([ - // Increment following count - User.update(follower._id, { - $inc: { - followingCount: 1 - } - }), - - FollowingLog.insert({ - createdAt: new Date(), - userId: followee._id, - count: follower.followingCount + 1 - }), - - // Increment followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: 1 - } - }), - - FollowedLog.insert({ - createdAt: new Date(), - userId: follower._id, - count: followee.followersCount + 1 - }), - - followee.host === null && Promise.all([ - // Notify - notify(followee.id, follower.id, 'follow'), - - // Publish follow event - packUser(follower, followee) - .then(packed => event(followee._id, 'followed', packed)) - ]) -]); diff --git a/src/index.ts b/src/index.ts index 21fb2f5530..68b289793b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,8 +30,12 @@ const ev = new Xev(); process.title = 'Misskey'; +if (process.env.NODE_ENV != 'production') { + process.env.DEBUG = 'misskey:*'; +} + // https://github.com/Automattic/kue/issues/822 -require('events').EventEmitter.prototype._maxListeners = 256; +require('events').EventEmitter.prototype._maxListeners = 512; // Start app main(); @@ -99,7 +103,7 @@ async function workerMain(opt) { if (!opt['only-server']) { // start processor - require('./queue').process(); + require('./queue').default(); } // Send a 'ready' message to parent process diff --git a/src/models/post-watching.ts b/src/models/post-watching.ts index b4ddcaafa6..032b9d10fa 100644 --- a/src/models/post-watching.ts +++ b/src/models/post-watching.ts @@ -2,6 +2,7 @@ import * as mongo from 'mongodb'; import db from '../db/mongodb'; const PostWatching = db.get<IPostWatching>('postWatching'); +PostWatching.createIndex(['userId', 'postId'], { unique: true }); export default PostWatching; export interface IPostWatching { diff --git a/src/models/post.ts b/src/models/post.ts index 2f2b51b946..ac7890d2e6 100644 --- a/src/models/post.ts +++ b/src/models/post.ts @@ -27,6 +27,7 @@ export type IPost = { _id: mongo.ObjectID; channelId: mongo.ObjectID; createdAt: Date; + deletedAt: Date; mediaIds: mongo.ObjectID[]; replyId: mongo.ObjectID; repostId: mongo.ObjectID; @@ -52,6 +53,20 @@ export type IPost = { speed: number; }; uri: string; + + _reply?: { + userId: mongo.ObjectID; + }; + _repost?: { + userId: mongo.ObjectID; + }; + _user: { + host: string; + hostLower: string; + account: { + inbox?: string; + }; + }; }; /** diff --git a/src/models/user.ts b/src/models/user.ts index f817c33aa2..92091c6879 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -21,7 +21,7 @@ type IUserBase = { deletedAt: Date; followersCount: number; followingCount: number; - name: string; + name?: string; postsCount: number; driveCapacity: number; username: string; @@ -99,8 +99,8 @@ export function validatePassword(password: string): boolean { return typeof password == 'string' && password != ''; } -export function isValidName(name: string): boolean { - return typeof name == 'string' && name.length < 30 && name.trim() != ''; +export function isValidName(name?: string): boolean { + return name === null || (typeof name == 'string' && name.length < 30 && name.trim() != ''); } export function isValidDescription(description: string): boolean { diff --git a/src/othello/ai/back.ts b/src/othello/ai/back.ts index d6704b1750..4d06ed9565 100644 --- a/src/othello/ai/back.ts +++ b/src/othello/ai/back.ts @@ -9,6 +9,7 @@ import * as request from 'request-promise-native'; import Othello, { Color } from '../core'; import conf from '../../config'; +import getUserName from '../../renderers/get-user-name'; let game; let form; @@ -47,8 +48,8 @@ process.on('message', async msg => { const user = game.user1Id == id ? game.user2 : game.user1; const isSettai = form[0].value === 0; const text = isSettai - ? `?[${user.name}](${conf.url}/@${user.username})さんの接待を始めました!` - : `対局を?[${user.name}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`; + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんの接待を始めました!` + : `対局を?[${getUserName(user)}](${conf.url}/@${user.username})さんと始めました! (強さ${form[0].value})`; const res = await request.post(`${conf.api_url}/posts/create`, { json: { i, @@ -72,15 +73,15 @@ process.on('message', async msg => { const isSettai = form[0].value === 0; const text = isSettai ? msg.body.winnerId === null - ? `?[${user.name}](${conf.url}/@${user.username})さんに接待で引き分けました...` + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で引き分けました...` : msg.body.winnerId == id - ? `?[${user.name}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...` - : `?[${user.name}](${conf.url}/@${user.username})さんに接待で負けてあげました♪` + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で勝ってしまいました...` + : `?[${getUserName(user)}](${conf.url}/@${user.username})さんに接待で負けてあげました♪` : msg.body.winnerId === null - ? `?[${user.name}](${conf.url}/@${user.username})さんと引き分けました~` + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんと引き分けました~` : msg.body.winnerId == id - ? `?[${user.name}](${conf.url}/@${user.username})さんに勝ちました♪` - : `?[${user.name}](${conf.url}/@${user.username})さんに負けました...`; + ? `?[${getUserName(user)}](${conf.url}/@${user.username})さんに勝ちました♪` + : `?[${getUserName(user)}](${conf.url}/@${user.username})さんに負けました...`; await request.post(`${conf.api_url}/posts/create`, { json: { i, diff --git a/src/post/create.ts b/src/post/create.ts deleted file mode 100644 index 4ad1503e0f..0000000000 --- a/src/post/create.ts +++ /dev/null @@ -1,40 +0,0 @@ -import Post from '../models/post'; - -export default async (post, reply, repost, mentions) => { - post.mentions = []; - - function addMention(mentionee) { - // Reject if already added - if (post.mentions.some(x => x.equals(mentionee))) return; - - // Add mention - post.mentions.push(mentionee); - } - - if (reply) { - // Add mention - addMention(reply.userId); - post.replyId = reply._id; - post._reply = { userId: reply.userId }; - } else { - post.replyId = null; - post._reply = null; - } - - if (repost) { - if (post.text) { - // Add mention - addMention(repost.userId); - } - - post.repostId = repost._id; - post._repost = { userId: repost.userId }; - } else { - post.repostId = null; - post._repost = null; - } - - await Promise.all(mentions.map(({ _id }) => addMention(_id))); - - return Post.insert(post); -}; diff --git a/src/post/distribute.ts b/src/post/distribute.ts deleted file mode 100644 index f748a620c0..0000000000 --- a/src/post/distribute.ts +++ /dev/null @@ -1,274 +0,0 @@ -import Channel from '../models/channel'; -import ChannelWatching from '../models/channel-watching'; -import Following from '../models/following'; -import Mute from '../models/mute'; -import Post, { pack } from '../models/post'; -import Watching from '../models/post-watching'; -import User, { isLocalUser } from '../models/user'; -import stream, { publishChannelStream } from '../publishers/stream'; -import notify from '../publishers/notify'; -import pushSw from '../publishers/push-sw'; -import { createHttp } from '../queue'; -import watch from './watch'; - -export default async (user, mentions, post) => { - const promisedPostObj = pack(post); - const promises = [ - User.update({ _id: user._id }, { - // Increment my posts count - $inc: { - postsCount: 1 - }, - - $set: { - latestPost: post._id - } - }), - ] as Array<Promise<any>>; - - function addMention(promisedMentionee, reason) { - // Publish event - promises.push(promisedMentionee.then(mentionee => { - if (user._id.equals(mentionee)) { - return Promise.resolve(); - } - - return Promise.all([ - promisedPostObj, - Mute.find({ - muterId: mentionee, - deletedAt: { $exists: false } - }) - ]).then(([postObj, mentioneeMutes]) => { - const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); - if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { - stream(mentionee, reason, postObj); - pushSw(mentionee, reason, postObj); - } - }); - })); - } - - // タイムラインへの投稿 - if (!post.channelId) { - promises.push( - // Publish event to myself's stream - promisedPostObj.then(postObj => { - stream(post.userId, 'post', postObj); - }), - - Promise.all([ - User.findOne({ _id: post.userId }), - - // Fetch all followers - Following.aggregate([{ - $lookup: { - from: 'users', - localField: 'followerId', - foreignField: '_id', - as: 'follower' - } - }, { - $match: { - followeeId: post.userId - } - }], { - _id: false - }) - ]).then(([user, followers]) => Promise.all(followers.map(following => { - if (isLocalUser(following.follower)) { - // Publish event to followers stream - return promisedPostObj.then(postObj => { - stream(following.followerId, 'post', postObj); - }); - } - - return new Promise((resolve, reject) => { - createHttp({ - type: 'deliverPost', - fromId: user._id, - toId: following.followerId, - postId: post._id - }).save(error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - }))) - ); - } - - // チャンネルへの投稿 - if (post.channelId) { - promises.push( - // Increment channel index(posts count) - Channel.update({ _id: post.channelId }, { - $inc: { - index: 1 - } - }), - - // Publish event to channel - promisedPostObj.then(postObj => { - publishChannelStream(post.channelId, 'post', postObj); - }), - - Promise.all([ - promisedPostObj, - - // Get channel watchers - ChannelWatching.find({ - channelId: post.channelId, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }) - ]).then(([postObj, watches]) => { - // チャンネルの視聴者(のタイムライン)に配信 - watches.forEach(w => { - stream(w.userId, 'post', postObj); - }); - }) - ); - } - - // If has in reply to post - if (post.replyId) { - promises.push( - // Increment replies count - Post.update({ _id: post.replyId }, { - $inc: { - repliesCount: 1 - } - }), - - // 自分自身へのリプライでない限りは通知を作成 - promisedPostObj.then(({ reply }) => { - return notify(reply.userId, user._id, 'reply', { - postId: post._id - }); - }), - - // Fetch watchers - Watching - .find({ - postId: post.replyId, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.userId, user._id, 'reply', { - postId: post._id - }); - }); - }) - ); - - // Add mention - addMention(promisedPostObj.then(({ reply }) => reply.userId), 'reply'); - - // この投稿をWatchする - if (user.account.settings.autoWatch !== false) { - promises.push(promisedPostObj.then(({ reply }) => { - return watch(user._id, reply); - })); - } - } - - // If it is repost - if (post.repostId) { - const type = post.text ? 'quote' : 'repost'; - - promises.push( - promisedPostObj.then(({ repost }) => Promise.all([ - // Notify - notify(repost.userId, user._id, type, { - postId: post._id - }), - - // この投稿をWatchする - // TODO: ユーザーが「Repostしたときに自動でWatchする」設定を - // オフにしていた場合はしない - watch(user._id, repost) - ])), - - // Fetch watchers - Watching - .find({ - postId: post.repostId, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.userId, user._id, type, { - postId: post._id - }); - }); - }) - ); - - // If it is quote repost - if (post.text) { - // Add mention - addMention(promisedPostObj.then(({ repost }) => repost.userId), 'quote'); - } else { - promises.push(promisedPostObj.then(postObj => { - // Publish event - if (!user._id.equals(postObj.repost.userId)) { - stream(postObj.repost.userId, 'repost', postObj); - } - })); - } - - // 今までで同じ投稿をRepostしているか - const existRepost = await Post.findOne({ - userId: user._id, - repostId: post.repostId, - _id: { - $ne: post._id - } - }); - - if (!existRepost) { - // Update repostee status - promises.push(Post.update({ _id: post.repostId }, { - $inc: { - repostCount: 1 - } - })); - } - } - - // Resolve all mentions - await promisedPostObj.then(({ reply, repost }) => Promise.all(mentions.map(async mention => { - // 既に言及されたユーザーに対する返信や引用repostの場合も無視 - if (reply && reply.userId.equals(mention)) return; - if (repost && repost.userId.equals(mention)) return; - - // Add mention - addMention(mention, 'mention'); - - // Create notification - await notify(mention, user._id, 'mention', { - postId: post._id - }); - }))); - - await Promise.all(promises); - - return promisedPostObj; -}; diff --git a/src/queue/index.ts b/src/queue/index.ts index f90754a561..4aa1dc032d 100644 --- a/src/queue/index.ts +++ b/src/queue/index.ts @@ -1,6 +1,6 @@ import { createQueue } from 'kue'; + import config from '../config'; -import db from './processors/db'; import http from './processors/http'; const queue = createQueue({ @@ -18,17 +18,19 @@ export function createHttp(data) { .backoff({ delay: 16384, type: 'exponential' }); } -export function createDb(data) { - return queue.create('db', data); +export function deliver(user, content, to) { + return createHttp({ + type: 'deliver', + user, + content, + to + }); } -export function process() { - queue.process('db', db); - +export default function() { /* 256 is the default concurrency limit of Mozilla Firefox and Google Chromium. - a8af215e691f3a2205a3758d2d96e9d328e100ff - chromium/src.git - Git at Google https://chromium.googlesource.com/chromium/src.git/+/a8af215e691f3a2205a3758d2d96e9d328e100ff Network.http.max-connections - MozillaZine Knowledge Base diff --git a/src/queue/processors/db/delete-post-dependents.ts b/src/queue/processors/db/delete-post-dependents.ts deleted file mode 100644 index fb6617e952..0000000000 --- a/src/queue/processors/db/delete-post-dependents.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Favorite from '../../../models/favorite'; -import Notification from '../../../models/notification'; -import PollVote from '../../../models/poll-vote'; -import PostReaction from '../../../models/post-reaction'; -import PostWatching from '../../../models/post-watching'; -import Post from '../../../models/post'; - -export default ({ data }, done) => Promise.all([ - Favorite.remove({ postId: data._id }), - Notification.remove({ postId: data._id }), - PollVote.remove({ postId: data._id }), - PostReaction.remove({ postId: data._id }), - PostWatching.remove({ postId: data._id }), - Post.find({ repostId: data._id }).then(reposts => Promise.all([ - Notification.remove({ - postId: { - $in: reposts.map(({ _id }) => _id) - } - }), - Post.remove({ repostId: data._id }) - ])) -]).then(() => done(), done); diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts deleted file mode 100644 index 468ec442ac..0000000000 --- a/src/queue/processors/db/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import deletePostDependents from './delete-post-dependents'; - -const handlers = { - deletePostDependents -}; - -export default (job, done) => handlers[job.data.type](job, done); diff --git a/src/queue/processors/http/deliver-post.ts b/src/queue/processors/http/deliver-post.ts deleted file mode 100644 index 8107c8bf74..0000000000 --- a/src/queue/processors/http/deliver-post.ts +++ /dev/null @@ -1,27 +0,0 @@ -import Post from '../../../models/post'; -import User, { IRemoteUser } from '../../../models/user'; -import context from '../../../remote/activitypub/renderer/context'; -import renderCreate from '../../../remote/activitypub/renderer/create'; -import renderNote from '../../../remote/activitypub/renderer/note'; -import request from '../../../remote/request'; - -export default async ({ data }, done) => { - try { - const promisedTo = User.findOne({ _id: data.toId }) as Promise<IRemoteUser>; - const [from, post] = await Promise.all([ - User.findOne({ _id: data.fromId }), - Post.findOne({ _id: data.postId }) - ]); - const note = await renderNote(from, post); - const to = await promisedTo; - const create = renderCreate(note); - - create['@context'] = context; - - await request(from, to.account.inbox, create); - } catch (error) { - done(error); - } - - done(); -}; diff --git a/src/queue/processors/http/deliver.ts b/src/queue/processors/http/deliver.ts new file mode 100644 index 0000000000..422e355b5f --- /dev/null +++ b/src/queue/processors/http/deliver.ts @@ -0,0 +1,19 @@ +import * as kue from 'kue'; + +import request from '../../../remote/request'; + +export default async (job: kue.Job, done): Promise<void> => { + try { + await request(job.data.user, job.data.to, job.data.content); + done(); + } catch (res) { + if (res.statusCode >= 400 && res.statusCode < 500) { + // HTTPステータスコード4xxはクライアントエラーであり、それはつまり + // 何回再送しても成功することはないということなのでエラーにはしないでおく + done(); + } else { + console.warn(`deliver failed: ${res.statusMessage}`); + done(new Error(res.statusMessage)); + } + } +}; diff --git a/src/queue/processors/http/follow.ts b/src/queue/processors/http/follow.ts deleted file mode 100644 index ba1cc31186..0000000000 --- a/src/queue/processors/http/follow.ts +++ /dev/null @@ -1,66 +0,0 @@ -import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user'; -import Following from '../../../models/following'; -import FollowingLog from '../../../models/following-log'; -import FollowedLog from '../../../models/followed-log'; -import event from '../../../publishers/stream'; -import notify from '../../../publishers/notify'; -import context from '../../../remote/activitypub/renderer/context'; -import render from '../../../remote/activitypub/renderer/follow'; -import request from '../../../remote/request'; - -export default ({ data }, done) => Following.findOne({ _id: data.following }).then(async ({ followerId, followeeId }) => { - const [follower, followee] = await Promise.all([ - User.findOne({ _id: followerId }), - User.findOne({ _id: followeeId }) - ]); - - if (isLocalUser(follower) && isRemoteUser(followee)) { - const rendered = render(follower, followee); - rendered['@context'] = context; - - await request(follower, followee.account.inbox, rendered); - } - - return [follower, followee]; -}).then(([follower, followee]) => Promise.all([ - // Increment following count - User.update(follower._id, { - $inc: { - followingCount: 1 - } - }), - - FollowingLog.insert({ - createdAt: data.following.createdAt, - userId: follower._id, - count: follower.followingCount + 1 - }), - - // Increment followers count - User.update({ _id: followee._id }, { - $inc: { - followersCount: 1 - } - }), - - FollowedLog.insert({ - createdAt: data.following.createdAt, - userId: follower._id, - count: followee.followersCount + 1 - }), - - // Publish follow event - isLocalUser(follower) && packUser(followee, follower) - .then(packed => event(follower._id, 'follow', packed)), - - isLocalUser(followee) && Promise.all([ - packUser(follower, followee) - .then(packed => event(followee._id, 'followed', packed)), - - // Notify - isLocalUser(followee) && notify(followee._id, follower._id, 'follow') - ]) -]).then(() => done(), error => { - done(); - throw error; -}), done); diff --git a/src/queue/processors/http/index.ts b/src/queue/processors/http/index.ts index 0ea79305c6..3dc2595374 100644 --- a/src/queue/processors/http/index.ts +++ b/src/queue/processors/http/index.ts @@ -1,17 +1,20 @@ -import deliverPost from './deliver-post'; -import follow from './follow'; -import performActivityPub from './perform-activitypub'; +import deliver from './deliver'; import processInbox from './process-inbox'; import reportGitHubFailure from './report-github-failure'; -import unfollow from './unfollow'; const handlers = { - deliverPost, - follow, - performActivityPub, - processInbox, - reportGitHubFailure, - unfollow + deliver, + processInbox, + reportGitHubFailure }; -export default (job, done) => handlers[job.data.type](job, done); +export default (job, done) => { + const handler = handlers[job.data.type]; + + if (handler) { + handler(job, done); + } else { + console.error(`Unknown job: ${job.data.type}`); + done(); + } +}; diff --git a/src/queue/processors/http/perform-activitypub.ts b/src/queue/processors/http/perform-activitypub.ts deleted file mode 100644 index ae70c0f0be..0000000000 --- a/src/queue/processors/http/perform-activitypub.ts +++ /dev/null @@ -1,8 +0,0 @@ -import User from '../../../models/user'; -import act from '../../../remote/activitypub/act'; -import Resolver from '../../../remote/activitypub/resolver'; - -export default ({ data }, done) => User.findOne({ _id: data.actor }) - .then(actor => act(new Resolver(), actor, data.outbox)) - .then(Promise.all) - .then(() => done(), done); diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index 7eeaa19f8a..eb4b62d37f 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -1,44 +1,66 @@ +import * as kue from 'kue'; +import * as debug from 'debug'; + import { verifySignature } from 'http-signature'; import parseAcct from '../../../acct/parse'; import User, { IRemoteUser } from '../../../models/user'; import act from '../../../remote/activitypub/act'; import resolvePerson from '../../../remote/activitypub/resolve-person'; -import Resolver from '../../../remote/activitypub/resolver'; -export default async ({ data }, done) => { - try { - const keyIdLower = data.signature.keyId.toLowerCase(); - let user; +const log = debug('misskey:queue:inbox'); - if (keyIdLower.startsWith('acct:')) { - const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); - if (host === null) { - done(); - return; - } +// ユーザーのinboxにアクティビティが届いた時の処理 +export default async (job: kue.Job, done): Promise<void> => { + const signature = job.data.signature; + const activity = job.data.activity; - user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; - } else { - user = await User.findOne({ - host: { $ne: null }, - 'account.publicKey.id': data.signature.keyId - }) as IRemoteUser; + //#region Log + const info = Object.assign({}, activity); + delete info['@context']; + delete info['signature']; + log(info); + //#endregion - if (user === null) { - user = await resolvePerson(new Resolver(), data.signature.keyId); - } - } + const keyIdLower = signature.keyId.toLowerCase(); + let user; - if (user === null || !verifySignature(data.signature, user.account.publicKey.publicKeyPem)) { + if (keyIdLower.startsWith('acct:')) { + const { username, host } = parseAcct(keyIdLower.slice('acct:'.length)); + if (host === null) { + console.warn(`request was made by local user: @${username}`); done(); return; } - await Promise.all(await act(new Resolver(), user, data.inbox, true)); - } catch (error) { - done(error); + user = await User.findOne({ usernameLower: username, hostLower: host }) as IRemoteUser; + } else { + user = await User.findOne({ + host: { $ne: null }, + 'account.publicKey.id': signature.keyId + }) as IRemoteUser; + + // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する + if (user === null) { + user = await resolvePerson(signature.keyId); + } + } + + if (user === null) { + done(new Error('failed to resolve user')); return; } - done(); + if (!verifySignature(signature, user.account.publicKey.publicKeyPem)) { + console.warn('signature verification failed'); + done(); + return; + } + + // アクティビティを処理 + try { + await act(user, activity); + done(); + } catch (e) { + done(e); + } }; diff --git a/src/queue/processors/http/report-github-failure.ts b/src/queue/processors/http/report-github-failure.ts index af9659bdac..1e0b51f89f 100644 --- a/src/queue/processors/http/report-github-failure.ts +++ b/src/queue/processors/http/report-github-failure.ts @@ -1,31 +1,24 @@ import * as request from 'request-promise-native'; import User from '../../../models/user'; -const createPost = require('../../../server/api/endpoints/posts/create'); +import createPost from '../../../services/post/create'; -export default async ({ data }, done) => { - try { - const asyncBot = User.findOne({ _id: data.userId }); +export default async ({ data }) => { + const asyncBot = User.findOne({ _id: data.userId }); - // Fetch parent status - const parentStatuses = await request({ - url: `${data.parentUrl}/statuses`, - headers: { - 'User-Agent': 'misskey' - }, - json: true - }); + // Fetch parent status + const parentStatuses = await request({ + url: `${data.parentUrl}/statuses`, + headers: { + 'User-Agent': 'misskey' + }, + json: true + }); - const parentState = parentStatuses[0].state; - const stillFailed = parentState == 'failure' || parentState == 'error'; - const text = stillFailed ? - `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : - `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; + const parentState = parentStatuses[0].state; + const stillFailed = parentState == 'failure' || parentState == 'error'; + const text = stillFailed ? + `**⚠️BUILD STILL FAILED⚠️**: ?[${data.message}](${data.htmlUrl})` : + `**🚨BUILD FAILED🚨**: →→→?[${data.message}](${data.htmlUrl})←←←`; - createPost({ text }, await asyncBot); - } catch (error) { - done(error); - return; - } - - done(); + createPost(await asyncBot, { text }); }; diff --git a/src/queue/processors/http/unfollow.ts b/src/queue/processors/http/unfollow.ts deleted file mode 100644 index d62eb280dc..0000000000 --- a/src/queue/processors/http/unfollow.ts +++ /dev/null @@ -1,71 +0,0 @@ -import FollowedLog from '../../../models/followed-log'; -import Following from '../../../models/following'; -import FollowingLog from '../../../models/following-log'; -import User, { isLocalUser, isRemoteUser, pack as packUser } from '../../../models/user'; -import stream from '../../../publishers/stream'; -import renderFollow from '../../../remote/activitypub/renderer/follow'; -import renderUndo from '../../../remote/activitypub/renderer/undo'; -import context from '../../../remote/activitypub/renderer/context'; -import request from '../../../remote/request'; - -export default async ({ data }, done) => { - const following = await Following.findOne({ _id: data.id }); - if (following === null) { - done(); - return; - } - - let follower; - let followee; - - try { - [follower, followee] = await Promise.all([ - User.findOne({ _id: following.followerId }), - User.findOne({ _id: following.followeeId }) - ]); - - if (isLocalUser(follower) && isRemoteUser(followee)) { - const undo = renderUndo(renderFollow(follower, followee)); - undo['@context'] = context; - - await request(follower, followee.account.inbox, undo); - } - } catch (error) { - done(error); - return; - } - - try { - await Promise.all([ - // Delete following - Following.findOneAndDelete({ _id: data.id }), - - // Decrement following count - User.update({ _id: follower._id }, { $inc: { followingCount: -1 } }), - FollowingLog.insert({ - createdAt: new Date(), - userId: follower._id, - count: follower.followingCount - 1 - }), - - // Decrement followers count - User.update({ _id: followee._id }, { $inc: { followersCount: -1 } }), - FollowedLog.insert({ - createdAt: new Date(), - userId: followee._id, - count: followee.followersCount - 1 - }) - ]); - - if (isLocalUser(follower)) { - return; - } - - const promisedPackedUser = packUser(followee, follower); - - // Publish follow event - stream(follower._id, 'unfollow', promisedPackedUser); - } finally { - done(); - } -}; diff --git a/src/remote/activitypub/act/create.ts b/src/remote/activitypub/act/create.ts deleted file mode 100644 index fa681982cf..0000000000 --- a/src/remote/activitypub/act/create.ts +++ /dev/null @@ -1,10 +0,0 @@ -import create from '../create'; -import Resolver from '../resolver'; - -export default (resolver: Resolver, actor, activity, distribute) => { - if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error(); - } - - return create(resolver, actor, activity.object, distribute); -}; diff --git a/src/remote/activitypub/act/create/image.ts b/src/remote/activitypub/act/create/image.ts new file mode 100644 index 0000000000..30a75e7377 --- /dev/null +++ b/src/remote/activitypub/act/create/image.ts @@ -0,0 +1,18 @@ +import * as debug from 'debug'; + +import uploadFromUrl from '../../../../services/drive/upload-from-url'; +import { IRemoteUser } from '../../../../models/user'; +import { IDriveFile } from '../../../../models/drive-file'; + +const log = debug('misskey:activitypub'); + +export default async function(actor: IRemoteUser, image): Promise<IDriveFile> { + if ('attributedTo' in image && actor.account.uri !== image.attributedTo) { + log(`invalid image: ${JSON.stringify(image, null, 2)}`); + throw new Error('invalid image'); + } + + log(`Creating the Image: ${image.id}`); + + return await uploadFromUrl(image.url, actor); +} diff --git a/src/remote/activitypub/act/create/index.ts b/src/remote/activitypub/act/create/index.ts new file mode 100644 index 0000000000..dd0b112141 --- /dev/null +++ b/src/remote/activitypub/act/create/index.ts @@ -0,0 +1,44 @@ +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import { IRemoteUser } from '../../../../models/user'; +import createNote from './note'; +import createImage from './image'; +import { ICreate } from '../../type'; + +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => { + if ('actor' in activity && actor.account.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const uri = activity.id || activity; + + log(`Create: ${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 'Image': + createImage(actor, object); + break; + + case 'Note': + createNote(resolver, actor, object); + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/act/create/note.ts b/src/remote/activitypub/act/create/note.ts new file mode 100644 index 0000000000..82a6207038 --- /dev/null +++ b/src/remote/activitypub/act/create/note.ts @@ -0,0 +1,89 @@ +import { JSDOM } from 'jsdom'; +import * as debug from 'debug'; + +import Resolver from '../../resolver'; +import Post, { IPost } from '../../../../models/post'; +import createPost from '../../../../services/post/create'; +import { IRemoteUser } from '../../../../models/user'; +import resolvePerson from '../../resolve-person'; +import createImage from './image'; +import config from '../../../../config'; + +const log = debug('misskey:activitypub'); + +/** + * 投稿作成アクティビティを捌きます + */ +export default async function createNote(resolver: Resolver, actor: IRemoteUser, note, silent = false): Promise<IPost> { + if (typeof note.id !== 'string') { + log(`invalid note: ${JSON.stringify(note, null, 2)}`); + throw new Error('invalid note'); + } + + // 既に同じURIを持つものが登録されていないかチェックし、登録されていたらそれを返す + const exist = await Post.findOne({ uri: note.id }); + if (exist) { + return exist; + } + + log(`Creating the Note: ${note.id}`); + + //#region Visibility + let visibility = 'public'; + if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted'; + if (note.cc.length == 0) visibility = 'private'; + // TODO + if (visibility != 'public') throw new Error('unspported visibility'); + //#endergion + + //#region 添付メディア + const media = []; + if ('attachment' in note && note.attachment != null) { + // TODO: attachmentは必ずしもImageではない + // TODO: attachmentは必ずしも配列ではない + // TODO: ループの中でawaitはすべきでない + note.attachment.forEach(async media => { + const created = await createImage(note.actor, media); + media.push(created); + }); + } + //#endregion + + //#region リプライ + let reply = null; + if ('inReplyTo' in note && note.inReplyTo != null) { + // リプライ先の投稿がMisskeyに登録されているか調べる + const uri: string = note.inReplyTo.id || note.inReplyTo; + const inReplyToPost = uri.startsWith(config.url + '/') + ? await Post.findOne({ _id: uri.split('/').pop() }) + : await Post.findOne({ uri }); + + if (inReplyToPost) { + reply = inReplyToPost; + } else { + // 無かったらフェッチ + const inReplyTo = await resolver.resolve(note.inReplyTo) as any; + + // リプライ先の投稿の投稿者をフェッチ + const actor = await resolvePerson(inReplyTo.attributedTo) as IRemoteUser; + + // TODO: silentを常にtrueにしてはならない + reply = await createNote(resolver, actor, inReplyTo); + } + } + //#endregion + + const { window } = new JSDOM(note.content); + + return await createPost(actor, { + createdAt: new Date(note.published), + media, + reply, + repost: undefined, + text: window.document.body.textContent, + viaMobile: false, + geo: undefined, + visibility, + uri: note.id + }); +} diff --git a/src/remote/activitypub/act/delete.ts b/src/remote/activitypub/act/delete.ts deleted file mode 100644 index f9eb4dd08d..0000000000 --- a/src/remote/activitypub/act/delete.ts +++ /dev/null @@ -1,21 +0,0 @@ -import create from '../create'; -import deleteObject from '../delete'; - -export default async (resolver, actor, activity) => { - if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error(); - } - - const results = await create(resolver, actor, activity.object); - - await Promise.all(results.map(async promisedResult => { - const result = await promisedResult; - if (result === null) { - return; - } - - await deleteObject(result); - })); - - return null; -}; diff --git a/src/remote/activitypub/act/delete/index.ts b/src/remote/activitypub/act/delete/index.ts new file mode 100644 index 0000000000..e34577b310 --- /dev/null +++ b/src/remote/activitypub/act/delete/index.ts @@ -0,0 +1,36 @@ +import Resolver from '../../resolver'; +import deleteNote from './note'; +import Post from '../../../../models/post'; +import { IRemoteUser } from '../../../../models/user'; + +/** + * 削除アクティビティを捌きます + */ +export default async (actor: IRemoteUser, activity): Promise<void> => { + if ('actor' in activity && actor.account.uri !== activity.actor) { + throw new Error('invalid actor'); + } + + const resolver = new Resolver(); + + const object = await resolver.resolve(activity.object); + + const uri = (object as any).id; + + switch (object.type) { + case 'Note': + deleteNote(actor, uri); + break; + + case 'Tombstone': + const post = await Post.findOne({ uri }); + if (post != null) { + deleteNote(actor, uri); + } + break; + + default: + console.warn(`Unknown type: ${object.type}`); + break; + } +}; diff --git a/src/remote/activitypub/act/delete/note.ts b/src/remote/activitypub/act/delete/note.ts new file mode 100644 index 0000000000..8e9447b481 --- /dev/null +++ b/src/remote/activitypub/act/delete/note.ts @@ -0,0 +1,30 @@ +import * as debug from 'debug'; + +import Post from '../../../../models/post'; +import { IRemoteUser } from '../../../../models/user'; + +const log = debug('misskey:activitypub'); + +export default async function(actor: IRemoteUser, uri: string): Promise<void> { + log(`Deleting the Note: ${uri}`); + + const post = await Post.findOne({ uri }); + + if (post == null) { + throw new Error('post not found'); + } + + if (!post.userId.equals(actor._id)) { + throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません'); + } + + Post.update({ _id: post._id }, { + $set: { + deletedAt: new Date(), + text: null, + textHtml: null, + mediaIds: [], + poll: null + } + }); +} diff --git a/src/remote/activitypub/act/follow.ts b/src/remote/activitypub/act/follow.ts index 222a257e1a..3dd029af54 100644 --- a/src/remote/activitypub/act/follow.ts +++ b/src/remote/activitypub/act/follow.ts @@ -1,17 +1,12 @@ -import { MongoError } from 'mongodb'; import parseAcct from '../../../acct/parse'; -import Following, { IFollowing } from '../../../models/following'; -import User from '../../../models/user'; +import User, { IRemoteUser } from '../../../models/user'; import config from '../../../config'; -import { createHttp } from '../../../queue'; -import context from '../renderer/context'; -import renderAccept from '../renderer/accept'; -import request from '../../request'; -import Resolver from '../resolver'; +import follow from '../../../services/following/create'; +import { IFollow } from '../type'; -export default async (resolver: Resolver, actor, activity, distribute) => { +export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { const prefix = config.url + '/@'; - const id = activity.object.id || activity.object; + const id = typeof activity == 'string' ? activity : activity.id; if (!id.startsWith(prefix)) { return null; @@ -27,52 +22,5 @@ export default async (resolver: Resolver, actor, activity, distribute) => { throw new Error(); } - if (!distribute) { - const { _id } = await Following.findOne({ - followerId: actor._id, - followeeId: followee._id - }); - - return { - resolver, - object: { $ref: 'following', $id: _id } - }; - } - - const promisedFollowing = Following.insert({ - createdAt: new Date(), - followerId: actor._id, - followeeId: followee._id - }).then(following => new Promise((resolve, reject) => { - createHttp({ - type: 'follow', - following: following._id - }).save(error => { - if (error) { - reject(error); - } else { - resolve(following); - } - }); - }) as Promise<IFollowing>, async error => { - // duplicate key error - if (error instanceof MongoError && error.code === 11000) { - return Following.findOne({ - followerId: actor._id, - followeeId: followee._id - }); - } - - throw error; - }); - - const accept = renderAccept(activity); - accept['@context'] = context; - - await request(followee, actor.account.inbox, accept); - - return promisedFollowing.then(({ _id }) => ({ - resolver, - object: { $ref: 'following', $id: _id } - })); + await follow(actor, followee, activity); }; diff --git a/src/remote/activitypub/act/index.ts b/src/remote/activitypub/act/index.ts index d282e12885..45d7bd16a9 100644 --- a/src/remote/activitypub/act/index.ts +++ b/src/remote/activitypub/act/index.ts @@ -1,36 +1,46 @@ +import { Object } from '../type'; +import { IRemoteUser } from '../../../models/user'; import create from './create'; import performDeleteActivity from './delete'; import follow from './follow'; import undo from './undo'; -import createObject from '../create'; -import Resolver from '../resolver'; +import like from './like'; -export default async (parentResolver: Resolver, actor, value, distribute?: boolean) => { - const collection = await parentResolver.resolveCollection(value); +const self = async (actor: IRemoteUser, activity: Object): Promise<void> => { + switch (activity.type) { + case 'Create': + await create(actor, activity); + break; - return collection.object.map(async element => { - const { resolver, object } = await collection.resolver.resolveOne(element); - const created = await (await createObject(resolver, actor, [object], distribute))[0]; + case 'Delete': + await performDeleteActivity(actor, activity); + break; - if (created !== null) { - return created; - } + case 'Follow': + await follow(actor, activity); + break; - switch (object.type) { - case 'Create': - return create(resolver, actor, object, distribute); + case 'Accept': + // noop + break; - case 'Delete': - return performDeleteActivity(resolver, actor, object); + case 'Like': + await like(actor, activity); + break; - case 'Follow': - return follow(resolver, actor, object, distribute); + case 'Undo': + await undo(actor, activity); + break; - case 'Undo': - return undo(resolver, actor, object); + case 'Collection': + case 'OrderedCollection': + // TODO + break; - default: - return null; - } - }); + default: + console.warn(`unknown activity type: ${(activity as any).type}`); + return null; + } }; + +export default self; diff --git a/src/remote/activitypub/act/like.ts b/src/remote/activitypub/act/like.ts index ea53242017..2f5e3f807d 100644 --- a/src/remote/activitypub/act/like.ts +++ b/src/remote/activitypub/act/like.ts @@ -1,10 +1,10 @@ -import { MongoError } from 'mongodb'; -import Reaction, { IPostReaction } from '../../../models/post-reaction'; import Post from '../../../models/post'; -import queue from '../../../queue'; +import { IRemoteUser } from '../../../models/user'; +import { ILike } from '../type'; +import create from '../../../services/post/reaction/create'; -export default async (resolver, actor, activity, distribute) => { - const id = activity.object.id || activity.object; +export default async (actor: IRemoteUser, activity: ILike) => { + const id = typeof activity.object == 'string' ? activity.object : activity.object.id; // Transform: // https://misskey.ex/@syuilo/xxxx to @@ -16,48 +16,5 @@ export default async (resolver, actor, activity, distribute) => { throw new Error(); } - if (!distribute) { - const { _id } = await Reaction.findOne({ - userId: actor._id, - postId: post._id - }); - - return { - resolver, - object: { $ref: 'postPeactions', $id: _id } - }; - } - - const promisedReaction = Reaction.insert({ - createdAt: new Date(), - userId: actor._id, - postId: post._id, - reaction: 'pudding' - }).then(reaction => new Promise<IPostReaction>((resolve, reject) => { - queue.create('http', { - type: 'reaction', - reactionId: reaction._id - }).save(error => { - if (error) { - reject(error); - } else { - resolve(reaction); - } - }); - }), async error => { - // duplicate key error - if (error instanceof MongoError && error.code === 11000) { - return Reaction.findOne({ - userId: actor._id, - postId: post._id - }); - } - - throw error; - }); - - return promisedReaction.then(({ _id }) => ({ - resolver, - object: { $ref: 'postPeactions', $id: _id } - })); + await create(actor, post, 'pudding'); }; diff --git a/src/remote/activitypub/act/undo/follow.ts b/src/remote/activitypub/act/undo/follow.ts new file mode 100644 index 0000000000..fcf27c9507 --- /dev/null +++ b/src/remote/activitypub/act/undo/follow.ts @@ -0,0 +1,26 @@ +import parseAcct from '../../../../acct/parse'; +import User, { IRemoteUser } from '../../../../models/user'; +import config from '../../../../config'; +import unfollow from '../../../../services/following/delete'; +import { IFollow } from '../../type'; + +export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => { + const prefix = config.url + '/@'; + const id = typeof activity == 'string' ? activity : activity.id; + + if (!id.startsWith(prefix)) { + return null; + } + + const { username, host } = parseAcct(id.slice(prefix.length)); + if (host !== null) { + throw new Error(); + } + + const followee = await User.findOne({ username, host }); + if (followee === null) { + throw new Error(); + } + + await unfollow(actor, followee, activity); +}; diff --git a/src/remote/activitypub/act/undo/index.ts b/src/remote/activitypub/act/undo/index.ts index aa60d3a4fa..3ede9fcfb8 100644 --- a/src/remote/activitypub/act/undo/index.ts +++ b/src/remote/activitypub/act/undo/index.ts @@ -1,27 +1,37 @@ -import act from '../../act'; -import deleteObject from '../../delete'; -import unfollow from './unfollow'; +import * as debug from 'debug'; + +import { IRemoteUser } from '../../../../models/user'; +import { IUndo } from '../../type'; +import unfollow from './follow'; import Resolver from '../../resolver'; -export default async (resolver: Resolver, actor, activity): Promise<void> => { +const log = debug('misskey:activitypub'); + +export default async (actor: IRemoteUser, activity: IUndo): Promise<void> => { if ('actor' in activity && actor.account.uri !== activity.actor) { - throw new Error(); + throw new Error('invalid actor'); } - const results = await act(resolver, actor, activity.object); + const uri = activity.id || activity; - await Promise.all(results.map(async promisedResult => { - const result = await promisedResult; + log(`Undo: ${uri}`); - if (result === null || await deleteObject(result) !== null) { - return; - } + const resolver = new Resolver(); - switch (result.object.$ref) { - case 'following': - await unfollow(result.object); - } - })); + let object; + + try { + object = await resolver.resolve(activity.object); + } catch (e) { + log(`Resolution failed: ${e}`); + throw e; + } + + switch (object.type) { + case 'Follow': + unfollow(actor, object); + break; + } return null; }; diff --git a/src/remote/activitypub/act/undo/unfollow.ts b/src/remote/activitypub/act/undo/unfollow.ts deleted file mode 100644 index 4f15d9a3e4..0000000000 --- a/src/remote/activitypub/act/undo/unfollow.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createHttp } from '../../../../queue'; - -export default ({ $id }) => new Promise((resolve, reject) => { - createHttp({ type: 'unfollow', id: $id }).save(error => { - if (error) { - reject(error); - } else { - resolve(); - } - }); -}); diff --git a/src/remote/activitypub/create.ts b/src/remote/activitypub/create.ts deleted file mode 100644 index 3bc0c66f35..0000000000 --- a/src/remote/activitypub/create.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { JSDOM } from 'jsdom'; -import { ObjectID } from 'mongodb'; -import config from '../../config'; -import DriveFile from '../../models/drive-file'; -import Post from '../../models/post'; -import { IRemoteUser } from '../../models/user'; -import uploadFromUrl from '../../drive/upload-from-url'; -import createPost from '../../post/create'; -import distributePost from '../../post/distribute'; -import resolvePerson from './resolve-person'; -import Resolver from './resolver'; -const createDOMPurify = require('dompurify'); - -type IResult = { - resolver: Resolver; - object: { - $ref: string; - $id: ObjectID; - }; -}; - -class Creator { - private actor: IRemoteUser; - private distribute: boolean; - - constructor(actor, distribute) { - this.actor = actor; - this.distribute = distribute; - } - - private async createImage(resolver: Resolver, image) { - if ('attributedTo' in image && this.actor.account.uri !== image.attributedTo) { - throw new Error(); - } - - const { _id } = await uploadFromUrl(image.url, this.actor, image.id || null); - return { - resolver, - object: { $ref: 'driveFiles.files', $id: _id } - }; - } - - private async createNote(resolver: Resolver, note) { - if ( - ('attributedTo' in note && this.actor.account.uri !== note.attributedTo) || - typeof note.id !== 'string' - ) { - throw new Error(); - } - - const { window } = new JSDOM(note.content); - const mentions = []; - const tags = []; - - for (const { href, name, type } of note.tags) { - switch (type) { - case 'Hashtag': - if (name.startsWith('#')) { - tags.push(name.slice(1)); - } - break; - - case 'Mention': - mentions.push(resolvePerson(resolver, href)); - break; - } - } - - const [mediaIds, reply] = await Promise.all([ - 'attachment' in note && this.create(resolver, note.attachment) - .then(collection => Promise.all(collection)) - .then(collection => collection - .filter(media => media !== null && media.object.$ref === 'driveFiles.files') - .map(({ object }: IResult) => object.$id)), - - 'inReplyTo' in note && this.create(resolver, note.inReplyTo) - .then(collection => Promise.all(collection.map(promise => promise.then(result => { - if (result !== null && result.object.$ref === 'posts') { - throw result.object; - } - }, () => { })))) - .then(() => null, ({ $id }) => Post.findOne({ _id: $id })) - ]); - - const inserted = await createPost({ - channelId: undefined, - index: undefined, - createdAt: new Date(note.published), - mediaIds, - poll: undefined, - text: window.document.body.textContent, - textHtml: note.content && createDOMPurify(window).sanitize(note.content), - userId: this.actor._id, - appId: null, - viaMobile: false, - geo: undefined, - uri: note.id, - tags - }, reply, null, await Promise.all(mentions)); - - const promises = []; - - if (this.distribute) { - promises.push(distributePost(this.actor, inserted.mentions, inserted)); - } - - // Register to search database - if (note.content && config.elasticsearch.enable) { - const es = require('../../db/elasticsearch'); - - promises.push(new Promise((resolve, reject) => { - es.index({ - index: 'misskey', - type: 'post', - id: inserted._id.toString(), - body: { - text: window.document.body.textContent - } - }, resolve); - })); - } - - await Promise.all(promises); - - return { - resolver, - object: { $ref: 'posts', id: inserted._id } - }; - } - - public async create(parentResolver: Resolver, value): Promise<Array<Promise<IResult>>> { - const collection = await parentResolver.resolveCollection(value); - - return collection.object.map(async element => { - const uri = element.id || element; - - try { - await Promise.all([ - DriveFile.findOne({ 'metadata.uri': uri }).then(file => { - if (file === null) { - return; - } - - throw { - $ref: 'driveFile.files', - $id: file._id - }; - }, () => {}), - Post.findOne({ uri }).then(post => { - if (post === null) { - return; - } - - throw { - $ref: 'posts', - $id: post._id - }; - }, () => {}) - ]); - } catch (object) { - return { - resolver: collection.resolver, - object - }; - } - - const { resolver, object } = await collection.resolver.resolveOne(element); - - switch (object.type) { - case 'Image': - return this.createImage(resolver, object); - - case 'Note': - return this.createNote(resolver, object); - } - - return null; - }); - } -} - -export default (resolver: Resolver, actor, value, distribute?: boolean) => { - const creator = new Creator(actor, distribute); - return creator.create(resolver, value); -}; diff --git a/src/remote/activitypub/delete/index.ts b/src/remote/activitypub/delete/index.ts deleted file mode 100644 index bc9104284b..0000000000 --- a/src/remote/activitypub/delete/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import deletePost from './post'; - -export default async ({ object }) => { - switch (object.$ref) { - case 'posts': - return deletePost(object); - } - - return null; -}; diff --git a/src/remote/activitypub/delete/post.ts b/src/remote/activitypub/delete/post.ts deleted file mode 100644 index 59ae8c2b94..0000000000 --- a/src/remote/activitypub/delete/post.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Post from '../../../models/post'; -import { createDb } from '../../../queue'; - -export default async ({ $id }) => { - const promisedDeletion = Post.findOneAndDelete({ _id: $id }); - - await new Promise((resolve, reject) => createDb({ - type: 'deletePostDependents', - id: $id - }).delay(65536).save(error => error ? reject(error) : resolve())); - - return promisedDeletion; -}; diff --git a/src/remote/activitypub/renderer/like.ts b/src/remote/activitypub/renderer/like.ts new file mode 100644 index 0000000000..903b10789e --- /dev/null +++ b/src/remote/activitypub/renderer/like.ts @@ -0,0 +1,9 @@ +import config from '../../../config'; + +export default (user, post) => { + return { + type: 'Like', + actor: `${config.url}/@${user.username}`, + object: post.uri ? post.uri : `${config.url}/posts/${post._id}` + }; +}; diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 43531b121a..bbab63db36 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -2,11 +2,14 @@ import renderDocument from './document'; import renderHashtag from './hashtag'; import config from '../../../config'; import DriveFile from '../../../models/drive-file'; -import Post from '../../../models/post'; -import User from '../../../models/user'; +import Post, { IPost } from '../../../models/post'; +import User, { IUser } from '../../../models/user'; + +export default async (user: IUser, post: IPost) => { + const promisedFiles = post.mediaIds + ? DriveFile.find({ _id: { $in: post.mediaIds } }) + : Promise.resolve([]); -export default async (user, post) => { - const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } }); let inReplyTo; if (post.replyId) { @@ -16,11 +19,11 @@ export default async (user, post) => { if (inReplyToPost !== null) { const inReplyToUser = await User.findOne({ - _id: post.userId, + _id: inReplyToPost.userId, }); if (inReplyToUser !== null) { - inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`; + inReplyTo = inReplyToPost.uri || `${config.url}/posts/${inReplyToPost._id}`; } } } else { @@ -30,7 +33,7 @@ export default async (user, post) => { const attributedTo = `${config.url}/@${user.username}`; return { - id: `${attributedTo}/${post._id}`, + id: `${config.url}/posts/${post._id}`, type: 'Note', attributedTo, content: post.textHtml, @@ -39,6 +42,6 @@ export default async (user, post) => { cc: `${attributedTo}/followers`, inReplyTo, attachment: (await promisedFiles).map(renderDocument), - tag: post.tags.map(renderHashtag) + tag: (post.tags || []).map(renderHashtag) }; }; diff --git a/src/remote/activitypub/resolve-person.ts b/src/remote/activitypub/resolve-person.ts index a7c0020dd8..b3bac3cd3f 100644 --- a/src/remote/activitypub/resolve-person.ts +++ b/src/remote/activitypub/resolve-person.ts @@ -1,42 +1,50 @@ import { JSDOM } from 'jsdom'; import { toUnicode } from 'punycode'; +import parseAcct from '../../acct/parse'; +import config from '../../config'; import User, { validateUsername, isValidName, isValidDescription } from '../../models/user'; -import { createHttp } from '../../queue'; import webFinger from '../webfinger'; -import create from './create'; +import Resolver from './resolver'; +import uploadFromUrl from '../../services/drive/upload-from-url'; +import { isCollectionOrOrderedCollection } from './type'; -async function isCollection(collection) { - return ['Collection', 'OrderedCollection'].includes(collection.type); -} +export default async (value, verifier?: string) => { + const id = value.id || value; + const localPrefix = config.url + '/@'; -export default async (parentResolver, value, verifier?: string) => { - const { resolver, object } = await parentResolver.resolveOne(value); + if (id.startsWith(localPrefix)) { + return User.findOne(parseAcct(id.slice(localPrefix))); + } + + const resolver = new Resolver(); + + const object = await resolver.resolve(value) as any; if ( - object === null || + object == null || object.type !== 'Person' || typeof object.preferredUsername !== 'string' || !validateUsername(object.preferredUsername) || - !isValidName(object.name) || + !isValidName(object.name == '' ? null : object.name) || !isValidDescription(object.summary) ) { - throw new Error(); + throw new Error('invalid person'); } - const [followers, following, outbox, finger] = await Promise.all([ - resolver.resolveOne(object.followers).then( - resolved => isCollection(resolved.object) ? resolved.object : null, - () => null + const [followersCount = 0, followingCount = 0, postsCount = 0, finger] = await Promise.all([ + resolver.resolve(object.followers).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined ), - resolver.resolveOne(object.following).then( - resolved => isCollection(resolved.object) ? resolved.object : null, - () => null + resolver.resolve(object.following).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined ), - resolver.resolveOne(object.outbox).then( - resolved => isCollection(resolved.object) ? resolved.object : null, - () => null + resolver.resolve(object.outbox).then( + resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, + () => undefined ), - webFinger(object.id, verifier), + webFinger(id, verifier) ]); const host = toUnicode(finger.subject.replace(/^.*?@/, '')); @@ -47,12 +55,12 @@ export default async (parentResolver, value, verifier?: string) => { const user = await User.insert({ avatarId: null, bannerId: null, - createdAt: Date.parse(object.published), + createdAt: Date.parse(object.published) || null, description: summaryDOM.textContent, - followersCount: followers ? followers.totalItem || 0 : 0, - followingCount: following ? following.totalItem || 0 : 0, + followersCount, + followingCount, + postsCount, name: object.name, - postsCount: outbox ? outbox.totalItem || 0 : 0, driveCapacity: 1024 * 1024 * 8, // 8MiB username: object.preferredUsername, usernameLower: object.preferredUsername.toLowerCase(), @@ -64,38 +72,18 @@ export default async (parentResolver, value, verifier?: string) => { publicKeyPem: object.publicKey.publicKeyPem }, inbox: object.inbox, - uri: object.id, + uri: id, }, }); - createHttp({ - type: 'performActivityPub', - actor: user._id, - outbox - }).save(); - - const [avatarId, bannerId] = await Promise.all([ + const [avatarId, bannerId] = (await Promise.all([ object.icon, object.image - ].map(async value => { - if (value === undefined) { - return null; - } - - try { - const created = await create(resolver, user, value); - - await Promise.all(created.map(asyncCreated => asyncCreated.then(created => { - if (created !== null && created.object.$ref === 'driveFiles.files') { - throw created.object.$id; - } - }, () => {}))); - - return null; - } catch (id) { - return id; - } - })); + ].map(img => + img == null + ? Promise.resolve(null) + : uploadFromUrl(img.url, user) + ))).map(file => file != null ? file._id : null); User.update({ _id: user._id }, { $set: { avatarId, bannerId } }); diff --git a/src/remote/activitypub/resolver.ts b/src/remote/activitypub/resolver.ts index 371ccdcc30..4a97e2ef66 100644 --- a/src/remote/activitypub/resolver.ts +++ b/src/remote/activitypub/resolver.ts @@ -1,20 +1,51 @@ -const request = require('request-promise-native'); +import * as request from 'request-promise-native'; +import * as debug from 'debug'; +import { IObject } from './type'; + +const log = debug('misskey:activitypub:resolver'); export default class Resolver { - private requesting: Set<string>; + private history: Set<string>; - constructor(iterable?: Iterable<string>) { - this.requesting = new Set(iterable); + constructor() { + this.history = new Set(); } - private async resolveUnrequestedOne(value) { - if (typeof value !== 'string') { - return { resolver: this, object: value }; + public async resolveCollection(value) { + const collection = typeof value === 'string' + ? await this.resolve(value) + : value; + + switch (collection.type) { + case 'Collection': + collection.objects = collection.object.items; + break; + + case 'OrderedCollection': + collection.objects = collection.object.orderedItems; + break; + + default: + throw new Error(`unknown collection type: ${collection.type}`); } - const resolver = new Resolver(this.requesting); + return collection; + } - resolver.requesting.add(value); + public async resolve(value): Promise<IObject> { + if (value == null) { + throw new Error('resolvee is null (or undefined)'); + } + + if (typeof value !== 'string') { + return value; + } + + if (this.history.has(value)) { + throw new Error('cannot resolve already resolved one'); + } + + this.history.add(value); const object = await request({ url: value, @@ -29,41 +60,11 @@ export default class Resolver { !object['@context'].includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' )) { - throw new Error(); + throw new Error('invalid response'); } - return { resolver, object }; - } + log(`resolved: ${JSON.stringify(object, null, 2)}`); - public async resolveCollection(value) { - const resolved = typeof value === 'string' ? - await this.resolveUnrequestedOne(value) : - { resolver: this, object: value }; - - switch (resolved.object.type) { - case 'Collection': - resolved.object = resolved.object.items; - break; - - case 'OrderedCollection': - resolved.object = resolved.object.orderedItems; - break; - - default: - if (!Array.isArray(value)) { - resolved.object = [resolved.object]; - } - break; - } - - return resolved; - } - - public resolveOne(value) { - if (this.requesting.has(value)) { - throw new Error(); - } - - return this.resolveUnrequestedOne(value); + return object; } } diff --git a/src/remote/activitypub/type.ts b/src/remote/activitypub/type.ts index 94e2c350a2..450d5906d8 100644 --- a/src/remote/activitypub/type.ts +++ b/src/remote/activitypub/type.ts @@ -1,3 +1,70 @@ -export type IObject = { +export type obj = { [x: string]: any }; + +export interface IObject { + '@context': string | obj | obj[]; type: string; -}; + id?: string; + summary?: string; +} + +export interface IActivity extends IObject { + //type: 'Activity'; + actor: IObject | string; + object: IObject | string; + target?: IObject | string; +} + +export interface ICollection extends IObject { + type: 'Collection'; + totalItems: number; + items: IObject | string | IObject[] | string[]; +} + +export interface IOrderedCollection extends IObject { + type: 'OrderedCollection'; + totalItems: number; + orderedItems: IObject | string | IObject[] | string[]; +} + +export const isCollection = (object: IObject): object is ICollection => + object.type === 'Collection'; + +export const isOrderedCollection = (object: IObject): object is IOrderedCollection => + object.type === 'OrderedCollection'; + +export const isCollectionOrOrderedCollection = (object: IObject): object is ICollection | IOrderedCollection => + isCollection(object) || isOrderedCollection(object); + +export interface ICreate extends IActivity { + type: 'Create'; +} + +export interface IDelete extends IActivity { + type: 'Delete'; +} + +export interface IUndo extends IActivity { + type: 'Undo'; +} + +export interface IFollow extends IActivity { + type: 'Follow'; +} + +export interface IAccept extends IActivity { + type: 'Accept'; +} + +export interface ILike extends IActivity { + type: 'Like'; +} + +export type Object = + ICollection | + IOrderedCollection | + ICreate | + IDelete | + IUndo | + IFollow | + IAccept | + ILike; diff --git a/src/remote/request.ts b/src/remote/request.ts index 72262cbf61..a375aebfbb 100644 --- a/src/remote/request.ts +++ b/src/remote/request.ts @@ -1,9 +1,15 @@ import { request } from 'https'; import { sign } from 'http-signature'; import { URL } from 'url'; +import * as debug from 'debug'; + import config from '../config'; +const log = debug('misskey:activitypub:deliver'); + export default ({ account, username }, url, object) => new Promise((resolve, reject) => { + log(`--> ${url}`); + const { protocol, hostname, port, pathname, search } = new URL(url); const req = request({ @@ -14,6 +20,8 @@ export default ({ account, username }, url, object) => new Promise((resolve, rej path: pathname + search, }, res => { res.on('end', () => { + log(`${url} --> ${res.statusCode}`); + if (res.statusCode >= 200 && res.statusCode < 300) { resolve(); } else { diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 097ed66738..9e1ae51952 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -1,7 +1,6 @@ import { toUnicode, toASCII } from 'punycode'; import User from '../models/user'; import resolvePerson from './activitypub/resolve-person'; -import Resolver from './activitypub/resolver'; import webFinger from './webfinger'; export default async (username, host, option) => { @@ -17,10 +16,10 @@ export default async (username, host, option) => { const finger = await webFinger(acctLower, acctLower); const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); if (!self) { - throw new Error(); + throw new Error('self link not found'); } - user = await resolvePerson(new Resolver(), self.href, acctLower); + user = await resolvePerson(self.href, acctLower); } return user; diff --git a/src/renderers/get-notification-summary.ts b/src/renderers/get-notification-summary.ts index 03db722c84..f5e38faf99 100644 --- a/src/renderers/get-notification-summary.ts +++ b/src/renderers/get-notification-summary.ts @@ -1,3 +1,4 @@ +import getUserName from '../renderers/get-user-name'; import getPostSummary from './get-post-summary'; import getReactionEmoji from './get-reaction-emoji'; @@ -8,19 +9,19 @@ import getReactionEmoji from './get-reaction-emoji'; export default function(notification: any): string { switch (notification.type) { case 'follow': - return `${notification.user.name}にフォローされました`; + return `${getUserName(notification.user)}にフォローされました`; case 'mention': - return `言及されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; + return `言及されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`; case 'reply': - return `返信されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; + return `返信されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`; case 'repost': - return `Repostされました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; + return `Repostされました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`; case 'quote': - return `引用されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; + return `引用されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`; case 'reaction': - return `リアクションされました:\n${notification.user.name} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`; + return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getPostSummary(notification.post)}」`; case 'poll_vote': - return `投票されました:\n${notification.user.name}「${getPostSummary(notification.post)}」`; + return `投票されました:\n${getUserName(notification.user)}「${getPostSummary(notification.post)}」`; default: return `<不明な通知タイプ: ${notification.type}>`; } diff --git a/src/renderers/get-user-name.ts b/src/renderers/get-user-name.ts new file mode 100644 index 0000000000..acd5e6626d --- /dev/null +++ b/src/renderers/get-user-name.ts @@ -0,0 +1,5 @@ +import { IUser } from '../models/user'; + +export default function(user: IUser): string { + return user.name || '名無し'; +} diff --git a/src/renderers/get-user-summary.ts b/src/renderers/get-user-summary.ts index 2089871130..d002933b6d 100644 --- a/src/renderers/get-user-summary.ts +++ b/src/renderers/get-user-summary.ts @@ -1,12 +1,13 @@ import { IUser, isLocalUser } from '../models/user'; import getAcct from '../acct/render'; +import getUserName from './get-user-name'; /** * ユーザーを表す文字列を取得します。 * @param user ユーザー */ export default function(user: IUser): string { - let string = `${user.name} (@${getAcct(user)})\n` + + let string = `${getUserName(user)} (@${getAcct(user)})\n` + `${user.postsCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`; if (isLocalUser(user)) { diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts index 0907823b23..1b6cc0c00a 100644 --- a/src/server/activitypub/inbox.ts +++ b/src/server/activitypub/inbox.ts @@ -3,9 +3,7 @@ import * as express from 'express'; import { parseRequest } from 'http-signature'; import { createHttp } from '../../queue'; -const app = express(); - -app.disable('x-powered-by'); +const app = express.Router(); app.post('/@:user/inbox', bodyParser.json({ type() { @@ -24,7 +22,7 @@ app.post('/@:user/inbox', bodyParser.json({ createHttp({ type: 'processInbox', - inbox: req.body, + activity: req.body, signature, }).save(); diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index 9ecb0c0711..976908d1f3 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -6,8 +6,7 @@ import config from '../../config'; import Post from '../../models/post'; import withUser from './with-user'; -const app = express(); -app.disable('x-powered-by'); +const app = express.Router(); app.get('/@:user/outbox', withUser(username => { return `${config.url}/@${username}/inbox`; diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts index 91d91aeb95..355c603563 100644 --- a/src/server/activitypub/post.ts +++ b/src/server/activitypub/post.ts @@ -5,8 +5,7 @@ import parseAcct from '../../acct/parse'; import Post from '../../models/post'; import User from '../../models/user'; -const app = express(); -app.disable('x-powered-by'); +const app = express.Router(); app.get('/@:user/:post', async (req, res, next) => { const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); diff --git a/src/server/activitypub/publickey.ts b/src/server/activitypub/publickey.ts index c564c437e6..b48504927a 100644 --- a/src/server/activitypub/publickey.ts +++ b/src/server/activitypub/publickey.ts @@ -4,8 +4,7 @@ import render from '../../remote/activitypub/renderer/key'; import config from '../../config'; import withUser from './with-user'; -const app = express(); -app.disable('x-powered-by'); +const app = express.Router(); app.get('/@:user/publickey', withUser(username => { return `${config.url}/@${username}/publickey`; diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts index baf2dc9a05..f054974510 100644 --- a/src/server/activitypub/user.ts +++ b/src/server/activitypub/user.ts @@ -11,8 +11,7 @@ const respond = withUser(username => `${config.url}/@${username}`, (user, req, r res.json(rendered); }); -const app = express(); -app.disable('x-powered-by'); +const app = express.Router(); app.get('/@:user', (req, res, next) => { const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); diff --git a/src/server/api/bot/core.ts b/src/server/api/bot/core.ts index 7e80f31e5e..a44aa9d7bc 100644 --- a/src/server/api/bot/core.ts +++ b/src/server/api/bot/core.ts @@ -4,6 +4,7 @@ import * as bcrypt from 'bcryptjs'; import User, { IUser, init as initUser, ILocalUser } from '../../../models/user'; import getPostSummary from '../../../renderers/get-post-summary'; +import getUserName from '../../../renderers/get-user-name'; import getUserSummary from '../../../renderers/get-user-summary'; import parseAcct from '../../../acct/parse'; import getNotificationSummary from '../../../renderers/get-notification-summary'; @@ -90,7 +91,7 @@ export default class BotCore extends EventEmitter { 'タイムラインや通知を見た後、「次」というとさらに遡ることができます。'; case 'me': - return this.user ? `${this.user.name}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません'; + return this.user ? `${getUserName(this.user)}としてサインインしています。\n\n${getUserSummary(this.user)}` : 'サインインしていません'; case 'login': case 'signin': @@ -230,7 +231,7 @@ class SigninContext extends Context { if (same) { this.bot.signin(this.temporaryUser); this.bot.clearContext(); - return `${this.temporaryUser.name}さん、おかえりなさい!`; + return `${getUserName(this.temporaryUser)}さん、おかえりなさい!`; } else { return `パスワードが違います... もう一度教えてください:`; } @@ -305,7 +306,7 @@ class TlContext extends Context { this.emit('updated'); const text = tl - .map(post => `${post.user.name}\n「${getPostSummary(post)}」`) + .map(post => `${getUserName(post.user)}\n「${getPostSummary(post)}」`) .join('\n-----\n'); return text; diff --git a/src/server/api/bot/interfaces/line.ts b/src/server/api/bot/interfaces/line.ts index 7847cbdeaa..1191aaf391 100644 --- a/src/server/api/bot/interfaces/line.ts +++ b/src/server/api/bot/interfaces/line.ts @@ -10,6 +10,7 @@ import prominence = require('prominence'); import getAcct from '../../../../acct/render'; import parseAcct from '../../../../acct/parse'; import getPostSummary from '../../../../renderers/get-post-summary'; +import getUserName from '../../../../renderers/get-user-name'; const redis = prominence(_redis); @@ -131,7 +132,7 @@ class LineBot extends BotCore { template: { type: 'buttons', thumbnailImageUrl: `${user.avatarUrl}?thumbnail&size=1024`, - title: `${user.name} (@${acct})`, + title: `${getUserName(user)} (@${acct})`, text: user.description || '(no description)', actions: actions } @@ -146,7 +147,7 @@ class LineBot extends BotCore { limit: 5 }, this.user); - const text = `${tl[0].user.name}さんのタイムラインはこちらです:\n\n` + tl + const text = `${getUserName(tl[0].user)}さんのタイムラインはこちらです:\n\n` + tl .map(post => getPostSummary(post)) .join('\n-----\n'); diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index 9ccbe20171..0ccac8d83d 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -4,7 +4,7 @@ import $ from 'cafy'; import User from '../../../../models/user'; import Following from '../../../../models/following'; -import { createHttp } from '../../../../queue'; +import create from '../../../../services/following/create'; /** * Follow a user @@ -50,15 +50,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Create following - const { _id } = await Following.insert({ - createdAt: new Date(), - followerId: follower._id, - followeeId: followee._id - }); - - createHttp({ type: 'follow', following: _id }).save(); + create(follower, followee); // Send response res(); - }); diff --git a/src/server/api/endpoints/posts/create.ts b/src/server/api/endpoints/posts/create.ts index 47897626f1..003a892bc0 100644 --- a/src/server/api/endpoints/posts/create.ts +++ b/src/server/api/endpoints/posts/create.ts @@ -3,17 +3,12 @@ */ import $ from 'cafy'; import deepEqual = require('deep-equal'); -import parseAcct from '../../../../acct/parse'; -import renderAcct from '../../../../acct/render'; -import config from '../../../../config'; -import html from '../../../../text/html'; -import parse from '../../../../text/parse'; -import Post, { IPost, isValidText, isValidCw } from '../../../../models/post'; -import User, { ILocalUser } from '../../../../models/user'; +import Post, { IPost, isValidText, isValidCw, pack } from '../../../../models/post'; +import { ILocalUser } from '../../../../models/user'; import Channel, { IChannel } from '../../../../models/channel'; import DriveFile from '../../../../models/drive-file'; -import create from '../../../../post/create'; -import distribute from '../../../../post/distribute'; +import create from '../../../../services/post/create'; +import { IApp } from '../../../../models/app'; /** * Create a post @@ -23,7 +18,7 @@ import distribute from '../../../../post/distribute'; * @param {any} app * @return {Promise<any>} */ -module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej) => { +module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { // Get 'visibility' parameter const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$; if (visibilityErr) return rej('invalid visibility'); @@ -231,85 +226,26 @@ module.exports = (params, user: ILocalUser, app) => new Promise(async (res, rej) } } - let tokens = null; - if (text) { - // Analyze - tokens = parse(text); - - // Extract hashtags - const hashtags = tokens - .filter(t => t.type == 'hashtag') - .map(t => t.hashtag); - - hashtags.forEach(tag => { - if (tags.indexOf(tag) == -1) { - tags.push(tag); - } - }); - } - - let atMentions = []; - - // If has text content - if (text) { - /* - // Extract a hashtags - const hashtags = tokens - .filter(t => t.type == 'hashtag') - .map(t => t.hashtag) - // Drop dupulicates - .filter((v, i, s) => s.indexOf(v) == i); - - // ハッシュタグをデータベースに登録 - registerHashtags(user, hashtags); - */ - // Extract an '@' mentions - atMentions = tokens - .filter(t => t.type == 'mention') - .map(renderAcct) - // Drop dupulicates - .filter((v, i, s) => s.indexOf(v) == i) - // Fetch mentioned user - // SELECT _id - .map(mention => User.findOne(parseAcct(mention), { _id: true })); - } - // 投稿を作成 - const post = await create({ + const post = await create(user, { createdAt: new Date(), - channelId: channel ? channel._id : undefined, - index: channel ? channel.index + 1 : undefined, - mediaIds: files ? files.map(file => file._id) : [], + media: files, poll: poll, text: text, - textHtml: tokens === null ? null : html(tokens), + reply, + repost, cw: cw, tags: tags, - userId: user._id, - appId: app ? app._id : null, + app: app, viaMobile: viaMobile, visibility, geo - }, reply, repost, await Promise.all(atMentions)); + }); - const postObj = await distribute(user, post.mentions, post); + const postObj = await pack(post, user); // Reponse res({ createdPost: postObj }); - - // Register to search database - if (post.text && config.elasticsearch.enable) { - const es = require('../../../db/elasticsearch'); - - es.index({ - index: 'misskey', - type: 'post', - id: post._id.toString(), - body: { - text: post.text - } - }); - } }); diff --git a/src/server/api/endpoints/posts/reactions/create.ts b/src/server/api/endpoints/posts/reactions/create.ts index f1b0c7dd29..71fa6a2955 100644 --- a/src/server/api/endpoints/posts/reactions/create.ts +++ b/src/server/api/endpoints/posts/reactions/create.ts @@ -3,20 +3,11 @@ */ import $ from 'cafy'; 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 watch from '../../../../../post/watch'; -import { publishPostStream } from '../../../../../publishers/stream'; -import notify from '../../../../../publishers/notify'; -import pushSw from '../../../../../publishers/push-sw'; +import Post from '../../../../../models/post'; +import create from '../../../../../services/post/reaction/create'; /** * React to a post - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'postId' parameter @@ -46,78 +37,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { return rej('post not found'); } - // Myself - if (post.userId.equals(user._id)) { - return rej('cannot react to my post'); + try { + await create(user, post, reaction); + } catch (e) { + rej(e); } - // if already reacted - const exist = await Reaction.findOne({ - postId: post._id, - userId: user._id, - deletedAt: { $exists: false } - }); - - if (exist !== null) { - return rej('already reacted'); - } - - // Create reaction - await Reaction.insert({ - createdAt: new Date(), - postId: post._id, - userId: user._id, - reaction: reaction - }); - - // Send response res(); - - const inc = {}; - inc[`reactionCounts.${reaction}`] = 1; - - // Increment reactions count - await Post.update({ _id: post._id }, { - $inc: inc - }); - - publishPostStream(post._id, 'reacted'); - - // Notify - notify(post.userId, user._id, 'reaction', { - postId: post._id, - reaction: reaction - }); - - pushSw(post.userId, 'reaction', { - user: await packUser(user, post.userId), - post: await packPost(post, post.userId), - reaction: reaction - }); - - // Fetch watchers - Watching - .find({ - postId: post._id, - userId: { $ne: user._id }, - // 削除されたドキュメントは除く - deletedAt: { $exists: false } - }, { - fields: { - userId: true - } - }) - .then(watchers => { - watchers.forEach(watcher => { - notify(watcher.userId, user._id, 'reaction', { - postId: post._id, - reaction: reaction - }); - }); - }); - - // この投稿をWatchする - if (user.account.settings.autoWatch !== false) { - watch(user._id, post); - } }); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 2b02799378..d272ce4639 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -37,7 +37,8 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (typeof host === 'string') { try { user = await resolveRemoteUser(username, host, cursorOption); - } catch (exception) { + } catch (e) { + console.warn(`failed to resolve remote user: ${e}`); return rej('failed to resolve remote user'); } } else { diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index 4203ce526d..c54d6f1a1b 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -47,7 +47,6 @@ export default async (req: express.Request, res: express.Response) => { const username = req.body['username']; const password = req.body['password']; - const name = '名無し'; // Validate username if (!validateUsername(username)) { @@ -113,7 +112,7 @@ export default async (req: express.Request, res: express.Response) => { description: null, followersCount: 0, followingCount: 0, - name: name, + name: null, postsCount: 0, driveCapacity: 1024 * 1024 * 128, // 128MiB username: username, diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 1445d1aefa..5b1b6409b9 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -11,7 +11,7 @@ import * as bodyParser from 'body-parser'; import * as favicon from 'serve-favicon'; import * as compression from 'compression'; -const client = `${__dirname}/../../client/`; +const client = path.resolve(`${__dirname}/../../client/`); // Create server const app = express(); diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts index 20057da31f..fd7ebc3fb5 100644 --- a/src/server/webfinger.ts +++ b/src/server/webfinger.ts @@ -1,11 +1,12 @@ +import * as express from 'express'; + import config from '../config'; import parseAcct from '../acct/parse'; import User from '../models/user'; -const express = require('express'); const app = express(); -app.get('/.well-known/webfinger', async (req, res) => { +app.get('/.well-known/webfinger', async (req: express.Request, res: express.Response) => { if (typeof req.query.resource !== 'string') { return res.sendStatus(400); } @@ -34,13 +35,15 @@ app.get('/.well-known/webfinger', async (req, res) => { return res.json({ subject: `acct:${user.username}@${config.host}`, - links: [ - { - rel: 'self', - type: 'application/activity+json', - href: `${config.url}/@${user.username}` - } - ] + links: [{ + rel: 'self', + type: 'application/activity+json', + href: `${config.url}/@${user.username}` + }, { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${config.url}/@${user.username}` + }] }); }); diff --git a/src/drive/add-file.ts b/src/services/drive/add-file.ts similarity index 95% rename from src/drive/add-file.ts rename to src/services/drive/add-file.ts index 24eb5208d5..64a2f18340 100644 --- a/src/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -10,12 +10,12 @@ import * as debug from 'debug'; import fileType = require('file-type'); import prominence = require('prominence'); -import DriveFile, { IMetadata, getGridFSBucket } from '../models/drive-file'; -import DriveFolder from '../models/drive-folder'; -import { pack } from '../models/drive-file'; -import event, { publishDriveStream } from '../publishers/stream'; -import getAcct from '../acct/render'; -import config from '../config'; +import DriveFile, { IMetadata, getGridFSBucket } from '../../models/drive-file'; +import DriveFolder from '../../models/drive-folder'; +import { pack } from '../../models/drive-file'; +import event, { publishDriveStream } from '../../publishers/stream'; +import getAcct from '../../acct/render'; +import config from '../../config'; const gm = _gm.subClass({ imageMagick: true diff --git a/src/drive/upload-from-url.ts b/src/services/drive/upload-from-url.ts similarity index 82% rename from src/drive/upload-from-url.ts rename to src/services/drive/upload-from-url.ts index f96af0f266..676586cd15 100644 --- a/src/drive/upload-from-url.ts +++ b/src/services/drive/upload-from-url.ts @@ -1,19 +1,23 @@ import * as URL from 'url'; -import { IDriveFile, validateFileName } from '../models/drive-file'; +import { IDriveFile, validateFileName } from '../../models/drive-file'; import create from './add-file'; import * as debug from 'debug'; import * as tmp from 'tmp'; import * as fs from 'fs'; import * as request from 'request'; -const log = debug('misskey:common:drive:upload_from_url'); +const log = debug('misskey:drive:upload-from-url'); export default async (url, user, folderId = null, uri = null): Promise<IDriveFile> => { + log(`REQUESTED: ${url}`); + let name = URL.parse(url).pathname.split('/').pop(); if (!validateFileName(name)) { name = null; } + log(`name: ${name}`); + // Create temp file const path = await new Promise((res: (string) => void, rej) => { tmp.file((e, path) => { @@ -37,6 +41,8 @@ export default async (url, user, folderId = null, uri = null): Promise<IDriveFil const driveFile = await create(user, path, name, null, folderId, false, uri); + log(`created: ${driveFile._id}`); + // clean-up fs.unlink(path, (e) => { if (e) log(e.stack); diff --git a/src/services/following/create.ts b/src/services/following/create.ts new file mode 100644 index 0000000000..d919f4487f --- /dev/null +++ b/src/services/following/create.ts @@ -0,0 +1,72 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import notify from '../../publishers/notify'; +import context from '../../remote/activitypub/renderer/context'; +import renderFollow from '../../remote/activitypub/renderer/follow'; +import renderAccept from '../../remote/activitypub/renderer/accept'; +import { deliver } from '../../queue'; + +export default async function(follower: IUser, followee: IUser, activity?) { + const following = await Following.insert({ + createdAt: new Date(), + followerId: follower._id, + followeeId: followee._id + }); + + //#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 + + //#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 followed event + if (isLocalUser(followee)) { + packUser(follower, followee).then(packed => event(followee._id, 'followed', packed)), + + // 通知を作成 + notify(followee._id, follower._id, 'follow'); + } + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = renderFollow(follower, followee); + content['@context'] = context; + + deliver(follower, content, followee.account.inbox).save(); + } + + if (isRemoteUser(follower) && isLocalUser(followee)) { + const content = renderAccept(activity); + content['@context'] = context; + + deliver(followee, content, follower.account.inbox).save(); + } +} diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts new file mode 100644 index 0000000000..364a4803b9 --- /dev/null +++ b/src/services/following/delete.ts @@ -0,0 +1,64 @@ +import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user'; +import Following from '../../models/following'; +import FollowingLog from '../../models/following-log'; +import FollowedLog from '../../models/followed-log'; +import event from '../../publishers/stream'; +import context from '../../remote/activitypub/renderer/context'; +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?) { + const following = await Following.findOne({ + followerId: follower._id, + followeeId: followee._id + }); + + if (following == null) { + console.warn('フォロー解除がリクエストされましたがフォローしていませんでした'); + return; + } + + Following.remove({ + _id: following._id + }); + + //#region Decrement following count + User.update({ _id: follower._id }, { + $inc: { + followingCount: -1 + } + }); + + FollowingLog.insert({ + createdAt: following.createdAt, + userId: follower._id, + count: follower.followingCount - 1 + }); + //#endregion + + //#region Decrement followers count + User.update({ _id: followee._id }, { + $inc: { + followersCount: -1 + } + }); + FollowedLog.insert({ + createdAt: following.createdAt, + userId: followee._id, + count: followee.followersCount - 1 + }); + //#endregion + + // Publish unfollow event + if (isLocalUser(follower)) { + packUser(followee, follower).then(packed => event(follower._id, 'unfollow', packed)); + } + + if (isLocalUser(follower) && isRemoteUser(followee)) { + const content = renderUndo(renderFollow(follower, followee)); + content['@context'] = context; + + deliver(follower, content, followee.account.inbox).save(); + } +} diff --git a/src/services/post/create.ts b/src/services/post/create.ts new file mode 100644 index 0000000000..745683b518 --- /dev/null +++ b/src/services/post/create.ts @@ -0,0 +1,358 @@ +import Post, { pack, IPost } from '../../models/post'; +import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user'; +import stream from '../../publishers/stream'; +import Following from '../../models/following'; +import { deliver } from '../../queue'; +import renderNote from '../../remote/activitypub/renderer/note'; +import renderCreate from '../../remote/activitypub/renderer/create'; +import context from '../../remote/activitypub/renderer/context'; +import { IDriveFile } from '../../models/drive-file'; +import notify from '../../publishers/notify'; +import PostWatching from '../../models/post-watching'; +import watch from './watch'; +import Mute from '../../models/mute'; +import pushSw from '../../publishers/push-sw'; +import event from '../../publishers/stream'; +import parse from '../../text/parse'; +import html from '../../text/html'; +import { IApp } from '../../models/app'; + +export default async (user: IUser, data: { + createdAt?: Date; + text?: string; + reply?: IPost; + repost?: IPost; + media?: IDriveFile[]; + geo?: any; + poll?: any; + viaMobile?: boolean; + tags?: string[]; + cw?: string; + visibility?: string; + uri?: string; + app?: IApp; +}, silent = false) => new Promise<IPost>(async (res, rej) => { + if (data.createdAt == null) data.createdAt = new Date(); + if (data.visibility == null) data.visibility = 'public'; + + const tags = data.tags || []; + + let tokens = null; + + if (data.text) { + // Analyze + tokens = parse(data.text); + + // Extract hashtags + const hashtags = tokens + .filter(t => t.type == 'hashtag') + .map(t => t.hashtag); + + hashtags.forEach(tag => { + if (tags.indexOf(tag) == -1) { + tags.push(tag); + } + }); + } + + const insert: any = { + createdAt: data.createdAt, + mediaIds: data.media ? data.media.map(file => file._id) : [], + replyId: data.reply ? data.reply._id : null, + repostId: data.repost ? data.repost._id : null, + text: data.text, + textHtml: tokens === null ? null : html(tokens), + poll: data.poll, + cw: data.cw, + tags, + userId: user._id, + viaMobile: data.viaMobile, + geo: data.geo || null, + appId: data.app ? data.app._id : null, + visibility: data.visibility, + + // 以下非正規化データ + _reply: data.reply ? { userId: data.reply.userId } : null, + _repost: data.repost ? { userId: data.repost.userId } : null, + _user: { + host: user.host, + hostLower: user.hostLower, + account: isLocalUser(user) ? {} : { + inbox: user.account.inbox + } + } + }; + + if (data.uri != null) insert.uri = data.uri; + + // 投稿を作成 + const post = await Post.insert(insert); + + res(post); + + User.update({ _id: user._id }, { + // Increment posts count + $inc: { + postsCount: 1 + }, + // Update latest post + $set: { + latestPost: post + } + }); + + // Serialize + const postObj = await pack(post); + + // タイムラインへの投稿 + if (post.channelId == null) { + // Publish event to myself's stream + if (isLocalUser(user)) { + stream(post.userId, 'post', postObj); + } + + // Fetch all followers + const followers = await Following.aggregate([{ + $lookup: { + from: 'users', + localField: 'followerId', + foreignField: '_id', + as: 'user' + } + }, { + $match: { + followeeId: post.userId + } + }], { + _id: false + }); + + if (!silent) { + const note = await renderNote(user, post); + const content = renderCreate(note); + content['@context'] = context; + + // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送 + if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) { + deliver(user, content, data.reply._user.account.inbox).save(); + } + + Promise.all(followers.map(follower => { + follower = follower.user[0]; + + if (isLocalUser(follower)) { + // Publish event to followers stream + stream(follower._id, 'post', postObj); + } else { + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 + if (isLocalUser(user)) { + deliver(user, content, follower.account.inbox).save(); + } + } + })); + } + } + + // チャンネルへの投稿 + /* TODO + if (post.channelId) { + promises.push( + // Increment channel index(posts count) + Channel.update({ _id: post.channelId }, { + $inc: { + index: 1 + } + }), + + // Publish event to channel + promisedPostObj.then(postObj => { + publishChannelStream(post.channelId, 'post', postObj); + }), + + Promise.all([ + promisedPostObj, + + // Get channel watchers + ChannelWatching.find({ + channelId: post.channelId, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }) + ]).then(([postObj, watches]) => { + // チャンネルの視聴者(のタイムライン)に配信 + watches.forEach(w => { + stream(w.userId, 'post', postObj); + }); + }) + ); + }*/ + + const mentions = []; + + async function addMention(mentionee, reason) { + // Reject if already added + if (mentions.some(x => x.equals(mentionee))) return; + + // Add mention + mentions.push(mentionee); + + // Publish event + if (!user._id.equals(mentionee)) { + const mentioneeMutes = await Mute.find({ + muter_id: mentionee, + deleted_at: { $exists: false } + }); + const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString()); + if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) { + event(mentionee, reason, postObj); + pushSw(mentionee, reason, postObj); + } + } + } + + // If has in reply to post + if (data.reply) { + // Increment replies count + Post.update({ _id: data.reply._id }, { + $inc: { + repliesCount: 1 + } + }); + + // (自分自身へのリプライでない限りは)通知を作成 + notify(data.reply.userId, user._id, 'reply', { + postId: post._id + }); + + // Fetch watchers + PostWatching.find({ + postId: data.reply._id, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }).then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, 'reply', { + postId: post._id + }); + }); + }); + + // この投稿をWatchする + if (isLocalUser(user) && user.account.settings.autoWatch !== false) { + watch(user._id, data.reply); + } + + // Add mention + addMention(data.reply.userId, 'reply'); + } + + // If it is repost + if (data.repost) { + // Notify + const type = data.text ? 'quote' : 'repost'; + notify(data.repost.userId, user._id, type, { + post_id: post._id + }); + + // Fetch watchers + PostWatching.find({ + postId: data.repost._id, + userId: { $ne: user._id }, + // 削除されたドキュメントは除く + deletedAt: { $exists: false } + }, { + fields: { + userId: true + } + }).then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, type, { + postId: post._id + }); + }); + }); + + // この投稿をWatchする + if (isLocalUser(user) && user.account.settings.autoWatch !== false) { + watch(user._id, data.repost); + } + + // If it is quote repost + if (data.text) { + // Add mention + addMention(data.repost.userId, 'quote'); + } else { + // Publish event + if (!user._id.equals(data.repost.userId)) { + event(data.repost.userId, 'repost', postObj); + } + } + + // 今までで同じ投稿をRepostしているか + const existRepost = await Post.findOne({ + userId: user._id, + repostId: data.repost._id, + _id: { + $ne: post._id + } + }); + + if (!existRepost) { + // Update repostee status + Post.update({ _id: data.repost._id }, { + $inc: { + repostCount: 1 + } + }); + } + } + + // If has text content + if (data.text) { + // Extract an '@' mentions + const atMentions = tokens + .filter(t => t.type == 'mention') + .map(m => m.username) + // Drop dupulicates + .filter((v, i, s) => s.indexOf(v) == i); + + // Resolve all mentions + await Promise.all(atMentions.map(async mention => { + // Fetch mentioned user + // SELECT _id + const mentionee = await User + .findOne({ + usernameLower: mention.toLowerCase() + }, { _id: true }); + + // When mentioned user not found + if (mentionee == null) return; + + // 既に言及されたユーザーに対する返信や引用repostの場合も無視 + if (data.reply && data.reply.userId.equals(mentionee._id)) return; + if (data.repost && data.repost.userId.equals(mentionee._id)) return; + + // Add mention + addMention(mentionee._id, 'mention'); + + // Create notification + notify(mentionee._id, user._id, 'mention', { + post_id: post._id + }); + })); + } + + // Append mentions data + if (mentions.length > 0) { + Post.update({ _id: post._id }, { + $set: { + mentions + } + }); + } +}); diff --git a/src/services/post/reaction/create.ts b/src/services/post/reaction/create.ts new file mode 100644 index 0000000000..c26efcfc75 --- /dev/null +++ b/src/services/post/reaction/create.ts @@ -0,0 +1,94 @@ +import { IUser, pack as packUser, isLocalUser, isRemoteUser } from '../../../models/user'; +import Post, { IPost, pack as packPost } from '../../../models/post'; +import PostReaction from '../../../models/post-reaction'; +import { publishPostStream } from '../../../publishers/stream'; +import notify from '../../../publishers/notify'; +import pushSw from '../../../publishers/push-sw'; +import PostWatching from '../../../models/post-watching'; +import watch from '../watch'; +import renderLike from '../../../remote/activitypub/renderer/like'; +import { deliver } from '../../../queue'; +import context from '../../../remote/activitypub/renderer/context'; + +export default async (user: IUser, post: IPost, reaction: string) => new Promise(async (res, rej) => { + // Myself + if (post.userId.equals(user._id)) { + return rej('cannot react to my post'); + } + + // if already reacted + const exist = await PostReaction.findOne({ + postId: post._id, + userId: user._id + }); + + if (exist !== null) { + return rej('already reacted'); + } + + // Create reaction + await PostReaction.insert({ + createdAt: new Date(), + postId: post._id, + userId: user._id, + reaction + }); + + res(); + + const inc = {}; + inc[`reactionCounts.${reaction}`] = 1; + + // Increment reactions count + await Post.update({ _id: post._id }, { + $inc: inc + }); + + publishPostStream(post._id, 'reacted'); + + // Notify + notify(post.userId, user._id, 'reaction', { + postId: post._id, + reaction: reaction + }); + + pushSw(post.userId, 'reaction', { + user: await packUser(user, post.userId), + post: await packPost(post, post.userId), + reaction: reaction + }); + + // Fetch watchers + PostWatching + .find({ + postId: post._id, + userId: { $ne: user._id } + }, { + fields: { + userId: true + } + }) + .then(watchers => { + watchers.forEach(watcher => { + notify(watcher.userId, user._id, 'reaction', { + postId: post._id, + reaction: reaction + }); + }); + }); + + // ユーザーがローカルユーザーかつ自動ウォッチ設定がオンならばこの投稿をWatchする + if (isLocalUser(user) && user.account.settings.autoWatch !== false) { + watch(user._id, post); + } + + //#region 配信 + const content = renderLike(user, post); + content['@context'] = context; + + // リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送 + if (isLocalUser(user) && isRemoteUser(post._user)) { + deliver(user, content, post._user.account.inbox).save(); + } + //#endregion +}); diff --git a/src/post/watch.ts b/src/services/post/watch.ts similarity index 90% rename from src/post/watch.ts rename to src/services/post/watch.ts index 61ea444430..bbd9976f40 100644 --- a/src/post/watch.ts +++ b/src/services/post/watch.ts @@ -1,5 +1,5 @@ import * as mongodb from 'mongodb'; -import Watching from '../models/post-watching'; +import Watching from '../../models/post-watching'; export default async (me: mongodb.ObjectID, post: object) => { // 自分の投稿はwatchできない