import mongoose = require ( 'mongoose' ) ; import { mStatsAllTimeDoc , mStatsDayDoc , mStatsHourDoc , mStatsAllTimeSchema , mStatsDaySchema , mStatsHourSchema , mStatsHourObject , mStatsObject , errorDoc , errorSchema , mStatsDayObject , megalogFunctions , createEmptyMStatsObject , bugDoc , botSuggestionDoc , bugSchema , botSuggestionSchema } from './schemas' ; import { Bot } from '..' ; import { durations , toNano } from '../utils/time' ; import crypto = require ( 'crypto' ) ; import { Message } from 'discord.js' ; export class export class MStats { type { mongoose . Connection } memberof connection : mongoose . Connection ; type { mongoose . Model < mStatsAllTimeDoc > } memberof allTime : mongoose . Model < mStatsAllTimeDoc > ; type { mongoose . Model < mStatsDayDoc > } memberof daily : mongoose . Model < mStatsDayDoc > ; hourly : { model : mongoose . Model < mStatsHourDoc > ; doc : mStatsHourDoc ; pingTestCounter : number ; interval : NodeJS . Timeout ; } ; type { mongoose . Model < errorDoc > } memberof errors : mongoose . Model < errorDoc > ; type { mongoose . Model < bugDoc > } memberof bugs : mongoose . Model < bugDoc > ; type { mongoose . Model < botSuggestionDoc > } memberof suggestions : mongoose . Model < botSuggestionDoc > ; param { string } URI param { string } authDB memberof constructor ( clusterInfo ) { this . connection = mongoose . createConnection ( clusterInfo . url + '/mStats' + clusterInfo . suffix , { useNewUrlParser : true } ) ; this . connection . on ( 'error' , error => { console . error ( 'connection error:' , error ) ; Bot . mStats . logError ( error ) ; } ) ; this . connection . once ( 'open' , function ( ) { console . log ( 'connected to /mStats database' ) ; Bot . mStats . allTime = Bot . mStats . connection . model ( 'allTime' , mStatsAllTimeSchema , 'allTime' ) ; Bot . mStats . daily = Bot . mStats . connection . model ( 'day' , mStatsDaySchema , 'daily' ) ; Bot . mStats . errors = Bot . mStats . connection . model ( 'error' , errorSchema , 'errors' ) ; Bot . mStats . bugs = Bot . mStats . connection . model ( 'bug' , bugSchema , 'bugs' ) ; Bot . mStats . suggestions = Bot . mStats . connection . model ( 'suggestion' , botSuggestionSchema , 'suggestions' ) ; Bot . mStats . init ( ) ; } ) ; } private memberof private async init ( ) { var date = new Date ( ) ; var UTC = date . getTime ( ) ; var day = UTC - ( UTC % durations . day ) ; var hour = date . getUTCHours ( ) ; var model = this . connection . model < mStatsHourDoc > ( 'hour' , mStatsHourSchema , 'hourly' ) ; let existingDays = await model . distinct ( 'day' ) . exec ( ) ; if ( existingDays . includes ( day ) ) existingDays . splice ( existingDays . indexOf ( day ) , 1 ) ; for ( const existingDay of existingDays ) { if ( ! await this . daily . findOne ( { day : existingDay } ) ) { let hourObjects = ( await model . find ( { day : existingDay } ) . exec ( ) ) . map ( x => x . toObject ( ) ) ; let mergedObject ; if ( hourObjects . length != 1 ) { let newestIndex = 0 ; let newestHour = hourObjects [ newestIndex ] ; hourObjects . forEach ( ( x , i ) => { if ( x . hour > newestHour . hour ) { newestHour = x ; newestIndex = i ; } } ) ; hourObjects . splice ( newestIndex , 1 ) ; mergedObject = this . mergeStats ( hourObjects , newestHour ) ; mergedObject . day = existingDay ; } else { mergedObject = hourObjects [ 0 ] ; delete mergedObject . day ; } await new this . daily ( mergedObject ) . save ( ) ; } model . deleteMany ( { day : existingDay } ) . exec ( ) ; } console . info ( `Resolved ${ existingDays . length } days in hourly collection.` ) ; var doc = await model . findOne ( { day : day , hour : hour } ) . exec ( ) ; var pingTestCounter = 1 ; if ( ! doc ) { let docObject = createEmptyMStatsObject ( ) ; docObject . day = day ; docObject . hour = hour ; doc = new model ( docObject ) ; doc . markModified ( 'commands' ) ; doc . markModified ( 'filters' ) ; doc . markModified ( 'webhooks' ) ; doc . save ( ) ; pingTestCounter = 0 ; } else { if ( ! doc . megalog ) doc . megalog = createEmptyMStatsObject ( ) . megalog ; if ( ! doc . megalog . enabled ) doc . megalog . enabled = createEmptyMStatsObject ( ) . megalog . enabled ; console . info ( 'Using existing hour document' ) ; } this . hourly = { model : model , doc : doc , pingTestCounter : pingTestCounter , interval : null } ; this . hourly . interval = this . createHourInterval ( durations . minute / 6 , durations . hour - ( UTC % durations . hour ) - durations . minute ) ; setTimeout ( ( ) => { this . changeHour ( ) ; setInterval ( ( ) => { this . changeHour ( ) ; } , durations . hour ) ; } , durations . hour - ( UTC % durations . hour ) + 1000 ) ; } param { { model : mongoose . Model < mStatsHourDoc > ; doc : mStatsHourDoc ; pingTestCounter : number ; interval : NodeJS . Timeout ; } } hourly returns memberof async saveHour ( hourly ) { try { var ping = hourly . doc . toObject ( ) . ping ; var clientAPI = Math . round ( Bot . client . ping ) ; var cluster = await Bot . database . ping ( ) ; hourly . doc . ping . clientAPI = ( ( ping . clientAPI * hourly . pingTestCounter ) + clientAPI ) / ( hourly . pingTestCounter + 1 ) ; hourly . doc . ping . cluster = ( ( ping . cluster * hourly . pingTestCounter ) + cluster ) / ( hourly . pingTestCounter + 1 ) ; hourly . pingTestCounter ++ ; hourly . doc . guildsTotal = Bot . client . guilds . size ; if ( ! hourly . doc . webhooks ) hourly . doc . webhooks = { } ; if ( ! hourly . doc . webhooks . youtube ) { hourly . doc . webhooks . youtube = { total : 0 , created : 0 , deleted : 0 , changed : 0 } ; } hourly . doc . webhooks . youtube . total = await Bot . youtube . webhooks . countDocuments ( ) . exec ( ) ; for ( const megalogFunction of megalogFunctions . all ) { let query = { } ; query [ megalogFunction ] = { $exists : true } ; hourly . doc . megalog . enabled [ megalogFunction ] = await Bot . database . mainDB . megalogs . countDocuments ( query ) . exec ( ) ; } for ( const keys in hourly . doc . toObject ( ) ) { hourly . doc . markModified ( keys ) ; } return await hourly . doc . save ( ) ; } catch ( e ) { console . error ( "from saveHour():" , e , hourly ) ; Bot . mStats . logError ( e ) ; } } private param { number } timeout param { number } clearTimeout returns memberof private createHourInterval ( timeout , clearTimeout = durations . hour - durations . minute ) { var interval = setInterval ( this . saveHour , timeout , this . hourly ) ; setTimeout ( ( ) => { clearInterval ( interval ) ; } , clearTimeout ) ; return interval ; } private memberof private async changeHour ( ) { var date = new Date ( ) ; var UTC = date . getTime ( ) ; var day = UTC - ( UTC % durations . day ) ; var hour = date . getUTCHours ( ) ; await this . saveHour ( this . hourly ) ; var oldHourObject = this . hourly . doc . toObject ( ) ; if ( oldHourObject . hour > hour ) { await this . changeDay ( oldHourObject . day ) ; } var hourObject = createEmptyMStatsObject ( ) ; hourObject . day = day ; hourObject . hour = hour ; this . hourly . doc = new this . hourly . model ( hourObject ) ; await this . saveHour ( this . hourly ) ; this . hourly . pingTestCounter = 0 ; this . hourly . interval = this . createHourInterval ( durations . minute ) ; console . log ( `MStats hour from ${ oldHourObject . hour } to ${ hour }` ) ; } param { number } day returns memberof async changeDay ( day ) { var hourDocs = await this . hourly . model . find ( { day : day } ) . exec ( ) ; var hourObjects = [ ] ; let newestHourObject ; for ( const hour of hourDocs ) { let hourObject = hour . toObject ( ) ; if ( ! newestHourObject || newestHourObject . hour < hourObject . hour ) newestHourObject = hourObject ; hourObjects . push ( hourObject ) ; hour . remove ( ) ; } var mergedObject = this . mergeStats ( hourObjects , newestHourObject ) ; mergedObject . day = day ; var dayDoc = new this . daily ( mergedObject ) ; dayDoc . markModified ( 'commands' ) ; dayDoc . markModified ( 'filters' ) ; dayDoc . markModified ( 'webhooks' ) ; await dayDoc . save ( ) ; var allTimeDoc = await this . allTime . findOne ( ) ; if ( ! allTimeDoc ) { mergedObject . from = day ; mergedObject . to = day + durations . day ; allTimeDoc = new this . allTime ( mergedObject ) ; allTimeDoc . markModified ( 'commands' ) ; allTimeDoc . markModified ( 'filters' ) ; allTimeDoc . markModified ( 'webhooks' ) ; allTimeDoc . save ( ) ; console . log ( 'made new all time doc' ) ; return ; } let dayObject = dayDoc . toObject ( ) ; var allTimeObject = this . mergeStats ( [ allTimeDoc . toObject ( ) , dayObject ] , dayObject ) ; allTimeDoc . set ( allTimeObject ) ; allTimeDoc . to = day + durations . day ; await allTimeDoc . save ( ) ; console . log ( `updated all time. ${ hourObjects . length } hours recorded for day ${ day }` ) ; } param { mStatsObject [ ] } docs param { mStatsObject } newestDoc returns memberof mergeStats ( docs , newestDoc ) { var mergedDoc = createEmptyMStatsObject ( ) ; for ( const doc of docs ) { mergedDoc . messagesReceived += doc . messagesReceived ; mergedDoc . messagesSend += doc . messagesSend ; mergedDoc . logs += doc . logs ; mergedDoc . guildsJoined += doc . guildsJoined ; mergedDoc . guildsLeft += doc . guildsLeft ; if ( mergedDoc . bugs ) mergedDoc . bugs += doc . bugs ; if ( mergedDoc . botSuggestions ) mergedDoc . botSuggestions += doc . botSuggestions ; mergedDoc . commandTotal += doc . commandTotal ; for ( const cmd in doc . commands ) { if ( ! mergedDoc . commands [ cmd ] ) { mergedDoc . commands [ cmd ] = { _errors : 0 , _resp : 0 } ; } for ( const subCmd in doc . commands [ cmd ] ) { if ( ! mergedDoc . commands [ cmd ] [ subCmd ] ) { mergedDoc . commands [ cmd ] [ subCmd ] = doc . commands [ cmd ] [ subCmd ] ; } else { mergedDoc . commands [ cmd ] [ subCmd ] += doc . commands [ cmd ] [ subCmd ] ; } } } for ( const filter in doc . filters ) { if ( ! mergedDoc . filters [ filter ] ) { mergedDoc . filters [ filter ] = doc . filters [ filter ] ; } else { mergedDoc . filters [ filter ] += doc . filters [ filter ] ; } } for ( const service in doc . webhooks ) { if ( ! mergedDoc . webhooks [ service ] ) { mergedDoc . webhooks [ service ] = doc . webhooks [ service ] ; } else { for ( const key in doc . webhooks [ service ] ) { mergedDoc . webhooks [ service ] [ key ] += doc . webhooks [ service ] [ key ] ; } } } mergedDoc . ping . clientAPI += doc . ping . clientAPI ; mergedDoc . ping . cluster += doc . ping . cluster ; if ( doc . megalog ) for ( const megalogFunction in doc . megalog . logged ) mergedDoc . megalog . logged [ megalogFunction ] += doc . megalog . logged [ megalogFunction ] ; } for ( const command in mergedDoc . commands ) { mergedDoc . commands [ command ] . _resp /= docs . length ; } mergedDoc . ping . clientAPI /= docs . length ; mergedDoc . ping . cluster /= docs . length ; mergedDoc . guildsTotal = newestDoc . guildsTotal ; for ( const service in mergedDoc . webhooks ) { if ( newestDoc . webhooks [ service ] && newestDoc . webhooks [ service ] . total ) { mergedDoc . webhooks [ service ] . total = newestDoc . webhooks [ service ] . total ; } else { mergedDoc . webhooks [ service ] . total = 0 ; } } if ( newestDoc . megalog ) mergedDoc . megalog . enabled = Object . assign ( { } , newestDoc . megalog . enabled ) ; return mergedDoc ; } memberof logMessageReceived ( ) { if ( ! this . hourly ) return ; if ( this . hourly . doc . messagesReceived ) { this . hourly . doc . messagesReceived += 1 ; } else { this . hourly . doc . messagesReceived = 1 ; } } memberof logMessageSend ( ) { if ( ! this . hourly ) return ; this . hourly . doc . messagesSend += 1 ; } memberof logLog ( ) { if ( ! this . hourly ) return ; this . hourly . doc . logs += 1 ; } memberof logGuildJoin ( ) { if ( ! this . hourly ) return ; this . hourly . doc . guildsJoined += 1 ; } memberof logGuildLeave ( ) { if ( ! this . hourly ) return ; this . hourly . doc . guildsLeft += 1 ; } param { Error } error param { string } command returns memberof async logError ( error , command ? ) { if ( ! this . hourly ) return ; this . hourly . doc . errorsTotal += 1 ; if ( command ) { if ( ! this . hourly . doc . commands ) { this . hourly . doc . commands = { } ; } if ( ! this . hourly . doc . commands [ command ] ) { this . hourly . doc . commands [ command ] = { _resp : 0 , _errors : 0 } ; } else { this . hourly . doc . commands [ command ] . _errors += 1 ; } } let date = new Date ( ) ; var stringifiedError = JSON . stringify ( error , Object . getOwnPropertyNames ( error ) ) ; var md5 = crypto . createHash ( 'md5' ) . update ( stringifiedError ) . digest ( 'hex' ) ; let errorDoc = await this . errors . findOne ( { md5 : md5 } ) . exec ( ) ; if ( ! errorDoc ) { errorDoc = new this . errors ( { first : date . getTime ( ) , last : date . getTime ( ) , md5 : md5 , count : 1 , error : JSON . parse ( stringifiedError ) } ) ; } else { errorDoc . last = date . getTime ( ) ; errorDoc . count += 1 ; } return await errorDoc . save ( ) ; } param { string } command param { string } subCommand memberof logCommandUsage ( command , subCommand ? ) { if ( ! this . hourly ) return ; this . hourly . doc . commandTotal += 1 ; if ( ! subCommand ) { subCommand = '_main' ; } if ( ! this . hourly . doc . commands ) { this . hourly . doc . commands = { } ; } if ( ! this . hourly . doc . commands [ command ] ) { this . hourly . doc . commands [ command ] = { _resp : 0 , _errors : 0 } ; } if ( this . hourly . doc . commands [ command ] [ subCommand ] ) { this . hourly . doc . commands [ command ] [ subCommand ] += 1 ; } else { this . hourly . doc . commands [ command ] [ subCommand ] = 1 ; } } param { string } command param { number } requestTime returns memberof logResponseTime ( command , requestTime ) { if ( ! this . hourly ) return ; var latency = toNano ( process . hrtime ( requestTime ) ) ; if ( ! this . hourly . doc . commands ) { this . hourly . doc . commands = { } ; } if ( ! this . hourly . doc . commands [ command ] ) { this . hourly . doc . commands [ command ] = { _errors : 0 , _resp : latency } ; return ; } var uses = 0 ; var commandStats = this . hourly . doc . toObject ( ) . commands [ command ] ; for ( const subCommand in commandStats ) { if ( subCommand == '_resp' || subCommand == '_errors' ) continue ; uses += commandStats [ subCommand ] ; } var resp = ( commandStats . _resp * uses ) + latency ; this . hourly . doc . commands [ command ] . _resp = resp / ( uses + 1 ) ; } param { string } filter memberof logFilterCatch ( filter ) { if ( ! this . hourly ) return ; if ( ! this . hourly . doc . filters ) { this . hourly . doc . filters = { } ; } if ( this . hourly . doc . filters [ filter ] ) { this . hourly . doc . filters [ filter ] += 1 ; } else { this . hourly . doc . filters [ filter ] = 1 ; } } param { string } service param { ( 'created' | 'changed' | 'deleted' ) } action memberof logWebhookAction ( service , action ) { if ( ! this . hourly ) return ; if ( ! this . hourly . doc . webhooks ) { this . hourly . doc . webhooks = { } ; } if ( ! this . hourly . doc . webhooks [ service ] || ! this . hourly . doc . webhooks [ service ] . created ) { this . hourly . doc . webhooks [ service ] = { total : 0 , created : 0 , changed : 0 , deleted : 0 } ; } this . hourly . doc . webhooks [ service ] [ action ] += 1 ; } param { string } megalogFunction returns memberof logMegalogLog ( megalogFunction ) { if ( ! this . hourly ) return ; if ( ! this . hourly . doc . megalog ) this . hourly . doc . megalog = createEmptyMStatsObject ( ) . megalog ; if ( ! this . hourly . doc . megalog . logged ) this . hourly . doc . megalog . logged = createEmptyMStatsObject ( ) . megalog . logged ; if ( ! megalogFunctions . all . includes ( megalogFunction ) ) { console . warn ( `Invalid input in mStats.logMegalogLog function: ${ megalogFunction }` ) ; return ; } if ( isNaN ( this . hourly . doc . megalog . logged [ megalogFunction ] ) ) { this . hourly . doc . megalog . logged [ megalogFunction ] = 1 ; } else { this . hourly . doc . megalog . logged [ megalogFunction ] += 1 ; } } param { Message } message param { string } bug returns memberof logBug ( message , bug ) { if ( ! this . hourly ) return ; this . hourly . doc . bugs ++ ; return new this . bugs ( { user : message . author . id , guild : message . guild ? message . guild . id : undefined , bug : bug } ) . save ( ) ; } param { Message } message param { suggestion } suggestion returns memberof logBotSuggestion ( message , suggestion ) { if ( ! this . hourly ) return ; this . hourly . doc . botSuggestions ++ ; return new this . suggestions ( { user : message . author . id , guild : message . guild ? message . guild . id : undefined , suggestion : suggestion } ) . save ( ) ; } }