xref: /plugin/botmon/script.js (revision 17cb08e869b6df6a8241d73925e2dc04d9cae04b)
143d9de6bSSascha Leib"use strict";
27bd08c30SSascha Leib/* DokuWiki BotMon Plugin Script file */
35526d629SSascha Leib/* 06.09.2025 - 0.2.0 - beta */
45526d629SSascha Leib/* Author: Sascha Leib <ad@hominem.info> */
5f125bc8dSSascha Leib
6f4417fdeSSascha Leib// enumeration of user types:
7f4417fdeSSascha Leibconst BM_USERTYPE = Object.freeze({
8f4417fdeSSascha Leib	'UNKNOWN': 'unknown',
9f4417fdeSSascha Leib	'KNOWN_USER': 'user',
10f4417fdeSSascha Leib	'HUMAN': 'human',
11f4417fdeSSascha Leib	'LIKELY_BOT': 'likely_bot',
12f4417fdeSSascha Leib	'KNOWN_BOT': 'known_bot'
13f4417fdeSSascha Leib});
14f4417fdeSSascha Leib
15f4417fdeSSascha Leib/* BotMon root object */
167bd08c30SSascha Leibconst BotMon = {
17f125bc8dSSascha Leib
18f125bc8dSSascha Leib	init: function() {
1993a5b18bSSascha Leib		//console.info('BotMon.init()');
20f125bc8dSSascha Leib
21f125bc8dSSascha Leib		// find the plugin basedir:
22f125bc8dSSascha Leib		this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/'))
237bd08c30SSascha Leib			+ '/plugins/botmon/';
24f125bc8dSSascha Leib
25f125bc8dSSascha Leib		// read the page language from the DOM:
26f125bc8dSSascha Leib		this._lang = document.getRootNode().documentElement.lang || this._lang;
27f125bc8dSSascha Leib
28f125bc8dSSascha Leib		// get the time offset:
297bd08c30SSascha Leib		this._timeDiff = BotMon.t._getTimeOffset();
30f125bc8dSSascha Leib
31f125bc8dSSascha Leib		// init the sub-objects:
327bd08c30SSascha Leib		BotMon.t._callInit(this);
33f125bc8dSSascha Leib	},
34f125bc8dSSascha Leib
35f125bc8dSSascha Leib	_baseDir: null,
36f125bc8dSSascha Leib	_lang: 'en',
37f125bc8dSSascha Leib	_today: (new Date()).toISOString().slice(0, 10),
38f125bc8dSSascha Leib	_timeDiff: '',
39f125bc8dSSascha Leib
40f125bc8dSSascha Leib	/* internal tools */
41f125bc8dSSascha Leib	t: {
42f125bc8dSSascha Leib		/* helper function to call inits of sub-objects */
43f125bc8dSSascha Leib		_callInit: function(obj) {
447bd08c30SSascha Leib			//console.info('BotMon.t._callInit(obj=',obj,')');
45f125bc8dSSascha Leib
46f125bc8dSSascha Leib			/* call init / _init on each sub-object: */
47f125bc8dSSascha Leib			Object.keys(obj).forEach( (key,i) => {
48f125bc8dSSascha Leib				const sub = obj[key];
49f125bc8dSSascha Leib				let init = null;
50f125bc8dSSascha Leib				if (typeof sub === 'object' && sub.init) {
51f125bc8dSSascha Leib					init = sub.init;
52f125bc8dSSascha Leib				}
53f125bc8dSSascha Leib
54f125bc8dSSascha Leib				// bind to object
55f125bc8dSSascha Leib				if (typeof init == 'function') {
56f125bc8dSSascha Leib					const init2 = init.bind(sub);
57f125bc8dSSascha Leib					init2(obj);
58f125bc8dSSascha Leib				}
59f125bc8dSSascha Leib			});
60f125bc8dSSascha Leib		},
61f125bc8dSSascha Leib
62f125bc8dSSascha Leib		/* helper function to calculate the time difference to UTC: */
63f125bc8dSSascha Leib		_getTimeOffset: function() {
64f125bc8dSSascha Leib			const now = new Date();
65f125bc8dSSascha Leib			let offset = now.getTimezoneOffset(); // in minutes
66f125bc8dSSascha Leib			const sign = Math.sign(offset); // +1 or -1
67f125bc8dSSascha Leib			offset = Math.abs(offset); // always positive
68f125bc8dSSascha Leib
69f125bc8dSSascha Leib			let hours = 0;
70f125bc8dSSascha Leib			while (offset >= 60) {
71f125bc8dSSascha Leib				hours += 1;
72f125bc8dSSascha Leib				offset -= 60;
73f125bc8dSSascha Leib			}
74f125bc8dSSascha Leib			return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : '');
7593a5b18bSSascha Leib		},
7693a5b18bSSascha Leib
7793a5b18bSSascha Leib		/* helper function to create a new element with all attributes and text content */
7893a5b18bSSascha Leib		_makeElement: function(name, atlist = undefined, text = undefined) {
7993a5b18bSSascha Leib			var r = null;
8093a5b18bSSascha Leib			try {
8193a5b18bSSascha Leib				r = document.createElement(name);
8293a5b18bSSascha Leib				if (atlist) {
8393a5b18bSSascha Leib					for (let attr in atlist) {
8493a5b18bSSascha Leib						r.setAttribute(attr, atlist[attr]);
8593a5b18bSSascha Leib					}
8693a5b18bSSascha Leib				}
8793a5b18bSSascha Leib				if (text) {
8893a5b18bSSascha Leib					r.textContent = text.toString();
8993a5b18bSSascha Leib				}
9093a5b18bSSascha Leib			} catch(e) {
9193a5b18bSSascha Leib				console.error(e);
9293a5b18bSSascha Leib			}
9393a5b18bSSascha Leib			return r;
945526d629SSascha Leib		},
955526d629SSascha Leib
965526d629SSascha Leib		/* helper to convert an ip address string to a normalised format: */
975526d629SSascha Leib		_ip2Num: function(ip) {
985526d629SSascha Leib			if (ip.indexOf(':') > 0) { /* IP6 */
995526d629SSascha Leib				return (ip.split(':').map(d => ('0000'+d).slice(-4) ).join(''));
1005526d629SSascha Leib			} else { /* IP4 */
1015526d629SSascha Leib				return Number(ip.split('.').map(d => ('000'+d).slice(-3) ).join(''));
1025526d629SSascha Leib			}
103446aa816SSascha Leib		},
104446aa816SSascha Leib
105446aa816SSascha Leib		/* helper function to format a Date object to show only the time. */
106446aa816SSascha Leib		/* returns String */
107446aa816SSascha Leib		_formatTime: function(date) {
108446aa816SSascha Leib
109446aa816SSascha Leib			if (date) {
110446aa816SSascha Leib				return ('0'+date.getHours()).slice(-2) + ':' + ('0'+date.getMinutes()).slice(-2) + ':' + ('0'+date.getSeconds()).slice(-2);
111446aa816SSascha Leib			} else {
112446aa816SSascha Leib				return null;
113446aa816SSascha Leib			}
114446aa816SSascha Leib
115446aa816SSascha Leib		},
116446aa816SSascha Leib
117446aa816SSascha Leib		/* helper function to show a time difference in seconds or minutes */
118446aa816SSascha Leib		/* returns String */
119446aa816SSascha Leib		_formatTimeDiff: function(dateA, dateB) {
120446aa816SSascha Leib
121446aa816SSascha Leib			// if the second date is ealier, swap them:
122446aa816SSascha Leib			if (dateA > dateB) dateB = [dateA, dateA = dateB][0];
123446aa816SSascha Leib
124446aa816SSascha Leib			// get the difference in milliseconds:
125446aa816SSascha Leib			let ms = dateB - dateA;
126446aa816SSascha Leib
127446aa816SSascha Leib			if (ms > 50) { /* ignore small time spans */
128446aa816SSascha Leib				const h = Math.floor((ms / (1000 * 60 * 60)) % 24);
129446aa816SSascha Leib				const m = Math.floor((ms / (1000 * 60)) % 60);
130446aa816SSascha Leib				const s = Math.floor((ms / 1000) % 60);
131446aa816SSascha Leib
132446aa816SSascha Leib				return ( h>0 ? h + 'h ': '') + ( m>0 ? m + 'm ': '') + ( s>0 ? s + 's': '');
133446aa816SSascha Leib			}
134446aa816SSascha Leib
135446aa816SSascha Leib			return null;
136446aa816SSascha Leib
137f125bc8dSSascha Leib		}
138f125bc8dSSascha Leib	}
139c7931771SSascha Leib};
140f125bc8dSSascha Leib
1419f1ee8c1SSascha Leib/* everything specific to the "Today" tab is self-contained in the "live" object: */
1427bd08c30SSascha LeibBotMon.live = {
143f125bc8dSSascha Leib	init: function() {
1447bd08c30SSascha Leib		//console.info('BotMon.live.init()');
145f125bc8dSSascha Leib
146f125bc8dSSascha Leib		// set the title:
1477bd08c30SSascha Leib		const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (BotMon._timeDiff != '' ? `, ${BotMon._timeDiff}` : '' ) + ')';
1487bd08c30SSascha Leib		BotMon.live.gui.status.setTitle(`Data for <time datetime=${BotMon._today}>${BotMon._today}</time> ${tDiff}`);
149f125bc8dSSascha Leib
150f125bc8dSSascha Leib		// init sub-objects:
1517bd08c30SSascha Leib		BotMon.t._callInit(this);
152f125bc8dSSascha Leib	},
153f125bc8dSSascha Leib
154f125bc8dSSascha Leib	data: {
155f125bc8dSSascha Leib		init: function() {
1567bd08c30SSascha Leib			//console.info('BotMon.live.data.init()');
157f125bc8dSSascha Leib
158f125bc8dSSascha Leib			// call sub-inits:
1597bd08c30SSascha Leib			BotMon.t._callInit(this);
1609f1ee8c1SSascha Leib		},
161f125bc8dSSascha Leib
1629f1ee8c1SSascha Leib		// this will be called when the known json files are done loading:
1639f1ee8c1SSascha Leib		_dispatch: function(file) {
1647bd08c30SSascha Leib			//console.info('BotMon.live.data._dispatch(,',file,')');
1659f1ee8c1SSascha Leib
1669f1ee8c1SSascha Leib			// shortcut to make code more readable:
1677bd08c30SSascha Leib			const data = BotMon.live.data;
1689f1ee8c1SSascha Leib
1699f1ee8c1SSascha Leib			// set the flags:
1709f1ee8c1SSascha Leib			switch(file) {
1715526d629SSascha Leib				case 'rules':
1725526d629SSascha Leib					data._dispatchRulesLoaded = true;
1735526d629SSascha Leib					break;
1749f1ee8c1SSascha Leib				case 'bots':
1759f1ee8c1SSascha Leib					data._dispatchBotsLoaded = true;
1769f1ee8c1SSascha Leib					break;
1779f1ee8c1SSascha Leib				case 'clients':
1789f1ee8c1SSascha Leib					data._dispatchClientsLoaded = true;
1799f1ee8c1SSascha Leib					break;
1809f1ee8c1SSascha Leib				case 'platforms':
1819f1ee8c1SSascha Leib					data._dispatchPlatformsLoaded = true;
1829f1ee8c1SSascha Leib					break;
1839f1ee8c1SSascha Leib				default:
1849f1ee8c1SSascha Leib					// ignore
1859f1ee8c1SSascha Leib			}
1869f1ee8c1SSascha Leib
1879f1ee8c1SSascha Leib			// are all the flags set?
188b82cba27SSascha Leib			if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded && data._dispatchRulesLoaded) {
1899f1ee8c1SSascha Leib				// chain the log files loading:
1907bd08c30SSascha Leib				BotMon.live.data.loadLogFile('srv', BotMon.live.data._onServerLogLoaded);
1919f1ee8c1SSascha Leib			}
1922f2bc93aSSascha Leib		},
1939f1ee8c1SSascha Leib		// flags to track which data files have been loaded:
1949f1ee8c1SSascha Leib		_dispatchBotsLoaded: false,
1959f1ee8c1SSascha Leib		_dispatchClientsLoaded: false,
1969f1ee8c1SSascha Leib		_dispatchPlatformsLoaded: false,
197b82cba27SSascha Leib		_dispatchRulesLoaded: false,
1982f2bc93aSSascha Leib
1999f1ee8c1SSascha Leib		// event callback, after the server log has been loaded:
2002f2bc93aSSascha Leib		_onServerLogLoaded: function() {
2017bd08c30SSascha Leib			//console.info('BotMon.live.data._onServerLogLoaded()');
2022f2bc93aSSascha Leib
2039f1ee8c1SSascha Leib			// chain the client log file to load:
2047bd08c30SSascha Leib			BotMon.live.data.loadLogFile('log', BotMon.live.data._onClientLogLoaded);
2052f2bc93aSSascha Leib		},
2062f2bc93aSSascha Leib
2079f1ee8c1SSascha Leib		// event callback, after the client log has been loaded:
2082f2bc93aSSascha Leib		_onClientLogLoaded: function() {
20993a5b18bSSascha Leib			//console.info('BotMon.live.data._onClientLogLoaded()');
2102f2bc93aSSascha Leib
2119f1ee8c1SSascha Leib			// chain the ticks file to load:
2127bd08c30SSascha Leib			BotMon.live.data.loadLogFile('tck', BotMon.live.data._onTicksLogLoaded);
2132f2bc93aSSascha Leib
2149f1ee8c1SSascha Leib		},
2159f1ee8c1SSascha Leib
2169f1ee8c1SSascha Leib		// event callback, after the tiker log has been loaded:
2179f1ee8c1SSascha Leib		_onTicksLogLoaded: function() {
21893a5b18bSSascha Leib			//console.info('BotMon.live.data._onTicksLogLoaded()');
2199f1ee8c1SSascha Leib
2209f1ee8c1SSascha Leib			// analyse the data:
2217bd08c30SSascha Leib			BotMon.live.data.analytics.analyseAll();
2229f1ee8c1SSascha Leib
2239f1ee8c1SSascha Leib			// sort the data:
2249f1ee8c1SSascha Leib			// #TODO
2259f1ee8c1SSascha Leib
2269f1ee8c1SSascha Leib			// display the data:
2277bd08c30SSascha Leib			BotMon.live.gui.overview.make();
2289f1ee8c1SSascha Leib
22993a5b18bSSascha Leib			//console.log(BotMon.live.data.model._visitors);
2309f1ee8c1SSascha Leib
2312f2bc93aSSascha Leib		},
2322f2bc93aSSascha Leib
2332f2bc93aSSascha Leib		model: {
2349f1ee8c1SSascha Leib			// visitors storage:
2352f2bc93aSSascha Leib			_visitors: [],
2362f2bc93aSSascha Leib
2372f2bc93aSSascha Leib			// find an already existing visitor record:
238f4417fdeSSascha Leib			findVisitor: function(visitor) {
239f4417fdeSSascha Leib				//console.info('BotMon.live.data.model.findVisitor()');
240f4417fdeSSascha Leib				//console.log(visitor);
2412f2bc93aSSascha Leib
2422f2bc93aSSascha Leib				// shortcut to make code more readable:
2437bd08c30SSascha Leib				const model = BotMon.live.data.model;
2442f2bc93aSSascha Leib
245446aa816SSascha Leib				const timeout = 60 * 60 * 1000; /* session timeout: One hour */
246446aa816SSascha Leib
2472f2bc93aSSascha Leib				// loop over all visitors already registered:
2482f2bc93aSSascha Leib				for (let i=0; i<model._visitors.length; i++) {
2492f2bc93aSSascha Leib					const v = model._visitors[i];
250f4417fdeSSascha Leib
251f4417fdeSSascha Leib					if (visitor._type == BM_USERTYPE.KNOWN_BOT) { /* known bots */
252f4417fdeSSascha Leib
253f4417fdeSSascha Leib						// bots match when their ID matches:
254f4417fdeSSascha Leib						if (v._bot && v._bot.id == visitor._bot.id) {
255f4417fdeSSascha Leib							return v;
256f4417fdeSSascha Leib						}
257f4417fdeSSascha Leib
258f4417fdeSSascha Leib					} else if (visitor._type == BM_USERTYPE.KNOWN_USER) { /* registered users */
259f4417fdeSSascha Leib
260f4417fdeSSascha Leib						// visitors match when their names match:
261f4417fdeSSascha Leib						if ( v.usr == visitor.usr
262f4417fdeSSascha Leib						&& v.ip == visitor.ip
263f4417fdeSSascha Leib						&& v.agent == visitor.agent) {
264f4417fdeSSascha Leib							return v;
265f4417fdeSSascha Leib						}
266f4417fdeSSascha Leib					} else { /* any other visitor */
267f4417fdeSSascha Leib
2685d78a53bSSascha Leib						if (Math.abs(v._lastSeen - visitor.ts) < timeout) { /* ignore timed out visits */
269f4417fdeSSascha Leib							if ( v.id == visitor.id) { /* match the pre-defined IDs */
270f4417fdeSSascha Leib								return v;
271259d3b85SSascha Leib							} else if (v.ip == visitor.ip && v.agent == visitor.agent) {
272451abfadSSascha Leib								if (v.typ !== 'ip') {
2735526d629SSascha Leib									console.warn(`Visitor ID “${v.id}” not found, using matchin IP + User-Agent instead.`);
274451abfadSSascha Leib								}
275259d3b85SSascha Leib								return v;
276f4417fdeSSascha Leib							}
277f4417fdeSSascha Leib						}
2785d78a53bSSascha Leib
2792f2bc93aSSascha Leib					}
280446aa816SSascha Leib				}
2812f2bc93aSSascha Leib				return null; // nothing found
2822f2bc93aSSascha Leib			},
2832f2bc93aSSascha Leib
284259d3b85SSascha Leib			/* if there is already this visit registered, return the page view item */
285259d3b85SSascha Leib			_getPageView: function(visit, view) {
2862f2bc93aSSascha Leib
287294f6af8SSascha Leib				// shortcut to make code more readable:
2887bd08c30SSascha Leib				const model = BotMon.live.data.model;
289294f6af8SSascha Leib
2902f2bc93aSSascha Leib				for (let i=0; i<visit._pageViews.length; i++) {
291294f6af8SSascha Leib					const pv = visit._pageViews[i];
292259d3b85SSascha Leib					if (pv.pg == view.pg) {
293259d3b85SSascha Leib						return pv;
2942f2bc93aSSascha Leib					}
2952f2bc93aSSascha Leib				}
2962f2bc93aSSascha Leib				return null; // not found
2972f2bc93aSSascha Leib			},
2982f2bc93aSSascha Leib
2992f2bc93aSSascha Leib			// register a new visitor (or update if already exists)
300f4417fdeSSascha Leib			registerVisit: function(nv, type) {
301f4417fdeSSascha Leib				//console.info('registerVisit', nv, type);
3022f2bc93aSSascha Leib
3032f2bc93aSSascha Leib				// shortcut to make code more readable:
3047bd08c30SSascha Leib				const model = BotMon.live.data.model;
3052f2bc93aSSascha Leib
30643d9de6bSSascha Leib				// is it a known bot?
307f4417fdeSSascha Leib				const bot = BotMon.live.data.bots.match(nv.agent);
3089f1ee8c1SSascha Leib
309f4417fdeSSascha Leib				// enrich new visitor with relevant data:
310f4417fdeSSascha Leib				if (!nv._bot) nv._bot = bot ?? null; // bot info
311259d3b85SSascha Leib				nv._type = ( bot ? BM_USERTYPE.KNOWN_BOT : ( nv.usr && nv.usr !== '' ? BM_USERTYPE.KNOWN_USER : BM_USERTYPE.UNKNOWN ) );
312f4417fdeSSascha Leib				if (!nv._firstSeen) nv._firstSeen = nv.ts;
313259d3b85SSascha Leib				nv._lastSeen = nv.ts;
31443d9de6bSSascha Leib
315abfc901fSSascha Leib				// check if it already exists:
316f4417fdeSSascha Leib				let visitor = model.findVisitor(nv);
317abfc901fSSascha Leib				if (!visitor) {
318f4417fdeSSascha Leib					visitor = nv;
31943d9de6bSSascha Leib					visitor._seenBy = [type];
3202f2bc93aSSascha Leib					visitor._pageViews = []; // array of page views
3212f2bc93aSSascha Leib					visitor._hasReferrer = false; // has at least one referrer
3222f2bc93aSSascha Leib					visitor._jsClient = false; // visitor has been seen logged by client js as well
323f4417fdeSSascha Leib					visitor._client = BotMon.live.data.clients.match(nv.agent) ?? null; // client info
324f4417fdeSSascha Leib					visitor._platform = BotMon.live.data.platforms.match(nv.agent); // platform info
325f4417fdeSSascha Leib					model._visitors.push(visitor);
326451abfadSSascha Leib				} else { // update existing
327446aa816SSascha Leib					if (visitor._firstSeen > nv.ts) {
328451abfadSSascha Leib						visitor._firstSeen = nv.ts;
329451abfadSSascha Leib					}
3302f2bc93aSSascha Leib				}
3312f2bc93aSSascha Leib
3329f1ee8c1SSascha Leib				// find browser
3339f1ee8c1SSascha Leib
3342f2bc93aSSascha Leib				// is this visit already registered?
335259d3b85SSascha Leib				let prereg = model._getPageView(visitor, nv);
3362f2bc93aSSascha Leib				if (!prereg) {
337259d3b85SSascha Leib					// add new page view:
338259d3b85SSascha Leib					prereg = model._makePageView(nv, type);
3392f2bc93aSSascha Leib					visitor._pageViews.push(prereg);
340259d3b85SSascha Leib				} else {
341259d3b85SSascha Leib					// update last seen date
342259d3b85SSascha Leib					prereg._lastSeen = nv.ts;
343259d3b85SSascha Leib					// increase view count:
344259d3b85SSascha Leib					prereg._viewCount += 1;
345446aa816SSascha Leib					prereg._tickCount += 1;
3462f2bc93aSSascha Leib				}
3472f2bc93aSSascha Leib
3482f2bc93aSSascha Leib				// update referrer state:
3492f2bc93aSSascha Leib				visitor._hasReferrer = visitor._hasReferrer ||
3502f2bc93aSSascha Leib					(prereg.ref !== undefined && prereg.ref !== '');
3512f2bc93aSSascha Leib
3522f2bc93aSSascha Leib				// update time stamp for last-seen:
353451abfadSSascha Leib				if (visitor._lastSeen < nv.ts) {
354f4417fdeSSascha Leib					visitor._lastSeen = nv.ts;
355451abfadSSascha Leib				}
3569f1ee8c1SSascha Leib
3579f1ee8c1SSascha Leib				// if needed:
3589f1ee8c1SSascha Leib				return visitor;
3592f2bc93aSSascha Leib			},
3602f2bc93aSSascha Leib
3612f2bc93aSSascha Leib			// updating visit data from the client-side log:
3622f2bc93aSSascha Leib			updateVisit: function(dat) {
3639f1ee8c1SSascha Leib				//console.info('updateVisit', dat);
3642f2bc93aSSascha Leib
3652f2bc93aSSascha Leib				// shortcut to make code more readable:
3667bd08c30SSascha Leib				const model = BotMon.live.data.model;
3672f2bc93aSSascha Leib
36843d9de6bSSascha Leib				const type = 'log';
36943d9de6bSSascha Leib
370f4417fdeSSascha Leib				let visitor = BotMon.live.data.model.findVisitor(dat);
3719f1ee8c1SSascha Leib				if (!visitor) {
37243d9de6bSSascha Leib					visitor = model.registerVisit(dat, type);
3739f1ee8c1SSascha Leib				}
3742f2bc93aSSascha Leib				if (visitor) {
37543d9de6bSSascha Leib
376446aa816SSascha Leib					if (visitor._lastSeen < dat.ts) {
3772f2bc93aSSascha Leib						visitor._lastSeen = dat.ts;
378446aa816SSascha Leib					}
379259d3b85SSascha Leib					if (!visitor._seenBy.includes(type)) {
380259d3b85SSascha Leib						visitor._seenBy.push(type);
381259d3b85SSascha Leib					}
3822f2bc93aSSascha Leib					visitor._jsClient = true; // seen by client js
3832f2bc93aSSascha Leib				}
3842f2bc93aSSascha Leib
3852f2bc93aSSascha Leib				// find the page view:
386259d3b85SSascha Leib				let prereg = BotMon.live.data.model._getPageView(visitor, dat);
3872f2bc93aSSascha Leib				if (prereg) {
3882f2bc93aSSascha Leib					// update the page view:
3892f2bc93aSSascha Leib					prereg._lastSeen = dat.ts;
390259d3b85SSascha Leib					if (!prereg._seenBy.includes(type)) prereg._seenBy.push(type);
3912f2bc93aSSascha Leib					prereg._jsClient = true; // seen by client js
3922f2bc93aSSascha Leib				} else {
3932f2bc93aSSascha Leib					// add the page view to the visitor:
394259d3b85SSascha Leib					prereg = model._makePageView(dat, type);
3952f2bc93aSSascha Leib					visitor._pageViews.push(prereg);
3962f2bc93aSSascha Leib				}
397446aa816SSascha Leib				prereg._tickCount += 1;
3989f1ee8c1SSascha Leib			},
3999f1ee8c1SSascha Leib
4009f1ee8c1SSascha Leib			// updating visit data from the ticker log:
4019f1ee8c1SSascha Leib			updateTicks: function(dat) {
402294f6af8SSascha Leib				//console.info('updateTicks', dat);
4039f1ee8c1SSascha Leib
4049f1ee8c1SSascha Leib				// shortcut to make code more readable:
4057bd08c30SSascha Leib				const model = BotMon.live.data.model;
4069f1ee8c1SSascha Leib
407259d3b85SSascha Leib				const type = 'tck';
408259d3b85SSascha Leib
4099f1ee8c1SSascha Leib				// find the visit info:
410f4417fdeSSascha Leib				let visitor = model.findVisitor(dat);
411294f6af8SSascha Leib				if (!visitor) {
4125526d629SSascha Leib					console.info(`No visitor with ID “${dat.id}” found, registering as a new one.`);
413259d3b85SSascha Leib					visitor = model.registerVisit(dat, type);
414294f6af8SSascha Leib				}
4159f1ee8c1SSascha Leib				if (visitor) {
41643d9de6bSSascha Leib					// update visitor:
417294f6af8SSascha Leib					if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts;
418259d3b85SSascha Leib					if (!visitor._seenBy.includes(type)) visitor._seenBy.push(type);
419294f6af8SSascha Leib
420294f6af8SSascha Leib					// get the page view info:
421259d3b85SSascha Leib					let pv = model._getPageView(visitor, dat);
422259d3b85SSascha Leib					if (!pv) {
423446aa816SSascha Leib						console.warn(`No page view for visit ID “${dat.id}”, page “${dat.pg}”, registering a new one.`);
424259d3b85SSascha Leib						pv = model._makePageView(dat, type);
425259d3b85SSascha Leib						visitor._pageViews.push(pv);
426259d3b85SSascha Leib					}
427294f6af8SSascha Leib
428259d3b85SSascha Leib					// update the page view info:
429259d3b85SSascha Leib					if (!pv._seenBy.includes(type)) pv._seenBy.push(type);
430259d3b85SSascha Leib					if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts;
431259d3b85SSascha Leib					pv._tickCount += 1;
432259d3b85SSascha Leib
433259d3b85SSascha Leib				}
434259d3b85SSascha Leib			},
435259d3b85SSascha Leib
436259d3b85SSascha Leib			// helper function to create a new "page view" item:
437259d3b85SSascha Leib			_makePageView: function(data, type) {
4385526d629SSascha Leib
4395526d629SSascha Leib				// try to parse the referrer:
4405526d629SSascha Leib				let rUrl = null;
4415526d629SSascha Leib				try {
4425526d629SSascha Leib					rUrl = ( data.ref && data.ref !== '' ? new URL(data.ref) : null );
4435526d629SSascha Leib				} catch (e) {
4445526d629SSascha Leib					console.info(`Invalid referer: “${data.ref}”.`);
4455526d629SSascha Leib				}
4465526d629SSascha Leib
447259d3b85SSascha Leib				return {
448259d3b85SSascha Leib					_by: type,
449259d3b85SSascha Leib					ip: data.ip,
450259d3b85SSascha Leib					pg: data.pg,
4515526d629SSascha Leib					_ref: rUrl,
452259d3b85SSascha Leib					_firstSeen: data.ts,
453259d3b85SSascha Leib					_lastSeen: data.ts,
454259d3b85SSascha Leib					_seenBy: [type],
455259d3b85SSascha Leib					_jsClient: ( type !== 'srv'),
456259d3b85SSascha Leib					_viewCount: 1,
457259d3b85SSascha Leib					_tickCount: 0
458294f6af8SSascha Leib				};
4592f2bc93aSSascha Leib			}
460f125bc8dSSascha Leib		},
461f125bc8dSSascha Leib
462294f6af8SSascha Leib		analytics: {
463294f6af8SSascha Leib
464294f6af8SSascha Leib			init: function() {
465f4417fdeSSascha Leib				//console.info('BotMon.live.data.analytics.init()');
466294f6af8SSascha Leib			},
467294f6af8SSascha Leib
468294f6af8SSascha Leib			// data storage:
469294f6af8SSascha Leib			data: {
470294f6af8SSascha Leib				totalVisits: 0,
471294f6af8SSascha Leib				totalPageViews: 0,
472294f6af8SSascha Leib				bots: {
473294f6af8SSascha Leib					known: 0,
47493a5b18bSSascha Leib					suspected: 0,
4759bc80cc5SSascha Leib					human: 0,
4769bc80cc5SSascha Leib					users: 0
477294f6af8SSascha Leib				}
478294f6af8SSascha Leib			},
479294f6af8SSascha Leib
480294f6af8SSascha Leib			// sort the visits by type:
481294f6af8SSascha Leib			groups: {
482294f6af8SSascha Leib				knownBots: [],
48393a5b18bSSascha Leib				suspectedBots: [],
484294f6af8SSascha Leib				humans: [],
485294f6af8SSascha Leib				users: []
486294f6af8SSascha Leib			},
487294f6af8SSascha Leib
488294f6af8SSascha Leib			// all analytics
489294f6af8SSascha Leib			analyseAll: function() {
4907bd08c30SSascha Leib				//console.info('BotMon.live.data.analytics.analyseAll()');
491294f6af8SSascha Leib
492294f6af8SSascha Leib				// shortcut to make code more readable:
4937bd08c30SSascha Leib				const model = BotMon.live.data.model;
494446aa816SSascha Leib				const me = BotMon.live.data.analytics;
495294f6af8SSascha Leib
496451abfadSSascha Leib				BotMon.live.gui.status.showBusy("Analysing data …");
497451abfadSSascha Leib
498294f6af8SSascha Leib				// loop over all visitors:
499294f6af8SSascha Leib				model._visitors.forEach( (v) => {
500294f6af8SSascha Leib
501294f6af8SSascha Leib					// count visits and page views:
502294f6af8SSascha Leib					this.data.totalVisits += 1;
503294f6af8SSascha Leib					this.data.totalPageViews += v._pageViews.length;
504294f6af8SSascha Leib
505294f6af8SSascha Leib					// check for typical bot aspects:
50643d9de6bSSascha Leib					let botScore = 0;
507294f6af8SSascha Leib
508f4417fdeSSascha Leib					if (v._type == BM_USERTYPE.KNOWN_BOT) { // known bots
509294f6af8SSascha Leib
510b82cba27SSascha Leib						this.data.bots.known += v._pageViews.length;
511294f6af8SSascha Leib						this.groups.knownBots.push(v);
512294f6af8SSascha Leib
513f4417fdeSSascha Leib					} else if (v._type == BM_USERTYPE.KNOWN_USER) { // known users */
514f4417fdeSSascha Leib
515b82cba27SSascha Leib						this.data.bots.users += v._pageViews.length;
516294f6af8SSascha Leib						this.groups.users.push(v);
517294f6af8SSascha Leib
518f4417fdeSSascha Leib					} else {
519294f6af8SSascha Leib
520b82cba27SSascha Leib						// get evaluation:
521b82cba27SSascha Leib						const e = BotMon.live.data.rules.evaluate(v);
522b82cba27SSascha Leib						v._eval = e.rules;
523b82cba27SSascha Leib						v._botVal = e.val;
524b82cba27SSascha Leib
525446aa816SSascha Leib						// add each page view to IP range information (unless it is already from a known bot IP range):
526446aa816SSascha Leib						v._pageViews.forEach( pv => {
527446aa816SSascha Leib							me._addToIPRanges(pv.ip);
528446aa816SSascha Leib						});
529446aa816SSascha Leib
530b82cba27SSascha Leib						if (e.isBot) { // likely bots
531b82cba27SSascha Leib							v._type = BM_USERTYPE.LIKELY_BOT;
532b82cba27SSascha Leib							this.data.bots.suspected += v._pageViews.length;
53393a5b18bSSascha Leib							this.groups.suspectedBots.push(v);
534b82cba27SSascha Leib						} else { // probably humans
535b82cba27SSascha Leib							v._type = BM_USERTYPE.HUMAN;
536b82cba27SSascha Leib							this.data.bots.human += v._pageViews.length;
537b82cba27SSascha Leib							this.groups.humans.push(v);
538b82cba27SSascha Leib						}
539b82cba27SSascha Leib						// TODO: find suspected bots
540f4417fdeSSascha Leib
541294f6af8SSascha Leib					}
542294f6af8SSascha Leib				});
543294f6af8SSascha Leib
5445d78a53bSSascha Leib				// clean up the ip ranges:
5455d78a53bSSascha Leib				me._cleanIPRanges();
546446aa816SSascha Leib				console.log(BotMon.live.data.analytics._ipRange);
547446aa816SSascha Leib
5485d78a53bSSascha Leib				BotMon.live.gui.status.hideBusy('Done.');
549446aa816SSascha Leib			},
550446aa816SSascha Leib
551446aa816SSascha Leib			// visits from IP ranges:
552446aa816SSascha Leib			_ipRange: {
553446aa816SSascha Leib				ip4: [],
554446aa816SSascha Leib				ip6: []
555446aa816SSascha Leib			},
556446aa816SSascha Leib			/**
557446aa816SSascha Leib			 * Adds a visit to the IP range statistics.
558446aa816SSascha Leib			 *
559446aa816SSascha Leib			 * This helps to identify IP ranges that are used by bots.
560446aa816SSascha Leib			 *
561446aa816SSascha Leib			 * @param {string} ip The IP address to add.
562446aa816SSascha Leib			 */
563446aa816SSascha Leib			_addToIPRanges: function(ip) {
564446aa816SSascha Leib
565446aa816SSascha Leib				const me = BotMon.live.data.analytics;
566446aa816SSascha Leib				const ipv = (ip.indexOf(':') > 0 ? 6 : 4);
567446aa816SSascha Leib
568446aa816SSascha Leib				const ipArr = ip.split( ipv == 6 ? ':' : '.');
569446aa816SSascha Leib				const maxSegments = (ipv == 6 ? 4 : 3);
570446aa816SSascha Leib
571446aa816SSascha Leib				let arr = (ipv == 6 ? me._ipRange.ip6 : me._ipRange.ip4);
5725d78a53bSSascha Leib
5735d78a53bSSascha Leib				// find any existing segment entry:
5745d78a53bSSascha Leib				it = null;
5755d78a53bSSascha Leib				for (let i=0; i < arr.length; i++) {
5765d78a53bSSascha Leib					const sig = arr[i];
5775d78a53bSSascha Leib					if (sig.seg == ipArr[0]) {
5785d78a53bSSascha Leib						it = sig;
5795d78a53bSSascha Leib						break;
5805d78a53bSSascha Leib					}
5815d78a53bSSascha Leib				}
5825d78a53bSSascha Leib
5835d78a53bSSascha Leib				// create if not found:
584446aa816SSascha Leib				if (!it) {
5855d78a53bSSascha Leib					it = {seg: ipArr[0], count: 1};
5865d78a53bSSascha Leib					//if (i<maxSegments) it.sub = [];
587446aa816SSascha Leib					arr.push(it);
5885d78a53bSSascha Leib
5895d78a53bSSascha Leib				} else { // increase count:
5905d78a53bSSascha Leib
591446aa816SSascha Leib					it.count += 1;
592446aa816SSascha Leib				}
5935d78a53bSSascha Leib
594446aa816SSascha Leib			},
595446aa816SSascha Leib			_cleanIPRanges: function() {
596446aa816SSascha Leib				const me = BotMon.live.data.analytics;
597446aa816SSascha Leib
5985d78a53bSSascha Leib				for (const [n, arr] of Object.entries(me._ipRange)) {
599451abfadSSascha Leib
6005d78a53bSSascha Leib					arr.forEach( (it, i) => {
601fe164fdaSSascha Leib						if (it.count <= 1) arr.splice(i, 1);
6025d78a53bSSascha Leib					});
6035d78a53bSSascha Leib
6045d78a53bSSascha Leib				};
605446aa816SSascha Leib			}
606294f6af8SSascha Leib		},
607294f6af8SSascha Leib
608f125bc8dSSascha Leib		bots: {
609f125bc8dSSascha Leib			// loads the list of known bots from a JSON file:
610f125bc8dSSascha Leib			init: async function() {
6117bd08c30SSascha Leib				//console.info('BotMon.live.data.bots.init()');
612f125bc8dSSascha Leib
613f125bc8dSSascha Leib				// Load the list of known bots:
6147bd08c30SSascha Leib				BotMon.live.gui.status.showBusy("Loading known bots …");
6157bd08c30SSascha Leib				const url = BotMon._baseDir + 'data/known-bots.json';
616f125bc8dSSascha Leib				try {
617f125bc8dSSascha Leib					const response = await fetch(url);
618f125bc8dSSascha Leib					if (!response.ok) {
619f125bc8dSSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
620f125bc8dSSascha Leib					}
621f125bc8dSSascha Leib
62243d9de6bSSascha Leib					this._list = await response.json();
62343d9de6bSSascha Leib					this._ready = true;
624f125bc8dSSascha Leib
625f125bc8dSSascha Leib				} catch (error) {
6267bd08c30SSascha Leib					BotMon.live.gui.status.setError("Error while loading the ‘known bots’ file: " + error.message);
627f125bc8dSSascha Leib				} finally {
6287bd08c30SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
6297bd08c30SSascha Leib					BotMon.live.data._dispatch('bots')
630f125bc8dSSascha Leib				}
631f125bc8dSSascha Leib			},
632f125bc8dSSascha Leib
633f125bc8dSSascha Leib			// returns bot info if the clientId matches a known bot, null otherwise:
63443d9de6bSSascha Leib			match: function(agent) {
63543d9de6bSSascha Leib				//console.info('BotMon.live.data.bots.match(',agent,')');
636f125bc8dSSascha Leib
63743d9de6bSSascha Leib				const BotList = BotMon.live.data.bots._list;
63843d9de6bSSascha Leib
63943d9de6bSSascha Leib				// default is: not found!
64043d9de6bSSascha Leib				let botInfo = null;
64143d9de6bSSascha Leib
642fa1532fcSSascha Leib				if (!agent) return null;
643fa1532fcSSascha Leib
64443d9de6bSSascha Leib				// check for known bots:
64543d9de6bSSascha Leib				BotList.find(bot => {
64643d9de6bSSascha Leib					let r = false;
6479f1ee8c1SSascha Leib					for (let j=0; j<bot.rx.length; j++) {
64843d9de6bSSascha Leib						const rxr = agent.match(new RegExp(bot.rx[j]));
64943d9de6bSSascha Leib						if (rxr) {
65043d9de6bSSascha Leib							botInfo = {
65143d9de6bSSascha Leib								n : bot.n,
65243d9de6bSSascha Leib								id: bot.id,
65343d9de6bSSascha Leib								url: bot.url,
65443d9de6bSSascha Leib								v: (rxr.length > 1 ? rxr[1] : -1)
6550359f250SSascha Leib							};
65643d9de6bSSascha Leib							r = true;
65743d9de6bSSascha Leib							break;
658446aa816SSascha Leib						};
6590359f250SSascha Leib					};
66043d9de6bSSascha Leib					return r;
66143d9de6bSSascha Leib				});
662259d3b85SSascha Leib
663259d3b85SSascha Leib				// check for unknown bots:
664259d3b85SSascha Leib				if (!botInfo) {
665259d3b85SSascha Leib					const botmatch = agent.match(/[^\s](\w*bot)[\/\s;\),$]/i);
666259d3b85SSascha Leib					if(botmatch) {
667259d3b85SSascha Leib						botInfo = {'id': "other", 'n': "Other", "bot": botmatch[0] };
668259d3b85SSascha Leib					}
6699f1ee8c1SSascha Leib				}
67043d9de6bSSascha Leib
67143d9de6bSSascha Leib				//console.log("botInfo:", botInfo);
67243d9de6bSSascha Leib				return botInfo;
673f125bc8dSSascha Leib			},
674f125bc8dSSascha Leib
67543d9de6bSSascha Leib
676f125bc8dSSascha Leib			// indicates if the list is loaded and ready to use:
677f125bc8dSSascha Leib			_ready: false,
678f125bc8dSSascha Leib
679f125bc8dSSascha Leib			// the actual bot list is stored here:
680f125bc8dSSascha Leib			_list: []
681f125bc8dSSascha Leib		},
682f125bc8dSSascha Leib
6839f1ee8c1SSascha Leib		clients: {
6849f1ee8c1SSascha Leib			// loads the list of known clients from a JSON file:
6859f1ee8c1SSascha Leib			init: async function() {
6867bd08c30SSascha Leib				//console.info('BotMon.live.data.clients.init()');
6879f1ee8c1SSascha Leib
6889f1ee8c1SSascha Leib				// Load the list of known bots:
6897bd08c30SSascha Leib				BotMon.live.gui.status.showBusy("Loading known clients");
6907bd08c30SSascha Leib				const url = BotMon._baseDir + 'data/known-clients.json';
6919f1ee8c1SSascha Leib				try {
6929f1ee8c1SSascha Leib					const response = await fetch(url);
6939f1ee8c1SSascha Leib					if (!response.ok) {
6949f1ee8c1SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
6959f1ee8c1SSascha Leib					}
6969f1ee8c1SSascha Leib
6977bd08c30SSascha Leib					BotMon.live.data.clients._list = await response.json();
6987bd08c30SSascha Leib					BotMon.live.data.clients._ready = true;
6999f1ee8c1SSascha Leib
7009f1ee8c1SSascha Leib				} catch (error) {
7017bd08c30SSascha Leib					BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message);
7029f1ee8c1SSascha Leib				} finally {
7037bd08c30SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
7047bd08c30SSascha Leib					BotMon.live.data._dispatch('clients')
7059f1ee8c1SSascha Leib				}
7069f1ee8c1SSascha Leib			},
7079f1ee8c1SSascha Leib
70893a5b18bSSascha Leib			// returns bot info if the user-agent matches a known bot, null otherwise:
70943d9de6bSSascha Leib			match: function(agent) {
71043d9de6bSSascha Leib				//console.info('BotMon.live.data.clients.match(',agent,')');
7119f1ee8c1SSascha Leib
7129f1ee8c1SSascha Leib				let match = {"n": "Unknown", "v": -1, "id": null};
7139f1ee8c1SSascha Leib
71443d9de6bSSascha Leib				if (agent) {
7157bd08c30SSascha Leib					BotMon.live.data.clients._list.find(client => {
7169f1ee8c1SSascha Leib						let r = false;
7179f1ee8c1SSascha Leib						for (let j=0; j<client.rx.length; j++) {
71843d9de6bSSascha Leib							const rxr = agent.match(new RegExp(client.rx[j]));
7199f1ee8c1SSascha Leib							if (rxr) {
7209f1ee8c1SSascha Leib								match.n = client.n;
7219f1ee8c1SSascha Leib								match.v = (rxr.length > 1 ? rxr[1] : -1);
7229f1ee8c1SSascha Leib								match.id = client.id || null;
7239f1ee8c1SSascha Leib								r = true;
7249f1ee8c1SSascha Leib								break;
7259f1ee8c1SSascha Leib							}
7269f1ee8c1SSascha Leib						}
7279f1ee8c1SSascha Leib						return r;
7289f1ee8c1SSascha Leib					});
7299f1ee8c1SSascha Leib				}
7309f1ee8c1SSascha Leib
73143d9de6bSSascha Leib				//console.log(match)
7329f1ee8c1SSascha Leib				return match;
7339f1ee8c1SSascha Leib			},
7349f1ee8c1SSascha Leib
7359f1ee8c1SSascha Leib			// indicates if the list is loaded and ready to use:
7369f1ee8c1SSascha Leib			_ready: false,
7379f1ee8c1SSascha Leib
7389f1ee8c1SSascha Leib			// the actual bot list is stored here:
7399f1ee8c1SSascha Leib			_list: []
7409f1ee8c1SSascha Leib
7419f1ee8c1SSascha Leib		},
7429f1ee8c1SSascha Leib
7439f1ee8c1SSascha Leib		platforms: {
7449f1ee8c1SSascha Leib			// loads the list of known platforms from a JSON file:
7459f1ee8c1SSascha Leib			init: async function() {
7467bd08c30SSascha Leib				//console.info('BotMon.live.data.platforms.init()');
7479f1ee8c1SSascha Leib
7489f1ee8c1SSascha Leib				// Load the list of known bots:
7497bd08c30SSascha Leib				BotMon.live.gui.status.showBusy("Loading known platforms");
7507bd08c30SSascha Leib				const url = BotMon._baseDir + 'data/known-platforms.json';
7519f1ee8c1SSascha Leib				try {
7529f1ee8c1SSascha Leib					const response = await fetch(url);
7539f1ee8c1SSascha Leib					if (!response.ok) {
7549f1ee8c1SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
7559f1ee8c1SSascha Leib					}
7569f1ee8c1SSascha Leib
7577bd08c30SSascha Leib					BotMon.live.data.platforms._list = await response.json();
7587bd08c30SSascha Leib					BotMon.live.data.platforms._ready = true;
7599f1ee8c1SSascha Leib
7609f1ee8c1SSascha Leib				} catch (error) {
7617bd08c30SSascha Leib					BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message);
7629f1ee8c1SSascha Leib				} finally {
7637bd08c30SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
7647bd08c30SSascha Leib					BotMon.live.data._dispatch('platforms')
7659f1ee8c1SSascha Leib				}
7669f1ee8c1SSascha Leib			},
7679f1ee8c1SSascha Leib
7689f1ee8c1SSascha Leib			// returns bot info if the browser id matches a known platform:
7699f1ee8c1SSascha Leib			match: function(cid) {
7707bd08c30SSascha Leib				//console.info('BotMon.live.data.platforms.match(',cid,')');
7719f1ee8c1SSascha Leib
7729f1ee8c1SSascha Leib				let match = {"n": "Unknown", "id": null};
7739f1ee8c1SSascha Leib
7749f1ee8c1SSascha Leib				if (cid) {
7757bd08c30SSascha Leib					BotMon.live.data.platforms._list.find(platform => {
7769f1ee8c1SSascha Leib						let r = false;
7779f1ee8c1SSascha Leib						for (let j=0; j<platform.rx.length; j++) {
7789f1ee8c1SSascha Leib							const rxr = cid.match(new RegExp(platform.rx[j]));
7799f1ee8c1SSascha Leib							if (rxr) {
7809f1ee8c1SSascha Leib								match.n = platform.n;
7819f1ee8c1SSascha Leib								match.v = (rxr.length > 1 ? rxr[1] : -1);
7829f1ee8c1SSascha Leib								match.id = platform.id || null;
7839f1ee8c1SSascha Leib								r = true;
7849f1ee8c1SSascha Leib								break;
7859f1ee8c1SSascha Leib							}
7869f1ee8c1SSascha Leib						}
7879f1ee8c1SSascha Leib						return r;
7889f1ee8c1SSascha Leib					});
7899f1ee8c1SSascha Leib				}
7909f1ee8c1SSascha Leib
7919f1ee8c1SSascha Leib				return match;
7929f1ee8c1SSascha Leib			},
7939f1ee8c1SSascha Leib
7949f1ee8c1SSascha Leib			// indicates if the list is loaded and ready to use:
7959f1ee8c1SSascha Leib			_ready: false,
7969f1ee8c1SSascha Leib
7979f1ee8c1SSascha Leib			// the actual bot list is stored here:
7989f1ee8c1SSascha Leib			_list: []
7999f1ee8c1SSascha Leib
8009f1ee8c1SSascha Leib		},
8019f1ee8c1SSascha Leib
802b82cba27SSascha Leib		rules: {
803b82cba27SSascha Leib			// loads the list of rules and settings from a JSON file:
804b82cba27SSascha Leib			init: async function() {
805b82cba27SSascha Leib				//console.info('BotMon.live.data.rules.init()');
806b82cba27SSascha Leib
807b82cba27SSascha Leib				// Load the list of known bots:
808b82cba27SSascha Leib				BotMon.live.gui.status.showBusy("Loading list of rules …");
809b82cba27SSascha Leib				const url = BotMon._baseDir + 'data/rules.json';
810b82cba27SSascha Leib				try {
811b82cba27SSascha Leib					const response = await fetch(url);
812b82cba27SSascha Leib					if (!response.ok) {
813b82cba27SSascha Leib						throw new Error(`${response.status} ${response.statusText}`);
814b82cba27SSascha Leib					}
815b82cba27SSascha Leib
816b82cba27SSascha Leib					const json = await response.json();
817b82cba27SSascha Leib
818b82cba27SSascha Leib					if (json.rules) {
819b82cba27SSascha Leib						this._rulesList = json.rules;
820b82cba27SSascha Leib					}
821b82cba27SSascha Leib
822b82cba27SSascha Leib					if (json.threshold) {
823b82cba27SSascha Leib						this._threshold = json.threshold;
824b82cba27SSascha Leib					}
825b82cba27SSascha Leib
8265526d629SSascha Leib					if (json.ipRanges) {
8275526d629SSascha Leib						// clean up the IPs first:
8285526d629SSascha Leib						let list = [];
8295526d629SSascha Leib						json.ipRanges.forEach( it => {
8305526d629SSascha Leib							let item = {
8315526d629SSascha Leib								'from': BotMon.t._ip2Num(it.from),
8325526d629SSascha Leib								'to': BotMon.t._ip2Num(it.to),
8336f3fa739SSascha Leib								'label': it.label
8345526d629SSascha Leib							};
8355526d629SSascha Leib							list.push(item);
8365526d629SSascha Leib						});
8375526d629SSascha Leib
8385526d629SSascha Leib						this._botIPs = list;
8395526d629SSascha Leib					}
8405526d629SSascha Leib
841b82cba27SSascha Leib					this._ready = true;
842b82cba27SSascha Leib
843b82cba27SSascha Leib				} catch (error) {
844b82cba27SSascha Leib					BotMon.live.gui.status.setError("Error while loading the ‘rules’ file: " + error.message);
845b82cba27SSascha Leib				} finally {
846b82cba27SSascha Leib					BotMon.live.gui.status.hideBusy("Status: Done.");
847b82cba27SSascha Leib					BotMon.live.data._dispatch('rules')
848b82cba27SSascha Leib				}
849b82cba27SSascha Leib			},
850b82cba27SSascha Leib
851b82cba27SSascha Leib			_rulesList: [], // list of rules to find out if a visitor is a bot
852b82cba27SSascha Leib			_threshold: 100, // above this, it is considered a bot.
853b82cba27SSascha Leib
854b82cba27SSascha Leib			// returns a descriptive text for a rule id
855b82cba27SSascha Leib			getRuleInfo: function(ruleId) {
856b82cba27SSascha Leib				// console.info('getRuleInfo', ruleId);
857b82cba27SSascha Leib
858b82cba27SSascha Leib				// shortcut for neater code:
859b82cba27SSascha Leib				const me = BotMon.live.data.rules;
860b82cba27SSascha Leib
861b82cba27SSascha Leib				for (let i=0; i<me._rulesList.length; i++) {
862b82cba27SSascha Leib					const rule = me._rulesList[i];
863b82cba27SSascha Leib					if (rule.id == ruleId) {
864b82cba27SSascha Leib						return rule;
865b82cba27SSascha Leib					}
866b82cba27SSascha Leib				}
867b82cba27SSascha Leib				return null;
868b82cba27SSascha Leib
869b82cba27SSascha Leib			},
870b82cba27SSascha Leib
871b82cba27SSascha Leib			// evaluate a visitor for lkikelihood of being a bot
872b82cba27SSascha Leib			evaluate: function(visitor) {
873b82cba27SSascha Leib
874b82cba27SSascha Leib				// shortcut for neater code:
875b82cba27SSascha Leib				const me = BotMon.live.data.rules;
876b82cba27SSascha Leib
877b82cba27SSascha Leib				let r =  {	// evaluation result
878b82cba27SSascha Leib					'val': 0,
879b82cba27SSascha Leib					'rules': [],
880b82cba27SSascha Leib					'isBot': false
881b82cba27SSascha Leib				};
882b82cba27SSascha Leib
883b82cba27SSascha Leib				for (let i=0; i<me._rulesList.length; i++) {
884b82cba27SSascha Leib					const rule = me._rulesList[i];
885b82cba27SSascha Leib					const params = ( rule.params ? rule.params : [] );
886b82cba27SSascha Leib
887b82cba27SSascha Leib					if (rule.func) { // rule is calling a function
888b82cba27SSascha Leib						if (me.func[rule.func]) {
889b82cba27SSascha Leib							if(me.func[rule.func](visitor, ...params)) {
890b82cba27SSascha Leib								r.val += rule.bot;
891b82cba27SSascha Leib								r.rules.push(rule.id)
892b82cba27SSascha Leib							}
893b82cba27SSascha Leib						} else {
894b82cba27SSascha Leib							//console.warn("Unknown rule function: “${rule.func}”. Ignoring rule.")
895b82cba27SSascha Leib						}
896b82cba27SSascha Leib					}
897b82cba27SSascha Leib				}
898b82cba27SSascha Leib
899b82cba27SSascha Leib				// is a bot?
900b82cba27SSascha Leib				r.isBot = (r.val >= me._threshold);
901b82cba27SSascha Leib
902b82cba27SSascha Leib				return r;
903b82cba27SSascha Leib			},
904b82cba27SSascha Leib
905b82cba27SSascha Leib			// list of functions that can be called by the rules list to evaluate a visitor:
906b82cba27SSascha Leib			func: {
907b82cba27SSascha Leib
9086f3fa739SSascha Leib				// check if client is on the list passed as parameter:
9096f3fa739SSascha Leib				matchesClient: function(visitor, ...clients) {
910b82cba27SSascha Leib
911b82cba27SSascha Leib					const clientId = ( visitor._client ? visitor._client.id : '');
91213592cacSSascha Leib					return clients.includes(clientId);
913b82cba27SSascha Leib				},
914b82cba27SSascha Leib
915b82cba27SSascha Leib				// check if OS/Platform is one of the obsolete ones:
9166f3fa739SSascha Leib				matchesPlatform: function(visitor, ...platforms) {
917b82cba27SSascha Leib
91813592cacSSascha Leib					const pId = ( visitor._platform ? visitor._platform.id : '');
91913592cacSSascha Leib					return platforms.includes(pId);
920b82cba27SSascha Leib				},
921b82cba27SSascha Leib
922b82cba27SSascha Leib				// are there at lest num pages loaded?
923b82cba27SSascha Leib				smallPageCount: function(visitor, num) {
924b82cba27SSascha Leib					return (visitor._pageViews.length <= Number(num));
925b82cba27SSascha Leib				},
926b82cba27SSascha Leib
927446aa816SSascha Leib				// There was no entry in a specific log file for this visitor:
928b82cba27SSascha Leib				// note that this will also trigger the "noJavaScript" rule:
929446aa816SSascha Leib				noRecord: function(visitor, type) {
930446aa816SSascha Leib					return !visitor._seenBy.includes(type);
931b82cba27SSascha Leib				},
932b82cba27SSascha Leib
9335526d629SSascha Leib				// there are no referrers in any of the page visits:
9345526d629SSascha Leib				noReferrer: function(visitor) {
9355526d629SSascha Leib
9365526d629SSascha Leib					let r = false; // return value
9375526d629SSascha Leib					for (let i = 0; i < visitor._pageViews.length; i++) {
9385526d629SSascha Leib						if (!visitor._pageViews[i]._ref) {
9395526d629SSascha Leib							r = true;
9405526d629SSascha Leib							break;
941b82cba27SSascha Leib						}
942b82cba27SSascha Leib					}
9435526d629SSascha Leib					return r;
9445526d629SSascha Leib				},
9455526d629SSascha Leib
9465526d629SSascha Leib				// test for specific client identifiers:
9476f3fa739SSascha Leib				/*matchesClients: function(visitor, ...list) {
9485526d629SSascha Leib
9495526d629SSascha Leib					for (let i=0; i<list.length; i++) {
9505526d629SSascha Leib						if (visitor._client.id == list[i]) {
9515526d629SSascha Leib								return true
9525526d629SSascha Leib						}
9535526d629SSascha Leib					};
9545526d629SSascha Leib					return false;
9556f3fa739SSascha Leib				},*/
9565526d629SSascha Leib
957451abfadSSascha Leib				// unusual combinations of Platform and Client:
9586f3fa739SSascha Leib				combinationTest: function(visitor, ...combinations) {
9595526d629SSascha Leib
9605526d629SSascha Leib					for (let i=0; i<combinations.length; i++) {
9615526d629SSascha Leib
9625526d629SSascha Leib						if (visitor._platform.id == combinations[i][0]
9635526d629SSascha Leib							&& visitor._client.id == combinations[i][1]) {
9645526d629SSascha Leib								return true
9655526d629SSascha Leib						}
9665526d629SSascha Leib					};
9675526d629SSascha Leib
9685526d629SSascha Leib					return false;
9695526d629SSascha Leib				},
9705526d629SSascha Leib
9715526d629SSascha Leib				// is the IP address from a known bot network?
9725526d629SSascha Leib				fromKnownBotIP: function(visitor) {
9735526d629SSascha Leib
9745526d629SSascha Leib					const ipInfo = BotMon.live.data.rules.getBotIPInfo(visitor.ip);
9755526d629SSascha Leib
976446aa816SSascha Leib					if (ipInfo) {
977446aa816SSascha Leib						visitor._ipInKnownBotRange = true;
978446aa816SSascha Leib					}
979446aa816SSascha Leib
9805526d629SSascha Leib					return (ipInfo !== null);
981451abfadSSascha Leib				},
982451abfadSSascha Leib
983451abfadSSascha Leib				// is the page language mentioned in the client's accepted languages?
984451abfadSSascha Leib				// the parameter holds an array of exceptions, i.e. page languages that should be ignored.
985451abfadSSascha Leib				matchLang: function(visitor, ...exceptions) {
986451abfadSSascha Leib
987*17cb08e8SSascha Leib					if (visitor.lang && visitor.accept && exceptions.indexOf(visitor.lang) < 0) {
988*17cb08e8SSascha Leib						return (visitor.accept.split(',').indexOf(visitor.lang) < 0);
989451abfadSSascha Leib					}
990451abfadSSascha Leib					return false;
991446aa816SSascha Leib				},
992446aa816SSascha Leib
993446aa816SSascha Leib				// At least x page views were recorded, but they come within less than y seconds
994446aa816SSascha Leib				loadSpeed: function(visitor, minItems, maxTime) {
995446aa816SSascha Leib
996446aa816SSascha Leib					if (visitor._pageViews.length >= minItems) {
997446aa816SSascha Leib						//console.log('loadSpeed', visitor._pageViews.length, minItems, maxTime);
998446aa816SSascha Leib
999446aa816SSascha Leib						const pvArr = visitor._pageViews.map(pv => pv._lastSeen).sort();
1000446aa816SSascha Leib
1001446aa816SSascha Leib						let totalTime = 0;
1002446aa816SSascha Leib						for (let i=1; i < pvArr.length; i++) {
1003446aa816SSascha Leib							totalTime += (pvArr[i] - pvArr[i-1]);
1004446aa816SSascha Leib						}
1005446aa816SSascha Leib
1006446aa816SSascha Leib						//console.log('     ', totalTime , Math.round(totalTime / (pvArr.length * 1000)), (( totalTime / pvArr.length ) <= maxTime * 1000), visitor.ip);
1007446aa816SSascha Leib
1008446aa816SSascha Leib						return (( totalTime / pvArr.length ) <= maxTime * 1000);
1009446aa816SSascha Leib					}
10105526d629SSascha Leib				}
10115526d629SSascha Leib			},
10125526d629SSascha Leib
10135526d629SSascha Leib			/* known bot IP ranges: */
10145526d629SSascha Leib			_botIPs: [],
10155526d629SSascha Leib
10165526d629SSascha Leib			// return information on a bot IP range:
10175526d629SSascha Leib			getBotIPInfo: function(ip) {
10185526d629SSascha Leib
10195526d629SSascha Leib				// shortcut to make code more readable:
10205526d629SSascha Leib				const me = BotMon.live.data.rules;
10215526d629SSascha Leib
10225526d629SSascha Leib				// convert IP address to easier comparable form:
10235526d629SSascha Leib				const ipNum = BotMon.t._ip2Num(ip);
10245526d629SSascha Leib
10255526d629SSascha Leib				for (let i=0; i < me._botIPs.length; i++) {
10265526d629SSascha Leib					const ipRange = me._botIPs[i];
10275526d629SSascha Leib
10285526d629SSascha Leib					if (ipNum >= ipRange.from && ipNum <= ipRange.to) {
10295526d629SSascha Leib						return ipRange;
10305526d629SSascha Leib					}
10315526d629SSascha Leib
10325526d629SSascha Leib				};
10335526d629SSascha Leib				return null;
10345526d629SSascha Leib
10355526d629SSascha Leib			}
1036b82cba27SSascha Leib
1037b82cba27SSascha Leib		},
1038b82cba27SSascha Leib
10392f2bc93aSSascha Leib		loadLogFile: async function(type, onLoaded = undefined) {
104013592cacSSascha Leib			//console.info('BotMon.live.data.loadLogFile(',type,')');
1041f125bc8dSSascha Leib
1042f125bc8dSSascha Leib			let typeName = '';
1043f125bc8dSSascha Leib			let columns = [];
1044f125bc8dSSascha Leib
1045f125bc8dSSascha Leib			switch (type) {
1046f125bc8dSSascha Leib				case "srv":
1047f125bc8dSSascha Leib					typeName = "Server";
1048451abfadSSascha Leib					columns = ['ts','ip','pg','id','typ','usr','agent','ref','lang','accept'];
1049f125bc8dSSascha Leib					break;
1050f125bc8dSSascha Leib				case "log":
1051f125bc8dSSascha Leib					typeName = "Page load";
105293a5b18bSSascha Leib					columns = ['ts','ip','pg','id','usr','lt','ref','agent'];
1053f125bc8dSSascha Leib					break;
1054f125bc8dSSascha Leib				case "tck":
1055f125bc8dSSascha Leib					typeName = "Ticker";
105693a5b18bSSascha Leib					columns = ['ts','ip','pg','id','agent'];
1057f125bc8dSSascha Leib					break;
1058f125bc8dSSascha Leib				default:
1059f125bc8dSSascha Leib					console.warn(`Unknown log type ${type}.`);
1060f125bc8dSSascha Leib					return;
1061f125bc8dSSascha Leib			}
1062f125bc8dSSascha Leib
10632f2bc93aSSascha Leib			// Show the busy indicator and set the visible status:
10647bd08c30SSascha Leib			BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`);
1065f125bc8dSSascha Leib
10662f2bc93aSSascha Leib			// compose the URL from which to load:
10677bd08c30SSascha Leib			const url = BotMon._baseDir + `logs/${BotMon._today}.${type}.txt`;
10682f2bc93aSSascha Leib			//console.log("Loading:",url);
1069f125bc8dSSascha Leib
10702f2bc93aSSascha Leib			// fetch the data:
1071f125bc8dSSascha Leib			try {
1072f125bc8dSSascha Leib				const response = await fetch(url);
1073f125bc8dSSascha Leib				if (!response.ok) {
1074f125bc8dSSascha Leib					throw new Error(`${response.status} ${response.statusText}`);
1075f125bc8dSSascha Leib				}
1076f125bc8dSSascha Leib
10772f2bc93aSSascha Leib				const logtxt = await response.text();
1078f125bc8dSSascha Leib
10792f2bc93aSSascha Leib				logtxt.split('\n').forEach((line) => {
10802f2bc93aSSascha Leib					if (line.trim() === '') return; // skip empty lines
10812f2bc93aSSascha Leib					const cols = line.split('\t');
10822f2bc93aSSascha Leib
10832f2bc93aSSascha Leib					// assign the columns to an object:
10842f2bc93aSSascha Leib					const data = {};
10852f2bc93aSSascha Leib					cols.forEach( (colVal,i) => {
10862f2bc93aSSascha Leib						colName = columns[i] || `col${i}`;
108743d9de6bSSascha Leib						const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim());
10882f2bc93aSSascha Leib						data[colName] = colValue;
10892f2bc93aSSascha Leib					});
10902f2bc93aSSascha Leib
10912f2bc93aSSascha Leib					// register the visit in the model:
10922f2bc93aSSascha Leib					switch(type) {
10932f2bc93aSSascha Leib						case 'srv':
109443d9de6bSSascha Leib							BotMon.live.data.model.registerVisit(data, type);
10952f2bc93aSSascha Leib							break;
10962f2bc93aSSascha Leib						case 'log':
109743d9de6bSSascha Leib							data.typ = 'js';
10987bd08c30SSascha Leib							BotMon.live.data.model.updateVisit(data);
10992f2bc93aSSascha Leib							break;
11009f1ee8c1SSascha Leib						case 'tck':
110143d9de6bSSascha Leib							data.typ = 'js';
11027bd08c30SSascha Leib							BotMon.live.data.model.updateTicks(data);
11039f1ee8c1SSascha Leib							break;
11042f2bc93aSSascha Leib						default:
11052f2bc93aSSascha Leib							console.warn(`Unknown log type ${type}.`);
11062f2bc93aSSascha Leib							return;
11072f2bc93aSSascha Leib					}
11082f2bc93aSSascha Leib				});
11092f2bc93aSSascha Leib
11102f2bc93aSSascha Leib				if (onLoaded) {
11112f2bc93aSSascha Leib					onLoaded(); // callback after loading is finished.
11122f2bc93aSSascha Leib				}
11132f2bc93aSSascha Leib
1114f125bc8dSSascha Leib			} catch (error) {
11157bd08c30SSascha Leib				BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`);
1116f125bc8dSSascha Leib			} finally {
11177bd08c30SSascha Leib				BotMon.live.gui.status.hideBusy("Status: Done.");
1118f125bc8dSSascha Leib			}
1119f125bc8dSSascha Leib		}
1120f125bc8dSSascha Leib	},
1121f125bc8dSSascha Leib
1122294f6af8SSascha Leib	gui: {
112393a5b18bSSascha Leib		init: function() {
112493a5b18bSSascha Leib			// init the lists view:
112593a5b18bSSascha Leib			this.lists.init();
112693a5b18bSSascha Leib		},
1127294f6af8SSascha Leib
1128294f6af8SSascha Leib		overview: {
1129294f6af8SSascha Leib			make: function() {
1130f4417fdeSSascha Leib
11317bd08c30SSascha Leib				const data = BotMon.live.data.analytics.data;
11327bd08c30SSascha Leib				const parent = document.getElementById('botmon__today__content');
1133f4417fdeSSascha Leib
1134f4417fdeSSascha Leib				// shortcut for neater code:
1135f4417fdeSSascha Leib				const makeElement = BotMon.t._makeElement;
1136f4417fdeSSascha Leib
1137294f6af8SSascha Leib				if (parent) {
1138ade4db36SSascha Leib
1139b82cba27SSascha Leib					const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 100);
1140ade4db36SSascha Leib
1141294f6af8SSascha Leib					jQuery(parent).prepend(jQuery(`
11427bd08c30SSascha Leib						<details id="botmon__today__overview" open>
11439bc80cc5SSascha Leib							<summary>Overview</summary>
11449bc80cc5SSascha Leib							<div class="grid-3-columns">
11459bc80cc5SSascha Leib								<dl>
11469bc80cc5SSascha Leib									<dt>Web metrics</dt>
1147b82cba27SSascha Leib									<dd><span>Total page views:</span><strong>${data.totalPageViews}</strong></dd>
1148b82cba27SSascha Leib									<dd><span>Total visitors (est.):</span><span>${data.totalVisits}</span></dd>
1149b82cba27SSascha Leib									<dd><span>Bounce rate (est.):</span><span>${bounceRate}%</span></dd>
11509bc80cc5SSascha Leib								</dl>
11519bc80cc5SSascha Leib								<dl>
115213592cacSSascha Leib									<dt>Bots vs. Humans (page views)</dt>
1153b82cba27SSascha Leib									<dd><span>Registered users:</span><strong>${data.bots.users}</strong></dd>
1154b82cba27SSascha Leib									<dd><span>Probably humans:</span><strong>${data.bots.human}</strong></dd>
1155b82cba27SSascha Leib									<dd><span>Suspected bots:</span><strong>${data.bots.suspected}</strong></dd>
1156b82cba27SSascha Leib									<dd><span>Known bots:</span><strong>${data.bots.known}</strong></dd>
11579bc80cc5SSascha Leib								</dl>
1158f4417fdeSSascha Leib								<dl id="botmon__botslist"></dl>
11599bc80cc5SSascha Leib							</div>
11609bc80cc5SSascha Leib						</details>
1161294f6af8SSascha Leib					`));
1162f4417fdeSSascha Leib
1163f4417fdeSSascha Leib					// update known bots list:
1164f4417fdeSSascha Leib					const block = document.getElementById('botmon__botslist');
116513592cacSSascha Leib					block.innerHTML = "<dt>Top known bots (page views)</dt>";
1166f4417fdeSSascha Leib
1167f4417fdeSSascha Leib					let bots = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => {
1168f4417fdeSSascha Leib						return b._pageViews.length - a._pageViews.length;
1169f4417fdeSSascha Leib					});
1170f4417fdeSSascha Leib
1171f4417fdeSSascha Leib					for (let i=0; i < Math.min(bots.length, 4); i++) {
1172f4417fdeSSascha Leib						const dd = makeElement('dd');
1173f4417fdeSSascha Leib						dd.appendChild(makeElement('span', {'class': 'bot bot_' + bots[i]._bot.id }, bots[i]._bot.n));
1174b82cba27SSascha Leib						dd.appendChild(makeElement('strong', undefined, bots[i]._pageViews.length));
1175f4417fdeSSascha Leib						block.appendChild(dd);
1176f4417fdeSSascha Leib					}
1177294f6af8SSascha Leib				}
1178294f6af8SSascha Leib			}
1179294f6af8SSascha Leib		},
1180f4417fdeSSascha Leib
1181f125bc8dSSascha Leib		status: {
1182f125bc8dSSascha Leib			setText: function(txt) {
11837bd08c30SSascha Leib				const el = document.getElementById('botmon__today__status');
11847bd08c30SSascha Leib				if (el && BotMon.live.gui.status._errorCount <= 0) {
1185f125bc8dSSascha Leib					el.innerText = txt;
1186f125bc8dSSascha Leib				}
1187f125bc8dSSascha Leib			},
1188f125bc8dSSascha Leib
1189f125bc8dSSascha Leib			setTitle: function(html) {
11907bd08c30SSascha Leib				const el = document.getElementById('botmon__today__title');
1191f125bc8dSSascha Leib				if (el) {
1192f125bc8dSSascha Leib					el.innerHTML = html;
1193f125bc8dSSascha Leib				}
1194f125bc8dSSascha Leib			},
1195f125bc8dSSascha Leib
1196f125bc8dSSascha Leib			setError: function(txt) {
1197f125bc8dSSascha Leib				console.error(txt);
11987bd08c30SSascha Leib				BotMon.live.gui.status._errorCount += 1;
11997bd08c30SSascha Leib				const el = document.getElementById('botmon__today__status');
1200f125bc8dSSascha Leib				if (el) {
1201f125bc8dSSascha Leib					el.innerText = "An error occured. See the browser log for details!";
1202f125bc8dSSascha Leib					el.classList.add('error');
1203f125bc8dSSascha Leib				}
1204f125bc8dSSascha Leib			},
1205f125bc8dSSascha Leib			_errorCount: 0,
1206f125bc8dSSascha Leib
1207f125bc8dSSascha Leib			showBusy: function(txt = null) {
12087bd08c30SSascha Leib				BotMon.live.gui.status._busyCount += 1;
12097bd08c30SSascha Leib				const el = document.getElementById('botmon__today__busy');
1210f125bc8dSSascha Leib				if (el) {
1211f125bc8dSSascha Leib					el.style.display = 'inline-block';
1212f125bc8dSSascha Leib				}
12137bd08c30SSascha Leib				if (txt) BotMon.live.gui.status.setText(txt);
1214f125bc8dSSascha Leib			},
1215f125bc8dSSascha Leib			_busyCount: 0,
1216f125bc8dSSascha Leib
1217f125bc8dSSascha Leib			hideBusy: function(txt = null) {
12187bd08c30SSascha Leib				const el = document.getElementById('botmon__today__busy');
12197bd08c30SSascha Leib				BotMon.live.gui.status._busyCount -= 1;
12207bd08c30SSascha Leib				if (BotMon.live.gui.status._busyCount <= 0) {
1221f125bc8dSSascha Leib					if (el) el.style.display = 'none';
12227bd08c30SSascha Leib					if (txt) BotMon.live.gui.status.setText(txt);
1223f125bc8dSSascha Leib				}
1224f125bc8dSSascha Leib			}
122593a5b18bSSascha Leib		},
122693a5b18bSSascha Leib
122793a5b18bSSascha Leib		lists: {
122893a5b18bSSascha Leib			init: function() {
122993a5b18bSSascha Leib
123013592cacSSascha Leib				// function shortcut:
123113592cacSSascha Leib				const makeElement = BotMon.t._makeElement;
123213592cacSSascha Leib
123393a5b18bSSascha Leib				const parent = document.getElementById('botmon__today__visitorlists');
123493a5b18bSSascha Leib				if (parent) {
123593a5b18bSSascha Leib
123693a5b18bSSascha Leib					for (let i=0; i < 4; i++) {
123793a5b18bSSascha Leib
123893a5b18bSSascha Leib						// change the id and title by number:
123993a5b18bSSascha Leib						let listTitle = '';
124093a5b18bSSascha Leib						let listId = '';
124193a5b18bSSascha Leib						switch (i) {
124293a5b18bSSascha Leib							case 0:
124393a5b18bSSascha Leib								listTitle = "Registered users";
124493a5b18bSSascha Leib								listId = 'users';
124593a5b18bSSascha Leib								break;
124693a5b18bSSascha Leib							case 1:
124793a5b18bSSascha Leib								listTitle = "Probably humans";
124893a5b18bSSascha Leib								listId = 'humans';
124993a5b18bSSascha Leib								break;
125093a5b18bSSascha Leib							case 2:
125193a5b18bSSascha Leib								listTitle = "Suspected bots";
125293a5b18bSSascha Leib								listId = 'suspectedBots';
125393a5b18bSSascha Leib								break;
125493a5b18bSSascha Leib							case 3:
125593a5b18bSSascha Leib								listTitle = "Known bots";
125693a5b18bSSascha Leib								listId = 'knownBots';
125793a5b18bSSascha Leib								break;
125893a5b18bSSascha Leib							default:
12594c5062c1SSascha Leib								console.warn('Unknown list number.');
1260f125bc8dSSascha Leib						}
1261294f6af8SSascha Leib
126213592cacSSascha Leib						const details = makeElement('details', {
126393a5b18bSSascha Leib							'data-group': listId,
126493a5b18bSSascha Leib							'data-loaded': false
126593a5b18bSSascha Leib						});
126613592cacSSascha Leib						const title = details.appendChild(makeElement('summary'));
12674c5062c1SSascha Leib						title.appendChild(makeElement('span', {'class':'title'}, listTitle));
12684c5062c1SSascha Leib						title.appendChild(makeElement('span', {'class':'counter'}, '–'));
126993a5b18bSSascha Leib						details.addEventListener("toggle", this._onDetailsToggle);
127093a5b18bSSascha Leib
127193a5b18bSSascha Leib						parent.appendChild(details);
127293a5b18bSSascha Leib
127393a5b18bSSascha Leib					}
127493a5b18bSSascha Leib				}
127593a5b18bSSascha Leib			},
127693a5b18bSSascha Leib
127793a5b18bSSascha Leib			_onDetailsToggle: function(e) {
1278f4417fdeSSascha Leib				//console.info('BotMon.live.gui.lists._onDetailsToggle()');
127993a5b18bSSascha Leib
128093a5b18bSSascha Leib				const target = e.target;
128193a5b18bSSascha Leib
128293a5b18bSSascha Leib				if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet
128393a5b18bSSascha Leib					target.setAttribute('data-loaded', 'loading');
128493a5b18bSSascha Leib
128593a5b18bSSascha Leib					const fillType = target.getAttribute('data-group');
128693a5b18bSSascha Leib					const fillList = BotMon.live.data.analytics.groups[fillType];
128793a5b18bSSascha Leib					if (fillList && fillList.length > 0) {
128893a5b18bSSascha Leib
128993a5b18bSSascha Leib						const ul = BotMon.t._makeElement('ul');
129093a5b18bSSascha Leib
129193a5b18bSSascha Leib						fillList.forEach( (it) => {
129293a5b18bSSascha Leib							ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType));
129393a5b18bSSascha Leib						});
129493a5b18bSSascha Leib
129593a5b18bSSascha Leib						target.appendChild(ul);
129693a5b18bSSascha Leib						target.setAttribute('data-loaded', 'true');
129793a5b18bSSascha Leib					} else {
129893a5b18bSSascha Leib						target.setAttribute('data-loaded', 'false');
129993a5b18bSSascha Leib					}
130093a5b18bSSascha Leib
130193a5b18bSSascha Leib				}
130293a5b18bSSascha Leib			},
130393a5b18bSSascha Leib
130493a5b18bSSascha Leib			_makeVisitorItem: function(data, type) {
130593a5b18bSSascha Leib
130693a5b18bSSascha Leib				// shortcut for neater code:
130793a5b18bSSascha Leib				const make = BotMon.t._makeElement;
130893a5b18bSSascha Leib
130943d9de6bSSascha Leib				let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' );
13105526d629SSascha Leib				const platformName = (data._platform ? data._platform.n : 'Unknown');
13115526d629SSascha Leib				const clientName = (data._client ? data._client.n: 'Unknown');
131243d9de6bSSascha Leib
131393a5b18bSSascha Leib				const li = make('li'); // root list item
131493a5b18bSSascha Leib				const details = make('details');
131593a5b18bSSascha Leib				const summary = make('summary');
131693a5b18bSSascha Leib				details.appendChild(summary);
131793a5b18bSSascha Leib
131893a5b18bSSascha Leib				const span1 = make('span'); /* left-hand group */
1319f4417fdeSSascha Leib				if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */
132093a5b18bSSascha Leib
1321259d3b85SSascha Leib					const botName = ( data._bot && data._bot.n ? data._bot.n : "Unknown");
132243d9de6bSSascha Leib					span1.appendChild(make('span', { /* Bot */
132343d9de6bSSascha Leib						'class': 'bot bot_' + (data._bot ? data._bot.id : 'unknown'),
1324259d3b85SSascha Leib						'title': "Bot: " + botName
1325259d3b85SSascha Leib					}, botName));
132643d9de6bSSascha Leib
1327f4417fdeSSascha Leib				} else if (data._type == BM_USERTYPE.KNOWN_USER) { /* User only */
132843d9de6bSSascha Leib
132943d9de6bSSascha Leib					span1.appendChild(make('span', { /* User */
1330f4417fdeSSascha Leib						'class': 'user_known',
133143d9de6bSSascha Leib						'title': "User: " + data.usr
133243d9de6bSSascha Leib					}, data.usr));
133343d9de6bSSascha Leib
133443d9de6bSSascha Leib				} else { /* others */
133543d9de6bSSascha Leib
133643d9de6bSSascha Leib					if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
133743d9de6bSSascha Leib					span1.appendChild(make('span', { /* IP-Address */
133843d9de6bSSascha Leib						'class': 'ipaddr ip' + ipType,
133943d9de6bSSascha Leib						'title': "IP-Address: " + data.ip
134043d9de6bSSascha Leib					}, data.ip));
134143d9de6bSSascha Leib
134243d9de6bSSascha Leib				}
134393a5b18bSSascha Leib
1344f4417fdeSSascha Leib				if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* Not for bots */
134593a5b18bSSascha Leib					span1.appendChild(make('span', { /* Platform */
134693a5b18bSSascha Leib						'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'),
134793a5b18bSSascha Leib						'title': "Platform: " + platformName
134893a5b18bSSascha Leib					}, platformName));
134993a5b18bSSascha Leib
135093a5b18bSSascha Leib					span1.appendChild(make('span', { /* Client */
135193a5b18bSSascha Leib						'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'),
135293a5b18bSSascha Leib						'title': "Client: " + clientName
135393a5b18bSSascha Leib					}, clientName));
1354f4417fdeSSascha Leib				}
135593a5b18bSSascha Leib
135693a5b18bSSascha Leib				summary.appendChild(span1);
135793a5b18bSSascha Leib				const span2 = make('span'); /* right-hand group */
135893a5b18bSSascha Leib
1359f4417fdeSSascha Leib				span2.appendChild(make('span', { /* page views */
1360f4417fdeSSascha Leib					'class': 'pageviews'
1361f4417fdeSSascha Leib				}, data._pageViews.length));
136293a5b18bSSascha Leib
136393a5b18bSSascha Leib				summary.appendChild(span2);
136493a5b18bSSascha Leib
13655526d629SSascha Leib				// add details expandable section:
13665526d629SSascha Leib				details.appendChild(BotMon.live.gui.lists._makeVisitorDetails(data, type));
13675526d629SSascha Leib
13685526d629SSascha Leib				li.appendChild(details);
13695526d629SSascha Leib				return li;
13705526d629SSascha Leib			},
13715526d629SSascha Leib
13725526d629SSascha Leib			_makeVisitorDetails: function(data, type) {
13735526d629SSascha Leib
13745526d629SSascha Leib				// shortcut for neater code:
13755526d629SSascha Leib				const make = BotMon.t._makeElement;
13765526d629SSascha Leib
13775526d629SSascha Leib				let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' );
13785526d629SSascha Leib				if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0';
13795526d629SSascha Leib				const platformName = (data._platform ? data._platform.n : 'Unknown');
13805526d629SSascha Leib				const clientName = (data._client ? data._client.n: 'Unknown');
138193a5b18bSSascha Leib
138293a5b18bSSascha Leib				const dl = make('dl', {'class': 'visitor_details'});
138393a5b18bSSascha Leib
1384f4417fdeSSascha Leib				if (data._type == BM_USERTYPE.KNOWN_BOT) {
1385f4417fdeSSascha Leib
1386f4417fdeSSascha Leib					dl.appendChild(make('dt', {}, "Bot name:")); /* bot info */
138743d9de6bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown')},
138843d9de6bSSascha Leib						(data._bot ? data._bot.n : 'Unknown')));
1389f4417fdeSSascha Leib
1390f4417fdeSSascha Leib					if (data._bot && data._bot.url) {
1391f4417fdeSSascha Leib						dl.appendChild(make('dt', {}, "Bot info:")); /* bot info */
1392f4417fdeSSascha Leib						const botInfoDd = dl.appendChild(make('dd'));
1393f4417fdeSSascha Leib						botInfoDd.appendChild(make('a', {
1394f4417fdeSSascha Leib							'href': data._bot.url,
1395f4417fdeSSascha Leib							'target': '_blank'
1396f4417fdeSSascha Leib						}, data._bot.url)); /* bot info link*/
1397f4417fdeSSascha Leib
139843d9de6bSSascha Leib					}
139943d9de6bSSascha Leib
1400f4417fdeSSascha Leib				} else { /* not for bots */
1401f4417fdeSSascha Leib
140293a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "Client:")); /* client */
140393a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')},
140493a5b18bSSascha Leib						clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) ));
140593a5b18bSSascha Leib
140693a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "Platform:")); /* platform */
140793a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')},
140893a5b18bSSascha Leib						platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) ));
140993a5b18bSSascha Leib
141093a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "IP-Address:"));
141193a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip));
1412b2e3bd8bSSascha Leib
14135d78a53bSSascha Leib					/*dl.appendChild(make('dt', {}, "ID:"));
14145d78a53bSSascha Leib					dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id));*/
1415259d3b85SSascha Leib				}
141693a5b18bSSascha Leib
1417446aa816SSascha Leib				if (Math.abs(data._lastSeen - data._firstSeen) < 100) {
141893a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "Seen:"));
141993a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString()));
142093a5b18bSSascha Leib				} else {
142193a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "First seen:"));
142293a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString()));
142393a5b18bSSascha Leib					dl.appendChild(make('dt', {}, "Last seen:"));
142493a5b18bSSascha Leib					dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString()));
142593a5b18bSSascha Leib				}
142693a5b18bSSascha Leib
142793a5b18bSSascha Leib				dl.appendChild(make('dt', {}, "User-Agent:"));
14285526d629SSascha Leib				dl.appendChild(make('dd', {'class': 'agent'}, data.agent));
142993a5b18bSSascha Leib
14306f3fa739SSascha Leib				dl.appendChild(make('dt', {}, "Languages:"));
14316f3fa739SSascha Leib				dl.appendChild(make('dd', {'class': 'langs'}, "Client accepts: [" + data.accept + "]; Page: [" + data.lang + ']'));
14326f3fa739SSascha Leib
14335d78a53bSSascha Leib				/*dl.appendChild(make('dt', {}, "Visitor Type:"));
14345d78a53bSSascha Leib				dl.appendChild(make('dd', undefined, data._type ));*/
1435f4417fdeSSascha Leib
1436f4417fdeSSascha Leib				dl.appendChild(make('dt', {}, "Seen by:"));
1437f4417fdeSSascha Leib				dl.appendChild(make('dd', undefined, data._seenBy.join(', ') ));
1438f4417fdeSSascha Leib
143993a5b18bSSascha Leib				dl.appendChild(make('dt', {}, "Visited pages:"));
144093a5b18bSSascha Leib				const pagesDd = make('dd', {'class': 'pages'});
144193a5b18bSSascha Leib				const pageList = make('ul');
14425526d629SSascha Leib
14435526d629SSascha Leib				/* list all page views */
144493a5b18bSSascha Leib				data._pageViews.forEach( (page) => {
144593a5b18bSSascha Leib					const pgLi = make('li');
144693a5b18bSSascha Leib
144793a5b18bSSascha Leib					let visitTimeStr = "Bounce";
144893a5b18bSSascha Leib					const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime();
144993a5b18bSSascha Leib					if (visitDuration > 0) {
145093a5b18bSSascha Leib						visitTimeStr = Math.floor(visitDuration / 1000) + "s";
145193a5b18bSSascha Leib					}
145293a5b18bSSascha Leib
14535526d629SSascha Leib					pgLi.appendChild(make('span', {}, page.pg)); /* DW Page ID */
14545526d629SSascha Leib					if (page._ref) {
14555526d629SSascha Leib						pgLi.appendChild(make('span', {
14565526d629SSascha Leib							'data-ref': page._ref.host,
14575526d629SSascha Leib							'title': "Referrer: " + page._ref.full
14585526d629SSascha Leib						}, page._ref.site));
14595526d629SSascha Leib					} else {
14605526d629SSascha Leib						pgLi.appendChild(make('span', {
14615526d629SSascha Leib						}, "No referer"));
14625526d629SSascha Leib					}
1463259d3b85SSascha Leib					pgLi.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount));
1464446aa816SSascha Leib					pgLi.appendChild(make('span', {}, BotMon.t._formatTime(page._firstSeen)));
1465446aa816SSascha Leib
1466446aa816SSascha Leib					// get the time difference:
1467446aa816SSascha Leib					const tDiff = BotMon.t._formatTimeDiff(page._firstSeen, page._lastSeen);
1468446aa816SSascha Leib					if (tDiff) {
1469446aa816SSascha Leib						pgLi.appendChild(make('span', {'class': 'visit-length', 'title': 'Last seen: ' + page._lastSeen.toLocaleString()}, tDiff));
1470446aa816SSascha Leib					} else {
1471446aa816SSascha Leib						pgLi.appendChild(make('span', {'class': 'bounce'}, "Bounce"));
1472446aa816SSascha Leib					}
1473446aa816SSascha Leib
147493a5b18bSSascha Leib					pageList.appendChild(pgLi);
147593a5b18bSSascha Leib				});
147693a5b18bSSascha Leib				pagesDd.appendChild(pageList);
147793a5b18bSSascha Leib				dl.appendChild(pagesDd);
147893a5b18bSSascha Leib
1479446aa816SSascha Leib				/* bot evaluation rating */
14805d78a53bSSascha Leib				if (data._type !== BM_USERTYPE.KNOWN_BOT && data._type !== BM_USERTYPE.KNOWN_USER) {
1481446aa816SSascha Leib					dl.appendChild(make('dt', undefined, "Bot rating:"));
14825d78a53bSSascha Leib					dl.appendChild(make('dd', {'class': 'bot-rating'}, ( data._botVal ? data._botVal : '–' ) + '/' + BotMon.live.data.rules._threshold ));
1483446aa816SSascha Leib
1484446aa816SSascha Leib					/* add bot evaluation details: */
14851c16f1b7SSascha Leib					if (data._eval) {
1486446aa816SSascha Leib						dl.appendChild(make('dt', {}, "Bot evaluation details:"));
1487b82cba27SSascha Leib						const evalDd = make('dd');
1488bc55f6a6SSascha Leib						const testList = make('ul',{
1489bc55f6a6SSascha Leib							'class': 'eval'
1490bc55f6a6SSascha Leib						});
14915526d629SSascha Leib						data._eval.forEach( test => {
1492b82cba27SSascha Leib
1493b82cba27SSascha Leib							const tObj = BotMon.live.data.rules.getRuleInfo(test);
14945526d629SSascha Leib							let tDesc = tObj ? tObj.desc : test;
1495b82cba27SSascha Leib
14965526d629SSascha Leib							// special case for Bot IP range test:
14975526d629SSascha Leib							if (tObj.func == 'fromKnownBotIP') {
14985526d629SSascha Leib								const rangeInfo = BotMon.live.data.rules.getBotIPInfo(data.ip);
14995526d629SSascha Leib								if (rangeInfo) {
15006f3fa739SSascha Leib									tDesc += ' (' + (rangeInfo.label ? rangeInfo.label : 'Unknown') + ')';
15015526d629SSascha Leib								}
15025526d629SSascha Leib							}
15035526d629SSascha Leib
15045526d629SSascha Leib							// create the entry field
1505b82cba27SSascha Leib							const tstLi = make('li');
1506b82cba27SSascha Leib							tstLi.appendChild(make('span', {
15075526d629SSascha Leib								'data-testid': test
15085526d629SSascha Leib							}, tDesc));
1509b82cba27SSascha Leib							tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') ));
1510b82cba27SSascha Leib							testList.appendChild(tstLi);
1511b82cba27SSascha Leib						});
1512b82cba27SSascha Leib
15135526d629SSascha Leib						// add total row
1514bc55f6a6SSascha Leib						const tst2Li = make('li', {
1515bc55f6a6SSascha Leib							'class': 'total'
1516bc55f6a6SSascha Leib						});
1517446aa816SSascha Leib						/*tst2Li.appendChild(make('span', {}, "Total:"));
1518b82cba27SSascha Leib						tst2Li.appendChild(make('span', {}, data._botVal));
1519446aa816SSascha Leib						testList.appendChild(tst2Li);*/
1520b82cba27SSascha Leib
1521b82cba27SSascha Leib						evalDd.appendChild(testList);
1522b82cba27SSascha Leib						dl.appendChild(evalDd);
1523bc55f6a6SSascha Leib					}
15245d78a53bSSascha Leib				}
15255d78a53bSSascha Leib				// return the element to add to the UI:
15265526d629SSascha Leib				return dl;
152793a5b18bSSascha Leib			}
1528f4417fdeSSascha Leib
152993a5b18bSSascha Leib		}
1530294f6af8SSascha Leib	}
1531c7931771SSascha Leib};
1532f125bc8dSSascha Leib
15337bd08c30SSascha Leib/* launch only if the BotMon admin panel is open: */
15347bd08c30SSascha Leibif (document.getElementById('botmon__admin')) {
15357bd08c30SSascha Leib	BotMon.init();
1536f125bc8dSSascha Leib}