Merge branch 'develop'
This commit is contained in:
commit
fad84203c0
64 changed files with 833 additions and 348 deletions
|
@ -9,11 +9,11 @@
|
|||
<ui-input v-model="iconUrl"><template #icon><fa icon="link"/></template>{{ $t('icon-url') }}</ui-input>
|
||||
<ui-input v-model="mascotImageUrl"><template #icon><fa icon="link"/></template>{{ $t('logo-url') }}</ui-input>
|
||||
<ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input>
|
||||
<ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input>
|
||||
<ui-input v-model="ToSUrl"><template #icon><fa icon="link"/></template>{{ $t('tos-url') }}</ui-input>
|
||||
<ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input>
|
||||
<details>
|
||||
<summary>{{ $t('advanced-config') }}</summary>
|
||||
<ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input>
|
||||
<ui-input v-model="languages"><template #icon><fa icon="language"/></template>{{ $t('languages') }}<template #desc>{{ $t('languages-desc') }}</template></ui-input>
|
||||
<ui-input v-model="repositoryUrl"><template #icon><fa icon="link"/></template>{{ $t('repository-url') }}</ui-input>
|
||||
<ui-input v-model="feedbackUrl"><template #icon><fa icon="link"/></template>{{ $t('feedback-url') }}</ui-input>
|
||||
</details>
|
||||
|
@ -159,6 +159,7 @@
|
|||
<ui-switch v-model="enableRecaptcha">{{ $t('enable-recaptcha') }}</ui-switch>
|
||||
<template v-if="enableRecaptcha">
|
||||
<ui-info>{{ $t('recaptcha-info') }}</ui-info>
|
||||
<ui-info warn>{{ $t('recaptcha-info2') }}</ui-info>
|
||||
<ui-horizon-group inputs>
|
||||
<ui-input v-model="recaptchaSiteKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-site-key') }}</ui-input>
|
||||
<ui-input v-model="recaptchaSecretKey" :disabled="!enableRecaptcha"><template #icon><fa icon="key"/></template>{{ $t('recaptcha-secret-key') }}</ui-input>
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
</details>
|
||||
</code>
|
||||
</div>
|
||||
|
||||
<ui-button @click="deleteAll()">{{ $t('delete-all') }}</ui-button>
|
||||
</section>
|
||||
</ui-card>
|
||||
</div>
|
||||
|
@ -78,6 +80,15 @@ export default Vue.extend({
|
|||
}).then(logs => {
|
||||
this.logs = logs.reverse();
|
||||
});
|
||||
},
|
||||
|
||||
deleteAll() {
|
||||
this.$root.api('admin/delete-logs').then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
splash: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -98,7 +98,7 @@ export default Vue.extend({
|
|||
return {
|
||||
inputValue: this.input && this.input.default ? this.input.default : null,
|
||||
userInputValue: null,
|
||||
selectedValue: null,
|
||||
selectedValue: this.select ? this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
|
||||
faTimesCircle, faQuestionCircle
|
||||
};
|
||||
},
|
||||
|
|
|
@ -300,17 +300,13 @@ export default Vue.extend({
|
|||
|
||||
<style lang="stylus" scoped>
|
||||
.mk-messaging-room
|
||||
display flex
|
||||
flex 1
|
||||
flex-direction column
|
||||
height 100%
|
||||
background var(--messagingRoomBg)
|
||||
|
||||
> .body
|
||||
width 100%
|
||||
max-width 600px
|
||||
margin 0 auto
|
||||
flex 1
|
||||
min-height calc(100% - 103px)
|
||||
|
||||
> .init,
|
||||
> .empty
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<p class="no-history" v-if="!fetching && (messages.length == 0 && groupMessages.length == 0)">{{ $t('no-history') }}</p>
|
||||
<p class="no-history" v-if="!fetching && messages.length == 0">{{ $t('no-history') }}</p>
|
||||
<p class="fetching" v-if="fetching"><fa icon="spinner" pulse fixed-width/>{{ $t('@.loading') }}<mk-ellipsis/></p>
|
||||
<ui-margin>
|
||||
<ui-button @click="startUser()"><fa :icon="faUser"/> {{ $t('start-with-user') }}</ui-button>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { concat, sum } from '../../../../../prelude/array';
|
|||
import MkFormula from './formula.vue';
|
||||
import MkCode from './code.vue';
|
||||
import MkGoogle from './google.vue';
|
||||
import { host } from '../../../config';
|
||||
import { host, url } from '../../../config';
|
||||
import { preorderF, countNodesF } from '../../../../../prelude/tree';
|
||||
|
||||
function sumTextsLength(ts: MfmForest): number {
|
||||
|
@ -175,7 +175,9 @@ export default Vue.component('misskey-flavored-markdown', {
|
|||
props: {
|
||||
url: token.node.props.url,
|
||||
rel: 'nofollow noopener',
|
||||
target: '_blank'
|
||||
...(token.node.props.url.startsWith(url) ? {} : {
|
||||
target: '_blank'
|
||||
})
|
||||
},
|
||||
attrs: {
|
||||
style: 'color:var(--mfmUrl);'
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<button @click="react('pudding')" @mouseover="onMouseover" @mouseout="onMouseout" tabindex="10" :title="$t('@.reactions.pudding')" v-particle><mk-reaction-icon reaction="pudding"/></button>
|
||||
</div>
|
||||
<div v-if="enableEmojiReaction" class="text">
|
||||
<input v-model="text" placeholder="または絵文字を入力" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
|
||||
<input v-model="text" :placeholder="$t('input-reaction-placeholder')" @keyup.enter="reactText" @input="tryReactText" v-autocomplete="{ model: 'text' }">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</blockquote>
|
||||
</div>
|
||||
<div v-else class="mk-url-preview">
|
||||
<a :class="{ mini: narrow, compact }" :href="url" rel="nofollow noopener" target="_blank" :title="url" v-if="!fetching">
|
||||
<component :is="self ? 'router-link' : 'a'" :class="{ mini: narrow, compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="self ? null : '_blank'" :title="url" v-if="!fetching">
|
||||
<div class="thumbnail" v-if="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||
<button v-if="!playerEnabled && player.url" @click.prevent="playerEnabled = true" :title="$t('enable-player')"><fa :icon="['far', 'play-circle']"/></button>
|
||||
</div>
|
||||
|
@ -23,17 +23,18 @@
|
|||
<p :title="sitename">{{ sitename }}</p>
|
||||
</footer>
|
||||
</article>
|
||||
</a>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import { url as misskeyUrl } from '../../../config';
|
||||
import { url as local } from '../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/url-preview.vue'),
|
||||
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
|
@ -74,7 +75,9 @@ export default Vue.extend({
|
|||
},
|
||||
tweetUrl: null,
|
||||
playerEnabled: false,
|
||||
misskeyUrl,
|
||||
local,
|
||||
self: this.url.startsWith(local),
|
||||
attr: this.url.startsWith(local) ? 'to' : 'href'
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -1,29 +1,35 @@
|
|||
<template>
|
||||
<a class="mk-url" :href="url" :rel="rel" :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>
|
||||
<component :is="self ? 'router-link' : 'a'" class="mk-url" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target">
|
||||
<template v-if="!self">
|
||||
<span class="schema">{{ schema }}//</span>
|
||||
<span class="hostname">{{ hostname }}</span>
|
||||
<span class="port" v-if="port != ''">:{{ port }}</span>
|
||||
</template>
|
||||
<span class="pathname" v-if="pathname != ''">{{ self ? pathname.substr(1) : pathname }}</span>
|
||||
<span class="query">{{ query }}</span>
|
||||
<span class="hash">{{ hash }}</span>
|
||||
<fa icon="external-link-square-alt"/>
|
||||
</a>
|
||||
<fa icon="external-link-square-alt" v-if="!self"/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { toUnicode as decodePunycode } from 'punycode';
|
||||
import { url as local } from '../../../config';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['url', 'rel', 'target'],
|
||||
data() {
|
||||
return {
|
||||
local,
|
||||
schema: null,
|
||||
hostname: null,
|
||||
port: null,
|
||||
pathname: null,
|
||||
query: null,
|
||||
hash: null
|
||||
hash: null,
|
||||
self: this.url.startsWith(local),
|
||||
attr: this.url.startsWith(local) ? 'to' : 'href'
|
||||
};
|
||||
},
|
||||
created() {
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<ui-container>
|
||||
<template #header><fa :icon="['far', 'comment-alt']"/> {{ $t('timeline') }}</template>
|
||||
<div>
|
||||
<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')"/>
|
||||
<x-notes ref="timeline" :pagination="pagination" @inited="() => $emit('loaded')" :key="user.id"/>
|
||||
</div>
|
||||
</ui-container>
|
||||
</div>
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import Progress from '../../scripts/loading';
|
||||
import { faUserClock } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/pages/follow-requests.vue'),
|
||||
|
@ -31,6 +32,12 @@ export default Vue.extend({
|
|||
requests: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.$emit('init', {
|
||||
title: this.$t('received-follow-requests'),
|
||||
icon: faUserClock
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
Progress.start();
|
||||
this.$root.api('following/requests/list').then(requests => {
|
||||
|
|
|
@ -19,7 +19,7 @@ import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
|||
import { faImage, faFolderOpen } from '@fortawesome/free-regular-svg-icons';
|
||||
import i18n from '../../../../../i18n';
|
||||
import XContainer from '../page-editor.container.vue';
|
||||
import XFileThumbnail from '../../drive-file-thumbnail.vue';
|
||||
import XFileThumbnail from '../../../components/drive-file-thumbnail.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('pages'),
|
|
@ -11,7 +11,7 @@
|
|||
</header>
|
||||
|
||||
<section>
|
||||
<a class="view" v-if="pageId" :href="`/@${ author.username }/pages/${ currentName }`" target="_blank"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('view-page') }}</a>
|
||||
<router-link class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><fa :icon="faExternalLinkSquareAlt"/> {{ $t('view-page') }}</router-link>
|
||||
|
||||
<ui-input v-model="title">
|
||||
<span>{{ $t('title') }}</span>
|
||||
|
@ -111,20 +111,25 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
initPageId: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
initPageName: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
initUser: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
author: this.$store.state.i,
|
||||
readonly: false,
|
||||
page: null,
|
||||
pageId: null,
|
||||
currentName: null,
|
||||
title: '',
|
||||
|
@ -156,7 +161,7 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
async created() {
|
||||
this.aiScript = new ASTypeChecker();
|
||||
|
||||
this.$watch('variables', () => {
|
||||
|
@ -167,6 +172,18 @@ export default Vue.extend({
|
|||
this.aiScript.pageVars = collectPageVars(this.content);
|
||||
}, { deep: true });
|
||||
|
||||
if (this.initPageId) {
|
||||
this.page = await this.$root.api('pages/show', {
|
||||
pageId: this.initPageId,
|
||||
});
|
||||
} else if (this.initPageName && this.initUser) {
|
||||
this.page = await this.$root.api('pages/show', {
|
||||
name: this.initPageName,
|
||||
username: this.initUser,
|
||||
});
|
||||
this.readonly = true;
|
||||
}
|
||||
|
||||
if (this.page) {
|
||||
this.author = this.page.user;
|
||||
this.pageId = this.page.id;
|
|
@ -1,11 +1,15 @@
|
|||
<template>
|
||||
<div class="">
|
||||
<div class="mrdgzndn">
|
||||
<mfm :text="text" :is-note="false" :i="$store.state.i" :key="text"/>
|
||||
|
||||
<mk-url-preview v-for="url in urls" :url="url" :key="url" class="url"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { parse } from '../../../../../../mfm/parse';
|
||||
import { unique } from '../../../../../../prelude/array';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
|
@ -23,6 +27,20 @@ export default Vue.extend({
|
|||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
urls(): string[] {
|
||||
if (this.text) {
|
||||
const ast = parse(this.text);
|
||||
// TODO: 再帰的にURL要素がないか調べる
|
||||
return unique(ast
|
||||
.filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
|
||||
.map(t => t.node.props.url));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$watch('script.vars', () => {
|
||||
this.text = this.script.interpolate(this.value.text);
|
||||
|
@ -32,4 +50,13 @@ export default Vue.extend({
|
|||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.mrdgzndn
|
||||
&:not(:first-child)
|
||||
margin-top 0.5em
|
||||
|
||||
&:not(:last-child)
|
||||
margin-bottom 0.5em
|
||||
|
||||
> .url
|
||||
margin 0.5em 0
|
||||
</style>
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faHeart } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faHeart, faStickyNote } from '@fortawesome/free-regular-svg-icons';
|
||||
import XBlock from './page.block.vue';
|
||||
import { ASEvaluator } from '../../../../../../misc/aiscript/evaluator';
|
||||
import { collectPageVars } from '../../../scripts/collect-page-vars';
|
||||
|
@ -91,6 +91,10 @@ export default Vue.extend({
|
|||
username: this.username,
|
||||
}).then(page => {
|
||||
this.page = page;
|
||||
this.$emit('init', {
|
||||
title: this.page.title,
|
||||
icon: faStickyNote
|
||||
});
|
||||
const pageVars = this.getPageVars();
|
||||
this.script = new Script(new ASEvaluator(this.page.variables, pageVars, {
|
||||
randomSeed: Math.random(),
|
||||
|
@ -148,8 +152,8 @@ export default Vue.extend({
|
|||
> .title
|
||||
z-index 1
|
||||
margin 0
|
||||
padding 32px 64px
|
||||
font-size 24px
|
||||
padding 16px 32px
|
||||
font-size 20px
|
||||
font-weight bold
|
||||
color var(--text)
|
||||
box-shadow 0 var(--lineWidth) rgba(#000, 0.07)
|
||||
|
@ -158,28 +162,40 @@ export default Vue.extend({
|
|||
padding 16px 32px
|
||||
font-size 20px
|
||||
|
||||
@media (max-width 400px)
|
||||
padding 10px 20px
|
||||
font-size 16px
|
||||
|
||||
> div
|
||||
color var(--text)
|
||||
padding 48px 64px
|
||||
font-size 18px
|
||||
padding 24px 32px
|
||||
font-size 16px
|
||||
|
||||
@media (max-width 600px)
|
||||
padding 24px 32px
|
||||
font-size 16px
|
||||
|
||||
@media (max-width 400px)
|
||||
padding 20px 20px
|
||||
font-size 15px
|
||||
|
||||
> footer
|
||||
color var(--text)
|
||||
padding 0 64px 38px 64px
|
||||
padding 0 32px 28px 32px
|
||||
|
||||
@media (max-width 600px)
|
||||
padding 0 32px 28px 32px
|
||||
|
||||
@media (max-width 400px)
|
||||
padding 0 20px 20px 20px
|
||||
font-size 14px
|
||||
|
||||
> small
|
||||
display block
|
||||
opacity 0.5
|
||||
|
||||
> a
|
||||
font-size 14px
|
||||
font-size 90%
|
||||
|
||||
> a + a
|
||||
margin-left 8px
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<ui-margin>
|
||||
<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button>
|
||||
<ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
|
||||
<ui-button @click="transfer"><fa :icon="faCrown"/> {{ $t('transfer') }}</ui-button>
|
||||
</ui-margin>
|
||||
</section>
|
||||
</ui-container>
|
||||
|
@ -28,9 +29,10 @@
|
|||
<div>
|
||||
<header>
|
||||
<b><mk-user-name :user="user"/></b>
|
||||
<span class="is-owner" v-if="group.ownerId === user.id">owner</span>
|
||||
<span class="username">@{{ user | acct }}</span>
|
||||
</header>
|
||||
<div>
|
||||
<div v-if="group.ownerId !== user.id">
|
||||
<a @click="remove(user)">{{ $t('remove-user') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,7 +46,7 @@
|
|||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import { faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faCrown, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
|
@ -60,7 +62,7 @@ export default Vue.extend({
|
|||
return {
|
||||
group: null,
|
||||
users: [],
|
||||
faICursor, faTrashAlt, faUsers, faPlus
|
||||
faCrown, faICursor, faTrashAlt, faUsers, faPlus
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -78,6 +80,14 @@ export default Vue.extend({
|
|||
},
|
||||
|
||||
methods: {
|
||||
fetchGroup() {
|
||||
this.$root.api('users/groups/show', {
|
||||
groupId: this.group.id
|
||||
}).then(group => {
|
||||
this.group = group;
|
||||
})
|
||||
},
|
||||
|
||||
fetchUsers() {
|
||||
this.$root.api('users/show', {
|
||||
userIds: this.group.userIds
|
||||
|
@ -97,8 +107,15 @@ export default Vue.extend({
|
|||
this.$root.api('users/groups/update', {
|
||||
groupId: this.group.id,
|
||||
name: name
|
||||
}).then(() => {
|
||||
this.fetchGroup();
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
del() {
|
||||
|
@ -130,7 +147,13 @@ export default Vue.extend({
|
|||
groupId: this.group.id,
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
this.fetchGroup();
|
||||
this.fetchUsers();
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -150,6 +173,43 @@ export default Vue.extend({
|
|||
type: 'success',
|
||||
text: t
|
||||
});
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async transfer() {
|
||||
const { result: user } = await this.$root.dialog({
|
||||
user: {
|
||||
local: true
|
||||
}
|
||||
});
|
||||
if (user == null) return;
|
||||
|
||||
this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('transfer-are-you-sure').replace('$1', this.group.name).replace('$2', user.username),
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('users/groups/transfer', {
|
||||
groupId: this.group.id,
|
||||
userId: user.id
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('transferred')
|
||||
});
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -179,6 +239,16 @@ export default Vue.extend({
|
|||
> header
|
||||
color var(--text)
|
||||
|
||||
> .is-owner
|
||||
flex-shrink 0
|
||||
align-self center
|
||||
margin-left 8px
|
||||
padding 1px 6px
|
||||
font-size 80%
|
||||
background var(--groupUserListOwnerBg)
|
||||
color var(--groupUserListOwnerFg)
|
||||
border-radius 3px
|
||||
|
||||
> .username
|
||||
margin-left 8px
|
||||
opacity 0.7
|
||||
|
|
|
@ -103,6 +103,10 @@ export default Vue.extend({
|
|||
});
|
||||
this.$root.api('i/user-group-invites').then(invites => {
|
||||
this.invites = invites;
|
||||
}).then(() => {
|
||||
this.$root.api('users/groups/joined').then(groups => {
|
||||
this.joinedGroups = groups;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -146,6 +146,7 @@ init(async (launch, os) => {
|
|||
{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) },
|
||||
{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
|
||||
{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
|
||||
{ path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
|
||||
]}
|
||||
: { path: '/', component: MkHome, children: [
|
||||
{ path: '', name: 'index', component: MkHomeTimeline },
|
||||
|
@ -167,11 +168,15 @@ init(async (launch, os) => {
|
|||
{ path: '/i/groups', component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) },
|
||||
{ path: '/i/groups/:groupId', props: true, component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/follow-requests', component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
|
||||
{ path: '/@:user/pages/:page', component: () => import('../common/views/pages/page/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
|
||||
{ path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
|
||||
]},
|
||||
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
|
||||
{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/edit/:pageId', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/new', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/edit/:pageId', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initPageId: route.params.pageId }) },
|
||||
{ path: '/@:user/pages/:page', component: () => import('../common/views/pages/page/page.vue').then(m => m.default), props: route => ({ pageName: route.params.page, username: route.params.user }) },
|
||||
{ path: '/@:user/pages/:pageName/view-source', component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
|
||||
{ path: '/i/messaging/group/:group', component: MkMessagingRoom },
|
||||
{ path: '/i/messaging/:user', component: MkMessagingRoom },
|
||||
{ path: '/i/drive', component: MkDrive },
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
<template>
|
||||
<mk-ui>
|
||||
<main>
|
||||
<x-page-editor v-if="page !== undefined" :page="page" :readonly="readonly"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPageEditor: () => import('../../../common/views/components/page-editor/page-editor.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
pageId: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
pageName: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
page: undefined,
|
||||
readonly: false
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.pageId) {
|
||||
this.$root.api('pages/show', {
|
||||
pageId: this.pageId,
|
||||
}).then(page => {
|
||||
this.page = page;
|
||||
});
|
||||
} else if (this.pageName && this.user) {
|
||||
this.$root.api('pages/show', {
|
||||
name: this.pageName,
|
||||
username: this.user,
|
||||
}).then(page => {
|
||||
this.readonly = true;
|
||||
this.page = page;
|
||||
});
|
||||
} else {
|
||||
this.page = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
margin 0 auto
|
||||
padding 16px
|
||||
max-width 900px
|
||||
|
||||
</style>
|
|
@ -1,36 +0,0 @@
|
|||
<template>
|
||||
<mk-ui>
|
||||
<main>
|
||||
<x-page :page-name="page" :username="user"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPage: () => import('../../../common/views/pages/page/page.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
margin 0 auto
|
||||
padding 16px
|
||||
max-width 950px
|
||||
|
||||
</style>
|
|
@ -128,6 +128,7 @@ init((launch, os) => {
|
|||
{ path: '/i/lists/:listId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-list-editor.vue').then(m => m.default), listId: route.params.listId }) },
|
||||
{ path: '/i/groups', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-groups.vue').then(m => m.default) }) },
|
||||
{ path: '/i/groups/:groupId', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/user-group-editor.vue').then(m => m.default), groupId: route.params.groupId }) },
|
||||
{ path: '/i/follow-requests', component: DeckColumn, props: route => ({ component: () => import('../common/views/pages/follow-requests.vue').then(m => m.default) }) },
|
||||
]}]
|
||||
: [
|
||||
{ path: '/', name: 'index', component: MkIndex },
|
||||
|
@ -148,8 +149,8 @@ init((launch, os) => {
|
|||
{ path: '/i/drive', name: 'drive', component: MkDrive },
|
||||
{ path: '/i/drive/folder/:folder', component: MkDrive },
|
||||
{ path: '/i/drive/file/:file', component: MkDrive },
|
||||
{ path: '/i/pages/new', component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/edit/:pageId', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/i/pages/new', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default) }) },
|
||||
{ path: '/i/pages/edit/:pageId', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initPageId: route.params.pageId }) },
|
||||
{ path: '/selectdrive', component: MkSelectDrive },
|
||||
{ path: '/search', component: MkSearch },
|
||||
{ path: '/tags/:tag', component: MkTag },
|
||||
|
@ -162,8 +163,8 @@ init((launch, os) => {
|
|||
{ path: 'following', component: () => import('../common/views/pages/following.vue').then(m => m.default) },
|
||||
{ path: 'followers', component: () => import('../common/views/pages/followers.vue').then(m => m.default) },
|
||||
]},
|
||||
{ path: '/@:user/pages/:page', props: true, component: () => import('./views/pages/page.vue').then(m => m.default) },
|
||||
{ path: '/@:user/pages/:pageName/view-source', props: true, component: () => import('./views/pages/page-editor.vue').then(m => m.default) },
|
||||
{ path: '/@:user/pages/:page', component: UI, props: route => ({ component: () => import('../common/views/pages/page/page.vue').then(m => m.default), pageName: route.params.page, username: route.params.user }) },
|
||||
{ path: '/@:user/pages/:pageName/view-source', component: UI, props: route => ({ component: () => import('../common/views/pages/page-editor/page-editor.vue').then(m => m.default), initUser: route.params.user, initPageName: route.params.pageName }) },
|
||||
{ path: '/notes/:note', component: MkNote },
|
||||
{ path: '/authorize-follow', component: MkFollow },
|
||||
{ path: '*', component: MkNotFound }
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div>
|
||||
<header>
|
||||
<mk-reaction-icon :reaction="notification.reaction"/>
|
||||
<router-link :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<mk-time :time="notification.createdAt"/>
|
||||
</header>
|
||||
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
|
||||
|
@ -21,7 +21,7 @@
|
|||
<div>
|
||||
<header>
|
||||
<fa icon="retweet"/>
|
||||
<router-link :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<mk-time :time="notification.createdAt"/>
|
||||
</header>
|
||||
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note.renote)">
|
||||
|
@ -37,7 +37,7 @@
|
|||
<div>
|
||||
<header>
|
||||
<fa icon="user-plus"/>
|
||||
<router-link :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<mk-time :time="notification.createdAt"/>
|
||||
</header>
|
||||
</div>
|
||||
|
@ -48,7 +48,7 @@
|
|||
<div>
|
||||
<header>
|
||||
<fa icon="user-clock"/>
|
||||
<router-link :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<mk-time :time="notification.createdAt"/>
|
||||
</header>
|
||||
</div>
|
||||
|
@ -59,7 +59,7 @@
|
|||
<div>
|
||||
<header>
|
||||
<fa icon="chart-pie"/>
|
||||
<router-link :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<router-link class="name" :to="notification.user | userPage"><mk-user-name :user="notification.user"/></router-link>
|
||||
<mk-time :time="notification.createdAt"/>
|
||||
</header>
|
||||
<router-link class="note-ref" :to="notification.note | notePage" :title="getNoteSummary(notification.note)">
|
||||
|
@ -130,6 +130,12 @@ export default Vue.extend({
|
|||
[data-icon], .mk-reaction-icon
|
||||
margin-right 4px
|
||||
|
||||
> .name
|
||||
text-overflow ellipsis
|
||||
white-space nowrap
|
||||
min-width 0
|
||||
overflow hidden
|
||||
|
||||
> .mk-time
|
||||
margin-left auto
|
||||
color var(--noteHeaderInfo)
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
<template>
|
||||
<mk-ui>
|
||||
<main>
|
||||
<x-page-editor v-if="page !== undefined" :page="page" :readonly="readonly"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPageEditor: () => import('../../../common/views/components/page-editor/page-editor.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
pageId: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
pageName: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
page: undefined,
|
||||
readonly: false
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.pageId) {
|
||||
this.$root.api('pages/show', {
|
||||
pageId: this.pageId,
|
||||
}).then(page => {
|
||||
this.page = page;
|
||||
});
|
||||
} else if (this.pageName && this.user) {
|
||||
this.$root.api('pages/show', {
|
||||
name: this.pageName,
|
||||
username: this.user,
|
||||
}).then(page => {
|
||||
this.readonly = true;
|
||||
this.page = page;
|
||||
});
|
||||
} else {
|
||||
this.page = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
margin 0 auto
|
||||
padding 16px
|
||||
max-width 1000px
|
||||
|
||||
</style>
|
|
@ -1,39 +0,0 @@
|
|||
<template>
|
||||
<mk-ui>
|
||||
<main>
|
||||
<x-page :page-name="page" :username="user"/>
|
||||
</main>
|
||||
</mk-ui>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
XPage: () => import('../../../common/views/pages/page/page.vue').then(m => m.default)
|
||||
},
|
||||
|
||||
props: {
|
||||
page: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
margin 0 auto
|
||||
padding 16px
|
||||
max-width 1000px
|
||||
|
||||
@media (min-width 600px)
|
||||
padding 32px
|
||||
|
||||
</style>
|
|
@ -235,5 +235,8 @@
|
|||
|
||||
pageBlockBorder: 'rgba(255, 255, 255, 0.1)',
|
||||
pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)',
|
||||
|
||||
groupUserListOwnerFg: '#f15f71',
|
||||
groupUserListOwnerBg: '#5d282e'
|
||||
},
|
||||
}
|
||||
|
|
|
@ -235,5 +235,8 @@
|
|||
|
||||
pageBlockBorder: 'rgba(0, 0, 0, 0.1)',
|
||||
pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)',
|
||||
|
||||
groupUserListOwnerFg: '#f15f71',
|
||||
groupUserListOwnerBg: '#ffdfdf'
|
||||
},
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export type Source = {
|
|||
db: string;
|
||||
user: string;
|
||||
pass: string;
|
||||
extra?: { [x: string]: string };
|
||||
};
|
||||
redis: {
|
||||
host: string;
|
||||
|
|
|
@ -93,8 +93,21 @@ export function initDb(justBorrow = false, sync = false, log = false) {
|
|||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
extra: config.db.extra,
|
||||
synchronize: process.env.NODE_ENV === 'test' || sync,
|
||||
dropSchema: process.env.NODE_ENV === 'test' && !justBorrow,
|
||||
cache: {
|
||||
type: 'redis',
|
||||
options: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
options:{
|
||||
auth_pass: config.redis.pass,
|
||||
prefix: config.redis.prefix,
|
||||
db: config.redis.db || 0
|
||||
}
|
||||
}
|
||||
},
|
||||
logging: log,
|
||||
logger: log ? new MyCustomLogger() : undefined,
|
||||
entities: [
|
||||
|
|
|
@ -21,6 +21,7 @@ export class UserGroupRepository extends Repository<UserGroup> {
|
|||
id: userGroup.id,
|
||||
createdAt: userGroup.createdAt.toISOString(),
|
||||
name: userGroup.name,
|
||||
ownerId: userGroup.userId,
|
||||
userIds: users.map(x => x.userId)
|
||||
};
|
||||
}
|
||||
|
@ -48,6 +49,11 @@ export const packedUserGroupSchema = {
|
|||
optional: bool.false, nullable: bool.false,
|
||||
description: 'The name of the UserGroup.'
|
||||
},
|
||||
ownerId: {
|
||||
type: types.string,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
format: 'id',
|
||||
},
|
||||
userIds: {
|
||||
type: types.array,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
|
|
|
@ -8,6 +8,7 @@ const json = {
|
|||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
extra: config.db.extra,
|
||||
entities: ['src/models/entities/*.ts'],
|
||||
migrations: ['migration/*.ts'],
|
||||
cli: {
|
||||
|
|
13
src/server/api/endpoints/admin/delete-logs.ts
Normal file
13
src/server/api/endpoints/admin/delete-logs.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import define from '../../define';
|
||||
import { Logs } from '../../../../models';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
};
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
await Logs.delete({});
|
||||
});
|
|
@ -59,6 +59,7 @@ export default define(meta, async () => {
|
|||
.where(`note.createdAt > :date`, { date: new Date(Date.now() - rangeA) })
|
||||
.andWhere(`note.tags != '{}'`)
|
||||
.select(['note.tags', 'note.userId'])
|
||||
.cache(60000) // 1 min
|
||||
.getMany();
|
||||
|
||||
if (tagNotes.length === 0) {
|
||||
|
@ -108,6 +109,7 @@ export default define(meta, async () => {
|
|||
.where(':tag = ANY(note.tags)', { tag: tag })
|
||||
.andWhere('note.createdAt < :lt', { lt: new Date(Date.now() - (interval * i)) })
|
||||
.andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * (i + 1))) })
|
||||
.cache(60000) // 1 min
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10))
|
||||
)));
|
||||
|
@ -119,6 +121,7 @@ export default define(meta, async () => {
|
|||
.select('count(distinct note.userId)')
|
||||
.where(':tag = ANY(note.tags)', { tag: tag })
|
||||
.andWhere('note.createdAt > :gt', { gt: new Date(Date.now() - (interval * range)) })
|
||||
.cache(60000) // 1 min
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10))
|
||||
));
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import $ from 'cafy';
|
||||
import { publishMainStream } from '../../../../services/stream';
|
||||
import define from '../../define';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import { fetchMeta } from '../../../../misc/fetch-meta';
|
||||
import rndstr from 'rndstr';
|
||||
import config from '../../../../config';
|
||||
import * as ms from 'ms';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { apiLogger } from '../../logger';
|
||||
import { Users, UserProfiles } from '../../../../models';
|
||||
import { ensure } from '../../../../prelude/ensure';
|
||||
import { sendEmail } from '../../../../services/send-email';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -63,36 +61,9 @@ export default define(meta, async (ps, user) => {
|
|||
emailVerifyCode: code
|
||||
});
|
||||
|
||||
const meta = await fetchMeta(true);
|
||||
|
||||
const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: meta.smtpHost,
|
||||
port: meta.smtpPort,
|
||||
secure: meta.smtpSecure,
|
||||
ignoreTLS: !enableAuth,
|
||||
auth: enableAuth ? {
|
||||
user: meta.smtpUser,
|
||||
pass: meta.smtpPass
|
||||
} : undefined
|
||||
} as any);
|
||||
|
||||
const link = `${config.url}/verify-email/${code}`;
|
||||
|
||||
transporter.sendMail({
|
||||
from: meta.email!,
|
||||
to: ps.email,
|
||||
subject: meta.name || 'Misskey',
|
||||
text: `To verify email, please click this link: ${link}`
|
||||
}, (error, info) => {
|
||||
if (error) {
|
||||
apiLogger.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
apiLogger.info('Message sent: %s', info.messageId);
|
||||
});
|
||||
sendEmail(ps.email, 'Email verification', `To verify email, please click this link: ${link}`);
|
||||
}
|
||||
|
||||
return iObj;
|
||||
|
|
|
@ -94,7 +94,7 @@ export const meta = {
|
|||
export default define(meta, async (ps, me) => {
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
const emojis = await Emojis.find({ host: null });
|
||||
const emojis = await Emojis.find({ where: { host: null }, cache: 3600000 }); // 1 hour
|
||||
|
||||
const response: any = {
|
||||
maintainerName: instance.maintainerName,
|
||||
|
|
|
@ -57,10 +57,10 @@ export default define(meta, async () => {
|
|||
driveUsageLocal,
|
||||
driveUsageRemote
|
||||
] = await Promise.all([
|
||||
Notes.count(),
|
||||
Notes.count({ userHost: null }),
|
||||
Users.count(),
|
||||
Users.count({ host: null }),
|
||||
Notes.count({ cache: 3600000 }), // 1 hour
|
||||
Notes.count({ where: { userHost: null }, cache: 3600000 }),
|
||||
Users.count({ cache: 3600000 }),
|
||||
Users.count({ where: { host: null }, cache: 3600000 }),
|
||||
federationChart.getChart('hour', 1).then(chart => chart.instance.total[0]),
|
||||
driveChart.getChart('hour', 1).then(chart => chart.local.totalSize[0]),
|
||||
driveChart.getChart('hour', 1).then(chart => chart.remote.totalSize[0]),
|
||||
|
|
93
src/server/api/endpoints/users/groups/transfer.ts
Normal file
93
src/server/api/endpoints/users/groups/transfer.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '../../../../../misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { getUser } from '../../../common/getters';
|
||||
import { UserGroups, UserGroupJoinings } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
'ja-JP': '指定したユーザーグループを指定したユーザーグループ内のユーザーに譲渡します。',
|
||||
'en-US': 'Transfer user group ownership to another user in group.'
|
||||
},
|
||||
|
||||
tags: ['groups', 'users'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
},
|
||||
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
desc: {
|
||||
'ja-JP': '対象のユーザーのID',
|
||||
'en-US': 'Target user ID'
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
ref: 'UserGroup',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db'
|
||||
},
|
||||
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9'
|
||||
},
|
||||
|
||||
noSuchGroupMember: {
|
||||
message: 'No such group member.',
|
||||
code: 'NO_SUCH_GROUP_MEMBER',
|
||||
id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the group
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
// Fetch the user
|
||||
const user = await getUser(ps.userId).catch(e => {
|
||||
if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw e;
|
||||
});
|
||||
|
||||
const joining = await UserGroupJoinings.findOne({
|
||||
userGroupId: userGroup.id,
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (joining == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroupMember);
|
||||
}
|
||||
|
||||
await UserGroups.update(userGroup.id, {
|
||||
userId: ps.userId
|
||||
});
|
||||
|
||||
return await UserGroups.pack(userGroup.id);
|
||||
});
|
69
src/server/api/endpoints/users/groups/update.ts
Normal file
69
src/server/api/endpoints/users/groups/update.ts
Normal file
|
@ -0,0 +1,69 @@
|
|||
import $ from 'cafy';
|
||||
import { ID } from '../../../../../misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { UserGroups } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
'ja-JP': '指定したユーザーグループを更新します。',
|
||||
'en-US': 'Update a user group'
|
||||
},
|
||||
|
||||
tags: ['groups'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:user-groups',
|
||||
|
||||
params: {
|
||||
groupId: {
|
||||
validator: $.type(ID),
|
||||
desc: {
|
||||
'ja-JP': '対象となるユーザーグループのID',
|
||||
'en-US': 'ID of target user group'
|
||||
}
|
||||
},
|
||||
|
||||
name: {
|
||||
validator: $.str.range(1, 100),
|
||||
desc: {
|
||||
'ja-JP': 'このユーザーグループの名前',
|
||||
'en-US': 'name of this user group'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
ref: 'UserGroup',
|
||||
},
|
||||
|
||||
errors: {
|
||||
noSuchGroup: {
|
||||
message: 'No such group.',
|
||||
code: 'NO_SUCH_GROUP',
|
||||
id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
// Fetch the group
|
||||
const userGroup = await UserGroups.findOne({
|
||||
id: ps.groupId,
|
||||
userId: me.id
|
||||
});
|
||||
|
||||
if (userGroup == null) {
|
||||
throw new ApiError(meta.errors.noSuchGroup);
|
||||
}
|
||||
|
||||
await UserGroups.update(userGroup.id, {
|
||||
name: ps.name
|
||||
});
|
||||
|
||||
return await UserGroups.pack(userGroup.id);
|
||||
});
|
36
src/services/send-email.ts
Normal file
36
src/services/send-email.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import * as nodemailer from 'nodemailer';
|
||||
import { fetchMeta } from '../misc/fetch-meta';
|
||||
import Logger from './logger';
|
||||
|
||||
export const logger = new Logger('email');
|
||||
|
||||
export async function sendEmail(to: string, subject: string, text: string) {
|
||||
const meta = await fetchMeta(true);
|
||||
|
||||
const enableAuth = meta.smtpUser != null && meta.smtpUser !== '';
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: meta.smtpHost,
|
||||
port: meta.smtpPort,
|
||||
secure: meta.smtpSecure,
|
||||
ignoreTLS: !enableAuth,
|
||||
auth: enableAuth ? {
|
||||
user: meta.smtpUser,
|
||||
pass: meta.smtpPass
|
||||
} : undefined
|
||||
} as any);
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
from: meta.email!,
|
||||
to: to,
|
||||
subject: subject || 'Misskey',
|
||||
text: text
|
||||
});
|
||||
|
||||
logger.info('Message sent: %s', info.messageId);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue