commit
2f45ac3712
31 changed files with 774 additions and 896 deletions
|
@ -5,6 +5,15 @@ ChangeLog
|
|||
|
||||
This document describes breaking changes only.
|
||||
|
||||
7.0.0
|
||||
-----
|
||||
|
||||
### Migration
|
||||
|
||||
起動する前に、`node cli/migration/7.0.0`してください。
|
||||
|
||||
Please run `node cli/migration/7.0.0` before launch.
|
||||
|
||||
6.0.0
|
||||
-----
|
||||
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
const chalk = require('chalk');
|
||||
const log = require('single-line-log').stdout;
|
||||
const sequential = require('promise-sequential');
|
||||
const { default: DriveFile, DriveFileChunk } = require('../built/models/drive-file');
|
||||
const { default: DriveFileThumbnail, DriveFileThumbnailChunk } = require('../built/models/drive-file-thumbnail');
|
||||
const { default: User } = require('../built/models/user');
|
||||
|
||||
const q = {
|
||||
'metadata._user.host': {
|
||||
$ne: null
|
||||
},
|
||||
'metadata.withoutChunks': false
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const promiseGens = [];
|
||||
|
||||
const count = await DriveFile.count(q);
|
||||
|
||||
let prev;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
promiseGens.push(() => {
|
||||
const promise = new Promise(async (res, rej) => {
|
||||
const file = await DriveFile.findOne(prev ? Object.assign({
|
||||
_id: { $lt: prev._id }
|
||||
}, q) : q, {
|
||||
sort: {
|
||||
_id: -1
|
||||
}
|
||||
});
|
||||
|
||||
prev = file;
|
||||
|
||||
function skip() {
|
||||
res([i, file, false]);
|
||||
}
|
||||
|
||||
if (file == null) return skip();
|
||||
|
||||
log(chalk`{gray ${i}} scanning {bold ${file._id}} ${file.filename} ...`);
|
||||
|
||||
const attachingUsersCount = await User.count({
|
||||
$or: [{
|
||||
avatarId: file._id
|
||||
}, {
|
||||
bannerId: file._id
|
||||
}]
|
||||
}, { limit: 1 });
|
||||
if (attachingUsersCount !== 0) return skip();
|
||||
|
||||
Promise.all([
|
||||
// チャンクをすべて削除
|
||||
DriveFileChunk.remove({
|
||||
files_id: file._id
|
||||
}),
|
||||
|
||||
DriveFile.update({ _id: file._id }, {
|
||||
$set: {
|
||||
'metadata.withoutChunks': true
|
||||
}
|
||||
})
|
||||
]).then(async () => {
|
||||
res([i, file, true]);
|
||||
|
||||
//#region サムネイルもあれば削除
|
||||
const thumbnail = await DriveFileThumbnail.findOne({
|
||||
'metadata.originalId': file._id
|
||||
});
|
||||
|
||||
if (thumbnail) {
|
||||
DriveFileThumbnailChunk.remove({
|
||||
files_id: thumbnail._id
|
||||
});
|
||||
|
||||
DriveFileThumbnail.remove({ _id: thumbnail._id });
|
||||
}
|
||||
//#endregion
|
||||
});
|
||||
});
|
||||
|
||||
promise.then(([i, file, deleted]) => {
|
||||
if (deleted) {
|
||||
log(chalk`{gray ${i}} {red deleted: {bold ${file._id}} ${file.filename}}`);
|
||||
} else {
|
||||
log(chalk`{gray ${i}} {green skipped: {bold ${file._id}} ${file.filename}}`);
|
||||
}
|
||||
log.clear();
|
||||
console.log();
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
}
|
||||
|
||||
return await sequential(promiseGens);
|
||||
}
|
||||
|
||||
main().then(() => {
|
||||
console.log('ALL DONE');
|
||||
}).catch(console.error);
|
|
@ -1,80 +0,0 @@
|
|||
const chalk = require('chalk');
|
||||
const log = require('single-line-log').stdout;
|
||||
const sequential = require('promise-sequential');
|
||||
const { default: DriveFile, deleteDriveFile } = require('../built/models/drive-file');
|
||||
const { default: Note } = require('../built/models/note');
|
||||
const { default: MessagingMessage } = require('../built/models/messaging-message');
|
||||
const { default: User } = require('../built/models/user');
|
||||
|
||||
async function main() {
|
||||
const promiseGens = [];
|
||||
|
||||
const count = await DriveFile.count({});
|
||||
|
||||
let prev;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
promiseGens.push(() => {
|
||||
const promise = new Promise(async (res, rej) => {
|
||||
const file = await DriveFile.findOne(prev ? {
|
||||
_id: { $lt: prev._id }
|
||||
} : {}, {
|
||||
sort: {
|
||||
_id: -1
|
||||
}
|
||||
});
|
||||
|
||||
prev = file;
|
||||
|
||||
function skip() {
|
||||
res([i, file, false]);
|
||||
}
|
||||
|
||||
if (file == null) return skip();
|
||||
|
||||
log(chalk`{gray ${i}} scanning {bold ${file._id}} ${file.filename} ...`);
|
||||
|
||||
const attachingUsersCount = await User.count({
|
||||
$or: [{
|
||||
avatarId: file._id
|
||||
}, {
|
||||
bannerId: file._id
|
||||
}]
|
||||
}, { limit: 1 });
|
||||
if (attachingUsersCount !== 0) return skip();
|
||||
|
||||
const attachingNotesCount = await Note.count({
|
||||
mediaIds: file._id
|
||||
}, { limit: 1 });
|
||||
if (attachingNotesCount !== 0) return skip();
|
||||
|
||||
const attachingMessagesCount = await MessagingMessage.count({
|
||||
fileId: file._id
|
||||
}, { limit: 1 });
|
||||
if (attachingMessagesCount !== 0) return skip();
|
||||
|
||||
deleteDriveFile(file).then(() => {
|
||||
res([i, file, true]);
|
||||
}).catch(rej);
|
||||
});
|
||||
|
||||
promise.then(([i, file, deleted]) => {
|
||||
if (deleted) {
|
||||
log(chalk`{gray ${i}} {red deleted: {bold ${file._id}} ${file.filename}}`);
|
||||
} else {
|
||||
log(chalk`{gray ${i}} {green skipped: {bold ${file._id}} ${file.filename}}`);
|
||||
}
|
||||
log.clear();
|
||||
console.log();
|
||||
});
|
||||
|
||||
return promise;
|
||||
});
|
||||
}
|
||||
|
||||
return await sequential(promiseGens);
|
||||
}
|
||||
|
||||
main().then(() => {
|
||||
console.log('done');
|
||||
}).catch(console.error);
|
134
cli/migration/7.0.0.js
Normal file
134
cli/migration/7.0.0.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
const { default: Stats } = require('../../built/models/stats');
|
||||
const { default: User } = require('../../built/models/user');
|
||||
const { default: Note } = require('../../built/models/note');
|
||||
const { default: DriveFile } = require('../../built/models/drive-file');
|
||||
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth();
|
||||
const d = now.getDate();
|
||||
const today = new Date(y, m, d);
|
||||
|
||||
async function main() {
|
||||
const localUsersCount = await User.count({
|
||||
host: null
|
||||
});
|
||||
|
||||
const remoteUsersCount = await User.count({
|
||||
host: { $ne: null }
|
||||
});
|
||||
|
||||
const localNotesCount = await Note.count({
|
||||
'_user.host': null
|
||||
});
|
||||
|
||||
const remoteNotesCount = await Note.count({
|
||||
'_user.host': { $ne: null }
|
||||
});
|
||||
|
||||
const localDriveFilesCount = await DriveFile.count({
|
||||
'metadata._user.host': null
|
||||
});
|
||||
|
||||
const remoteDriveFilesCount = await DriveFile.count({
|
||||
'metadata._user.host': { $ne: null }
|
||||
});
|
||||
|
||||
const localDriveFilesSize = await DriveFile
|
||||
.aggregate([{
|
||||
$match: {
|
||||
'metadata._user.host': null,
|
||||
'metadata.deletedAt': { $exists: false }
|
||||
}
|
||||
}, {
|
||||
$project: {
|
||||
length: true
|
||||
}
|
||||
}, {
|
||||
$group: {
|
||||
_id: null,
|
||||
usage: { $sum: '$length' }
|
||||
}
|
||||
}])
|
||||
.then(aggregates => {
|
||||
if (aggregates.length > 0) {
|
||||
return aggregates[0].usage;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const remoteDriveFilesSize = await DriveFile
|
||||
.aggregate([{
|
||||
$match: {
|
||||
'metadata._user.host': { $ne: null },
|
||||
'metadata.deletedAt': { $exists: false }
|
||||
}
|
||||
}, {
|
||||
$project: {
|
||||
length: true
|
||||
}
|
||||
}, {
|
||||
$group: {
|
||||
_id: null,
|
||||
usage: { $sum: '$length' }
|
||||
}
|
||||
}])
|
||||
.then(aggregates => {
|
||||
if (aggregates.length > 0) {
|
||||
return aggregates[0].usage;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
await Stats.insert({
|
||||
date: today,
|
||||
users: {
|
||||
local: {
|
||||
total: localUsersCount,
|
||||
diff: 0
|
||||
},
|
||||
remote: {
|
||||
total: remoteUsersCount,
|
||||
diff: 0
|
||||
}
|
||||
},
|
||||
notes: {
|
||||
local: {
|
||||
total: localNotesCount,
|
||||
diff: 0,
|
||||
diffs: {
|
||||
normal: 0,
|
||||
reply: 0,
|
||||
renote: 0
|
||||
}
|
||||
},
|
||||
remote: {
|
||||
total: remoteNotesCount,
|
||||
diff: 0,
|
||||
diffs: {
|
||||
normal: 0,
|
||||
reply: 0,
|
||||
renote: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
drive: {
|
||||
local: {
|
||||
totalCount: localDriveFilesCount,
|
||||
totalSize: localDriveFilesSize,
|
||||
diffCount: 0,
|
||||
diffSize: 0
|
||||
},
|
||||
remote: {
|
||||
totalCount: remoteDriveFilesCount,
|
||||
totalSize: remoteDriveFilesSize,
|
||||
diffCount: 0,
|
||||
diffSize: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('done');
|
||||
}
|
||||
|
||||
main();
|
|
@ -33,14 +33,3 @@ node cli/suspend @syuilo@misskey.xyz
|
|||
``` shell
|
||||
node cli/reset-password (User-ID or Username)
|
||||
```
|
||||
|
||||
## Clean up cached remote files
|
||||
``` shell
|
||||
node cli/clean-cached-remote-files
|
||||
```
|
||||
|
||||
## Clean up unused drive files
|
||||
``` shell
|
||||
node cli/clean-unused-drive-files
|
||||
```
|
||||
> We recommend that you announce a user that unused drive files will be deleted before performing this operation, as it may delete the user's important files.
|
||||
|
|
|
@ -33,14 +33,3 @@ node cli/suspend @syuilo@misskey.xyz
|
|||
``` shell
|
||||
node cli/reset-password (ユーザーID または ユーザー名)
|
||||
```
|
||||
|
||||
## キャッシュされたリモートファイルをクリーンアップする
|
||||
``` shell
|
||||
node cli/clean-cached-remote-files
|
||||
```
|
||||
|
||||
## 使われていないドライブのファイルをクリーンアップする
|
||||
``` shell
|
||||
node cli/clean-unused-drive-files
|
||||
```
|
||||
> ユーザーの大事なファイルを削除する可能性があるので、この操作を実行する前にユーザーに告知することをお勧めします。
|
||||
|
|
|
@ -946,6 +946,11 @@ desktop/views/pages/admin/admin.users-chart.vue:
|
|||
local: "ローカル"
|
||||
remote: "リモート"
|
||||
|
||||
desktop/views/pages/admin/admin.drive-chart.vue:
|
||||
title: "ドライブ"
|
||||
local: "ローカル"
|
||||
remote: "リモート"
|
||||
|
||||
desktop/views/pages/deck/deck.tl-column.vue:
|
||||
is-media-only: "メディア投稿のみ"
|
||||
is-media-view: "メディアビュー"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "6.4.1",
|
||||
"version": "7.0.0",
|
||||
"clientVersion": "1.0.8520",
|
||||
"codename": "nighthike",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<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>
|
|
@ -0,0 +1,34 @@
|
|||
<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>
|
|
@ -29,7 +29,7 @@ import Vue from 'vue';
|
|||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
data: {
|
||||
chart: {
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
|
@ -39,7 +39,6 @@ export default Vue.extend({
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
chart: this.data,
|
||||
viewBoxX: 365,
|
||||
viewBoxY: 70,
|
||||
pointsNote: null,
|
||||
|
@ -49,23 +48,19 @@ export default Vue.extend({
|
|||
};
|
||||
},
|
||||
created() {
|
||||
this.chart.forEach(d => {
|
||||
d.notes = this.type == 'local' ? d.localNotes : d.remoteNotes;
|
||||
d.replies = this.type == 'local' ? d.localReplies : d.remoteReplies;
|
||||
d.renotes = this.type == 'local' ? d.localRenotes : d.remoteRenotes;
|
||||
});
|
||||
|
||||
this.chart.forEach(d => {
|
||||
d.total = d.notes + d.replies + d.renotes;
|
||||
});
|
||||
|
||||
const peak = Math.max.apply(null, this.chart.map(d => d.total));
|
||||
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();
|
||||
this.pointsNote = data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsReply = data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
|
||||
this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
|
||||
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(' ');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<header>%i18n:@title%</header>
|
||||
<div class="card">
|
||||
<header>%i18n:@local%</header>
|
||||
<x-chart v-if="data" :data="data" type="local"/>
|
||||
<x-chart v-if="chart" :chart="chart" type="local"/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<header>%i18n:@remote%</header>
|
||||
<x-chart v-if="data" :data="data" type="remote"/>
|
||||
<x-chart v-if="chart" :chart="chart" type="remote"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -20,15 +20,10 @@ export default Vue.extend({
|
|||
components: {
|
||||
XChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
(this as any).api('aggregation/notes').then(res => {
|
||||
this.data = res;
|
||||
});
|
||||
props: {
|
||||
chart: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -13,7 +13,7 @@ import Vue from 'vue';
|
|||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
data: {
|
||||
chart: {
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
|
@ -23,21 +23,19 @@ export default Vue.extend({
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
chart: this.data,
|
||||
viewBoxX: 365,
|
||||
viewBoxY: 70,
|
||||
points: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.chart.forEach(d => {
|
||||
d.count = this.type == 'local' ? d.local : d.remote;
|
||||
});
|
||||
|
||||
const peak = Math.max.apply(null, this.chart.map(d => d.count));
|
||||
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();
|
||||
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(' ');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<header>%i18n:@title%</header>
|
||||
<div class="card">
|
||||
<header>%i18n:@local%</header>
|
||||
<x-chart v-if="data" :data="data" type="local"/>
|
||||
<x-chart v-if="chart" :chart="chart" type="local"/>
|
||||
</div>
|
||||
<div class="card">
|
||||
<header>%i18n:@remote%</header>
|
||||
<x-chart v-if="data" :data="data" type="remote"/>
|
||||
<x-chart v-if="chart" :chart="chart" type="remote"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -20,15 +20,10 @@ export default Vue.extend({
|
|||
components: {
|
||||
XChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
(this as any).api('aggregation/users').then(res => {
|
||||
this.data = res;
|
||||
});
|
||||
props: {
|
||||
chart: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -11,8 +11,9 @@
|
|||
<main>
|
||||
<div v-show="page == 'dashboard'">
|
||||
<x-dashboard/>
|
||||
<x-users-chart/>
|
||||
<x-notes-chart/>
|
||||
<x-users-chart :chart="chart"/>
|
||||
<x-notes-chart :chart="chart"/>
|
||||
<x-drive-chart :chart="chart"/>
|
||||
</div>
|
||||
<div v-if="page == 'users'">
|
||||
<x-suspend-user/>
|
||||
|
@ -35,6 +36,7 @@ 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";
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
|
@ -44,13 +46,20 @@ export default Vue.extend({
|
|||
XVerifyUser,
|
||||
XUnverifyUser,
|
||||
XUsersChart,
|
||||
XNotesChart
|
||||
XNotesChart,
|
||||
XDriveChart
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
page: 'dashboard'
|
||||
page: 'dashboard',
|
||||
chart: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
(this as any).api('admin/chart').then(chart => {
|
||||
this.chart = chart;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
nav(page: string) {
|
||||
this.page = page;
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
color #456267
|
||||
background #fff
|
||||
|
||||
body
|
||||
margin 0
|
||||
padding 0
|
|
@ -1,209 +0,0 @@
|
|||
<mk-index>
|
||||
<h1>Misskey<i>Statistics</i></h1>
|
||||
<main v-if="!initializing">
|
||||
<mk-users stats={ stats }/>
|
||||
<mk-notes stats={ stats }/>
|
||||
</main>
|
||||
<footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 0 16px
|
||||
max-width 700px
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
padding 24px 0 0 0
|
||||
font-size 24px
|
||||
font-weight normal
|
||||
|
||||
> i
|
||||
font-style normal
|
||||
color #f43b16
|
||||
|
||||
> main
|
||||
> *
|
||||
margin 24px 0
|
||||
padding-top 24px
|
||||
border-top solid 1px #eee
|
||||
|
||||
> h2
|
||||
margin 0 0 12px 0
|
||||
font-size 18px
|
||||
font-weight normal
|
||||
|
||||
> footer
|
||||
margin 24px 0
|
||||
text-align center
|
||||
|
||||
> a
|
||||
color #546567
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
this.mixin('api');
|
||||
|
||||
this.initializing = true;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.$root.$data.os.api('stats').then(stats => {
|
||||
this.update({
|
||||
initializing: false,
|
||||
stats
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</mk-index>
|
||||
|
||||
<mk-notes>
|
||||
<h2>%i18n:stats.notes-count% <b>{ stats.notesCount }</b></h2>
|
||||
<mk-notes-chart v-if="!initializing" data={ data }/>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
this.mixin('api');
|
||||
|
||||
this.initializing = true;
|
||||
this.stats = this.opts.stats;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.$root.$data.os.api('aggregation/notes', {
|
||||
limit: 365
|
||||
}).then(data => {
|
||||
this.update({
|
||||
initializing: false,
|
||||
data
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</mk-notes>
|
||||
|
||||
<mk-users>
|
||||
<h2>%i18n:stats.users-count% <b>{ stats.usersCount }</b></h2>
|
||||
<mk-users-chart v-if="!initializing" data={ data }/>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
this.mixin('api');
|
||||
|
||||
this.initializing = true;
|
||||
this.stats = this.opts.stats;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.$root.$data.os.api('aggregation/users', {
|
||||
limit: 365
|
||||
}).then(data => {
|
||||
this.update({
|
||||
initializing: false,
|
||||
data
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</mk-users>
|
||||
|
||||
<mk-notes-chart>
|
||||
<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
|
||||
<title>Black ... Total<br/>Blue ... Notes<br/>Red ... Replies<br/>Green ... Renotes</title>
|
||||
<polyline
|
||||
riot-points={ pointsNote }
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#41ddde"/>
|
||||
<polyline
|
||||
riot-points={ pointsReply }
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#f7796c"/>
|
||||
<polyline
|
||||
riot-points={ pointsRenote }
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#a1de41"/>
|
||||
<polyline
|
||||
riot-points={ pointsTotal }
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#555"
|
||||
stroke-dasharray="2 2"/>
|
||||
</svg>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> svg
|
||||
display block
|
||||
padding 1px
|
||||
width 100%
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
this.viewBoxX = 365;
|
||||
this.viewBoxY = 80;
|
||||
|
||||
this.data = this.opts.data.reverse();
|
||||
this.data.forEach(d => d.total = d.notes + d.replies + d.renotes);
|
||||
const peak = Math.max.apply(null, this.data.map(d => d.total));
|
||||
|
||||
this.on('mount', () => {
|
||||
this.render();
|
||||
});
|
||||
|
||||
this.render = () => {
|
||||
this.update({
|
||||
pointsNote: this.data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' '),
|
||||
pointsReply: this.data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '),
|
||||
pointsRenote: this.data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' '),
|
||||
pointsTotal: this.data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ')
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-notes-chart>
|
||||
|
||||
<mk-users-chart>
|
||||
<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
|
||||
<polyline
|
||||
riot-points={ createdPoints }
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#1cde84"/>
|
||||
<polyline
|
||||
riot-points={ totalPoints }
|
||||
fill="none"
|
||||
stroke-width="1"
|
||||
stroke="#555"/>
|
||||
</svg>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
|
||||
> svg
|
||||
display block
|
||||
padding 1px
|
||||
width 100%
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
this.viewBoxX = 365;
|
||||
this.viewBoxY = 80;
|
||||
|
||||
this.data = this.opts.data.reverse();
|
||||
const totalPeak = Math.max.apply(null, this.data.map(d => d.total));
|
||||
const createdPeak = Math.max.apply(null, this.data.map(d => d.created));
|
||||
|
||||
this.on('mount', () => {
|
||||
this.render();
|
||||
});
|
||||
|
||||
this.render = () => {
|
||||
this.update({
|
||||
totalPoints: this.data.map((d, i) => `${i},${(1 - (d.total / totalPeak)) * this.viewBoxY}`).join(' '),
|
||||
createdPoints: this.data.map((d, i) => `${i},${(1 - (d.created / createdPeak)) * this.viewBoxY}`).join(' ')
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-users-chart>
|
|
@ -1 +0,0 @@
|
|||
require('./index.tag');
|
|
@ -1,10 +0,0 @@
|
|||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
color #456267
|
||||
background #fff
|
||||
|
||||
body
|
||||
margin 0
|
||||
padding 0
|
|
@ -1,201 +0,0 @@
|
|||
<mk-index>
|
||||
<h1>Misskey<i>Status</i></h1>
|
||||
<p>%fa:info-circle%%i18n:status.all-systems-maybe-operational%</p>
|
||||
<main>
|
||||
<mk-cpu-usage connection={ connection }/>
|
||||
<mk-mem-usage connection={ connection }/>
|
||||
</main>
|
||||
<footer><a href={ _URL_ }>{ _HOST_ }</a></footer>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
margin 0 auto
|
||||
padding 0 16px
|
||||
max-width 700px
|
||||
|
||||
> h1
|
||||
margin 0
|
||||
padding 24px 0 16px 0
|
||||
font-size 24px
|
||||
font-weight normal
|
||||
|
||||
> [data-fa]
|
||||
font-style normal
|
||||
color #f43b16
|
||||
|
||||
> p
|
||||
display block
|
||||
margin 0
|
||||
padding 12px 16px
|
||||
background #eaf4ef
|
||||
//border solid 1px #99ccb2
|
||||
border-radius 4px
|
||||
|
||||
> [data-fa]
|
||||
margin-right 5px
|
||||
|
||||
> main
|
||||
> *
|
||||
margin 24px 0
|
||||
|
||||
> h2
|
||||
margin 0 0 12px 0
|
||||
font-size 18px
|
||||
font-weight normal
|
||||
|
||||
> footer
|
||||
margin 24px 0
|
||||
text-align center
|
||||
|
||||
> a
|
||||
color #546567
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
import Connection from '../../common/scripts/streaming/server-stream';
|
||||
|
||||
this.mixin('api');
|
||||
|
||||
this.initializing = true;
|
||||
this.connection = new Connection();
|
||||
|
||||
this.on('mount', () => {
|
||||
this.$root.$data.os.api('meta').then(meta => {
|
||||
this.update({
|
||||
initializing: false,
|
||||
meta
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.connection.close();
|
||||
});
|
||||
|
||||
</script>
|
||||
</mk-index>
|
||||
|
||||
<mk-cpu-usage>
|
||||
<h2>CPU <b>{ percentage }%</b></h2>
|
||||
<mk-line-chart ref="chart"/>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
this.connection = this.opts.connection;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.connection.on('stats', this.onStats);
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.connection.off('stats', this.onStats);
|
||||
});
|
||||
|
||||
this.onStats = stats => {
|
||||
this.$refs.chart.addData(1 - stats.cpu_usage);
|
||||
|
||||
const percentage = (stats.cpu_usage * 100).toFixed(0);
|
||||
|
||||
this.update({
|
||||
percentage
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-cpu-usage>
|
||||
|
||||
<mk-mem-usage>
|
||||
<h2>MEM <b>{ percentage }%</b></h2>
|
||||
<mk-line-chart ref="chart"/>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
this.connection = this.opts.connection;
|
||||
|
||||
this.on('mount', () => {
|
||||
this.connection.on('stats', this.onStats);
|
||||
});
|
||||
|
||||
this.on('unmount', () => {
|
||||
this.connection.off('stats', this.onStats);
|
||||
});
|
||||
|
||||
this.onStats = stats => {
|
||||
stats.mem.used = stats.mem.total - stats.mem.free;
|
||||
this.$refs.chart.addData(1 - (stats.mem.used / stats.mem.total));
|
||||
|
||||
const percentage = (stats.mem.used / stats.mem.total * 100).toFixed(0);
|
||||
|
||||
this.update({
|
||||
percentage
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-mem-usage>
|
||||
|
||||
<mk-line-chart>
|
||||
<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id={ gradientId } x1="0" x2="0" y1="1" y2="0">
|
||||
<stop offset="0%" stop-color="rgba(244, 59, 22, 0)"></stop>
|
||||
<stop offset="100%" stop-color="#f43b16"></stop>
|
||||
</linearGradient>
|
||||
<mask id={ maskId } x="0" y="0" riot-width={ viewBoxX } riot-height={ viewBoxY }>
|
||||
<polygon
|
||||
riot-points={ polygonPoints }
|
||||
fill="#fff"
|
||||
fill-opacity="0.5"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<line x1="0" y1="0" riot-x2={ viewBoxX } y2="0" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
|
||||
<line x1="0" y1="25%" riot-x2={ viewBoxX } y2="25%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
|
||||
<line x1="0" y1="50%" riot-x2={ viewBoxX } y2="50%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
|
||||
<line x1="0" y1="75%" riot-x2={ viewBoxX } y2="75%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
|
||||
<line x1="0" y1="100%" riot-x2={ viewBoxX } y2="100%" stroke="rgba(255, 255, 255, 0.1)" stroke-width="0.25" stroke-dasharray="1"/>
|
||||
<rect
|
||||
x="-1" y="-1"
|
||||
riot-width={ viewBoxX + 2 } riot-height={ viewBoxY + 2 }
|
||||
style="stroke: none; fill: url(#{ gradientId }); mask: url(#{ maskId })"/>
|
||||
<polyline
|
||||
riot-points={ polylinePoints }
|
||||
fill="none"
|
||||
stroke="#f43b16"
|
||||
stroke-width="0.5"/>
|
||||
</svg>
|
||||
<style lang="stylus" scoped>
|
||||
:scope
|
||||
display block
|
||||
padding 16px
|
||||
border-radius 8px
|
||||
background #1c2531
|
||||
|
||||
> svg
|
||||
display block
|
||||
padding 1px
|
||||
width 100%
|
||||
</style>
|
||||
<script lang="typescript">
|
||||
import uuid from 'uuid';
|
||||
|
||||
this.viewBoxX = 100;
|
||||
this.viewBoxY = 30;
|
||||
this.data = [];
|
||||
this.gradientId = uuid();
|
||||
this.maskId = uuid();
|
||||
|
||||
this.addData = data => {
|
||||
this.data.push(data);
|
||||
if (this.data.length > 100) this.data.shift();
|
||||
|
||||
const polylinePoints = this.data.map((d, i) => `${this.viewBoxX - ((this.data.length - 1) - i)},${d * this.viewBoxY}`).join(' ');
|
||||
const polygonPoints = `${this.viewBoxX - (this.data.length - 1)},${ this.viewBoxY } ${ polylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`;
|
||||
|
||||
this.update({
|
||||
polylinePoints,
|
||||
polygonPoints
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</mk-line-chart>
|
|
@ -1 +0,0 @@
|
|||
require('./index.tag');
|
|
@ -52,6 +52,11 @@ export type IDriveFile = {
|
|||
filename: string;
|
||||
contentType: string;
|
||||
metadata: IMetadata;
|
||||
|
||||
/**
|
||||
* ファイルサイズ
|
||||
*/
|
||||
length: number;
|
||||
};
|
||||
|
||||
export function validateFileName(name: string): boolean {
|
||||
|
|
153
src/models/stats.ts
Normal file
153
src/models/stats.ts
Normal file
|
@ -0,0 +1,153 @@
|
|||
import * as mongo from 'mongodb';
|
||||
import db from '../db/mongodb';
|
||||
|
||||
const Stats = db.get<IStats>('stats');
|
||||
Stats.createIndex({ date: -1 }, { unique: true });
|
||||
export default Stats;
|
||||
|
||||
export interface IStats {
|
||||
_id: mongo.ObjectID;
|
||||
|
||||
date: Date;
|
||||
|
||||
/**
|
||||
* ユーザーに関する統計
|
||||
*/
|
||||
users: {
|
||||
local: {
|
||||
/**
|
||||
* この日時点での、ローカルのユーザーの総計
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* ローカルのユーザー数の前日比
|
||||
*/
|
||||
diff: number;
|
||||
};
|
||||
|
||||
remote: {
|
||||
/**
|
||||
* この日時点での、リモートのユーザーの総計
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* リモートのユーザー数の前日比
|
||||
*/
|
||||
diff: number;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 投稿に関する統計
|
||||
*/
|
||||
notes: {
|
||||
local: {
|
||||
/**
|
||||
* この日時点での、ローカルの投稿の総計
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* ローカルの投稿数の前日比
|
||||
*/
|
||||
diff: number;
|
||||
|
||||
diffs: {
|
||||
/**
|
||||
* ローカルの通常の投稿数の前日比
|
||||
*/
|
||||
normal: number;
|
||||
|
||||
/**
|
||||
* ローカルのリプライの投稿数の前日比
|
||||
*/
|
||||
reply: number;
|
||||
|
||||
/**
|
||||
* ローカルのRenoteの投稿数の前日比
|
||||
*/
|
||||
renote: number;
|
||||
};
|
||||
};
|
||||
|
||||
remote: {
|
||||
/**
|
||||
* この日時点での、リモートの投稿の総計
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* リモートの投稿数の前日比
|
||||
*/
|
||||
diff: number;
|
||||
|
||||
diffs: {
|
||||
/**
|
||||
* リモートの通常の投稿数の前日比
|
||||
*/
|
||||
normal: number;
|
||||
|
||||
/**
|
||||
* リモートのリプライの投稿数の前日比
|
||||
*/
|
||||
reply: number;
|
||||
|
||||
/**
|
||||
* リモートのRenoteの投稿数の前日比
|
||||
*/
|
||||
renote: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ドライブ(のファイル)に関する統計
|
||||
*/
|
||||
drive: {
|
||||
local: {
|
||||
/**
|
||||
* この日時点での、ローカルのドライブファイル数の総計
|
||||
*/
|
||||
totalCount: number;
|
||||
|
||||
/**
|
||||
* この日時点での、ローカルのドライブファイルサイズの総計
|
||||
*/
|
||||
totalSize: number;
|
||||
|
||||
/**
|
||||
* ローカルのドライブファイル数の前日比
|
||||
*/
|
||||
diffCount: number;
|
||||
|
||||
/**
|
||||
* ローカルのドライブファイルサイズの前日比
|
||||
*/
|
||||
diffSize: number;
|
||||
};
|
||||
|
||||
remote: {
|
||||
/**
|
||||
* この日時点での、リモートのドライブファイル数の総計
|
||||
*/
|
||||
totalCount: number;
|
||||
|
||||
/**
|
||||
* この日時点での、リモートのドライブファイルサイズの総計
|
||||
*/
|
||||
totalSize: number;
|
||||
|
||||
/**
|
||||
* リモートのドライブファイル数の前日比
|
||||
*/
|
||||
diffCount: number;
|
||||
|
||||
/**
|
||||
* リモートのドライブファイルサイズの前日比
|
||||
*/
|
||||
diffSize: number;
|
||||
};
|
||||
};
|
||||
}
|
101
src/server/api/endpoints/admin/chart.ts
Normal file
101
src/server/api/endpoints/admin/chart.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
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);
|
||||
});
|
|
@ -1,116 +0,0 @@
|
|||
import Note from '../../../../models/note';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggregate notes
|
||||
*/
|
||||
export default (params: any) => new Promise(async (res, rej) => {
|
||||
const query = [{
|
||||
$match: {
|
||||
createdAt: {
|
||||
$gt: new Date(new Date().setFullYear(new Date().getFullYear() - 1))
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$project: {
|
||||
renoteId: '$renoteId',
|
||||
replyId: '$replyId',
|
||||
user: '$_user',
|
||||
createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}
|
||||
}, {
|
||||
$project: {
|
||||
date: {
|
||||
year: { $year: '$createdAt' },
|
||||
month: { $month: '$createdAt' },
|
||||
day: { $dayOfMonth: '$createdAt' }
|
||||
},
|
||||
type: {
|
||||
$cond: {
|
||||
if: { $ne: ['$renoteId', null] },
|
||||
then: 'renote',
|
||||
else: {
|
||||
$cond: {
|
||||
if: { $ne: ['$replyId', null] },
|
||||
then: 'reply',
|
||||
else: 'note'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
origin: {
|
||||
$cond: {
|
||||
if: { $eq: ['$user.host', null] },
|
||||
then: 'local',
|
||||
else: 'remote'
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$group: {
|
||||
_id: {
|
||||
date: '$date',
|
||||
type: '$type',
|
||||
origin: '$origin'
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}, {
|
||||
$group: {
|
||||
_id: '$_id.date',
|
||||
data: {
|
||||
$addToSet: {
|
||||
type: '$_id.type',
|
||||
origin: '$_id.origin',
|
||||
count: '$count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}] as any;
|
||||
|
||||
const datas = await Note.aggregate(query);
|
||||
|
||||
datas.forEach((data: any) => {
|
||||
data.date = data._id;
|
||||
delete data._id;
|
||||
|
||||
data.localNotes = (data.data.filter((x: any) => x.type == 'note' && x.origin == 'local')[0] || { count: 0 }).count;
|
||||
data.localRenotes = (data.data.filter((x: any) => x.type == 'renote' && x.origin == 'local')[0] || { count: 0 }).count;
|
||||
data.localReplies = (data.data.filter((x: any) => x.type == 'reply' && x.origin == 'local')[0] || { count: 0 }).count;
|
||||
data.remoteNotes = (data.data.filter((x: any) => x.type == 'note' && x.origin == 'remote')[0] || { count: 0 }).count;
|
||||
data.remoteRenotes = (data.data.filter((x: any) => x.type == 'renote' && x.origin == 'remote')[0] || { count: 0 }).count;
|
||||
data.remoteReplies = (data.data.filter((x: any) => x.type == 'reply' && x.origin == 'remote')[0] || { count: 0 }).count;
|
||||
|
||||
delete data.data;
|
||||
});
|
||||
|
||||
const graph = [];
|
||||
|
||||
for (let i = 0; i < 365; i++) {
|
||||
const day = new Date(new Date().setDate(new Date().getDate() - i));
|
||||
|
||||
const data = datas.filter((d: any) =>
|
||||
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
|
||||
)[0];
|
||||
|
||||
if (data) {
|
||||
graph.push(data);
|
||||
} else {
|
||||
graph.push({
|
||||
date: { year: day.getFullYear(), month: day.getMonth() + 1, day: day.getDate() },
|
||||
localNotes: 0,
|
||||
localRenotes: 0,
|
||||
localReplies: 0,
|
||||
remoteNotes: 0,
|
||||
remoteRenotes: 0,
|
||||
remoteReplies: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res(graph);
|
||||
});
|
|
@ -1,92 +0,0 @@
|
|||
import User from '../../../../models/user';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireAdmin: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggregate users
|
||||
*/
|
||||
export default (params: any) => new Promise(async (res, rej) => {
|
||||
const query = [{
|
||||
$match: {
|
||||
createdAt: {
|
||||
$gt: new Date(new Date().setFullYear(new Date().getFullYear() - 1))
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$project: {
|
||||
host: '$host',
|
||||
createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
|
||||
}
|
||||
}, {
|
||||
$project: {
|
||||
date: {
|
||||
year: { $year: '$createdAt' },
|
||||
month: { $month: '$createdAt' },
|
||||
day: { $dayOfMonth: '$createdAt' }
|
||||
},
|
||||
origin: {
|
||||
$cond: {
|
||||
if: { $eq: ['$host', null] },
|
||||
then: 'local',
|
||||
else: 'remote'
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
$group: {
|
||||
_id: {
|
||||
date: '$date',
|
||||
origin: '$origin'
|
||||
},
|
||||
count: { $sum: 1 }
|
||||
}
|
||||
}, {
|
||||
$group: {
|
||||
_id: '$_id.date',
|
||||
data: {
|
||||
$addToSet: {
|
||||
type: '$_id.type',
|
||||
origin: '$_id.origin',
|
||||
count: '$count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}] as any;
|
||||
|
||||
const datas = await User.aggregate(query);
|
||||
|
||||
datas.forEach((data: any) => {
|
||||
data.date = data._id;
|
||||
delete data._id;
|
||||
|
||||
data.local = (data.data.filter((x: any) => x.origin == 'local')[0] || { count: 0 }).count;
|
||||
data.remote = (data.data.filter((x: any) => x.origin == 'remote')[0] || { count: 0 }).count;
|
||||
|
||||
delete data.data;
|
||||
});
|
||||
|
||||
const graph = [];
|
||||
|
||||
for (let i = 0; i < 365; i++) {
|
||||
const day = new Date(new Date().setDate(new Date().getDate() - i));
|
||||
|
||||
const data = datas.filter((d: any) =>
|
||||
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
|
||||
)[0];
|
||||
|
||||
if (data) {
|
||||
graph.push(data);
|
||||
} else {
|
||||
graph.push({
|
||||
date: { year: day.getFullYear(), month: day.getMonth() + 1, day: day.getDate() },
|
||||
local: 0,
|
||||
remote: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res(graph);
|
||||
});
|
|
@ -17,6 +17,7 @@ import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
|
|||
import delFile from './delete-file';
|
||||
import config from '../../config';
|
||||
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
||||
import { updateDriveStats } from '../update-chart';
|
||||
|
||||
const log = debug('misskey:drive:add-file');
|
||||
|
||||
|
@ -377,7 +378,8 @@ export default async function(
|
|||
publishDriveStream(user._id, 'file_created', packedFile);
|
||||
});
|
||||
|
||||
// TODO: サムネイル生成
|
||||
// 統計を更新
|
||||
updateDriveStats(driveFile, true);
|
||||
|
||||
return driveFile;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as Minio from 'minio';
|
|||
import DriveFile, { DriveFileChunk, IDriveFile } from '../../models/drive-file';
|
||||
import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-file-thumbnail';
|
||||
import config from '../../config';
|
||||
import { updateDriveStats } from '../update-chart';
|
||||
|
||||
export default async function(file: IDriveFile, isExpired = false) {
|
||||
if (file.metadata.storage == 'minio') {
|
||||
|
@ -45,4 +46,7 @@ export default async function(file: IDriveFile, isExpired = false) {
|
|||
await DriveFileThumbnail.remove({ _id: thumbnail._id });
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 統計を更新
|
||||
updateDriveStats(file, false);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import registerHashtag from '../register-hashtag';
|
|||
import isQuote from '../../misc/is-quote';
|
||||
import { TextElementMention } from '../../mfm/parse/elements/mention';
|
||||
import { TextElementHashtag } from '../../mfm/parse/elements/hashtag';
|
||||
import { updateNoteStats } from '../update-chart';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
@ -142,6 +143,9 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
|||
return;
|
||||
}
|
||||
|
||||
// 統計を更新
|
||||
updateNoteStats(note, true);
|
||||
|
||||
// ハッシュタグ登録
|
||||
tags.map(tag => registerHashtag(user, tag));
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import pack from '../../remote/activitypub/renderer';
|
|||
import { deliver } from '../../queue';
|
||||
import Following from '../../models/following';
|
||||
import renderNote from '../../remote/activitypub/renderer/note';
|
||||
import { updateNoteStats } from '../update-chart';
|
||||
|
||||
/**
|
||||
* 投稿を削除します。
|
||||
|
@ -43,4 +44,7 @@ export default async function(user: IUser, note: INote) {
|
|||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 統計を更新
|
||||
updateNoteStats(note, false);
|
||||
}
|
||||
|
|
223
src/services/update-chart.ts
Normal file
223
src/services/update-chart.ts
Normal file
|
@ -0,0 +1,223 @@
|
|||
import { INote } from '../models/note';
|
||||
import Stats, { IStats } from '../models/stats';
|
||||
import { isLocalUser, IUser } from '../models/user';
|
||||
import { IDriveFile } from '../models/drive-file';
|
||||
|
||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
|
||||
|
||||
async function getTodayStats(): 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 todayStats = await Stats.findOne({
|
||||
date: today
|
||||
});
|
||||
|
||||
// 日付が変わってから、初めてのチャート更新なら
|
||||
if (todayStats == null) {
|
||||
// 最も最近の統計を持ってくる
|
||||
// * 昨日何もチャートを更新するような出来事がなかった場合は、
|
||||
// 統計がそもそも作られずドキュメントが存在しないということがあり得るため、
|
||||
// 「昨日の」と決め打ちせずに「もっとも最近の」とします
|
||||
const mostRecentStats = await Stats.findOne({}, {
|
||||
sort: {
|
||||
date: -1
|
||||
}
|
||||
});
|
||||
|
||||
// 統計が存在しなかったら
|
||||
// * Misskeyインスタンスを建てて初めてのチャート更新時など
|
||||
if (mostRecentStats == null) {
|
||||
// 空の統計を作成
|
||||
const chart: Omit<IStats, '_id'> = {
|
||||
date: today,
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stats = await Stats.insert(chart);
|
||||
|
||||
return stats;
|
||||
} else {
|
||||
// 今日の統計を初期挿入
|
||||
const chart: Omit<IStats, '_id'> = {
|
||||
date: today,
|
||||
users: {
|
||||
local: {
|
||||
total: mostRecentStats.users.local.total,
|
||||
diff: 0
|
||||
},
|
||||
remote: {
|
||||
total: mostRecentStats.users.remote.total,
|
||||
diff: 0
|
||||
}
|
||||
},
|
||||
notes: {
|
||||
local: {
|
||||
total: mostRecentStats.notes.local.total,
|
||||
diff: 0,
|
||||
diffs: {
|
||||
normal: 0,
|
||||
reply: 0,
|
||||
renote: 0
|
||||
}
|
||||
},
|
||||
remote: {
|
||||
total: mostRecentStats.notes.remote.total,
|
||||
diff: 0,
|
||||
diffs: {
|
||||
normal: 0,
|
||||
reply: 0,
|
||||
renote: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
drive: {
|
||||
local: {
|
||||
totalCount: mostRecentStats.drive.local.totalCount,
|
||||
totalSize: mostRecentStats.drive.local.totalSize,
|
||||
diffCount: 0,
|
||||
diffSize: 0
|
||||
},
|
||||
remote: {
|
||||
totalCount: mostRecentStats.drive.remote.totalCount,
|
||||
totalSize: mostRecentStats.drive.remote.totalSize,
|
||||
diffCount: 0,
|
||||
diffSize: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stats = await Stats.insert(chart);
|
||||
|
||||
return stats;
|
||||
}
|
||||
} else {
|
||||
return todayStats;
|
||||
}
|
||||
}
|
||||
|
||||
async function update(inc: any) {
|
||||
const stats = await getTodayStats();
|
||||
|
||||
await Stats.findOneAndUpdate({
|
||||
_id: stats._id
|
||||
}, {
|
||||
$inc: inc
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateUserStats(user: IUser, isAdditional: boolean) {
|
||||
const inc = {} as any;
|
||||
|
||||
const amount = isAdditional ? 1 : -1;
|
||||
|
||||
if (isLocalUser(user)) {
|
||||
inc['users.local.total'] = amount;
|
||||
inc['users.local.diff'] = amount;
|
||||
} else {
|
||||
inc['users.remote.total'] = amount;
|
||||
inc['users.remote.diff'] = amount;
|
||||
}
|
||||
|
||||
await update(inc);
|
||||
}
|
||||
|
||||
export async function updateNoteStats(note: INote, isAdditional: boolean) {
|
||||
const inc = {} as any;
|
||||
|
||||
const amount = isAdditional ? 1 : -1;
|
||||
|
||||
if (isLocalUser(note._user)) {
|
||||
inc['notes.local.total'] = amount;
|
||||
inc['notes.local.diff'] = amount;
|
||||
|
||||
if (note.replyId != null) {
|
||||
inc['notes.local.diffs.reply'] = amount;
|
||||
} else if (note.renoteId != null) {
|
||||
inc['notes.local.diffs.renote'] = amount;
|
||||
} else {
|
||||
inc['notes.local.diffs.normal'] = amount;
|
||||
}
|
||||
} else {
|
||||
inc['notes.remote.total'] = amount;
|
||||
inc['notes.remote.diff'] = amount;
|
||||
|
||||
if (note.replyId != null) {
|
||||
inc['notes.remote.diffs.reply'] = amount;
|
||||
} else if (note.renoteId != null) {
|
||||
inc['notes.remote.diffs.renote'] = amount;
|
||||
} else {
|
||||
inc['notes.remote.diffs.normal'] = amount;
|
||||
}
|
||||
}
|
||||
|
||||
await update(inc);
|
||||
}
|
||||
|
||||
export async function updateDriveStats(file: IDriveFile, isAdditional: boolean) {
|
||||
const inc = {} as any;
|
||||
|
||||
const amount = isAdditional ? 1 : -1;
|
||||
const size = isAdditional ? file.length : -file.length;
|
||||
|
||||
if (isLocalUser(file.metadata._user)) {
|
||||
inc['drive.local.totalCount'] = amount;
|
||||
inc['drive.local.diffCount'] = amount;
|
||||
inc['drive.local.totalSize'] = size;
|
||||
inc['drive.local.diffSize'] = size;
|
||||
} else {
|
||||
inc['drive.remote.total'] = amount;
|
||||
inc['drive.remote.diff'] = amount;
|
||||
inc['drive.remote.totalSize'] = size;
|
||||
inc['drive.remote.diffSize'] = size;
|
||||
}
|
||||
|
||||
await update(inc);
|
||||
}
|
Loading…
Reference in a new issue