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
 | ||||
|  |  | |||
|  | @ -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 = { | ||||
|  |  | |||
|  | @ -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