2025-09-03 23:20:56 +02:00
|
|
|
|
"use strict";
|
2025-09-01 16:25:25 +02:00
|
|
|
|
/* DokuWiki BotMon Plugin Script file */
|
2025-09-12 15:38:28 +02:00
|
|
|
|
/* 12.09.2025 - 0.3.0 - beta */
|
2025-09-06 16:20:58 +02:00
|
|
|
|
/* Author: Sascha Leib <ad@hominem.info> */
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
// enumeration of user types:
|
|
|
|
|
|
const BM_USERTYPE = Object.freeze({
|
|
|
|
|
|
'UNKNOWN': 'unknown',
|
|
|
|
|
|
'KNOWN_USER': 'user',
|
2025-09-19 15:08:25 +02:00
|
|
|
|
'PROBABLY_HUMAN': 'human',
|
2025-09-04 23:01:46 +02:00
|
|
|
|
'LIKELY_BOT': 'likely_bot',
|
|
|
|
|
|
'KNOWN_BOT': 'known_bot'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
// enumeration of log types:
|
|
|
|
|
|
const BM_LOGTYPE = Object.freeze({
|
|
|
|
|
|
'SERVER': 'srv',
|
|
|
|
|
|
'CLIENT': 'log',
|
|
|
|
|
|
'TICKER': 'tck'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
/* BotMon root object */
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const BotMon = {
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
init: function() {
|
2025-09-03 18:22:29 +02:00
|
|
|
|
//console.info('BotMon.init()');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// find the plugin basedir:
|
|
|
|
|
|
this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/'))
|
2025-09-01 16:25:25 +02:00
|
|
|
|
+ '/plugins/botmon/';
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// read the page language from the DOM:
|
|
|
|
|
|
this._lang = document.getRootNode().documentElement.lang || this._lang;
|
|
|
|
|
|
|
|
|
|
|
|
// get the time offset:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
this._timeDiff = BotMon.t._getTimeOffset();
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// init the sub-objects:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.t._callInit(this);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
_baseDir: null,
|
|
|
|
|
|
_lang: 'en',
|
2025-09-17 22:29:47 +02:00
|
|
|
|
_datestr: (new Date()).toISOString().slice(0, 10),
|
2025-08-29 23:14:10 +03:00
|
|
|
|
_timeDiff: '',
|
|
|
|
|
|
|
|
|
|
|
|
/* internal tools */
|
|
|
|
|
|
t: {
|
|
|
|
|
|
/* helper function to call inits of sub-objects */
|
|
|
|
|
|
_callInit: function(obj) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.t._callInit(obj=',obj,')');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
/* call init / _init on each sub-object: */
|
|
|
|
|
|
Object.keys(obj).forEach( (key,i) => {
|
|
|
|
|
|
const sub = obj[key];
|
|
|
|
|
|
let init = null;
|
|
|
|
|
|
if (typeof sub === 'object' && sub.init) {
|
|
|
|
|
|
init = sub.init;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// bind to object
|
|
|
|
|
|
if (typeof init == 'function') {
|
|
|
|
|
|
const init2 = init.bind(sub);
|
|
|
|
|
|
init2(obj);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* helper function to calculate the time difference to UTC: */
|
|
|
|
|
|
_getTimeOffset: function() {
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
let offset = now.getTimezoneOffset(); // in minutes
|
|
|
|
|
|
const sign = Math.sign(offset); // +1 or -1
|
|
|
|
|
|
offset = Math.abs(offset); // always positive
|
|
|
|
|
|
|
|
|
|
|
|
let hours = 0;
|
|
|
|
|
|
while (offset >= 60) {
|
|
|
|
|
|
hours += 1;
|
|
|
|
|
|
offset -= 60;
|
|
|
|
|
|
}
|
|
|
|
|
|
return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : '');
|
2025-09-03 18:22:29 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* helper function to create a new element with all attributes and text content */
|
|
|
|
|
|
_makeElement: function(name, atlist = undefined, text = undefined) {
|
|
|
|
|
|
var r = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
r = document.createElement(name);
|
|
|
|
|
|
if (atlist) {
|
|
|
|
|
|
for (let attr in atlist) {
|
|
|
|
|
|
r.setAttribute(attr, atlist[attr]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (text) {
|
|
|
|
|
|
r.textContent = text.toString();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch(e) {
|
|
|
|
|
|
console.error(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
2025-09-06 16:20:58 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* helper to convert an ip address string to a normalised format: */
|
|
|
|
|
|
_ip2Num: function(ip) {
|
2025-09-14 19:48:09 +02:00
|
|
|
|
if (!ip) {
|
|
|
|
|
|
return 'null';
|
|
|
|
|
|
} else if (ip.indexOf(':') > 0) { /* IP6 */
|
2025-09-06 16:20:58 +02:00
|
|
|
|
return (ip.split(':').map(d => ('0000'+d).slice(-4) ).join(''));
|
|
|
|
|
|
} else { /* IP4 */
|
|
|
|
|
|
return Number(ip.split('.').map(d => ('000'+d).slice(-3) ).join(''));
|
|
|
|
|
|
}
|
2025-09-07 16:11:17 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* helper function to format a Date object to show only the time. */
|
|
|
|
|
|
/* returns String */
|
|
|
|
|
|
_formatTime: function(date) {
|
|
|
|
|
|
|
|
|
|
|
|
if (date) {
|
2025-09-14 19:32:48 +02:00
|
|
|
|
return date.getHours() + ':' + ('0'+date.getMinutes()).slice(-2) + ':' + ('0'+date.getSeconds()).slice(-2);
|
2025-09-07 16:11:17 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* helper function to show a time difference in seconds or minutes */
|
|
|
|
|
|
/* returns String */
|
|
|
|
|
|
_formatTimeDiff: function(dateA, dateB) {
|
|
|
|
|
|
|
|
|
|
|
|
// if the second date is ealier, swap them:
|
|
|
|
|
|
if (dateA > dateB) dateB = [dateA, dateA = dateB][0];
|
|
|
|
|
|
|
|
|
|
|
|
// get the difference in milliseconds:
|
|
|
|
|
|
let ms = dateB - dateA;
|
|
|
|
|
|
|
|
|
|
|
|
if (ms > 50) { /* ignore small time spans */
|
|
|
|
|
|
const h = Math.floor((ms / (1000 * 60 * 60)) % 24);
|
|
|
|
|
|
const m = Math.floor((ms / (1000 * 60)) % 60);
|
|
|
|
|
|
const s = Math.floor((ms / 1000) % 60);
|
|
|
|
|
|
|
|
|
|
|
|
return ( h>0 ? h + 'h ': '') + ( m>0 ? m + 'm ': '') + ( s>0 ? s + 's': '');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-29 23:28:19 +03:00
|
|
|
|
};
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
/* everything specific to the "Today" tab is self-contained in the "live" object: */
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live = {
|
2025-08-29 23:14:10 +03:00
|
|
|
|
init: function() {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.init()');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// set the title:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (BotMon._timeDiff != '' ? `, ${BotMon._timeDiff}` : '' ) + ')';
|
2025-09-17 22:29:47 +02:00
|
|
|
|
BotMon.live.gui.status.setTitle(`Data for <time datetime="${BotMon._datestr}">${BotMon._datestr}</time> ${tDiff}`);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// init sub-objects:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.t._callInit(this);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
data: {
|
|
|
|
|
|
init: function() {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.data.init()');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// call sub-inits:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.t._callInit(this);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// this will be called when the known json files are done loading:
|
|
|
|
|
|
_dispatch: function(file) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.data._dispatch(,',file,')');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// shortcut to make code more readable:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const data = BotMon.live.data;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// set the flags:
|
|
|
|
|
|
switch(file) {
|
2025-09-06 16:20:58 +02:00
|
|
|
|
case 'rules':
|
|
|
|
|
|
data._dispatchRulesLoaded = true;
|
|
|
|
|
|
break;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
case 'bots':
|
|
|
|
|
|
data._dispatchBotsLoaded = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'clients':
|
|
|
|
|
|
data._dispatchClientsLoaded = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'platforms':
|
|
|
|
|
|
data._dispatchPlatformsLoaded = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// are all the flags set?
|
2025-09-05 16:04:40 +02:00
|
|
|
|
if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded && data._dispatchRulesLoaded) {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// chain the log files loading:
|
2025-09-19 15:08:25 +02:00
|
|
|
|
BotMon.live.data.loadLogFile(BM_LOGTYPE.SERVER, BotMon.live.data._onServerLogLoaded);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
},
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// flags to track which data files have been loaded:
|
|
|
|
|
|
_dispatchBotsLoaded: false,
|
|
|
|
|
|
_dispatchClientsLoaded: false,
|
|
|
|
|
|
_dispatchPlatformsLoaded: false,
|
2025-09-05 16:04:40 +02:00
|
|
|
|
_dispatchRulesLoaded: false,
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// event callback, after the server log has been loaded:
|
2025-08-30 13:01:50 +03:00
|
|
|
|
_onServerLogLoaded: function() {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.data._onServerLogLoaded()');
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// chain the client log file to load:
|
2025-09-19 15:08:25 +02:00
|
|
|
|
BotMon.live.data.loadLogFile(BM_LOGTYPE.CLIENT, BotMon.live.data._onClientLogLoaded);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// event callback, after the client log has been loaded:
|
2025-08-30 13:01:50 +03:00
|
|
|
|
_onClientLogLoaded: function() {
|
2025-09-03 18:22:29 +02:00
|
|
|
|
//console.info('BotMon.live.data._onClientLogLoaded()');
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// chain the ticks file to load:
|
2025-09-19 15:08:25 +02:00
|
|
|
|
BotMon.live.data.loadLogFile(BM_LOGTYPE.TICKER, BotMon.live.data._onTicksLogLoaded);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// event callback, after the tiker log has been loaded:
|
|
|
|
|
|
_onTicksLogLoaded: function() {
|
2025-09-03 18:22:29 +02:00
|
|
|
|
//console.info('BotMon.live.data._onTicksLogLoaded()');
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// analyse the data:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.data.analytics.analyseAll();
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// sort the data:
|
|
|
|
|
|
// #TODO
|
|
|
|
|
|
|
|
|
|
|
|
// display the data:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.overview.make();
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-09-03 18:22:29 +02:00
|
|
|
|
//console.log(BotMon.live.data.model._visitors);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
model: {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// visitors storage:
|
2025-08-30 13:01:50 +03:00
|
|
|
|
_visitors: [],
|
|
|
|
|
|
|
|
|
|
|
|
// find an already existing visitor record:
|
2025-09-19 15:08:25 +02:00
|
|
|
|
findVisitor: function(visitor, type) {
|
|
|
|
|
|
//console.info('BotMon.live.data.model.findVisitor()', type);
|
2025-09-04 23:01:46 +02:00
|
|
|
|
//console.log(visitor);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const model = BotMon.live.data.model;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
const timeout = 60 * 60 * 1000; // session timeout: One hour
|
2025-09-07 16:11:17 +02:00
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
if (visitor._type == BM_USERTYPE.KNOWN_BOT) { // known bots match by their bot ID:
|
2025-09-04 23:01:46 +02:00
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
for (let i=0; i<model._visitors.length; i++) {
|
|
|
|
|
|
const v = model._visitors[i];
|
2025-09-04 23:01:46 +02:00
|
|
|
|
|
2025-09-07 20:52:12 +02:00
|
|
|
|
// bots match when their ID matches:
|
|
|
|
|
|
if (v._bot && v._bot.id == visitor._bot.id) {
|
|
|
|
|
|
return v;
|
|
|
|
|
|
}
|
2025-09-19 15:08:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
} else { // other types match by their DW/PHPIDs:
|
2025-09-05 12:47:36 +02:00
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
// loop over all visitors already registered and check for ID matches:
|
|
|
|
|
|
for (let i=0; i<model._visitors.length; i++) {
|
|
|
|
|
|
const v = model._visitors[i];
|
2025-09-04 23:01:46 +02:00
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
if ( v.id == visitor.id) { // match the DW/PHP IDs
|
2025-09-07 20:52:12 +02:00
|
|
|
|
return v;
|
|
|
|
|
|
}
|
2025-09-19 15:08:25 +02:00
|
|
|
|
}
|
2025-09-07 16:11:17 +02:00
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
// if not found, try to match IP address and user agent:
|
|
|
|
|
|
for (let i=0; i<model._visitors.length; i++) {
|
|
|
|
|
|
const v = model._visitors[i];
|
|
|
|
|
|
if ( v.ip == visitor.ip && v.agent == visitor.agent) {
|
|
|
|
|
|
return v;
|
|
|
|
|
|
}
|
2025-09-04 23:01:46 +02:00
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
}
|
2025-09-19 15:08:25 +02:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
return null; // nothing found
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-05 12:47:36 +02:00
|
|
|
|
/* if there is already this visit registered, return the page view item */
|
|
|
|
|
|
_getPageView: function(visit, view) {
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
// shortcut to make code more readable:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const model = BotMon.live.data.model;
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
for (let i=0; i<visit._pageViews.length; i++) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
const pv = visit._pageViews[i];
|
2025-09-05 12:47:36 +02:00
|
|
|
|
if (pv.pg == view.pg) {
|
|
|
|
|
|
return pv;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null; // not found
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// register a new visitor (or update if already exists)
|
2025-09-04 23:01:46 +02:00
|
|
|
|
registerVisit: function(nv, type) {
|
|
|
|
|
|
//console.info('registerVisit', nv, type);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const model = BotMon.live.data.model;
|
2025-09-03 23:20:56 +02:00
|
|
|
|
|
2025-09-04 09:01:37 +02:00
|
|
|
|
// is it a known bot?
|
2025-09-04 23:01:46 +02:00
|
|
|
|
const bot = BotMon.live.data.bots.match(nv.agent);
|
2025-09-04 09:01:37 +02:00
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
// enrich new visitor with relevant data:
|
|
|
|
|
|
if (!nv._bot) nv._bot = bot ?? null; // bot info
|
2025-09-12 15:38:28 +02:00
|
|
|
|
nv._type = ( bot ? BM_USERTYPE.KNOWN_BOT : ( nv.usr && nv.usr !== '' ? BM_USERTYPE.KNOWN_USER : BM_USERTYPE.UNKNOWN ) ); // user type
|
|
|
|
|
|
if (bot && bot.geo) {
|
|
|
|
|
|
if (!nv.geo || nv.geo == '' || nv.geo == 'ZZ') nv.geo = bot.geo;
|
|
|
|
|
|
} else if (!nv.geo ||nv.geo == '') {
|
|
|
|
|
|
nv.geo = 'ZZ';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// update first and last seen:
|
|
|
|
|
|
if (!nv._firstSeen) nv._firstSeen = nv.ts; // first-seen
|
|
|
|
|
|
nv._lastSeen = nv.ts; // last-seen
|
2025-09-10 14:44:08 +02:00
|
|
|
|
|
|
|
|
|
|
// country name:
|
|
|
|
|
|
try {
|
2025-09-12 15:38:28 +02:00
|
|
|
|
nv._country = ( nv.geo == 'local' ? "localhost" : "Unknown" );
|
|
|
|
|
|
if (nv.geo && nv.geo !== '' && nv.geo !== 'ZZ' && nv.geo !== 'local') {
|
2025-09-10 14:44:08 +02:00
|
|
|
|
const countryName = new Intl.DisplayNames(['en', BotMon._lang], {type: 'region'});
|
2025-09-13 23:20:43 +02:00
|
|
|
|
nv._country = countryName.of(nv.geo.substring(0,2)) ?? nv.geo;
|
2025-09-10 14:44:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error(err);
|
|
|
|
|
|
nv._country = 'Error';
|
|
|
|
|
|
}
|
2025-09-04 09:01:37 +02:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// check if it already exists:
|
2025-09-19 15:08:25 +02:00
|
|
|
|
let visitor = model.findVisitor(nv, type);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
if (!visitor) {
|
2025-09-04 23:01:46 +02:00
|
|
|
|
visitor = nv;
|
2025-09-03 23:20:56 +02:00
|
|
|
|
visitor._seenBy = [type];
|
2025-08-30 13:01:50 +03:00
|
|
|
|
visitor._pageViews = []; // array of page views
|
|
|
|
|
|
visitor._hasReferrer = false; // has at least one referrer
|
|
|
|
|
|
visitor._jsClient = false; // visitor has been seen logged by client js as well
|
2025-09-04 23:01:46 +02:00
|
|
|
|
visitor._client = BotMon.live.data.clients.match(nv.agent) ?? null; // client info
|
|
|
|
|
|
visitor._platform = BotMon.live.data.platforms.match(nv.agent); // platform info
|
|
|
|
|
|
model._visitors.push(visitor);
|
2025-09-06 20:01:03 +02:00
|
|
|
|
} else { // update existing
|
2025-09-07 16:11:17 +02:00
|
|
|
|
if (visitor._firstSeen > nv.ts) {
|
2025-09-06 20:01:03 +02:00
|
|
|
|
visitor._firstSeen = nv.ts;
|
|
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// find browser
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// is this visit already registered?
|
2025-09-05 12:47:36 +02:00
|
|
|
|
let prereg = model._getPageView(visitor, nv);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
if (!prereg) {
|
2025-09-05 12:47:36 +02:00
|
|
|
|
// add new page view:
|
|
|
|
|
|
prereg = model._makePageView(nv, type);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
visitor._pageViews.push(prereg);
|
2025-09-05 12:47:36 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
// update last seen date
|
|
|
|
|
|
prereg._lastSeen = nv.ts;
|
|
|
|
|
|
// increase view count:
|
|
|
|
|
|
prereg._viewCount += 1;
|
2025-09-07 16:11:17 +02:00
|
|
|
|
prereg._tickCount += 1;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// update referrer state:
|
|
|
|
|
|
visitor._hasReferrer = visitor._hasReferrer ||
|
|
|
|
|
|
(prereg.ref !== undefined && prereg.ref !== '');
|
|
|
|
|
|
|
|
|
|
|
|
// update time stamp for last-seen:
|
2025-09-06 20:01:03 +02:00
|
|
|
|
if (visitor._lastSeen < nv.ts) {
|
|
|
|
|
|
visitor._lastSeen = nv.ts;
|
|
|
|
|
|
}
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// if needed:
|
|
|
|
|
|
return visitor;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// updating visit data from the client-side log:
|
|
|
|
|
|
updateVisit: function(dat) {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
//console.info('updateVisit', dat);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const model = BotMon.live.data.model;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
const type = BM_LOGTYPE.CLIENT;
|
2025-09-03 23:20:56 +02:00
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
let visitor = BotMon.live.data.model.findVisitor(dat, type);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
if (!visitor) {
|
2025-09-03 23:20:56 +02:00
|
|
|
|
visitor = model.registerVisit(dat, type);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
if (visitor) {
|
2025-09-03 23:20:56 +02:00
|
|
|
|
|
2025-09-07 16:11:17 +02:00
|
|
|
|
if (visitor._lastSeen < dat.ts) {
|
|
|
|
|
|
visitor._lastSeen = dat.ts;
|
|
|
|
|
|
}
|
2025-09-05 12:47:36 +02:00
|
|
|
|
if (!visitor._seenBy.includes(type)) {
|
|
|
|
|
|
visitor._seenBy.push(type);
|
|
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
visitor._jsClient = true; // seen by client js
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// find the page view:
|
2025-09-05 12:47:36 +02:00
|
|
|
|
let prereg = BotMon.live.data.model._getPageView(visitor, dat);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
if (prereg) {
|
|
|
|
|
|
// update the page view:
|
|
|
|
|
|
prereg._lastSeen = dat.ts;
|
2025-09-05 12:47:36 +02:00
|
|
|
|
if (!prereg._seenBy.includes(type)) prereg._seenBy.push(type);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
prereg._jsClient = true; // seen by client js
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// add the page view to the visitor:
|
2025-09-05 12:47:36 +02:00
|
|
|
|
prereg = model._makePageView(dat, type);
|
2025-08-30 13:01:50 +03:00
|
|
|
|
visitor._pageViews.push(prereg);
|
|
|
|
|
|
}
|
2025-09-07 16:11:17 +02:00
|
|
|
|
prereg._tickCount += 1;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// updating visit data from the ticker log:
|
|
|
|
|
|
updateTicks: function(dat) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
//console.info('updateTicks', dat);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const model = BotMon.live.data.model;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
const type = BM_LOGTYPE.TICKER;
|
2025-09-05 12:47:36 +02:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// find the visit info:
|
2025-09-19 15:08:25 +02:00
|
|
|
|
let visitor = model.findVisitor(dat, type);
|
2025-08-30 21:11:01 +03:00
|
|
|
|
if (!visitor) {
|
2025-09-06 16:20:58 +02:00
|
|
|
|
console.info(`No visitor with ID “${dat.id}” found, registering as a new one.`);
|
2025-09-05 12:47:36 +02:00
|
|
|
|
visitor = model.registerVisit(dat, type);
|
2025-08-30 21:11:01 +03:00
|
|
|
|
}
|
2025-08-30 18:40:16 +03:00
|
|
|
|
if (visitor) {
|
2025-09-03 23:20:56 +02:00
|
|
|
|
// update visitor:
|
2025-08-30 21:11:01 +03:00
|
|
|
|
if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts;
|
2025-09-05 12:47:36 +02:00
|
|
|
|
if (!visitor._seenBy.includes(type)) visitor._seenBy.push(type);
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
|
|
|
|
|
// get the page view info:
|
2025-09-05 12:47:36 +02:00
|
|
|
|
let pv = model._getPageView(visitor, dat);
|
|
|
|
|
|
if (!pv) {
|
2025-09-14 19:32:48 +02:00
|
|
|
|
console.info(`No page view for visit ID “${dat.id}”, page “${dat.pg}”, registering a new one.`);
|
2025-09-05 12:47:36 +02:00
|
|
|
|
pv = model._makePageView(dat, type);
|
|
|
|
|
|
visitor._pageViews.push(pv);
|
2025-08-30 21:11:01 +03:00
|
|
|
|
}
|
2025-09-05 12:47:36 +02:00
|
|
|
|
|
|
|
|
|
|
// update the page view info:
|
|
|
|
|
|
if (!pv._seenBy.includes(type)) pv._seenBy.push(type);
|
|
|
|
|
|
if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts;
|
|
|
|
|
|
pv._tickCount += 1;
|
|
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
}
|
2025-09-05 12:47:36 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// helper function to create a new "page view" item:
|
|
|
|
|
|
_makePageView: function(data, type) {
|
2025-09-14 19:32:48 +02:00
|
|
|
|
// console.info('_makePageView', data);
|
2025-09-06 16:20:58 +02:00
|
|
|
|
|
|
|
|
|
|
// try to parse the referrer:
|
|
|
|
|
|
let rUrl = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
rUrl = ( data.ref && data.ref !== '' ? new URL(data.ref) : null );
|
|
|
|
|
|
} catch (e) {
|
2025-09-14 19:32:48 +02:00
|
|
|
|
console.warn(`Invalid referer: “${data.ref}”.`);
|
2025-09-19 15:08:25 +02:00
|
|
|
|
console.info(data);
|
2025-09-06 16:20:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 12:47:36 +02:00
|
|
|
|
return {
|
|
|
|
|
|
_by: type,
|
|
|
|
|
|
ip: data.ip,
|
|
|
|
|
|
pg: data.pg,
|
2025-09-14 19:32:48 +02:00
|
|
|
|
lang: data.lang || '??',
|
2025-09-06 16:20:58 +02:00
|
|
|
|
_ref: rUrl,
|
2025-09-05 12:47:36 +02:00
|
|
|
|
_firstSeen: data.ts,
|
|
|
|
|
|
_lastSeen: data.ts,
|
|
|
|
|
|
_seenBy: [type],
|
2025-09-19 15:08:25 +02:00
|
|
|
|
_jsClient: ( type !== BM_LOGTYPE.SERVER),
|
2025-09-05 12:47:36 +02:00
|
|
|
|
_viewCount: 1,
|
|
|
|
|
|
_tickCount: 0
|
|
|
|
|
|
};
|
2025-08-30 13:01:50 +03:00
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
analytics: {
|
|
|
|
|
|
|
|
|
|
|
|
init: function() {
|
2025-09-04 23:01:46 +02:00
|
|
|
|
//console.info('BotMon.live.data.analytics.init()');
|
2025-08-30 21:11:01 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// data storage:
|
|
|
|
|
|
data: {
|
|
|
|
|
|
totalVisits: 0,
|
|
|
|
|
|
totalPageViews: 0,
|
2025-09-10 23:07:51 +02:00
|
|
|
|
humanPageViews: 0,
|
2025-08-30 21:11:01 +03:00
|
|
|
|
bots: {
|
|
|
|
|
|
known: 0,
|
2025-09-03 18:22:29 +02:00
|
|
|
|
suspected: 0,
|
2025-09-01 15:42:06 +02:00
|
|
|
|
human: 0,
|
|
|
|
|
|
users: 0
|
2025-08-30 21:11:01 +03:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// sort the visits by type:
|
|
|
|
|
|
groups: {
|
|
|
|
|
|
knownBots: [],
|
2025-09-03 18:22:29 +02:00
|
|
|
|
suspectedBots: [],
|
2025-08-30 21:11:01 +03:00
|
|
|
|
humans: [],
|
|
|
|
|
|
users: []
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// all analytics
|
|
|
|
|
|
analyseAll: function() {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.data.analytics.analyseAll()');
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const model = BotMon.live.data.model;
|
2025-09-07 16:11:17 +02:00
|
|
|
|
const me = BotMon.live.data.analytics;
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
2025-09-06 20:01:03 +02:00
|
|
|
|
BotMon.live.gui.status.showBusy("Analysing data …");
|
|
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
// loop over all visitors:
|
|
|
|
|
|
model._visitors.forEach( (v) => {
|
|
|
|
|
|
|
|
|
|
|
|
// count visits and page views:
|
|
|
|
|
|
this.data.totalVisits += 1;
|
|
|
|
|
|
this.data.totalPageViews += v._pageViews.length;
|
|
|
|
|
|
|
|
|
|
|
|
// check for typical bot aspects:
|
2025-09-03 23:20:56 +02:00
|
|
|
|
let botScore = 0;
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
if (v._type == BM_USERTYPE.KNOWN_BOT) { // known bots
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
2025-09-05 16:04:40 +02:00
|
|
|
|
this.data.bots.known += v._pageViews.length;
|
2025-08-30 21:11:01 +03:00
|
|
|
|
this.groups.knownBots.push(v);
|
|
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
} else if (v._type == BM_USERTYPE.KNOWN_USER) { // known users */
|
|
|
|
|
|
|
2025-09-05 16:04:40 +02:00
|
|
|
|
this.data.bots.users += v._pageViews.length;
|
2025-08-30 21:11:01 +03:00
|
|
|
|
this.groups.users.push(v);
|
2025-09-05 09:15:08 +02:00
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
} else {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
2025-09-05 16:04:40 +02:00
|
|
|
|
// get evaluation:
|
|
|
|
|
|
const e = BotMon.live.data.rules.evaluate(v);
|
|
|
|
|
|
v._eval = e.rules;
|
|
|
|
|
|
v._botVal = e.val;
|
|
|
|
|
|
|
|
|
|
|
|
if (e.isBot) { // likely bots
|
|
|
|
|
|
v._type = BM_USERTYPE.LIKELY_BOT;
|
|
|
|
|
|
this.data.bots.suspected += v._pageViews.length;
|
|
|
|
|
|
this.groups.suspectedBots.push(v);
|
|
|
|
|
|
} else { // probably humans
|
2025-09-19 15:08:25 +02:00
|
|
|
|
v._type = BM_USERTYPE.PROBABLY_HUMAN;
|
2025-09-05 16:04:40 +02:00
|
|
|
|
this.data.bots.human += v._pageViews.length;
|
|
|
|
|
|
this.groups.humans.push(v);
|
2025-09-10 23:07:51 +02:00
|
|
|
|
}
|
2025-09-04 23:01:46 +02:00
|
|
|
|
}
|
2025-09-10 14:44:08 +02:00
|
|
|
|
|
2025-09-10 23:07:51 +02:00
|
|
|
|
// perform actions depending on the visitor type:
|
|
|
|
|
|
if (v._type == BM_USERTYPE.KNOWN_BOT || v._type == BM_USERTYPE.LIKELY_BOT) { /* bots only */
|
|
|
|
|
|
|
|
|
|
|
|
// add bot views to IP range information:
|
2025-09-13 23:20:43 +02:00
|
|
|
|
/*v._pageViews.forEach( pv => {
|
2025-09-10 23:07:51 +02:00
|
|
|
|
me.addToIPRanges(pv.ip);
|
2025-09-13 23:20:43 +02:00
|
|
|
|
});*/
|
2025-09-10 23:07:51 +02:00
|
|
|
|
|
|
|
|
|
|
// add to the country lists:
|
|
|
|
|
|
me.addToCountries(v.geo, v._country, v._type);
|
|
|
|
|
|
|
|
|
|
|
|
} else { /* humans only */
|
|
|
|
|
|
|
|
|
|
|
|
// add browser and platform statistics:
|
|
|
|
|
|
me.addBrowserPlatform(v);
|
2025-09-15 22:58:08 +02:00
|
|
|
|
|
|
|
|
|
|
// add
|
|
|
|
|
|
v._pageViews.forEach( pv => {
|
|
|
|
|
|
me.addToRefererList(pv._ref);
|
|
|
|
|
|
});
|
2025-09-10 23:07:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-07 20:52:12 +02:00
|
|
|
|
BotMon.live.gui.status.hideBusy('Done.');
|
2025-09-07 16:11:17 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
// get a list of known bots:
|
|
|
|
|
|
getTopBots: function(max) {
|
|
|
|
|
|
//console.info('BotMon.live.data.analytics.getTopBots('+max+')');
|
|
|
|
|
|
|
|
|
|
|
|
//console.log(BotMon.live.data.analytics.groups.knownBots);
|
|
|
|
|
|
|
|
|
|
|
|
let botsList = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => {
|
|
|
|
|
|
return b._pageViews.length - a._pageViews.length;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const other = {
|
|
|
|
|
|
'id': 'other',
|
|
|
|
|
|
'name': "Others",
|
|
|
|
|
|
'count': 0
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const rList = [];
|
|
|
|
|
|
const max2 = ( botsList.length > max ? max-1 : botsList.length );
|
|
|
|
|
|
let total = 0; // adding up the items
|
|
|
|
|
|
for (let i=0; i<botsList.length; i++) {
|
|
|
|
|
|
const it = botsList[i];
|
|
|
|
|
|
if (it && it._bot) {
|
|
|
|
|
|
if (i < max2) {
|
|
|
|
|
|
rList.push({
|
|
|
|
|
|
id: it._bot.id,
|
|
|
|
|
|
name: (it._bot.n ? it._bot.n : it._bot.id),
|
|
|
|
|
|
count: it._pageViews.length
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
other.count += it._pageViews.length;
|
|
|
|
|
|
};
|
|
|
|
|
|
total += it._pageViews.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// add the "other" item, if needed:
|
|
|
|
|
|
if (botsList.length > max2) {
|
|
|
|
|
|
rList.push(other);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
rList.forEach( it => {
|
|
|
|
|
|
it.pct = (it.count * 100 / total);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return rList;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-15 22:58:08 +02:00
|
|
|
|
// Referer List:
|
2025-09-16 23:08:54 +02:00
|
|
|
|
_refererList: [],
|
2025-09-15 22:58:08 +02:00
|
|
|
|
|
|
|
|
|
|
addToRefererList: function(ref) {
|
|
|
|
|
|
//console.log('BotMon.live.data.analytics.addToRefererList',ref);
|
2025-09-07 16:11:17 +02:00
|
|
|
|
|
|
|
|
|
|
const me = BotMon.live.data.analytics;
|
|
|
|
|
|
|
2025-09-15 22:58:08 +02:00
|
|
|
|
// ignore internal references:
|
2025-09-15 23:05:19 +02:00
|
|
|
|
if (ref && ref.host == window.location.host) {
|
2025-09-15 22:58:08 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-07 20:52:12 +02:00
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
const refInfo = me.getRefererInfo(ref);
|
2025-09-15 22:58:08 +02:00
|
|
|
|
|
|
|
|
|
|
// already exists?
|
|
|
|
|
|
let refObj = null;
|
|
|
|
|
|
for (let i = 0; i < me._refererList.length; i++) {
|
2025-09-16 23:08:54 +02:00
|
|
|
|
if (me._refererList[i].id == refInfo.id) {
|
2025-09-15 22:58:08 +02:00
|
|
|
|
refObj = me._refererList[i];
|
2025-09-07 20:52:12 +02:00
|
|
|
|
break;
|
2025-09-07 16:11:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-07 20:52:12 +02:00
|
|
|
|
|
2025-09-15 22:58:08 +02:00
|
|
|
|
// if not exists, create it:
|
|
|
|
|
|
if (!refObj) {
|
2025-09-16 23:08:54 +02:00
|
|
|
|
refObj = refInfo;
|
|
|
|
|
|
refObj.count = 1;
|
2025-09-15 22:58:08 +02:00
|
|
|
|
me._refererList.push(refObj);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
refObj.count += 1;
|
2025-09-07 20:52:12 +02:00
|
|
|
|
}
|
2025-09-16 23:08:54 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getRefererInfo: function(url) {
|
|
|
|
|
|
//console.log('BotMon.live.data.analytics.getRefererInfo',url);
|
2025-09-19 15:08:25 +02:00
|
|
|
|
try {
|
|
|
|
|
|
url = new URL(url);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
'id': 'null',
|
|
|
|
|
|
'n': 'Invalid Referer'
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-09-16 23:08:54 +02:00
|
|
|
|
|
|
|
|
|
|
// find the referer ID:
|
|
|
|
|
|
let refId = 'null';
|
|
|
|
|
|
let refName = 'No Referer';
|
|
|
|
|
|
if (url && url.host) {
|
|
|
|
|
|
const hArr = url.host.split('.');
|
|
|
|
|
|
const tld = hArr[hArr.length-1];
|
2025-09-19 15:08:25 +02:00
|
|
|
|
refId = ( tld == 'localhost' ? tld : hArr[hArr.length-2]);
|
2025-09-16 23:08:54 +02:00
|
|
|
|
refName = hArr[hArr.length-2] + '.' + tld;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
'id': refId,
|
|
|
|
|
|
'n': refName
|
|
|
|
|
|
};
|
2025-09-15 22:58:08 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getTopReferers: function(max) {
|
|
|
|
|
|
//console.info(('BotMon.live.data.analytics.getTopReferers(' + max + ')'));
|
2025-09-10 00:02:42 +02:00
|
|
|
|
|
2025-09-07 16:11:17 +02:00
|
|
|
|
const me = BotMon.live.data.analytics;
|
2025-09-07 20:52:12 +02:00
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
return me._makeTopList(me._refererList, max);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
_makeTopList: function(arr, max) {
|
|
|
|
|
|
//console.info(('BotMon.live.data.analytics._makeTopList(arr,' + max + ')'));
|
|
|
|
|
|
|
|
|
|
|
|
const me = BotMon.live.data.analytics;
|
2025-09-10 00:02:42 +02:00
|
|
|
|
|
2025-09-15 22:58:08 +02:00
|
|
|
|
// sort the list:
|
2025-09-16 23:08:54 +02:00
|
|
|
|
arr.sort( (a,b) => {
|
2025-09-15 22:58:08 +02:00
|
|
|
|
return b.count - a.count;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
const rList = []; // return array
|
|
|
|
|
|
const max2 = ( arr.length >= max ? max-1 : arr.length );
|
|
|
|
|
|
const other = {
|
|
|
|
|
|
'id': 'other',
|
|
|
|
|
|
'name': "Others",
|
|
|
|
|
|
'count': 0
|
|
|
|
|
|
};
|
|
|
|
|
|
let total = 0; // adding up the items
|
|
|
|
|
|
for (let i=0; Math.min(max, arr.length) > i; i++) {
|
|
|
|
|
|
const it = arr[i];
|
|
|
|
|
|
if (it) {
|
|
|
|
|
|
if (i < max2) {
|
|
|
|
|
|
const rIt = {
|
|
|
|
|
|
id: it.id,
|
|
|
|
|
|
name: (it.n ? it.n : it.id),
|
|
|
|
|
|
count: it.count
|
|
|
|
|
|
};
|
|
|
|
|
|
rList.push(rIt);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
other.count += it.count;
|
|
|
|
|
|
}
|
|
|
|
|
|
total += it.count;
|
|
|
|
|
|
}
|
2025-09-10 00:02:42 +02:00
|
|
|
|
}
|
2025-09-16 23:08:54 +02:00
|
|
|
|
|
|
|
|
|
|
// add the "other" item, if needed:
|
|
|
|
|
|
if (arr.length > max2) {
|
|
|
|
|
|
rList.push(other);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
rList.forEach( it => {
|
|
|
|
|
|
it.pct = (it.count * 100 / total);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-10 00:02:42 +02:00
|
|
|
|
return rList;
|
2025-09-15 22:58:08 +02:00
|
|
|
|
},
|
2025-09-10 14:44:08 +02:00
|
|
|
|
|
|
|
|
|
|
/* countries of visits */
|
|
|
|
|
|
_countries: {
|
|
|
|
|
|
'user': [],
|
|
|
|
|
|
'human': [],
|
|
|
|
|
|
'likelyBot': [],
|
|
|
|
|
|
'known_bot': []
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Adds a country code to the statistics.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} iso The ISO 3166-1 alpha-2 country code.
|
|
|
|
|
|
*/
|
2025-09-10 23:07:51 +02:00
|
|
|
|
addToCountries: function(iso, name, type) {
|
2025-09-10 14:44:08 +02:00
|
|
|
|
|
|
|
|
|
|
const me = BotMon.live.data.analytics;
|
|
|
|
|
|
|
|
|
|
|
|
// find the correct array:
|
|
|
|
|
|
let arr = null;
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
|
|
|
|
|
|
case BM_USERTYPE.KNOWN_USER:
|
|
|
|
|
|
arr = me._countries.user;
|
|
|
|
|
|
break;
|
2025-09-19 15:08:25 +02:00
|
|
|
|
case BM_USERTYPE.PROBABLY_HUMAN:
|
2025-09-10 14:44:08 +02:00
|
|
|
|
arr = me._countries.human;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case BM_USERTYPE.LIKELY_BOT:
|
|
|
|
|
|
arr = me._countries.likelyBot;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case BM_USERTYPE.KNOWN_BOT:
|
|
|
|
|
|
arr = me._countries.known_bot;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
2025-09-10 23:07:51 +02:00
|
|
|
|
console.warn(`Unknown user type ${type} in function addToCountries.`);
|
2025-09-10 14:44:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (arr) {
|
2025-09-16 23:08:54 +02:00
|
|
|
|
let cRec = arr.find( it => it.id == iso);
|
2025-09-10 14:44:08 +02:00
|
|
|
|
if (!cRec) {
|
|
|
|
|
|
cRec = {
|
2025-09-16 23:08:54 +02:00
|
|
|
|
'id': iso,
|
|
|
|
|
|
'n': name,
|
2025-09-10 14:44:08 +02:00
|
|
|
|
'count': 1
|
|
|
|
|
|
};
|
|
|
|
|
|
arr.push(cRec);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cRec.count += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Returns a list of countries with visit counts, sorted by visit count in descending order.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {BM_USERTYPE} type The type of visitors to return.
|
|
|
|
|
|
* @param {number} max The maximum number of entries to return.
|
|
|
|
|
|
* @return {Array} A list of objects with properties 'iso' (ISO 3166-1 alpha-2 country code) and 'count' (visit count).
|
|
|
|
|
|
*/
|
|
|
|
|
|
getCountryList: function(type, max) {
|
|
|
|
|
|
|
|
|
|
|
|
const me = BotMon.live.data.analytics;
|
|
|
|
|
|
|
|
|
|
|
|
// find the correct array:
|
|
|
|
|
|
let arr = null;
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
|
|
|
|
|
|
case BM_USERTYPE.KNOWN_USER:
|
|
|
|
|
|
arr = me._countries.user;
|
|
|
|
|
|
break;
|
2025-09-19 15:08:25 +02:00
|
|
|
|
case BM_USERTYPE.PROBABLY_HUMAN:
|
2025-09-10 14:44:08 +02:00
|
|
|
|
arr = me._countries.human;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case BM_USERTYPE.LIKELY_BOT:
|
|
|
|
|
|
arr = me._countries.likelyBot;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case BM_USERTYPE.KNOWN_BOT:
|
|
|
|
|
|
arr = me._countries.known_bot;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
console.warn(`Unknown user type ${type} in function getCountryList.`);
|
2025-09-16 23:08:54 +02:00
|
|
|
|
return;
|
2025-09-10 14:44:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
return me._makeTopList(arr, max);
|
2025-09-10 23:07:51 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* browser and platform of human visitors */
|
|
|
|
|
|
_browsers: [],
|
|
|
|
|
|
_platforms: [],
|
|
|
|
|
|
|
|
|
|
|
|
addBrowserPlatform: function(visitor) {
|
|
|
|
|
|
//console.info('addBrowserPlatform', visitor);
|
|
|
|
|
|
|
|
|
|
|
|
const me = BotMon.live.data.analytics;
|
|
|
|
|
|
|
|
|
|
|
|
// add to browsers list:
|
|
|
|
|
|
let browserRec = ( visitor._client ? visitor._client : {'id': 'unknown'});
|
|
|
|
|
|
if (visitor._client) {
|
|
|
|
|
|
let bRec = me._browsers.find( it => it.id == browserRec.id);
|
|
|
|
|
|
if (!bRec) {
|
|
|
|
|
|
bRec = {
|
2025-09-16 23:08:54 +02:00
|
|
|
|
id: browserRec.id,
|
|
|
|
|
|
n: browserRec.n,
|
|
|
|
|
|
count: 1
|
2025-09-10 23:07:51 +02:00
|
|
|
|
};
|
|
|
|
|
|
me._browsers.push(bRec);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
bRec.count += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// add to platforms list:
|
|
|
|
|
|
let platformRec = ( visitor._platform ? visitor._platform : {'id': 'unknown'});
|
|
|
|
|
|
if (visitor._platform) {
|
|
|
|
|
|
let pRec = me._platforms.find( it => it.id == platformRec.id);
|
|
|
|
|
|
if (!pRec) {
|
|
|
|
|
|
pRec = {
|
2025-09-16 23:08:54 +02:00
|
|
|
|
id: platformRec.id,
|
|
|
|
|
|
n: platformRec.n,
|
|
|
|
|
|
count: 1
|
2025-09-10 23:07:51 +02:00
|
|
|
|
};
|
|
|
|
|
|
me._platforms.push(pRec);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pRec.count += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getTopBrowsers: function(max) {
|
|
|
|
|
|
|
|
|
|
|
|
const me = BotMon.live.data.analytics;
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
return me._makeTopList(me._browsers, max);
|
2025-09-10 23:07:51 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getTopPlatforms: function(max) {
|
|
|
|
|
|
|
|
|
|
|
|
const me = BotMon.live.data.analytics;
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
return me._makeTopList(me._platforms, max);
|
2025-09-07 16:11:17 +02:00
|
|
|
|
}
|
2025-08-30 21:11:01 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-29 23:14:10 +03:00
|
|
|
|
bots: {
|
|
|
|
|
|
// loads the list of known bots from a JSON file:
|
|
|
|
|
|
init: async function() {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.data.bots.init()');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
// Load the list of known bots:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.showBusy("Loading known bots …");
|
2025-09-13 23:20:43 +02:00
|
|
|
|
const url = BotMon._baseDir + 'config/known-bots.json';
|
2025-08-29 23:14:10 +03:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 23:20:56 +02:00
|
|
|
|
this._list = await response.json();
|
|
|
|
|
|
this._ready = true;
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-09-13 23:20:43 +02:00
|
|
|
|
BotMon.live.gui.status.setError("Error while loading the known bots file:", error.message);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
} finally {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.hideBusy("Status: Done.");
|
|
|
|
|
|
BotMon.live.data._dispatch('bots')
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// returns bot info if the clientId matches a known bot, null otherwise:
|
2025-09-03 23:20:56 +02:00
|
|
|
|
match: function(agent) {
|
|
|
|
|
|
//console.info('BotMon.live.data.bots.match(',agent,')');
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-09-03 23:20:56 +02:00
|
|
|
|
const BotList = BotMon.live.data.bots._list;
|
|
|
|
|
|
|
|
|
|
|
|
// default is: not found!
|
|
|
|
|
|
let botInfo = null;
|
|
|
|
|
|
|
2025-09-06 16:27:10 +02:00
|
|
|
|
if (!agent) return null;
|
|
|
|
|
|
|
2025-09-03 23:20:56 +02:00
|
|
|
|
// check for known bots:
|
2025-09-05 12:47:36 +02:00
|
|
|
|
BotList.find(bot => {
|
|
|
|
|
|
let r = false;
|
|
|
|
|
|
for (let j=0; j<bot.rx.length; j++) {
|
|
|
|
|
|
const rxr = agent.match(new RegExp(bot.rx[j]));
|
|
|
|
|
|
if (rxr) {
|
|
|
|
|
|
botInfo = {
|
|
|
|
|
|
n : bot.n,
|
|
|
|
|
|
id: bot.id,
|
2025-09-12 15:38:28 +02:00
|
|
|
|
geo: (bot.geo ? bot.geo : null),
|
2025-09-05 12:47:36 +02:00
|
|
|
|
url: bot.url,
|
|
|
|
|
|
v: (rxr.length > 1 ? rxr[1] : -1)
|
|
|
|
|
|
};
|
|
|
|
|
|
r = true;
|
|
|
|
|
|
break;
|
2025-09-07 16:11:17 +02:00
|
|
|
|
};
|
2025-09-05 12:47:36 +02:00
|
|
|
|
};
|
|
|
|
|
|
return r;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// check for unknown bots:
|
|
|
|
|
|
if (!botInfo) {
|
2025-09-19 15:35:23 +02:00
|
|
|
|
const botmatch = agent.match(/([\s\d\w\-]*bot|[\s\d\w\-]*crawler|[\s\d\w\-]*spider)[\/\s;\),\\.$]/i);
|
2025-09-05 12:47:36 +02:00
|
|
|
|
if(botmatch) {
|
2025-09-10 23:07:51 +02:00
|
|
|
|
botInfo = {'id': ( botmatch[1] || "other_" ), 'n': "Other" + ( botmatch[1] ? " (" + botmatch[1] + ")" : "" ) , "bot": botmatch[1] };
|
2025-09-05 12:47:36 +02:00
|
|
|
|
}
|
2025-08-30 18:40:16 +03:00
|
|
|
|
}
|
2025-09-03 23:20:56 +02:00
|
|
|
|
|
|
|
|
|
|
//console.log("botInfo:", botInfo);
|
|
|
|
|
|
return botInfo;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-03 23:20:56 +02:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// indicates if the list is loaded and ready to use:
|
|
|
|
|
|
_ready: false,
|
|
|
|
|
|
|
|
|
|
|
|
// the actual bot list is stored here:
|
|
|
|
|
|
_list: []
|
|
|
|
|
|
},
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
clients: {
|
|
|
|
|
|
// loads the list of known clients from a JSON file:
|
|
|
|
|
|
init: async function() {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.data.clients.init()');
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// Load the list of known bots:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.showBusy("Loading known clients");
|
2025-09-13 23:20:43 +02:00
|
|
|
|
const url = BotMon._baseDir + 'config/known-clients.json';
|
2025-08-30 18:40:16 +03:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.data.clients._list = await response.json();
|
|
|
|
|
|
BotMon.live.data.clients._ready = true;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
} finally {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.hideBusy("Status: Done.");
|
|
|
|
|
|
BotMon.live.data._dispatch('clients')
|
2025-08-30 18:40:16 +03:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-03 18:22:29 +02:00
|
|
|
|
// returns bot info if the user-agent matches a known bot, null otherwise:
|
2025-09-03 23:20:56 +02:00
|
|
|
|
match: function(agent) {
|
|
|
|
|
|
//console.info('BotMon.live.data.clients.match(',agent,')');
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-09-14 11:58:40 +02:00
|
|
|
|
let match = {"n": "Unknown", "v": -1, "id": 'null'};
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-09-03 23:20:56 +02:00
|
|
|
|
if (agent) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.data.clients._list.find(client => {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
let r = false;
|
|
|
|
|
|
for (let j=0; j<client.rx.length; j++) {
|
2025-09-03 23:20:56 +02:00
|
|
|
|
const rxr = agent.match(new RegExp(client.rx[j]));
|
2025-08-30 18:40:16 +03:00
|
|
|
|
if (rxr) {
|
|
|
|
|
|
match.n = client.n;
|
|
|
|
|
|
match.v = (rxr.length > 1 ? rxr[1] : -1);
|
|
|
|
|
|
match.id = client.id || null;
|
|
|
|
|
|
r = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 23:20:56 +02:00
|
|
|
|
//console.log(match)
|
2025-08-30 18:40:16 +03:00
|
|
|
|
return match;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-10 23:07:51 +02:00
|
|
|
|
// return the browser name for a browser ID:
|
|
|
|
|
|
getName: function(id) {
|
|
|
|
|
|
const it = BotMon.live.data.clients._list.find(client => client.id == id);
|
2025-09-13 23:20:43 +02:00
|
|
|
|
return ( it && it.n ? it.n : "Unknown"); //it.n;
|
2025-09-10 23:07:51 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-30 18:40:16 +03:00
|
|
|
|
// indicates if the list is loaded and ready to use:
|
|
|
|
|
|
_ready: false,
|
|
|
|
|
|
|
|
|
|
|
|
// the actual bot list is stored here:
|
|
|
|
|
|
_list: []
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
platforms: {
|
|
|
|
|
|
// loads the list of known platforms from a JSON file:
|
|
|
|
|
|
init: async function() {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.data.platforms.init()');
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
// Load the list of known bots:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.showBusy("Loading known platforms");
|
2025-09-13 23:20:43 +02:00
|
|
|
|
const url = BotMon._baseDir + 'config/known-platforms.json';
|
2025-08-30 18:40:16 +03:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.data.platforms._list = await response.json();
|
|
|
|
|
|
BotMon.live.data.platforms._ready = true;
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message);
|
2025-08-30 18:40:16 +03:00
|
|
|
|
} finally {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.hideBusy("Status: Done.");
|
|
|
|
|
|
BotMon.live.data._dispatch('platforms')
|
2025-08-30 18:40:16 +03:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// returns bot info if the browser id matches a known platform:
|
|
|
|
|
|
match: function(cid) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
//console.info('BotMon.live.data.platforms.match(',cid,')');
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-09-14 11:58:40 +02:00
|
|
|
|
let match = {"n": "Unknown", "id": 'null'};
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
|
|
|
|
|
if (cid) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.data.platforms._list.find(platform => {
|
2025-08-30 18:40:16 +03:00
|
|
|
|
let r = false;
|
|
|
|
|
|
for (let j=0; j<platform.rx.length; j++) {
|
|
|
|
|
|
const rxr = cid.match(new RegExp(platform.rx[j]));
|
|
|
|
|
|
if (rxr) {
|
|
|
|
|
|
match.n = platform.n;
|
|
|
|
|
|
match.v = (rxr.length > 1 ? rxr[1] : -1);
|
|
|
|
|
|
match.id = platform.id || null;
|
|
|
|
|
|
r = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return match;
|
2025-08-29 23:14:10 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-10 23:07:51 +02:00
|
|
|
|
// return the platform name for a given ID:
|
|
|
|
|
|
getName: function(id) {
|
|
|
|
|
|
const it = BotMon.live.data.platforms._list.find( pf => pf.id == id);
|
|
|
|
|
|
return ( it ? it.n : 'Unknown' );
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-29 23:14:10 +03:00
|
|
|
|
// indicates if the list is loaded and ready to use:
|
|
|
|
|
|
_ready: false,
|
|
|
|
|
|
|
|
|
|
|
|
// the actual bot list is stored here:
|
|
|
|
|
|
_list: []
|
2025-08-30 18:40:16 +03:00
|
|
|
|
|
2025-08-29 23:14:10 +03:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-05 16:04:40 +02:00
|
|
|
|
rules: {
|
|
|
|
|
|
// loads the list of rules and settings from a JSON file:
|
|
|
|
|
|
init: async function() {
|
|
|
|
|
|
//console.info('BotMon.live.data.rules.init()');
|
|
|
|
|
|
|
|
|
|
|
|
// Load the list of known bots:
|
|
|
|
|
|
BotMon.live.gui.status.showBusy("Loading list of rules …");
|
2025-09-12 15:38:28 +02:00
|
|
|
|
|
|
|
|
|
|
// relative file path to the rules file:
|
2025-09-13 23:20:43 +02:00
|
|
|
|
const filePath = 'config/default-config.json';
|
2025-09-12 15:38:28 +02:00
|
|
|
|
|
2025-09-13 23:20:43 +02:00
|
|
|
|
// load the rules file:
|
|
|
|
|
|
this._loadrulesFile(BotMon._baseDir + filePath);
|
2025-09-12 15:38:28 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Loads the list of rules and settings from a JSON file.
|
|
|
|
|
|
* @param {String} url - the URL from which to load the rules file.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
_loadrulesFile: async function(url) {
|
|
|
|
|
|
//console.info('BotMon.live.data.rules._loadrulesFile(',url,')');}
|
|
|
|
|
|
|
|
|
|
|
|
const me = BotMon.live.data.rules;
|
2025-09-05 16:04:40 +02:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const json = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (json.rules) {
|
2025-09-12 15:38:28 +02:00
|
|
|
|
me._rulesList = json.rules;
|
2025-09-05 16:04:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
// override the threshold?
|
|
|
|
|
|
if (json.threshold) me._threshold = json.threshold;
|
2025-09-05 16:04:40 +02:00
|
|
|
|
|
2025-09-06 16:20:58 +02:00
|
|
|
|
if (json.ipRanges) {
|
|
|
|
|
|
// clean up the IPs first:
|
|
|
|
|
|
let list = [];
|
|
|
|
|
|
json.ipRanges.forEach( it => {
|
|
|
|
|
|
let item = {
|
|
|
|
|
|
'from': BotMon.t._ip2Num(it.from),
|
|
|
|
|
|
'to': BotMon.t._ip2Num(it.to),
|
2025-09-08 20:38:07 +02:00
|
|
|
|
'label': it.label
|
2025-09-06 16:20:58 +02:00
|
|
|
|
};
|
|
|
|
|
|
list.push(item);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
me._botIPs = list;
|
2025-09-06 16:20:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
me._ready = true;
|
2025-09-05 16:04:40 +02:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-09-13 23:20:43 +02:00
|
|
|
|
BotMon.live.gui.status.setError("Error while loading the config file: " + error.message);
|
2025-09-05 16:04:40 +02:00
|
|
|
|
} finally {
|
|
|
|
|
|
BotMon.live.gui.status.hideBusy("Status: Done.");
|
|
|
|
|
|
BotMon.live.data._dispatch('rules')
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
_rulesList: [], // list of rules to find out if a visitor is a bot
|
|
|
|
|
|
_threshold: 100, // above this, it is considered a bot.
|
|
|
|
|
|
|
|
|
|
|
|
// returns a descriptive text for a rule id
|
|
|
|
|
|
getRuleInfo: function(ruleId) {
|
|
|
|
|
|
// console.info('getRuleInfo', ruleId);
|
|
|
|
|
|
|
|
|
|
|
|
// shortcut for neater code:
|
|
|
|
|
|
const me = BotMon.live.data.rules;
|
|
|
|
|
|
|
|
|
|
|
|
for (let i=0; i<me._rulesList.length; i++) {
|
|
|
|
|
|
const rule = me._rulesList[i];
|
|
|
|
|
|
if (rule.id == ruleId) {
|
|
|
|
|
|
return rule;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// evaluate a visitor for lkikelihood of being a bot
|
|
|
|
|
|
evaluate: function(visitor) {
|
|
|
|
|
|
|
|
|
|
|
|
// shortcut for neater code:
|
|
|
|
|
|
const me = BotMon.live.data.rules;
|
|
|
|
|
|
|
|
|
|
|
|
let r = { // evaluation result
|
|
|
|
|
|
'val': 0,
|
|
|
|
|
|
'rules': [],
|
|
|
|
|
|
'isBot': false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (let i=0; i<me._rulesList.length; i++) {
|
|
|
|
|
|
const rule = me._rulesList[i];
|
|
|
|
|
|
const params = ( rule.params ? rule.params : [] );
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.func) { // rule is calling a function
|
|
|
|
|
|
if (me.func[rule.func]) {
|
|
|
|
|
|
if(me.func[rule.func](visitor, ...params)) {
|
|
|
|
|
|
r.val += rule.bot;
|
|
|
|
|
|
r.rules.push(rule.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
//console.warn("Unknown rule function: “${rule.func}”. Ignoring rule.")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// is a bot?
|
|
|
|
|
|
r.isBot = (r.val >= me._threshold);
|
|
|
|
|
|
|
|
|
|
|
|
return r;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// list of functions that can be called by the rules list to evaluate a visitor:
|
|
|
|
|
|
func: {
|
|
|
|
|
|
|
2025-09-08 20:38:07 +02:00
|
|
|
|
// check if client is on the list passed as parameter:
|
|
|
|
|
|
matchesClient: function(visitor, ...clients) {
|
2025-09-05 16:04:40 +02:00
|
|
|
|
|
|
|
|
|
|
const clientId = ( visitor._client ? visitor._client.id : '');
|
2025-09-05 23:03:22 +02:00
|
|
|
|
return clients.includes(clientId);
|
2025-09-05 16:04:40 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// check if OS/Platform is one of the obsolete ones:
|
2025-09-08 20:38:07 +02:00
|
|
|
|
matchesPlatform: function(visitor, ...platforms) {
|
2025-09-05 16:04:40 +02:00
|
|
|
|
|
2025-09-05 23:03:22 +02:00
|
|
|
|
const pId = ( visitor._platform ? visitor._platform.id : '');
|
2025-09-14 11:58:40 +02:00
|
|
|
|
|
|
|
|
|
|
if (visitor._platform.id == null) console.log(visitor._platform);
|
|
|
|
|
|
|
2025-09-05 23:03:22 +02:00
|
|
|
|
return platforms.includes(pId);
|
2025-09-05 16:04:40 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// are there at lest num pages loaded?
|
|
|
|
|
|
smallPageCount: function(visitor, num) {
|
|
|
|
|
|
return (visitor._pageViews.length <= Number(num));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-07 16:11:17 +02:00
|
|
|
|
// There was no entry in a specific log file for this visitor:
|
2025-09-05 16:04:40 +02:00
|
|
|
|
// note that this will also trigger the "noJavaScript" rule:
|
2025-09-07 16:11:17 +02:00
|
|
|
|
noRecord: function(visitor, type) {
|
|
|
|
|
|
return !visitor._seenBy.includes(type);
|
2025-09-05 16:04:40 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-06 16:20:58 +02:00
|
|
|
|
// there are no referrers in any of the page visits:
|
|
|
|
|
|
noReferrer: function(visitor) {
|
|
|
|
|
|
|
|
|
|
|
|
let r = false; // return value
|
|
|
|
|
|
for (let i = 0; i < visitor._pageViews.length; i++) {
|
|
|
|
|
|
if (!visitor._pageViews[i]._ref) {
|
|
|
|
|
|
r = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// test for specific client identifiers:
|
2025-09-08 20:38:07 +02:00
|
|
|
|
/*matchesClients: function(visitor, ...list) {
|
2025-09-06 16:20:58 +02:00
|
|
|
|
|
|
|
|
|
|
for (let i=0; i<list.length; i++) {
|
|
|
|
|
|
if (visitor._client.id == list[i]) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
return false;
|
2025-09-08 20:38:07 +02:00
|
|
|
|
},*/
|
2025-09-06 16:20:58 +02:00
|
|
|
|
|
2025-09-06 20:01:03 +02:00
|
|
|
|
// unusual combinations of Platform and Client:
|
2025-09-08 20:38:07 +02:00
|
|
|
|
combinationTest: function(visitor, ...combinations) {
|
2025-09-06 16:20:58 +02:00
|
|
|
|
|
|
|
|
|
|
for (let i=0; i<combinations.length; i++) {
|
|
|
|
|
|
|
|
|
|
|
|
if (visitor._platform.id == combinations[i][0]
|
|
|
|
|
|
&& visitor._client.id == combinations[i][1]) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// is the IP address from a known bot network?
|
|
|
|
|
|
fromKnownBotIP: function(visitor) {
|
|
|
|
|
|
|
|
|
|
|
|
const ipInfo = BotMon.live.data.rules.getBotIPInfo(visitor.ip);
|
|
|
|
|
|
|
2025-09-07 16:11:17 +02:00
|
|
|
|
if (ipInfo) {
|
|
|
|
|
|
visitor._ipInKnownBotRange = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 16:20:58 +02:00
|
|
|
|
return (ipInfo !== null);
|
2025-09-06 20:01:03 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// is the page language mentioned in the client's accepted languages?
|
|
|
|
|
|
// the parameter holds an array of exceptions, i.e. page languages that should be ignored.
|
|
|
|
|
|
matchLang: function(visitor, ...exceptions) {
|
|
|
|
|
|
|
2025-09-08 21:00:47 +02:00
|
|
|
|
if (visitor.lang && visitor.accept && exceptions.indexOf(visitor.lang) < 0) {
|
|
|
|
|
|
return (visitor.accept.split(',').indexOf(visitor.lang) < 0);
|
2025-09-06 20:01:03 +02:00
|
|
|
|
}
|
|
|
|
|
|
return false;
|
2025-09-07 16:11:17 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-14 19:32:48 +02:00
|
|
|
|
// the "Accept language" header contains certain entries:
|
|
|
|
|
|
clientAccepts: function(visitor, ...languages) {
|
|
|
|
|
|
//console.info('clientAccepts', visitor.accept, languages);
|
|
|
|
|
|
|
|
|
|
|
|
if (visitor.accept && languages) {;
|
|
|
|
|
|
return ( visitor.accept.split(',').filter(lang => languages.includes(lang)).length > 0 );
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-09 09:36:13 +02:00
|
|
|
|
// Is there an accept-language field defined at all?
|
|
|
|
|
|
noAcceptLang: function(visitor) {
|
|
|
|
|
|
|
|
|
|
|
|
if (!visitor.accept || visitor.accept.length <= 0) { // no accept-languages header
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// TODO: parametrize this!
|
|
|
|
|
|
return false;
|
|
|
|
|
|
},
|
2025-09-07 16:11:17 +02:00
|
|
|
|
// At least x page views were recorded, but they come within less than y seconds
|
|
|
|
|
|
loadSpeed: function(visitor, minItems, maxTime) {
|
|
|
|
|
|
|
|
|
|
|
|
if (visitor._pageViews.length >= minItems) {
|
|
|
|
|
|
//console.log('loadSpeed', visitor._pageViews.length, minItems, maxTime);
|
|
|
|
|
|
|
|
|
|
|
|
const pvArr = visitor._pageViews.map(pv => pv._lastSeen).sort();
|
|
|
|
|
|
|
|
|
|
|
|
let totalTime = 0;
|
|
|
|
|
|
for (let i=1; i < pvArr.length; i++) {
|
|
|
|
|
|
totalTime += (pvArr[i] - pvArr[i-1]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//console.log(' ', totalTime , Math.round(totalTime / (pvArr.length * 1000)), (( totalTime / pvArr.length ) <= maxTime * 1000), visitor.ip);
|
|
|
|
|
|
|
|
|
|
|
|
return (( totalTime / pvArr.length ) <= maxTime * 1000);
|
|
|
|
|
|
}
|
2025-09-10 14:44:08 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// Country code matches one of those in the list:
|
|
|
|
|
|
matchesCountry: function(visitor, ...countries) {
|
|
|
|
|
|
|
|
|
|
|
|
// ingore if geoloc is not set or unknown:
|
2025-09-12 15:38:28 +02:00
|
|
|
|
if (visitor.geo) {
|
2025-09-10 14:44:08 +02:00
|
|
|
|
return (countries.indexOf(visitor.geo) >= 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// Country does not match one of the given codes.
|
|
|
|
|
|
notFromCountry: function(visitor, ...countries) {
|
|
|
|
|
|
|
|
|
|
|
|
// ingore if geoloc is not set or unknown:
|
|
|
|
|
|
if (visitor.geo && visitor.geo !== 'ZZ') {
|
|
|
|
|
|
return (countries.indexOf(visitor.geo) < 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
2025-09-05 16:04:40 +02:00
|
|
|
|
}
|
2025-09-06 16:20:58 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/* known bot IP ranges: */
|
|
|
|
|
|
_botIPs: [],
|
|
|
|
|
|
|
|
|
|
|
|
// return information on a bot IP range:
|
|
|
|
|
|
getBotIPInfo: function(ip) {
|
|
|
|
|
|
|
|
|
|
|
|
// shortcut to make code more readable:
|
|
|
|
|
|
const me = BotMon.live.data.rules;
|
|
|
|
|
|
|
|
|
|
|
|
// convert IP address to easier comparable form:
|
|
|
|
|
|
const ipNum = BotMon.t._ip2Num(ip);
|
|
|
|
|
|
|
|
|
|
|
|
for (let i=0; i < me._botIPs.length; i++) {
|
|
|
|
|
|
const ipRange = me._botIPs[i];
|
|
|
|
|
|
|
|
|
|
|
|
if (ipNum >= ipRange.from && ipNum <= ipRange.to) {
|
|
|
|
|
|
return ipRange;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
2025-09-05 16:04:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Loads a log file (server, page load, or ticker) and parses it.
|
|
|
|
|
|
* @param {String} type - the type of the log file to load (srv, log, or tck)
|
|
|
|
|
|
* @param {Function} [onLoaded] - an optional callback function to call after loading is finished.
|
|
|
|
|
|
*/
|
2025-08-30 13:01:50 +03:00
|
|
|
|
loadLogFile: async function(type, onLoaded = undefined) {
|
2025-09-05 23:03:22 +02:00
|
|
|
|
//console.info('BotMon.live.data.loadLogFile(',type,')');
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
let typeName = '';
|
|
|
|
|
|
let columns = [];
|
|
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case "srv":
|
|
|
|
|
|
typeName = "Server";
|
2025-09-10 14:44:08 +02:00
|
|
|
|
columns = ['ts','ip','pg','id','typ','usr','agent','ref','lang','accept','geo'];
|
2025-08-29 23:14:10 +03:00
|
|
|
|
break;
|
|
|
|
|
|
case "log":
|
|
|
|
|
|
typeName = "Page load";
|
2025-09-03 18:22:29 +02:00
|
|
|
|
columns = ['ts','ip','pg','id','usr','lt','ref','agent'];
|
2025-08-29 23:14:10 +03:00
|
|
|
|
break;
|
|
|
|
|
|
case "tck":
|
|
|
|
|
|
typeName = "Ticker";
|
2025-09-03 18:22:29 +02:00
|
|
|
|
columns = ['ts','ip','pg','id','agent'];
|
2025-08-29 23:14:10 +03:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
console.warn(`Unknown log type ${type}.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// Show the busy indicator and set the visible status:
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// compose the URL from which to load:
|
2025-09-17 22:29:47 +02:00
|
|
|
|
const url = BotMon._baseDir + `logs/${BotMon._datestr}.${type}.txt`;
|
2025-08-30 13:01:50 +03:00
|
|
|
|
//console.log("Loading:",url);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-08-30 13:01:50 +03:00
|
|
|
|
// fetch the data:
|
2025-08-29 23:14:10 +03:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
if (!response.ok) {
|
2025-09-12 15:38:28 +02:00
|
|
|
|
|
2025-08-29 23:14:10 +03:00
|
|
|
|
throw new Error(`${response.status} ${response.statusText}`);
|
|
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
|
|
// parse the data:
|
|
|
|
|
|
const logtxt = await response.text();
|
|
|
|
|
|
if (logtxt.length <= 0) {
|
|
|
|
|
|
throw new Error(`Empty log file ${url}.`);
|
|
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
logtxt.split('\n').forEach((line) => {
|
|
|
|
|
|
if (line.trim() === '') return; // skip empty lines
|
|
|
|
|
|
const cols = line.split('\t');
|
2025-08-30 13:01:50 +03:00
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
// assign the columns to an object:
|
|
|
|
|
|
const data = {};
|
|
|
|
|
|
cols.forEach( (colVal,i) => {
|
|
|
|
|
|
colName = columns[i] || `col${i}`;
|
|
|
|
|
|
const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim());
|
|
|
|
|
|
data[colName] = colValue;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// register the visit in the model:
|
|
|
|
|
|
switch(type) {
|
2025-09-19 15:08:25 +02:00
|
|
|
|
case BM_LOGTYPE.SERVER:
|
2025-09-12 15:38:28 +02:00
|
|
|
|
BotMon.live.data.model.registerVisit(data, type);
|
|
|
|
|
|
break;
|
2025-09-19 15:08:25 +02:00
|
|
|
|
case BM_LOGTYPE.CLIENT:
|
2025-09-12 15:38:28 +02:00
|
|
|
|
data.typ = 'js';
|
|
|
|
|
|
BotMon.live.data.model.updateVisit(data);
|
|
|
|
|
|
break;
|
2025-09-19 15:08:25 +02:00
|
|
|
|
case BM_LOGTYPE.TICKER:
|
2025-09-12 15:38:28 +02:00
|
|
|
|
data.typ = 'js';
|
|
|
|
|
|
BotMon.live.data.model.updateTicks(data);
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
console.warn(`Unknown log type ${type}.`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-08-30 13:01:50 +03:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-09-13 23:20:43 +02:00
|
|
|
|
BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message} – data may be incomplete.`);
|
2025-08-29 23:14:10 +03:00
|
|
|
|
} finally {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status.hideBusy("Status: Done.");
|
2025-09-12 15:38:28 +02:00
|
|
|
|
if (onLoaded) {
|
|
|
|
|
|
onLoaded(); // callback after loading is finished.
|
|
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
gui: {
|
2025-09-03 18:22:29 +02:00
|
|
|
|
init: function() {
|
|
|
|
|
|
// init the lists view:
|
|
|
|
|
|
this.lists.init();
|
|
|
|
|
|
},
|
2025-08-30 21:11:01 +03:00
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
/* The Overview / web metrics section of the live tab */
|
2025-08-30 21:11:01 +03:00
|
|
|
|
overview: {
|
2025-09-12 15:38:28 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Populates the overview part of the today tab with the analytics data.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @method make
|
|
|
|
|
|
* @memberof BotMon.live.gui.overview
|
|
|
|
|
|
*/
|
2025-08-30 21:11:01 +03:00
|
|
|
|
make: function() {
|
2025-09-04 23:01:46 +02:00
|
|
|
|
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const data = BotMon.live.data.analytics.data;
|
2025-09-04 23:01:46 +02:00
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
const maxItemsPerList = 5; // how many list items to show?
|
|
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
// shortcut for neater code:
|
|
|
|
|
|
const makeElement = BotMon.t._makeElement;
|
|
|
|
|
|
|
2025-09-10 00:02:42 +02:00
|
|
|
|
const botsVsHumans = document.getElementById('botmon__today__botsvshumans');
|
|
|
|
|
|
if (botsVsHumans) {
|
2025-09-16 23:08:54 +02:00
|
|
|
|
botsVsHumans.appendChild(makeElement('dt', {}, "Page views by category:"));
|
2025-09-01 18:55:56 +02:00
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
for (let i = 0; i <= 4; i++) {
|
2025-09-10 00:02:42 +02:00
|
|
|
|
const dd = makeElement('dd');
|
|
|
|
|
|
let title = '';
|
|
|
|
|
|
let value = '';
|
|
|
|
|
|
switch(i) {
|
|
|
|
|
|
case 0:
|
|
|
|
|
|
title = "Registered users:";
|
|
|
|
|
|
value = data.bots.users;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
title = "Probably humans:";
|
|
|
|
|
|
value = data.bots.human;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
title = "Suspected bots:";
|
|
|
|
|
|
value = data.bots.suspected;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 3:
|
|
|
|
|
|
title = "Known bots:";
|
|
|
|
|
|
value = data.bots.known;
|
|
|
|
|
|
break;
|
2025-09-16 23:08:54 +02:00
|
|
|
|
case 4:
|
|
|
|
|
|
title = "Total:";
|
|
|
|
|
|
value = data.totalPageViews;
|
|
|
|
|
|
break;
|
2025-09-10 00:02:42 +02:00
|
|
|
|
default:
|
|
|
|
|
|
console.warn(`Unknown list type ${i}.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
dd.appendChild(makeElement('span', {}, title));
|
|
|
|
|
|
dd.appendChild(makeElement('strong', {}, value));
|
|
|
|
|
|
botsVsHumans.appendChild(dd);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-01 18:55:56 +02:00
|
|
|
|
|
2025-09-10 00:02:42 +02:00
|
|
|
|
// update known bots list:
|
2025-09-16 23:08:54 +02:00
|
|
|
|
const botElement = document.getElementById('botmon__botslist'); /* Known bots */
|
|
|
|
|
|
if (botElement) {
|
2025-09-19 15:08:25 +02:00
|
|
|
|
botElement.innerHTML = `<dt>Top visiting bots:</dt>`;
|
2025-09-16 23:08:54 +02:00
|
|
|
|
|
|
|
|
|
|
let botList = BotMon.live.data.analytics.getTopBots(maxItemsPerList);
|
|
|
|
|
|
botList.forEach( (botInfo) => {
|
|
|
|
|
|
const bli = makeElement('dd');
|
|
|
|
|
|
bli.appendChild(makeElement('span', {'class': 'has_icon bot bot_' + botInfo.id }, botInfo.name));
|
|
|
|
|
|
bli.appendChild(makeElement('span', {'class': 'count' }, botInfo.count));
|
|
|
|
|
|
botElement.append(bli)
|
|
|
|
|
|
});
|
2025-09-10 00:02:42 +02:00
|
|
|
|
}
|
2025-09-04 23:01:46 +02:00
|
|
|
|
|
2025-09-10 00:02:42 +02:00
|
|
|
|
// update the suspected bot IP ranges list:
|
2025-09-13 23:20:43 +02:00
|
|
|
|
/*const botIps = document.getElementById('botmon__today__botips');
|
2025-09-10 00:02:42 +02:00
|
|
|
|
if (botIps) {
|
2025-09-12 15:38:28 +02:00
|
|
|
|
botIps.appendChild(makeElement('dt', {}, "Bot IP ranges (top 5)"));
|
2025-09-10 00:02:42 +02:00
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
const ipList = BotMon.live.data.analytics.getTopBotIPRanges(5);
|
2025-09-10 00:02:42 +02:00
|
|
|
|
ipList.forEach( (ipInfo) => {
|
|
|
|
|
|
const li = makeElement('dd');
|
2025-09-12 15:38:28 +02:00
|
|
|
|
li.appendChild(makeElement('span', {'class': 'has_icon ipaddr ip' + ipInfo.typ }, ipInfo.ip));
|
2025-09-10 00:02:42 +02:00
|
|
|
|
li.appendChild(makeElement('span', {'class': 'count' }, ipInfo.num));
|
|
|
|
|
|
botIps.append(li)
|
2025-09-10 14:44:08 +02:00
|
|
|
|
});
|
2025-09-13 23:20:43 +02:00
|
|
|
|
}*/
|
2025-09-10 14:44:08 +02:00
|
|
|
|
|
|
|
|
|
|
// update the top bot countries list:
|
|
|
|
|
|
const botCountries = document.getElementById('botmon__today__countries');
|
|
|
|
|
|
if (botCountries) {
|
2025-09-19 15:08:25 +02:00
|
|
|
|
botCountries.appendChild(makeElement('dt', {}, `Top bot Countries:`));
|
2025-09-12 15:38:28 +02:00
|
|
|
|
const countryList = BotMon.live.data.analytics.getCountryList('likely_bot', 5);
|
2025-09-10 14:44:08 +02:00
|
|
|
|
countryList.forEach( (cInfo) => {
|
|
|
|
|
|
const cLi = makeElement('dd');
|
2025-09-16 23:08:54 +02:00
|
|
|
|
cLi.appendChild(makeElement('span', {'class': 'has_icon country ctry_' + cInfo.id.toLowerCase() }, cInfo.name));
|
2025-09-10 14:44:08 +02:00
|
|
|
|
cLi.appendChild(makeElement('span', {'class': 'count' }, cInfo.count));
|
|
|
|
|
|
botCountries.appendChild(cLi);
|
|
|
|
|
|
});
|
2025-09-10 00:02:42 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// update the webmetrics overview:
|
|
|
|
|
|
const wmoverview = document.getElementById('botmon__today__wm_overview');
|
|
|
|
|
|
if (wmoverview) {
|
|
|
|
|
|
const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 100);
|
|
|
|
|
|
|
2025-09-19 15:08:25 +02:00
|
|
|
|
wmoverview.appendChild(makeElement('dt', {}, "Visitor overview"));
|
2025-09-10 00:02:42 +02:00
|
|
|
|
for (let i = 0; i < 3; i++) {
|
2025-09-04 23:01:46 +02:00
|
|
|
|
const dd = makeElement('dd');
|
2025-09-10 00:02:42 +02:00
|
|
|
|
let title = '';
|
|
|
|
|
|
let value = '';
|
|
|
|
|
|
switch(i) {
|
|
|
|
|
|
case 0:
|
|
|
|
|
|
title = "Total page views:";
|
|
|
|
|
|
value = data.totalPageViews;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
title = "Total visitors (est.):";
|
|
|
|
|
|
value = data.totalVisits;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
title = "Bounce rate (est.):";
|
|
|
|
|
|
value = bounceRate + '%';
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
console.warn(`Unknown list type ${i}.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
dd.appendChild(makeElement('span', {}, title));
|
|
|
|
|
|
dd.appendChild(makeElement('strong', {}, value));
|
|
|
|
|
|
wmoverview.appendChild(dd);
|
2025-09-04 23:01:46 +02:00
|
|
|
|
}
|
2025-08-30 21:11:01 +03:00
|
|
|
|
}
|
2025-09-10 00:02:42 +02:00
|
|
|
|
|
2025-09-10 23:07:51 +02:00
|
|
|
|
// update the webmetrics clients list:
|
|
|
|
|
|
const wmclients = document.getElementById('botmon__today__wm_clients');
|
|
|
|
|
|
if (wmclients) {
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
wmclients.appendChild(makeElement('dt', {}, "Browsers"));
|
2025-09-10 23:07:51 +02:00
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
const clientList = BotMon.live.data.analytics.getTopBrowsers(maxItemsPerList);
|
2025-09-10 23:07:51 +02:00
|
|
|
|
if (clientList) {
|
|
|
|
|
|
clientList.forEach( (cInfo) => {
|
|
|
|
|
|
const cDd = makeElement('dd');
|
2025-09-12 15:38:28 +02:00
|
|
|
|
cDd.appendChild(makeElement('span', {'class': 'has_icon client cl_' + cInfo.id }, ( cInfo.name ? cInfo.name : cInfo.id)));
|
2025-09-10 23:07:51 +02:00
|
|
|
|
cDd.appendChild(makeElement('span', {
|
|
|
|
|
|
'class': 'count',
|
|
|
|
|
|
'title': cInfo.count + " page views"
|
2025-09-16 23:08:54 +02:00
|
|
|
|
}, cInfo.pct.toFixed(1) + '%'));
|
2025-09-10 23:07:51 +02:00
|
|
|
|
wmclients.appendChild(cDd);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// update the webmetrics platforms list:
|
|
|
|
|
|
const wmplatforms = document.getElementById('botmon__today__wm_platforms');
|
|
|
|
|
|
if (wmplatforms) {
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
wmplatforms.appendChild(makeElement('dt', {}, "Platforms"));
|
2025-09-10 23:07:51 +02:00
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
const pfList = BotMon.live.data.analytics.getTopPlatforms(maxItemsPerList);
|
2025-09-10 23:07:51 +02:00
|
|
|
|
if (pfList) {
|
|
|
|
|
|
pfList.forEach( (pInfo) => {
|
|
|
|
|
|
const pDd = makeElement('dd');
|
2025-09-12 15:38:28 +02:00
|
|
|
|
pDd.appendChild(makeElement('span', {'class': 'has_icon platform pf_' + pInfo.id }, ( pInfo.name ? pInfo.name : pInfo.id)));
|
2025-09-10 23:07:51 +02:00
|
|
|
|
pDd.appendChild(makeElement('span', {
|
|
|
|
|
|
'class': 'count',
|
|
|
|
|
|
'title': pInfo.count + " page views"
|
2025-09-16 23:08:54 +02:00
|
|
|
|
}, pInfo.pct.toFixed(1) + '%'));
|
2025-09-10 23:07:51 +02:00
|
|
|
|
wmplatforms.appendChild(pDd);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-15 22:58:08 +02:00
|
|
|
|
// update the top referrers;
|
|
|
|
|
|
const wmreferers = document.getElementById('botmon__today__wm_referers');
|
|
|
|
|
|
if (wmreferers) {
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
wmreferers.appendChild(makeElement('dt', {}, "Referers"));
|
2025-09-15 22:58:08 +02:00
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
const refList = BotMon.live.data.analytics.getTopReferers(maxItemsPerList);
|
2025-09-15 22:58:08 +02:00
|
|
|
|
if (refList) {
|
|
|
|
|
|
refList.forEach( (rInfo) => {
|
|
|
|
|
|
const rDd = makeElement('dd');
|
2025-09-16 23:08:54 +02:00
|
|
|
|
rDd.appendChild(makeElement('span', {'class': 'has_icon referer ref_' + rInfo.id }, rInfo.name));
|
2025-09-15 22:58:08 +02:00
|
|
|
|
rDd.appendChild(makeElement('span', {
|
|
|
|
|
|
'class': 'count',
|
|
|
|
|
|
'title': rInfo.count + " references"
|
2025-09-16 23:08:54 +02:00
|
|
|
|
}, rInfo.pct.toFixed(1) + '%'));
|
2025-09-15 22:58:08 +02:00
|
|
|
|
wmreferers.appendChild(rDd);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-09-04 23:01:46 +02:00
|
|
|
|
|
2025-08-30 21:11:01 +03:00
|
|
|
|
status: {
|
|
|
|
|
|
setText: function(txt) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const el = document.getElementById('botmon__today__status');
|
|
|
|
|
|
if (el && BotMon.live.gui.status._errorCount <= 0) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
el.innerText = txt;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setTitle: function(html) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const el = document.getElementById('botmon__today__title');
|
2025-08-30 21:11:01 +03:00
|
|
|
|
if (el) {
|
|
|
|
|
|
el.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setError: function(txt) {
|
|
|
|
|
|
console.error(txt);
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status._errorCount += 1;
|
|
|
|
|
|
const el = document.getElementById('botmon__today__status');
|
2025-08-30 21:11:01 +03:00
|
|
|
|
if (el) {
|
2025-09-13 23:20:43 +02:00
|
|
|
|
el.innerText = "Data may be incomplete.";
|
2025-08-30 21:11:01 +03:00
|
|
|
|
el.classList.add('error');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
_errorCount: 0,
|
|
|
|
|
|
|
|
|
|
|
|
showBusy: function(txt = null) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
BotMon.live.gui.status._busyCount += 1;
|
|
|
|
|
|
const el = document.getElementById('botmon__today__busy');
|
2025-08-30 21:11:01 +03:00
|
|
|
|
if (el) {
|
|
|
|
|
|
el.style.display = 'inline-block';
|
|
|
|
|
|
}
|
2025-09-01 16:25:25 +02:00
|
|
|
|
if (txt) BotMon.live.gui.status.setText(txt);
|
2025-08-30 21:11:01 +03:00
|
|
|
|
},
|
|
|
|
|
|
_busyCount: 0,
|
|
|
|
|
|
|
|
|
|
|
|
hideBusy: function(txt = null) {
|
2025-09-01 16:25:25 +02:00
|
|
|
|
const el = document.getElementById('botmon__today__busy');
|
|
|
|
|
|
BotMon.live.gui.status._busyCount -= 1;
|
|
|
|
|
|
if (BotMon.live.gui.status._busyCount <= 0) {
|
2025-08-30 21:11:01 +03:00
|
|
|
|
if (el) el.style.display = 'none';
|
2025-09-01 16:25:25 +02:00
|
|
|
|
if (txt) BotMon.live.gui.status.setText(txt);
|
2025-08-30 21:11:01 +03:00
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
2025-09-03 18:22:29 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
lists: {
|
|
|
|
|
|
init: function() {
|
|
|
|
|
|
|
2025-09-05 23:03:22 +02:00
|
|
|
|
// function shortcut:
|
|
|
|
|
|
const makeElement = BotMon.t._makeElement;
|
|
|
|
|
|
|
2025-09-03 18:22:29 +02:00
|
|
|
|
const parent = document.getElementById('botmon__today__visitorlists');
|
|
|
|
|
|
if (parent) {
|
|
|
|
|
|
|
|
|
|
|
|
for (let i=0; i < 4; i++) {
|
|
|
|
|
|
|
|
|
|
|
|
// change the id and title by number:
|
|
|
|
|
|
let listTitle = '';
|
|
|
|
|
|
let listId = '';
|
2025-09-20 11:54:38 +02:00
|
|
|
|
let infolink = null;
|
2025-09-03 18:22:29 +02:00
|
|
|
|
switch (i) {
|
|
|
|
|
|
case 0:
|
|
|
|
|
|
listTitle = "Registered users";
|
|
|
|
|
|
listId = 'users';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
listTitle = "Probably humans";
|
|
|
|
|
|
listId = 'humans';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
listTitle = "Suspected bots";
|
|
|
|
|
|
listId = 'suspectedBots';
|
2025-09-20 11:54:38 +02:00
|
|
|
|
infolink = 'https://leib.be/sascha/projects/dokuwiki/botmon/info/suspected_bots';
|
2025-09-03 18:22:29 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case 3:
|
|
|
|
|
|
listTitle = "Known bots";
|
|
|
|
|
|
listId = 'knownBots';
|
2025-09-20 11:54:38 +02:00
|
|
|
|
infolink = 'https://leib.be/sascha/projects/dokuwiki/botmon/info/known_bots';
|
2025-09-03 18:22:29 +02:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
2025-09-05 23:11:37 +02:00
|
|
|
|
console.warn('Unknown list number.');
|
2025-09-03 18:22:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 23:03:22 +02:00
|
|
|
|
const details = makeElement('details', {
|
2025-09-03 18:22:29 +02:00
|
|
|
|
'data-group': listId,
|
|
|
|
|
|
'data-loaded': false
|
|
|
|
|
|
});
|
2025-09-05 23:03:22 +02:00
|
|
|
|
const title = details.appendChild(makeElement('summary'));
|
2025-09-12 15:38:28 +02:00
|
|
|
|
title.appendChild(makeElement('span', {'class': 'title'}, listTitle));
|
2025-09-20 11:54:38 +02:00
|
|
|
|
if (infolink) {
|
|
|
|
|
|
title.appendChild(makeElement('a', {
|
2025-09-20 11:58:58 +02:00
|
|
|
|
'class': 'ext_info',
|
2025-09-20 11:54:38 +02:00
|
|
|
|
'target': '_blank',
|
|
|
|
|
|
'href': infolink,
|
|
|
|
|
|
'title': "More information"
|
|
|
|
|
|
}, "Info"));
|
|
|
|
|
|
}
|
2025-09-03 18:22:29 +02:00
|
|
|
|
details.addEventListener("toggle", this._onDetailsToggle);
|
|
|
|
|
|
|
|
|
|
|
|
parent.appendChild(details);
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
_onDetailsToggle: function(e) {
|
2025-09-04 23:01:46 +02:00
|
|
|
|
//console.info('BotMon.live.gui.lists._onDetailsToggle()');
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
|
|
|
|
|
const target = e.target;
|
|
|
|
|
|
|
|
|
|
|
|
if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet
|
|
|
|
|
|
target.setAttribute('data-loaded', 'loading');
|
|
|
|
|
|
|
|
|
|
|
|
const fillType = target.getAttribute('data-group');
|
|
|
|
|
|
const fillList = BotMon.live.data.analytics.groups[fillType];
|
|
|
|
|
|
if (fillList && fillList.length > 0) {
|
|
|
|
|
|
|
|
|
|
|
|
const ul = BotMon.t._makeElement('ul');
|
|
|
|
|
|
|
|
|
|
|
|
fillList.forEach( (it) => {
|
|
|
|
|
|
ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
target.appendChild(ul);
|
|
|
|
|
|
target.setAttribute('data-loaded', 'true');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
target.setAttribute('data-loaded', 'false');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
_makeVisitorItem: function(data, type) {
|
|
|
|
|
|
|
|
|
|
|
|
// shortcut for neater code:
|
|
|
|
|
|
const make = BotMon.t._makeElement;
|
|
|
|
|
|
|
2025-09-03 23:20:56 +02:00
|
|
|
|
let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' );
|
2025-09-12 15:38:28 +02:00
|
|
|
|
if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
|
|
|
|
|
|
|
2025-09-06 16:20:58 +02:00
|
|
|
|
const platformName = (data._platform ? data._platform.n : 'Unknown');
|
|
|
|
|
|
const clientName = (data._client ? data._client.n: 'Unknown');
|
2025-09-03 23:20:56 +02:00
|
|
|
|
|
2025-09-19 15:35:23 +02:00
|
|
|
|
const sumClass = ( !data._seenBy || data._seenBy.indexOf(BM_LOGTYPE.SERVER) < 0 ? 'noServer' : 'hasServer');
|
2025-09-13 23:20:43 +02:00
|
|
|
|
|
2025-09-03 18:22:29 +02:00
|
|
|
|
const li = make('li'); // root list item
|
|
|
|
|
|
const details = make('details');
|
2025-09-13 23:20:43 +02:00
|
|
|
|
const summary = make('summary', {
|
|
|
|
|
|
'class': sumClass
|
|
|
|
|
|
});
|
2025-09-03 18:22:29 +02:00
|
|
|
|
details.appendChild(summary);
|
|
|
|
|
|
|
|
|
|
|
|
const span1 = make('span'); /* left-hand group */
|
2025-09-10 14:44:08 +02:00
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* No platform/client for bots */
|
|
|
|
|
|
span1.appendChild(make('span', { /* Platform */
|
|
|
|
|
|
'class': 'icon_only platform pf_' + (data._platform ? data._platform.id : 'unknown'),
|
|
|
|
|
|
'title': "Platform: " + platformName
|
|
|
|
|
|
}, platformName));
|
|
|
|
|
|
|
|
|
|
|
|
span1.appendChild(make('span', { /* Client */
|
|
|
|
|
|
'class': 'icon_only client client cl_' + (data._client ? data._client.id : 'unknown'),
|
|
|
|
|
|
'title': "Client: " + clientName
|
|
|
|
|
|
}, clientName));
|
2025-09-10 14:44:08 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// identifier:
|
2025-09-04 23:01:46 +02:00
|
|
|
|
if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
2025-09-05 12:47:36 +02:00
|
|
|
|
const botName = ( data._bot && data._bot.n ? data._bot.n : "Unknown");
|
2025-09-03 23:20:56 +02:00
|
|
|
|
span1.appendChild(make('span', { /* Bot */
|
2025-09-12 15:38:28 +02:00
|
|
|
|
'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown'),
|
2025-09-05 12:47:36 +02:00
|
|
|
|
'title': "Bot: " + botName
|
|
|
|
|
|
}, botName));
|
2025-09-03 23:20:56 +02:00
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
} else if (data._type == BM_USERTYPE.KNOWN_USER) { /* User only */
|
2025-09-03 23:20:56 +02:00
|
|
|
|
|
|
|
|
|
|
span1.appendChild(make('span', { /* User */
|
2025-09-12 15:38:28 +02:00
|
|
|
|
'class': 'has_icon user_known',
|
2025-09-03 23:20:56 +02:00
|
|
|
|
'title': "User: " + data.usr
|
|
|
|
|
|
}, data.usr));
|
|
|
|
|
|
|
|
|
|
|
|
} else { /* others */
|
|
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
|
|
|
|
|
|
/*span1.appendChild(make('span', { // IP-Address
|
|
|
|
|
|
'class': 'has_icon ipaddr ip' + ipType,
|
2025-09-03 23:20:56 +02:00
|
|
|
|
'title': "IP-Address: " + data.ip
|
2025-09-12 15:38:28 +02:00
|
|
|
|
}, data.ip));*/
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
span1.appendChild(make('span', { /* Internal ID */
|
|
|
|
|
|
'class': 'has_icon session typ_' + data.typ,
|
|
|
|
|
|
'title': "ID: " + data.id
|
|
|
|
|
|
}, data.id));
|
2025-09-04 23:01:46 +02:00
|
|
|
|
}
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
2025-09-13 23:20:43 +02:00
|
|
|
|
// country flag:
|
|
|
|
|
|
if (data.geo && data.geo !== 'ZZ') {
|
|
|
|
|
|
span1.appendChild(make('span', {
|
|
|
|
|
|
'class': 'icon_only country ctry_' + data.geo.toLowerCase(),
|
|
|
|
|
|
'data-ctry': data.geo,
|
|
|
|
|
|
'title': "Country: " + ( data._country || "Unknown")
|
|
|
|
|
|
}, ( data._country || "Unknown") ));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 23:08:54 +02:00
|
|
|
|
// referer icons:
|
2025-09-19 15:08:25 +02:00
|
|
|
|
if ((data._type == BM_USERTYPE.PROBABLY_HUMAN || data._type == BM_USERTYPE.LIKELY_BOT) && data.ref) {
|
|
|
|
|
|
const refInfo = BotMon.live.data.analytics.getRefererInfo(data.ref);
|
2025-09-16 23:08:54 +02:00
|
|
|
|
span1.appendChild(make('span', {
|
|
|
|
|
|
'class': 'icon_only referer ref_' + refInfo.id,
|
|
|
|
|
|
'title': "Referer: " + data.ref
|
|
|
|
|
|
}, refInfo.n));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 18:22:29 +02:00
|
|
|
|
summary.appendChild(span1);
|
|
|
|
|
|
const span2 = make('span'); /* right-hand group */
|
|
|
|
|
|
|
2025-09-15 17:49:16 +02:00
|
|
|
|
span2.appendChild(make('span', { /* first-seen */
|
|
|
|
|
|
'class': 'has_iconfirst-seen',
|
|
|
|
|
|
'title': "First seen: " + data._firstSeen.toLocaleString() + " UTC"
|
|
|
|
|
|
}, BotMon.t._formatTime(data._firstSeen)));
|
|
|
|
|
|
|
|
|
|
|
|
span2.appendChild(make('span', { /* page views */
|
|
|
|
|
|
'class': 'has_icon pageviews',
|
|
|
|
|
|
'title': data._pageViews.length + " page view(s)"
|
|
|
|
|
|
}, data._pageViews.length));
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
|
|
|
|
|
summary.appendChild(span2);
|
|
|
|
|
|
|
2025-09-06 16:20:58 +02:00
|
|
|
|
// add details expandable section:
|
|
|
|
|
|
details.appendChild(BotMon.live.gui.lists._makeVisitorDetails(data, type));
|
|
|
|
|
|
|
|
|
|
|
|
li.appendChild(details);
|
|
|
|
|
|
return li;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
_makeVisitorDetails: function(data, type) {
|
|
|
|
|
|
|
|
|
|
|
|
// shortcut for neater code:
|
|
|
|
|
|
const make = BotMon.t._makeElement;
|
|
|
|
|
|
|
|
|
|
|
|
let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' );
|
|
|
|
|
|
if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
|
|
|
|
|
|
const platformName = (data._platform ? data._platform.n : 'Unknown');
|
|
|
|
|
|
const clientName = (data._client ? data._client.n: 'Unknown');
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
|
|
|
|
|
const dl = make('dl', {'class': 'visitor_details'});
|
|
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
if (data._type == BM_USERTYPE.KNOWN_BOT) {
|
|
|
|
|
|
|
|
|
|
|
|
dl.appendChild(make('dt', {}, "Bot name:")); /* bot info */
|
2025-09-12 15:38:28 +02:00
|
|
|
|
dl.appendChild(make('dd', {'class': 'icon_only bot bot_' + (data._bot ? data._bot.id : 'unknown')},
|
2025-09-03 23:20:56 +02:00
|
|
|
|
(data._bot ? data._bot.n : 'Unknown')));
|
|
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
if (data._bot && data._bot.url) {
|
|
|
|
|
|
dl.appendChild(make('dt', {}, "Bot info:")); /* bot info */
|
|
|
|
|
|
const botInfoDd = dl.appendChild(make('dd'));
|
|
|
|
|
|
botInfoDd.appendChild(make('a', {
|
|
|
|
|
|
'href': data._bot.url,
|
|
|
|
|
|
'target': '_blank'
|
|
|
|
|
|
}, data._bot.url)); /* bot info link*/
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} else { /* not for bots */
|
|
|
|
|
|
|
|
|
|
|
|
dl.appendChild(make('dt', {}, "Client:")); /* client */
|
2025-09-12 15:38:28 +02:00
|
|
|
|
dl.appendChild(make('dd', {'class': 'has_icon client cl_' + (data._client ? data._client.id : 'unknown')},
|
2025-09-04 23:01:46 +02:00
|
|
|
|
clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) ));
|
|
|
|
|
|
|
|
|
|
|
|
dl.appendChild(make('dt', {}, "Platform:")); /* platform */
|
2025-09-12 15:38:28 +02:00
|
|
|
|
dl.appendChild(make('dd', {'class': 'has_icon platform pf_' + (data._platform ? data._platform.id : 'unknown')},
|
2025-09-04 23:01:46 +02:00
|
|
|
|
platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) ));
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
2025-09-05 12:47:36 +02:00
|
|
|
|
dl.appendChild(make('dt', {}, "IP-Address:"));
|
2025-09-14 19:32:48 +02:00
|
|
|
|
const ipItem = make('dd', {'class': 'has_icon ipaddr ip' + ipType});
|
|
|
|
|
|
ipItem.appendChild(make('span', {'class': 'address'} , data.ip));
|
|
|
|
|
|
ipItem.appendChild(make('a', {
|
|
|
|
|
|
'class': 'icon_only extlink dnscheck',
|
|
|
|
|
|
'href': `https://dnschecker.org/ip-location.php?ip=${encodeURIComponent(data.ip)}`,
|
|
|
|
|
|
'target': 'dnscheck',
|
|
|
|
|
|
'title': "View this address on DNSChecker.org"
|
|
|
|
|
|
} , "Check Address"));
|
|
|
|
|
|
ipItem.appendChild(make('a', {
|
|
|
|
|
|
'class': 'icon_only extlink ipinfo',
|
|
|
|
|
|
'href': `https://ipinfo.io/${encodeURIComponent(data.ip)}`,
|
|
|
|
|
|
'target': 'ipinfo',
|
|
|
|
|
|
'title': "View this address on IPInfo.io"
|
|
|
|
|
|
} , "DNS Info"));
|
|
|
|
|
|
dl.appendChild(ipItem);
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
2025-09-07 20:52:12 +02:00
|
|
|
|
/*dl.appendChild(make('dt', {}, "ID:"));
|
|
|
|
|
|
dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id));*/
|
2025-09-05 12:47:36 +02:00
|
|
|
|
}
|
2025-09-05 09:15:08 +02:00
|
|
|
|
|
2025-09-07 16:11:17 +02:00
|
|
|
|
if (Math.abs(data._lastSeen - data._firstSeen) < 100) {
|
2025-09-03 18:22:29 +02:00
|
|
|
|
dl.appendChild(make('dt', {}, "Seen:"));
|
|
|
|
|
|
dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString()));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dl.appendChild(make('dt', {}, "First seen:"));
|
|
|
|
|
|
dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString()));
|
|
|
|
|
|
dl.appendChild(make('dt', {}, "Last seen:"));
|
|
|
|
|
|
dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dl.appendChild(make('dt', {}, "User-Agent:"));
|
2025-09-06 16:20:58 +02:00
|
|
|
|
dl.appendChild(make('dd', {'class': 'agent'}, data.agent));
|
2025-09-03 18:22:29 +02:00
|
|
|
|
|
2025-09-08 20:38:07 +02:00
|
|
|
|
dl.appendChild(make('dt', {}, "Languages:"));
|
2025-09-14 19:32:48 +02:00
|
|
|
|
dl.appendChild(make('dd', {'class': 'langs'}, ` [${data.accept}]`));
|
2025-09-08 20:38:07 +02:00
|
|
|
|
|
2025-09-10 14:44:08 +02:00
|
|
|
|
if (data.geo && data.geo !=='') {
|
|
|
|
|
|
dl.appendChild(make('dt', {}, "Location:"));
|
|
|
|
|
|
dl.appendChild(make('dd', {
|
2025-09-12 15:38:28 +02:00
|
|
|
|
'class': 'has_icon country ctry_' + data.geo.toLowerCase(),
|
2025-09-10 14:44:08 +02:00
|
|
|
|
'data-ctry': data.geo,
|
|
|
|
|
|
'title': "Country: " + data._country
|
|
|
|
|
|
}, data._country + ' (' + data.geo + ')'));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 15:38:28 +02:00
|
|
|
|
dl.appendChild(make('dt', {}, "Session ID:"));
|
|
|
|
|
|
dl.appendChild(make('dd', {'class': 'has_icon session typ_' + data.typ}, data.id));
|
|
|
|
|
|
|
2025-09-04 23:01:46 +02:00
|
|
|
|
dl.appendChild(make('dt', {}, "Seen by:"));
|
|
|
|
|
|
dl.appendChild(make('dd', undefined, data._seenBy.join(', ') ));
|
|
|
|
|
|
|
2025-09-03 18:22:29 +02:00
|
|
|
|
dl.appendChild(make('dt', {}, "Visited pages:"));
|
|
|
|
|
|
const pagesDd = make('dd', {'class': 'pages'});
|
|
|
|
|
|
const pageList = make('ul');
|
2025-09-06 16:20:58 +02:00
|
|
|
|
|
|
|
|
|
|
/* list all page views */
|
2025-09-14 19:32:48 +02:00
|
|
|
|
data._pageViews.sort( (a, b) => a._firstSeen - b._firstSeen );
|
2025-09-03 18:22:29 +02:00
|
|
|
|
data._pageViews.forEach( (page) => {
|
2025-09-15 17:49:16 +02:00
|
|
|
|
pageList.appendChild(BotMon.live.gui.lists._makePageViewItem(page));
|
2025-09-03 18:22:29 +02:00
|
|
|
|
});
|
|
|
|
|
|
pagesDd.appendChild(pageList);
|
|
|
|
|
|
dl.appendChild(pagesDd);
|
|
|
|
|
|
|
2025-09-07 16:11:17 +02:00
|
|
|
|
/* bot evaluation rating */
|
2025-09-07 20:52:12 +02:00
|
|
|
|
if (data._type !== BM_USERTYPE.KNOWN_BOT && data._type !== BM_USERTYPE.KNOWN_USER) {
|
|
|
|
|
|
dl.appendChild(make('dt', undefined, "Bot rating:"));
|
2025-09-10 14:44:08 +02:00
|
|
|
|
dl.appendChild(make('dd', {'class': 'bot-rating'}, ( data._botVal ? data._botVal : '–' ) + ' (of ' + BotMon.live.data.rules._threshold + ')'));
|
2025-09-07 20:52:12 +02:00
|
|
|
|
|
|
|
|
|
|
/* add bot evaluation details: */
|
|
|
|
|
|
if (data._eval) {
|
2025-09-15 22:58:08 +02:00
|
|
|
|
dl.appendChild(make('dt', {}, "Bot evaluation:"));
|
2025-09-07 20:52:12 +02:00
|
|
|
|
const evalDd = make('dd');
|
|
|
|
|
|
const testList = make('ul',{
|
|
|
|
|
|
'class': 'eval'
|
|
|
|
|
|
});
|
|
|
|
|
|
data._eval.forEach( test => {
|
2025-09-05 16:22:39 +02:00
|
|
|
|
|
2025-09-07 20:52:12 +02:00
|
|
|
|
const tObj = BotMon.live.data.rules.getRuleInfo(test);
|
|
|
|
|
|
let tDesc = tObj ? tObj.desc : test;
|
2025-09-06 16:20:58 +02:00
|
|
|
|
|
2025-09-07 20:52:12 +02:00
|
|
|
|
// special case for Bot IP range test:
|
|
|
|
|
|
if (tObj.func == 'fromKnownBotIP') {
|
|
|
|
|
|
const rangeInfo = BotMon.live.data.rules.getBotIPInfo(data.ip);
|
|
|
|
|
|
if (rangeInfo) {
|
2025-09-08 20:38:07 +02:00
|
|
|
|
tDesc += ' (' + (rangeInfo.label ? rangeInfo.label : 'Unknown') + ')';
|
2025-09-07 20:52:12 +02:00
|
|
|
|
}
|
2025-09-06 16:20:58 +02:00
|
|
|
|
}
|
2025-09-05 16:22:39 +02:00
|
|
|
|
|
2025-09-07 20:52:12 +02:00
|
|
|
|
// create the entry field
|
|
|
|
|
|
const tstLi = make('li');
|
|
|
|
|
|
tstLi.appendChild(make('span', {
|
|
|
|
|
|
'data-testid': test
|
|
|
|
|
|
}, tDesc));
|
|
|
|
|
|
tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') ));
|
|
|
|
|
|
testList.appendChild(tstLi);
|
|
|
|
|
|
});
|
2025-09-05 16:04:40 +02:00
|
|
|
|
|
2025-09-07 20:52:12 +02:00
|
|
|
|
// add total row
|
|
|
|
|
|
const tst2Li = make('li', {
|
|
|
|
|
|
'class': 'total'
|
|
|
|
|
|
});
|
|
|
|
|
|
/*tst2Li.appendChild(make('span', {}, "Total:"));
|
|
|
|
|
|
tst2Li.appendChild(make('span', {}, data._botVal));
|
|
|
|
|
|
testList.appendChild(tst2Li);*/
|
2025-09-05 16:04:40 +02:00
|
|
|
|
|
2025-09-07 20:52:12 +02:00
|
|
|
|
evalDd.appendChild(testList);
|
|
|
|
|
|
dl.appendChild(evalDd);
|
|
|
|
|
|
}
|
2025-09-05 16:31:11 +02:00
|
|
|
|
}
|
2025-09-07 20:52:12 +02:00
|
|
|
|
// return the element to add to the UI:
|
2025-09-06 16:20:58 +02:00
|
|
|
|
return dl;
|
2025-09-15 17:49:16 +02:00
|
|
|
|
},
|
2025-09-04 23:01:46 +02:00
|
|
|
|
|
2025-09-15 17:49:16 +02:00
|
|
|
|
// make a page view item:
|
|
|
|
|
|
_makePageViewItem: function(page) {
|
2025-09-15 19:34:19 +02:00
|
|
|
|
//console.log("makePageViewItem:",page);
|
2025-09-15 17:49:16 +02:00
|
|
|
|
|
|
|
|
|
|
// shortcut for neater code:
|
|
|
|
|
|
const make = BotMon.t._makeElement;
|
|
|
|
|
|
|
|
|
|
|
|
// the actual list item:
|
|
|
|
|
|
const pgLi = make('li');
|
|
|
|
|
|
|
|
|
|
|
|
const row1 = make('div', {'class': 'row'});
|
|
|
|
|
|
|
|
|
|
|
|
row1.appendChild(make('span', { // page id is the left group
|
|
|
|
|
|
'data-lang': page.lang,
|
|
|
|
|
|
'title': "PageID: " + page.pg
|
|
|
|
|
|
}, page.pg)); /* DW Page ID */
|
|
|
|
|
|
|
|
|
|
|
|
// get the time difference:
|
|
|
|
|
|
row1.appendChild(make('span', {
|
|
|
|
|
|
'class': 'first-seen',
|
|
|
|
|
|
'title': "First visited: " + page._firstSeen.toLocaleString() + " UTC"
|
|
|
|
|
|
}, BotMon.t._formatTime(page._firstSeen)));
|
|
|
|
|
|
|
|
|
|
|
|
pgLi.appendChild(row1);
|
|
|
|
|
|
|
|
|
|
|
|
/* LINE 2 */
|
|
|
|
|
|
|
|
|
|
|
|
const row2 = make('div', {'class': 'row'});
|
|
|
|
|
|
|
|
|
|
|
|
// page referrer:
|
|
|
|
|
|
if (page._ref) {
|
|
|
|
|
|
row2.appendChild(make('span', {
|
|
|
|
|
|
'class': 'referer',
|
|
|
|
|
|
'title': "Referrer: " + page._ref.href
|
|
|
|
|
|
}, page._ref.hostname));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
row2.appendChild(make('span', {
|
|
|
|
|
|
'class': 'referer'
|
|
|
|
|
|
}, "No referer"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// visit duration:
|
|
|
|
|
|
let visitTimeStr = "Bounce";
|
|
|
|
|
|
const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime();
|
|
|
|
|
|
if (visitDuration > 0) {
|
|
|
|
|
|
visitTimeStr = Math.floor(visitDuration / 1000) + "s";
|
|
|
|
|
|
}
|
|
|
|
|
|
const tDiff = BotMon.t._formatTimeDiff(page._firstSeen, page._lastSeen);
|
|
|
|
|
|
if (tDiff) {
|
|
|
|
|
|
row2.appendChild(make('span', {'class': 'visit-length', 'title': 'Last seen: ' + page._lastSeen.toLocaleString()}, tDiff));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
row2.appendChild(make('span', {
|
|
|
|
|
|
'class': 'bounce',
|
|
|
|
|
|
'title': "Visitor bounced"}, "Bounce"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pgLi.appendChild(row2);
|
|
|
|
|
|
|
|
|
|
|
|
return pgLi;
|
|
|
|
|
|
}
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-29 23:28:19 +03:00
|
|
|
|
};
|
2025-08-29 23:14:10 +03:00
|
|
|
|
|
2025-09-01 16:25:25 +02:00
|
|
|
|
/* launch only if the BotMon admin panel is open: */
|
|
|
|
|
|
if (document.getElementById('botmon__admin')) {
|
|
|
|
|
|
BotMon.init();
|
2025-08-29 23:14:10 +03:00
|
|
|
|
}
|