diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2019-05-25 09:22:25 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2019-05-25 09:22:25 +0900 |
| commit | fad84203c02136a1ee28b55830d57df3c22dc312 (patch) | |
| tree | 93f12bdf716b3e4d0a1f5f2e552a711c291e4db3 /src | |
| parent | Merge branch 'develop' (diff) | |
| parent | Merge branch 'develop' of https://github.com/syuilo/misskey into develop (diff) | |
| download | misskey-fad84203c02136a1ee28b55830d57df3c22dc312.tar.gz misskey-fad84203c02136a1ee28b55830d57df3c22dc312.tar.bz2 misskey-fad84203c02136a1ee28b55830d57df3c22dc312.zip | |
Merge branch 'develop'
Diffstat (limited to 'src')
52 files changed, 479 insertions, 304 deletions
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 72ae3384b5..588b24f6b5 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -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> diff --git a/src/client/app/admin/views/logs.vue b/src/client/app/admin/views/logs.vue index 5c2cfdb396..cb54318187 100644 --- a/src/client/app/admin/views/logs.vue +++ b/src/client/app/admin/views/logs.vue @@ -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 + }); + }); } } }); diff --git a/src/client/app/common/views/components/dialog.vue b/src/client/app/common/views/components/dialog.vue index 9f38031d62..a577da5a21 100644 --- a/src/client/app/common/views/components/dialog.vue +++ b/src/client/app/common/views/components/dialog.vue @@ -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 }; }, diff --git a/src/client/app/common/views/components/messaging-room.vue b/src/client/app/common/views/components/messaging-room.vue index 658dc93f64..d5fa4143a0 100644 --- a/src/client/app/common/views/components/messaging-room.vue +++ b/src/client/app/common/views/components/messaging-room.vue @@ -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 diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue index cdd35ee8ab..b21104bf9a 100644 --- a/src/client/app/common/views/components/messaging.vue +++ b/src/client/app/common/views/components/messaging.vue @@ -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> diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/app/common/views/components/mfm.ts index 9459172881..7de29b2aad 100644 --- a/src/client/app/common/views/components/mfm.ts +++ b/src/client/app/common/views/components/mfm.ts @@ -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);' diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue index 461e7c9090..970d430069 100644 --- a/src/client/app/common/views/components/reaction-picker.vue +++ b/src/client/app/common/views/components/reaction-picker.vue @@ -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> diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index 9ca4497ad8..6e0d73eef5 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -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' }; }, diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue index faf439814d..2829812f99 100644 --- a/src/client/app/common/views/components/url.vue +++ b/src/client/app/common/views/components/url.vue @@ -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() { diff --git a/src/client/app/common/views/deck/deck.user-column.home.vue b/src/client/app/common/views/deck/deck.user-column.home.vue index ad05d6548b..c30cb397e4 100644 --- a/src/client/app/common/views/deck/deck.user-column.home.vue +++ b/src/client/app/common/views/deck/deck.user-column.home.vue @@ -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> diff --git a/src/client/app/common/views/pages/follow-requests.vue b/src/client/app/common/views/pages/follow-requests.vue index 146d4b0c94..e5b6e2902d 100644 --- a/src/client/app/common/views/pages/follow-requests.vue +++ b/src/client/app/common/views/pages/follow-requests.vue @@ -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 => { diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue index f89279f05a..f89279f05a 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.button.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.button.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.counter.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue index 95c15b01b2..95c15b01b2 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.counter.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.counter.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.if.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue index c83cd421ae..c83cd421ae 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.if.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.if.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.image.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue index 98ec39a512..e2e72b04c2 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.image.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.image.vue @@ -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'), diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.number-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue index 30c3938111..30c3938111 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.number-input.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.number-input.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.post.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue index fc2f5f9032..fc2f5f9032 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.post.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.post.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.section.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue index c20f824e23..c20f824e23 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.section.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.section.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.switch.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue index 174a344640..174a344640 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.switch.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.switch.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.text-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue index 50f95fd205..50f95fd205 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.text-input.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.text-input.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.text.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue index c09f9cc1cf..c09f9cc1cf 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.text.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.text.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.textarea-input.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue index da3eead080..da3eead080 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.textarea-input.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea-input.vue diff --git a/src/client/app/common/views/components/page-editor/els/page-editor.el.textarea.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue index a0cc1966e8..a0cc1966e8 100644 --- a/src/client/app/common/views/components/page-editor/els/page-editor.el.textarea.vue +++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.textarea.vue diff --git a/src/client/app/common/views/components/page-editor/page-editor.blocks.vue b/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue index c5f3419e7b..c5f3419e7b 100644 --- a/src/client/app/common/views/components/page-editor/page-editor.blocks.vue +++ b/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue diff --git a/src/client/app/common/views/components/page-editor/page-editor.container.vue b/src/client/app/common/views/pages/page-editor/page-editor.container.vue index a3a501afb4..a3a501afb4 100644 --- a/src/client/app/common/views/components/page-editor/page-editor.container.vue +++ b/src/client/app/common/views/pages/page-editor/page-editor.container.vue diff --git a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue b/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue index faecaf61fa..faecaf61fa 100644 --- a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue +++ b/src/client/app/common/views/pages/page-editor/page-editor.script-block.vue diff --git a/src/client/app/common/views/components/page-editor/page-editor.vue b/src/client/app/common/views/pages/page-editor/page-editor.vue index a3f69ea5c3..ebe0f4688d 100644 --- a/src/client/app/common/views/components/page-editor/page-editor.vue +++ b/src/client/app/common/views/pages/page-editor/page-editor.vue @@ -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; diff --git a/src/client/app/common/views/pages/page/page.text.vue b/src/client/app/common/views/pages/page/page.text.vue index eadc6f0aed..326fd39050 100644 --- a/src/client/app/common/views/pages/page/page.text.vue +++ b/src/client/app/common/views/pages/page/page.text.vue @@ -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> diff --git a/src/client/app/common/views/pages/page/page.vue b/src/client/app/common/views/pages/page/page.vue index d3fb948c85..7938d706b9 100644 --- a/src/client/app/common/views/pages/page/page.vue +++ b/src/client/app/common/views/pages/page/page.vue @@ -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 diff --git a/src/client/app/common/views/pages/user-group-editor.vue b/src/client/app/common/views/pages/user-group-editor.vue index ef79689ae8..9cc012af7a 100644 --- a/src/client/app/common/views/pages/user-group-editor.vue +++ b/src/client/app/common/views/pages/user-group-editor.vue @@ -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 diff --git a/src/client/app/common/views/pages/user-groups.vue b/src/client/app/common/views/pages/user-groups.vue index f5609d690d..a51a5f723f 100644 --- a/src/client/app/common/views/pages/user-groups.vue +++ b/src/client/app/common/views/pages/user-groups.vue @@ -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; + }); }); }); }, diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 845f8ee5c0..ce0b96e0ac 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -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 }, diff --git a/src/client/app/desktop/views/pages/page-editor.vue b/src/client/app/desktop/views/pages/page-editor.vue deleted file mode 100644 index 35b4008e4f..0000000000 --- a/src/client/app/desktop/views/pages/page-editor.vue +++ /dev/null @@ -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> diff --git a/src/client/app/desktop/views/pages/page.vue b/src/client/app/desktop/views/pages/page.vue deleted file mode 100644 index 1ddff08c76..0000000000 --- a/src/client/app/desktop/views/pages/page.vue +++ /dev/null @@ -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> diff --git a/src/client/app/mobile/script.ts b/src/client/app/mobile/script.ts index 7a80f21759..d04662cc1f 100644 --- a/src/client/app/mobile/script.ts +++ b/src/client/app/mobile/script.ts @@ -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 } diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue index 62df76cba5..9dae95c9b1 100644 --- a/src/client/app/mobile/views/components/notification.vue +++ b/src/client/app/mobile/views/components/notification.vue @@ -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) diff --git a/src/client/app/mobile/views/pages/page-editor.vue b/src/client/app/mobile/views/pages/page-editor.vue deleted file mode 100644 index 0b04f25802..0000000000 --- a/src/client/app/mobile/views/pages/page-editor.vue +++ /dev/null @@ -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> diff --git a/src/client/app/mobile/views/pages/page.vue b/src/client/app/mobile/views/pages/page.vue deleted file mode 100644 index f494abffd7..0000000000 --- a/src/client/app/mobile/views/pages/page.vue +++ /dev/null @@ -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> diff --git a/src/client/themes/dark.json5 b/src/client/themes/dark.json5 index 8e0c726b4c..0665d59901 100644 --- a/src/client/themes/dark.json5 +++ b/src/client/themes/dark.json5 @@ -235,5 +235,8 @@ pageBlockBorder: 'rgba(255, 255, 255, 0.1)', pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)', + + groupUserListOwnerFg: '#f15f71', + groupUserListOwnerBg: '#5d282e' }, } diff --git a/src/client/themes/light.json5 b/src/client/themes/light.json5 index 1fff18176a..cbe456ca5d 100644 --- a/src/client/themes/light.json5 +++ b/src/client/themes/light.json5 @@ -235,5 +235,8 @@ pageBlockBorder: 'rgba(0, 0, 0, 0.1)', pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)', + + groupUserListOwnerFg: '#f15f71', + groupUserListOwnerBg: '#ffdfdf' }, } diff --git a/src/config/types.ts b/src/config/types.ts index 7da9820f22..c35bc63573 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -14,6 +14,7 @@ export type Source = { db: string; user: string; pass: string; + extra?: { [x: string]: string }; }; redis: { host: string; diff --git a/src/db/postgre.ts b/src/db/postgre.ts index fcec68602f..8e2d585e99 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -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: [ diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts index 8bb1ae8330..38174ce235 100644 --- a/src/models/repositories/user-group.ts +++ b/src/models/repositories/user-group.ts @@ -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, diff --git a/src/ormconfig.ts b/src/ormconfig.ts index 91f33181f4..6b7297b2c3 100644 --- a/src/ormconfig.ts +++ b/src/ormconfig.ts @@ -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: { diff --git a/src/server/api/endpoints/admin/delete-logs.ts b/src/server/api/endpoints/admin/delete-logs.ts new file mode 100644 index 0000000000..26cf13c1f6 --- /dev/null +++ b/src/server/api/endpoints/admin/delete-logs.ts @@ -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({}); +}); diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index 05d571851e..53a3514718 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -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)) )); diff --git a/src/server/api/endpoints/i/update-email.ts b/src/server/api/endpoints/i/update-email.ts index d4b9721d82..56284499d3 100644 --- a/src/server/api/endpoints/i/update-email.ts +++ b/src/server/api/endpoints/i/update-email.ts @@ -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; diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 4f418c63c1..d18543f56a 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -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, diff --git a/src/server/api/endpoints/stats.ts b/src/server/api/endpoints/stats.ts index f85109b4b4..4dca62445f 100644 --- a/src/server/api/endpoints/stats.ts +++ b/src/server/api/endpoints/stats.ts @@ -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]), diff --git a/src/server/api/endpoints/users/groups/transfer.ts b/src/server/api/endpoints/users/groups/transfer.ts new file mode 100644 index 0000000000..b4284ab484 --- /dev/null +++ b/src/server/api/endpoints/users/groups/transfer.ts @@ -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); +}); diff --git a/src/server/api/endpoints/users/groups/update.ts b/src/server/api/endpoints/users/groups/update.ts new file mode 100644 index 0000000000..bc974621a3 --- /dev/null +++ b/src/server/api/endpoints/users/groups/update.ts @@ -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); +}); diff --git a/src/services/send-email.ts b/src/services/send-email.ts new file mode 100644 index 0000000000..8818c5fcde --- /dev/null +++ b/src/services/send-email.ts @@ -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; + } +} |