diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2019-02-08 04:31:33 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2019-02-08 04:31:33 +0900 |
| commit | aba85b977dfc868c1a65ce06ed58ea59d0371f7f (patch) | |
| tree | 5e27a5397bb3ee93ae1790ed2f92c6264ae86956 /src/services/chart | |
| parent | Implement instance blocking (#4182) (diff) | |
| download | sharkey-aba85b977dfc868c1a65ce06ed58ea59d0371f7f.tar.gz sharkey-aba85b977dfc868c1a65ce06ed58ea59d0371f7f.tar.bz2 sharkey-aba85b977dfc868c1a65ce06ed58ea59d0371f7f.zip | |
Refactoring: Move chart dir into services dir
Diffstat (limited to 'src/services/chart')
| -rw-r--r-- | src/services/chart/active-users.ts | 48 | ||||
| -rw-r--r-- | src/services/chart/drive.ts | 122 | ||||
| -rw-r--r-- | src/services/chart/federation.ts | 66 | ||||
| -rw-r--r-- | src/services/chart/hashtag.ts | 56 | ||||
| -rw-r--r-- | src/services/chart/index.ts | 348 | ||||
| -rw-r--r-- | src/services/chart/network.ts | 64 | ||||
| -rw-r--r-- | src/services/chart/notes.ts | 114 | ||||
| -rw-r--r-- | src/services/chart/per-user-drive.ts | 101 | ||||
| -rw-r--r-- | src/services/chart/per-user-following.ts | 128 | ||||
| -rw-r--r-- | src/services/chart/per-user-notes.ts | 94 | ||||
| -rw-r--r-- | src/services/chart/per-user-reactions.ts | 45 | ||||
| -rw-r--r-- | src/services/chart/users.ts | 75 |
12 files changed, 1261 insertions, 0 deletions
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(); |