summaryrefslogtreecommitdiff
path: root/src/services
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2019-02-08 04:31:33 +0900
committersyuilo <syuilotan@yahoo.co.jp>2019-02-08 04:31:33 +0900
commitaba85b977dfc868c1a65ce06ed58ea59d0371f7f (patch)
tree5e27a5397bb3ee93ae1790ed2f92c6264ae86956 /src/services
parentImplement instance blocking (#4182) (diff)
downloadsharkey-aba85b977dfc868c1a65ce06ed58ea59d0371f7f.tar.gz
sharkey-aba85b977dfc868c1a65ce06ed58ea59d0371f7f.tar.bz2
sharkey-aba85b977dfc868c1a65ce06ed58ea59d0371f7f.zip
Refactoring: Move chart dir into services dir
Diffstat (limited to 'src/services')
-rw-r--r--src/services/blocking/create.ts2
-rw-r--r--src/services/chart/active-users.ts48
-rw-r--r--src/services/chart/drive.ts122
-rw-r--r--src/services/chart/federation.ts66
-rw-r--r--src/services/chart/hashtag.ts56
-rw-r--r--src/services/chart/index.ts348
-rw-r--r--src/services/chart/network.ts64
-rw-r--r--src/services/chart/notes.ts114
-rw-r--r--src/services/chart/per-user-drive.ts101
-rw-r--r--src/services/chart/per-user-following.ts128
-rw-r--r--src/services/chart/per-user-notes.ts94
-rw-r--r--src/services/chart/per-user-reactions.ts45
-rw-r--r--src/services/chart/users.ts75
-rw-r--r--src/services/drive/add-file.ts4
-rw-r--r--src/services/drive/delete-file.ts4
-rw-r--r--src/services/following/create.ts2
-rw-r--r--src/services/following/delete.ts2
-rw-r--r--src/services/following/requests/accept.ts2
-rw-r--r--src/services/note/create.ts6
-rw-r--r--src/services/note/delete.ts4
-rw-r--r--src/services/note/reaction/create.ts2
-rw-r--r--src/services/register-hashtag.ts2
-rw-r--r--src/services/register-or-fetch-instance-doc.ts2
23 files changed, 1277 insertions, 16 deletions
diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts
index 2b46d6b94a..c20666ef26 100644
--- a/src/services/blocking/create.ts
+++ b/src/services/blocking/create.ts
@@ -8,7 +8,7 @@ import renderUndo from '../../remote/activitypub/renderer/undo';
import renderBlock from '../../remote/activitypub/renderer/block';
import { deliver } from '../../queue';
import renderReject from '../../remote/activitypub/renderer/reject';
-import perUserFollowingChart from '../../chart/per-user-following';
+import perUserFollowingChart from '../../services/chart/per-user-following';
import Blocking from '../../models/blocking';
export default async function(blocker: IUser, blockee: IUser) {
diff --git a/src/services/chart/active-users.ts b/src/services/chart/active-users.ts
new file mode 100644
index 0000000000..2a4e1a97ac
--- /dev/null
+++ b/src/services/chart/active-users.ts
@@ -0,0 +1,48 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from '.';
+import { IUser, isLocalUser } from '../../models/user';
+
+/**
+ * アクティブユーザーに関するチャート
+ */
+type ActiveUsersLog = {
+ local: {
+ /**
+ * アクティブユーザー数
+ */
+ count: number;
+ };
+
+ remote: ActiveUsersLog['local'];
+};
+
+class ActiveUsersChart extends Chart<ActiveUsersLog> {
+ constructor() {
+ super('activeUsers');
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: ActiveUsersLog): Promise<ActiveUsersLog> {
+ return {
+ local: {
+ count: 0
+ },
+ remote: {
+ count: 0
+ }
+ };
+ }
+
+ @autobind
+ public async update(user: IUser) {
+ const update: Obj = {
+ count: 1
+ };
+
+ await this.incIfUnique({
+ [isLocalUser(user) ? 'local' : 'remote']: update
+ }, 'users', user._id.toHexString());
+ }
+}
+
+export default new ActiveUsersChart();
diff --git a/src/services/chart/drive.ts b/src/services/chart/drive.ts
new file mode 100644
index 0000000000..972f8c5709
--- /dev/null
+++ b/src/services/chart/drive.ts
@@ -0,0 +1,122 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from './';
+import DriveFile, { IDriveFile } from '../../models/drive-file';
+import { isLocalUser } from '../../models/user';
+
+/**
+ * ドライブに関するチャート
+ */
+type DriveLog = {
+ local: {
+ /**
+ * 集計期間時点での、全ドライブファイル数
+ */
+ totalCount: number;
+
+ /**
+ * 集計期間時点での、全ドライブファイルの合計サイズ
+ */
+ totalSize: number;
+
+ /**
+ * 増加したドライブファイル数
+ */
+ incCount: number;
+
+ /**
+ * 増加したドライブ使用量
+ */
+ incSize: number;
+
+ /**
+ * 減少したドライブファイル数
+ */
+ decCount: number;
+
+ /**
+ * 減少したドライブ使用量
+ */
+ decSize: number;
+ };
+
+ remote: DriveLog['local'];
+};
+
+class DriveChart extends Chart<DriveLog> {
+ constructor() {
+ super('drive');
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: DriveLog): Promise<DriveLog> {
+ const calcSize = (local: boolean) => DriveFile
+ .aggregate([{
+ $match: {
+ 'metadata._user.host': local ? null : { $ne: null },
+ 'metadata.deletedAt': { $exists: false }
+ }
+ }, {
+ $project: {
+ length: true
+ }
+ }, {
+ $group: {
+ _id: null,
+ usage: { $sum: '$length' }
+ }
+ }])
+ .then(res => res.length > 0 ? res[0].usage : 0);
+
+ const [localCount, remoteCount, localSize, remoteSize] = init ? await Promise.all([
+ DriveFile.count({ 'metadata._user.host': null }),
+ DriveFile.count({ 'metadata._user.host': { $ne: null } }),
+ calcSize(true),
+ calcSize(false)
+ ]) : [
+ latest ? latest.local.totalCount : 0,
+ latest ? latest.remote.totalCount : 0,
+ latest ? latest.local.totalSize : 0,
+ latest ? latest.remote.totalSize : 0
+ ];
+
+ return {
+ local: {
+ totalCount: localCount,
+ totalSize: localSize,
+ incCount: 0,
+ incSize: 0,
+ decCount: 0,
+ decSize: 0
+ },
+ remote: {
+ totalCount: remoteCount,
+ totalSize: remoteSize,
+ incCount: 0,
+ incSize: 0,
+ decCount: 0,
+ decSize: 0
+ }
+ };
+ }
+
+ @autobind
+ public async update(file: IDriveFile, isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.totalCount = isAdditional ? 1 : -1;
+ update.totalSize = isAdditional ? file.length : -file.length;
+ if (isAdditional) {
+ update.incCount = 1;
+ update.incSize = file.length;
+ } else {
+ update.decCount = 1;
+ update.decSize = file.length;
+ }
+
+ await this.inc({
+ [isLocalUser(file.metadata._user) ? 'local' : 'remote']: update
+ });
+ }
+}
+
+export default new DriveChart();
diff --git a/src/services/chart/federation.ts b/src/services/chart/federation.ts
new file mode 100644
index 0000000000..20da7a7421
--- /dev/null
+++ b/src/services/chart/federation.ts
@@ -0,0 +1,66 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from '.';
+import Instance from '../../models/instance';
+
+/**
+ * フェデレーションに関するチャート
+ */
+type FederationLog = {
+ instance: {
+ /**
+ * インスタンス数の合計
+ */
+ total: number;
+
+ /**
+ * 増加インスタンス数
+ */
+ inc: number;
+
+ /**
+ * 減少インスタンス数
+ */
+ dec: number;
+ };
+};
+
+class FederationChart extends Chart<FederationLog> {
+ constructor() {
+ super('federation');
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: FederationLog): Promise<FederationLog> {
+ const [total] = init ? await Promise.all([
+ Instance.count({})
+ ]) : [
+ latest ? latest.instance.total : 0
+ ];
+
+ return {
+ instance: {
+ total: total,
+ inc: 0,
+ dec: 0
+ }
+ };
+ }
+
+ @autobind
+ public async update(isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.total = isAdditional ? 1 : -1;
+ if (isAdditional) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ await this.inc({
+ instance: update
+ });
+ }
+}
+
+export default new FederationChart();
diff --git a/src/services/chart/hashtag.ts b/src/services/chart/hashtag.ts
new file mode 100644
index 0000000000..7a31e9cced
--- /dev/null
+++ b/src/services/chart/hashtag.ts
@@ -0,0 +1,56 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from './';
+import { IUser, isLocalUser } from '../../models/user';
+import db from '../../db/mongodb';
+
+/**
+ * ハッシュタグに関するチャート
+ */
+type HashtagLog = {
+ local: {
+ /**
+ * 投稿された数
+ */
+ count: number;
+ };
+
+ remote: HashtagLog['local'];
+};
+
+class HashtagChart extends Chart<HashtagLog> {
+ constructor() {
+ super('hashtag', true);
+
+ // 後方互換性のため
+ db.get('chart.hashtag').findOne().then(doc => {
+ if (doc != null && doc.data.local == null) {
+ db.get('chart.hashtag').drop();
+ }
+ });
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: HashtagLog): Promise<HashtagLog> {
+ return {
+ local: {
+ count: 0
+ },
+ remote: {
+ count: 0
+ }
+ };
+ }
+
+ @autobind
+ public async update(hashtag: string, user: IUser) {
+ const update: Obj = {
+ count: 1
+ };
+
+ await this.incIfUnique({
+ [isLocalUser(user) ? 'local' : 'remote']: update
+ }, 'users', user._id.toHexString(), hashtag);
+ }
+}
+
+export default new HashtagChart();
diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts
new file mode 100644
index 0000000000..30ef2847d6
--- /dev/null
+++ b/src/services/chart/index.ts
@@ -0,0 +1,348 @@
+/**
+ * チャートエンジン
+ */
+
+import * as moment from 'moment';
+import * as nestedProperty from 'nested-property';
+import autobind from 'autobind-decorator';
+import * as mongo from 'mongodb';
+import db from '../../db/mongodb';
+import { ICollection } from 'monk';
+import Logger from '../../misc/logger';
+
+const logger = new Logger('chart');
+
+const utc = moment.utc;
+
+export type Obj = { [key: string]: any };
+
+export type Partial<T> = {
+ [P in keyof T]?: Partial<T[P]>;
+};
+
+type ArrayValue<T> = {
+ [P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>;
+};
+
+type Span = 'day' | 'hour';
+
+type Log<T extends Obj> = {
+ _id: mongo.ObjectID;
+
+ /**
+ * 集計のグループ
+ */
+ group?: any;
+
+ /**
+ * 集計日時
+ */
+ date: Date;
+
+ /**
+ * 集計期間
+ */
+ span: Span;
+
+ /**
+ * データ
+ */
+ data: T;
+
+ /**
+ * ユニークインクリメント用
+ */
+ unique?: Obj;
+};
+
+/**
+ * 様々なチャートの管理を司るクラス
+ */
+export default abstract class Chart<T extends Obj> {
+ protected collection: ICollection<Log<T>>;
+ protected abstract async getTemplate(init: boolean, latest?: T, group?: any): Promise<T>;
+ private name: string;
+
+ constructor(name: string, grouped = false) {
+ this.name = name;
+ this.collection = db.get<Log<T>>(`chart.${name}`);
+
+ const keys = {
+ span: -1,
+ date: -1
+ } as { [key: string]: 1 | -1; };
+ if (grouped) keys.group = -1;
+
+ this.collection.createIndex(keys, { unique: true });
+ }
+
+ @autobind
+ private convertQuery(x: Obj, path: string): Obj {
+ const query: Obj = {};
+
+ const dive = (x: Obj, path: string) => {
+ for (const [k, v] of Object.entries(x)) {
+ const p = path ? `${path}.${k}` : k;
+ if (typeof v === 'number') {
+ query[p] = v;
+ } else {
+ dive(v, p);
+ }
+ }
+ };
+
+ dive(x, path);
+
+ return query;
+ }
+
+ @autobind
+ private getCurrentDate(): [number, number, number, number] {
+ const now = moment().utc();
+
+ const y = now.year();
+ const m = now.month();
+ const d = now.date();
+ const h = now.hour();
+
+ return [y, m, d, h];
+ }
+
+ @autobind
+ private getLatestLog(span: Span, group?: any): Promise<Log<T>> {
+ return this.collection.findOne({
+ group: group,
+ span: span
+ }, {
+ sort: {
+ date: -1
+ }
+ });
+ }
+
+ @autobind
+ private async getCurrentLog(span: Span, group?: any): Promise<Log<T>> {
+ const [y, m, d, h] = this.getCurrentDate();
+
+ const current =
+ span == 'day' ? utc([y, m, d]) :
+ span == 'hour' ? utc([y, m, d, h]) :
+ null;
+
+ // 現在(今日または今のHour)のログ
+ const currentLog = await this.collection.findOne({
+ group: group,
+ span: span,
+ date: current.toDate()
+ });
+
+ // ログがあればそれを返して終了
+ if (currentLog != null) {
+ return currentLog;
+ }
+
+ let log: Log<T>;
+ let data: T;
+
+ // 集計期間が変わってから、初めてのチャート更新なら
+ // 最も最近のログを持ってくる
+ // * 例えば集計期間が「日」である場合で考えると、
+ // * 昨日何もチャートを更新するような出来事がなかった場合は、
+ // * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
+ // * 「昨日の」と決め打ちせずに「もっとも最近の」とします
+ const latest = await this.getLatestLog(span, group);
+
+ if (latest != null) {
+ // 空ログデータを作成
+ data = await this.getTemplate(false, latest.data);
+ } else {
+ // ログが存在しなかったら
+ // (Misskeyインスタンスを建てて初めてのチャート更新時など
+ // または何らかの理由でチャートコレクションを抹消した場合)
+
+ // 初期ログデータを作成
+ data = await this.getTemplate(true, null, group);
+
+ logger.info(`${this.name}: Initial commit created`);
+ }
+
+ try {
+ // 新規ログ挿入
+ log = await this.collection.insert({
+ group: group,
+ span: span,
+ date: current.toDate(),
+ data: data
+ });
+ } catch (e) {
+ // 11000 is duplicate key error
+ // 並列動作している他のチャートエンジンプロセスと処理が重なる場合がある
+ // その場合は再度最も新しいログを持ってくる
+ if (e.code === 11000) {
+ log = await this.getLatestLog(span, group);
+ } else {
+ logger.error(e);
+ throw e;
+ }
+ }
+
+ return log;
+ }
+
+ @autobind
+ protected commit(query: Obj, group?: any, uniqueKey?: string, uniqueValue?: string): void {
+ const update = (log: Log<T>) => {
+ // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く
+ if (
+ uniqueKey &&
+ log.unique &&
+ log.unique[uniqueKey] &&
+ log.unique[uniqueKey].includes(uniqueValue)
+ ) return;
+
+ // ユニークインクリメントの指定のキーに値を追加
+ if (uniqueKey) {
+ query['$push'] = {
+ [`unique.${uniqueKey}`]: uniqueValue
+ };
+ }
+
+ // ログ更新
+ this.collection.update({
+ _id: log._id
+ }, query);
+ };
+
+ this.getCurrentLog('day', group).then(log => update(log));
+ this.getCurrentLog('hour', group).then(log => update(log));
+ }
+
+ @autobind
+ protected inc(inc: Partial<T>, group?: any): void {
+ this.commit({
+ $inc: this.convertQuery(inc, 'data')
+ }, group);
+ }
+
+ @autobind
+ protected incIfUnique(inc: Partial<T>, key: string, value: string, group?: any): void {
+ this.commit({
+ $inc: this.convertQuery(inc, 'data')
+ }, group, key, value);
+ }
+
+ @autobind
+ public async getChart(span: Span, range: number, group?: any): Promise<ArrayValue<T>> {
+ const promisedChart: Promise<T>[] = [];
+
+ const [y, m, d, h] = this.getCurrentDate();
+
+ const gt =
+ span == 'day' ? utc([y, m, d]).subtract(range, 'days') :
+ span == 'hour' ? utc([y, m, d, h]).subtract(range, 'hours') :
+ null;
+
+ // ログ取得
+ let logs = await this.collection.find({
+ group: group,
+ span: span,
+ date: {
+ $gte: gt.toDate()
+ }
+ }, {
+ sort: {
+ date: -1
+ },
+ fields: {
+ _id: 0
+ }
+ });
+
+ // 要求された範囲にログがひとつもなかったら
+ if (logs.length == 0) {
+ // もっとも新しいログを持ってくる
+ // (すくなくともひとつログが無いと隙間埋めできないため)
+ const recentLog = await this.collection.findOne({
+ group: group,
+ span: span
+ }, {
+ sort: {
+ date: -1
+ },
+ fields: {
+ _id: 0
+ }
+ });
+
+ if (recentLog) {
+ logs = [recentLog];
+ }
+
+ // 要求された範囲の最も古い箇所に位置するログが存在しなかったら
+ } else if (!utc(logs[logs.length - 1].date).isSame(gt)) {
+ // 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
+ // (隙間埋めできないため)
+ const outdatedLog = await this.collection.findOne({
+ group: group,
+ span: span,
+ date: {
+ $lt: gt.toDate()
+ }
+ }, {
+ sort: {
+ date: -1
+ },
+ fields: {
+ _id: 0
+ }
+ });
+
+ if (outdatedLog) {
+ logs.push(outdatedLog);
+ }
+ }
+
+ // 整形
+ for (let i = (range - 1); i >= 0; i--) {
+ const current =
+ span == 'day' ? utc([y, m, d]).subtract(i, 'days') :
+ span == 'hour' ? utc([y, m, d, h]).subtract(i, 'hours') :
+ null;
+
+ const log = logs.find(l => utc(l.date).isSame(current));
+
+ if (log) {
+ promisedChart.unshift(Promise.resolve(log.data));
+ } else {
+ // 隙間埋め
+ const latest = logs.find(l => utc(l.date).isBefore(current));
+ promisedChart.unshift(this.getTemplate(false, latest ? latest.data : null));
+ }
+ }
+
+ const chart = await Promise.all(promisedChart);
+
+ const res: ArrayValue<T> = {} as any;
+
+ /**
+ * [{ foo: 1, bar: 5 }, { foo: 2, bar: 6 }, { foo: 3, bar: 7 }]
+ * を
+ * { foo: [1, 2, 3], bar: [5, 6, 7] }
+ * にする
+ */
+ const dive = (x: Obj, path?: string) => {
+ for (const [k, v] of Object.entries(x)) {
+ const p = path ? `${path}.${k}` : k;
+ if (typeof v == 'object') {
+ dive(v, p);
+ } else {
+ nestedProperty.set(res, p, chart.map(s => nestedProperty.get(s, p)));
+ }
+ }
+ };
+
+ dive(chart[0]);
+
+ return res;
+ }
+}
diff --git a/src/services/chart/network.ts b/src/services/chart/network.ts
new file mode 100644
index 0000000000..fce47099d1
--- /dev/null
+++ b/src/services/chart/network.ts
@@ -0,0 +1,64 @@
+import autobind from 'autobind-decorator';
+import Chart, { Partial } from './';
+
+/**
+ * ネットワークに関するチャート
+ */
+type NetworkLog = {
+ /**
+ * 受信したリクエスト数
+ */
+ incomingRequests: number;
+
+ /**
+ * 送信したリクエスト数
+ */
+ outgoingRequests: number;
+
+ /**
+ * 応答時間の合計
+ * TIP: (totalTime / incomingRequests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる
+ */
+ totalTime: number;
+
+ /**
+ * 合計受信データ量
+ */
+ incomingBytes: number;
+
+ /**
+ * 合計送信データ量
+ */
+ outgoingBytes: number;
+};
+
+class NetworkChart extends Chart<NetworkLog> {
+ constructor() {
+ super('network');
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: NetworkLog): Promise<NetworkLog> {
+ return {
+ incomingRequests: 0,
+ outgoingRequests: 0,
+ totalTime: 0,
+ incomingBytes: 0,
+ outgoingBytes: 0
+ };
+ }
+
+ @autobind
+ public async update(incomingRequests: number, time: number, incomingBytes: number, outgoingBytes: number) {
+ const inc: Partial<NetworkLog> = {
+ incomingRequests: incomingRequests,
+ totalTime: time,
+ incomingBytes: incomingBytes,
+ outgoingBytes: outgoingBytes
+ };
+
+ await this.inc(inc);
+ }
+}
+
+export default new NetworkChart();
diff --git a/src/services/chart/notes.ts b/src/services/chart/notes.ts
new file mode 100644
index 0000000000..8f95f63638
--- /dev/null
+++ b/src/services/chart/notes.ts
@@ -0,0 +1,114 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from '.';
+import Note, { INote } from '../../models/note';
+import { isLocalUser } from '../../models/user';
+
+/**
+ * 投稿に関するチャート
+ */
+type NotesLog = {
+ local: {
+ /**
+ * 集計期間時点での、全投稿数
+ */
+ total: number;
+
+ /**
+ * 増加した投稿数
+ */
+ inc: number;
+
+ /**
+ * 減少した投稿数
+ */
+ dec: number;
+
+ diffs: {
+ /**
+ * 通常の投稿数の差分
+ */
+ normal: number;
+
+ /**
+ * リプライの投稿数の差分
+ */
+ reply: number;
+
+ /**
+ * Renoteの投稿数の差分
+ */
+ renote: number;
+ };
+ };
+
+ remote: NotesLog['local'];
+};
+
+class NotesChart extends Chart<NotesLog> {
+ constructor() {
+ super('notes');
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: NotesLog): Promise<NotesLog> {
+ const [localCount, remoteCount] = init ? await Promise.all([
+ Note.count({ '_user.host': null }),
+ Note.count({ '_user.host': { $ne: null } })
+ ]) : [
+ latest ? latest.local.total : 0,
+ latest ? latest.remote.total : 0
+ ];
+
+ return {
+ local: {
+ total: localCount,
+ inc: 0,
+ dec: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ },
+ remote: {
+ total: remoteCount,
+ inc: 0,
+ dec: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ }
+ };
+ }
+
+ @autobind
+ public async update(note: INote, isAdditional: boolean) {
+ const update: Obj = {
+ diffs: {}
+ };
+
+ update.total = isAdditional ? 1 : -1;
+
+ if (isAdditional) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ if (note.replyId != null) {
+ update.diffs.reply = isAdditional ? 1 : -1;
+ } else if (note.renoteId != null) {
+ update.diffs.renote = isAdditional ? 1 : -1;
+ } else {
+ update.diffs.normal = isAdditional ? 1 : -1;
+ }
+
+ await this.inc({
+ [isLocalUser(note._user) ? 'local' : 'remote']: update
+ });
+ }
+}
+
+export default new NotesChart();
diff --git a/src/services/chart/per-user-drive.ts b/src/services/chart/per-user-drive.ts
new file mode 100644
index 0000000000..d23852bdd9
--- /dev/null
+++ b/src/services/chart/per-user-drive.ts
@@ -0,0 +1,101 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from './';
+import DriveFile, { IDriveFile } from '../../models/drive-file';
+
+/**
+ * ユーザーごとのドライブに関するチャート
+ */
+type PerUserDriveLog = {
+ /**
+ * 集計期間時点での、全ドライブファイル数
+ */
+ totalCount: number;
+
+ /**
+ * 集計期間時点での、全ドライブファイルの合計サイズ
+ */
+ totalSize: number;
+
+ /**
+ * 増加したドライブファイル数
+ */
+ incCount: number;
+
+ /**
+ * 増加したドライブ使用量
+ */
+ incSize: number;
+
+ /**
+ * 減少したドライブファイル数
+ */
+ decCount: number;
+
+ /**
+ * 減少したドライブ使用量
+ */
+ decSize: number;
+};
+
+class PerUserDriveChart extends Chart<PerUserDriveLog> {
+ constructor() {
+ super('perUserDrive', true);
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: PerUserDriveLog, group?: any): Promise<PerUserDriveLog> {
+ const calcSize = () => DriveFile
+ .aggregate([{
+ $match: {
+ 'metadata.userId': group,
+ 'metadata.deletedAt': { $exists: false }
+ }
+ }, {
+ $project: {
+ length: true
+ }
+ }, {
+ $group: {
+ _id: null,
+ usage: { $sum: '$length' }
+ }
+ }])
+ .then(res => res.length > 0 ? res[0].usage : 0);
+
+ const [count, size] = init ? await Promise.all([
+ DriveFile.count({ 'metadata.userId': group }),
+ calcSize()
+ ]) : [
+ latest ? latest.totalCount : 0,
+ latest ? latest.totalSize : 0
+ ];
+
+ return {
+ totalCount: count,
+ totalSize: size,
+ incCount: 0,
+ incSize: 0,
+ decCount: 0,
+ decSize: 0
+ };
+ }
+
+ @autobind
+ public async update(file: IDriveFile, isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.totalCount = isAdditional ? 1 : -1;
+ update.totalSize = isAdditional ? file.length : -file.length;
+ if (isAdditional) {
+ update.incCount = 1;
+ update.incSize = file.length;
+ } else {
+ update.decCount = 1;
+ update.decSize = file.length;
+ }
+
+ await this.inc(update, file.metadata.userId);
+ }
+}
+
+export default new PerUserDriveChart();
diff --git a/src/services/chart/per-user-following.ts b/src/services/chart/per-user-following.ts
new file mode 100644
index 0000000000..9d6d347ef6
--- /dev/null
+++ b/src/services/chart/per-user-following.ts
@@ -0,0 +1,128 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from './';
+import Following from '../../models/following';
+import { IUser, isLocalUser } from '../../models/user';
+
+/**
+ * ユーザーごとのフォローに関するチャート
+ */
+type PerUserFollowingLog = {
+ local: {
+ /**
+ * フォローしている
+ */
+ followings: {
+ /**
+ * 合計
+ */
+ total: number;
+
+ /**
+ * フォローした数
+ */
+ inc: number;
+
+ /**
+ * フォロー解除した数
+ */
+ dec: number;
+ };
+
+ /**
+ * フォローされている
+ */
+ followers: {
+ /**
+ * 合計
+ */
+ total: number;
+
+ /**
+ * フォローされた数
+ */
+ inc: number;
+
+ /**
+ * フォロー解除された数
+ */
+ dec: number;
+ };
+ };
+
+ remote: PerUserFollowingLog['local'];
+};
+
+class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
+ constructor() {
+ super('perUserFollowing', true);
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: PerUserFollowingLog, group?: any): Promise<PerUserFollowingLog> {
+ const [
+ localFollowingsCount,
+ localFollowersCount,
+ remoteFollowingsCount,
+ remoteFollowersCount
+ ] = init ? await Promise.all([
+ Following.count({ followerId: group, '_followee.host': null }),
+ Following.count({ followeeId: group, '_follower.host': null }),
+ Following.count({ followerId: group, '_followee.host': { $ne: null } }),
+ Following.count({ followeeId: group, '_follower.host': { $ne: null } })
+ ]) : [
+ latest ? latest.local.followings.total : 0,
+ latest ? latest.local.followers.total : 0,
+ latest ? latest.remote.followings.total : 0,
+ latest ? latest.remote.followers.total : 0
+ ];
+
+ return {
+ local: {
+ followings: {
+ total: localFollowingsCount,
+ inc: 0,
+ dec: 0
+ },
+ followers: {
+ total: localFollowersCount,
+ inc: 0,
+ dec: 0
+ }
+ },
+ remote: {
+ followings: {
+ total: remoteFollowingsCount,
+ inc: 0,
+ dec: 0
+ },
+ followers: {
+ total: remoteFollowersCount,
+ inc: 0,
+ dec: 0
+ }
+ }
+ };
+ }
+
+ @autobind
+ public async update(follower: IUser, followee: IUser, isFollow: boolean) {
+ const update: Obj = {};
+
+ update.total = isFollow ? 1 : -1;
+
+ if (isFollow) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ this.inc({
+ [isLocalUser(follower) ? 'local' : 'remote']: { followings: update }
+ }, follower._id);
+ this.inc({
+ [isLocalUser(followee) ? 'local' : 'remote']: { followers: update }
+ }, followee._id);
+ }
+}
+
+export default new PerUserFollowingChart();
diff --git a/src/services/chart/per-user-notes.ts b/src/services/chart/per-user-notes.ts
new file mode 100644
index 0000000000..9ce4e67c50
--- /dev/null
+++ b/src/services/chart/per-user-notes.ts
@@ -0,0 +1,94 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from './';
+import Note, { INote } from '../../models/note';
+import { IUser } from '../../models/user';
+
+/**
+ * ユーザーごとの投稿に関するチャート
+ */
+type PerUserNotesLog = {
+ /**
+ * 集計期間時点での、全投稿数
+ */
+ total: number;
+
+ /**
+ * 増加した投稿数
+ */
+ inc: number;
+
+ /**
+ * 減少した投稿数
+ */
+ dec: number;
+
+ diffs: {
+ /**
+ * 通常の投稿数の差分
+ */
+ normal: number;
+
+ /**
+ * リプライの投稿数の差分
+ */
+ reply: number;
+
+ /**
+ * Renoteの投稿数の差分
+ */
+ renote: number;
+ };
+};
+
+class PerUserNotesChart extends Chart<PerUserNotesLog> {
+ constructor() {
+ super('perUserNotes', true);
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: PerUserNotesLog, group?: any): Promise<PerUserNotesLog> {
+ const [count] = init ? await Promise.all([
+ Note.count({ userId: group, deletedAt: null }),
+ ]) : [
+ latest ? latest.total : 0
+ ];
+
+ return {
+ total: count,
+ inc: 0,
+ dec: 0,
+ diffs: {
+ normal: 0,
+ reply: 0,
+ renote: 0
+ }
+ };
+ }
+
+ @autobind
+ public async update(user: IUser, note: INote, isAdditional: boolean) {
+ const update: Obj = {
+ diffs: {}
+ };
+
+ update.total = isAdditional ? 1 : -1;
+
+ if (isAdditional) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ if (note.replyId != null) {
+ update.diffs.reply = isAdditional ? 1 : -1;
+ } else if (note.renoteId != null) {
+ update.diffs.renote = isAdditional ? 1 : -1;
+ } else {
+ update.diffs.normal = isAdditional ? 1 : -1;
+ }
+
+ await this.inc(update, user._id);
+ }
+}
+
+export default new PerUserNotesChart();
diff --git a/src/services/chart/per-user-reactions.ts b/src/services/chart/per-user-reactions.ts
new file mode 100644
index 0000000000..60495aeb02
--- /dev/null
+++ b/src/services/chart/per-user-reactions.ts
@@ -0,0 +1,45 @@
+import autobind from 'autobind-decorator';
+import Chart from './';
+import { IUser, isLocalUser } from '../../models/user';
+import { INote } from '../../models/note';
+
+/**
+ * ユーザーごとのリアクションに関するチャート
+ */
+type PerUserReactionsLog = {
+ local: {
+ /**
+ * リアクションされた数
+ */
+ count: number;
+ };
+
+ remote: PerUserReactionsLog['local'];
+};
+
+class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
+ constructor() {
+ super('perUserReaction', true);
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: PerUserReactionsLog, group?: any): Promise<PerUserReactionsLog> {
+ return {
+ local: {
+ count: 0
+ },
+ remote: {
+ count: 0
+ }
+ };
+ }
+
+ @autobind
+ public async update(user: IUser, note: INote) {
+ this.inc({
+ [isLocalUser(user) ? 'local' : 'remote']: { count: 1 }
+ }, note.userId);
+ }
+}
+
+export default new PerUserReactionsChart();
diff --git a/src/services/chart/users.ts b/src/services/chart/users.ts
new file mode 100644
index 0000000000..ce23209aea
--- /dev/null
+++ b/src/services/chart/users.ts
@@ -0,0 +1,75 @@
+import autobind from 'autobind-decorator';
+import Chart, { Obj } from './';
+import User, { IUser, isLocalUser } from '../../models/user';
+
+/**
+ * ユーザーに関するチャート
+ */
+type UsersLog = {
+ local: {
+ /**
+ * 集計期間時点での、全ユーザー数
+ */
+ total: number;
+
+ /**
+ * 増加したユーザー数
+ */
+ inc: number;
+
+ /**
+ * 減少したユーザー数
+ */
+ dec: number;
+ };
+
+ remote: UsersLog['local'];
+};
+
+class UsersChart extends Chart<UsersLog> {
+ constructor() {
+ super('users');
+ }
+
+ @autobind
+ protected async getTemplate(init: boolean, latest?: UsersLog): Promise<UsersLog> {
+ const [localCount, remoteCount] = init ? await Promise.all([
+ User.count({ host: null }),
+ User.count({ host: { $ne: null } })
+ ]) : [
+ latest ? latest.local.total : 0,
+ latest ? latest.remote.total : 0
+ ];
+
+ return {
+ local: {
+ total: localCount,
+ inc: 0,
+ dec: 0
+ },
+ remote: {
+ total: remoteCount,
+ inc: 0,
+ dec: 0
+ }
+ };
+ }
+
+ @autobind
+ public async update(user: IUser, isAdditional: boolean) {
+ const update: Obj = {};
+
+ update.total = isAdditional ? 1 : -1;
+ if (isAdditional) {
+ update.inc = 1;
+ } else {
+ update.dec = 1;
+ }
+
+ await this.inc({
+ [isLocalUser(user) ? 'local' : 'remote']: update
+ });
+ }
+}
+
+export default new UsersChart();
diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts
index 31902b2425..0e588d3442 100644
--- a/src/services/drive/add-file.ts
+++ b/src/services/drive/add-file.ts
@@ -18,8 +18,8 @@ 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 '../../chart/drive';
-import perUserDriveChart from '../../chart/per-user-drive';
+import driveChart from '../../services/chart/drive';
+import perUserDriveChart from '../../services/chart/per-user-drive';
import fetchMeta from '../../misc/fetch-meta';
import { GenerateVideoThumbnail } from './generate-video-thumbnail';
import { driveLogger } from './logger';
diff --git a/src/services/drive/delete-file.ts b/src/services/drive/delete-file.ts
index 609c3a86ea..4211cd8291 100644
--- a/src/services/drive/delete-file.ts
+++ b/src/services/drive/delete-file.ts
@@ -2,8 +2,8 @@ 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 driveChart from '../../chart/drive';
-import perUserDriveChart from '../../chart/per-user-drive';
+import driveChart from '../../services/chart/drive';
+import perUserDriveChart from '../../services/chart/per-user-drive';
import DriveFileWebpublic, { DriveFileWebpublicChunk } from '../../models/drive-file-webpublic';
export default async function(file: IDriveFile, isExpired = false) {
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index 4df271a977..65b80dcf84 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -9,7 +9,7 @@ import renderAccept from '../../remote/activitypub/renderer/accept';
import renderReject from '../../remote/activitypub/renderer/reject';
import { deliver } from '../../queue';
import createFollowRequest from './requests/create';
-import perUserFollowingChart from '../../chart/per-user-following';
+import perUserFollowingChart from '../../services/chart/per-user-following';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
import Instance from '../../models/instance';
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index d56edd3cc3..87eaf826e5 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -5,7 +5,7 @@ import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderUndo from '../../remote/activitypub/renderer/undo';
import { deliver } from '../../queue';
-import perUserFollowingChart from '../../chart/per-user-following';
+import perUserFollowingChart from '../../services/chart/per-user-following';
import Logger from '../../misc/logger';
const logger = new Logger('following/delete');
diff --git a/src/services/following/requests/accept.ts b/src/services/following/requests/accept.ts
index 8c42b5a783..34c7036d19 100644
--- a/src/services/following/requests/accept.ts
+++ b/src/services/following/requests/accept.ts
@@ -6,7 +6,7 @@ import renderAccept from '../../../remote/activitypub/renderer/accept';
import { deliver } from '../../../queue';
import Following from '../../../models/following';
import { publishMainStream } from '../../stream';
-import perUserFollowingChart from '../../../chart/per-user-following';
+import perUserFollowingChart from '../../../services/chart/per-user-following';
import Logger from '../../../misc/logger';
const logger = new Logger('following/requests/accept');
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index d47f4ea9f1..0b71a9670c 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -21,9 +21,9 @@ import Meta from '../../models/meta';
import config from '../../config';
import registerHashtag from '../register-hashtag';
import isQuote from '../../misc/is-quote';
-import notesChart from '../../chart/notes';
-import perUserNotesChart from '../../chart/per-user-notes';
-import activeUsersChart from '../../chart/active-users';
+import notesChart from '../../services/chart/notes';
+import perUserNotesChart from '../../services/chart/per-user-notes';
+import activeUsersChart from '../../services/chart/active-users';
import { erase, concat } from '../../prelude/array';
import insertNoteUnread from './unread';
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index 557872d751..8e8c20bfce 100644
--- a/src/services/note/delete.ts
+++ b/src/services/note/delete.ts
@@ -6,8 +6,8 @@ import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
import Following from '../../models/following';
import renderTombstone from '../../remote/activitypub/renderer/tombstone';
-import notesChart from '../../chart/notes';
-import perUserNotesChart from '../../chart/per-user-notes';
+import notesChart from '../../services/chart/notes';
+import perUserNotesChart from '../../services/chart/per-user-notes';
import config from '../../config';
import NoteUnread from '../../models/note-unread';
import read from './read';
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index e6a9fe7d65..cf72927642 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -8,7 +8,7 @@ import watch from '../watch';
import renderLike from '../../../remote/activitypub/renderer/like';
import { deliver } from '../../../queue';
import { renderActivity } from '../../../remote/activitypub/renderer';
-import perUserReactionsChart from '../../../chart/per-user-reactions';
+import perUserReactionsChart from '../../../services/chart/per-user-reactions';
export default async (user: IUser, note: INote, reaction: string) => new Promise(async (res, rej) => {
// Myself
diff --git a/src/services/register-hashtag.ts b/src/services/register-hashtag.ts
index 57ba2080f2..01b7bc871a 100644
--- a/src/services/register-hashtag.ts
+++ b/src/services/register-hashtag.ts
@@ -1,6 +1,6 @@
import { IUser } from '../models/user';
import Hashtag from '../models/hashtag';
-import hashtagChart from '../chart/hashtag';
+import hashtagChart from '../services/chart/hashtag';
export default async function(user: IUser, tag: string) {
tag = tag.toLowerCase();
diff --git a/src/services/register-or-fetch-instance-doc.ts b/src/services/register-or-fetch-instance-doc.ts
index 3b338b48af..d418cd12ce 100644
--- a/src/services/register-or-fetch-instance-doc.ts
+++ b/src/services/register-or-fetch-instance-doc.ts
@@ -1,5 +1,5 @@
import Instance, { IInstance } from '../models/instance';
-import federationChart from '../chart/federation';
+import federationChart from '../services/chart/federation';
export async function registerOrFetchInstanceDoc(host: string): Promise<IInstance> {
if (host == null) return null;