mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-12-23 19:53:07 +02:00
Merge branch 'master' into greenkeeper/html-minifier-3.5.13
This commit is contained in:
commit
fabda94932
53 changed files with 923 additions and 924 deletions
|
@ -7,7 +7,7 @@ Misskey
|
|||
[![][dependencies-badge]][dependencies-link]
|
||||
[![][himawari-badge]][himasaku]
|
||||
[![][sakurako-badge]][himasaku]
|
||||
[![][agpl-3.0-badge]][AGPL-3.0]
|
||||
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
157
src/client/app/common/views/components/post-html.ts
Normal file
157
src/client/app/common/views/components/post-html.ts
Normal file
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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>
|
57
src/client/app/common/views/components/url.vue
Normal file
57
src/client/app/common/views/components/url.vue
Normal file
|
@ -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>
|
|
@ -15,7 +15,7 @@
|
|||
</div>
|
||||
</header>
|
||||
<div class="text">
|
||||
<mk-post-html :html="post.textHtml"/>
|
||||
<mk-post-html :text="post.text"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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: {
|
||||
|
|
5
src/common/remote/activitypub/renderer/context.ts
Normal file
5
src/common/remote/activitypub/renderer/context.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{ Hashtag: 'as:Hashtag' }
|
||||
];
|
7
src/common/remote/activitypub/renderer/document.ts
Normal file
7
src/common/remote/activitypub/renderer/document.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import config from '../../../../conf';
|
||||
|
||||
export default ({ _id, contentType }) => ({
|
||||
type: 'Document',
|
||||
mediaType: contentType,
|
||||
url: `${config.drive_url}/${_id}`
|
||||
});
|
7
src/common/remote/activitypub/renderer/hashtag.ts
Normal file
7
src/common/remote/activitypub/renderer/hashtag.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import config from '../../../../conf';
|
||||
|
||||
export default tag => ({
|
||||
type: 'Hashtag',
|
||||
href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
|
||||
name: '#' + tag
|
||||
});
|
6
src/common/remote/activitypub/renderer/image.ts
Normal file
6
src/common/remote/activitypub/renderer/image.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import config from '../../../../conf';
|
||||
|
||||
export default ({ _id }) => ({
|
||||
type: 'Image',
|
||||
url: `${config.drive_url}/${_id}`
|
||||
});
|
9
src/common/remote/activitypub/renderer/key.ts
Normal file
9
src/common/remote/activitypub/renderer/key.ts
Normal file
|
@ -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)
|
||||
});
|
44
src/common/remote/activitypub/renderer/note.ts
Normal file
44
src/common/remote/activitypub/renderer/note.ts
Normal file
|
@ -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)
|
||||
};
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export default (id, totalItems, orderedItems) => ({
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
totalItems,
|
||||
orderedItems
|
||||
});
|
20
src/common/remote/activitypub/renderer/person.ts
Normal file
20
src/common/remote/activitypub/renderer/person.ts
Normal file
|
@ -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)
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ const elements = [
|
|||
require('./elements/emoji')
|
||||
];
|
||||
|
||||
export default (source: string) => {
|
||||
export default (source: string): any[] => {
|
||||
|
||||
if (source == '') {
|
||||
return null;
|
||||
|
|
1
src/crypto_key.d.ts
vendored
1
src/crypto_key.d.ts
vendored
|
@ -1 +1,2 @@
|
|||
export function extractPublic(keypair: String): String;
|
||||
export function generate(): String;
|
||||
|
|
|
@ -30,6 +30,7 @@ export type IPost = {
|
|||
repostId: mongo.ObjectID;
|
||||
poll: any; // todo
|
||||
text: string;
|
||||
tags: string[];
|
||||
textHtml: string;
|
||||
cw: string;
|
||||
userId: mongo.ObjectID;
|
||||
|
|
|
@ -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 {
|
||||
|
|
42
src/server/activitypub/inbox.ts
Normal file
42
src/server/activitypub/inbox.ts
Normal file
|
@ -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;
|
16
src/server/activitypub/index.ts
Normal file
16
src/server/activitypub/index.ts
Normal file
|
@ -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;
|
45
src/server/activitypub/outbox.ts
Normal file
45
src/server/activitypub/outbox.ts
Normal file
|
@ -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;
|
44
src/server/activitypub/post.ts
Normal file
44
src/server/activitypub/post.ts
Normal file
|
@ -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;
|
40
src/server/activitypub/user.ts
Normal file
40
src/server/activitypub/user.ts
Normal file
|
@ -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;
|
|
@ -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') {
|
||||
|
|
|
@ -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() {
|
||||
|
|
47
src/server/webfinger.ts
Normal file
47
src/server/webfinger.ts
Normal file
|
@ -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;
|
340
test/api.js
340
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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\
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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);
|
||||
});
|
|
@ -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({})
|
|
@ -1,5 +0,0 @@
|
|||
db.posts.update({}, {
|
||||
$rename: {
|
||||
reply_to_id: 'reply_id'
|
||||
}
|
||||
}, false, true);
|
Loading…
Reference in a new issue