diff --git a/README.md b/README.md index f7d67247a0..4c0506709d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Misskey [![][dependencies-badge]][dependencies-link] [![][himawari-badge]][himasaku] [![][sakurako-badge]][himasaku] -[![][agpl-3.0-badge]][AGPL-3.0] +[](http://makeapullrequest.com) > Lead Maintainer: [syuilo][syuilo-link] @@ -50,6 +50,8 @@ If you want to donate to Misskey, please see [this](./docs/donate.ja.md). Misskey is an open-source software licensed under [GNU AGPLv3](LICENSE). +[![][agpl-3.0-badge]][AGPL-3.0] + [agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html [agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square [travis-link]: https://travis-ci.org/syuilo/misskey diff --git a/package.json b/package.json index 2b8c1bca5a..328ba72523 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "autwh": "0.0.1", "bcryptjs": "2.4.3", "body-parser": "1.18.2", - "bootstrap-vue": "2.0.0-rc.1", + "bootstrap-vue": "2.0.0-rc.4", "cafy": "3.2.1", "chai": "4.1.2", "chai-http": "4.0.0", @@ -134,6 +134,7 @@ "hard-source-webpack-plugin": "0.6.4", "highlight.js": "9.12.0", "html-minifier": "3.5.13", + "http-signature": "^1.2.0", "inquirer": "5.2.0", "is-root": "2.0.0", "is-url": "1.2.4", diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index 8c10bdee28..b58ba37ecb 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -4,7 +4,7 @@ import signin from './signin.vue'; import signup from './signup.vue'; import forkit from './forkit.vue'; import nav from './nav.vue'; -import postHtml from './post-html.vue'; +import postHtml from './post-html'; import poll from './poll.vue'; import pollEditor from './poll-editor.vue'; import reactionIcon from './reaction-icon.vue'; diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index 25ceab85a1..91af26bffe 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -4,13 +4,13 @@ <img class="avatar" :src="`${message.user.avatarUrl}?thumbnail&size=80`" alt=""/> </router-link> <div class="content"> - <div class="balloon" :data-no-text="message.textHtml == null"> + <div class="balloon" :data-no-text="message.text == null"> <p class="read" v-if="isMe && message.isRead">%i18n:common.tags.mk-messaging-message.is-read%</p> <button class="delete-button" v-if="isMe" title="%i18n:common.delete%"> <img src="/assets/desktop/messaging/delete.png" alt="Delete"/> </button> <div class="content" v-if="!message.isDeleted"> - <mk-post-html class="text" v-if="message.textHtml" ref="text" :html="message.textHtml" :i="os.i"/> + <mk-post-html class="text" v-if="message.text" ref="text" :text="message.text" :i="os.i"/> <div class="file" v-if="message.file"> <a :href="message.file.url" target="_blank" :title="message.file.name"> <img v-if="message.file.type.split('/')[0] == 'image'" :src="message.file.url" :alt="message.file.name"/> @@ -35,35 +35,30 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; export default Vue.extend({ - props: ['message'], - data() { - return { - urls: [] - }; + props: { + message: { + required: true + } }, computed: { - acct() { + acct(): string { return getAcct(this.message.user); }, isMe(): boolean { return this.message.userId == (this as any).os.i.id; - } - }, - watch: { - message: { - handler(newMessage, oldMessage) { - if (!oldMessage || newMessage.textHtml !== oldMessage.textHtml) { - this.$nextTick(() => { - const elements = this.$refs.text.$el.getElementsByTagName('a'); - - this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) - .map(({ href }) => href); - }); - } - }, - immediate: true + }, + urls(): string[] { + if (this.message.text) { + const ast = parse(this.message.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } } } }); diff --git a/src/client/app/common/views/components/post-html.ts b/src/client/app/common/views/components/post-html.ts new file mode 100644 index 0000000000..c5c3b72758 --- /dev/null +++ b/src/client/app/common/views/components/post-html.ts @@ -0,0 +1,157 @@ +import Vue from 'vue'; +import * as emojilib from 'emojilib'; +import parse from '../../../../../common/text/parse'; +import getAcct from '../../../../../common/user/get-acct'; +import { url } from '../../../config'; +import MkUrl from './url.vue'; + +const flatten = list => list.reduce( + (a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [] +); + +export default Vue.component('mk-post-html', { + props: { + text: { + type: String, + required: true + }, + ast: { + type: [], + required: false + }, + shouldBreak: { + type: Boolean, + default: true + }, + i: { + type: Object, + default: null + } + }, + + render(createElement) { + let ast; + + if (this.ast == null) { + // Parse text to ast + ast = parse(this.text); + } else { + ast = this.ast; + } + + // Parse ast to DOM + const els = flatten(ast.map(token => { + switch (token.type) { + case 'text': + const text = token.content.replace(/(\r\n|\n|\r)/g, '\n'); + + if (this.shouldBreak) { + const x = text.split('\n') + .map(t => t == '' ? [createElement('br')] : [createElement('span', t), createElement('br')]); + x[x.length - 1].pop(); + return x; + } else { + return createElement('span', text.replace(/\n/g, ' ')); + } + + case 'bold': + return createElement('strong', token.bold); + + case 'url': + return createElement(MkUrl, { + props: { + url: token.content, + target: '_blank' + } + }); + + case 'link': + return createElement('a', { + attrs: { + class: 'link', + href: token.url, + target: '_blank', + title: token.url + } + }, token.title); + + case 'mention': + return (createElement as any)('a', { + attrs: { + href: `${url}/@${getAcct(token)}`, + target: '_blank', + dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token) + }, + directives: [{ + name: 'user-preview', + value: token.content + }] + }, token.content); + + case 'hashtag': + return createElement('a', { + attrs: { + href: `${url}/search?q=${token.content}`, + target: '_blank' + } + }, token.content); + + case 'code': + return createElement('pre', [ + createElement('code', { + domProps: { + innerHTML: token.html + } + }) + ]); + + case 'inline-code': + return createElement('code', { + domProps: { + innerHTML: token.html + } + }); + + case 'quote': + const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n'); + + if (this.shouldBreak) { + const x = text2.split('\n') + .map(t => [createElement('span', t), createElement('br')]); + x[x.length - 1].pop(); + return createElement('div', { + attrs: { + class: 'quote' + } + }, x); + } else { + return createElement('span', { + attrs: { + class: 'quote' + } + }, text2.replace(/\n/g, ' ')); + } + + case 'emoji': + const emoji = emojilib.lib[token.emoji]; + return createElement('span', emoji ? emoji.char : token.content); + + default: + console.log('unknown ast type:', token.type); + } + })); + + const _els = []; + els.forEach((el, i) => { + if (el.tag == 'br') { + if (els[i - 1].tag != 'div') { + _els.push(el); + } + } else { + _els.push(el); + } + }); + + return createElement('span', _els); + } +}); diff --git a/src/client/app/common/views/components/post-html.vue b/src/client/app/common/views/components/post-html.vue deleted file mode 100644 index 1c949052b9..0000000000 --- a/src/client/app/common/views/components/post-html.vue +++ /dev/null @@ -1,103 +0,0 @@ -<template><div class="mk-post-html" v-html="html"></div></template> - -<script lang="ts"> -import Vue from 'vue'; -import getAcct from '../../../../../common/user/get-acct'; -import { url } from '../../../config'; - -function markUrl(a) { - while (a.firstChild) { - a.removeChild(a.firstChild); - } - - const schema = document.createElement('span'); - const delimiter = document.createTextNode('//'); - const host = document.createElement('span'); - const pathname = document.createElement('span'); - const query = document.createElement('span'); - const hash = document.createElement('span'); - - schema.className = 'schema'; - schema.textContent = a.protocol; - - host.className = 'host'; - host.textContent = a.host; - - pathname.className = 'pathname'; - pathname.textContent = a.pathname; - - query.className = 'query'; - query.textContent = a.search; - - hash.className = 'hash'; - hash.textContent = a.hash; - - a.appendChild(schema); - a.appendChild(delimiter); - a.appendChild(host); - a.appendChild(pathname); - a.appendChild(query); - a.appendChild(hash); -} - -function markMe(me, a) { - a.setAttribute("data-is-me", me && `${url}/@${getAcct(me)}` == a.href); -} - -function markTarget(a) { - a.setAttribute("target", "_blank"); -} - -export default Vue.component('mk-post-html', { - props: { - html: { - type: String, - required: true - }, - i: { - type: Object, - default: null - } - }, - watch { - html: { - handler() { - this.$nextTick(() => [].forEach.call(this.$el.getElementsByTagName('a'), a => { - if (a.href === a.textContent) { - markUrl(a); - } else { - markMe((this as any).i, a); - } - - markTarget(a); - })); - }, - immediate: true, - } - } -}); -</script> - -<style lang="stylus"> -.mk-post-html - a - word-break break-all - - > .schema - opacity 0.5 - - > .host - font-weight bold - - > .pathname - opacity 0.8 - - > .query - opacity 0.5 - - > .hash - font-style italic - - p - margin 0 -</style> diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue new file mode 100644 index 0000000000..e6ffe4466d --- /dev/null +++ b/src/client/app/common/views/components/url.vue @@ -0,0 +1,57 @@ +<template> +<a class="mk-url" :href="url" :target="target"> + <span class="schema">{{ schema }}//</span> + <span class="hostname">{{ hostname }}</span> + <span class="port" v-if="port != ''">:{{ port }}</span> + <span class="pathname" v-if="pathname != ''">{{ pathname }}</span> + <span class="query">{{ query }}</span> + <span class="hash">{{ hash }}</span> + %fa:external-link-square-alt% +</a> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + props: ['url', 'target'], + data() { + return { + schema: null, + hostname: null, + port: null, + pathname: null, + query: null, + hash: null + }; + }, + created() { + const url = new URL(this.url); + this.schema = url.protocol; + this.hostname = url.hostname; + this.port = url.port; + this.pathname = url.pathname; + this.query = url.search; + this.hash = url.hash; + } +}); +</script> + +<style lang="stylus" scoped> +.mk-url + word-break break-all + > [data-fa] + padding-left 2px + font-size .9em + font-weight 400 + font-style normal + > .schema + opacity 0.5 + > .hostname + font-weight bold + > .pathname + opacity 0.8 + > .query + opacity 0.5 + > .hash + font-style italic +</style> diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue index f379029f9f..09b090bdc1 100644 --- a/src/client/app/common/views/components/welcome-timeline.vue +++ b/src/client/app/common/views/components/welcome-timeline.vue @@ -15,7 +15,7 @@ </div> </header> <div class="text"> - <mk-post-html :html="post.textHtml"/> + <mk-post-html :text="post.text"/> </div> </div> </div> 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 b6148d9b28..1d5649cf92 100644 --- a/src/client/app/desktop/views/components/post-detail.sub.vue +++ b/src/client/app/desktop/views/components/post-detail.sub.vue @@ -16,7 +16,7 @@ </div> </header> <div class="body"> - <mk-post-html v-if="post.textHtml" :html="post.textHtml" :i="os.i" :class="$style.text"/> + <mk-post-html v-if="post.text" :text="post.text" :i="os.i" :class="$style.text"/> <div class="media" v-if="post.media > 0"> <mk-media-list :media-list="post.media"/> </div> diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue index e75ebe34b4..d6481e13d0 100644 --- a/src/client/app/desktop/views/components/post-detail.vue +++ b/src/client/app/desktop/views/components/post-detail.vue @@ -27,18 +27,18 @@ </p> </div> <article> - <router-link class="avatar-anchor" :to="`/@${acct}`"> + <router-link class="avatar-anchor" :to="`/@${pAcct}`"> <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="`/@${acct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link> - <span class="username">@{{ acct }}</span> - <router-link class="time" :to="`/@${acct}/${p.id}`"> + <router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ p.user.name }}</router-link> + <span class="username">@{{ pAcct }}</span> + <router-link class="time" :to="`/@${pAcct}/${p.id}`"> <mk-time :time="p.createdAt"/> </router-link> </header> <div class="body"> - <mk-post-html :class="$style.text" v-if="p.text" ref="text" :text="p.text" :i="os.i"/> + <mk-post-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/> <div class="media" v-if="p.media.length > 0"> <mk-media-list :media-list="p.media"/> </div> @@ -79,6 +79,7 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; import MkPostFormWindow from './post-form-window.vue'; import MkRepostFormWindow from './repost-form-window.vue'; @@ -90,6 +91,7 @@ export default Vue.extend({ components: { XSub }, + props: { post: { type: Object, @@ -99,19 +101,15 @@ export default Vue.extend({ default: false } }, - computed: { - acct() { - return getAcct(this.post.user); - } - }, + data() { return { context: [], contextFetching: false, - replies: [], - urls: [] + replies: [] }; }, + computed: { isRepost(): boolean { return (this.post.repost && @@ -131,8 +129,25 @@ export default Vue.extend({ }, title(): string { return dateStringify(this.p.createdAt); + }, + acct(): string { + return getAcct(this.post.user); + }, + pAcct(): string { + return getAcct(this.p.user); + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } } }, + mounted() { // Get replies if (!this.compact) { @@ -162,21 +177,7 @@ export default Vue.extend({ } } }, - watch: { - post: { - handler(newPost, oldPost) { - if (!oldPost || newPost.text !== oldPost.text) { - this.$nextTick(() => { - const elements = this.$refs.text.$el.getElementsByTagName('a'); - this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) - .map(({ href }) => href); - }); - } - }, - immediate: true - } - }, methods: { fetchContext() { this.contextFetching = true; diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue index f3566c81bf..c31e28d67f 100644 --- a/src/client/app/desktop/views/components/posts.post.vue +++ b/src/client/app/desktop/views/components/posts.post.vue @@ -38,7 +38,7 @@ </p> <div class="text"> <a class="reply" v-if="p.reply">%fa:reply%</a> - <mk-post-html v-if="p.textHtml" ref="text" :html="p.textHtml" :i="os.i" :class="$style.text"/> + <mk-post-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/> <a class="rp" v-if="p.repost">RP:</a> </div> <div class="media" v-if="p.media.length > 0"> @@ -86,6 +86,8 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; + import MkPostFormWindow from './post-form-window.vue'; import MkRepostFormWindow from './repost-form-window.vue'; import MkPostMenu from '../../../common/views/components/post-menu.vue'; @@ -107,17 +109,19 @@ export default Vue.extend({ components: { XSub }, + props: ['post'], + data() { return { isDetailOpened: false, connection: null, - connectionId: null, - urls: [] + connectionId: null }; }, + computed: { - acct() { + acct(): string { return getAcct(this.p.user); }, isRepost(): boolean { @@ -141,14 +145,26 @@ export default Vue.extend({ }, url(): string { return `/@${this.acct}/${this.p.id}`; + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } } }, + created() { if ((this as any).os.isSignedIn) { this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); } }, + mounted() { this.capture(true); @@ -174,6 +190,7 @@ export default Vue.extend({ } } }, + beforeDestroy() { this.decapture(true); @@ -182,21 +199,7 @@ export default Vue.extend({ (this as any).os.stream.dispose(this.connectionId); } }, - watch: { - post: { - handler(newPost, oldPost) { - if (!oldPost || newPost.textHtml !== oldPost.textHtml) { - this.$nextTick(() => { - const elements = this.$refs.text.$el.getElementsByTagName('a'); - this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) - .map(({ href }) => href); - }); - } - }, - immediate: true - } - }, methods: { capture(withHandler = false) { if ((this as any).os.isSignedIn) { @@ -457,7 +460,7 @@ export default Vue.extend({ font-size 1.1em color #717171 - >>> blockquote + >>> .quote margin 8px padding 6px 12px color #aaa diff --git a/src/client/app/desktop/views/components/sub-post-content.vue b/src/client/app/desktop/views/components/sub-post-content.vue index 58c81e7552..17899af280 100644 --- a/src/client/app/desktop/views/components/sub-post-content.vue +++ b/src/client/app/desktop/views/components/sub-post-content.vue @@ -2,7 +2,7 @@ <div class="mk-sub-post-content"> <div class="body"> <a class="reply" v-if="post.replyId">%fa:reply%</a> - <mk-post-html ref="text" :html="post.textHtml" :i="os.i"/> + <mk-post-html :text="post.text" :i="os.i"/> <a class="rp" v-if="post.repostId" :href="`/post:${post.repostId}`">RP: ...</a> </div> <details v-if="post.media.length > 0"> diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index 7e337d2ae5..448d04d261 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -95,7 +95,7 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -.header +root(isDark) position -webkit-sticky position sticky top 0 @@ -112,7 +112,7 @@ export default Vue.extend({ z-index 1000 width 100% height 48px - background #f7f7f7 + background isDark ? #313543 : #f7f7f7 > .main z-index 1001 @@ -169,4 +169,10 @@ export default Vue.extend({ > .mk-ui-header-search display none +.header[data-is-darkmode] + root(true) + +.header + root(false) + </style> diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue index 77a73426f2..0a4e36fc60 100644 --- a/src/client/app/mobile/views/components/post-detail.vue +++ b/src/client/app/mobile/views/components/post-detail.vue @@ -81,6 +81,8 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; + import MkPostMenu from '../../../common/views/components/post-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import XSub from './post-detail.sub.vue'; @@ -89,6 +91,7 @@ export default Vue.extend({ components: { XSub }, + props: { post: { type: Object, @@ -98,19 +101,20 @@ export default Vue.extend({ default: false } }, + data() { return { context: [], contextFetching: false, - replies: [], - urls: [] + replies: [] }; }, + computed: { - acct() { + acct(): string { return getAcct(this.post.user); }, - pAcct() { + pAcct(): string { return getAcct(this.p.user); }, isRepost(): boolean { @@ -128,8 +132,19 @@ export default Vue.extend({ .map(key => this.p.reactionCounts[key]) .reduce((a, b) => a + b) : 0; + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } } }, + mounted() { // Get replies if (!this.compact) { @@ -159,21 +174,7 @@ export default Vue.extend({ } } }, - watch: { - post: { - handler(newPost, oldPost) { - if (!oldPost || newPost.text !== oldPost.text) { - this.$nextTick(() => { - const elements = this.$refs.text.$el.getElementsByTagName('a'); - this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) - .map(({ href }) => href); - }); - } - }, - immediate: true - } - }, methods: { fetchContext() { this.contextFetching = true; diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue index 96ec9632f1..f4f845b49a 100644 --- a/src/client/app/mobile/views/components/post.vue +++ b/src/client/app/mobile/views/components/post.vue @@ -37,7 +37,7 @@ <a class="reply" v-if="p.reply"> %fa:reply% </a> - <mk-post-html v-if="p.text" ref="text" :text="p.text" :i="os.i" :class="$style.text"/> + <mk-post-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/> <a class="rp" v-if="p.repost != null">RP:</a> </div> <div class="media" v-if="p.media.length > 0"> @@ -78,6 +78,8 @@ <script lang="ts"> import Vue from 'vue'; import getAcct from '../../../../../common/user/get-acct'; +import parse from '../../../../../common/text/parse'; + import MkPostMenu from '../../../common/views/components/post-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import XSub from './post.sub.vue'; @@ -86,19 +88,21 @@ export default Vue.extend({ components: { XSub }, + props: ['post'], + data() { return { connection: null, - connectionId: null, - urls: [] + connectionId: null }; }, + computed: { - acct() { + acct(): string { return getAcct(this.post.user); }, - pAcct() { + pAcct(): string { return getAcct(this.p.user); }, isRepost(): boolean { @@ -119,14 +123,26 @@ export default Vue.extend({ }, url(): string { return `/@${this.pAcct}/${this.p.id}`; + }, + urls(): string[] { + if (this.p.text) { + const ast = parse(this.p.text); + return ast + .filter(t => (t.type == 'url' || t.type == 'link') && !t.silent) + .map(t => t.url); + } else { + return null; + } } }, + created() { if ((this as any).os.isSignedIn) { this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); } }, + mounted() { this.capture(true); @@ -152,6 +168,7 @@ export default Vue.extend({ } } }, + beforeDestroy() { this.decapture(true); @@ -160,21 +177,7 @@ export default Vue.extend({ (this as any).os.stream.dispose(this.connectionId); } }, - watch: { - post: { - handler(newPost, oldPost) { - if (!oldPost || newPost.text !== oldPost.text) { - this.$nextTick(() => { - const elements = this.$refs.text.$el.getElementsByTagName('a'); - this.urls = [].filter.call(elements, ({ origin }) => origin !== location.origin) - .map(({ href }) => href); - }); - } - }, - immediate: true - } - }, methods: { capture(withHandler = false) { if ((this as any).os.isSignedIn) { @@ -396,7 +399,7 @@ export default Vue.extend({ font-size 1.1em color #717171 - >>> blockquote + >>> .quote margin 8px padding 6px 12px color #aaa diff --git a/src/client/app/mobile/views/components/sub-post-content.vue b/src/client/app/mobile/views/components/sub-post-content.vue index 955bb406b4..97dd987dd7 100644 --- a/src/client/app/mobile/views/components/sub-post-content.vue +++ b/src/client/app/mobile/views/components/sub-post-content.vue @@ -2,7 +2,7 @@ <div class="mk-sub-post-content"> <div class="body"> <a class="reply" v-if="post.replyId">%fa:reply%</a> - <mk-post-html v-if="post.text" :ast="post.text" :i="os.i"/> + <mk-post-html v-if="post.text" :text="post.text" :i="os.i"/> <a class="rp" v-if="post.repostId">RP: ...</a> </div> <details v-if="post.media.length > 0"> diff --git a/src/client/docs/api/gulpfile.ts b/src/client/docs/api/gulpfile.ts index 16066b0d2e..4b962fe0c6 100644 --- a/src/client/docs/api/gulpfile.ts +++ b/src/client/docs/api/gulpfile.ts @@ -101,7 +101,7 @@ gulp.task('doc:api:endpoints', async () => { } //console.log(files); files.forEach(file => { - const ep = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); + const ep: any = yaml.safeLoad(fs.readFileSync(file, 'utf-8')); const vars = { endpoint: ep.endpoint, url: { diff --git a/src/common/remote/activitypub/renderer/context.ts b/src/common/remote/activitypub/renderer/context.ts new file mode 100644 index 0000000000..b56f727ae7 --- /dev/null +++ b/src/common/remote/activitypub/renderer/context.ts @@ -0,0 +1,5 @@ +export default [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { Hashtag: 'as:Hashtag' } +]; diff --git a/src/common/remote/activitypub/renderer/document.ts b/src/common/remote/activitypub/renderer/document.ts new file mode 100644 index 0000000000..4a456416a9 --- /dev/null +++ b/src/common/remote/activitypub/renderer/document.ts @@ -0,0 +1,7 @@ +import config from '../../../../conf'; + +export default ({ _id, contentType }) => ({ + type: 'Document', + mediaType: contentType, + url: `${config.drive_url}/${_id}` +}); diff --git a/src/common/remote/activitypub/renderer/hashtag.ts b/src/common/remote/activitypub/renderer/hashtag.ts new file mode 100644 index 0000000000..ad42700204 --- /dev/null +++ b/src/common/remote/activitypub/renderer/hashtag.ts @@ -0,0 +1,7 @@ +import config from '../../../../conf'; + +export default tag => ({ + type: 'Hashtag', + href: `${config.url}/search?q=#${encodeURIComponent(tag)}`, + name: '#' + tag +}); diff --git a/src/common/remote/activitypub/renderer/image.ts b/src/common/remote/activitypub/renderer/image.ts new file mode 100644 index 0000000000..345fbbec59 --- /dev/null +++ b/src/common/remote/activitypub/renderer/image.ts @@ -0,0 +1,6 @@ +import config from '../../../../conf'; + +export default ({ _id }) => ({ + type: 'Image', + url: `${config.drive_url}/${_id}` +}); diff --git a/src/common/remote/activitypub/renderer/key.ts b/src/common/remote/activitypub/renderer/key.ts new file mode 100644 index 0000000000..7148c59745 --- /dev/null +++ b/src/common/remote/activitypub/renderer/key.ts @@ -0,0 +1,9 @@ +import config from '../../../../conf'; +import { extractPublic } from '../../../../crypto_key'; +import { ILocalAccount } from '../../../../models/user'; + +export default ({ username, account }) => ({ + type: 'Key', + owner: `${config.url}/@${username}`, + publicKeyPem: extractPublic((account as ILocalAccount).keypair) +}); diff --git a/src/common/remote/activitypub/renderer/note.ts b/src/common/remote/activitypub/renderer/note.ts new file mode 100644 index 0000000000..2fe20b2136 --- /dev/null +++ b/src/common/remote/activitypub/renderer/note.ts @@ -0,0 +1,44 @@ +import renderDocument from './document'; +import renderHashtag from './hashtag'; +import config from '../../../../conf'; +import DriveFile from '../../../../models/drive-file'; +import Post from '../../../../models/post'; +import User from '../../../../models/user'; + +export default async (user, post) => { + const promisedFiles = DriveFile.find({ _id: { $in: post.mediaIds } }); + let inReplyTo; + + if (post.replyId) { + const inReplyToPost = await Post.findOne({ + _id: post.replyId, + }); + + if (inReplyToPost !== null) { + const inReplyToUser = await User.findOne({ + _id: post.userId, + }); + + if (inReplyToUser !== null) { + inReplyTo = `${config.url}@${inReplyToUser.username}/${inReplyToPost._id}`; + } + } + } else { + inReplyTo = null; + } + + const attributedTo = `${config.url}/@${user.username}`; + + return { + id: `${attributedTo}/${post._id}`, + type: 'Note', + attributedTo, + content: post.textHtml, + published: post.createdAt.toISOString(), + to: 'https://www.w3.org/ns/activitystreams#Public', + cc: `${attributedTo}/followers`, + inReplyTo, + attachment: (await promisedFiles).map(renderDocument), + tag: post.tags.map(renderHashtag) + }; +}; diff --git a/src/common/remote/activitypub/renderer/ordered-collection.ts b/src/common/remote/activitypub/renderer/ordered-collection.ts new file mode 100644 index 0000000000..2ca0f77354 --- /dev/null +++ b/src/common/remote/activitypub/renderer/ordered-collection.ts @@ -0,0 +1,6 @@ +export default (id, totalItems, orderedItems) => ({ + id, + type: 'OrderedCollection', + totalItems, + orderedItems +}); diff --git a/src/common/remote/activitypub/renderer/person.ts b/src/common/remote/activitypub/renderer/person.ts new file mode 100644 index 0000000000..7303b30385 --- /dev/null +++ b/src/common/remote/activitypub/renderer/person.ts @@ -0,0 +1,20 @@ +import renderImage from './image'; +import renderKey from './key'; +import config from '../../../../conf'; + +export default user => { + const id = `${config.url}/@${user.username}`; + + return { + type: 'Person', + id, + inbox: `${id}/inbox`, + outbox: `${id}/outbox`, + preferredUsername: user.username, + name: user.name, + summary: user.description, + icon: user.avatarId && renderImage({ _id: user.avatarId }), + image: user.bannerId && renderImage({ _id: user.bannerId }), + publicKey: renderKey(user) + }; +}; diff --git a/src/common/remote/activitypub/resolve-person.ts b/src/common/remote/activitypub/resolve-person.ts index c7c131b0ea..999a37eea1 100644 --- a/src/common/remote/activitypub/resolve-person.ts +++ b/src/common/remote/activitypub/resolve-person.ts @@ -62,6 +62,10 @@ export default async (value, usernameLower, hostLower, acctLower) => { host: toUnicode(finger.subject.replace(/^.*?@/, '')), hostLower, account: { + publicKey: { + id: object.publicKey.id, + publicKeyPem: object.publicKey.publicKeyPem + }, uri: object.id, }, }); diff --git a/src/common/text/parse/index.ts b/src/common/text/parse/index.ts index 1e2398dc38..b958da81b0 100644 --- a/src/common/text/parse/index.ts +++ b/src/common/text/parse/index.ts @@ -14,7 +14,7 @@ const elements = [ require('./elements/emoji') ]; -export default (source: string) => { +export default (source: string): any[] => { if (source == '') { return null; diff --git a/src/crypto_key.d.ts b/src/crypto_key.d.ts index 28ac2f9683..48efef2980 100644 --- a/src/crypto_key.d.ts +++ b/src/crypto_key.d.ts @@ -1 +1,2 @@ +export function extractPublic(keypair: String): String; export function generate(): String; diff --git a/src/models/post.ts b/src/models/post.ts index 6c853e4f81..4daad306d6 100644 --- a/src/models/post.ts +++ b/src/models/post.ts @@ -30,6 +30,7 @@ export type IPost = { repostId: mongo.ObjectID; poll: any; // todo text: string; + tags: string[]; textHtml: string; cw: string; userId: mongo.ObjectID; diff --git a/src/models/user.ts b/src/models/user.ts index 4728682d67..9588c45153 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -71,6 +71,10 @@ export type ILocalAccount = { export type IRemoteAccount = { uri: string; + publicKey: { + id: string; + publicKeyPem: string; + }; }; export type IUser = { @@ -278,61 +282,6 @@ export const pack = ( resolve(_user); }); -/** - * Pack a user for ActivityPub - * - * @param user target - * @return Packed user - */ -export const packForAp = ( - user: string | mongo.ObjectID | IUser -) => new Promise<any>(async (resolve, reject) => { - - let _user: any; - - const fields = { - // something - }; - - // Populate the user if 'user' is ID - if (mongo.ObjectID.prototype.isPrototypeOf(user)) { - _user = await User.findOne({ - _id: user - }, { fields }); - } else if (typeof user === 'string') { - _user = await User.findOne({ - _id: new mongo.ObjectID(user) - }, { fields }); - } else { - _user = deepcopy(user); - } - - if (!_user) return reject('invalid user arg.'); - - const userUrl = `${config.url}/@@${_user._id}`; - - resolve({ - "@context": ["https://www.w3.org/ns/activitystreams", { - "@language": "ja" - }], - "type": "Person", - "id": userUrl, - "following": `${userUrl}/following.json`, - "followers": `${userUrl}/followers.json`, - "liked": `${userUrl}/liked.json`, - "inbox": `${userUrl}/inbox.json`, - "outbox": `${userUrl}/outbox.json`, - "sharedInbox": `${config.url}/inbox`, - "url": `${config.url}/@${_user.username}`, - "preferredUsername": _user.username, - "name": _user.name, - "summary": _user.description, - "icon": [ - `${config.drive_url}/${_user.avatarId}` - ] - }); -}); - /* function img(url) { return { diff --git a/src/server/activitypub/inbox.ts b/src/server/activitypub/inbox.ts new file mode 100644 index 0000000000..9151297487 --- /dev/null +++ b/src/server/activitypub/inbox.ts @@ -0,0 +1,42 @@ +import * as bodyParser from 'body-parser'; +import * as express from 'express'; +import { parseRequest, verifySignature } from 'http-signature'; +import User, { IRemoteAccount } from '../../models/user'; +import queue from '../../queue'; + +const app = express(); +app.disable('x-powered-by'); +app.use(bodyParser.json()); + +app.post('/@:user/inbox', async (req, res) => { + let parsed; + + try { + parsed = parseRequest(req); + } catch (exception) { + return res.sendStatus(401); + } + + const user = await User.findOne({ + host: { $ne: null }, + 'account.publicKey.id': parsed.keyId + }); + + if (user === null) { + return res.sendStatus(401); + } + + if (!verifySignature(parsed, (user.account as IRemoteAccount).publicKey.publicKeyPem)) { + return res.sendStatus(401); + } + + queue.create('http', { + type: 'performActivityPub', + actor: user._id, + outbox: req.body, + }).save(); + + return res.status(202).end(); +}); + +export default app; diff --git a/src/server/activitypub/index.ts b/src/server/activitypub/index.ts new file mode 100644 index 0000000000..c81024d15f --- /dev/null +++ b/src/server/activitypub/index.ts @@ -0,0 +1,16 @@ +import * as express from 'express'; + +import user from './user'; +import inbox from './inbox'; +import outbox from './outbox'; +import post from './post'; + +const app = express(); +app.disable('x-powered-by'); + +app.use(user); +app.use(inbox); +app.use(outbox); +app.use(post); + +export default app; diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts new file mode 100644 index 0000000000..c5a42ae0a9 --- /dev/null +++ b/src/server/activitypub/outbox.ts @@ -0,0 +1,45 @@ +import * as express from 'express'; +import context from '../../common/remote/activitypub/renderer/context'; +import renderNote from '../../common/remote/activitypub/renderer/note'; +import renderOrderedCollection from '../../common/remote/activitypub/renderer/ordered-collection'; +import parseAcct from '../../common/user/parse-acct'; +import config from '../../conf'; +import Post from '../../models/post'; +import User from '../../models/user'; + +const app = express(); +app.disable('x-powered-by'); + +app.get('/@:user/outbox', async (req, res) => { + const { username, host } = parseAcct(req.params.user); + if (host !== null) { + return res.sendStatus(422); + } + + const user = await User.findOne({ + usernameLower: username.toLowerCase(), + host: null + }); + if (user === null) { + return res.sendStatus(404); + } + + const id = `${config.url}/@${user.username}/inbox`; + + if (username !== user.username) { + return res.redirect(id); + } + + const posts = await Post.find({ userId: user._id }, { + limit: 20, + sort: { _id: -1 } + }); + + const renderedPosts = await Promise.all(posts.map(post => renderNote(user, post))); + const rendered = renderOrderedCollection(id, user.postsCount, renderedPosts); + rendered['@context'] = context; + + res.json(rendered); +}); + +export default app; diff --git a/src/server/activitypub/post.ts b/src/server/activitypub/post.ts new file mode 100644 index 0000000000..6644563d8c --- /dev/null +++ b/src/server/activitypub/post.ts @@ -0,0 +1,44 @@ +import * as express from 'express'; +import context from '../../common/remote/activitypub/renderer/context'; +import render from '../../common/remote/activitypub/renderer/note'; +import parseAcct from '../../common/user/parse-acct'; +import Post from '../../models/post'; +import User from '../../models/user'; + +const app = express(); +app.disable('x-powered-by'); + +app.get('/@:user/:post', async (req, res, next) => { + const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); + if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) { + return next(); + } + + const { username, host } = parseAcct(req.params.user); + if (host !== null) { + return res.sendStatus(422); + } + + const user = await User.findOne({ + usernameLower: username.toLowerCase(), + host: null + }); + if (user === null) { + return res.sendStatus(404); + } + + const post = await Post.findOne({ + _id: req.params.post, + userId: user._id + }); + if (post === null) { + return res.sendStatus(404); + } + + const rendered = await render(user, post); + rendered['@context'] = context; + + res.json(rendered); +}); + +export default app; diff --git a/src/server/activitypub/user.ts b/src/server/activitypub/user.ts new file mode 100644 index 0000000000..d43a9793d4 --- /dev/null +++ b/src/server/activitypub/user.ts @@ -0,0 +1,40 @@ +import * as express from 'express'; +import config from '../../conf'; +import context from '../../common/remote/activitypub/renderer/context'; +import render from '../../common/remote/activitypub/renderer/person'; +import parseAcct from '../../common/user/parse-acct'; +import User from '../../models/user'; + +const app = express(); +app.disable('x-powered-by'); + +app.get('/@:user', async (req, res, next) => { + const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']); + if (!(['application/activity+json', 'application/ld+json'] as Array<any>).includes(accepted)) { + return next(); + } + + const { username, host } = parseAcct(req.params.user); + if (host !== null) { + return res.sendStatus(422); + } + + const user = await User.findOne({ + usernameLower: username.toLowerCase(), + host: null + }); + if (user === null) { + return res.sendStatus(404); + } + + if (username !== user.username) { + return res.redirect(`${config.url}/@${user.username}`); + } + + const rendered = render(user); + rendered['@context'] = context; + + res.json(rendered); +}); + +export default app; diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 9cd8716fe5..f7b37193b8 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -26,7 +26,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (usernameErr) return rej('invalid username param'); // Get 'host' parameter - const [host, hostErr] = $(params.host).optional.string().$; + const [host, hostErr] = $(params.host).nullable.optional.string().$; if (hostErr) return rej('invalid host param'); if (userId === undefined && typeof username !== 'string') { diff --git a/src/server/index.ts b/src/server/index.ts index fe22d9c9b3..1874790116 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -9,6 +9,8 @@ import * as express from 'express'; import * as morgan from 'morgan'; import Accesses from 'accesses'; +import activityPub from './activitypub'; +import webFinger from './webfinger'; import log from './log-request'; import config from '../conf'; @@ -53,6 +55,8 @@ app.use((req, res, next) => { */ app.use('/api', require('./api')); app.use('/files', require('./file')); +app.use(activityPub); +app.use(webFinger); app.use(require('./web')); function createServer() { diff --git a/src/server/webfinger.ts b/src/server/webfinger.ts new file mode 100644 index 0000000000..864bb4af52 --- /dev/null +++ b/src/server/webfinger.ts @@ -0,0 +1,47 @@ +import config from '../conf'; +import parseAcct from '../common/user/parse-acct'; +import User from '../models/user'; +const express = require('express'); + +const app = express(); + +app.get('/.well-known/webfinger', async (req, res) => { + if (typeof req.query.resource !== 'string') { + return res.sendStatus(400); + } + + const resourceLower = req.query.resource.toLowerCase(); + const webPrefix = config.url.toLowerCase() + '/@'; + let acctLower; + + if (resourceLower.startsWith(webPrefix)) { + acctLower = resourceLower.slice(webPrefix.length); + } else if (resourceLower.startsWith('acct:')) { + acctLower = resourceLower.slice('acct:'.length); + } else { + acctLower = resourceLower; + } + + const parsedAcctLower = parseAcct(acctLower); + if (![null, config.host.toLowerCase()].includes(parsedAcctLower.host)) { + return res.sendStatus(422); + } + + const user = await User.findOne({ usernameLower: parsedAcctLower.username, host: null }); + if (user === null) { + return res.sendStatus(404); + } + + return res.json({ + subject: `acct:${user.username}@${config.host}`, + links: [ + { + rel: 'self', + type: 'application/activity+json', + href: `${config.url}/@${user.username}` + } + ] + }); +}); + +export default app; diff --git a/test/api.js b/test/api.js index c2c08dd95d..9627308362 100644 --- a/test/api.js +++ b/test/api.js @@ -17,7 +17,7 @@ const should = _chai.should(); _chai.use(chaiHttp); -const server = require('../built/server/api/server'); +const server = require('../built/server/api'); const db = require('../built/db/mongodb').default; const async = fn => (done) => { @@ -46,12 +46,12 @@ describe('API', () => { beforeEach(() => Promise.all([ db.get('users').drop(), db.get('posts').drop(), - db.get('drive_files.files').drop(), - db.get('drive_files.chunks').drop(), - db.get('drive_folders').drop(), + db.get('driveFiles.files').drop(), + db.get('driveFiles.chunks').drop(), + db.get('driveFolders').drop(), db.get('apps').drop(), - db.get('access_tokens').drop(), - db.get('auth_sessions').drop() + db.get('accessTokens').drop(), + db.get('authSessions').drop() ])); it('greet server', done => { @@ -195,7 +195,7 @@ describe('API', () => { it('ユーザーが取得できる', async(async () => { const me = await insertSakurako(); const res = await request('/users/show', { - user_id: me._id.toString() + userId: me._id.toString() }, me); res.should.have.status(200); res.body.should.be.a('object'); @@ -204,14 +204,14 @@ describe('API', () => { it('ユーザーが存在しなかったら怒る', async(async () => { const res = await request('/users/show', { - user_id: '000000000000000000000000' + userId: '000000000000000000000000' }); res.should.have.status(400); })); it('間違ったIDで怒られる', async(async () => { const res = await request('/users/show', { - user_id: 'kyoppie' + userId: 'kyoppie' }); res.should.have.status(400); })); @@ -226,32 +226,32 @@ describe('API', () => { const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('created_post'); - res.body.created_post.should.have.property('text').eql(post.text); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('text').eql(post.text); })); it('ファイルを添付できる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const res = await request('/posts/create', { - media_ids: [file._id.toString()] + mediaIds: [file._id.toString()] }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('created_post'); - res.body.created_post.should.have.property('media_ids').eql([file._id.toString()]); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('mediaIds').eql([file._id.toString()]); })); it('他人のファイルは添付できない', async(async () => { const me = await insertSakurako(); const hima = await insertHimawari(); const file = await insertDriveFile({ - user_id: hima._id + userId: hima._id }); const res = await request('/posts/create', { - media_ids: [file._id.toString()] + mediaIds: [file._id.toString()] }, me); res.should.have.status(400); })); @@ -259,7 +259,7 @@ describe('API', () => { it('存在しないファイルは添付できない', async(async () => { const me = await insertSakurako(); const res = await request('/posts/create', { - media_ids: ['000000000000000000000000'] + mediaIds: ['000000000000000000000000'] }, me); res.should.have.status(400); })); @@ -267,7 +267,7 @@ describe('API', () => { it('不正なファイルIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/posts/create', { - media_ids: ['kyoppie'] + mediaIds: ['kyoppie'] }, me); res.should.have.status(400); })); @@ -275,65 +275,65 @@ describe('API', () => { it('返信できる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); const post = { text: 'さく', - reply_id: himaPost._id.toString() + replyId: himaPost._id.toString() }; const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('created_post'); - res.body.created_post.should.have.property('text').eql(post.text); - res.body.created_post.should.have.property('reply_id').eql(post.reply_id); - res.body.created_post.should.have.property('reply'); - res.body.created_post.reply.should.have.property('text').eql(himaPost.text); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('text').eql(post.text); + res.body.createdPost.should.have.property('replyId').eql(post.replyId); + res.body.createdPost.should.have.property('reply'); + res.body.createdPost.reply.should.have.property('text').eql(himaPost.text); })); it('repostできる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'こらっさくらこ!' }); const me = await insertSakurako(); const post = { - repost_id: himaPost._id.toString() + repostId: himaPost._id.toString() }; const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('created_post'); - res.body.created_post.should.have.property('repost_id').eql(post.repost_id); - res.body.created_post.should.have.property('repost'); - res.body.created_post.repost.should.have.property('text').eql(himaPost.text); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('repostId').eql(post.repostId); + res.body.createdPost.should.have.property('repost'); + res.body.createdPost.repost.should.have.property('text').eql(himaPost.text); })); it('引用repostできる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'こらっさくらこ!' }); const me = await insertSakurako(); const post = { text: 'さく', - repost_id: himaPost._id.toString() + repostId: himaPost._id.toString() }; const res = await request('/posts/create', post, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('created_post'); - res.body.created_post.should.have.property('text').eql(post.text); - res.body.created_post.should.have.property('repost_id').eql(post.repost_id); - res.body.created_post.should.have.property('repost'); - res.body.created_post.repost.should.have.property('text').eql(himaPost.text); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('text').eql(post.text); + res.body.createdPost.should.have.property('repostId').eql(post.repostId); + res.body.createdPost.should.have.property('repost'); + res.body.createdPost.repost.should.have.property('text').eql(himaPost.text); })); it('文字数ぎりぎりで怒られない', async(async () => { @@ -358,7 +358,7 @@ describe('API', () => { const me = await insertSakurako(); const post = { text: 'さく', - reply_id: '000000000000000000000000' + replyId: '000000000000000000000000' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -367,7 +367,7 @@ describe('API', () => { it('存在しないrepost対象で怒られる', async(async () => { const me = await insertSakurako(); const post = { - repost_id: '000000000000000000000000' + repostId: '000000000000000000000000' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -377,7 +377,7 @@ describe('API', () => { const me = await insertSakurako(); const post = { text: 'さく', - reply_id: 'kyoppie' + replyId: 'kyoppie' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -386,7 +386,7 @@ describe('API', () => { it('不正なrepost対象IDで怒られる', async(async () => { const me = await insertSakurako(); const post = { - repost_id: 'kyoppie' + repostId: 'kyoppie' }; const res = await request('/posts/create', post, me); res.should.have.status(400); @@ -402,8 +402,8 @@ describe('API', () => { }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('created_post'); - res.body.created_post.should.have.property('poll'); + res.body.should.have.property('createdPost'); + res.body.createdPost.should.have.property('poll'); })); it('投票の選択肢が無くて怒られる', async(async () => { @@ -439,11 +439,11 @@ describe('API', () => { it('投稿が取得できる', async(async () => { const me = await insertSakurako(); const myPost = await db.get('posts').insert({ - user_id: me._id, + userId: me._id, text: 'お腹ペコい' }); const res = await request('/posts/show', { - post_id: myPost._id.toString() + postId: myPost._id.toString() }, me); res.should.have.status(200); res.body.should.be.a('object'); @@ -452,14 +452,14 @@ describe('API', () => { it('投稿が存在しなかったら怒る', async(async () => { const res = await request('/posts/show', { - post_id: '000000000000000000000000' + postId: '000000000000000000000000' }); res.should.have.status(400); })); it('間違ったIDで怒られる', async(async () => { const res = await request('/posts/show', { - post_id: 'kyoppie' + postId: 'kyoppie' }); res.should.have.status(400); })); @@ -469,13 +469,13 @@ describe('API', () => { it('リアクションできる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); const res = await request('/posts/reactions/create', { - post_id: himaPost._id.toString(), + postId: himaPost._id.toString(), reaction: 'like' }, me); res.should.have.status(204); @@ -484,12 +484,12 @@ describe('API', () => { it('自分の投稿にはリアクションできない', async(async () => { const me = await insertSakurako(); const myPost = await db.get('posts').insert({ - user_id: me._id, + userId: me._id, text: 'お腹ペコい' }); const res = await request('/posts/reactions/create', { - post_id: myPost._id.toString(), + postId: myPost._id.toString(), reaction: 'like' }, me); res.should.have.status(400); @@ -498,19 +498,19 @@ describe('API', () => { it('二重にリアクションできない', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); - await db.get('post_reactions').insert({ - user_id: me._id, - post_id: himaPost._id, + await db.get('postReactions').insert({ + userId: me._id, + postId: himaPost._id, reaction: 'like' }); const res = await request('/posts/reactions/create', { - post_id: himaPost._id.toString(), + postId: himaPost._id.toString(), reaction: 'like' }, me); res.should.have.status(400); @@ -519,7 +519,7 @@ describe('API', () => { it('存在しない投稿にはリアクションできない', async(async () => { const me = await insertSakurako(); const res = await request('/posts/reactions/create', { - post_id: '000000000000000000000000', + postId: '000000000000000000000000', reaction: 'like' }, me); res.should.have.status(400); @@ -534,7 +534,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/posts/reactions/create', { - post_id: 'kyoppie', + postId: 'kyoppie', reaction: 'like' }, me); res.should.have.status(400); @@ -545,19 +545,19 @@ describe('API', () => { it('リアクションをキャンセルできる', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); - await db.get('post_reactions').insert({ - user_id: me._id, - post_id: himaPost._id, + await db.get('postReactions').insert({ + userId: me._id, + postId: himaPost._id, reaction: 'like' }); const res = await request('/posts/reactions/delete', { - post_id: himaPost._id.toString() + postId: himaPost._id.toString() }, me); res.should.have.status(204); })); @@ -565,13 +565,13 @@ describe('API', () => { it('リアクションしていない投稿はリアクションをキャンセルできない', async(async () => { const hima = await insertHimawari(); const himaPost = await db.get('posts').insert({ - user_id: hima._id, + userId: hima._id, text: 'ひま' }); const me = await insertSakurako(); const res = await request('/posts/reactions/delete', { - post_id: himaPost._id.toString() + postId: himaPost._id.toString() }, me); res.should.have.status(400); })); @@ -579,7 +579,7 @@ describe('API', () => { it('存在しない投稿はリアクションをキャンセルできない', async(async () => { const me = await insertSakurako(); const res = await request('/posts/reactions/delete', { - post_id: '000000000000000000000000' + postId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -593,7 +593,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/posts/reactions/delete', { - post_id: 'kyoppie' + postId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -604,7 +604,7 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); const res = await request('/following/create', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(204); })); @@ -613,12 +613,12 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id, - deleted_at: new Date() + followeeId: hima._id, + followerId: me._id, + deletedAt: new Date() }); const res = await request('/following/create', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(204); })); @@ -627,11 +627,11 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id + followeeId: hima._id, + followerId: me._id }); const res = await request('/following/create', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(400); })); @@ -639,7 +639,7 @@ describe('API', () => { it('存在しないユーザーはフォローできない', async(async () => { const me = await insertSakurako(); const res = await request('/following/create', { - user_id: '000000000000000000000000' + userId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -647,7 +647,7 @@ describe('API', () => { it('自分自身はフォローできない', async(async () => { const me = await insertSakurako(); const res = await request('/following/create', { - user_id: me._id.toString() + userId: me._id.toString() }, me); res.should.have.status(400); })); @@ -661,7 +661,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/following/create', { - user_id: 'kyoppie' + userId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -672,11 +672,11 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id + followeeId: hima._id, + followerId: me._id }); const res = await request('/following/delete', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(204); })); @@ -685,16 +685,16 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id, - deleted_at: new Date() + followeeId: hima._id, + followerId: me._id, + deletedAt: new Date() }); await db.get('following').insert({ - followee_id: hima._id, - follower_id: me._id + followeeId: hima._id, + followerId: me._id }); const res = await request('/following/delete', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(204); })); @@ -703,7 +703,7 @@ describe('API', () => { const hima = await insertHimawari(); const me = await insertSakurako(); const res = await request('/following/delete', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(400); })); @@ -711,7 +711,7 @@ describe('API', () => { it('存在しないユーザーはフォロー解除できない', async(async () => { const me = await insertSakurako(); const res = await request('/following/delete', { - user_id: '000000000000000000000000' + userId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -719,7 +719,7 @@ describe('API', () => { it('自分自身はフォロー解除できない', async(async () => { const me = await insertSakurako(); const res = await request('/following/delete', { - user_id: me._id.toString() + userId: me._id.toString() }, me); res.should.have.status(400); })); @@ -733,7 +733,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/following/delete', { - user_id: 'kyoppie' + userId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -743,15 +743,15 @@ describe('API', () => { it('ドライブ情報を取得できる', async(async () => { const me = await insertSakurako(); await insertDriveFile({ - user_id: me._id, + userId: me._id, datasize: 256 }); await insertDriveFile({ - user_id: me._id, + userId: me._id, datasize: 512 }); await insertDriveFile({ - user_id: me._id, + userId: me._id, datasize: 1024 }); const res = await request('/drive', {}, me); @@ -784,11 +784,11 @@ describe('API', () => { it('名前を更新できる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const newName = 'いちごパスタ.png'; const res = await request('/drive/files/update', { - file_id: file._id.toString(), + fileId: file._id.toString(), name: newName }, me); res.should.have.status(200); @@ -800,10 +800,10 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const file = await insertDriveFile({ - user_id: hima._id + userId: hima._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), + fileId: file._id.toString(), name: 'いちごパスタ.png' }, me); res.should.have.status(400); @@ -812,47 +812,47 @@ describe('API', () => { it('親フォルダを更新できる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const folder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: folder._id.toString() + fileId: file._id.toString(), + folderId: folder._id.toString() }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('folder_id').eql(folder._id.toString()); + res.body.should.have.property('folderId').eql(folder._id.toString()); })); it('親フォルダを無しにできる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id, - folder_id: '000000000000000000000000' + userId: me._id, + folderId: '000000000000000000000000' }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: null + fileId: file._id.toString(), + folderId: null }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('folder_id').eql(null); + res.body.should.have.property('folderId').eql(null); })); it('他人のフォルダには入れられない', async(async () => { const me = await insertSakurako(); const hima = await insertHimawari(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const folder = await insertDriveFolder({ - user_id: hima._id + userId: hima._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: folder._id.toString() + fileId: file._id.toString(), + folderId: folder._id.toString() }, me); res.should.have.status(400); })); @@ -860,11 +860,11 @@ describe('API', () => { it('存在しないフォルダで怒られる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: '000000000000000000000000' + fileId: file._id.toString(), + folderId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -872,11 +872,11 @@ describe('API', () => { it('不正なフォルダIDで怒られる', async(async () => { const me = await insertSakurako(); const file = await insertDriveFile({ - user_id: me._id + userId: me._id }); const res = await request('/drive/files/update', { - file_id: file._id.toString(), - folder_id: 'kyoppie' + fileId: file._id.toString(), + folderId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -884,7 +884,7 @@ describe('API', () => { it('ファイルが存在しなかったら怒る', async(async () => { const me = await insertSakurako(); const res = await request('/drive/files/update', { - file_id: '000000000000000000000000', + fileId: '000000000000000000000000', name: 'いちごパスタ.png' }, me); res.should.have.status(400); @@ -893,7 +893,7 @@ describe('API', () => { it('間違ったIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/drive/files/update', { - file_id: 'kyoppie', + fileId: 'kyoppie', name: 'いちごパスタ.png' }, me); res.should.have.status(400); @@ -916,10 +916,10 @@ describe('API', () => { it('名前を更新できる', async(async () => { const me = await insertSakurako(); const folder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), + folderId: folder._id.toString(), name: 'new name' }, me); res.should.have.status(200); @@ -931,10 +931,10 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const folder = await insertDriveFolder({ - user_id: hima._id + userId: hima._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), + folderId: folder._id.toString(), name: 'new name' }, me); res.should.have.status(400); @@ -943,47 +943,47 @@ describe('API', () => { it('親フォルダを更新できる', async(async () => { const me = await insertSakurako(); const folder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const parentFolder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: parentFolder._id.toString() + folderId: folder._id.toString(), + parentId: parentFolder._id.toString() }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('parent_id').eql(parentFolder._id.toString()); + res.body.should.have.property('parentId').eql(parentFolder._id.toString()); })); it('親フォルダを無しに更新できる', async(async () => { const me = await insertSakurako(); const folder = await insertDriveFolder({ - user_id: me._id, - parent_id: '000000000000000000000000' + userId: me._id, + parentId: '000000000000000000000000' }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: null + folderId: folder._id.toString(), + parentId: null }, me); res.should.have.status(200); res.body.should.be.a('object'); - res.body.should.have.property('parent_id').eql(null); + res.body.should.have.property('parentId').eql(null); })); it('他人のフォルダを親フォルダに設定できない', async(async () => { const me = await insertSakurako(); const hima = await insertHimawari(); const folder = await insertDriveFolder({ - user_id: me._id + userId: me._id }); const parentFolder = await insertDriveFolder({ - user_id: hima._id + userId: hima._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: parentFolder._id.toString() + folderId: folder._id.toString(), + parentId: parentFolder._id.toString() }, me); res.should.have.status(400); })); @@ -992,11 +992,11 @@ describe('API', () => { const me = await insertSakurako(); const folder = await insertDriveFolder(); const parentFolder = await insertDriveFolder({ - parent_id: folder._id + parentId: folder._id }); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: parentFolder._id.toString() + folderId: folder._id.toString(), + parentId: parentFolder._id.toString() }, me); res.should.have.status(400); })); @@ -1005,14 +1005,14 @@ describe('API', () => { const me = await insertSakurako(); const folderA = await insertDriveFolder(); const folderB = await insertDriveFolder({ - parent_id: folderA._id + parentId: folderA._id }); const folderC = await insertDriveFolder({ - parent_id: folderB._id + parentId: folderB._id }); const res = await request('/drive/folders/update', { - folder_id: folderA._id.toString(), - parent_id: folderC._id.toString() + folderId: folderA._id.toString(), + parentId: folderC._id.toString() }, me); res.should.have.status(400); })); @@ -1021,8 +1021,8 @@ describe('API', () => { const me = await insertSakurako(); const folder = await insertDriveFolder(); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: '000000000000000000000000' + folderId: folder._id.toString(), + parentId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -1031,8 +1031,8 @@ describe('API', () => { const me = await insertSakurako(); const folder = await insertDriveFolder(); const res = await request('/drive/folders/update', { - folder_id: folder._id.toString(), - parent_id: 'kyoppie' + folderId: folder._id.toString(), + parentId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -1040,7 +1040,7 @@ describe('API', () => { it('存在しないフォルダを更新できない', async(async () => { const me = await insertSakurako(); const res = await request('/drive/folders/update', { - folder_id: '000000000000000000000000' + folderId: '000000000000000000000000' }, me); res.should.have.status(400); })); @@ -1048,7 +1048,7 @@ describe('API', () => { it('不正なフォルダIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/drive/folders/update', { - folder_id: 'kyoppie' + folderId: 'kyoppie' }, me); res.should.have.status(400); })); @@ -1059,7 +1059,7 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const res = await request('/messaging/messages/create', { - user_id: hima._id.toString(), + userId: hima._id.toString(), text: 'Hey hey ひまわり' }, me); res.should.have.status(200); @@ -1070,7 +1070,7 @@ describe('API', () => { it('自分自身にはメッセージを送信できない', async(async () => { const me = await insertSakurako(); const res = await request('/messaging/messages/create', { - user_id: me._id.toString(), + userId: me._id.toString(), text: 'Yo' }, me); res.should.have.status(400); @@ -1079,7 +1079,7 @@ describe('API', () => { it('存在しないユーザーにはメッセージを送信できない', async(async () => { const me = await insertSakurako(); const res = await request('/messaging/messages/create', { - user_id: '000000000000000000000000', + userId: '000000000000000000000000', text: 'Yo' }, me); res.should.have.status(400); @@ -1088,7 +1088,7 @@ describe('API', () => { it('不正なユーザーIDで怒られる', async(async () => { const me = await insertSakurako(); const res = await request('/messaging/messages/create', { - user_id: 'kyoppie', + userId: 'kyoppie', text: 'Yo' }, me); res.should.have.status(400); @@ -1098,7 +1098,7 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const res = await request('/messaging/messages/create', { - user_id: hima._id.toString() + userId: hima._id.toString() }, me); res.should.have.status(400); })); @@ -1107,7 +1107,7 @@ describe('API', () => { const me = await insertSakurako(); const hima = await insertHimawari(); const res = await request('/messaging/messages/create', { - user_id: hima._id.toString(), + userId: hima._id.toString(), text: '!'.repeat(1001) }, me); res.should.have.status(400); @@ -1118,7 +1118,7 @@ describe('API', () => { it('認証セッションを作成できる', async(async () => { const app = await insertApp(); const res = await request('/auth/session/generate', { - app_secret: app.secret + appSecret: app.secret }); res.should.have.status(200); res.body.should.be.a('object'); @@ -1126,14 +1126,14 @@ describe('API', () => { res.body.should.have.property('url'); })); - it('app_secret 無しで怒られる', async(async () => { + it('appSecret 無しで怒られる', async(async () => { const res = await request('/auth/session/generate', {}); res.should.have.status(400); })); - it('誤った app secret で怒られる', async(async () => { + it('誤った appSecret で怒られる', async(async () => { const res = await request('/auth/session/generate', { - app_secret: 'kyoppie' + appSecret: 'kyoppie' }); res.should.have.status(400); })); @@ -1159,14 +1159,14 @@ function deepAssign(destination, ...sources) { function insertSakurako(opts) { return db.get('users').insert(deepAssign({ username: 'sakurako', - username_lower: 'sakurako', + usernameLower: 'sakurako', account: { keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n', token: '!00000000000000000000000000000000', password: '$2a$08$FnHXg3tP.M/kINWgQSXNqeoBsiVrkj.ecXX8mW9rfBzMRkibYfjYy', // HimawariDaisuki06160907 profile: {}, settings: {}, - client_settings: {} + clientSettings: {} } }, opts)); } @@ -1174,20 +1174,20 @@ function insertSakurako(opts) { function insertHimawari(opts) { return db.get('users').insert(deepAssign({ username: 'himawari', - username_lower: 'himawari', + usernameLower: 'himawari', account: { keypair: '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAtdTG9rlFWjNqhgbg2V6X5XF1WpQXZS3KNXykEWl2UAiMyfVV\nBvf3zQP0dDEdNtcqdPJgis03bpiHCzQusc/YLyHYB0m+TJXsxJatb8cqUogOFeE4\ngQ4Dc5kAT6gLh/d4yz03EIg9bizX07EiGWnZqWxb+21ypqsPxST64sAtG9f5O/G4\nXe2m3cSbfAAvEUP1Ig1LUNyJB4jhM60w1cQic/qO8++sk/+GoX9g71X+i4NArGv+\n1c11acDIIPGAAQpFeYVeGaKakNDNp8RtJJp8R8FLwJXZ4/gATBnScCiHUSrGfRly\nYyR0w/BNlQ6/NijAdB9pR5csPvyIPkx1gauZewIDAQABAoIBAQCwWf/mhuY2h6uG\n9eDZsZ7Mj2/sO7k9Dl4R5iMSKCDxmnlB3slqitExa+aJUqEs8R5icjkkJcjfYNuJ\nCEFJf3YCsGZfGyyQBtCuEh2ATcBEb2SJ3/f3YuoCEaB1oVwdsOzc4TAovpol4yQo\nUqHp1/mdElVb01jhQQN4h1c02IJnfzvfU1C8szBni+Etfd+MxqGfv006DY3KOEb3\nlCrCS3GmooJW2Fjj7q1kCcaEQbMB1/aQHLXd1qe3KJOzXh3Voxsp/jEH0hvp2TII\nfY9UK+b7mA+xlvXwKuTkHVaZm0ylg0nbembS8MF4GfFMujinSexvLrVKaQhdMFoF\nvBLxHYHRAoGBANfNVYJYeCDPFNLmak5Xg33Rfvc2II8UmrZOVdhOWs8ZK0pis9e+\nPo2MKtTzrzipXI2QXv5w7kO+LJWNDva+xRlW8Wlj9Dde9QdQ7Y8+dk7SJgf24DzM\n023elgX5DvTeLODjStk6SMPRL0FmGovUqAAA8ZeHtJzkIr1HROWnQiwnAoGBANez\nhFwKnVoQu0RpBz/i4W0RKIxOwltN2zmlN8KjJPhSy00A7nBUfKLRbcwiSHE98Yi/\nUrXwMwR5QeD2ngngRppddJnpiRfjNjnsaqeqNtpO8AxB3XjpCC5zmHUMFHKvPpDj\n1zU/F44li0YjKcMBebZy9PbfAjrIgJfxhPo/oXiNAoGAfx6gaTjOAp2ZaaZ7Jozc\nkyft/5et1DrR6+P3I4T8bxQncRj1UXfqhxzzOiAVrm3tbCKIIp/JarRCtRGzp9u2\nZPfXGzra6CcSdW3Rkli7/jBCYNynOIl7XjQI8ZnFmq6phwu80ntH07mMeZy4tHff\nQqlLpvQ0i1rDr/Wkexdsnm8CgYBgxha9ILoF/Xm3MJPjEsxmnYsen/tM8XpIu5pv\nxbhBfQvfKWrQlOcyOVnUexEbVVo3KvdVz0VkXW60GpE/BxNGEGXO49rxD6x1gl87\nh/+CJGZIaYiOxaY5CP2+jcPizEL6yG32Yq8TxD5fIkmLRu8vbxX+aIFclDY1dVNe\n3wt3xQKBgGEL0EjwRch+P2V+YHAhbETPrEqJjHRWT95pIdF9XtC8fasSOVH81cLX\nXXsX1FTvOJNwG9Nk8rQjYJXGTb2O/2unaazlYUwxKwVpwuGzz/vhH/roHZBAkIVT\njvpykpn9QMezEdpzj5BEv01QzSYBPzIh5myrpoJIoSW7py7zFG3h\n-----END RSA PRIVATE KEY-----\n', token: '!00000000000000000000000000000001', password: '$2a$08$OPESxR2RE/ZijjGanNKk6ezSqGFitqsbZqTjWUZPLhORMKxHCbc4O', // ilovesakurako profile: {}, settings: {}, - client_settings: {} + clientSettings: {} } }, opts)); } function insertDriveFile(opts) { - return db.get('drive_files.files').insert({ + return db.get('driveFiles.files').insert({ length: opts.datasize, filename: 'strawberry-pasta.png', metadata: opts @@ -1195,9 +1195,9 @@ function insertDriveFile(opts) { } function insertDriveFolder(opts) { - return db.get('drive_folders').insert(deepAssign({ + return db.get('driveFolders').insert(deepAssign({ name: 'my folder', - parent_id: null + parentId: null }, opts)); } diff --git a/test/text.js b/test/text.js index 4f739cc1b1..1c034d6338 100644 --- a/test/text.js +++ b/test/text.js @@ -4,8 +4,8 @@ const assert = require('assert'); -const analyze = require('../built/server/api/common/text').default; -const syntaxhighlighter = require('../built/server/api/common/text/core/syntax-highlighter').default; +const analyze = require('../built/common/text/parse').default; +const syntaxhighlighter = require('../built/common/text/core/syntax-highlighter').default; describe('Text', () => { it('can be analyzed', () => { diff --git a/tools/letsencrypt/get-cert.sh b/tools/letsencrypt/get-cert.sh deleted file mode 100644 index d44deb1443..0000000000 --- a/tools/letsencrypt/get-cert.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -certbot certonly --standalone\ - -d $1\ - -d api.$1\ - -d auth.$1\ - -d docs.$1\ - -d ch.$1\ - -d stats.$1\ - -d status.$1\ - -d dev.$1\ - -d file.$2\ diff --git a/tools/migration/nighthike/6.js b/tools/migration/nighthike/6.js index ff78df4e09..8b97e9f7b3 100644 --- a/tools/migration/nighthike/6.js +++ b/tools/migration/nighthike/6.js @@ -1 +1,13 @@ -db.posts.update({ mediaIds: null }, { $set: { mediaIds: [] } }, false, true); +db.posts.update({ + $or: [{ + mediaIds: null + }, { + mediaIds: { + $exist: false + } + }] +}, { + $set: { + mediaIds: [] + } +}, false, true); diff --git a/tools/migration/nighthike/7.js b/tools/migration/nighthike/7.js index c5055da8ba..c8efb8952b 100644 --- a/tools/migration/nighthike/7.js +++ b/tools/migration/nighthike/7.js @@ -1,16 +1,40 @@ -// for Node.js interpretation +// for Node.js interpret -const Message = require('../../../built/models/messaging-message').default; -const Post = require('../../../built/models/post').default; +const { default: Post } = require('../../../built/api/models/post'); +const { default: zip } = require('@prezzemolo/zip') const html = require('../../../built/common/text/html').default; const parse = require('../../../built/common/text/parse').default; -Promise.all([Message, Post].map(async model => { - const documents = await model.find(); - - return Promise.all(documents.map(({ _id, text }) => model.update(_id, { +const migrate = async (post) => { + const result = await Post.update(post._id, { $set: { - textHtml: html(parse(text)) + textHtml: post.text ? html(parse(post.text)) : null } - }))); -})).catch(console.error).then(process.exit); + }); + return result.ok === 1; +} + +async function main() { + const count = await Post.count({}); + + const dop = Number.parseInt(process.argv[2]) || 5 + const idop = ((count - (count % dop)) / dop) + 1 + + return zip( + 1, + async (time) => { + console.log(`${time} / ${idop}`) + const doc = await Post.find({}, { + limit: dop, skip: time * dop + }) + return Promise.all(doc.map(migrate)) + }, + idop + ).then(a => { + const rv = [] + a.forEach(e => rv.push(...e)) + return rv + }) +} + +main().then(console.dir).catch(console.error) diff --git a/tools/migration/node.2018-03-13.othello.js b/tools/migration/nighthike/8.js similarity index 55% rename from tools/migration/node.2018-03-13.othello.js rename to tools/migration/nighthike/8.js index 4598f8d832..5e0cf95078 100644 --- a/tools/migration/node.2018-03-13.othello.js +++ b/tools/migration/nighthike/8.js @@ -1,27 +1,21 @@ // for Node.js interpret -const { default: Othello } = require('../../built/api/models/othello-game') +const { default: Message } = require('../../../built/api/models/message'); const { default: zip } = require('@prezzemolo/zip') +const html = require('../../../built/common/text/html').default; +const parse = require('../../../built/common/text/parse').default; -const migrate = async (doc) => { - const x = {}; - - doc.logs.forEach(log => { - log.color = log.color == 'black'; - }); - - const result = await Othello.update(doc._id, { +const migrate = async (message) => { + const result = await Message.update(message._id, { $set: { - logs: doc.logs + textHtml: message.text ? html(parse(message.text)) : null } }); - return result.ok === 1; } async function main() { - - const count = await Othello.count({}); + const count = await Message.count({}); const dop = Number.parseInt(process.argv[2]) || 5 const idop = ((count - (count % dop)) / dop) + 1 @@ -30,7 +24,7 @@ async function main() { 1, async (time) => { console.log(`${time} / ${idop}`) - const doc = await Othello.find({}, { + const doc = await Message.find({}, { limit: dop, skip: time * dop }) return Promise.all(doc.map(migrate)) diff --git a/tools/migration/node.1509958623.use-gridfs.js b/tools/migration/node.1509958623.use-gridfs.js deleted file mode 100644 index a9d2b12e95..0000000000 --- a/tools/migration/node.1509958623.use-gridfs.js +++ /dev/null @@ -1,71 +0,0 @@ -// for Node.js interpret - -const { default: db } = require('../../built/db/mongodb') -const { default: DriveFile, getGridFSBucket } = require('../../built/api/models/drive-file') -const { Duplex } = require('stream') -const { default: zip } = require('@prezzemolo/zip') - -const writeToGridFS = (bucket, buffer, ...rest) => new Promise((resolve, reject) => { - const writeStream = bucket.openUploadStreamWithId(...rest) - - const dataStream = new Duplex() - dataStream.push(buffer) - dataStream.push(null) - - writeStream.once('finish', resolve) - writeStream.on('error', reject) - - dataStream.pipe(writeStream) -}) - -const migrateToGridFS = async (doc) => { - const id = doc._id - const buffer = doc.data ? doc.data.buffer : Buffer.from([0x00]) // アップロードのバグなのか知らないけどなぜか data が存在しない drive_file ドキュメントがまれにあることがわかったので - const created_at = doc.created_at - const name = doc.name - const type = doc.type - - delete doc._id - delete doc.created_at - delete doc.datasize - delete doc.hash - delete doc.data - delete doc.name - delete doc.type - - const bucket = await getGridFSBucket() - const added = await writeToGridFS(bucket, buffer, id, name, { contentType: type, metadata: doc }) - - const result = await DriveFile.update(id, { - $set: { - uploadDate: created_at - } - }) - - return added && result.ok === 1 -} - -async function main() { - const count = await db.get('drive_files').count({}); - - console.log(`there are ${count} files.`) - - const dop = Number.parseInt(process.argv[2]) || 5 - const idop = ((count - (count % dop)) / dop) + 1 - - return zip( - 1, - async (time) => { - console.log(`${time} / ${idop}`) - const doc = await db.get('drive_files').find({}, { limit: dop, skip: time * dop }) - return Promise.all(doc.map(migrateToGridFS)) - }, - idop - ).then(a => { - const rv = [] - a.forEach(e => rv.push(...e)) - return rv - }) -} - -main().then(console.dir).catch(console.error) diff --git a/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js b/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js deleted file mode 100644 index d7b2a6eff4..0000000000 --- a/tools/migration/node.1510016282.change-gridfs-metadata-name-to-filename.js +++ /dev/null @@ -1,50 +0,0 @@ -// for Node.js interpret -/** - * change usage of GridFS filename - * see commit fb422b4d603c53a70712caba55b35a48a8c2e619 - */ - -const { default: DriveFile } = require('../../built/api/models/drive-file') - -async function applyNewChange (doc) { - const result = await DriveFile.update(doc._id, { - $set: { - filename: doc.metadata.name - }, - $unset: { - 'metadata.name': '' - } - }) - return result.ok === 1 -} - -async function main () { - const query = { - 'metadata.name': { - $exists: true - } - } - - const count = await DriveFile.count(query) - - const dop = Number.parseInt(process.argv[2]) || 5 - const idop = ((count - (count % dop)) / dop) + 1 - - return zip( - 1, - async (time) => { - console.log(`${time} / ${idop}`) - const doc = await DriveFile.find(query, { - limit: dop, skip: time * dop - }) - return Promise.all(doc.map(applyNewChange)) - }, - idop - ).then(a => { - const rv = [] - a.forEach(e => rv.push(...e)) - return rv - }) -} - -main().then(console.dir).catch(console.error) diff --git a/tools/migration/node.1510056272.issue_882.js b/tools/migration/node.1510056272.issue_882.js deleted file mode 100644 index 302ef3de65..0000000000 --- a/tools/migration/node.1510056272.issue_882.js +++ /dev/null @@ -1,47 +0,0 @@ -// for Node.js interpret - -const { default: DriveFile } = require('../../built/api/models/drive-file') -const { default: zip } = require('@prezzemolo/zip') - -const migrate = async (doc) => { - const result = await DriveFile.update(doc._id, { - $set: { - contentType: doc.metadata.type - }, - $unset: { - 'metadata.type': '' - } - }) - return result.ok === 1 -} - -async function main() { - const query = { - 'metadata.type': { - $exists: true - } - } - - const count = await DriveFile.count(query); - - const dop = Number.parseInt(process.argv[2]) || 5 - const idop = ((count - (count % dop)) / dop) + 1 - - return zip( - 1, - async (time) => { - console.log(`${time} / ${idop}`) - const doc = await DriveFile.find(query, { - limit: dop, skip: time * dop - }) - return Promise.all(doc.map(migrate)) - }, - idop - ).then(a => { - const rv = [] - a.forEach(e => rv.push(...e)) - return rv - }) -} - -main().then(console.dir).catch(console.error) diff --git a/tools/migration/node.2017-11-08.js b/tools/migration/node.2017-11-08.js deleted file mode 100644 index 196a5a90c8..0000000000 --- a/tools/migration/node.2017-11-08.js +++ /dev/null @@ -1,88 +0,0 @@ -const uuid = require('uuid'); -const { default: User } = require('../../built/api/models/user') -const { default: zip } = require('@prezzemolo/zip') - -const home = { - left: [ - 'profile', - 'calendar', - 'activity', - 'rss-reader', - 'trends', - 'photo-stream', - 'version' - ], - right: [ - 'broadcast', - 'notifications', - 'user-recommendation', - 'recommended-polls', - 'server', - 'donation', - 'nav', - 'tips' - ] -}; - -const migrate = async (doc) => { - - //#region Construct home data - const homeData = []; - - home.left.forEach(widget => { - homeData.push({ - name: widget, - id: uuid(), - place: 'left', - data: {} - }); - }); - - home.right.forEach(widget => { - homeData.push({ - name: widget, - id: uuid(), - place: 'right', - data: {} - }); - }); - //#endregion - - const result = await User.update(doc._id, { - $unset: { - data: '' - }, - $set: { - 'settings': {}, - 'client_settings.home': homeData, - 'client_settings.show_donation': false - } - }) - - return result.ok === 1 -} - -async function main() { - const count = await User.count(); - - console.log(`there are ${count} users.`) - - const dop = Number.parseInt(process.argv[2]) || 5 - const idop = ((count - (count % dop)) / dop) + 1 - - return zip( - 1, - async (time) => { - console.log(`${time} / ${idop}`) - const docs = await User.find({}, { limit: dop, skip: time * dop }) - return Promise.all(docs.map(migrate)) - }, - idop - ).then(a => { - const rv = [] - a.forEach(e => rv.push(...e)) - return rv - }) -} - -main().then(console.dir).catch(console.error) diff --git a/tools/migration/node.2017-12-11.js b/tools/migration/node.2017-12-11.js deleted file mode 100644 index b9686b8b4d..0000000000 --- a/tools/migration/node.2017-12-11.js +++ /dev/null @@ -1,71 +0,0 @@ -// for Node.js interpret - -const { default: DriveFile, getGridFSBucket } = require('../../built/api/models/drive-file') -const { default: zip } = require('@prezzemolo/zip') - -const _gm = require('gm'); -const gm = _gm.subClass({ - imageMagick: true -}); - -const migrate = doc => new Promise(async (res, rej) => { - const bucket = await getGridFSBucket(); - - const readable = bucket.openDownloadStream(doc._id); - - gm(readable) - .setFormat('ppm') - .resize(1, 1) - .toBuffer(async (err, buffer) => { - if (err) { - console.error(err); - res(false); - return; - } - const r = buffer.readUInt8(buffer.length - 3); - const g = buffer.readUInt8(buffer.length - 2); - const b = buffer.readUInt8(buffer.length - 1); - - const result = await DriveFile.update(doc._id, { - $set: { - 'metadata.properties.average_color': [r, g, b] - } - }) - - res(result.ok === 1); - }); -}); - -async function main() { - const query = { - contentType: { - $in: [ - 'image/png', - 'image/jpeg' - ] - } - } - - const count = await DriveFile.count(query); - - const dop = Number.parseInt(process.argv[2]) || 5 - const idop = ((count - (count % dop)) / dop) + 1 - - return zip( - 1, - async (time) => { - console.log(`${time} / ${idop}`) - const doc = await DriveFile.find(query, { - limit: dop, skip: time * dop - }) - return Promise.all(doc.map(migrate)) - }, - idop - ).then(a => { - const rv = [] - a.forEach(e => rv.push(...e)) - return rv - }) -} - -main().then(console.dir).catch(console.error) diff --git a/tools/migration/node.2017-12-22.hiseikika.js b/tools/migration/node.2017-12-22.hiseikika.js deleted file mode 100644 index ff8294c8d1..0000000000 --- a/tools/migration/node.2017-12-22.hiseikika.js +++ /dev/null @@ -1,67 +0,0 @@ -// for Node.js interpret - -const { default: Post } = require('../../built/api/models/post') -const { default: zip } = require('@prezzemolo/zip') - -const migrate = async (post) => { - const x = {}; - if (post.reply_id != null) { - const reply = await Post.findOne({ - _id: post.reply_id - }); - x['_reply.user_id'] = reply.user_id; - } - if (post.repost_id != null) { - const repost = await Post.findOne({ - _id: post.repost_id - }); - x['_repost.user_id'] = repost.user_id; - } - if (post.reply_id != null || post.repost_id != null) { - const result = await Post.update(post._id, { - $set: x, - }); - return result.ok === 1; - } else { - return true; - } -} - -async function main() { - const query = { - $or: [{ - reply_id: { - $exists: true, - $ne: null - } - }, { - repost_id: { - $exists: true, - $ne: null - } - }] - } - - const count = await Post.count(query); - - const dop = Number.parseInt(process.argv[2]) || 5 - const idop = ((count - (count % dop)) / dop) + 1 - - return zip( - 1, - async (time) => { - console.log(`${time} / ${idop}`) - const doc = await Post.find(query, { - limit: dop, skip: time * dop - }) - return Promise.all(doc.map(migrate)) - }, - idop - ).then(a => { - const rv = [] - a.forEach(e => rv.push(...e)) - return rv - }) -} - -main().then(console.dir).catch(console.error) diff --git a/tools/migration/shell.1487734995.user-profile.js b/tools/migration/shell.1487734995.user-profile.js deleted file mode 100644 index e6666319e1..0000000000 --- a/tools/migration/shell.1487734995.user-profile.js +++ /dev/null @@ -1,18 +0,0 @@ -db.users.find({}).forEach(function(user) { - print(user._id); - db.users.update({ _id: user._id }, { - $rename: { - bio: 'description' - }, - $unset: { - location: '', - birthday: '' - }, - $set: { - profile: { - location: user.location || null, - birthday: user.birthday || null - } - } - }, false, false); -}); diff --git a/tools/migration/shell.1489951459.like-to-reactions.js b/tools/migration/shell.1489951459.like-to-reactions.js deleted file mode 100644 index 962a0f00ef..0000000000 --- a/tools/migration/shell.1489951459.like-to-reactions.js +++ /dev/null @@ -1,22 +0,0 @@ -db.users.update({}, { - $unset: { - likes_count: 1, - liked_count: 1 - } -}, false, true) - -db.likes.renameCollection('post_reactions') - -db.post_reactions.update({}, { - $set: { - reaction: 'like' - } -}, false, true) - -db.posts.update({}, { - $rename: { - likes_count: 'reaction_counts.like' - } -}, false, true); - -db.notifications.remove({}) diff --git a/tools/migration/shell.1509507382.reply_to-to-reply.js b/tools/migration/shell.1509507382.reply_to-to-reply.js deleted file mode 100644 index ceb272ebc9..0000000000 --- a/tools/migration/shell.1509507382.reply_to-to-reply.js +++ /dev/null @@ -1,5 +0,0 @@ -db.posts.update({}, { - $rename: { - reply_to_id: 'reply_id' - } -}, false, true);