Merge branch 'master' into greenkeeper/html-minifier-3.5.13

This commit is contained in:
syuilo 2018-04-01 19:57:36 +09:00 committed by GitHub
commit fabda94932
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 923 additions and 924 deletions

View file

@ -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

View file

@ -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",

View file

@ -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';

View file

@ -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);
});
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;
}
},
immediate: true
}
}
});

View 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);
}
});

View file

@ -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>

View 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>

View file

@ -15,7 +15,7 @@
</div>
</header>
<div class="text">
<mk-post-html :html="post.textHtml"/>
<mk-post-html :text="post.text"/>
</div>
</div>
</div>

View file

@ -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>

View file

@ -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;

View file

@ -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

View file

@ -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">

View file

@ -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>

View file

@ -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;

View file

@ -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

View file

@ -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">

View file

@ -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: {

View file

@ -0,0 +1,5 @@
export default [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
{ Hashtag: 'as:Hashtag' }
];

View file

@ -0,0 +1,7 @@
import config from '../../../../conf';
export default ({ _id, contentType }) => ({
type: 'Document',
mediaType: contentType,
url: `${config.drive_url}/${_id}`
});

View file

@ -0,0 +1,7 @@
import config from '../../../../conf';
export default tag => ({
type: 'Hashtag',
href: `${config.url}/search?q=#${encodeURIComponent(tag)}`,
name: '#' + tag
});

View file

@ -0,0 +1,6 @@
import config from '../../../../conf';
export default ({ _id }) => ({
type: 'Image',
url: `${config.drive_url}/${_id}`
});

View 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)
});

View 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)
};
};

View file

@ -0,0 +1,6 @@
export default (id, totalItems, orderedItems) => ({
id,
type: 'OrderedCollection',
totalItems,
orderedItems
});

View 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)
};
};

View file

@ -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,
},
});

View file

@ -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
View file

@ -1 +1,2 @@
export function extractPublic(keypair: String): String;
export function generate(): String;

View file

@ -30,6 +30,7 @@ export type IPost = {
repostId: mongo.ObjectID;
poll: any; // todo
text: string;
tags: string[];
textHtml: string;
cw: string;
userId: mongo.ObjectID;

View file

@ -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 {

View 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;

View 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;

View 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;

View 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;

View 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;

View file

@ -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') {

View file

@ -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
View 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;

View file

@ -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));
}

View file

@ -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', () => {

View file

@ -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\

View file

@ -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);

View file

@ -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)

View file

@ -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))

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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);
});

View file

@ -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({})

View file

@ -1,5 +0,0 @@
db.posts.update({}, {
$rename: {
reply_to_id: 'reply_id'
}
}, false, true);