diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-08-30 03:56:51 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-08-30 03:56:51 +0900 |
| commit | 4e11da98d90c1c44fce1abaf63c248896feff03a (patch) | |
| tree | cbe91363f87a3cc29b142433c16e4b16ccf2aa7d /src | |
| parent | New translations ja-JP.yml (French) (diff) | |
| parent | :art: (diff) | |
| download | sharkey-4e11da98d90c1c44fce1abaf63c248896feff03a.tar.gz sharkey-4e11da98d90c1c44fce1abaf63c248896feff03a.tar.bz2 sharkey-4e11da98d90c1c44fce1abaf63c248896feff03a.zip | |
Merge branch 'develop' into l10n_develop
Diffstat (limited to 'src')
150 files changed, 1944 insertions, 1037 deletions
diff --git a/src/client/app/boot.js b/src/client/app/boot.js index 952881f6cb..54397c98c6 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -38,15 +38,22 @@ //#endregion //#region Detect the user language - let lang = navigator.language; + let lang = null; - if (!LANGS.includes(lang)) lang = lang.split('-')[0]; + if (LANGS.includes(navigator.language)) { + lang = navigator.language; + } else { + lang = LANGS.find(x => x.split('-')[0] == navigator.language); - // The default language is English - if (!LANGS.includes(lang)) lang = 'en'; + if (lang == null) { + // Fallback + lang = 'en-US'; + } + } - if (settings) { - if (settings.device.lang) lang = settings.device.lang; + if (settings && settings.device.lang && + LANGS.includes(settings.device.lang)) { + lang = settings.device.lang; } //#endregion diff --git a/src/client/app/common/views/components/nav.vue b/src/client/app/common/views/components/nav.vue index 1f745bf69d..27e66358e4 100644 --- a/src/client/app/common/views/components/nav.vue +++ b/src/client/app/common/views/components/nav.vue @@ -26,8 +26,8 @@ export default Vue.extend({ }, created() { (this as any).os.getMeta().then(meta => { - if (meta.repositoryUrl) this.repositoryUrl = meta.repositoryUrl; - if (meta.feedbackUrl) this.feedbackUrl = meta.feedbackUrl; + if (meta.maintainer.repository_url) this.repositoryUrl = meta.maintainer.repository_url; + if (meta.maintainer.feedback_url) this.feedbackUrl = meta.maintainer.feedback_url; }); } }); diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index be69012737..242d9ba5c6 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -28,18 +28,99 @@ import Vue from 'vue'; import { url as misskeyUrl } from '../../../config'; +// THIS IS THE WHITELIST FOR THE EMBED PLAYER +const whiteList = [ + 'afreecatv.com', + 'aparat.com', + 'applemusic.com', + 'amazon.com', + 'awa.fm', + 'bandcamp.com', + 'bbc.co.uk', + 'beatport.com', + 'bilibili.com', + 'boomstream.com', + 'breakers.tv', + 'cam4.com', + 'cavelis.net', + 'chaturbate.com', + 'cnn.com', + 'cybergame.tv', + 'dailymotion.com', + 'deezer.com', + 'djlive.pl', + 'e-onkyo.com', + 'eventials.com', + 'facebook.com', + 'fc2.com', + 'gameplank.tv', + 'goodgame.ru', + 'google.com', + 'hardtunes.com', + 'instagram.com', + 'johnnylooch.com', + 'kexp.org', + 'lahzenegar.com', + 'liveedu.tv', + 'livetube.cc', + 'livestream.com', + 'meridix.com', + 'mixcloud.com', + 'mixer.com', + 'mobcrush.com', + 'mylive.in.th', + 'myspace.com', + 'netflix.com', + 'newretrowave.com', + 'nhk.or.jp', + 'nicovideo.jp', + 'nico.ms', + 'noisetrade.com', + 'nood.tv', + 'npr.org', + 'openrec.tv', + 'pandora.com', + 'pandora.tv', + 'picarto.tv', + 'pscp.tv', + 'restream.io', + 'reverbnation.com', + 'sermonaudio.com', + 'smashcast.tv', + 'songkick.com', + 'soundcloud.com', + 'spinninrecords.com', + 'spotify.com', + 'stitcher.com', + 'stream.me', + 'switchboard.live', + 'tunein.com', + 'twitcasting.tv', + 'twitch.tv', + 'twitter.com', + 'vaughnlive.tv', + 'veoh.com', + 'vimeo.com', + 'watchpeoplecode.com', + 'web.tv', + 'youtube.com', + 'youtu.be' +]; + export default Vue.extend({ props: { url: { type: String, require: true }, + detail: { type: Boolean, required: false, default: false } }, + data() { return { fetching: true, @@ -57,6 +138,7 @@ export default Vue.extend({ misskeyUrl }; }, + created() { const url = new URL(this.url); @@ -81,102 +163,27 @@ export default Vue.extend({ } return; } + fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { res.json().then(info => { - if (info.url != null) { - this.title = info.title; - this.description = info.description; - this.thumbnail = info.thumbnail; - this.icon = info.icon; - this.sitename = info.sitename; - this.fetching = false; - if ([ // THIS IS THE WHITELIST FOR THE EMBED PLAYER - 'afreecatv.com', - 'aparat.com', - 'applemusic.com', - 'amazon.com', - 'awa.fm', - 'bandcamp.com', - 'bbc.co.uk', - 'beatport.com', - 'bilibili.com', - 'boomstream.com', - 'breakers.tv', - 'cam4.com', - 'cavelis.net', - 'chaturbate.com', - 'cnn.com', - 'cybergame.tv', - 'dailymotion.com', - 'deezer.com', - 'djlive.pl', - 'e-onkyo.com', - 'eventials.com', - 'facebook.com', - 'fc2.com', - 'gameplank.tv', - 'goodgame.ru', - 'google.com', - 'hardtunes.com', - 'instagram.com', - 'johnnylooch.com', - 'kexp.org', - 'lahzenegar.com', - 'liveedu.tv', - 'livetube.cc', - 'livestream.com', - 'meridix.com', - 'mixcloud.com', - 'mixer.com', - 'mobcrush.com', - 'mylive.in.th', - 'myspace.com', - 'netflix.com', - 'newretrowave.com', - 'nhk.or.jp', - 'nicovideo.jp', - 'nico.ms', - 'noisetrade.com', - 'nood.tv', - 'npr.org', - 'openrec.tv', - 'pandora.com', - 'pandora.tv', - 'picarto.tv', - 'pscp.tv', - 'restream.io', - 'reverbnation.com', - 'sermonaudio.com', - 'smashcast.tv', - 'songkick.com', - 'soundcloud.com', - 'spinninrecords.com', - 'spotify.com', - 'stitcher.com', - 'stream.me', - 'switchboard.live', - 'tunein.com', - 'twitcasting.tv', - 'twitch.tv', - 'twitter.com', - 'vaughnlive.tv', - 'veoh.com', - 'vimeo.com', - 'watchpeoplecode.com', - 'web.tv', - 'youtube.com', - 'youtu.be' - ].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) - this.player = info.player; - } // info.url - }) // json - }); // fetch - } // created + if (info.url == null) return; + this.title = info.title; + this.description = info.description; + this.thumbnail = info.thumbnail; + this.icon = info.icon; + this.sitename = info.sitename; + this.fetching = false; + if (whiteList.some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) { + this.player = info.player; + } + }) + }); + } }); </script> <style lang="stylus" scoped> -.twitter +.player position relative width 100% diff --git a/src/client/app/common/views/filters/bytes.ts b/src/client/app/common/views/filters/bytes.ts index 3afb11e9ae..f7a1b2690f 100644 --- a/src/client/app/common/views/filters/bytes.ts +++ b/src/client/app/common/views/filters/bytes.ts @@ -1,8 +1,10 @@ import Vue from 'vue'; Vue.filter('bytes', (v, digits = 0) => { - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; - if (v == 0) return '0Byte'; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + if (v == 0) return '0'; + const isMinus = v < 0; + if (isMinus) v = -v; const i = Math.floor(Math.log(v) / Math.log(1024)); - return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; + return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i]; }); diff --git a/src/client/app/common/views/widgets/donation.vue b/src/client/app/common/views/widgets/donation.vue index 470576d5e6..544ca1bd9d 100644 --- a/src/client/app/common/views/widgets/donation.vue +++ b/src/client/app/common/views/widgets/donation.vue @@ -2,9 +2,9 @@ <div class="mkw-donation" :data-mobile="platform == 'mobile'"> <article> <h1>%fa:heart%%i18n:@title%</h1> - <p> + <p v-if="meta"> {{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }} - <a href="https://syuilo.com">@syuilo</a> + <a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a> {{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }} </p> </article> @@ -15,6 +15,17 @@ import define from '../../../common/define-widget'; export default define({ name: 'donation' +}).extend({ + data() { + return { + meta: null + }; + }, + created() { + (this as any).os.getMeta().then(meta => { + this.meta = meta; + }); + } }); </script> diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts index 83820f92bd..e9d92d1eb1 100644 --- a/src/client/app/desktop/api/update-avatar.ts +++ b/src/client/app/desktop/api/update-avatar.ts @@ -3,8 +3,21 @@ import { apiUrl } from '../../config'; import CropWindow from '../views/components/crop-window.vue'; import ProgressDialog from '../views/components/progress-dialog.vue'; -export default (os: OS) => (cb, file = null) => { - const fileSelected = file => { +export default (os: OS) => { + + const cropImage = file => new Promise((resolve, reject) => { + + const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$'); + if (!regex.test(file.name) ) { + os.apis.dialog({ + title: '%fa:info-circle% %i18n:desktop.invalid-filetype%', + text: null, + actions: [{ + text: '%i18n:common.got-it%' + }] + }); + reject(); + } const w = os.new(CropWindow, { image: file, @@ -19,27 +32,29 @@ export default (os: OS) => (cb, file = null) => { os.api('drive/folders/find', { name: '%i18n:desktop.avatar%' - }).then(iconFolder => { - if (iconFolder.length === 0) { + }).then(avatarFolder => { + if (avatarFolder.length === 0) { os.api('drive/folders/create', { name: '%i18n:desktop.avatar%' }).then(iconFolder => { - upload(data, iconFolder); + resolve(upload(data, iconFolder)); }); } else { - upload(data, iconFolder[0]); + resolve(upload(data, avatarFolder[0])); } }); }); w.$once('skipped', () => { - set(file); + resolve(file); }); + w.$once('cancelled', reject); + document.body.appendChild(w.$el); - }; + }); - const upload = (data, folder) => { + const upload = (data, folder) => new Promise((resolve, reject) => { const dialog = os.new(ProgressDialog, { title: '%i18n:desktop.uploading-avatar%' }); @@ -52,18 +67,19 @@ export default (os: OS) => (cb, file = null) => { xhr.onload = e => { const file = JSON.parse((e.target as any).response); (dialog as any).close(); - set(file); + resolve(file); }; + xhr.onerror = reject; xhr.upload.onprogress = e => { if (e.lengthComputable) (dialog as any).update(e.loaded, e.total); }; xhr.send(data); - }; + }); - const set = file => { - os.api('i/update', { + const setAvatar = file => { + return os.api('i/update', { avatarId: file.id }).then(i => { os.store.commit('updateIKeyValue', { @@ -83,18 +99,21 @@ export default (os: OS) => (cb, file = null) => { }] }); - if (cb) cb(i); + return i; }); }; - if (file) { - fileSelected(file); - } else { - os.apis.chooseDriveFile({ - multiple: false, - title: '%fa:image% %i18n:desktop.choose-avatar%' - }).then(file => { - fileSelected(file); - }); - } + return (file = null) => { + const selectedFile = file + ? Promise.resolve(file) + : os.apis.chooseDriveFile({ + multiple: false, + title: '%fa:image% %i18n:desktop.choose-avatar%' + }); + + return selectedFile + .then(cropImage) + .then(setAvatar) + .catch(err => err && console.warn(err)); + }; }; diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts index 33c4e306a2..e8fa35149b 100644 --- a/src/client/app/desktop/api/update-banner.ts +++ b/src/client/app/desktop/api/update-banner.ts @@ -6,6 +6,19 @@ import ProgressDialog from '../views/components/progress-dialog.vue'; export default (os: OS) => { const cropImage = file => new Promise((resolve, reject) => { + + const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$'); + if (!regex.test(file.name) ) { + os.apis.dialog({ + title: '%fa:info-circle% %i18n:desktop.invalid-filetype%', + text: null, + actions: [{ + text: '%i18n:common.got-it%' + }] + }); + reject(); + } + const w = os.new(CropWindow, { image: file, title: '%i18n:desktop.banner-crop-title%', diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 8dc0482191..f0e8a42662 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -25,6 +25,7 @@ import updateBanner from './api/update-banner'; import MkIndex from './views/pages/index.vue'; import MkDeck from './views/pages/deck/deck.vue'; import MkAdmin from './views/pages/admin/admin.vue'; +import MkStats from './views/pages/stats/stats.vue'; import MkUser from './views/pages/user/user.vue'; import MkFavorites from './views/pages/favorites.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; @@ -57,6 +58,7 @@ init(async (launch) => { { path: '/', name: 'index', component: MkIndex }, { path: '/deck', name: 'deck', component: MkDeck }, { path: '/admin', name: 'admin', component: MkAdmin }, + { path: '/stats', name: 'stats', component: MkStats }, { path: '/i/customize-home', component: MkHomeCustomize }, { path: '/i/favorites', component: MkFavorites }, { path: '/i/messaging/:user', component: MkMessagingRoom }, @@ -94,7 +96,7 @@ init(async (launch) => { /** * Init Notification */ - if ('Notification' in window) { + if ('Notification' in window && os.store.getters.isSignedIn) { // 許可を得ていなかったらリクエスト if ((Notification as any).permission == 'default') { await Notification.requestPermission(); diff --git a/src/client/app/desktop/views/components/charts.chart.ts b/src/client/app/desktop/views/components/charts.chart.ts new file mode 100644 index 0000000000..6a241631e9 --- /dev/null +++ b/src/client/app/desktop/views/components/charts.chart.ts @@ -0,0 +1,42 @@ +import Vue from 'vue'; +import { Line } from 'vue-chartjs'; +import * as mergeOptions from 'merge-options'; + +export default Vue.extend({ + extends: Line, + props: { + data: { + required: true + }, + opts: { + required: false + } + }, + watch: { + data() { + this.render(); + } + }, + mounted() { + this.render(); + }, + methods: { + render() { + this.renderChart(this.data, mergeOptions({ + responsive: true, + maintainAspectRatio: false, + scales: { + xAxes: [{ + type: 'time', + distribution: 'series' + }] + }, + tooltips: { + intersect: false, + mode: 'x', + position: 'nearest' + } + }, this.opts || {})); + } + } +}); diff --git a/src/client/app/desktop/views/components/charts.vue b/src/client/app/desktop/views/components/charts.vue new file mode 100644 index 0000000000..c4e92e429f --- /dev/null +++ b/src/client/app/desktop/views/components/charts.vue @@ -0,0 +1,587 @@ +<template> +<div class="gkgckalzgidaygcxnugepioremxvxvpt"> + <header> + <b>%i18n:@title%:</b> + <select v-model="chartType"> + <optgroup label="%i18n:@users%"> + <option value="users">%i18n:@charts.users%</option> + <option value="users-total">%i18n:@charts.users-total%</option> + </optgroup> + <optgroup label="%i18n:@notes%"> + <option value="notes">%i18n:@charts.notes%</option> + <option value="local-notes">%i18n:@charts.local-notes%</option> + <option value="remote-notes">%i18n:@charts.remote-notes%</option> + <option value="notes-total">%i18n:@charts.notes-total%</option> + </optgroup> + <optgroup label="%i18n:@drive%"> + <option value="drive-files">%i18n:@charts.drive-files%</option> + <option value="drive-files-total">%i18n:@charts.drive-files-total%</option> + <option value="drive">%i18n:@charts.drive%</option> + <option value="drive-total">%i18n:@charts.drive-total%</option> + </optgroup> + </select> + <div> + <span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span> + </div> + </header> + <div> + <x-chart v-if="chart" :data="data[0]" :opts="data[1]"/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XChart from './charts.chart.ts'; + +const colors = { + local: 'rgb(246, 88, 79)', + remote: 'rgb(65, 221, 222)', + + localPlus: 'rgb(52, 178, 118)', + remotePlus: 'rgb(158, 255, 209)', + localMinus: 'rgb(255, 97, 74)', + remoteMinus: 'rgb(255, 149, 134)' +}; + +const rgba = (color: string): string => { + return color.replace('rgb', 'rgba').replace(')', ', 0.1)'); +}; + +export default Vue.extend({ + components: { + XChart + }, + + data() { + return { + chart: null, + chartType: 'notes', + span: 'hour' + }; + }, + + computed: { + data(): any { + if (this.chart == null) return null; + switch (this.chartType) { + case 'users': return this.usersChart(false); + case 'users-total': return this.usersChart(true); + case 'notes': return this.notesChart('combined'); + case 'local-notes': return this.notesChart('local'); + case 'remote-notes': return this.notesChart('remote'); + case 'notes-total': return this.notesTotalChart(); + case 'drive': return this.driveChart(); + case 'drive-total': return this.driveTotalChart(); + case 'drive-files': return this.driveFilesChart(); + case 'drive-files-total': return this.driveFilesTotalChart(); + } + }, + + stats(): any[] { + return ( + this.span == 'day' ? this.chart.perDay : + this.span == 'hour' ? this.chart.perHour : + null + ); + } + }, + + created() { + (this as any).api('chart', { + limit: 32 + }).then(chart => { + this.chart = chart; + }); + }, + + methods: { + notesChart(type: string): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + normal: type == 'local' ? x.notes.local.diffs.normal : type == 'remote' ? x.notes.remote.diffs.normal : x.notes.local.diffs.normal + x.notes.remote.diffs.normal, + reply: type == 'local' ? x.notes.local.diffs.reply : type == 'remote' ? x.notes.remote.diffs.reply : x.notes.local.diffs.reply + x.notes.remote.diffs.reply, + renote: type == 'local' ? x.notes.local.diffs.renote : type == 'remote' ? x.notes.remote.diffs.renote : x.notes.local.diffs.renote + x.notes.remote.diffs.renote, + all: type == 'local' ? (x.notes.local.inc + -x.notes.local.dec) : type == 'remote' ? (x.notes.remote.inc + -x.notes.remote.dec) : (x.notes.local.inc + -x.notes.local.dec) + (x.notes.remote.inc + -x.notes.remote.dec) + })); + + return [{ + datasets: [{ + label: 'All', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.all })) + }, { + label: 'Renotes', + fill: true, + backgroundColor: 'rgba(161, 222, 65, 0.1)', + borderColor: '#a1de41', + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.renote })) + }, { + label: 'Replies', + fill: true, + backgroundColor: 'rgba(247, 121, 108, 0.1)', + borderColor: '#f7796c', + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.reply })) + }, { + label: 'Normal', + fill: true, + backgroundColor: 'rgba(65, 221, 222, 0.1)', + borderColor: '#41ddde', + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.normal })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + }, + + notesTotalChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localCount: x.notes.local.total, + remoteCount: x.notes.remote.total + })); + + return [{ + datasets: [{ + label: 'Combined', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount })) + }, { + label: 'Local', + fill: true, + backgroundColor: rgba(colors.local), + borderColor: colors.local, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localCount })) + }, { + label: 'Remote', + fill: true, + backgroundColor: rgba(colors.remote), + borderColor: colors.remote, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + }, + + usersChart(total: boolean): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localCount: total ? x.users.local.total : (x.users.local.inc + -x.users.local.dec), + remoteCount: total ? x.users.remote.total : (x.users.remote.inc + -x.users.remote.dec) + })); + + return [{ + datasets: [{ + label: 'Combined', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount })) + }, { + label: 'Local', + fill: true, + backgroundColor: rgba(colors.local), + borderColor: colors.local, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localCount })) + }, { + label: 'Remote', + fill: true, + backgroundColor: rgba(colors.remote), + borderColor: colors.remote, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + }, + + driveChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localInc: x.drive.local.incSize, + localDec: -x.drive.local.decSize, + remoteInc: x.drive.remote.incSize, + remoteDec: -x.drive.remote.decSize, + })); + + return [{ + datasets: [{ + label: 'All', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec })) + }, { + label: 'Local +', + fill: true, + backgroundColor: rgba(colors.localPlus), + borderColor: colors.localPlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localInc })) + }, { + label: 'Local -', + fill: true, + backgroundColor: rgba(colors.localMinus), + borderColor: colors.localMinus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localDec })) + }, { + label: 'Remote +', + fill: true, + backgroundColor: rgba(colors.remotePlus), + borderColor: colors.remotePlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteInc })) + }, { + label: 'Remote -', + fill: true, + backgroundColor: rgba(colors.remoteMinus), + borderColor: colors.remoteMinus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteDec })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('bytes')(value, 1); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; + } + } + } + }]; + }, + + driveTotalChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localSize: x.drive.local.totalSize, + remoteSize: x.drive.remote.totalSize + })); + + return [{ + datasets: [{ + label: 'Combined', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteSize + x.localSize })) + }, { + label: 'Local', + fill: true, + backgroundColor: rgba(colors.local), + borderColor: colors.local, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localSize })) + }, { + label: 'Remote', + fill: true, + backgroundColor: rgba(colors.remote), + borderColor: colors.remote, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteSize })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('bytes')(value, 1); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; + } + } + } + }]; + }, + + driveFilesChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localInc: x.drive.local.incCount, + localDec: -x.drive.local.decCount, + remoteInc: x.drive.remote.incCount, + remoteDec: -x.drive.remote.decCount + })); + + return [{ + datasets: [{ + label: 'All', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec })) + }, { + label: 'Local +', + fill: true, + backgroundColor: rgba(colors.localPlus), + borderColor: colors.localPlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localInc })) + }, { + label: 'Local -', + fill: true, + backgroundColor: rgba(colors.localMinus), + borderColor: colors.localMinus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localDec })) + }, { + label: 'Remote +', + fill: true, + backgroundColor: rgba(colors.remotePlus), + borderColor: colors.remotePlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteInc })) + }, { + label: 'Remote -', + fill: true, + backgroundColor: rgba(colors.remoteMinus), + borderColor: colors.remoteMinus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteDec })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + }, + + driveFilesTotalChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + localCount: x.drive.local.totalCount, + remoteCount: x.drive.remote.totalCount, + })); + + return [{ + datasets: [{ + label: 'Combined', + fill: false, + borderColor: '#555', + borderWidth: 2, + borderDash: [4, 4], + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localCount + x.remoteCount })) + }, { + label: 'Local', + fill: true, + backgroundColor: rgba(colors.local), + borderColor: colors.local, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.localCount })) + }, { + label: 'Remote', + fill: true, + backgroundColor: rgba(colors.remote), + borderColor: colors.remote, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.remoteCount })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('number')(value); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`; + } + } + } + }]; + } + } +}); +</script> + +<style lang="stylus" scoped> +@import '~const.styl' + +.gkgckalzgidaygcxnugepioremxvxvpt + padding 32px + background #fff + box-shadow 0 2px 8px rgba(#000, 0.1) + + * + user-select none + + > header + display flex + margin 0 0 1em 0 + padding 0 0 8px 0 + font-size 1em + color #555 + border-bottom solid 1px #eee + + > b + margin-right 8px + + > *:last-child + margin-left auto + + * + &:not(.active) + color $theme-color + cursor pointer + + > div + > * + display block + height 320px + +</style> diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index 227bcc349d..1ba4a9a447 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -47,7 +47,7 @@ </div> <mk-poll v-if="p.poll" :note="p"/> <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 87acf7974d..7592ae3905 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -32,7 +32,7 @@ <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index df131a1a65..7d6f1d55fb 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -49,6 +49,7 @@ </div> <mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/> <mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/> + <mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/> <mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/> <mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/> <mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/> @@ -333,6 +334,12 @@ export default Vue.extend({ value: v }); }, + onChangeShowClockOnHeader(v) { + this.$store.dispatch('settings/set', { + key: 'showClockOnHeader', + value: v + }); + }, onChangeShowReplyTarget(v) { this.$store.dispatch('settings/set', { key: 'showReplyTarget', diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue index 4e0fc1cf1a..5e26389d89 100644 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -30,10 +30,8 @@ <li @click="settings"> <p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p> </li> - </ul> - <ul> - <li @click="signout"> - <p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p> + <li v-if="$store.state.i.isAdmin"> + <router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link> </li> </ul> <ul> @@ -41,6 +39,11 @@ <p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p> </li> </ul> + <ul> + <li @click="signout"> + <p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p> + </li> + </ul> </div> </transition> </div> diff --git a/src/client/app/desktop/views/components/ui.header.nav.vue b/src/client/app/desktop/views/components/ui.header.nav.vue index f01aade306..6292b764c6 100644 --- a/src/client/app/desktop/views/components/ui.header.nav.vue +++ b/src/client/app/desktop/views/components/ui.header.nav.vue @@ -11,7 +11,7 @@ <li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop"> <router-link to="/deck"> %fa:columns% - <p>%i18n:@deck% <small>(beta)</small></p> + <p>%i18n:@deck%</p> </router-link> </li> <li class="messaging"> diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue index edd9829c1c..6de4eaf744 100644 --- a/src/client/app/desktop/views/components/ui.header.vue +++ b/src/client/app/desktop/views/components/ui.header.vue @@ -17,7 +17,7 @@ <x-account v-if="$store.getters.isSignedIn"/> <x-notifications v-if="$store.getters.isSignedIn"/> <x-post v-if="$store.getters.isSignedIn"/> - <x-clock/> + <x-clock v-if="$store.state.settings.showClockOnHeader"/> </div> </div> </div> diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue index 7ef8dff5be..1e1755ec3c 100644 --- a/src/client/app/desktop/views/components/user-preview.vue +++ b/src/client/app/desktop/views/components/user-preview.vue @@ -48,7 +48,7 @@ export default Vue.extend({ this.open(); }); } else { - const query = this.user[0] == '@' ? + const query = this.user.startsWith('@') ? parseAcct(this.user.substr(1)) : { userId: this.user }; diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue index 3567585cb8..ebb54d782e 100644 --- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue +++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue @@ -1,16 +1,20 @@ <template> -<div class="obdskegsannmntldydackcpzezagxqfy card"> +<div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card"> <header>%i18n:@dashboard%</header> <div v-if="stats" class="stats"> <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> <div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> - <div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> - <div><span>%fa:pen% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> + <div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> + <div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> </div> <div class="cpu-memory"> <x-cpu-memory :connection="connection"/> </div> <div> + <label> + <input type="checkbox" v-model="disableRegistration" @change="updateMeta"> + <span>disableRegistration</span> + </label> <button class="ui" @click="invite">%i18n:@invite%</button> <p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> </div> @@ -28,6 +32,7 @@ export default Vue.extend({ data() { return { stats: null, + disableRegistration: false, inviteCode: null, connection: null, connectionId: null @@ -37,6 +42,10 @@ export default Vue.extend({ this.connection = (this as any).os.streams.serverStatsStream.getConnection(); this.connectionId = (this as any).os.streams.serverStatsStream.use(); + (this as any).os.getMeta().then(meta => { + this.disableRegistration = meta.disableRegistration; + }); + (this as any).api('stats').then(stats => { this.stats = stats; }); @@ -49,6 +58,11 @@ export default Vue.extend({ (this as any).api('admin/invite').then(x => { this.inviteCode = x.code; }); + }, + updateMeta() { + (this as any).api('admin/update-meta', { + disableRegistration: this.disableRegistration + }); } } }); diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue deleted file mode 100644 index 3c537d8d6d..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.drive-chart.chart.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <polyline - :points="points" - fill="none" - stroke-width="1" - stroke="#555"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - chart: { - required: true - }, - type: { - type: String, - required: true - } - }, - data() { - return { - viewBoxX: 365, - viewBoxY: 70, - points: null - }; - }, - created() { - const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize)); - - if (peak != 0) { - const data = this.chart.slice().reverse().map(x => ({ - size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize - })); - - this.points = data.map((d, i) => `${i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' '); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue b/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue deleted file mode 100644 index 4f94fd2372..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.drive-chart.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<div class="card"> - <header>%i18n:@title%</header> - <div class="card"> - <header>%i18n:@local%</header> - <x-chart v-if="chart" :chart="chart" type="local"/> - </div> - <div class="card"> - <header>%i18n:@remote%</header> - <x-chart v-if="chart" :chart="chart" type="remote"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from "vue"; -import XChart from "./admin.drive-chart.chart.vue"; - -export default Vue.extend({ - components: { - XChart - }, - props: { - chart: { - required: true - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue deleted file mode 100644 index 83c61c1313..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.notes-chart.chart.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <polyline - :points="pointsNote" - fill="none" - stroke-width="1" - stroke="#41ddde"/> - <polyline - :points="pointsReply" - fill="none" - stroke-width="1" - stroke="#f7796c"/> - <polyline - :points="pointsRenote" - fill="none" - stroke-width="1" - stroke="#a1de41"/> - <polyline - :points="pointsTotal" - fill="none" - stroke-width="1" - stroke="#555" - stroke-dasharray="2 2"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - chart: { - required: true - }, - type: { - type: String, - required: true - } - }, - data() { - return { - viewBoxX: 365, - viewBoxY: 70, - pointsNote: null, - pointsReply: null, - pointsRenote: null, - pointsTotal: null - }; - }, - created() { - const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff)); - - if (peak != 0) { - const data = this.chart.slice().reverse().map(x => ({ - normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal, - reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply, - renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote, - total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff - })); - - this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' '); - this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' '); - this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' '); - this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' '); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue b/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue deleted file mode 100644 index e4d396d9c6..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.notes-chart.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<div class="card"> - <header>%i18n:@title%</header> - <div class="card"> - <header>%i18n:@local%</header> - <x-chart v-if="chart" :chart="chart" type="local"/> - </div> - <div class="card"> - <header>%i18n:@remote%</header> - <x-chart v-if="chart" :chart="chart" type="remote"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from "vue"; -import XChart from "./admin.notes-chart.chart.vue"; - -export default Vue.extend({ - components: { - XChart - }, - props: { - chart: { - required: true - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue index 59932f4be7..8d8e37e181 100644 --- a/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.suspend-user.vue @@ -1,5 +1,5 @@ <template> -<div class="card"> +<div class="mk-admin-card"> <header>%i18n:@suspend-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue index a75c0bd64e..ec423969be 100644 --- a/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.unsuspend-user.vue @@ -1,5 +1,5 @@ <template> -<div class="card"> +<div class="mk-admin-card"> <header>%i18n:@unsuspend-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue index 72962870d9..e8204e69f4 100644 --- a/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.unverify-user.vue @@ -1,5 +1,5 @@ <template> -<div class="card"> +<div class="mk-admin-card"> <header>%i18n:@unverify-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue deleted file mode 100644 index c2ab4a78e3..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.users-chart.chart.vue +++ /dev/null @@ -1,51 +0,0 @@ -<template> -<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`"> - <polyline - :points="points" - fill="none" - stroke-width="1" - stroke="#555"/> -</svg> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -export default Vue.extend({ - props: { - chart: { - required: true - }, - type: { - type: String, - required: true - } - }, - data() { - return { - viewBoxX: 365, - viewBoxY: 70, - points: null - }; - }, - created() { - const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff)); - - if (peak != 0) { - const data = this.chart.slice().reverse().map(x => ({ - count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff - })); - - this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' '); - } - } -}); -</script> - -<style lang="stylus" scoped> -svg - display block - padding 10px - width 100% - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.users-chart.vue b/src/client/app/desktop/views/pages/admin/admin.users-chart.vue deleted file mode 100644 index e620012702..0000000000 --- a/src/client/app/desktop/views/pages/admin/admin.users-chart.vue +++ /dev/null @@ -1,34 +0,0 @@ -<template> -<div class="card"> - <header>%i18n:@title%</header> - <div class="card"> - <header>%i18n:@local%</header> - <x-chart v-if="chart" :chart="chart" type="local"/> - </div> - <div class="card"> - <header>%i18n:@remote%</header> - <x-chart v-if="chart" :chart="chart" type="remote"/> - </div> -</div> -</template> - -<script lang="ts"> -import Vue from "vue"; -import XChart from "./admin.users-chart.chart.vue"; - -export default Vue.extend({ - components: { - XChart - }, - props: { - chart: { - required: true - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -</style> diff --git a/src/client/app/desktop/views/pages/admin/admin.verify-user.vue b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue index 3902d4bddd..91fb04af80 100644 --- a/src/client/app/desktop/views/pages/admin/admin.verify-user.vue +++ b/src/client/app/desktop/views/pages/admin/admin.verify-user.vue @@ -1,5 +1,5 @@ <template> -<div class="card"> +<div class="mk-admin-card"> <header>%i18n:@verify-user%</header> <input v-model="username" type="text" class="ui"/> <button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button> diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue index cbb1890cc3..3438462cd6 100644 --- a/src/client/app/desktop/views/pages/admin/admin.vue +++ b/src/client/app/desktop/views/pages/admin/admin.vue @@ -11,9 +11,7 @@ <main> <div v-show="page == 'dashboard'"> <x-dashboard/> - <x-users-chart :chart="chart"/> - <x-notes-chart :chart="chart"/> - <x-drive-chart :chart="chart"/> + <x-charts/> </div> <div v-if="page == 'users'"> <x-suspend-user/> @@ -34,9 +32,7 @@ import XSuspendUser from "./admin.suspend-user.vue"; import XUnsuspendUser from "./admin.unsuspend-user.vue"; import XVerifyUser from "./admin.verify-user.vue"; import XUnverifyUser from "./admin.unverify-user.vue"; -import XUsersChart from "./admin.users-chart.vue"; -import XNotesChart from "./admin.notes-chart.vue"; -import XDriveChart from "./admin.drive-chart.vue"; +import XCharts from "../../components/charts.vue"; export default Vue.extend({ components: { @@ -45,21 +41,13 @@ export default Vue.extend({ XUnsuspendUser, XVerifyUser, XUnverifyUser, - XUsersChart, - XNotesChart, - XDriveChart + XCharts }, data() { return { - page: 'dashboard', - chart: null + page: 'dashboard' }; }, - created() { - (this as any).api('admin/chart').then(chart => { - this.chart = chart; - }); - }, methods: { nav(page: string) { this.page = page; @@ -115,7 +103,7 @@ export default Vue.extend({ > div max-width 800px -.card +.mk-admin-card padding 32px background #fff box-shadow 0 2px 8px rgba(#000, 0.1) diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue index c7df715a05..e6d062eac9 100644 --- a/src/client/app/desktop/views/pages/deck/deck.note.vue +++ b/src/client/app/desktop/views/pages/deck/deck.note.vue @@ -32,7 +32,7 @@ <mk-media-list :media-list="p.media"/> </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote" :mini="true"/> </div> diff --git a/src/client/app/desktop/views/pages/share.vue b/src/client/app/desktop/views/pages/share.vue index 4dd6080690..69ecbf115f 100644 --- a/src/client/app/desktop/views/pages/share.vue +++ b/src/client/app/desktop/views/pages/share.vue @@ -16,7 +16,7 @@ import Vue from 'vue'; export default Vue.extend({ data() { return { - name: (this as any).os.instanceName, + name: null, posted: false, text: new URLSearchParams(location.search).get('text') }; @@ -25,6 +25,11 @@ export default Vue.extend({ close() { window.close(); } + }, + mounted() { + (this as any).os.getMeta().then(meta => { + this.name = meta.name; + }); } }); </script> diff --git a/src/client/app/desktop/views/pages/stats/stats.vue b/src/client/app/desktop/views/pages/stats/stats.vue new file mode 100644 index 0000000000..41005b6398 --- /dev/null +++ b/src/client/app/desktop/views/pages/stats/stats.vue @@ -0,0 +1,64 @@ +<template> +<div class="tcrwdhwpuxrwmcttxjcsehgpagpstqey"> + <div v-if="stats" class="stats"> + <div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div> + <div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div> + <div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div> + <div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div> + </div> + <div> + <x-charts/> + </div> +</div> +</template> + +<script lang="ts"> +import Vue from "vue"; +import XCharts from "../../components/charts.vue"; + +export default Vue.extend({ + components: { + XCharts + }, + data() { + return { + stats: null + }; + }, + created() { + (this as any).api('stats').then(stats => { + this.stats = stats; + }); + }, +}); +</script> + +<style lang="stylus"> +@import '~const.styl' + +.tcrwdhwpuxrwmcttxjcsehgpagpstqey + width 100% + padding 16px + + > .stats + display flex + justify-content center + margin-bottom 16px + padding 32px + background #fff + box-shadow 0 2px 8px rgba(#000, 0.1) + + > div + flex 1 + text-align center + + > *:first-child + display block + color $theme-color + + > *:last-child + font-size 70% + + > div + max-width 850px +</style> diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue index 4af0f0bca6..516eea0288 100644 --- a/src/client/app/desktop/views/pages/user/user.friends.vue +++ b/src/client/app/desktop/views/pages/user/user.friends.vue @@ -40,10 +40,12 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> +root(isDark) .friends - background #fff + background isDark ? #282C37 : #fff border solid 1px rgba(#000, 0.075) border-radius 6px + overflow hidden > .title z-index 1 @@ -52,7 +54,8 @@ export default Vue.extend({ line-height 42px font-size 0.9em font-weight bold - color #888 + background isDark ? #313543 : inherit + color isDark ? #e3e5e8 : #888 box-shadow 0 1px rgba(#000, 0.07) > i @@ -70,7 +73,7 @@ export default Vue.extend({ > .user padding 16px - border-bottom solid 1px #eee + border-bottom solid 1px isDark ? #21242f : #eee &:last-child border-bottom none @@ -96,18 +99,24 @@ export default Vue.extend({ margin 0 font-size 16px line-height 24px - color #555 + color isDark ? #ccc : #555 > .username display block margin 0 font-size 15px line-height 16px - color #ccc + color isDark ? #555 : #ccc > .mk-follow-button position absolute top 16px right 16px +.friends[data-darkmode] + root(true) + +.friends:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index ce7791a96b..8397e56484 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -39,10 +39,12 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> +root(isDark) .photos - background #fff + background isDark ? #282C37 : #fff border solid 1px rgba(#000, 0.075) border-radius 6px + overflow hidden > .title z-index 1 @@ -51,7 +53,8 @@ export default Vue.extend({ line-height 42px font-size 0.9em font-weight bold - color #888 + background: isDark ? #313543 : inherit + color isDark ? #e3e5e8 : #888 box-shadow 0 1px rgba(#000, 0.07) > i @@ -85,4 +88,10 @@ export default Vue.extend({ > i margin-right 4px +.photos[data-darkmode] + root(true) + +.photos:not([data-darkmode]) + root(false) + </style> diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index 300fd68f06..afb5e674d9 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -138,7 +138,7 @@ root(isDark) padding 16px font-size 12px color #aaa - background #fff + background isDark ? #21242f : #fff border solid 1px rgba(#000, 0.075) border-radius 6px diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 18f510ea24..cf97957400 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -19,8 +19,8 @@ import { version, codename, lang } from './config'; let elementLocale; switch (lang) { - case 'ja': elementLocale = ElementLocaleJa; break; - case 'en': elementLocale = ElementLocaleEn; break; + case 'ja-JP': elementLocale = ElementLocaleJa; break; + case 'en-US': elementLocale = ElementLocaleEn; break; default: elementLocale = ElementLocaleEn; break; } diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index 317f08dcfa..f9996f9da6 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -45,7 +45,7 @@ </div> <mk-poll v-if="p.poll" :note="p"/> <mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 8fc8af7f8d..d0cea135f9 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -33,7 +33,7 @@ </div> <mk-poll v-if="p.poll" :note="p" ref="pollViewer"/> <mk-url-preview v-for="url in urls" :url="url" :key="url"/> - <a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> + <a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a> <div class="map" v-if="p.geo" ref="map"></div> <div class="renote" v-if="p.renote"> <mk-note-preview :note="p.renote"/> diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue index 74564a48bb..39ea513b76 100644 --- a/src/client/app/mobile/views/components/ui.nav.vue +++ b/src/client/app/mobile/views/components/ui.nav.vue @@ -30,6 +30,7 @@ <ul> <li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li> <li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li> + <li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link></li> <li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li> </ul> </div> diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index 6b82be099d..7437eb8b47 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -42,6 +42,12 @@ </ui-card> <ui-card> + <div slot="title">%fa:volume-up% %i18n:@sound%</div> + + <ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch> + </ui-card> + + <ui-card> <div slot="title">%fa:language% %i18n:@lang%</div> <ui-select v-model="lang" placeholder="%i18n:@auto%"> @@ -142,6 +148,11 @@ export default Vue.extend({ get() { return this.$store.state.device.lang; }, set(value) { this.$store.commit('device/set', { key: 'lang', value }); } }, + + enableSounds: { + get() { return this.$store.state.device.enableSounds; }, + set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); } + }, }, mounted() { diff --git a/src/client/app/mobile/views/pages/share.vue b/src/client/app/mobile/views/pages/share.vue index 588b0941e6..d75763c525 100644 --- a/src/client/app/mobile/views/pages/share.vue +++ b/src/client/app/mobile/views/pages/share.vue @@ -16,7 +16,7 @@ import Vue from 'vue'; export default Vue.extend({ data() { return { - name: (this as any).os.instanceName, + name: null, posted: false, text: new URLSearchParams(location.search).get('text') }; @@ -25,6 +25,11 @@ export default Vue.extend({ close() { window.close(); } + }, + mounted() { + (this as any).os.getMeta().then(meta => { + this.name = meta.name; + }); } }); </script> diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index e72867352f..8918847a8f 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -11,7 +11,7 @@ <a class="avatar"> <img :src="user.avatarUrl" alt="avatar"/> </a> - <mk-mute-button v-if="$store.state.i.id != user.id" :user="user"/> + <mk-mute-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> <mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/> </div> <div class="title"> diff --git a/src/client/app/store.ts b/src/client/app/store.ts index ba91a11f25..469563495f 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -13,6 +13,7 @@ const defaultSettings = { showMaps: true, showPostFormOnTopOfTl: false, suggestRecentHashtags: true, + showClockOnHeader: true, circleIcons: true, gradientWindowHeader: false, showReplyTarget: true, diff --git a/src/config/load.ts b/src/config/load.ts index 1c59f82b3e..8929cf8d3e 100644 --- a/src/config/load.ts +++ b/src/config/load.ts @@ -53,5 +53,5 @@ export default function load() { } function normalizeUrl(url: string) { - return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; + return url.endsWith('/') ? url.substr(0, url.length - 1) : url; } diff --git a/src/config/types.ts b/src/config/types.ts index f220e15822..a1dc9a5bd4 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -62,6 +62,8 @@ export type Source = { */ ghost?: string; + summalyProxy?: string; + accesslog?: string; twitter?: { consumer_key: string; diff --git a/src/docs/about.en.md b/src/docs/about.en-US.md index bb1c51927b..bb1c51927b 100644 --- a/src/docs/about.en.md +++ b/src/docs/about.en-US.md diff --git a/src/docs/about.ja.md b/src/docs/about.ja-JP.md index 1b06361f0f..1b06361f0f 100644 --- a/src/docs/about.ja.md +++ b/src/docs/about.ja-JP.md diff --git a/src/docs/api.ja.md b/src/docs/api.ja-JP.md index ecc80cc05e..ecc80cc05e 100644 --- a/src/docs/api.ja.md +++ b/src/docs/api.ja-JP.md diff --git a/src/docs/api/endpoints/view.pug b/src/docs/api/endpoints/view.pug index 76e1183302..be7e84faa1 100644 --- a/src/docs/api/endpoints/view.pug +++ b/src/docs/api/endpoints/view.pug @@ -15,7 +15,7 @@ block main span.path= endpointUrl.path if endpoint.desc - p#desc= endpoint.desc[lang] || endpoint.desc['ja'] + p#desc= endpoint.desc[lang] || endpoint.desc['ja-JP'] if endpoint.requireCredential div.ui.info: p diff --git a/src/docs/api/entities/drive-file.yaml b/src/docs/api/entities/drive-file.yaml index 62dbec363a..0c2195ac08 100644 --- a/src/docs/api/entities/drive-file.yaml +++ b/src/docs/api/entities/drive-file.yaml @@ -1,90 +1,90 @@ name: "DriveFile" desc: - ja: "ドライブのファイル。" - en: "A file of Drive." + ja-JP: "ドライブのファイル。" + en-US: "A file of Drive." props: id: type: "id" optional: false desc: - ja: "ファイルID" - en: "The ID of this file" + ja-JP: "ファイルID" + en-US: "The ID of this file" createdAt: type: "date" optional: false desc: - ja: "アップロード日時" - en: "The upload date of this file" + ja-JP: "アップロード日時" + en-US: "The upload date of this file" userId: type: "id(User)" optional: false desc: - ja: "所有者ID" - en: "The ID of the owner of this file" + ja-JP: "所有者ID" + en-US: "The ID of the owner of this file" user: type: "entity(User)" optional: true desc: - ja: "所有者" - en: "The owner of this file" + ja-JP: "所有者" + en-US: "The owner of this file" name: type: "string" optional: false desc: - ja: "ファイル名" - en: "The name of this file" + ja-JP: "ファイル名" + en-US: "The name of this file" md5: type: "string" optional: false desc: - ja: "ファイルのMD5ハッシュ値" - en: "The md5 hash value of this file" + ja-JP: "ファイルのMD5ハッシュ値" + en-US: "The md5 hash value of this file" type: type: "string" optional: false desc: - ja: "ファイルの種類" - en: "The type of this file" + ja-JP: "ファイルの種類" + en-US: "The type of this file" datasize: type: "number" optional: false desc: - ja: "ファイルサイズ(bytes)" - en: "The size of this file (bytes)" + ja-JP: "ファイルサイズ(bytes)" + en-US: "The size of this file (bytes)" url: type: "string" optional: false desc: - ja: "ファイルのURL" - en: "The URL of this file" + ja-JP: "ファイルのURL" + en-US: "The URL of this file" folderId: type: "id(DriveFolder)" optional: true desc: - ja: "フォルダID" - en: "The ID of the folder of this file" + ja-JP: "フォルダID" + en-US: "The ID of the folder of this file" folder: type: "entity(DriveFolder)" optional: true desc: - ja: "フォルダ" - en: "The folder of this file" + ja-JP: "フォルダ" + en-US: "The folder of this file" isSensitive: type: "boolean" optional: true desc: - ja: "このメディアが「閲覧注意」(NSFW)かどうか" - en: "Whether this media is NSFW" + ja-JP: "このメディアが「閲覧注意」(NSFW)かどうか" + en-US: "Whether this media is NSFW" diff --git a/src/docs/api/entities/drive-folder.yaml b/src/docs/api/entities/drive-folder.yaml index 0fb8308dd4..e3dfd2ca01 100644 --- a/src/docs/api/entities/drive-folder.yaml +++ b/src/docs/api/entities/drive-folder.yaml @@ -1,41 +1,41 @@ name: "DriveFolder" desc: - ja: "ドライブのフォルダを表します。" - en: "A folder of Drive." + ja-JP: "ドライブのフォルダを表します。" + en-US: "A folder of Drive." props: id: type: "id" optional: false desc: - ja: "フォルダID" - en: "The ID of this folder" + ja-JP: "フォルダID" + en-US: "The ID of this folder" createdAt: type: "date" optional: false desc: - ja: "作成日時" - en: "The created date of this folder" + ja-JP: "作成日時" + en-US: "The created date of this folder" userId: type: "id(User)" optional: false desc: - ja: "所有者ID" - en: "The ID of the owner of this folder" + ja-JP: "所有者ID" + en-US: "The ID of the owner of this folder" parentId: type: "entity(DriveFolder)" optional: false desc: - ja: "親フォルダのID (ルートなら null)" - en: "The ID of parent folder" + ja-JP: "親フォルダのID (ルートなら null)" + en-US: "The ID of parent folder" name: type: "string" optional: false desc: - ja: "フォルダ名" - en: "The name of this folder" + ja-JP: "フォルダ名" + en-US: "The name of this folder" diff --git a/src/docs/api/entities/note.yaml b/src/docs/api/entities/note.yaml index 04cb3c9824..cae9a53f82 100644 --- a/src/docs/api/entities/note.yaml +++ b/src/docs/api/entities/note.yaml @@ -1,190 +1,190 @@ name: "Note" desc: - ja: "投稿。" - en: "A note." + ja-JP: "投稿。" + en-US: "A note." props: id: type: "id" optional: false desc: - ja: "投稿ID" - en: "The ID of this note" + ja-JP: "投稿ID" + en-US: "The ID of this note" createdAt: type: "date" optional: false desc: - ja: "投稿日時" - en: "The posted date of this note" + ja-JP: "投稿日時" + en-US: "The posted date of this note" viaMobile: type: "boolean" optional: true desc: - ja: "モバイル端末から投稿したか否か(自己申告であることに留意)" - en: "Whether this note sent via a mobile device" + ja-JP: "モバイル端末から投稿したか否か(自己申告であることに留意)" + en-US: "Whether this note sent via a mobile device" text: type: "string" optional: true desc: - ja: "投稿の本文" - en: "The text of this note" + ja-JP: "投稿の本文" + en-US: "The text of this note" mediaIds: type: "id(DriveFile)[]" optional: true desc: - ja: "添付されているメディアのID (なければレスポンスでは空配列)" - en: "The IDs of the attached media (empty array for response if no media is attached)" + ja-JP: "添付されているメディアのID (なければレスポンスでは空配列)" + en-US: "The IDs of the attached media (empty array for response if no media is attached)" media: type: "entity(DriveFile)[]" optional: true desc: - ja: "添付されているメディア" - en: "The attached media" + ja-JP: "添付されているメディア" + en-US: "The attached media" userId: type: "id(User)" optional: false desc: - ja: "投稿者ID" - en: "The ID of author of this note" + ja-JP: "投稿者ID" + en-US: "The ID of author of this note" user: type: "entity(User)" optional: true desc: - ja: "投稿者" - en: "The author of this note" + ja-JP: "投稿者" + en-US: "The author of this note" myReaction: type: "string" optional: true desc: - ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" - en: "The your <a href='/docs/api/reactions'>reaction</a> of this note" + ja-JP: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>" + en-US: "The your <a href='/docs/api/reactions'>reaction</a> of this note" reactionCounts: type: "object" optional: false desc: - ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" + ja-JP: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト" replyId: type: "id(Note)" optional: true desc: - ja: "返信した投稿のID" - en: "The ID of the replyed note" + ja-JP: "返信した投稿のID" + en-US: "The ID of the replyed note" reply: type: "entity(Note)" optional: true desc: - ja: "返信した投稿" - en: "The replyed note" + ja-JP: "返信した投稿" + en-US: "The replyed note" renoteId: type: "id(Note)" optional: true desc: - ja: "引用した投稿のID" - en: "The ID of the quoted note" + ja-JP: "引用した投稿のID" + en-US: "The ID of the quoted note" renote: type: "entity(Note)" optional: true desc: - ja: "引用した投稿" - en: "The quoted note" + ja-JP: "引用した投稿" + en-US: "The quoted note" poll: type: "object" optional: true desc: - ja: "投票" - en: "The poll" + ja-JP: "投票" + en-US: "The poll" props: choices: type: "object[]" optional: false desc: - ja: "投票の選択肢" - en: "The choices of this poll" + ja-JP: "投票の選択肢" + en-US: "The choices of this poll" props: id: type: "number" optional: false desc: - ja: "選択肢ID" - en: "The ID of this choice" + ja-JP: "選択肢ID" + en-US: "The ID of this choice" isVoted: type: "boolean" optional: true desc: - ja: "自分がこの選択肢に投票したかどうか" - en: "Whether you voted to this choice" + ja-JP: "自分がこの選択肢に投票したかどうか" + en-US: "Whether you voted to this choice" text: type: "string" optional: false desc: - ja: "選択肢本文" - en: "The text of this choice" + ja-JP: "選択肢本文" + en-US: "The text of this choice" votes: type: "number" optional: false desc: - ja: "この選択肢に投票された数" - en: "The number voted for this choice" + ja-JP: "この選択肢に投票された数" + en-US: "The number voted for this choice" geo: type: "object" optional: true desc: - ja: "位置情報" - en: "Geo location" + ja-JP: "位置情報" + en-US: "Geo location" props: coordinates: type: "number[]" optional: false desc: - ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" + ja-JP: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。" altitude: type: "number" optional: false desc: - ja: "高度。メートル単位で表す。" + ja-JP: "高度。メートル単位で表す。" accuracy: type: "number" optional: false desc: - ja: "緯度、経度の精度。メートル単位で表す。" + ja-JP: "緯度、経度の精度。メートル単位で表す。" altitudeAccuracy: type: "number" optional: false desc: - ja: "高度の精度。メートル単位で表す。" + ja-JP: "高度の精度。メートル単位で表す。" heading: type: "number" optional: false desc: - ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" + ja-JP: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。" speed: type: "number" optional: false desc: - ja: "速度。メートル / 秒数で表す。" + ja-JP: "速度。メートル / 秒数で表す。" diff --git a/src/docs/api/entities/user.yaml b/src/docs/api/entities/user.yaml index c245974568..c90b55ee88 100644 --- a/src/docs/api/entities/user.yaml +++ b/src/docs/api/entities/user.yaml @@ -1,174 +1,174 @@ name: "User" desc: - ja: "ユーザー。" - en: "A user." + ja-JP: "ユーザー。" + en-US: "A user." props: id: type: "id" optional: false desc: - ja: "ユーザーID" - en: "The ID of this user" + ja-JP: "ユーザーID" + en-US: "The ID of this user" createdAt: type: "date" optional: false desc: - ja: "アカウント作成日時" - en: "The registered date of this user" + ja-JP: "アカウント作成日時" + en-US: "The registered date of this user" username: type: "string" optional: false desc: - ja: "ユーザー名" - en: "The username of this user" + ja-JP: "ユーザー名" + en-US: "The username of this user" description: type: "string" optional: false desc: - ja: "アカウントの説明(自己紹介)" - en: "The description of this user" + ja-JP: "アカウントの説明(自己紹介)" + en-US: "The description of this user" avatarId: type: "id(DriveFile)" optional: true desc: - ja: "アバターのID" - en: "The ID of the avatar of this user" + ja-JP: "アバターのID" + en-US: "The ID of the avatar of this user" avatarUrl: type: "string" optional: false desc: - ja: "アバターのURL" - en: "The URL of the avatar of this user" + ja-JP: "アバターのURL" + en-US: "The URL of the avatar of this user" bannerId: type: "id(DriveFile)" optional: true desc: - ja: "バナーのID" - en: "The ID of the banner of this user" + ja-JP: "バナーのID" + en-US: "The ID of the banner of this user" bannerUrl: type: "string" optional: false desc: - ja: "バナーのURL" - en: "The URL of the banner of this user" + ja-JP: "バナーのURL" + en-US: "The URL of the banner of this user" followersCount: type: "number" optional: false desc: - ja: "フォロワーの数" - en: "The number of the followers for this user" + ja-JP: "フォロワーの数" + en-US: "The number of the followers for this user" followingCount: type: "number" optional: false desc: - ja: "フォローしているユーザーの数" - en: "The number of the following users for this user" + ja-JP: "フォローしているユーザーの数" + en-US: "The number of the following users for this user" isFollowing: type: "boolean" optional: true desc: - ja: "自分がこのユーザーをフォローしているか" + ja-JP: "自分がこのユーザーをフォローしているか" isFollowed: type: "boolean" optional: true desc: - ja: "自分がこのユーザーにフォローされているか" + ja-JP: "自分がこのユーザーにフォローされているか" isMuted: type: "boolean" optional: true desc: - ja: "自分がこのユーザーをミュートしているか" - en: "Whether you muted this user" + ja-JP: "自分がこのユーザーをミュートしているか" + en-US: "Whether you muted this user" notesCount: type: "number" optional: false desc: - ja: "投稿の数" - en: "The number of the notes of this user" + ja-JP: "投稿の数" + en-US: "The number of the notes of this user" pinnedNote: type: "entity(Note)" optional: true desc: - ja: "ピン留めされた投稿" - en: "The pinned note of this user" + ja-JP: "ピン留めされた投稿" + en-US: "The pinned note of this user" pinnedNoteId: type: "id(Note)" optional: true desc: - ja: "ピン留めされた投稿のID" - en: "The ID of the pinned note of this user" + ja-JP: "ピン留めされた投稿のID" + en-US: "The ID of the pinned note of this user" host: type: "string | null" optional: false desc: - ja: "ホスト (例: example.com:3000)" - en: "Host (e.g. example.com:3000)" + ja-JP: "ホスト (例: example.com:3000)" + en-US: "Host (e.g. example.com:3000)" twitter: type: "object" optional: true desc: - ja: "連携されているTwitterアカウント情報" - en: "The info of the connected twitter account of this user" + ja-JP: "連携されているTwitterアカウント情報" + en-US: "The info of the connected twitter account of this user" props: userId: type: "string" optional: false desc: - ja: "ユーザーID" - en: "The user ID" + ja-JP: "ユーザーID" + en-US: "The user ID" screenName: type: "string" optional: false desc: - ja: "ユーザー名" - en: "The screen name of this user" + ja-JP: "ユーザー名" + en-US: "The screen name of this user" isBot: type: "boolean" optional: true desc: - ja: "botか否か(自己申告であることに留意)" - en: "Whether is bot or not" + ja-JP: "botか否か(自己申告であることに留意)" + en-US: "Whether is bot or not" profile: type: "object" optional: false desc: - ja: "プロフィール" - en: "The profile of this user" + ja-JP: "プロフィール" + en-US: "The profile of this user" props: location: type: "string" optional: true desc: - ja: "場所" - en: "The location of this user" + ja-JP: "場所" + en-US: "The location of this user" birthday: type: "string" optional: true desc: - ja: "誕生日 (YYYY-MM-DD)" - en: "The birthday of this user (YYYY-MM-DD)" + ja-JP: "誕生日 (YYYY-MM-DD)" + en-US: "The birthday of this user (YYYY-MM-DD)" diff --git a/src/docs/api/entities/view.pug b/src/docs/api/entities/view.pug index d5c192f438..1f166d053c 100644 --- a/src/docs/api/entities/view.pug +++ b/src/docs/api/entities/view.pug @@ -7,7 +7,7 @@ block meta block main h1= name - p#desc= desc[lang] || desc['ja'] + p#desc= desc[lang] || desc['ja-JP'] section h2= i18n('docs.api.entities.properties') diff --git a/src/docs/api/mixins.pug b/src/docs/api/mixins.pug index 925aab2934..563739d52b 100644 --- a/src/docs/api/mixins.pug +++ b/src/docs/api/mixins.pug @@ -31,4 +31,4 @@ mixin propTable(props) td.name= prop.name td.type +type(prop) - td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja'] : null + td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja-JP'] : null diff --git a/src/docs/base.pug b/src/docs/base.pug index aeafaeffff..26f19ddf09 100644 --- a/src/docs/base.pug +++ b/src/docs/base.pug @@ -16,7 +16,7 @@ html(lang= lang) nav ul each doc in docs - li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja'] + li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja-JP'] section h2 API ul diff --git a/src/docs/follow.ja.md b/src/docs/follow.ja-JP.md index a883435ab4..a883435ab4 100644 --- a/src/docs/follow.ja.md +++ b/src/docs/follow.ja-JP.md diff --git a/src/docs/mute.ja.md b/src/docs/mute.ja-JP.md index 6a9608662a..6a9608662a 100644 --- a/src/docs/mute.ja.md +++ b/src/docs/mute.ja-JP.md diff --git a/src/docs/reversi-bot.ja.md b/src/docs/reversi-bot.ja-JP.md index 98b543ca6c..98b543ca6c 100644 --- a/src/docs/reversi-bot.ja.md +++ b/src/docs/reversi-bot.ja-JP.md diff --git a/src/docs/stream.ja.md b/src/docs/stream.ja-JP.md index c720299932..c720299932 100644 --- a/src/docs/stream.ja.md +++ b/src/docs/stream.ja-JP.md diff --git a/src/docs/timelines.ja.md b/src/docs/timelines.ja-JP.md index 36ba61bd2d..36ba61bd2d 100644 --- a/src/docs/timelines.ja.md +++ b/src/docs/timelines.ja-JP.md diff --git a/src/mfm/parse/core/syntax-highlighter.ts b/src/mfm/parse/core/syntax-highlighter.ts index 3fb7a3b73d..2b13608d2b 100644 --- a/src/mfm/parse/core/syntax-highlighter.ts +++ b/src/mfm/parse/core/syntax-highlighter.ts @@ -197,7 +197,7 @@ const elements: Element[] = [ if (thisIsNotARegexp) return null; if (regexp == '') return null; - if (regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') return null; + if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null; return { html: `<span class="regexp">/${escape(regexp)}/</span>`, diff --git a/src/mfm/parse/elements/hashtag.ts b/src/mfm/parse/elements/hashtag.ts index 129041774f..f4b6a78fa8 100644 --- a/src/mfm/parse/elements/hashtag.ts +++ b/src/mfm/parse/elements/hashtag.ts @@ -10,7 +10,7 @@ export type TextElementHashtag = { export default function(text: string, i: number) { if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null; - const isHead = text[0] == '#'; + const isHead = text.startsWith('#'); const hashtag = text.match(/^\s?#[^\s]+/)[0]; const res: any[] = !isHead ? [{ type: 'text', diff --git a/src/mfm/parse/elements/link.ts b/src/mfm/parse/elements/link.ts index b353aebc5c..796aeb1ab3 100644 --- a/src/mfm/parse/elements/link.ts +++ b/src/mfm/parse/elements/link.ts @@ -13,7 +13,7 @@ export type TextElementLink = { export default function(text: string) { const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/); if (!match) return null; - const silent = text[0] == '?'; + const silent = text.startsWith('?'); const link = match[0]; const title = match[1]; const url = match[2]; diff --git a/src/misc/fa.ts b/src/misc/fa.ts index 077bb51e6d..8be06362c3 100644 --- a/src/misc/fa.ts +++ b/src/misc/fa.ts @@ -25,9 +25,9 @@ export const replacement = (match: string, key: string) => { arg == 'S' ? 'fas' : arg == 'B' ? 'fab' : ''; - } else if (arg[0] == '.') { + } else if (arg.startsWith('.')) { classes.push('fa-' + arg.substr(1)); - } else if (arg[0] == '-') { + } else if (arg.startsWith('-')) { transform = arg.substr(1).split('|').join(' '); } else { name = arg; diff --git a/src/misc/i18n.ts b/src/misc/i18n.ts index a07af3e939..3dbfd7fe7b 100644 --- a/src/misc/i18n.ts +++ b/src/misc/i18n.ts @@ -27,10 +27,12 @@ export default class Replacer { let text = texts; if (path) { + path = path.replace('.ts', ''); + if (text.hasOwnProperty(path)) { text = text[path]; } else { - if (this.lang === 'ja') console.warn(`path '${path}' not found`); + if (this.lang === 'ja-JP') console.warn(`path '${path}' not found`); return key; // Fallback } } @@ -46,10 +48,10 @@ export default class Replacer { }); if (error) { - if (this.lang === 'ja') console.warn(`key '${key}' not found in '${path}'`); + if (this.lang === 'ja-JP') console.warn(`key '${key}' not found in '${path}'`); return key; // Fallback } else if (typeof text !== 'string') { - if (this.lang === 'ja') console.warn(`key '${key}' is not string in '${path}'`); + if (this.lang === 'ja-JP') console.warn(`key '${key}' is not string in '${path}'`); return key; // Fallback } else { return text; diff --git a/src/models/stats.ts b/src/models/stats.ts index 7bff475c63..326bfacc80 100644 --- a/src/models/stats.ts +++ b/src/models/stats.ts @@ -2,40 +2,59 @@ import * as mongo from 'mongodb'; import db from '../db/mongodb'; const Stats = db.get<IStats>('stats'); -Stats.createIndex({ date: -1 }, { unique: true }); +Stats.dropIndex({ date: -1 }); // 後方互換性のため +Stats.createIndex({ span: -1, date: -1 }, { unique: true }); export default Stats; export interface IStats { _id: mongo.ObjectID; + /** + * 集計日時 + */ date: Date; /** + * 集計期間 + */ + span: 'day' | 'hour'; + + /** * ユーザーに関する統計 */ users: { local: { /** - * この日時点での、ローカルのユーザーの総計 + * 集計期間時点での、全ユーザー数 (ローカル) */ total: number; /** - * ローカルのユーザー数の前日比 + * 増加したユーザー数 (ローカル) + */ + inc: number; + + /** + * 減少したユーザー数 (ローカル) */ - diff: number; + dec: number; }; remote: { /** - * この日時点での、リモートのユーザーの総計 + * 集計期間時点での、全ユーザー数 (リモート) */ total: number; /** - * リモートのユーザー数の前日比 + * 増加したユーザー数 (リモート) */ - diff: number; + inc: number; + + /** + * 減少したユーザー数 (リモート) + */ + dec: number; }; }; @@ -45,28 +64,33 @@ export interface IStats { notes: { local: { /** - * この日時点での、ローカルの投稿の総計 + * 集計期間時点での、全投稿数 (ローカル) */ total: number; /** - * ローカルの投稿数の前日比 + * 増加した投稿数 (ローカル) */ - diff: number; + inc: number; + + /** + * 減少した投稿数 (ローカル) + */ + dec: number; diffs: { /** - * ローカルの通常の投稿数の前日比 + * 通常の投稿数の差分 (ローカル) */ normal: number; /** - * ローカルのリプライの投稿数の前日比 + * リプライの投稿数の差分 (ローカル) */ reply: number; /** - * ローカルのRenoteの投稿数の前日比 + * Renoteの投稿数の差分 (ローカル) */ renote: number; }; @@ -74,28 +98,33 @@ export interface IStats { remote: { /** - * この日時点での、リモートの投稿の総計 + * 集計期間時点での、全投稿数 (リモート) */ total: number; /** - * リモートの投稿数の前日比 + * 増加した投稿数 (リモート) */ - diff: number; + inc: number; + + /** + * 減少した投稿数 (リモート) + */ + dec: number; diffs: { /** - * リモートの通常の投稿数の前日比 + * 通常の投稿数の差分 (リモート) */ normal: number; /** - * リモートのリプライの投稿数の前日比 + * リプライの投稿数の差分 (リモート) */ reply: number; /** - * リモートのRenoteの投稿数の前日比 + * Renoteの投稿数の差分 (リモート) */ renote: number; }; @@ -108,46 +137,66 @@ export interface IStats { drive: { local: { /** - * この日時点での、ローカルのドライブファイル数の総計 + * 集計期間時点での、全ドライブファイル数 (ローカル) */ totalCount: number; /** - * この日時点での、ローカルのドライブファイルサイズの総計 + * 集計期間時点での、全ドライブファイルの合計サイズ (ローカル) */ totalSize: number; /** - * ローカルのドライブファイル数の前日比 + * 増加したドライブファイル数 (ローカル) + */ + incCount: number; + + /** + * 増加したドライブ使用量 (ローカル) + */ + incSize: number; + + /** + * 減少したドライブファイル数 (ローカル) */ - diffCount: number; + decCount: number; /** - * ローカルのドライブファイルサイズの前日比 + * 減少したドライブ使用量 (ローカル) */ - diffSize: number; + decSize: number; }; remote: { /** - * この日時点での、リモートのドライブファイル数の総計 + * 集計期間時点での、全ドライブファイル数 (リモート) */ totalCount: number; /** - * この日時点での、リモートのドライブファイルサイズの総計 + * 集計期間時点での、全ドライブファイルの合計サイズ (リモート) */ totalSize: number; /** - * リモートのドライブファイル数の前日比 + * 増加したドライブファイル数 (リモート) + */ + incCount: number; + + /** + * 増加したドライブ使用量 (リモート) + */ + incSize: number; + + /** + * 減少したドライブファイル数 (リモート) */ - diffCount: number; + decCount: number; /** - * リモートのドライブファイルサイズの前日比 + * 減少したドライブ使用量 (リモート) */ - diffSize: number; + decSize: number; }; }; } diff --git a/src/queue/processors/http/process-inbox.ts b/src/queue/processors/http/process-inbox.ts index 0738853dd1..c9c2fa72cb 100644 --- a/src/queue/processors/http/process-inbox.ts +++ b/src/queue/processors/http/process-inbox.ts @@ -46,7 +46,7 @@ export default async (job: bq.Job, done: any): Promise<void> => { // アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する if (user === null) { - user = await resolvePerson(signature.keyId) as IRemoteUser; + user = await resolvePerson(activity.actor) as IRemoteUser; } } diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 02bce6fec7..1dfeebfdf7 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -131,5 +131,7 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver): //#endregion // リモートサーバーからフェッチしてきて登録 - return await createNote(value, resolver); + // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが + // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 + return await createNote(uri, resolver); } diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 61bcf77c43..3bd4e16763 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -4,18 +4,25 @@ import * as debug from 'debug'; import config from '../../../config'; import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user'; -import webFinger from '../../webfinger'; import Resolver from '../resolver'; import { resolveImage } from './image'; -import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; +import { isCollectionOrOrderedCollection, IPerson } from '../type'; import { IDriveFile } from '../../../models/drive-file'; import Meta from '../../../models/meta'; import htmlToMFM from '../../../mfm/html-to-mfm'; import { updateUserStats } from '../../../services/update-chart'; +import { URL } from 'url'; const log = debug('misskey:activitypub'); -function validatePerson(x: any) { +/** + * Validate Person object + * @param x Fetched person object + * @param uri Fetch target URI + */ +function validatePerson(x: any, uri: string) { + const expectHost = toUnicode(new URL(uri).hostname.toLowerCase()); + if (x == null) { return new Error('invalid person: object is null'); } @@ -40,6 +47,24 @@ function validatePerson(x: any) { return new Error('invalid person: invalid name'); } + if (typeof x.id !== 'string') { + return new Error('invalid person: id is not a string'); + } + + const idHost = toUnicode(new URL(x.id).hostname.toLowerCase()); + if (idHost !== expectHost) { + return new Error('invalid person: id has different host'); + } + + if (typeof x.publicKey.id !== 'string') { + return new Error('invalid person: publicKey.id is not a string'); + } + + const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase()); + if (publicKeyIdHost !== expectHost) { + return new Error('invalid person: publicKey.id has different host'); + } + return null; } @@ -48,8 +73,8 @@ function validatePerson(x: any) { * * Misskeyに対象のPersonが登録されていればそれを返します。 */ -export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise<IUser> { - const uri = typeof value == 'string' ? value : value.id; +export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> { + if (typeof uri !== 'string') throw 'uri is not string'; // URIがこのサーバーを指しているならデータベースからフェッチ if (uri.startsWith(config.url + '/')) { @@ -71,12 +96,14 @@ export async function fetchPerson(value: string | IObject, resolver?: Resolver): /** * Personを作成します。 */ -export async function createPerson(value: any, resolver?: Resolver): Promise<IUser> { +export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> { + if (typeof uri !== 'string') throw 'uri is not string'; + if (resolver == null) resolver = new Resolver(); - const object = await resolver.resolve(value) as any; + const object = await resolver.resolve(uri) as any; - const err = validatePerson(object); + const err = validatePerson(object, uri); if (err) { throw err; @@ -86,7 +113,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs log(`Creating the Person: ${person.id}`); - const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([ + const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([ resolver.resolve(person.followers).then( resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, () => undefined @@ -98,11 +125,10 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs resolver.resolve(person.outbox).then( resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined, () => undefined - ), - webFinger(person.id) + ) ]); - const host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase(); + const host = toUnicode(new URL(object.id).hostname.toLowerCase()); const isBot = object.type == 'Service'; @@ -166,8 +192,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs const avatarId = avatar ? avatar._id : null; const bannerId = banner ? banner._id : null; - const avatarUrl = avatar && avatar.metadata.url ? avatar.metadata.url : null; - const bannerUrl = banner && banner.metadata.url ? banner.metadata.url : null; + const avatarUrl = (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null; + const bannerUrl = (banner && banner.metadata.url) ? banner.metadata.url : null; await User.update({ _id: user._id }, { $set: { @@ -192,8 +218,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs * * Misskeyに対象のPersonが登録されていなければ無視します。 */ -export async function updatePerson(value: string | IObject, resolver?: Resolver): Promise<void> { - const uri = typeof value == 'string' ? value : value.id; +export async function updatePerson(uri: string, resolver?: Resolver): Promise<void> { + if (typeof uri !== 'string') throw 'uri is not string'; // URIがこのサーバーを指しているならスキップ if (uri.startsWith(config.url + '/')) { @@ -210,9 +236,9 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) if (resolver == null) resolver = new Resolver(); - const object = await resolver.resolve(value) as any; + const object = await resolver.resolve(uri) as any; - const err = validatePerson(object); + const err = validatePerson(object, uri); if (err) { throw err; @@ -255,7 +281,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) sharedInbox: person.sharedInbox, avatarId: avatar ? avatar._id : null, bannerId: banner ? banner._id : null, - avatarUrl: avatar && avatar.metadata.url ? avatar.metadata.url : null, + avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null, bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null, description: htmlToMFM(person.summary), followersCount, @@ -275,8 +301,8 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ -export async function resolvePerson(value: string | IObject, verifier?: string): Promise<IUser> { - const uri = typeof value == 'string' ? value : value.id; +export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> { + if (typeof uri !== 'string') throw 'uri is not string'; //#region このサーバーに既に登録されていたらそれを返す const exist = await fetchPerson(uri); @@ -287,5 +313,5 @@ export async function resolvePerson(value: string | IObject, verifier?: string): //#endregion // リモートサーバーからフェッチしてきて登録 - return await createPerson(value); + return await createPerson(uri); } diff --git a/src/remote/activitypub/renderer/announce.ts b/src/remote/activitypub/renderer/announce.ts index 6d5a67b5c3..f6276ade04 100644 --- a/src/remote/activitypub/renderer/announce.ts +++ b/src/remote/activitypub/renderer/announce.ts @@ -6,6 +6,7 @@ export default (object: any, note: INote) => { return { id: `${config.url}/notes/${note._id}`, + actor: `${config.url}/users/${note.userId}`, type: 'Announce', published: note.createdAt.toISOString(), to: ['https://www.w3.org/ns/activitystreams#Public'], diff --git a/src/remote/activitypub/renderer/create.ts b/src/remote/activitypub/renderer/create.ts index b8bf98a655..1ee1418fce 100644 --- a/src/remote/activitypub/renderer/create.ts +++ b/src/remote/activitypub/renderer/create.ts @@ -1,4 +1,17 @@ -export default (object: any) => ({ - type: 'Create', - object -}); +import config from '../../../config'; +import { INote } from '../../../models/note'; + +export default (object: any, note: INote) => { + const activity = { + id: `${config.url}/notes/${note._id}/activity`, + actor: `${config.url}/users/${note.userId}`, + type: 'Create', + published: note.createdAt.toISOString(), + object + } as any; + + if (object.to) activity.to = object.to; + if (object.cc) activity.cc = object.cc; + + return activity; +}; diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts index f468a22e24..2a4e70e25e 100644 --- a/src/remote/activitypub/renderer/delete.ts +++ b/src/remote/activitypub/renderer/delete.ts @@ -1,4 +1,8 @@ -export default (object: any) => ({ +import config from '../../../config'; +import { ILocalUser } from "../../../models/user"; + +export default (object: any, user: ILocalUser) => ({ type: 'Delete', + actor: `${config.url}/users/${user._id}`, object }); diff --git a/src/remote/activitypub/renderer/index.ts b/src/remote/activitypub/renderer/index.ts index ee7f496162..55b2801cad 100644 --- a/src/remote/activitypub/renderer/index.ts +++ b/src/remote/activitypub/renderer/index.ts @@ -1,7 +1,16 @@ -export default (x: any) => Object.assign({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - { Hashtag: 'as:Hashtag' } - ] -}, x); +import config from '../../../config'; +import * as uuid from 'uuid'; + +export default (x: any) => { + if (x !== null && typeof x === 'object' && x.id == null) { + x.id = `${config.url}/${uuid.v4()}`; + } + + return Object.assign({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + { Hashtag: 'as:Hashtag' } + ] + }, x); +}; diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts index 4498409a57..bf90a3f281 100644 --- a/src/remote/activitypub/renderer/undo.ts +++ b/src/remote/activitypub/renderer/undo.ts @@ -1,4 +1,8 @@ -export default (object: any) => ({ +import config from '../../../config'; +import { ILocalUser, IUser } from "../../../models/user"; + +export default (object: any, user: ILocalUser | IUser) => ({ type: 'Undo', + actor: `${config.url}/users/${user._id}`, object }); diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts index 585c1c0ce8..6238d3acb1 100644 --- a/src/remote/activitypub/request.ts +++ b/src/remote/activitypub/request.ts @@ -19,6 +19,9 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso port, method: 'POST', path: pathname + search, + headers: { + 'Content-Type': 'application/activity+json' + } }, res => { log(`${url} --> ${res.statusCode}`); @@ -32,7 +35,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso sign(req, { authorizationHeaderName: 'Signature', key: user.keypair, - keyId: `acct:${user.username}@${config.host}` + keyId: `${config.url}/users/${user._id}/publickey` }); // Signature: Signature ... => Signature: ... diff --git a/src/remote/resolve-user.ts b/src/remote/resolve-user.ts index 1e8fc5d750..e199b6f147 100644 --- a/src/remote/resolve-user.ts +++ b/src/remote/resolve-user.ts @@ -15,7 +15,7 @@ export default async (username: string, _host: string, option?: any): Promise<IU const host = toUnicode(hostAscii); if (config.host == host) { - return await User.findOne({ usernameLower }); + return await User.findOne({ usernameLower, host: null }); } let user = await User.findOne({ usernameLower, host }, option); diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts index d04ffd38f9..1007790ca6 100644 --- a/src/server/activitypub.ts +++ b/src/server/activitypub.ts @@ -25,7 +25,7 @@ function inbox(ctx: Router.IRouterContext) { ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature; try { - signature = httpSignature.parseRequest(ctx.req); + signature = httpSignature.parseRequest(ctx.req, { 'headers': [] }); } catch (e) { ctx.status = 401; return; diff --git a/src/server/api/common/is-native-token.ts b/src/server/api/common/is-native-token.ts index 0769a4812e..6afbc99ab5 100644 --- a/src/server/api/common/is-native-token.ts +++ b/src/server/api/common/is-native-token.ts @@ -1 +1 @@ -export default (token: string) => token[0] == '!'; +export default (token: string) => token.startsWith('!'); diff --git a/src/server/api/endpoints/admin/chart.ts b/src/server/api/endpoints/admin/chart.ts deleted file mode 100644 index a0566b11f5..0000000000 --- a/src/server/api/endpoints/admin/chart.ts +++ /dev/null @@ -1,101 +0,0 @@ -import Stats, { IStats } from '../../../../models/stats'; - -type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; - -export const meta = { - requireCredential: true, - requireAdmin: true -}; - -export default (params: any) => new Promise(async (res, rej) => { - const now = new Date(); - const y = now.getFullYear(); - const m = now.getMonth(); - const d = now.getDate(); - - const stats = await Stats.find({ - date: { - $gt: new Date(y - 1, m, d) - } - }, { - sort: { - date: -1 - }, - fields: { - _id: 0 - } - }); - - const chart: Array<Omit<IStats, '_id'>> = []; - - for (let i = 364; i >= 0; i--) { - const day = new Date(y, m, d - i); - - const stat = stats.find(s => s.date.getTime() == day.getTime()); - - if (stat) { - chart.unshift(stat); - } else { // 隙間埋め - const mostRecent = stats.find(s => s.date.getTime() < day.getTime()); - if (mostRecent) { - chart.unshift(Object.assign({}, mostRecent, { - date: day - })); - } else { - chart.unshift({ - date: day, - users: { - local: { - total: 0, - diff: 0 - }, - remote: { - total: 0, - diff: 0 - } - }, - notes: { - local: { - total: 0, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - }, - remote: { - total: 0, - diff: 0, - diffs: { - normal: 0, - reply: 0, - renote: 0 - } - } - }, - drive: { - local: { - totalCount: 0, - totalSize: 0, - diffCount: 0, - diffSize: 0 - }, - remote: { - totalCount: 0, - totalSize: 0, - diffCount: 0, - diffSize: 0 - } - } - }); - } - } - } - - chart.forEach(x => { - delete x.date; - }); - - res(chart); -}); diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts index 77608e715c..892b2579f2 100644 --- a/src/server/api/endpoints/admin/invite.ts +++ b/src/server/api/endpoints/admin/invite.ts @@ -3,7 +3,7 @@ import RegistrationTicket from '../../../../models/registration-tickets'; export const meta = { desc: { - ja: '招待コードを発行します。' + 'ja-JP': '招待コードを発行します。' }, requireCredential: true, diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts index 9b492c6e15..32c2416fb5 100644 --- a/src/server/api/endpoints/admin/suspend-user.ts +++ b/src/server/api/endpoints/admin/suspend-user.ts @@ -5,8 +5,8 @@ import User from '../../../../models/user'; export const meta = { desc: { - ja: '指定したユーザーを凍結します。', - en: 'Suspend a user.' + 'ja-JP': '指定したユーザーを凍結します。', + 'en-US': 'Suspend a user.' }, requireCredential: true, @@ -15,8 +15,8 @@ export const meta = { params: { userId: $.type(ID).note({ desc: { - ja: '対象のユーザーID', - en: 'The user ID which you want to suspend' + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to suspend' } }), } diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts index 8409bd1b76..879c23ab14 100644 --- a/src/server/api/endpoints/admin/unsuspend-user.ts +++ b/src/server/api/endpoints/admin/unsuspend-user.ts @@ -5,8 +5,8 @@ import User from '../../../../models/user'; export const meta = { desc: { - ja: '指定したユーザーの凍結を解除します。', - en: 'Unsuspend a user.' + 'ja-JP': '指定したユーザーの凍結を解除します。', + 'en-US': 'Unsuspend a user.' }, requireCredential: true, @@ -15,8 +15,8 @@ export const meta = { params: { userId: $.type(ID).note({ desc: { - ja: '対象のユーザーID', - en: 'The user ID which you want to unsuspend' + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to unsuspend' } }), } diff --git a/src/server/api/endpoints/admin/unverify-user.ts b/src/server/api/endpoints/admin/unverify-user.ts index 34653cd78a..178049fa1d 100644 --- a/src/server/api/endpoints/admin/unverify-user.ts +++ b/src/server/api/endpoints/admin/unverify-user.ts @@ -5,8 +5,8 @@ import User from '../../../../models/user'; export const meta = { desc: { - ja: '指定したユーザーの公式アカウントを解除します。', - en: 'Mark a user as unverified.' + 'ja-JP': '指定したユーザーの公式アカウントを解除します。', + 'en-US': 'Mark a user as unverified.' }, requireCredential: true, @@ -15,8 +15,8 @@ export const meta = { params: { userId: $.type(ID).note({ desc: { - ja: '対象のユーザーID', - en: 'The user ID which you want to unverify' + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to unverify' } }), } diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts new file mode 100644 index 0000000000..2c7929fabe --- /dev/null +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -0,0 +1,37 @@ +import $ from 'cafy'; +import Meta from '../../../../models/meta'; +import getParams from '../../get-params'; + +export const meta = { + desc: { + 'ja-JP': 'インスタンスの設定を更新します。' + }, + + requireCredential: true, + requireAdmin: true, + + params: { + disableRegistration: $.bool.optional.nullable.note({ + desc: { + 'ja-JP': '招待制か否か' + } + }), + } +}; + +export default (params: any) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) return rej(psErr); + + const set = {} as any; + + if (ps.disableRegistration === true || ps.disableRegistration === false) { + set.disableRegistration = ps.disableRegistration; + } + + await Meta.update({}, { + $set: set + }, { upsert: true }); + + res(); +}); diff --git a/src/server/api/endpoints/admin/verify-user.ts b/src/server/api/endpoints/admin/verify-user.ts index 5b826eb1c3..dd07684ded 100644 --- a/src/server/api/endpoints/admin/verify-user.ts +++ b/src/server/api/endpoints/admin/verify-user.ts @@ -5,8 +5,8 @@ import User from '../../../../models/user'; export const meta = { desc: { - ja: '指定したユーザーを公式アカウントにします。', - en: 'Mark a user as verified.' + 'ja-JP': '指定したユーザーを公式アカウントにします。', + 'en-US': 'Mark a user as verified.' }, requireCredential: true, @@ -15,8 +15,8 @@ export const meta = { params: { userId: $.type(ID).note({ desc: { - ja: '対象のユーザーID', - en: 'The user ID which you want to verify' + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to verify' } }), } diff --git a/src/server/api/endpoints/chart.ts b/src/server/api/endpoints/chart.ts new file mode 100644 index 0000000000..7da970131e --- /dev/null +++ b/src/server/api/endpoints/chart.ts @@ -0,0 +1,256 @@ +import $ from 'cafy'; +import Stats, { IStats } from '../../../models/stats'; +import getParams from '../get-params'; + +type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; + +function migrateStats(stats: IStats[]) { + stats.forEach(stat => { + const isOldData = + stat.users.local.inc == null || + stat.users.local.dec == null || + stat.users.remote.inc == null || + stat.users.remote.dec == null || + stat.notes.local.inc == null || + stat.notes.local.dec == null || + stat.notes.remote.inc == null || + stat.notes.remote.dec == null || + stat.drive.local.incCount == null || + stat.drive.local.decCount == null || + stat.drive.local.incSize == null || + stat.drive.local.decSize == null || + stat.drive.remote.incCount == null || + stat.drive.remote.decCount == null || + stat.drive.remote.incSize == null || + stat.drive.remote.decSize == null; + + if (!isOldData) return; + + stat.users.local.inc = (stat as any).users.local.diff; + stat.users.local.dec = 0; + stat.users.remote.inc = (stat as any).users.remote.diff; + stat.users.remote.dec = 0; + stat.notes.local.inc = (stat as any).notes.local.diff; + stat.notes.local.dec = 0; + stat.notes.remote.inc = (stat as any).notes.remote.diff; + stat.notes.remote.dec = 0; + stat.drive.local.incCount = (stat as any).drive.local.diffCount; + stat.drive.local.decCount = 0; + stat.drive.local.incSize = (stat as any).drive.local.diffSize; + stat.drive.local.decSize = 0; + stat.drive.remote.incCount = (stat as any).drive.remote.diffCount; + stat.drive.remote.decCount = 0; + stat.drive.remote.incSize = (stat as any).drive.remote.diffSize; + stat.drive.remote.decSize = 0; + }); +} + +export const meta = { + desc: { + 'ja-JP': 'インスタンスの統計を取得します。' + }, + + params: { + limit: $.num.optional.range(1, 100).note({ + default: 30, + desc: { + 'ja-JP': '最大数' + } + }), + } +}; + +export default (params: any) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; + + const daysRange = ps.limit; + const hoursRange = ps.limit; + + const now = new Date(); + const y = now.getFullYear(); + const m = now.getMonth(); + const d = now.getDate(); + const h = now.getHours(); + + const [statsPerDay, statsPerHour] = await Promise.all([ + Stats.find({ + span: 'day', + date: { + $gt: new Date(y, m, d - daysRange) + } + }, { + sort: { + date: -1 + }, + fields: { + _id: 0 + } + }), + Stats.find({ + span: 'hour', + date: { + $gt: new Date(y, m, d, h - hoursRange) + } + }, { + sort: { + date: -1 + }, + fields: { + _id: 0 + } + }), + ]); + + // 後方互換性のため + migrateStats(statsPerDay); + migrateStats(statsPerHour); + + const format = (src: IStats[], span: 'day' | 'hour') => { + const chart: Array<Omit<Omit<IStats, '_id'>, 'span'>> = []; + + const range = + span == 'day' ? daysRange : + span == 'hour' ? hoursRange : + null; + + for (let i = (range - 1); i >= 0; i--) { + const current = + span == 'day' ? new Date(y, m, d - i) : + span == 'hour' ? new Date(y, m, d, h - i) : + null; + + const stat = src.find(s => s.date.getTime() == current.getTime()); + + if (stat) { + chart.unshift(stat); + } else { // 隙間埋め + const mostRecent = src.find(s => s.date.getTime() < current.getTime()); + if (mostRecent) { + chart.unshift({ + date: current, + users: { + local: { + total: mostRecent.users.local.total, + inc: 0, + dec: 0 + }, + remote: { + total: mostRecent.users.remote.total, + inc: 0, + dec: 0 + } + }, + notes: { + local: { + total: mostRecent.notes.local.total, + inc: 0, + dec: 0, + diffs: { + normal: 0, + reply: 0, + renote: 0 + } + }, + remote: { + total: mostRecent.notes.remote.total, + inc: 0, + dec: 0, + diffs: { + normal: 0, + reply: 0, + renote: 0 + } + } + }, + drive: { + local: { + totalCount: mostRecent.drive.local.totalCount, + totalSize: mostRecent.drive.local.totalSize, + incCount: 0, + incSize: 0, + decCount: 0, + decSize: 0 + }, + remote: { + totalCount: mostRecent.drive.remote.totalCount, + totalSize: mostRecent.drive.remote.totalSize, + incCount: 0, + incSize: 0, + decCount: 0, + decSize: 0 + } + } + }); + } else { + chart.unshift({ + date: current, + users: { + local: { + total: 0, + inc: 0, + dec: 0 + }, + remote: { + total: 0, + inc: 0, + dec: 0 + } + }, + notes: { + local: { + total: 0, + inc: 0, + dec: 0, + diffs: { + normal: 0, + reply: 0, + renote: 0 + } + }, + remote: { + total: 0, + inc: 0, + dec: 0, + diffs: { + normal: 0, + reply: 0, + renote: 0 + } + } + }, + drive: { + local: { + totalCount: 0, + totalSize: 0, + incCount: 0, + incSize: 0, + decCount: 0, + decSize: 0 + }, + remote: { + totalCount: 0, + totalSize: 0, + incCount: 0, + incSize: 0, + decCount: 0, + decSize: 0 + } + } + }); + } + } + } + + chart.forEach(x => { + delete (x as any).span; + }); + + return chart; + }; + + res({ + perDay: format(statsPerDay, 'day'), + perHour: format(statsPerHour, 'hour') + }); +}); diff --git a/src/server/api/endpoints/drive.ts b/src/server/api/endpoints/drive.ts index 8ad961494f..063cd475d4 100644 --- a/src/server/api/endpoints/drive.ts +++ b/src/server/api/endpoints/drive.ts @@ -4,8 +4,8 @@ import config from '../../../config'; export const meta = { desc: { - ja: 'ドライブの情報を取得します。', - en: 'Get drive information.' + 'ja-JP': 'ドライブの情報を取得します。', + 'en-US': 'Get drive information.' }, requireCredential: true, diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index 063b4adde1..dc6a602e10 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: 'ドライブのファイル一覧を取得します。', - en: 'Get files of drive.' + 'ja-JP': 'ドライブのファイル一覧を取得します。', + 'en-US': 'Get files of drive.' }, requireCredential: true, diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index 41b7e04b46..dfbd11d0c2 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -8,8 +8,8 @@ import getParams from '../../../get-params'; export const meta = { desc: { - ja: 'ドライブにファイルをアップロードします。', - en: 'Upload a file to drive.' + 'ja-JP': 'ドライブにファイルをアップロードします。', + 'en-US': 'Upload a file to drive.' }, requireCredential: true, @@ -27,15 +27,15 @@ export const meta = { folderId: $.type(ID).optional.nullable.note({ default: null, desc: { - ja: 'フォルダID' + 'ja-JP': 'フォルダID' } }), isSensitive: $.bool.optional.note({ default: false, desc: { - ja: 'このメディアが「閲覧注意」(NSFW)かどうか', - en: 'Whether this media is NSFW' + 'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか', + 'en-US': 'Whether this media is NSFW' } }) } diff --git a/src/server/api/endpoints/drive/files/delete.ts b/src/server/api/endpoints/drive/files/delete.ts index 02cd96dd8f..fb7340df38 100644 --- a/src/server/api/endpoints/drive/files/delete.ts +++ b/src/server/api/endpoints/drive/files/delete.ts @@ -6,8 +6,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: 'ドライブのファイルを削除します。', - en: 'Delete a file of drive.' + 'ja-JP': 'ドライブのファイルを削除します。', + 'en-US': 'Delete a file of drive.' }, requireCredential: true, diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts index 6a66c7a272..718fb8c2d7 100644 --- a/src/server/api/endpoints/drive/files/show.ts +++ b/src/server/api/endpoints/drive/files/show.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '指定したドライブのファイルの情報を取得します。', - en: 'Get specified file of drive.' + 'ja-JP': '指定したドライブのファイルの情報を取得します。', + 'en-US': 'Get specified file of drive.' }, requireCredential: true, diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index 9ae2719aa7..ba9abfec61 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -7,8 +7,8 @@ import getParams from '../../../get-params'; export const meta = { desc: { - ja: '指定したドライブのファイルの情報を更新します。', - en: 'Update specified file of drive.' + 'ja-JP': '指定したドライブのファイルの情報を更新します。', + 'en-US': 'Update specified file of drive.' }, requireCredential: true, @@ -18,30 +18,30 @@ export const meta = { params: { fileId: $.type(ID).note({ desc: { - ja: '対象のファイルID' + 'ja-JP': '対象のファイルID' } }), folderId: $.type(ID).optional.nullable.note({ default: undefined, desc: { - ja: 'フォルダID' + 'ja-JP': 'フォルダID' } }), name: $.str.optional.pipe(validateFileName).note({ default: undefined, desc: { - ja: 'ファイル名', - en: 'Name of the file' + 'ja-JP': 'ファイル名', + 'en-US': 'Name of the file' } }), isSensitive: $.bool.optional.note({ default: undefined, desc: { - ja: 'このメディアが「閲覧注意」(NSFW)かどうか', - en: 'Whether this media is NSFW' + 'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか', + 'en-US': 'Whether this media is NSFW' } }) } diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts index d634cf46db..783646feb3 100644 --- a/src/server/api/endpoints/drive/files/upload_from_url.ts +++ b/src/server/api/endpoints/drive/files/upload_from_url.ts @@ -6,7 +6,7 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: 'ドライブに指定されたURLに存在するファイルをアップロードします。' + 'ja-JP': 'ドライブに指定されたURLに存在するファイルをアップロードします。' }, limit: { diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts index de398eb720..19c2ef7aca 100644 --- a/src/server/api/endpoints/drive/folders.ts +++ b/src/server/api/endpoints/drive/folders.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: 'ドライブのフォルダ一覧を取得します。', - en: 'Get folders of drive.' + 'ja-JP': 'ドライブのフォルダ一覧を取得します。', + 'en-US': 'Get folders of drive.' }, requireCredential: true, diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts index 03f9504774..5997dedf0f 100644 --- a/src/server/api/endpoints/drive/folders/create.ts +++ b/src/server/api/endpoints/drive/folders/create.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: 'ドライブのフォルダを作成します。', - en: 'Create a folder of drive.' + 'ja-JP': 'ドライブのフォルダを作成します。', + 'en-US': 'Create a folder of drive.' }, requireCredential: true, diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts index 6a6c879a01..bb25bcba3c 100644 --- a/src/server/api/endpoints/drive/folders/show.ts +++ b/src/server/api/endpoints/drive/folders/show.ts @@ -4,7 +4,7 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '指定したドライブのフォルダの情報を取得します。' + 'ja-JP': '指定したドライブのフォルダの情報を取得します。' }, requireCredential: true, diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts index 1b449428a6..259f373bfc 100644 --- a/src/server/api/endpoints/drive/folders/update.ts +++ b/src/server/api/endpoints/drive/folders/update.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '指定したドライブのフォルダの情報を更新します。', - en: 'Update specified folder of drive.' + 'ja-JP': '指定したドライブのフォルダの情報を更新します。', + 'en-US': 'Update specified folder of drive.' }, requireCredential: true, diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index ebe319e0cf..c9bea0e3d2 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -6,8 +6,8 @@ import create from '../../../../services/following/create'; export const meta = { desc: { - ja: '指定したユーザーをフォローします。', - en: 'Follow a user.' + 'ja-JP': '指定したユーザーをフォローします。', + 'en-US': 'Follow a user.' }, limit: { diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts index 4806fe4e39..f3b4a73ae8 100644 --- a/src/server/api/endpoints/following/delete.ts +++ b/src/server/api/endpoints/following/delete.ts @@ -6,8 +6,8 @@ import deleteFollowing from '../../../../services/following/delete'; export const meta = { desc: { - ja: '指定したユーザーのフォローを解除します。', - en: 'Unfollow a user.' + 'ja-JP': '指定したユーザーのフォローを解除します。', + 'en-US': 'Unfollow a user.' }, limit: { diff --git a/src/server/api/endpoints/following/requests/accept.ts b/src/server/api/endpoints/following/requests/accept.ts index b3bf2dd667..f6a7dcf120 100644 --- a/src/server/api/endpoints/following/requests/accept.ts +++ b/src/server/api/endpoints/following/requests/accept.ts @@ -4,8 +4,8 @@ import User, { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '自分に届いた、指定したフォローリクエストを承認します。', - en: 'Accept a follow request.' + 'ja-JP': '自分に届いた、指定したフォローリクエストを承認します。', + 'en-US': 'Accept a follow request.' }, requireCredential: true, diff --git a/src/server/api/endpoints/following/requests/cancel.ts b/src/server/api/endpoints/following/requests/cancel.ts index 9bfc40ce65..3da4f4734f 100644 --- a/src/server/api/endpoints/following/requests/cancel.ts +++ b/src/server/api/endpoints/following/requests/cancel.ts @@ -4,8 +4,8 @@ import User, { pack, ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '自分が作成した、指定したフォローリクエストをキャンセルします。', - en: 'Cancel a follow request.' + 'ja-JP': '自分が作成した、指定したフォローリクエストをキャンセルします。', + 'en-US': 'Cancel a follow request.' }, requireCredential: true, @@ -27,7 +27,11 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = return rej('followee not found'); } - await cancelFollowRequest(followee, user); + try { + await cancelFollowRequest(followee, user); + } catch (e) { + return rej(e); + } // Send response res(await pack(followee._id, user)); diff --git a/src/server/api/endpoints/following/requests/list.ts b/src/server/api/endpoints/following/requests/list.ts index b06a158c08..11a387cf16 100644 --- a/src/server/api/endpoints/following/requests/list.ts +++ b/src/server/api/endpoints/following/requests/list.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '自分に届いたフォローリクエストの一覧を取得します。', - en: 'Get all pending received follow requests.' + 'ja-JP': '自分に届いたフォローリクエストの一覧を取得します。', + 'en-US': 'Get all pending received follow requests.' }, requireCredential: true, diff --git a/src/server/api/endpoints/following/requests/reject.ts b/src/server/api/endpoints/following/requests/reject.ts index a232549bb8..98febe9e9f 100644 --- a/src/server/api/endpoints/following/requests/reject.ts +++ b/src/server/api/endpoints/following/requests/reject.ts @@ -4,8 +4,8 @@ import User, { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '自分に届いた、指定したフォローリクエストを拒否します。', - en: 'Reject a follow request.' + 'ja-JP': '自分に届いた、指定したフォローリクエストを拒否します。', + 'en-US': 'Reject a follow request.' }, requireCredential: true, diff --git a/src/server/api/endpoints/following/stalk.ts b/src/server/api/endpoints/following/stalk.ts index 79a3fb976c..d44cea2cc4 100644 --- a/src/server/api/endpoints/following/stalk.ts +++ b/src/server/api/endpoints/following/stalk.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: '指定したユーザーをストーキングします。', - en: 'Stalk a user.' + 'ja-JP': '指定したユーザーをストーキングします。', + 'en-US': 'Stalk a user.' }, requireCredential: true, diff --git a/src/server/api/endpoints/following/unstalk.ts b/src/server/api/endpoints/following/unstalk.ts index 71a7a97eeb..8b66f0727e 100644 --- a/src/server/api/endpoints/following/unstalk.ts +++ b/src/server/api/endpoints/following/unstalk.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: '指定したユーザーのストーキングをやめます。', - en: 'Unstalk a user.' + 'ja-JP': '指定したユーザーのストーキングをやめます。', + 'en-US': 'Unstalk a user.' }, requireCredential: true, diff --git a/src/server/api/endpoints/games/reversi/games/surrender.ts b/src/server/api/endpoints/games/reversi/games/surrender.ts index 49821650ed..8ca0143674 100644 --- a/src/server/api/endpoints/games/reversi/games/surrender.ts +++ b/src/server/api/endpoints/games/reversi/games/surrender.ts @@ -6,7 +6,7 @@ import { publishReversiGameStream } from '../../../../../../stream'; export const meta = { desc: { - ja: '指定したリバーシの対局で投了します。' + 'ja-JP': '指定したリバーシの対局で投了します。' }, requireCredential: true, @@ -14,7 +14,7 @@ export const meta = { params: { gameId: $.type(ID).note({ desc: { - ja: '投了したい対局' + 'ja-JP': '投了したい対局' } }) } diff --git a/src/server/api/endpoints/hashtags/search.ts b/src/server/api/endpoints/hashtags/search.ts index 262370cacc..f6fb35b2ff 100644 --- a/src/server/api/endpoints/hashtags/search.ts +++ b/src/server/api/endpoints/hashtags/search.ts @@ -5,7 +5,7 @@ const escapeRegexp = require('escape-regexp'); export const meta = { desc: { - ja: 'ハッシュタグを検索します。' + 'ja-JP': 'ハッシュタグを検索します。' }, requireCredential: false, @@ -14,20 +14,20 @@ export const meta = { limit: $.num.optional.range(1, 100).note({ default: 10, desc: { - ja: '最大数' + 'ja-JP': '最大数' } }), query: $.str.note({ desc: { - ja: 'クエリ' + 'ja-JP': 'クエリ' } }), offset: $.num.optional.min(0).note({ default: 0, desc: { - ja: 'オフセット' + 'ja-JP': 'オフセット' } }) } diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index 7f25c07957..1f99ef2d8d 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -3,7 +3,7 @@ import { IApp } from '../../../models/app'; export const meta = { desc: { - ja: '自分のアカウント情報を取得します。' + 'ja-JP': '自分のアカウント情報を取得します。' }, requireCredential: true, diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index 47c8a87fd9..32c1a55fb0 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: 'お気に入りに登録した投稿一覧を取得します。', - en: 'Get favorited notes' + 'ja-JP': 'お気に入りに登録した投稿一覧を取得します。', + 'en-US': 'Get favorited notes' }, requireCredential: true, diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 922b396797..cdb4eb3f56 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -8,8 +8,8 @@ import config from '../../../../config'; export const meta = { desc: { - ja: 'アカウント情報を更新します。', - en: 'Update myself' + 'ja-JP': 'アカウント情報を更新します。', + 'en-US': 'Update myself' }, requireCredential: true, @@ -84,7 +84,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a if (avatar == null) return rej('avatar not found'); - updates.avatarUrl = avatar.metadata.url || `${config.drive_url}/${avatar._id}`; + updates.avatarUrl = avatar.metadata.thumbnailUrl || avatar.metadata.url || `${config.drive_url}/${avatar._id}`; if (avatar.metadata.properties.avgColor) { updates.avatarColor = avatar.metadata.properties.avgColor; diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index 43cceacf95..1dd08cd13c 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -6,8 +6,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: 'Messagingの履歴を取得します。', - en: 'Show messaging history.' + 'ja-JP': 'Messagingの履歴を取得します。', + 'en-US': 'Show messaging history.' }, requireCredential: true, diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index ae26419bc6..dec0638eed 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -6,8 +6,8 @@ import read from '../../common/read-messaging-message'; export const meta = { desc: { - ja: '指定したユーザーとのMessagingのメッセージ一覧を取得します。', - en: 'Get messages of messaging.' + 'ja-JP': '指定したユーザーとのMessagingのメッセージ一覧を取得します。', + 'en-US': 'Get messages of messaging.' }, requireCredential: true, diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index d33d9e7e77..a6fabcfa45 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -12,8 +12,8 @@ import pushSw from '../../../../../push-sw'; export const meta = { desc: { - ja: '指定したユーザーへMessagingのメッセージを送信します。', - en: 'Create a message of messaging.' + 'ja-JP': '指定したユーザーへMessagingのメッセージを送信します。', + 'en-US': 'Create a message of messaging.' }, requireCredential: true, diff --git a/src/server/api/endpoints/messaging/messages/read.ts b/src/server/api/endpoints/messaging/messages/read.ts index f609337523..581b57579b 100644 --- a/src/server/api/endpoints/messaging/messages/read.ts +++ b/src/server/api/endpoints/messaging/messages/read.ts @@ -6,8 +6,8 @@ import getParams from '../../../get-params'; export const meta = { desc: { - ja: '指定した自分宛てのメッセージを既読にします。', - en: 'Mark as read a message of messaging.' + 'ja-JP': '指定した自分宛てのメッセージを既読にします。', + 'en-US': 'Mark as read a message of messaging.' }, requireCredential: true, @@ -17,8 +17,8 @@ export const meta = { params: { messageId: $.type(ID).note({ desc: { - ja: '既読にするメッセージのID', - en: 'The ID of a message that you want to mark as read' + 'ja-JP': '既読にするメッセージのID', + 'en-US': 'The ID of a message that you want to mark as read' } }) } diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts index bd70cd62ef..5b2e7a8d71 100644 --- a/src/server/api/endpoints/mute/create.ts +++ b/src/server/api/endpoints/mute/create.ts @@ -4,8 +4,8 @@ import Mute from '../../../../models/mute'; export const meta = { desc: { - ja: 'ユーザーをミュートします。', - en: 'Mute a user' + 'ja-JP': 'ユーザーをミュートします。', + 'en-US': 'Mute a user' }, requireCredential: true, diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts index 3187c46f83..e8ed75a847 100644 --- a/src/server/api/endpoints/mute/delete.ts +++ b/src/server/api/endpoints/mute/delete.ts @@ -4,8 +4,8 @@ import Mute from '../../../../models/mute'; export const meta = { desc: { - ja: 'ユーザーのミュートを解除します。', - en: 'Unmute a user' + 'ja-JP': 'ユーザーのミュートを解除します。', + 'en-US': 'Unmute a user' }, requireCredential: true, diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts index e297605338..387b2396f5 100644 --- a/src/server/api/endpoints/mute/list.ts +++ b/src/server/api/endpoints/mute/list.ts @@ -5,8 +5,8 @@ import { getFriendIds } from '../../common/get-friends'; export const meta = { desc: { - ja: 'ミュートしているユーザー一覧を取得します。', - en: 'Get muted users.' + 'ja-JP': 'ミュートしているユーザー一覧を取得します。', + 'en-US': 'Get muted users.' }, requireCredential: true, diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts index 35185db41d..412dff6164 100644 --- a/src/server/api/endpoints/my/apps.ts +++ b/src/server/api/endpoints/my/apps.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: '自分のアプリケーション一覧を取得します。', - en: 'Get my apps' + 'ja-JP': '自分のアプリケーション一覧を取得します。', + 'en-US': 'Get my apps' }, requireCredential: true diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 9cdbec5270..04f5f7562e 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -9,7 +9,7 @@ import getParams from '../../get-params'; export const meta = { desc: { - ja: '投稿します。' + 'ja-JP': '投稿します。' }, requireCredential: true, @@ -25,33 +25,33 @@ export const meta = { visibility: $.str.optional.or(['public', 'home', 'followers', 'specified', 'private']).note({ default: 'public', desc: { - ja: '投稿の公開範囲' + 'ja-JP': '投稿の公開範囲' } }), visibleUserIds: $.arr($.type(ID)).optional.unique().min(1).note({ desc: { - ja: '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー' + 'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー' } }), text: $.str.optional.nullable.pipe(isValidText).note({ default: null, desc: { - ja: '投稿内容' + 'ja-JP': '投稿内容' } }), cw: $.str.optional.nullable.pipe(isValidCw).note({ desc: { - ja: 'コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。' + 'ja-JP': 'コンテンツの警告。このパラメータを指定すると設定したテキストで投稿のコンテンツを隠す事が出来ます。' } }), viaMobile: $.bool.optional.note({ default: false, desc: { - ja: 'モバイルデバイスからの投稿か否か。' + 'ja-JP': 'モバイルデバイスからの投稿か否か。' } }), @@ -66,20 +66,20 @@ export const meta = { speed: $.num.nullable }).optional.nullable.strict().note({ desc: { - ja: '位置情報' + 'ja-JP': '位置情報' }, ref: 'geo' }), mediaIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({ desc: { - ja: '添付するメディア' + 'ja-JP': '添付するメディア' } }), renoteId: $.type(ID).optional.note({ desc: { - ja: 'Renote対象' + 'ja-JP': 'Renote対象' } }), @@ -90,7 +90,7 @@ export const meta = { .each(c => c.length > 0 && c.length < 50) }).optional.strict().note({ desc: { - ja: 'アンケート' + 'ja-JP': 'アンケート' }, ref: 'poll' }) @@ -102,7 +102,7 @@ export const meta = { createdNote: { type: 'entity(Note)', desc: { - ja: '作成した投稿' + 'ja-JP': '作成した投稿' } } } diff --git a/src/server/api/endpoints/notes/delete.ts b/src/server/api/endpoints/notes/delete.ts index 22c6101e14..6d9826cf7b 100644 --- a/src/server/api/endpoints/notes/delete.ts +++ b/src/server/api/endpoints/notes/delete.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: '指定した投稿を削除します。', - en: 'Delete a note.' + 'ja-JP': '指定した投稿を削除します。', + 'en-US': 'Delete a note.' }, requireCredential: true, diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts index 87f6cf1f08..daf7780abc 100644 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '指定した投稿をお気に入りに登録します。', - en: 'Favorite a note.' + 'ja-JP': '指定した投稿をお気に入りに登録します。', + 'en-US': 'Favorite a note.' }, requireCredential: true, diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts index 3906fe99bb..e42b24d324 100644 --- a/src/server/api/endpoints/notes/favorites/delete.ts +++ b/src/server/api/endpoints/notes/favorites/delete.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '指定した投稿のお気に入りを解除します。', - en: 'Unfavorite a note.' + 'ja-JP': '指定した投稿のお気に入りを解除します。', + 'en-US': 'Unfavorite a note.' }, requireCredential: true, diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 3fce4fb9af..2dbb1190c1 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -10,65 +10,65 @@ export const meta = { name: 'notes/hybrid-timeline', desc: { - ja: 'ハイブリッドタイムラインを取得します。' + 'ja-JP': 'ハイブリッドタイムラインを取得します。' }, params: { limit: $.num.optional.range(1, 100).note({ default: 10, desc: { - ja: '最大数' + 'ja-JP': '最大数' } }), sinceId: $.type(ID).optional.note({ desc: { - ja: '指定すると、この投稿を基点としてより新しい投稿を取得します' + 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' } }), untilId: $.type(ID).optional.note({ desc: { - ja: '指定すると、この投稿を基点としてより古い投稿を取得します' + 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' } }), sinceDate: $.num.optional.note({ desc: { - ja: '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + 'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' } }), untilDate: $.num.optional.note({ desc: { - ja: '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + 'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' } }), includeMyRenotes: $.bool.optional.note({ default: true, desc: { - ja: '自分の行ったRenoteを含めるかどうか' + 'ja-JP': '自分の行ったRenoteを含めるかどうか' } }), includeRenotedMyNotes: $.bool.optional.note({ default: true, desc: { - ja: 'Renoteされた自分の投稿を含めるかどうか' + 'ja-JP': 'Renoteされた自分の投稿を含めるかどうか' } }), includeLocalRenotes: $.bool.optional.note({ default: true, desc: { - ja: 'Renoteされたローカルの投稿を含めるかどうか' + 'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか' } }), mediaOnly: $.bool.optional.note({ desc: { - ja: 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' } }), } diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index db91230a81..a7fb14d8a9 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -6,8 +6,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: '自分に言及している投稿の一覧を取得します。', - en: 'Get mentions of myself.' + 'ja-JP': '自分に言及している投稿の一覧を取得します。', + 'en-US': 'Get mentions of myself.' }, requireCredential: true diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts index a0469d1870..9af223c010 100644 --- a/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/src/server/api/endpoints/notes/polls/recommendation.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: 'おすすめのアンケート一覧を取得します。', - en: 'Get recommended polls.' + 'ja-JP': 'おすすめのアンケート一覧を取得します。', + 'en-US': 'Get recommended polls.' }, requireCredential: true, diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index 568c187f8a..ab80e7f5d0 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -9,8 +9,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '指定した投稿のアンケートに投票します。', - en: 'Vote poll of a note.' + 'ja-JP': '指定した投稿のアンケートに投票します。', + 'en-US': 'Vote poll of a note.' }, requireCredential: true, diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts index 8921c55916..6e7d60e0f0 100644 --- a/src/server/api/endpoints/notes/reactions.ts +++ b/src/server/api/endpoints/notes/reactions.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: '指定した投稿のリアクション一覧を取得します。', - en: 'Show reactions of a note.' + 'ja-JP': '指定した投稿のリアクション一覧を取得します。', + 'en-US': 'Show reactions of a note.' }, requireCredential: true diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts index 65e24e7c06..0781db16c5 100644 --- a/src/server/api/endpoints/notes/reactions/create.ts +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -7,8 +7,8 @@ import getParams from '../../../get-params'; export const meta = { desc: { - ja: '指定した投稿にリアクションします。', - en: 'React to a note.' + 'ja-JP': '指定した投稿にリアクションします。', + 'en-US': 'React to a note.' }, requireCredential: true, @@ -18,13 +18,13 @@ export const meta = { params: { noteId: $.type(ID).note({ desc: { - ja: '対象の投稿' + 'ja-JP': '対象の投稿' } }), reaction: $.str.pipe(validateReaction.ok).note({ desc: { - ja: 'リアクションの種類' + 'ja-JP': 'リアクションの種類' } }) } diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts index 62af0407bc..598eb65364 100644 --- a/src/server/api/endpoints/notes/reactions/delete.ts +++ b/src/server/api/endpoints/notes/reactions/delete.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '指定した投稿へのリアクションを取り消します。', - en: 'Unreact to a note.' + 'ja-JP': '指定した投稿へのリアクションを取り消します。', + 'en-US': 'Unreact to a note.' }, requireCredential: true, diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 3e3fa8c4aa..099bf2010b 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -8,8 +8,8 @@ import getParams from '../../get-params'; export const meta = { desc: { - ja: 'タイムラインを取得します。', - en: 'Get timeline of myself.' + 'ja-JP': 'タイムラインを取得します。', + 'en-US': 'Get timeline of myself.' }, requireCredential: true, @@ -18,58 +18,58 @@ export const meta = { limit: $.num.optional.range(1, 100).note({ default: 10, desc: { - ja: '最大数' + 'ja-JP': '最大数' } }), sinceId: $.type(ID).optional.note({ desc: { - ja: '指定すると、この投稿を基点としてより新しい投稿を取得します' + 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' } }), untilId: $.type(ID).optional.note({ desc: { - ja: '指定すると、この投稿を基点としてより古い投稿を取得します' + 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' } }), sinceDate: $.num.optional.note({ desc: { - ja: '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + 'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' } }), untilDate: $.num.optional.note({ desc: { - ja: '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + 'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' } }), includeMyRenotes: $.bool.optional.note({ default: true, desc: { - ja: '自分の行ったRenoteを含めるかどうか' + 'ja-JP': '自分の行ったRenoteを含めるかどうか' } }), includeRenotedMyNotes: $.bool.optional.note({ default: true, desc: { - ja: 'Renoteされた自分の投稿を含めるかどうか' + 'ja-JP': 'Renoteされた自分の投稿を含めるかどうか' } }), includeLocalRenotes: $.bool.optional.note({ default: true, desc: { - ja: 'Renoteされたローカルの投稿を含めるかどうか' + 'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか' } }), mediaOnly: $.bool.optional.note({ desc: { - ja: 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' } }), } diff --git a/src/server/api/endpoints/notes/trend.ts b/src/server/api/endpoints/notes/trend.ts index 1cbbfacadc..7a0a098f28 100644 --- a/src/server/api/endpoints/notes/trend.ts +++ b/src/server/api/endpoints/notes/trend.ts @@ -5,8 +5,8 @@ import { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: '人気の投稿の一覧を取得します。', - en: 'Get trend notes.' + 'ja-JP': '人気の投稿の一覧を取得します。', + 'en-US': 'Get trend notes.' }, requireCredential: true diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts index dcef548666..a7b43014ed 100644 --- a/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/src/server/api/endpoints/notes/user-list-timeline.ts @@ -8,8 +8,8 @@ import getParams from '../../get-params'; export const meta = { desc: { - ja: '指定したユーザーリストのタイムラインを取得します。', - en: 'Get timeline of a user list.' + 'ja-JP': '指定したユーザーリストのタイムラインを取得します。', + 'en-US': 'Get timeline of a user list.' }, requireCredential: true, @@ -17,65 +17,65 @@ export const meta = { params: { listId: $.type(ID).note({ desc: { - ja: 'リストのID' + 'ja-JP': 'リストのID' } }), limit: $.num.optional.range(1, 100).note({ default: 10, desc: { - ja: '最大数' + 'ja-JP': '最大数' } }), sinceId: $.type(ID).optional.note({ desc: { - ja: '指定すると、この投稿を基点としてより新しい投稿を取得します' + 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' } }), untilId: $.type(ID).optional.note({ desc: { - ja: '指定すると、この投稿を基点としてより古い投稿を取得します' + 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' } }), sinceDate: $.num.optional.note({ desc: { - ja: '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + 'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' } }), untilDate: $.num.optional.note({ desc: { - ja: '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + 'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' } }), includeMyRenotes: $.bool.optional.note({ default: true, desc: { - ja: '自分の行ったRenoteを含めるかどうか' + 'ja-JP': '自分の行ったRenoteを含めるかどうか' } }), includeRenotedMyNotes: $.bool.optional.note({ default: true, desc: { - ja: 'Renoteされた自分の投稿を含めるかどうか' + 'ja-JP': 'Renoteされた自分の投稿を含めるかどうか' } }), includeLocalRenotes: $.bool.optional.note({ default: true, desc: { - ja: 'Renoteされたローカルの投稿を含めるかどうか' + 'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか' } }), mediaOnly: $.bool.optional.note({ desc: { - ja: 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' } }), } diff --git a/src/server/api/endpoints/notifications/mark_all_as_read.ts b/src/server/api/endpoints/notifications/mark_all_as_read.ts index a9875ebb01..e2bde777b3 100644 --- a/src/server/api/endpoints/notifications/mark_all_as_read.ts +++ b/src/server/api/endpoints/notifications/mark_all_as_read.ts @@ -4,8 +4,8 @@ import User, { ILocalUser } from '../../../../models/user'; export const meta = { desc: { - ja: '全ての通知を既読にします。', - en: 'Mark all notifications as read.' + 'ja-JP': '全ての通知を既読にします。', + 'en-US': 'Mark all notifications as read.' }, requireCredential: true, diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts index d7dc2a5f70..ac4f957a0d 100644 --- a/src/server/api/endpoints/users/lists/create.ts +++ b/src/server/api/endpoints/users/lists/create.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: 'ユーザーリストを作成します。', - en: 'Create a user list' + 'ja-JP': 'ユーザーリストを作成します。', + 'en-US': 'Create a user list' }, requireCredential: true, diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts index 31fef26bdc..966e1d3ad9 100644 --- a/src/server/api/endpoints/users/lists/list.ts +++ b/src/server/api/endpoints/users/lists/list.ts @@ -3,7 +3,7 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '自分の作成したユーザーリスト一覧を取得します。' + 'ja-JP': '自分の作成したユーザーリスト一覧を取得します。' }, requireCredential: true, diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts index bd4e201bde..2d68ec7458 100644 --- a/src/server/api/endpoints/users/lists/push.ts +++ b/src/server/api/endpoints/users/lists/push.ts @@ -8,8 +8,8 @@ import { deliver } from '../../../../../queue'; export const meta = { desc: { - ja: '指定したユーザーリストに指定したユーザーを追加します。', - en: 'Add a user to a user list.' + 'ja-JP': '指定したユーザーリストに指定したユーザーを追加します。', + 'en-US': 'Add a user to a user list.' }, requireCredential: true, diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts index 2fd142a609..a2dd00c6e1 100644 --- a/src/server/api/endpoints/users/lists/show.ts +++ b/src/server/api/endpoints/users/lists/show.ts @@ -4,8 +4,8 @@ import { ILocalUser } from '../../../../../models/user'; export const meta = { desc: { - ja: '指定したユーザーリストの情報を取得します。', - en: 'Show a user list.' + 'ja-JP': '指定したユーザーリストの情報を取得します。', + 'en-US': 'Show a user list.' }, requireCredential: true, diff --git a/src/server/api/endpoints/users/recommendation.ts b/src/server/api/endpoints/users/recommendation.ts index 13377e6fff..e0a5cb9e36 100644 --- a/src/server/api/endpoints/users/recommendation.ts +++ b/src/server/api/endpoints/users/recommendation.ts @@ -6,7 +6,7 @@ import Mute from '../../../../models/mute'; export const meta = { desc: { - ja: 'おすすめのユーザー一覧を取得します。' + 'ja-JP': 'おすすめのユーザー一覧を取得します。' }, requireCredential: true, diff --git a/src/server/api/endpoints/users/search.ts b/src/server/api/endpoints/users/search.ts index eda3f95728..307a8f6894 100644 --- a/src/server/api/endpoints/users/search.ts +++ b/src/server/api/endpoints/users/search.ts @@ -5,7 +5,7 @@ import getParams from '../../get-params'; export const meta = { desc: { - ja: 'ユーザーを検索します。' + 'ja-JP': 'ユーザーを検索します。' }, requireCredential: false, @@ -13,28 +13,28 @@ export const meta = { params: { query: $.str.note({ desc: { - ja: 'クエリ' + 'ja-JP': 'クエリ' } }), offset: $.num.optional.min(0).note({ default: 0, desc: { - ja: 'オフセット' + 'ja-JP': 'オフセット' } }), limit: $.num.optional.range(1, 100).note({ default: 10, desc: { - ja: '取得する数' + 'ja-JP': '取得する数' } }), localOnly: $.bool.optional.note({ default: false, desc: { - ja: 'ローカルユーザーのみ検索対象にするか否か' + 'ja-JP': 'ローカルユーザーのみ検索対象にするか否か' } }), }, diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 3ec7a28df9..a8f6455d9a 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -46,6 +46,11 @@ router.post('/signin', require('./private/signin').default); router.use(require('./service/github').routes()); router.use(require('./service/twitter').routes()); +// Return 404 for unknown API +router.all('*', async ctx => { + ctx.status = 404; +}); + // Register router app.use(router.routes()); diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index 8c668e832a..aad2846bb4 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -14,7 +14,7 @@ function getUserToken(ctx: Koa.Context) { function compareOrigin(ctx: Koa.Context) { function normalizeUrl(url: string) { - return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; + return url.endsWith('/') ? url.substr(0, url.length - 1) : url; } const referer = ctx.headers['referer']; diff --git a/src/server/web/index.ts b/src/server/web/index.ts index 7291f8a0a5..452e36fe95 100644 --- a/src/server/web/index.ts +++ b/src/server/web/index.ts @@ -122,8 +122,7 @@ router.get('/notes/:note', async ctx => { router.get('*', async ctx => { await send(ctx, `app/base.html`, { root: client, - maxage: ms('3 days'), - immutable: true + maxage: ms('5m') }); }); diff --git a/src/server/web/url-preview.ts b/src/server/web/url-preview.ts index e96eb309fe..41ca6bad8b 100644 --- a/src/server/web/url-preview.ts +++ b/src/server/web/url-preview.ts @@ -1,11 +1,20 @@ import * as Koa from 'koa'; +import * as request from 'request-promise-native'; import summaly from 'summaly'; +import config from '../../config'; module.exports = async (ctx: Koa.Context) => { try { - const summary = await summaly(ctx.query.url, { + const summary = config.summalyProxy ? await request.get({ + url: config.summalyProxy, + qs: { + url: ctx.query.url + }, + json: true + }) : await summaly(ctx.query.url, { followRedirects: false }); + summary.icon = wrap(summary.icon); summary.thumbnail = wrap(summary.thumbnail); diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index b090d56cee..1da0f49a24 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -40,7 +40,7 @@ async function save(path: string, name: string, type: string, hash: string, size const thumbnailKey = `${config.drive.prefix}/${uuid.v4()}/${name}.thumbnail.jpg`; const baseUrl = config.drive.baseUrl - || `${ config.drive.config.secure ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? ':' + config.drive.config.port : '' }/${ config.drive.bucket }`; + || `${ config.drive.config.useSSL ? 'https' : 'http' }://${ config.drive.config.endPoint }${ config.drive.config.port ? ':' + config.drive.config.port : '' }/${ config.drive.bucket }`; await minio.putObject(config.drive.bucket, key, fs.createReadStream(path), size, { 'Content-Type': type, @@ -116,7 +116,8 @@ async function deleteOldFile(user: IRemoteUser) { const oldFile = await DriveFile.findOne({ _id: { $nin: [user.avatarId, user.bannerId] - } + }, + 'metadata.userId': user._id }, { sort: { _id: 1 diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts index 8a9f739bd4..7c285e9eac 100644 --- a/src/services/following/delete.ts +++ b/src/services/following/delete.ts @@ -56,7 +56,7 @@ export default async function(follower: IUser, followee: IUser) { } if (isLocalUser(follower) && isRemoteUser(followee)) { - const content = pack(renderUndo(renderFollow(follower, followee))); + const content = pack(renderUndo(renderFollow(follower, followee), follower)); deliver(follower, content, followee.inbox); } } diff --git a/src/services/following/requests/cancel.ts b/src/services/following/requests/cancel.ts index b0b574da58..9655a95f04 100644 --- a/src/services/following/requests/cancel.ts +++ b/src/services/following/requests/cancel.ts @@ -8,10 +8,19 @@ import { publishUserStream } from '../../../stream'; export default async function(followee: IUser, follower: IUser) { if (isRemoteUser(followee)) { - const content = pack(renderUndo(renderFollow(follower, followee))); + const content = pack(renderUndo(renderFollow(follower, followee), follower)); deliver(follower as ILocalUser, content, followee.inbox); } + const request = await FollowRequest.findOne({ + followeeId: followee._id, + followerId: follower._id + }); + + if (request == null) { + throw 'request not found'; + } + await FollowRequest.remove({ followeeId: followee._id, followerId: follower._id diff --git a/src/services/note/create.ts b/src/services/note/create.ts index d8f0f57b63..63e3557828 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -239,8 +239,8 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< async function renderActivity(data: Option, note: INote) { const content = data.renote && data.text == null - ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote), note) - : renderCreate(await renderNote(note)); + ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note) + : renderCreate(await renderNote(note, false), note); return packAp(content); } diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index d444b13a8b..d0e2b12b41 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -32,7 +32,7 @@ export default async function(user: IUser, note: INote) { //#region ローカルの投稿なら削除アクティビティを配送 if (isLocalUser(user)) { - const content = pack(renderDelete(await renderNote(note))); + const content = pack(renderDelete(await renderNote(note), user)); const followings = await Following.find({ followeeId: user._id, diff --git a/src/services/update-chart.ts b/src/services/update-chart.ts index 6b69adbdc3..1f8da6be9f 100644 --- a/src/services/update-chart.ts +++ b/src/services/update-chart.ts @@ -5,50 +5,63 @@ import { IDriveFile } from '../models/drive-file'; type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; -async function getTodayStats(): Promise<IStats> { +async function getCurrentStats(span: 'day' | 'hour'): Promise<IStats> { const now = new Date(); const y = now.getFullYear(); const m = now.getMonth(); const d = now.getDate(); - const today = new Date(y, m, d); + const h = now.getHours(); - // 今日の統計 - const todayStats = await Stats.findOne({ - date: today + const current = + span == 'day' ? new Date(y, m, d) : + span == 'hour' ? new Date(y, m, d, h) : + null; + + // 現在(今日または今のHour)の統計 + const currentStats = await Stats.findOne({ + span: span, + date: current }); - // 日付が変わってから、初めてのチャート更新なら - if (todayStats == null) { + if (currentStats) { + return currentStats; + } else { + // 集計期間が変わってから、初めてのチャート更新なら // 最も最近の統計を持ってくる + // * 例えば集計期間が「日」である場合で考えると、 // * 昨日何もチャートを更新するような出来事がなかった場合は、 - // 統計がそもそも作られずドキュメントが存在しないということがあり得るため、 - // 「昨日の」と決め打ちせずに「もっとも最近の」とします - const mostRecentStats = await Stats.findOne({}, { + // * 統計がそもそも作られずドキュメントが存在しないということがあり得るため、 + // * 「昨日の」と決め打ちせずに「もっとも最近の」とします + const mostRecentStats = await Stats.findOne({ + span: span + }, { sort: { date: -1 } }); - // 統計が存在しなかったら - // * Misskeyインスタンスを建てて初めてのチャート更新時など - if (mostRecentStats == null) { - // 空の統計を作成 + if (mostRecentStats) { + // 現在の統計を初期挿入 const data: Omit<IStats, '_id'> = { - date: today, + span: span, + date: current, users: { local: { - total: 0, - diff: 0 + total: mostRecentStats.users.local.total, + inc: 0, + dec: 0 }, remote: { - total: 0, - diff: 0 + total: mostRecentStats.users.remote.total, + inc: 0, + dec: 0 } }, notes: { local: { - total: 0, - diff: 0, + total: mostRecentStats.notes.local.total, + inc: 0, + dec: 0, diffs: { normal: 0, reply: 0, @@ -56,8 +69,9 @@ async function getTodayStats(): Promise<IStats> { } }, remote: { - total: 0, - diff: 0, + total: mostRecentStats.notes.remote.total, + inc: 0, + dec: 0, diffs: { normal: 0, reply: 0, @@ -67,16 +81,20 @@ async function getTodayStats(): Promise<IStats> { }, drive: { local: { - totalCount: 0, - totalSize: 0, - diffCount: 0, - diffSize: 0 + totalCount: mostRecentStats.drive.local.totalCount, + totalSize: mostRecentStats.drive.local.totalSize, + incCount: 0, + incSize: 0, + decCount: 0, + decSize: 0 }, remote: { - totalCount: 0, - totalSize: 0, - diffCount: 0, - diffSize: 0 + totalCount: mostRecentStats.drive.remote.totalCount, + totalSize: mostRecentStats.drive.remote.totalSize, + incCount: 0, + incSize: 0, + decCount: 0, + decSize: 0 } } }; @@ -85,23 +103,30 @@ async function getTodayStats(): Promise<IStats> { return stats; } else { - // 今日の統計を初期挿入 - const data: Omit<IStats, '_id'> = { - date: today, + // 統計が存在しなかったら + // * Misskeyインスタンスを建てて初めてのチャート更新時など + + // 空の統計を作成 + const emptyStat: Omit<IStats, '_id'> = { + span: span, + date: current, users: { local: { - total: mostRecentStats.users.local.total, - diff: 0 + total: 0, + inc: 0, + dec: 0 }, remote: { - total: mostRecentStats.users.remote.total, - diff: 0 + total: 0, + inc: 0, + dec: 0 } }, notes: { local: { - total: mostRecentStats.notes.local.total, - diff: 0, + total: 0, + inc: 0, + dec: 0, diffs: { normal: 0, reply: 0, @@ -109,8 +134,9 @@ async function getTodayStats(): Promise<IStats> { } }, remote: { - total: mostRecentStats.notes.remote.total, - diff: 0, + total: 0, + inc: 0, + dec: 0, diffs: { normal: 0, reply: 0, @@ -120,80 +146,100 @@ async function getTodayStats(): Promise<IStats> { }, drive: { local: { - totalCount: mostRecentStats.drive.local.totalCount, - totalSize: mostRecentStats.drive.local.totalSize, - diffCount: 0, - diffSize: 0 + totalCount: 0, + totalSize: 0, + incCount: 0, + incSize: 0, + decCount: 0, + decSize: 0 }, remote: { - totalCount: mostRecentStats.drive.remote.totalCount, - totalSize: mostRecentStats.drive.remote.totalSize, - diffCount: 0, - diffSize: 0 + totalCount: 0, + totalSize: 0, + incCount: 0, + incSize: 0, + decCount: 0, + decSize: 0 } } }; - const stats = await Stats.insert(data); + const stats = await Stats.insert(emptyStat); return stats; } - } else { - return todayStats; } } -async function update(inc: any) { - const stats = await getTodayStats(); +function update(inc: any) { + getCurrentStats('day').then(stats => { + Stats.findOneAndUpdate({ + _id: stats._id + }, { + $inc: inc + }); + }); - await Stats.findOneAndUpdate({ - _id: stats._id - }, { - $inc: inc + getCurrentStats('hour').then(stats => { + Stats.findOneAndUpdate({ + _id: stats._id + }, { + $inc: inc + }); }); } export async function updateUserStats(user: IUser, isAdditional: boolean) { - const amount = isAdditional ? 1 : -1; const origin = isLocalUser(user) ? 'local' : 'remote'; const inc = {} as any; - inc[`users.${origin}.total`] = amount; - inc[`users.${origin}.diff`] = amount; + inc[`users.${origin}.total`] = isAdditional ? 1 : -1; + if (isAdditional) { + inc[`users.${origin}.inc`] = 1; + } else { + inc[`users.${origin}.dec`] = 1; + } await update(inc); } export async function updateNoteStats(note: INote, isAdditional: boolean) { - const amount = isAdditional ? 1 : -1; const origin = isLocalUser(note._user) ? 'local' : 'remote'; const inc = {} as any; - inc[`notes.${origin}.total`] = amount; - inc[`notes.${origin}.diff`] = amount; + inc[`notes.${origin}.total`] = isAdditional ? 1 : -1; + + if (isAdditional) { + inc[`notes.${origin}.inc`] = 1; + } else { + inc[`notes.${origin}.dec`] = 1; + } if (note.replyId != null) { - inc[`notes.${origin}.diffs.reply`] = amount; + inc[`notes.${origin}.diffs.reply`] = isAdditional ? 1 : -1; } else if (note.renoteId != null) { - inc[`notes.${origin}.diffs.renote`] = amount; + inc[`notes.${origin}.diffs.renote`] = isAdditional ? 1 : -1; } else { - inc[`notes.${origin}.diffs.normal`] = amount; + inc[`notes.${origin}.diffs.normal`] = isAdditional ? 1 : -1; } await update(inc); } export async function updateDriveStats(file: IDriveFile, isAdditional: boolean) { - const amount = isAdditional ? 1 : -1; - const size = isAdditional ? file.length : -file.length; const origin = isLocalUser(file.metadata._user) ? 'local' : 'remote'; const inc = {} as any; - inc[`drive.${origin}.totalCount`] = amount; - inc[`drive.${origin}.diffCount`] = amount; - inc[`drive.${origin}.totalSize`] = size; - inc[`drive.${origin}.diffSize`] = size; + inc[`drive.${origin}.totalCount`] = isAdditional ? 1 : -1; + inc[`drive.${origin}.totalSize`] = isAdditional ? file.length : -file.length; + if (isAdditional) { + inc[`drive.${origin}.incCount`] = 1; + inc[`drive.${origin}.incSize`] = file.length; + } else { + inc[`drive.${origin}.decCount`] = 1; + inc[`drive.${origin}.decSize`] = file.length; + } await update(inc); } |