Files
dokuwiki-plugin-botmon/script.js

1245 lines
34 KiB
JavaScript
Raw Normal View History

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-04 09:01:37 +02:00
/* 04.09.2025 - 0.1.8 - pre-release */
2025-08-29 23:14:10 +03:00
/* Authors: Sascha Leib <ad@hominem.info> */
2025-09-04 23:01:46 +02:00
// enumeration of user types:
const BM_USERTYPE = Object.freeze({
'UNKNOWN': 'unknown',
'KNOWN_USER': 'user',
'HUMAN': 'human',
'LIKELY_BOT': 'likely_bot',
'KNOWN_BOT': 'known_bot'
});
/* BotMon root object */
2025-09-01 16:25:25 +02:00
const BotMon = {
2025-08-29 23:14:10 +03:00
init: function() {
//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',
_today: (new Date()).toISOString().slice(0, 10),
_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` : '');
},
/* 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-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}` : '' ) + ')';
BotMon.live.gui.status.setTitle(`Data for <time datetime=${BotMon._today}>${BotMon._today}</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) {
case 'bots':
data._dispatchBotsLoaded = true;
break;
case 'clients':
data._dispatchClientsLoaded = true;
break;
case 'platforms':
data._dispatchPlatformsLoaded = true;
break;
2025-09-05 16:04:40 +02:00
case 'rules':
data._dispatchRulesLoaded = true;
break;
2025-08-30 18:40:16 +03:00
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-01 16:25:25 +02:00
BotMon.live.data.loadLogFile('srv', 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-01 16:25:25 +02:00
BotMon.live.data.loadLogFile('log', 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() {
//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-01 16:25:25 +02:00
BotMon.live.data.loadLogFile('tck', BotMon.live.data._onTicksLogLoaded);
2025-08-30 18:40:16 +03:00
},
// event callback, after the tiker log has been loaded:
_onTicksLogLoaded: function() {
//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
//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-04 23:01:46 +02:00
findVisitor: function(visitor) {
//console.info('BotMon.live.data.model.findVisitor()');
//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
// loop over all visitors already registered:
for (let i=0; i<model._visitors.length; i++) {
const v = model._visitors[i];
2025-09-04 23:01:46 +02:00
if (visitor._type == BM_USERTYPE.KNOWN_BOT) { /* known bots */
// bots match when their ID matches:
if (v._bot && v._bot.id == visitor._bot.id) {
return v;
}
} else if (visitor._type == BM_USERTYPE.KNOWN_USER) { /* registered users */
2025-09-05 12:47:36 +02:00
//if (visitor.id == 'fsmoe7lgqb89t92vt4ju8vdl0q') console.log(visitor);
2025-09-04 23:01:46 +02:00
// visitors match when their names match:
if ( v.usr == visitor.usr
&& v.ip == visitor.ip
&& v.agent == visitor.agent) {
return v;
}
} else { /* any other visitor */
if ( v.id == visitor.id) { /* match the pre-defined IDs */
return v;
2025-09-05 12:47:36 +02:00
} else if (v.ip == visitor.ip && v.agent == visitor.agent) {
console.info("Visitor ID not found, using matchin IP + User-Agent instead.");
return v;
2025-09-04 23:01:46 +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-05 12:47:36 +02:00
nv._type = ( bot ? BM_USERTYPE.KNOWN_BOT : ( nv.usr && nv.usr !== '' ? BM_USERTYPE.KNOWN_USER : BM_USERTYPE.UNKNOWN ) );
2025-09-04 23:01:46 +02:00
if (!nv._firstSeen) nv._firstSeen = nv.ts;
2025-09-05 12:47:36 +02:00
nv._lastSeen = nv.ts;
2025-09-04 09:01:37 +02:00
2025-08-30 13:01:50 +03:00
// check if it already exists:
2025-09-04 23:01:46 +02:00
let visitor = model.findVisitor(nv);
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-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-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-04 23:01:46 +02:00
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-03 23:20:56 +02:00
const type = 'log';
2025-09-04 23:01:46 +02:00
let visitor = BotMon.live.data.model.findVisitor(dat);
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-08-30 13:01:50 +03:00
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-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-05 12:47:36 +02:00
const type = 'tck';
2025-08-30 18:40:16 +03:00
// find the visit info:
2025-09-04 23:01:46 +02:00
let visitor = model.findVisitor(dat);
2025-08-30 21:11:01 +03:00
if (!visitor) {
2025-09-05 12:47:36 +02:00
console.warn(`No visitor with ID ${dat.id}, registering a new one.`);
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) {
console.warn(`No page view for visit ID ${dat.id}, page ${dat.pg}, registering a new one.`);
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) {
return {
_by: type,
ip: data.ip,
pg: data.pg,
ref: data.ref || '',
_firstSeen: data.ts,
_lastSeen: data.ts,
_seenBy: [type],
_jsClient: ( type !== 'srv'),
_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,
bots: {
known: 0,
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: [],
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-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
v._type = BM_USERTYPE.HUMAN;
this.data.bots.human += v._pageViews.length;
this.groups.humans.push(v);
}
2025-09-04 23:01:46 +02:00
// TODO: find suspected bots
}
2025-08-30 21:11:01 +03:00
});
2025-09-04 23:01:46 +02:00
//console.log(this.data);
//console.log(this.groups);
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 …");
const url = BotMon._baseDir + 'data/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-01 16:25:25 +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;
// 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,
url: bot.url,
v: (rxr.length > 1 ? rxr[1] : -1)
};
r = true;
break;
}
};
return r;
});
// check for unknown bots:
if (!botInfo) {
const botmatch = agent.match(/[^\s](\w*bot)[\/\s;\),$]/i);
if(botmatch) {
botInfo = {'id': "other", 'n': "Other", "bot": botmatch[0] };
}
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");
const url = BotMon._baseDir + 'data/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
}
},
// 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
let match = {"n": "Unknown", "v": -1, "id": null};
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;
},
// 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");
const url = BotMon._baseDir + 'data/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
let match = {"n": "Unknown", "id": null};
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
},
// 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 …");
const url = BotMon._baseDir + 'data/rules.json';
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
const json = await response.json();
if (json.rules) {
console.log(json.rules);
this._rulesList = json.rules;
}
if (json.threshold) {
this._threshold = json.threshold;
}
this._ready = true;
} catch (error) {
BotMon.live.gui.status.setError("Error while loading the rules file: " + error.message);
} 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: {
// check if client is one of the obsolete ones:
obsoleteClient: function(visitor) {
const obsClients = ['aol', 'msie', 'chromeold'];
const clientId = ( visitor._client ? visitor._client.id : '');
return obsClients.includes(clientId);
},
// check if OS/Platform is one of the obsolete ones:
obsoletePlatform: function(visitor) {
const obsPlatforms = ['winold', 'macosold'];
const platformId = ( visitor._platform ? visitor._platform.id : '');
return obsPlatforms.includes(platformId);
},
// client does not use JavaScript:
noJavaScript: function(visitor) {
return (visitor._jsClient === false);
},
// are there at lest num pages loaded?
smallPageCount: function(visitor, num) {
return (visitor._pageViews.length <= Number(num));
},
// there are no ticks recorded for a visitor
// note that this will also trigger the "noJavaScript" rule:
noTicks: function(visitor) {
2025-09-05 16:22:39 +02:00
return !visitor._seenBy.includes('tck');
2025-09-05 16:04:40 +02:00
},
// there are no references in any of the page visits:
noReferences: function(visitor) {
return (visitor._hasReferrer === true);
}
}
},
2025-08-30 13:01:50 +03:00
loadLogFile: async function(type, onLoaded = undefined) {
2025-09-03 23:20:56 +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";
columns = ['ts','ip','pg','id','typ','usr','agent','ref'];
2025-08-29 23:14:10 +03:00
break;
case "log":
typeName = "Page load";
columns = ['ts','ip','pg','id','usr','lt','ref','agent'];
2025-08-29 23:14:10 +03:00
break;
case "tck":
typeName = "Ticker";
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-01 16:25:25 +02:00
const url = BotMon._baseDir + `logs/${BotMon._today}.${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) {
throw new Error(`${response.status} ${response.statusText}`);
}
2025-08-30 13:01:50 +03:00
const logtxt = await response.text();
logtxt.split('\n').forEach((line) => {
if (line.trim() === '') return; // skip empty lines
const cols = line.split('\t');
// assign the columns to an object:
const data = {};
cols.forEach( (colVal,i) => {
colName = columns[i] || `col${i}`;
2025-09-03 23:20:56 +02:00
const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim());
2025-08-30 13:01:50 +03:00
data[colName] = colValue;
});
// register the visit in the model:
switch(type) {
case 'srv':
2025-09-03 23:20:56 +02:00
BotMon.live.data.model.registerVisit(data, type);
2025-08-30 13:01:50 +03:00
break;
case 'log':
2025-09-03 23:20:56 +02:00
data.typ = 'js';
2025-09-01 16:25:25 +02:00
BotMon.live.data.model.updateVisit(data);
2025-08-30 13:01:50 +03:00
break;
2025-08-30 18:40:16 +03:00
case 'tck':
2025-09-03 23:20:56 +02:00
data.typ = 'js';
2025-09-01 16:25:25 +02:00
BotMon.live.data.model.updateTicks(data);
2025-08-30 18:40:16 +03:00
break;
2025-08-30 13:01:50 +03:00
default:
console.warn(`Unknown log type ${type}.`);
return;
}
});
if (onLoaded) {
onLoaded(); // callback after loading is finished.
}
2025-08-29 23:14:10 +03:00
} catch (error) {
2025-09-01 16:25:25 +02:00
BotMon.live.gui.status.setError(`Error while loading the ${typeName} log 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.");
2025-08-29 23:14:10 +03:00
}
}
},
2025-08-30 21:11:01 +03:00
gui: {
init: function() {
// init the lists view:
this.lists.init();
},
2025-08-30 21:11:01 +03:00
overview: {
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;
const parent = document.getElementById('botmon__today__content');
2025-09-04 23:01:46 +02:00
// shortcut for neater code:
const makeElement = BotMon.t._makeElement;
2025-08-30 21:11:01 +03:00
if (parent) {
2025-09-01 18:55:56 +02:00
2025-09-05 16:04:40 +02:00
const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 100);
2025-09-01 18:55:56 +02:00
2025-08-30 21:11:01 +03:00
jQuery(parent).prepend(jQuery(`
2025-09-01 16:25:25 +02:00
<details id="botmon__today__overview" open>
2025-09-01 15:42:06 +02:00
<summary>Overview</summary>
<div class="grid-3-columns">
<dl>
<dt>Web metrics</dt>
2025-09-05 16:04:40 +02:00
<dd><span>Total page views:</span><strong>${data.totalPageViews}</strong></dd>
<dd><span>Total visitors (est.):</span><span>${data.totalVisits}</span></dd>
<dd><span>Bounce rate (est.):</span><span>${bounceRate}%</span></dd>
2025-09-01 15:42:06 +02:00
</dl>
<dl>
<dt>Bots vs. Humans</dt>
2025-09-05 16:04:40 +02:00
<dd><span>Registered users:</span><strong>${data.bots.users}</strong></dd>
<dd><span>Probably humans:</span><strong>${data.bots.human}</strong></dd>
<dd><span>Suspected bots:</span><strong>${data.bots.suspected}</strong></dd>
<dd><span>Known bots:</span><strong>${data.bots.known}</strong></dd>
2025-09-01 15:42:06 +02:00
</dl>
2025-09-04 23:01:46 +02:00
<dl id="botmon__botslist"></dl>
2025-09-01 15:42:06 +02:00
</div>
</details>
2025-08-30 21:11:01 +03:00
`));
2025-09-04 23:01:46 +02:00
// update known bots list:
const block = document.getElementById('botmon__botslist');
block.innerHTML = "<dt>Top known bots</dt>";
let bots = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => {
return b._pageViews.length - a._pageViews.length;
});
for (let i=0; i < Math.min(bots.length, 4); i++) {
const dd = makeElement('dd');
dd.appendChild(makeElement('span', {'class': 'bot bot_' + bots[i]._bot.id}, bots[i]._bot.n));
2025-09-05 16:04:40 +02:00
dd.appendChild(makeElement('strong', undefined, bots[i]._pageViews.length));
2025-09-04 23:01:46 +02:00
block.appendChild(dd);
}
2025-08-30 21:11:01 +03:00
}
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) {
el.innerText = "An error occured. See the browser log for details!";
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
}
},
lists: {
init: function() {
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 = '';
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';
break;
case 3:
listTitle = "Known bots";
listId = 'knownBots';
break;
default:
console.warn('Unknwon list number.');
}
const details = BotMon.t._makeElement('details', {
'data-group': listId,
'data-loaded': false
});
details.appendChild(BotMon.t._makeElement('summary',
undefined,
listTitle
));
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()');
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' );
const li = make('li'); // root list item
const details = make('details');
const summary = make('summary');
details.appendChild(summary);
const span1 = make('span'); /* left-hand group */
2025-09-04 23:01:46 +02:00
const platformName = (data._platform ? data._platform.n : 'Unknown');
const clientName = (data._client ? data._client.n: 'Unknown');
if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */
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 */
'class': '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-04 23:01:46 +02:00
'class': 'user_known',
2025-09-03 23:20:56 +02:00
'title': "User: " + data.usr
}, data.usr));
} else { /* others */
if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
span1.appendChild(make('span', { /* IP-Address */
'class': 'ipaddr ip' + ipType,
'title': "IP-Address: " + data.ip
}, data.ip));
}
2025-09-04 23:01:46 +02:00
if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* Not for bots */
span1.appendChild(make('span', { /* Platform */
'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'),
'title': "Platform: " + platformName
}, platformName));
2025-09-04 23:01:46 +02:00
span1.appendChild(make('span', { /* Client */
'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'),
'title': "Client: " + clientName
}, clientName));
}
summary.appendChild(span1);
const span2 = make('span'); /* right-hand group */
2025-09-04 23:01:46 +02:00
span2.appendChild(make('span', { /* page views */
'class': 'pageviews'
}, data._pageViews.length));
summary.appendChild(span2);
// create expanable section:
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-03 23:20:56 +02:00
dl.appendChild(make('dd', {'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown')},
(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-04 23:01:46 +02:00
}
} else { /* not for bots */
dl.appendChild(make('dt', {}, "Client:")); /* client */
dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')},
clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) ));
dl.appendChild(make('dt', {}, "Platform:")); /* platform */
dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')},
platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) ));
2025-09-05 12:47:36 +02:00
dl.appendChild(make('dt', {}, "IP-Address:"));
dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip));
2025-09-05 12:47:36 +02:00
dl.appendChild(make('dt', {}, "ID:"));
dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id));
}
2025-09-05 09:15:08 +02:00
if ((data._lastSeen - data._firstSeen) < 1) {
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:"));
dl.appendChild(make('dd', {'class': 'agent' + ipType}, data.agent));
2025-09-04 23:01:46 +02:00
dl.appendChild(make('dt', {}, "Visitor Type:"));
dl.appendChild(make('dd', undefined, data._type ));
dl.appendChild(make('dt', {}, "Seen by:"));
dl.appendChild(make('dd', undefined, data._seenBy.join(', ') ));
dl.appendChild(make('dt', {}, "Visited pages:"));
const pagesDd = make('dd', {'class': 'pages'});
const pageList = make('ul');
data._pageViews.forEach( (page) => {
const pgLi = make('li');
let visitTimeStr = "Bounce";
const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime();
if (visitDuration > 0) {
visitTimeStr = Math.floor(visitDuration / 1000) + "s";
}
pgLi.appendChild(make('span', {}, page.pg));
2025-09-05 12:47:36 +02:00
// pgLi.appendChild(make('span', {}, page.ref));
pgLi.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount));
pgLi.appendChild(make('span', {}, page._firstSeen.toLocaleString()));
pgLi.appendChild(make('span', {}, page._lastSeen.toLocaleString()));
pageList.appendChild(pgLi);
});
pagesDd.appendChild(pageList);
dl.appendChild(pagesDd);
2025-09-05 16:22:39 +02:00
if (data._eval) {
dl.appendChild(make('dt', {}, "Evaluation:"));
const evalDd = make('dd');
const testList = make('ul');
data._eval.forEach( (test) => {
console.log(test);
const tObj = BotMon.live.data.rules.getRuleInfo(test);
const tDesc = tObj ? tObj.desc : test;
const tstLi = make('li');
tstLi.appendChild(make('span', {
'class': 'test test_' . test
}, ( tObj ? tObj.desc : test )));
tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') ));
testList.appendChild(tstLi);
});
2025-09-05 16:04:40 +02:00
2025-09-05 16:22:39 +02:00
const tst2Li = make('li');
tst2Li.appendChild(make('span', {}, "Total:"));
tst2Li.appendChild(make('span', {}, data._botVal));
testList.appendChild(tst2Li);
}
2025-09-05 16:04:40 +02:00
evalDd.appendChild(testList);
dl.appendChild(evalDd);
details.appendChild(dl);
li.appendChild(details);
return li;
}
2025-09-04 23:01:46 +02:00
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
}