forked from mirror/misskey
Introduce per-instance chart (#4183)
* Introduce per-instance chart * Implement chart view in client * Handle note deleting * More chart srcs * Add drive stats * Improve drive stats * Fix bug * Add icon
This commit is contained in:
parent
f35688bab8
commit
56275bcfcb
@ -1399,11 +1399,31 @@ admin/views/federation.vue:
|
||||
followingDesc: "フォローが多い順"
|
||||
followersAsc: "フォロワーが少ない順"
|
||||
followersDesc: "フォロワーが多い順"
|
||||
driveUsageAsc: "ドライブ使用量が少ない順"
|
||||
driveUsageDesc: "ドライブ使用量が多い順"
|
||||
driveFilesAsc: "ドライブのファイル数が少ない順"
|
||||
driveFilesDesc: "ドライブのファイル数が多い順"
|
||||
state: "状態"
|
||||
states:
|
||||
all: "すべて"
|
||||
blocked: "ブロック"
|
||||
result-is-truncated: "上位{n}件を表示しています。"
|
||||
charts: "チャート"
|
||||
chart-srcs:
|
||||
requests: "リクエスト"
|
||||
users: "ユーザーの増減"
|
||||
users-total: "ユーザーの積算"
|
||||
notes: "投稿の増減"
|
||||
notes-total: "投稿の積算"
|
||||
ff: "フォロー/フォロワーの増減"
|
||||
ff-total: "フォロー/フォロワーの積算"
|
||||
drive-usage: "ドライブ使用量の増減"
|
||||
drive-usage-total: "ドライブ使用量の増減"
|
||||
drive-files: "ドライブファイル数の増減"
|
||||
drive-files-total: "ドライブファイル数の増減"
|
||||
chart-spans:
|
||||
hour: "1時間ごと"
|
||||
day: "1日ごと"
|
||||
|
||||
desktop/views/pages/welcome.vue:
|
||||
about: "詳しく..."
|
||||
|
@ -40,6 +40,29 @@
|
||||
<span>{{ $t('latest-request-received-at') }}</span>
|
||||
</ui-input>
|
||||
<ui-switch v-model="instance.isBlocked" @change="updateInstance()">{{ $t('block') }}</ui-switch>
|
||||
<details>
|
||||
<summary>{{ $t('charts') }}</summary>
|
||||
<ui-horizon-group inputs>
|
||||
<ui-select v-model="chartSrc">
|
||||
<option value="requests">{{ $t('chart-srcs.requests') }}</option>
|
||||
<option value="users">{{ $t('chart-srcs.users') }}</option>
|
||||
<option value="users-total">{{ $t('chart-srcs.users-total') }}</option>
|
||||
<option value="notes">{{ $t('chart-srcs.notes') }}</option>
|
||||
<option value="notes-total">{{ $t('chart-srcs.notes-total') }}</option>
|
||||
<option value="ff">{{ $t('chart-srcs.ff') }}</option>
|
||||
<option value="ff-total">{{ $t('chart-srcs.ff-total') }}</option>
|
||||
<option value="drive-usage">{{ $t('chart-srcs.drive-usage') }}</option>
|
||||
<option value="drive-usage-total">{{ $t('chart-srcs.drive-usage-total') }}</option>
|
||||
<option value="drive-files">{{ $t('chart-srcs.drive-files') }}</option>
|
||||
<option value="drive-files-total">{{ $t('chart-srcs.drive-files-total') }}</option>
|
||||
</ui-select>
|
||||
<ui-select v-model="chartSpan">
|
||||
<option value="hour">{{ $t('chart-spans.hour') }}</option>
|
||||
<option value="day">{{ $t('chart-spans.day') }}</option>
|
||||
</ui-select>
|
||||
</ui-horizon-group>
|
||||
<div ref="chart"></div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>{{ $t('remove-all-following') }}</summary>
|
||||
<ui-button @click="removeAllFollowing()" style="margin-top: 16px;"><fa :icon="faMinusCircle"/> {{ $t('remove-all-following') }}</ui-button>
|
||||
@ -50,7 +73,7 @@
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
<div slot="title"><fa :icon="faUsers"/> {{ $t('instances') }}</div>
|
||||
<div slot="title"><fa :icon="faServer"/> {{ $t('instances') }}</div>
|
||||
<section class="fit-top">
|
||||
<ui-horizon-group inputs>
|
||||
<ui-select v-model="sort">
|
||||
@ -65,6 +88,10 @@
|
||||
<option value="+following">{{ $t('sorts.followingDesc') }}</option>
|
||||
<option value="-followers">{{ $t('sorts.followersAsc') }}</option>
|
||||
<option value="+followers">{{ $t('sorts.followersDesc') }}</option>
|
||||
<option value="-driveUsage">{{ $t('sorts.driveUsageAsc') }}</option>
|
||||
<option value="+driveUsage">{{ $t('sorts.driveUsageDesc') }}</option>
|
||||
<option value="-driveFiles">{{ $t('sorts.driveFilesAsc') }}</option>
|
||||
<option value="+driveFiles">{{ $t('sorts.driveFilesDesc') }}</option>
|
||||
</ui-select>
|
||||
<ui-select v-model="state">
|
||||
<span slot="label">{{ $t('state') }}</span>
|
||||
@ -101,7 +128,13 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import { faGlobe, faTerminal, faSearch, faMinusCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import ApexCharts from 'apexcharts';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
|
||||
const chartLimit = 90;
|
||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||
const negate = arr => arr.map(x => -x);
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/federation.vue'),
|
||||
@ -114,10 +147,42 @@ export default Vue.extend({
|
||||
state: 'all',
|
||||
limit: 50,
|
||||
instances: [],
|
||||
faGlobe, faTerminal, faSearch, faMinusCircle
|
||||
chart: null,
|
||||
chartSrc: 'requests',
|
||||
chartSpan: 'hour',
|
||||
chartInstance: null,
|
||||
faGlobe, faTerminal, faSearch, faMinusCircle, faServer
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
data(): any {
|
||||
if (this.chart == null) return null;
|
||||
switch (this.chartSrc) {
|
||||
case 'requests': return this.requestsChart();
|
||||
case 'users': return this.usersChart(false);
|
||||
case 'users-total': return this.usersChart(true);
|
||||
case 'notes': return this.notesChart(false);
|
||||
case 'notes-total': return this.notesChart(true);
|
||||
case 'ff': return this.ffChart(false);
|
||||
case 'ff-total': return this.ffChart(true);
|
||||
case 'drive-usage': return this.driveUsageChart(false);
|
||||
case 'drive-usage-total': return this.driveUsageChart(true);
|
||||
case 'drive-files': return this.driveFilesChart(false);
|
||||
case 'drive-files-total': return this.driveFilesChart(true);
|
||||
}
|
||||
},
|
||||
|
||||
stats(): any[] {
|
||||
const stats =
|
||||
this.chartSpan == 'day' ? this.chart.perDay :
|
||||
this.chartSpan == 'hour' ? this.chart.perHour :
|
||||
null;
|
||||
|
||||
return stats;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
sort() {
|
||||
this.fetchInstances();
|
||||
@ -126,12 +191,42 @@ export default Vue.extend({
|
||||
state() {
|
||||
this.fetchInstances();
|
||||
},
|
||||
|
||||
async instance() {
|
||||
this.now = new Date();
|
||||
|
||||
const [perHour, perDay] = await Promise.all([
|
||||
this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'hour' }),
|
||||
this.$root.api('charts/instance', { host: this.instance.host, limit: chartLimit, span: 'day' }),
|
||||
]);
|
||||
|
||||
const chart = {
|
||||
perHour: perHour,
|
||||
perDay: perDay
|
||||
};
|
||||
|
||||
this.chart = chart;
|
||||
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
chartSrc() {
|
||||
this.renderChart();
|
||||
},
|
||||
|
||||
chartSpan() {
|
||||
this.renderChart();
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchInstances();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.chartInstance.destroy();
|
||||
},
|
||||
|
||||
methods: {
|
||||
showInstance() {
|
||||
this.$root.api('federation/show-instance', {
|
||||
@ -177,6 +272,180 @@ export default Vue.extend({
|
||||
isBlocked: this.instance.isBlocked,
|
||||
});
|
||||
},
|
||||
|
||||
setSrc(src) {
|
||||
this.chartSrc = src;
|
||||
},
|
||||
|
||||
renderChart() {
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.destroy();
|
||||
}
|
||||
|
||||
this.chartInstance = new ApexCharts(this.$refs.chart, {
|
||||
chart: {
|
||||
type: 'area',
|
||||
height: 300,
|
||||
animations: {
|
||||
dynamicAnimation: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
toolbar: {
|
||||
show: false
|
||||
},
|
||||
zoom: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
clipMarkers: false,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
width: 2
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
style: {
|
||||
colors: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||
}
|
||||
},
|
||||
axisBorder: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
axisTicks: {
|
||||
color: 'rgba(0, 0, 0, 0.1)'
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: this.data.bytes ? v => Vue.filter('bytes')(v, 0) : v => Vue.filter('number')(v),
|
||||
style: {
|
||||
color: tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--text')).toRgbString()
|
||||
}
|
||||
}
|
||||
},
|
||||
series: this.data.series
|
||||
});
|
||||
|
||||
this.chartInstance.render();
|
||||
},
|
||||
|
||||
getDate(i: number) {
|
||||
const y = this.now.getFullYear();
|
||||
const m = this.now.getMonth();
|
||||
const d = this.now.getDate();
|
||||
const h = this.now.getHours();
|
||||
|
||||
return (
|
||||
this.chartSpan == 'day' ? new Date(y, m, d - i) :
|
||||
this.chartSpan == 'hour' ? new Date(y, m, d, h - i) :
|
||||
null
|
||||
);
|
||||
},
|
||||
|
||||
format(arr) {
|
||||
return arr.map((v, i) => ({ x: this.getDate(i).getTime(), y: v }));
|
||||
},
|
||||
|
||||
requestsChart(): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Incoming',
|
||||
data: this.format(this.stats.requests.received)
|
||||
}, {
|
||||
name: 'Outgoing (succeeded)',
|
||||
data: this.format(this.stats.requests.succeeded)
|
||||
}, {
|
||||
name: 'Outgoing (failed)',
|
||||
data: this.format(this.stats.requests.failed)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
usersChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Users',
|
||||
type: 'area',
|
||||
data: this.format(total
|
||||
? this.stats.users.total
|
||||
: sum(this.stats.users.inc, negate(this.stats.users.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
notesChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Notes',
|
||||
type: 'area',
|
||||
data: this.format(total
|
||||
? this.stats.notes.total
|
||||
: sum(this.stats.notes.inc, negate(this.stats.notes.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
ffChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Following',
|
||||
type: 'area',
|
||||
data: this.format(total
|
||||
? this.stats.following.total
|
||||
: sum(this.stats.following.inc, negate(this.stats.following.dec))
|
||||
)
|
||||
}, {
|
||||
name: 'Followers',
|
||||
type: 'area',
|
||||
data: this.format(total
|
||||
? this.stats.followers.total
|
||||
: sum(this.stats.followers.inc, negate(this.stats.followers.dec))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveUsageChart(total: boolean): any {
|
||||
return {
|
||||
bytes: true,
|
||||
series: [{
|
||||
name: 'Drive usage',
|
||||
type: 'area',
|
||||
data: this.format(total
|
||||
? this.stats.drive.totalUsage
|
||||
: sum(this.stats.drive.incUsage, negate(this.stats.drive.decUsage))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
|
||||
driveFilesChart(total: boolean): any {
|
||||
return {
|
||||
series: [{
|
||||
name: 'Drive files',
|
||||
type: 'area',
|
||||
data: this.format(total
|
||||
? this.stats.drive.totalFiles
|
||||
: sum(this.stats.drive.incFiles, negate(this.stats.drive.decFiles))
|
||||
)
|
||||
}]
|
||||
};
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -11,6 +11,7 @@ DriveFile.createIndex('md5');
|
||||
DriveFile.createIndex('metadata.uri');
|
||||
DriveFile.createIndex('metadata.userId');
|
||||
DriveFile.createIndex('metadata.folderId');
|
||||
DriveFile.createIndex('metadata._user.host');
|
||||
export default DriveFile;
|
||||
|
||||
export const DriveFileChunk = monkDb.get('driveFiles.chunks');
|
||||
|
@ -43,6 +43,16 @@ export interface IInstance {
|
||||
*/
|
||||
followersCount: number;
|
||||
|
||||
/**
|
||||
* ドライブ使用量
|
||||
*/
|
||||
driveUsage: number;
|
||||
|
||||
/**
|
||||
* ドライブのファイル数
|
||||
*/
|
||||
driveFiles: number;
|
||||
|
||||
/**
|
||||
* 直近のリクエスト送信日時
|
||||
*/
|
||||
|
@ -17,6 +17,7 @@ const User = db.get<IUser>('users');
|
||||
|
||||
User.createIndex('username');
|
||||
User.createIndex('usernameLower');
|
||||
User.createIndex('host');
|
||||
User.createIndex(['username', 'host'], { unique: true });
|
||||
User.createIndex(['usernameLower', 'host'], { unique: true });
|
||||
User.createIndex('token', { sparse: true, unique: true });
|
||||
|
@ -4,6 +4,7 @@ import request from '../../../remote/activitypub/request';
|
||||
import { queueLogger } from '../../logger';
|
||||
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
|
||||
import Instance from '../../../models/instance';
|
||||
import instanceChart from '../../../services/chart/instance';
|
||||
|
||||
export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
const { host } = new URL(job.data.to);
|
||||
@ -19,6 +20,8 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
latestStatus: 200
|
||||
}
|
||||
});
|
||||
|
||||
instanceChart.requestSent(i.host, true);
|
||||
});
|
||||
|
||||
done();
|
||||
@ -31,6 +34,8 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
latestStatus: res != null && res.hasOwnProperty('statusCode') ? res.statusCode : null
|
||||
}
|
||||
});
|
||||
|
||||
instanceChart.requestSent(i.host, false);
|
||||
});
|
||||
|
||||
if (res != null && res.hasOwnProperty('statusCode')) {
|
||||
|
@ -10,6 +10,7 @@ import { publishApLogStream } from '../../../services/stream';
|
||||
import Logger from '../../../misc/logger';
|
||||
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
|
||||
import Instance from '../../../models/instance';
|
||||
import instanceChart from '../../../services/chart/instance';
|
||||
|
||||
const logger = new Logger('inbox');
|
||||
|
||||
@ -128,6 +129,8 @@ export default async (job: bq.Job, done: any): Promise<void> => {
|
||||
latestRequestReceivedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
instanceChart.requestReceived(i.host);
|
||||
});
|
||||
|
||||
// アクティビティを処理
|
||||
|
@ -10,6 +10,7 @@ import { IDriveFile } from '../../../models/drive-file';
|
||||
import Meta from '../../../models/meta';
|
||||
import { fromHtml } from '../../../mfm/fromHtml';
|
||||
import usersChart from '../../../services/chart/users';
|
||||
import instanceChart from '../../../services/chart/instance';
|
||||
import { URL } from 'url';
|
||||
import { resolveNote, extractEmojis } from './note';
|
||||
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
|
||||
@ -195,8 +196,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
||||
}
|
||||
});
|
||||
|
||||
// TODO
|
||||
//perInstanceChart.newUser();
|
||||
instanceChart.newUser(i.host);
|
||||
});
|
||||
|
||||
//#region Increment users count
|
||||
|
42
src/server/api/endpoints/charts/instance.ts
Normal file
42
src/server/api/endpoints/charts/instance.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import instanceChart from '../../../../services/chart/instance';
|
||||
|
||||
export const meta = {
|
||||
stability: 'stable',
|
||||
|
||||
desc: {
|
||||
'ja-JP': 'インスタンスごとのチャートを取得します。'
|
||||
},
|
||||
|
||||
params: {
|
||||
span: {
|
||||
validator: $.str.or(['day', 'hour']),
|
||||
desc: {
|
||||
'ja-JP': '集計のスパン (day または hour)'
|
||||
}
|
||||
},
|
||||
|
||||
limit: {
|
||||
validator: $.num.optional.range(1, 500),
|
||||
default: 30,
|
||||
desc: {
|
||||
'ja-JP': '最大数。例えば 30 を指定したとすると、スパンが"day"の場合は30日分のデータが、スパンが"hour"の場合は30時間分のデータが返ります。'
|
||||
}
|
||||
},
|
||||
|
||||
host: {
|
||||
validator: $.str,
|
||||
desc: {
|
||||
'ja-JP': '対象のインスタンスのホスト',
|
||||
'en-US': 'Target instance host'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, (ps) => new Promise(async (res, rej) => {
|
||||
const stats = await instanceChart.getChart(ps.span as any, ps.limit, ps.host);
|
||||
|
||||
res(stats);
|
||||
}));
|
@ -70,6 +70,22 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||
sort = {
|
||||
caughtAt: 1
|
||||
};
|
||||
} else if (ps.sort == '+driveUsage') {
|
||||
sort = {
|
||||
driveUsage: -1
|
||||
};
|
||||
} else if (ps.sort == '-driveUsage') {
|
||||
sort = {
|
||||
driveUsage: 1
|
||||
};
|
||||
} else if (ps.sort == '+driveFiles') {
|
||||
sort = {
|
||||
driveFiles: -1
|
||||
};
|
||||
} else if (ps.sort == '-driveFiles') {
|
||||
sort = {
|
||||
driveFiles: 1
|
||||
};
|
||||
}
|
||||
} else {
|
||||
sort = {
|
||||
|
302
src/services/chart/instance.ts
Normal file
302
src/services/chart/instance.ts
Normal file
@ -0,0 +1,302 @@
|
||||
import autobind from 'autobind-decorator';
|
||||
import Chart, { Obj } from '.';
|
||||
import User from '../../models/user';
|
||||
import Note from '../../models/note';
|
||||
import Following from '../../models/following';
|
||||
import DriveFile, { IDriveFile } from '../../models/drive-file';
|
||||
|
||||
/**
|
||||
* インスタンスごとのチャート
|
||||
*/
|
||||
type InstanceLog = {
|
||||
requests: {
|
||||
/**
|
||||
* 失敗したリクエスト数
|
||||
*/
|
||||
failed: number;
|
||||
|
||||
/**
|
||||
* 成功したリクエスト数
|
||||
*/
|
||||
succeeded: number;
|
||||
|
||||
/**
|
||||
* 受信したリクエスト数
|
||||
*/
|
||||
received: number;
|
||||
};
|
||||
|
||||
notes: {
|
||||
/**
|
||||
* 集計期間時点での、全投稿数
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* 増加した投稿数
|
||||
*/
|
||||
inc: number;
|
||||
|
||||
/**
|
||||
* 減少した投稿数
|
||||
*/
|
||||
dec: number;
|
||||
};
|
||||
|
||||
users: {
|
||||
/**
|
||||
* 集計期間時点での、全ユーザー数
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* 増加したユーザー数
|
||||
*/
|
||||
inc: number;
|
||||
|
||||
/**
|
||||
* 減少したユーザー数
|
||||
*/
|
||||
dec: number;
|
||||
};
|
||||
|
||||
following: {
|
||||
/**
|
||||
* 集計期間時点での、全フォロー数
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* 増加したフォロー数
|
||||
*/
|
||||
inc: number;
|
||||
|
||||
/**
|
||||
* 減少したフォロー数
|
||||
*/
|
||||
dec: number;
|
||||
};
|
||||
|
||||
followers: {
|
||||
/**
|
||||
* 集計期間時点での、全フォロワー数
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* 増加したフォロワー数
|
||||
*/
|
||||
inc: number;
|
||||
|
||||
/**
|
||||
* 減少したフォロワー数
|
||||
*/
|
||||
dec: number;
|
||||
};
|
||||
|
||||
drive: {
|
||||
/**
|
||||
* 集計期間時点での、全ドライブファイル数
|
||||
*/
|
||||
totalFiles: number;
|
||||
|
||||
/**
|
||||
* 集計期間時点での、全ドライブファイルの合計サイズ
|
||||
*/
|
||||
totalUsage: number;
|
||||
|
||||
/**
|
||||
* 増加したドライブファイル数
|
||||
*/
|
||||
incFiles: number;
|
||||
|
||||
/**
|
||||
* 増加したドライブ使用量
|
||||
*/
|
||||
incUsage: number;
|
||||
|
||||
/**
|
||||
* 減少したドライブファイル数
|
||||
*/
|
||||
decFiles: number;
|
||||
|
||||
/**
|
||||
* 減少したドライブ使用量
|
||||
*/
|
||||
decUsage: number;
|
||||
};
|
||||
};
|
||||
|
||||
class InstanceChart extends Chart<InstanceLog> {
|
||||
constructor() {
|
||||
super('instance', true);
|
||||
}
|
||||
|
||||
@autobind
|
||||
protected async getTemplate(init: boolean, latest?: InstanceLog, group?: any): Promise<InstanceLog> {
|
||||
const calcUsage = () => DriveFile
|
||||
.aggregate([{
|
||||
$match: {
|
||||
'metadata._user.host': group,
|
||||
'metadata.deletedAt': { $exists: false }
|
||||
}
|
||||
}, {
|
||||
$project: {
|
||||
length: true
|
||||
}
|
||||
}, {
|
||||
$group: {
|
||||
_id: null,
|
||||
usage: { $sum: '$length' }
|
||||
}
|
||||
}])
|
||||
.then(res => res.length > 0 ? res[0].usage : 0);
|
||||
|
||||
const [
|
||||
notesCount,
|
||||
usersCount,
|
||||
followingCount,
|
||||
followersCount,
|
||||
driveFiles,
|
||||
driveUsage,
|
||||
] = init ? await Promise.all([
|
||||
Note.count({ '_user.host': group }),
|
||||
User.count({ host: group }),
|
||||
Following.count({ '_follower.host': group }),
|
||||
Following.count({ '_followee.host': group }),
|
||||
DriveFile.count({ 'metadata._user.host': group }),
|
||||
calcUsage(),
|
||||
]) : [
|
||||
latest ? latest.notes.total : 0,
|
||||
latest ? latest.users.total : 0,
|
||||
latest ? latest.following.total : 0,
|
||||
latest ? latest.followers.total : 0,
|
||||
latest ? latest.drive.totalFiles : 0,
|
||||
latest ? latest.drive.totalUsage : 0,
|
||||
];
|
||||
|
||||
return {
|
||||
requests: {
|
||||
failed: 0,
|
||||
succeeded: 0,
|
||||
received: 0
|
||||
},
|
||||
notes: {
|
||||
total: notesCount,
|
||||
inc: 0,
|
||||
dec: 0
|
||||
},
|
||||
users: {
|
||||
total: usersCount,
|
||||
inc: 0,
|
||||
dec: 0
|
||||
},
|
||||
following: {
|
||||
total: followingCount,
|
||||
inc: 0,
|
||||
dec: 0
|
||||
},
|
||||
followers: {
|
||||
total: followersCount,
|
||||
inc: 0,
|
||||
dec: 0
|
||||
},
|
||||
drive: {
|
||||
totalFiles: driveFiles,
|
||||
totalUsage: driveUsage,
|
||||
incFiles: 0,
|
||||
incUsage: 0,
|
||||
decFiles: 0,
|
||||
decUsage: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@autobind
|
||||
public async requestReceived(host: string) {
|
||||
await this.inc({
|
||||
requests: {
|
||||
received: 1
|
||||
}
|
||||
}, host);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public async requestSent(host: string, isSucceeded: boolean) {
|
||||
const update: Obj = {};
|
||||
|
||||
if (isSucceeded) {
|
||||
update.succeeded = 1;
|
||||
} else {
|
||||
update.failed = 1;
|
||||
}
|
||||
|
||||
await this.inc({
|
||||
requests: update
|
||||
}, host);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public async newUser(host: string) {
|
||||
await this.inc({
|
||||
users: {
|
||||
total: 1,
|
||||
inc: 1
|
||||
}
|
||||
}, host);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public async updateNote(host: string, isAdditional: boolean) {
|
||||
await this.inc({
|
||||
notes: {
|
||||
total: isAdditional ? 1 : -1,
|
||||
inc: isAdditional ? 1 : 0,
|
||||
dec: isAdditional ? 0 : 1,
|
||||
}
|
||||
}, host);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public async updateFollowing(host: string, isAdditional: boolean) {
|
||||
await this.inc({
|
||||
following: {
|
||||
total: isAdditional ? 1 : -1,
|
||||
inc: isAdditional ? 1 : 0,
|
||||
dec: isAdditional ? 0 : 1,
|
||||
}
|
||||
}, host);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public async updateFollowers(host: string, isAdditional: boolean) {
|
||||
await this.inc({
|
||||
followers: {
|
||||
total: isAdditional ? 1 : -1,
|
||||
inc: isAdditional ? 1 : 0,
|
||||
dec: isAdditional ? 0 : 1,
|
||||
}
|
||||
}, host);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public async updateDrive(file: IDriveFile, isAdditional: boolean) {
|
||||
const update: Obj = {};
|
||||
|
||||
update.totalFiles = isAdditional ? 1 : -1;
|
||||
update.totalUsage = isAdditional ? file.length : -file.length;
|
||||
if (isAdditional) {
|
||||
update.incFiles = 1;
|
||||
update.incUsage = file.length;
|
||||
} else {
|
||||
update.decFiles = 1;
|
||||
update.decUsage = file.length;
|
||||
}
|
||||
|
||||
await this.inc({
|
||||
drive: update
|
||||
}, file.metadata._user.host);
|
||||
}
|
||||
}
|
||||
|
||||
export default new InstanceChart();
|
@ -13,17 +13,19 @@ import DriveFile, { IMetadata, getDriveFileBucket, IDriveFile } from '../../mode
|
||||
import DriveFolder from '../../models/drive-folder';
|
||||
import { pack } from '../../models/drive-file';
|
||||
import { publishMainStream, publishDriveStream } from '../stream';
|
||||
import { isLocalUser, IUser, IRemoteUser } from '../../models/user';
|
||||
import { isLocalUser, IUser, IRemoteUser, isRemoteUser } from '../../models/user';
|
||||
import delFile from './delete-file';
|
||||
import config from '../../config';
|
||||
import { getDriveFileWebpublicBucket } from '../../models/drive-file-webpublic';
|
||||
import { getDriveFileThumbnailBucket } from '../../models/drive-file-thumbnail';
|
||||
import driveChart from '../../services/chart/drive';
|
||||
import perUserDriveChart from '../../services/chart/per-user-drive';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
import fetchMeta from '../../misc/fetch-meta';
|
||||
import { GenerateVideoThumbnail } from './generate-video-thumbnail';
|
||||
import { driveLogger } from './logger';
|
||||
import { IImage, ConvertToJpeg, ConvertToWebp, ConvertToPng } from './image-processor';
|
||||
import Instance from '../../models/instance';
|
||||
|
||||
const logger = driveLogger.createSubLogger('register', 'yellow');
|
||||
|
||||
@ -523,6 +525,15 @@ export default async function(
|
||||
// 統計を更新
|
||||
driveChart.update(driveFile, true);
|
||||
perUserDriveChart.update(driveFile, true);
|
||||
if (isRemoteUser(driveFile.metadata._user)) {
|
||||
instanceChart.updateDrive(driveFile, true);
|
||||
Instance.update({ host: driveFile.metadata._user.host }, {
|
||||
$inc: {
|
||||
driveUsage: driveFile.length,
|
||||
driveFiles: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return driveFile;
|
||||
}
|
||||
|
@ -4,7 +4,10 @@ import DriveFileThumbnail, { DriveFileThumbnailChunk } from '../../models/drive-
|
||||
import config from '../../config';
|
||||
import driveChart from '../../services/chart/drive';
|
||||
import perUserDriveChart from '../../services/chart/per-user-drive';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic';
|
||||
import Instance from '../../models/instance';
|
||||
import { isRemoteUser } from '../../models/user';
|
||||
|
||||
export default async function(file: IDriveFile, isExpired = false) {
|
||||
if (file.metadata.storage == 'minio') {
|
||||
@ -84,4 +87,13 @@ export default async function(file: IDriveFile, isExpired = false) {
|
||||
// 統計を更新
|
||||
driveChart.update(file, false);
|
||||
perUserDriveChart.update(file, false);
|
||||
if (isRemoteUser(file.metadata._user)) {
|
||||
instanceChart.updateDrive(file, false);
|
||||
Instance.update({ host: file.metadata._user.host }, {
|
||||
$inc: {
|
||||
driveUsage: -file.length,
|
||||
driveFiles: -1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import createFollowRequest from './requests/create';
|
||||
import perUserFollowingChart from '../../services/chart/per-user-following';
|
||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
|
||||
import Instance from '../../models/instance';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
|
||||
export default async function(follower: IUser, followee: IUser, requestId?: string) {
|
||||
// check blocking
|
||||
@ -108,8 +109,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
|
||||
}
|
||||
});
|
||||
|
||||
// TODO
|
||||
//perInstanceChart.newFollowing();
|
||||
instanceChart.updateFollowing(i.host, true);
|
||||
});
|
||||
} else if (isLocalUser(follower) && isRemoteUser(followee)) {
|
||||
registerOrFetchInstanceDoc(followee.host).then(i => {
|
||||
@ -119,8 +119,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
|
||||
}
|
||||
});
|
||||
|
||||
// TODO
|
||||
//perInstanceChart.newFollower();
|
||||
instanceChart.updateFollowers(i.host, true);
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
@ -7,6 +7,9 @@ import renderUndo from '../../remote/activitypub/renderer/undo';
|
||||
import { deliver } from '../../queue';
|
||||
import perUserFollowingChart from '../../services/chart/per-user-following';
|
||||
import Logger from '../../misc/logger';
|
||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
|
||||
import Instance from '../../models/instance';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
|
||||
const logger = new Logger('following/delete');
|
||||
|
||||
@ -41,6 +44,30 @@ export default async function(follower: IUser, followee: IUser) {
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Update instance stats
|
||||
if (isRemoteUser(follower) && isLocalUser(followee)) {
|
||||
registerOrFetchInstanceDoc(follower.host).then(i => {
|
||||
Instance.update({ _id: i._id }, {
|
||||
$inc: {
|
||||
followingCount: -1
|
||||
}
|
||||
});
|
||||
|
||||
instanceChart.updateFollowing(i.host, false);
|
||||
});
|
||||
} else if (isLocalUser(follower) && isRemoteUser(followee)) {
|
||||
registerOrFetchInstanceDoc(followee.host).then(i => {
|
||||
Instance.update({ _id: i._id }, {
|
||||
$inc: {
|
||||
followersCount: -1
|
||||
}
|
||||
});
|
||||
|
||||
instanceChart.updateFollowers(i.host, false);
|
||||
});
|
||||
}
|
||||
//#endregion
|
||||
|
||||
perUserFollowingChart.update(follower, followee, false);
|
||||
|
||||
// Publish unfollow event
|
||||
|
@ -24,6 +24,7 @@ import isQuote from '../../misc/is-quote';
|
||||
import notesChart from '../../services/chart/notes';
|
||||
import perUserNotesChart from '../../services/chart/per-user-notes';
|
||||
import activeUsersChart from '../../services/chart/active-users';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
|
||||
import { erase, concat } from '../../prelude/array';
|
||||
import insertNoteUnread from './unread';
|
||||
@ -229,8 +230,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
|
||||
}
|
||||
});
|
||||
|
||||
// TODO
|
||||
//perInstanceChart.newNote();
|
||||
instanceChart.updateNote(i.host, true);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Note, { INote } from '../../models/note';
|
||||
import { IUser, isLocalUser } from '../../models/user';
|
||||
import { IUser, isLocalUser, isRemoteUser } from '../../models/user';
|
||||
import { publishNoteStream } from '../stream';
|
||||
import renderDelete from '../../remote/activitypub/renderer/delete';
|
||||
import { renderActivity } from '../../remote/activitypub/renderer';
|
||||
@ -12,6 +12,9 @@ import config from '../../config';
|
||||
import NoteUnread from '../../models/note-unread';
|
||||
import read from './read';
|
||||
import DriveFile from '../../models/drive-file';
|
||||
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
|
||||
import Instance from '../../models/instance';
|
||||
import instanceChart from '../../services/chart/instance';
|
||||
|
||||
/**
|
||||
* 投稿を削除します。
|
||||
@ -91,4 +94,16 @@ export default async function(user: IUser, note: INote) {
|
||||
// 統計を更新
|
||||
notesChart.update(note, false);
|
||||
perUserNotesChart.update(user, note, false);
|
||||
|
||||
if (isRemoteUser(user)) {
|
||||
registerOrFetchInstanceDoc(user.host).then(i => {
|
||||
Instance.update({ _id: i._id }, {
|
||||
$inc: {
|
||||
notesCount: -1
|
||||
}
|
||||
});
|
||||
|
||||
instanceChart.updateNote(i.host, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user