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}