1"use strict"; 2/* DokuWiki BotMon Plugin Script file */ 3/* 04.09.2025 - 0.1.8 - pre-release */ 4/* Authors: Sascha Leib <ad@hominem.info> */ 5 6// enumeration of user types: 7const BM_USERTYPE = Object.freeze({ 8 'UNKNOWN': 'unknown', 9 'KNOWN_USER': 'user', 10 'HUMAN': 'human', 11 'LIKELY_BOT': 'likely_bot', 12 'KNOWN_BOT': 'known_bot' 13}); 14 15/* BotMon root object */ 16const BotMon = { 17 18 init: function() { 19 //console.info('BotMon.init()'); 20 21 // find the plugin basedir: 22 this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/')) 23 + '/plugins/botmon/'; 24 25 // read the page language from the DOM: 26 this._lang = document.getRootNode().documentElement.lang || this._lang; 27 28 // get the time offset: 29 this._timeDiff = BotMon.t._getTimeOffset(); 30 31 // init the sub-objects: 32 BotMon.t._callInit(this); 33 }, 34 35 _baseDir: null, 36 _lang: 'en', 37 _today: (new Date()).toISOString().slice(0, 10), 38 _timeDiff: '', 39 40 /* internal tools */ 41 t: { 42 43 /* helper function to call inits of sub-objects */ 44 _callInit: function(obj) { 45 //console.info('BotMon.t._callInit(obj=',obj,')'); 46 47 /* call init / _init on each sub-object: */ 48 Object.keys(obj).forEach( (key,i) => { 49 const sub = obj[key]; 50 let init = null; 51 if (typeof sub === 'object' && sub.init) { 52 init = sub.init; 53 } 54 55 // bind to object 56 if (typeof init == 'function') { 57 const init2 = init.bind(sub); 58 init2(obj); 59 } 60 }); 61 }, 62 63 /* helper function to calculate the time difference to UTC: */ 64 _getTimeOffset: function() { 65 const now = new Date(); 66 let offset = now.getTimezoneOffset(); // in minutes 67 const sign = Math.sign(offset); // +1 or -1 68 offset = Math.abs(offset); // always positive 69 70 let hours = 0; 71 while (offset >= 60) { 72 hours += 1; 73 offset -= 60; 74 } 75 return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : ''); 76 }, 77 78 /* helper function to create a new element with all attributes and text content */ 79 _makeElement: function(name, atlist = undefined, text = undefined) { 80 var r = null; 81 try { 82 r = document.createElement(name); 83 if (atlist) { 84 for (let attr in atlist) { 85 r.setAttribute(attr, atlist[attr]); 86 } 87 } 88 if (text) { 89 r.textContent = text.toString(); 90 } 91 } catch(e) { 92 console.error(e); 93 } 94 return r; 95 } 96 } 97}; 98 99/* everything specific to the "Today" tab is self-contained in the "live" object: */ 100BotMon.live = { 101 init: function() { 102 //console.info('BotMon.live.init()'); 103 104 // set the title: 105 const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (BotMon._timeDiff != '' ? `, ${BotMon._timeDiff}` : '' ) + ')'; 106 BotMon.live.gui.status.setTitle(`Data for <time datetime=${BotMon._today}>${BotMon._today}</time> ${tDiff}`); 107 108 // init sub-objects: 109 BotMon.t._callInit(this); 110 }, 111 112 data: { 113 init: function() { 114 //console.info('BotMon.live.data.init()'); 115 116 // call sub-inits: 117 BotMon.t._callInit(this); 118 }, 119 120 // this will be called when the known json files are done loading: 121 _dispatch: function(file) { 122 //console.info('BotMon.live.data._dispatch(,',file,')'); 123 124 // shortcut to make code more readable: 125 const data = BotMon.live.data; 126 127 // set the flags: 128 switch(file) { 129 case 'bots': 130 data._dispatchBotsLoaded = true; 131 break; 132 case 'clients': 133 data._dispatchClientsLoaded = true; 134 break; 135 case 'platforms': 136 data._dispatchPlatformsLoaded = true; 137 break; 138 case 'rules': 139 data._dispatchRulesLoaded = true; 140 break; 141 default: 142 // ignore 143 } 144 145 // are all the flags set? 146 if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded && data._dispatchRulesLoaded) { 147 // chain the log files loading: 148 BotMon.live.data.loadLogFile('srv', BotMon.live.data._onServerLogLoaded); 149 } 150 }, 151 // flags to track which data files have been loaded: 152 _dispatchBotsLoaded: false, 153 _dispatchClientsLoaded: false, 154 _dispatchPlatformsLoaded: false, 155 _dispatchRulesLoaded: false, 156 157 // event callback, after the server log has been loaded: 158 _onServerLogLoaded: function() { 159 //console.info('BotMon.live.data._onServerLogLoaded()'); 160 161 // chain the client log file to load: 162 BotMon.live.data.loadLogFile('log', BotMon.live.data._onClientLogLoaded); 163 }, 164 165 // event callback, after the client log has been loaded: 166 _onClientLogLoaded: function() { 167 //console.info('BotMon.live.data._onClientLogLoaded()'); 168 169 // chain the ticks file to load: 170 BotMon.live.data.loadLogFile('tck', BotMon.live.data._onTicksLogLoaded); 171 172 }, 173 174 // event callback, after the tiker log has been loaded: 175 _onTicksLogLoaded: function() { 176 //console.info('BotMon.live.data._onTicksLogLoaded()'); 177 178 // analyse the data: 179 BotMon.live.data.analytics.analyseAll(); 180 181 // sort the data: 182 // #TODO 183 184 // display the data: 185 BotMon.live.gui.overview.make(); 186 187 //console.log(BotMon.live.data.model._visitors); 188 189 }, 190 191 model: { 192 // visitors storage: 193 _visitors: [], 194 195 // find an already existing visitor record: 196 findVisitor: function(visitor) { 197 //console.info('BotMon.live.data.model.findVisitor()'); 198 //console.log(visitor); 199 200 // shortcut to make code more readable: 201 const model = BotMon.live.data.model; 202 203 // loop over all visitors already registered: 204 for (let i=0; i<model._visitors.length; i++) { 205 const v = model._visitors[i]; 206 207 if (visitor._type == BM_USERTYPE.KNOWN_BOT) { /* known bots */ 208 209 // bots match when their ID matches: 210 if (v._bot && v._bot.id == visitor._bot.id) { 211 return v; 212 } 213 214 } else if (visitor._type == BM_USERTYPE.KNOWN_USER) { /* registered users */ 215 216 //if (visitor.id == 'fsmoe7lgqb89t92vt4ju8vdl0q') console.log(visitor); 217 218 // visitors match when their names match: 219 if ( v.usr == visitor.usr 220 && v.ip == visitor.ip 221 && v.agent == visitor.agent) { 222 return v; 223 } 224 } else { /* any other visitor */ 225 226 if ( v.id == visitor.id) { /* match the pre-defined IDs */ 227 return v; 228 } else if (v.ip == visitor.ip && v.agent == visitor.agent) { 229 console.info("Visitor ID not found, using matchin IP + User-Agent instead."); 230 return v; 231 } 232 233 } 234 } 235 return null; // nothing found 236 }, 237 238 /* if there is already this visit registered, return the page view item */ 239 _getPageView: function(visit, view) { 240 241 // shortcut to make code more readable: 242 const model = BotMon.live.data.model; 243 244 for (let i=0; i<visit._pageViews.length; i++) { 245 const pv = visit._pageViews[i]; 246 if (pv.pg == view.pg) { 247 return pv; 248 } 249 } 250 return null; // not found 251 }, 252 253 // register a new visitor (or update if already exists) 254 registerVisit: function(nv, type) { 255 //console.info('registerVisit', nv, type); 256 257 // shortcut to make code more readable: 258 const model = BotMon.live.data.model; 259 260 // is it a known bot? 261 const bot = BotMon.live.data.bots.match(nv.agent); 262 263 // enrich new visitor with relevant data: 264 if (!nv._bot) nv._bot = bot ?? null; // bot info 265 nv._type = ( bot ? BM_USERTYPE.KNOWN_BOT : ( nv.usr && nv.usr !== '' ? BM_USERTYPE.KNOWN_USER : BM_USERTYPE.UNKNOWN ) ); 266 if (!nv._firstSeen) nv._firstSeen = nv.ts; 267 nv._lastSeen = nv.ts; 268 269 // check if it already exists: 270 let visitor = model.findVisitor(nv); 271 if (!visitor) { 272 visitor = nv; 273 visitor._seenBy = [type]; 274 visitor._pageViews = []; // array of page views 275 visitor._hasReferrer = false; // has at least one referrer 276 visitor._jsClient = false; // visitor has been seen logged by client js as well 277 visitor._client = BotMon.live.data.clients.match(nv.agent) ?? null; // client info 278 visitor._platform = BotMon.live.data.platforms.match(nv.agent); // platform info 279 model._visitors.push(visitor); 280 } 281 282 // find browser 283 284 // is this visit already registered? 285 let prereg = model._getPageView(visitor, nv); 286 if (!prereg) { 287 // add new page view: 288 prereg = model._makePageView(nv, type); 289 visitor._pageViews.push(prereg); 290 } else { 291 // update last seen date 292 prereg._lastSeen = nv.ts; 293 // increase view count: 294 prereg._viewCount += 1; 295 } 296 297 // update referrer state: 298 visitor._hasReferrer = visitor._hasReferrer || 299 (prereg.ref !== undefined && prereg.ref !== ''); 300 301 // update time stamp for last-seen: 302 visitor._lastSeen = nv.ts; 303 304 // if needed: 305 return visitor; 306 }, 307 308 // updating visit data from the client-side log: 309 updateVisit: function(dat) { 310 //console.info('updateVisit', dat); 311 312 // shortcut to make code more readable: 313 const model = BotMon.live.data.model; 314 315 const type = 'log'; 316 317 let visitor = BotMon.live.data.model.findVisitor(dat); 318 if (!visitor) { 319 visitor = model.registerVisit(dat, type); 320 } 321 if (visitor) { 322 323 visitor._lastSeen = dat.ts; 324 if (!visitor._seenBy.includes(type)) { 325 visitor._seenBy.push(type); 326 } 327 visitor._jsClient = true; // seen by client js 328 } 329 330 // find the page view: 331 let prereg = BotMon.live.data.model._getPageView(visitor, dat); 332 if (prereg) { 333 // update the page view: 334 prereg._lastSeen = dat.ts; 335 if (!prereg._seenBy.includes(type)) prereg._seenBy.push(type); 336 prereg._jsClient = true; // seen by client js 337 } else { 338 // add the page view to the visitor: 339 prereg = model._makePageView(dat, type); 340 visitor._pageViews.push(prereg); 341 } 342 }, 343 344 // updating visit data from the ticker log: 345 updateTicks: function(dat) { 346 //console.info('updateTicks', dat); 347 348 // shortcut to make code more readable: 349 const model = BotMon.live.data.model; 350 351 const type = 'tck'; 352 353 // find the visit info: 354 let visitor = model.findVisitor(dat); 355 if (!visitor) { 356 console.warn(`No visitor with ID ${dat.id}, registering a new one.`); 357 visitor = model.registerVisit(dat, type); 358 } 359 if (visitor) { 360 // update visitor: 361 if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts; 362 if (!visitor._seenBy.includes(type)) visitor._seenBy.push(type); 363 364 // get the page view info: 365 let pv = model._getPageView(visitor, dat); 366 if (!pv) { 367 console.warn(`No page view for visit ID ${dat.id}, page ${dat.pg}, registering a new one.`); 368 pv = model._makePageView(dat, type); 369 visitor._pageViews.push(pv); 370 } 371 372 // update the page view info: 373 if (!pv._seenBy.includes(type)) pv._seenBy.push(type); 374 if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts; 375 pv._tickCount += 1; 376 377 } 378 }, 379 380 // helper function to create a new "page view" item: 381 _makePageView: function(data, type) { 382 return { 383 _by: type, 384 ip: data.ip, 385 pg: data.pg, 386 ref: data.ref || '', 387 _firstSeen: data.ts, 388 _lastSeen: data.ts, 389 _seenBy: [type], 390 _jsClient: ( type !== 'srv'), 391 _viewCount: 1, 392 _tickCount: 0 393 }; 394 } 395 }, 396 397 analytics: { 398 399 init: function() { 400 //console.info('BotMon.live.data.analytics.init()'); 401 }, 402 403 // data storage: 404 data: { 405 totalVisits: 0, 406 totalPageViews: 0, 407 bots: { 408 known: 0, 409 suspected: 0, 410 human: 0, 411 users: 0 412 } 413 }, 414 415 // sort the visits by type: 416 groups: { 417 knownBots: [], 418 suspectedBots: [], 419 humans: [], 420 users: [] 421 }, 422 423 // all analytics 424 analyseAll: function() { 425 //console.info('BotMon.live.data.analytics.analyseAll()'); 426 427 // shortcut to make code more readable: 428 const model = BotMon.live.data.model; 429 430 // loop over all visitors: 431 model._visitors.forEach( (v) => { 432 433 // count visits and page views: 434 this.data.totalVisits += 1; 435 this.data.totalPageViews += v._pageViews.length; 436 437 // check for typical bot aspects: 438 let botScore = 0; 439 440 if (v._type == BM_USERTYPE.KNOWN_BOT) { // known bots 441 442 this.data.bots.known += v._pageViews.length; 443 this.groups.knownBots.push(v); 444 445 } else if (v._type == BM_USERTYPE.KNOWN_USER) { // known users */ 446 447 this.data.bots.users += v._pageViews.length; 448 this.groups.users.push(v); 449 450 } else { 451 452 // get evaluation: 453 const e = BotMon.live.data.rules.evaluate(v); 454 v._eval = e.rules; 455 v._botVal = e.val; 456 457 if (e.isBot) { // likely bots 458 v._type = BM_USERTYPE.LIKELY_BOT; 459 this.data.bots.suspected += v._pageViews.length; 460 this.groups.suspectedBots.push(v); 461 } else { // probably humans 462 v._type = BM_USERTYPE.HUMAN; 463 this.data.bots.human += v._pageViews.length; 464 this.groups.humans.push(v); 465 } 466 // TODO: find suspected bots 467 468 } 469 }); 470 471 //console.log(this.data); 472 //console.log(this.groups); 473 } 474 475 }, 476 477 bots: { 478 // loads the list of known bots from a JSON file: 479 init: async function() { 480 //console.info('BotMon.live.data.bots.init()'); 481 482 // Load the list of known bots: 483 BotMon.live.gui.status.showBusy("Loading known bots …"); 484 const url = BotMon._baseDir + 'data/known-bots.json'; 485 try { 486 const response = await fetch(url); 487 if (!response.ok) { 488 throw new Error(`${response.status} ${response.statusText}`); 489 } 490 491 this._list = await response.json(); 492 this._ready = true; 493 494 } catch (error) { 495 BotMon.live.gui.status.setError("Error while loading the ‘known bots’ file: " + error.message); 496 } finally { 497 BotMon.live.gui.status.hideBusy("Status: Done."); 498 BotMon.live.data._dispatch('bots') 499 } 500 }, 501 502 // returns bot info if the clientId matches a known bot, null otherwise: 503 match: function(agent) { 504 //console.info('BotMon.live.data.bots.match(',agent,')'); 505 506 const BotList = BotMon.live.data.bots._list; 507 508 // default is: not found! 509 let botInfo = null; 510 511 // check for known bots: 512 BotList.find(bot => { 513 let r = false; 514 for (let j=0; j<bot.rx.length; j++) { 515 const rxr = agent.match(new RegExp(bot.rx[j])); 516 if (rxr) { 517 botInfo = { 518 n : bot.n, 519 id: bot.id, 520 url: bot.url, 521 v: (rxr.length > 1 ? rxr[1] : -1) 522 }; 523 r = true; 524 break; 525 } 526 }; 527 return r; 528 }); 529 530 // check for unknown bots: 531 if (!botInfo) { 532 const botmatch = agent.match(/[^\s](\w*bot)[\/\s;\),$]/i); 533 if(botmatch) { 534 botInfo = {'id': "other", 'n': "Other", "bot": botmatch[0] }; 535 } 536 } 537 538 //console.log("botInfo:", botInfo); 539 return botInfo; 540 }, 541 542 543 // indicates if the list is loaded and ready to use: 544 _ready: false, 545 546 // the actual bot list is stored here: 547 _list: [] 548 }, 549 550 clients: { 551 // loads the list of known clients from a JSON file: 552 init: async function() { 553 //console.info('BotMon.live.data.clients.init()'); 554 555 // Load the list of known bots: 556 BotMon.live.gui.status.showBusy("Loading known clients"); 557 const url = BotMon._baseDir + 'data/known-clients.json'; 558 try { 559 const response = await fetch(url); 560 if (!response.ok) { 561 throw new Error(`${response.status} ${response.statusText}`); 562 } 563 564 BotMon.live.data.clients._list = await response.json(); 565 BotMon.live.data.clients._ready = true; 566 567 } catch (error) { 568 BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message); 569 } finally { 570 BotMon.live.gui.status.hideBusy("Status: Done."); 571 BotMon.live.data._dispatch('clients') 572 } 573 }, 574 575 // returns bot info if the user-agent matches a known bot, null otherwise: 576 match: function(agent) { 577 //console.info('BotMon.live.data.clients.match(',agent,')'); 578 579 let match = {"n": "Unknown", "v": -1, "id": null}; 580 581 if (agent) { 582 BotMon.live.data.clients._list.find(client => { 583 let r = false; 584 for (let j=0; j<client.rx.length; j++) { 585 const rxr = agent.match(new RegExp(client.rx[j])); 586 if (rxr) { 587 match.n = client.n; 588 match.v = (rxr.length > 1 ? rxr[1] : -1); 589 match.id = client.id || null; 590 r = true; 591 break; 592 } 593 } 594 return r; 595 }); 596 } 597 598 //console.log(match) 599 return match; 600 }, 601 602 // indicates if the list is loaded and ready to use: 603 _ready: false, 604 605 // the actual bot list is stored here: 606 _list: [] 607 608 }, 609 610 platforms: { 611 // loads the list of known platforms from a JSON file: 612 init: async function() { 613 //console.info('BotMon.live.data.platforms.init()'); 614 615 // Load the list of known bots: 616 BotMon.live.gui.status.showBusy("Loading known platforms"); 617 const url = BotMon._baseDir + 'data/known-platforms.json'; 618 try { 619 const response = await fetch(url); 620 if (!response.ok) { 621 throw new Error(`${response.status} ${response.statusText}`); 622 } 623 624 BotMon.live.data.platforms._list = await response.json(); 625 BotMon.live.data.platforms._ready = true; 626 627 } catch (error) { 628 BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message); 629 } finally { 630 BotMon.live.gui.status.hideBusy("Status: Done."); 631 BotMon.live.data._dispatch('platforms') 632 } 633 }, 634 635 // returns bot info if the browser id matches a known platform: 636 match: function(cid) { 637 //console.info('BotMon.live.data.platforms.match(',cid,')'); 638 639 let match = {"n": "Unknown", "id": null}; 640 641 if (cid) { 642 BotMon.live.data.platforms._list.find(platform => { 643 let r = false; 644 for (let j=0; j<platform.rx.length; j++) { 645 const rxr = cid.match(new RegExp(platform.rx[j])); 646 if (rxr) { 647 match.n = platform.n; 648 match.v = (rxr.length > 1 ? rxr[1] : -1); 649 match.id = platform.id || null; 650 r = true; 651 break; 652 } 653 } 654 return r; 655 }); 656 } 657 658 return match; 659 }, 660 661 // indicates if the list is loaded and ready to use: 662 _ready: false, 663 664 // the actual bot list is stored here: 665 _list: [] 666 667 }, 668 669 rules: { 670 // loads the list of rules and settings from a JSON file: 671 init: async function() { 672 //console.info('BotMon.live.data.rules.init()'); 673 674 // Load the list of known bots: 675 BotMon.live.gui.status.showBusy("Loading list of rules …"); 676 const url = BotMon._baseDir + 'data/rules.json'; 677 try { 678 const response = await fetch(url); 679 if (!response.ok) { 680 throw new Error(`${response.status} ${response.statusText}`); 681 } 682 683 const json = await response.json(); 684 685 if (json.rules) { 686 console.log(json.rules); 687 this._rulesList = json.rules; 688 } 689 690 if (json.threshold) { 691 this._threshold = json.threshold; 692 } 693 694 this._ready = true; 695 696 } catch (error) { 697 BotMon.live.gui.status.setError("Error while loading the ‘rules’ file: " + error.message); 698 } finally { 699 BotMon.live.gui.status.hideBusy("Status: Done."); 700 BotMon.live.data._dispatch('rules') 701 } 702 }, 703 704 _rulesList: [], // list of rules to find out if a visitor is a bot 705 _threshold: 100, // above this, it is considered a bot. 706 707 // returns a descriptive text for a rule id 708 getRuleInfo: function(ruleId) { 709 // console.info('getRuleInfo', ruleId); 710 711 // shortcut for neater code: 712 const me = BotMon.live.data.rules; 713 714 for (let i=0; i<me._rulesList.length; i++) { 715 const rule = me._rulesList[i]; 716 if (rule.id == ruleId) { 717 return rule; 718 } 719 } 720 return null; 721 722 }, 723 724 // evaluate a visitor for lkikelihood of being a bot 725 evaluate: function(visitor) { 726 727 // shortcut for neater code: 728 const me = BotMon.live.data.rules; 729 730 let r = { // evaluation result 731 'val': 0, 732 'rules': [], 733 'isBot': false 734 }; 735 736 for (let i=0; i<me._rulesList.length; i++) { 737 const rule = me._rulesList[i]; 738 const params = ( rule.params ? rule.params : [] ); 739 740 if (rule.func) { // rule is calling a function 741 if (me.func[rule.func]) { 742 if(me.func[rule.func](visitor, ...params)) { 743 r.val += rule.bot; 744 r.rules.push(rule.id) 745 } 746 } else { 747 //console.warn("Unknown rule function: “${rule.func}”. Ignoring rule.") 748 } 749 } 750 } 751 752 // is a bot? 753 r.isBot = (r.val >= me._threshold); 754 755 return r; 756 }, 757 758 // list of functions that can be called by the rules list to evaluate a visitor: 759 func: { 760 761 // check if client is one of the obsolete ones: 762 obsoleteClient: function(visitor) { 763 764 const obsClients = ['aol', 'msie', 'chromeold']; 765 const clientId = ( visitor._client ? visitor._client.id : ''); 766 return obsClients.includes(clientId); 767 }, 768 769 // check if OS/Platform is one of the obsolete ones: 770 obsoletePlatform: function(visitor) { 771 772 const obsPlatforms = ['winold', 'macosold']; 773 const platformId = ( visitor._platform ? visitor._platform.id : ''); 774 return obsPlatforms.includes(platformId); 775 }, 776 777 // client does not use JavaScript: 778 noJavaScript: function(visitor) { 779 return (visitor._jsClient === false); 780 }, 781 782 // are there at lest num pages loaded? 783 smallPageCount: function(visitor, num) { 784 return (visitor._pageViews.length <= Number(num)); 785 }, 786 787 // there are no ticks recorded for a visitor 788 // note that this will also trigger the "noJavaScript" rule: 789 noTicks: function(visitor) { 790 return visitor._seenBy.includes('tck'); 791 }, 792 793 // there are no references in any of the page visits: 794 noReferences: function(visitor) { 795 return (visitor._hasReferrer === true); 796 } 797 } 798 799 }, 800 801 loadLogFile: async function(type, onLoaded = undefined) { 802 console.info('BotMon.live.data.loadLogFile(',type,')'); 803 804 let typeName = ''; 805 let columns = []; 806 807 switch (type) { 808 case "srv": 809 typeName = "Server"; 810 columns = ['ts','ip','pg','id','typ','usr','agent','ref']; 811 break; 812 case "log": 813 typeName = "Page load"; 814 columns = ['ts','ip','pg','id','usr','lt','ref','agent']; 815 break; 816 case "tck": 817 typeName = "Ticker"; 818 columns = ['ts','ip','pg','id','agent']; 819 break; 820 default: 821 console.warn(`Unknown log type ${type}.`); 822 return; 823 } 824 825 // Show the busy indicator and set the visible status: 826 BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`); 827 828 // compose the URL from which to load: 829 const url = BotMon._baseDir + `logs/${BotMon._today}.${type}.txt`; 830 //console.log("Loading:",url); 831 832 // fetch the data: 833 try { 834 const response = await fetch(url); 835 if (!response.ok) { 836 throw new Error(`${response.status} ${response.statusText}`); 837 } 838 839 const logtxt = await response.text(); 840 841 logtxt.split('\n').forEach((line) => { 842 if (line.trim() === '') return; // skip empty lines 843 const cols = line.split('\t'); 844 845 // assign the columns to an object: 846 const data = {}; 847 cols.forEach( (colVal,i) => { 848 colName = columns[i] || `col${i}`; 849 const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim()); 850 data[colName] = colValue; 851 }); 852 853 // register the visit in the model: 854 switch(type) { 855 case 'srv': 856 BotMon.live.data.model.registerVisit(data, type); 857 break; 858 case 'log': 859 data.typ = 'js'; 860 BotMon.live.data.model.updateVisit(data); 861 break; 862 case 'tck': 863 data.typ = 'js'; 864 BotMon.live.data.model.updateTicks(data); 865 break; 866 default: 867 console.warn(`Unknown log type ${type}.`); 868 return; 869 } 870 }); 871 872 if (onLoaded) { 873 onLoaded(); // callback after loading is finished. 874 } 875 876 } catch (error) { 877 BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); 878 } finally { 879 BotMon.live.gui.status.hideBusy("Status: Done."); 880 } 881 } 882 }, 883 884 gui: { 885 init: function() { 886 // init the lists view: 887 this.lists.init(); 888 }, 889 890 overview: { 891 make: function() { 892 893 const data = BotMon.live.data.analytics.data; 894 const parent = document.getElementById('botmon__today__content'); 895 896 // shortcut for neater code: 897 const makeElement = BotMon.t._makeElement; 898 899 if (parent) { 900 901 const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 100); 902 903 jQuery(parent).prepend(jQuery(` 904 <details id="botmon__today__overview" open> 905 <summary>Overview</summary> 906 <div class="grid-3-columns"> 907 <dl> 908 <dt>Web metrics</dt> 909 <dd><span>Total page views:</span><strong>${data.totalPageViews}</strong></dd> 910 <dd><span>Total visitors (est.):</span><span>${data.totalVisits}</span></dd> 911 <dd><span>Bounce rate (est.):</span><span>${bounceRate}%</span></dd> 912 </dl> 913 <dl> 914 <dt>Bots vs. Humans</dt> 915 <dd><span>Registered users:</span><strong>${data.bots.users}</strong></dd> 916 <dd><span>Probably humans:</span><strong>${data.bots.human}</strong></dd> 917 <dd><span>Suspected bots:</span><strong>${data.bots.suspected}</strong></dd> 918 <dd><span>Known bots:</span><strong>${data.bots.known}</strong></dd> 919 </dl> 920 <dl id="botmon__botslist"></dl> 921 </div> 922 </details> 923 `)); 924 925 // update known bots list: 926 const block = document.getElementById('botmon__botslist'); 927 block.innerHTML = "<dt>Top known bots</dt>"; 928 929 let bots = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => { 930 return b._pageViews.length - a._pageViews.length; 931 }); 932 933 for (let i=0; i < Math.min(bots.length, 4); i++) { 934 const dd = makeElement('dd'); 935 dd.appendChild(makeElement('span', {'class': 'bot bot_' + bots[i]._bot.id}, bots[i]._bot.n)); 936 dd.appendChild(makeElement('strong', undefined, bots[i]._pageViews.length)); 937 block.appendChild(dd); 938 } 939 } 940 } 941 }, 942 943 status: { 944 setText: function(txt) { 945 const el = document.getElementById('botmon__today__status'); 946 if (el && BotMon.live.gui.status._errorCount <= 0) { 947 el.innerText = txt; 948 } 949 }, 950 951 setTitle: function(html) { 952 const el = document.getElementById('botmon__today__title'); 953 if (el) { 954 el.innerHTML = html; 955 } 956 }, 957 958 setError: function(txt) { 959 console.error(txt); 960 BotMon.live.gui.status._errorCount += 1; 961 const el = document.getElementById('botmon__today__status'); 962 if (el) { 963 el.innerText = "An error occured. See the browser log for details!"; 964 el.classList.add('error'); 965 } 966 }, 967 _errorCount: 0, 968 969 showBusy: function(txt = null) { 970 BotMon.live.gui.status._busyCount += 1; 971 const el = document.getElementById('botmon__today__busy'); 972 if (el) { 973 el.style.display = 'inline-block'; 974 } 975 if (txt) BotMon.live.gui.status.setText(txt); 976 }, 977 _busyCount: 0, 978 979 hideBusy: function(txt = null) { 980 const el = document.getElementById('botmon__today__busy'); 981 BotMon.live.gui.status._busyCount -= 1; 982 if (BotMon.live.gui.status._busyCount <= 0) { 983 if (el) el.style.display = 'none'; 984 if (txt) BotMon.live.gui.status.setText(txt); 985 } 986 } 987 }, 988 989 lists: { 990 init: function() { 991 992 const parent = document.getElementById('botmon__today__visitorlists'); 993 if (parent) { 994 995 for (let i=0; i < 4; i++) { 996 997 // change the id and title by number: 998 let listTitle = ''; 999 let listId = ''; 1000 switch (i) { 1001 case 0: 1002 listTitle = "Registered users"; 1003 listId = 'users'; 1004 break; 1005 case 1: 1006 listTitle = "Probably humans"; 1007 listId = 'humans'; 1008 break; 1009 case 2: 1010 listTitle = "Suspected bots"; 1011 listId = 'suspectedBots'; 1012 break; 1013 case 3: 1014 listTitle = "Known bots"; 1015 listId = 'knownBots'; 1016 break; 1017 default: 1018 console.warn('Unknwon list number.'); 1019 } 1020 1021 const details = BotMon.t._makeElement('details', { 1022 'data-group': listId, 1023 'data-loaded': false 1024 }); 1025 details.appendChild(BotMon.t._makeElement('summary', 1026 undefined, 1027 listTitle 1028 )); 1029 details.addEventListener("toggle", this._onDetailsToggle); 1030 1031 parent.appendChild(details); 1032 1033 } 1034 } 1035 }, 1036 1037 _onDetailsToggle: function(e) { 1038 //console.info('BotMon.live.gui.lists._onDetailsToggle()'); 1039 1040 const target = e.target; 1041 1042 if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet 1043 target.setAttribute('data-loaded', 'loading'); 1044 1045 const fillType = target.getAttribute('data-group'); 1046 const fillList = BotMon.live.data.analytics.groups[fillType]; 1047 if (fillList && fillList.length > 0) { 1048 1049 const ul = BotMon.t._makeElement('ul'); 1050 1051 fillList.forEach( (it) => { 1052 ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType)); 1053 }); 1054 1055 target.appendChild(ul); 1056 target.setAttribute('data-loaded', 'true'); 1057 } else { 1058 target.setAttribute('data-loaded', 'false'); 1059 } 1060 1061 } 1062 }, 1063 1064 _makeVisitorItem: function(data, type) { 1065 1066 // shortcut for neater code: 1067 const make = BotMon.t._makeElement; 1068 1069 let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); 1070 1071 const li = make('li'); // root list item 1072 const details = make('details'); 1073 const summary = make('summary'); 1074 details.appendChild(summary); 1075 1076 const span1 = make('span'); /* left-hand group */ 1077 1078 const platformName = (data._platform ? data._platform.n : 'Unknown'); 1079 const clientName = (data._client ? data._client.n: 'Unknown'); 1080 1081 if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */ 1082 1083 const botName = ( data._bot && data._bot.n ? data._bot.n : "Unknown"); 1084 span1.appendChild(make('span', { /* Bot */ 1085 'class': 'bot bot_' + (data._bot ? data._bot.id : 'unknown'), 1086 'title': "Bot: " + botName 1087 }, botName)); 1088 1089 } else if (data._type == BM_USERTYPE.KNOWN_USER) { /* User only */ 1090 1091 span1.appendChild(make('span', { /* User */ 1092 'class': 'user_known', 1093 'title': "User: " + data.usr 1094 }, data.usr)); 1095 1096 } else { /* others */ 1097 1098 if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; 1099 span1.appendChild(make('span', { /* IP-Address */ 1100 'class': 'ipaddr ip' + ipType, 1101 'title': "IP-Address: " + data.ip 1102 }, data.ip)); 1103 1104 } 1105 1106 if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* Not for bots */ 1107 span1.appendChild(make('span', { /* Platform */ 1108 'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'), 1109 'title': "Platform: " + platformName 1110 }, platformName)); 1111 1112 span1.appendChild(make('span', { /* Client */ 1113 'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'), 1114 'title': "Client: " + clientName 1115 }, clientName)); 1116 } 1117 1118 summary.appendChild(span1); 1119 const span2 = make('span'); /* right-hand group */ 1120 1121 span2.appendChild(make('span', { /* page views */ 1122 'class': 'pageviews' 1123 }, data._pageViews.length)); 1124 1125 summary.appendChild(span2); 1126 1127 // create expanable section: 1128 1129 const dl = make('dl', {'class': 'visitor_details'}); 1130 1131 if (data._type == BM_USERTYPE.KNOWN_BOT) { 1132 1133 dl.appendChild(make('dt', {}, "Bot name:")); /* bot info */ 1134 dl.appendChild(make('dd', {'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown')}, 1135 (data._bot ? data._bot.n : 'Unknown'))); 1136 1137 if (data._bot && data._bot.url) { 1138 dl.appendChild(make('dt', {}, "Bot info:")); /* bot info */ 1139 const botInfoDd = dl.appendChild(make('dd')); 1140 botInfoDd.appendChild(make('a', { 1141 'href': data._bot.url, 1142 'target': '_blank' 1143 }, data._bot.url)); /* bot info link*/ 1144 1145 } 1146 1147 } else { /* not for bots */ 1148 1149 dl.appendChild(make('dt', {}, "Client:")); /* client */ 1150 dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')}, 1151 clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) )); 1152 1153 dl.appendChild(make('dt', {}, "Platform:")); /* platform */ 1154 dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')}, 1155 platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) )); 1156 1157 dl.appendChild(make('dt', {}, "IP-Address:")); 1158 dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip)); 1159 1160 dl.appendChild(make('dt', {}, "ID:")); 1161 dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id)); 1162 } 1163 1164 if ((data._lastSeen - data._firstSeen) < 1) { 1165 dl.appendChild(make('dt', {}, "Seen:")); 1166 dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString())); 1167 } else { 1168 dl.appendChild(make('dt', {}, "First seen:")); 1169 dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString())); 1170 dl.appendChild(make('dt', {}, "Last seen:")); 1171 dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString())); 1172 } 1173 1174 dl.appendChild(make('dt', {}, "User-Agent:")); 1175 dl.appendChild(make('dd', {'class': 'agent' + ipType}, data.agent)); 1176 1177 dl.appendChild(make('dt', {}, "Visitor Type:")); 1178 dl.appendChild(make('dd', undefined, data._type )); 1179 1180 dl.appendChild(make('dt', {}, "Seen by:")); 1181 dl.appendChild(make('dd', undefined, data._seenBy.join(', ') )); 1182 1183 dl.appendChild(make('dt', {}, "Visited pages:")); 1184 const pagesDd = make('dd', {'class': 'pages'}); 1185 const pageList = make('ul'); 1186 data._pageViews.forEach( (page) => { 1187 const pgLi = make('li'); 1188 1189 let visitTimeStr = "Bounce"; 1190 const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); 1191 if (visitDuration > 0) { 1192 visitTimeStr = Math.floor(visitDuration / 1000) + "s"; 1193 } 1194 1195 pgLi.appendChild(make('span', {}, page.pg)); 1196 // pgLi.appendChild(make('span', {}, page.ref)); 1197 pgLi.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount)); 1198 pgLi.appendChild(make('span', {}, page._firstSeen.toLocaleString())); 1199 pgLi.appendChild(make('span', {}, page._lastSeen.toLocaleString())); 1200 pageList.appendChild(pgLi); 1201 }); 1202 pagesDd.appendChild(pageList); 1203 dl.appendChild(pagesDd); 1204 1205 dl.appendChild(make('dt', {}, "Evaluation:")); 1206 const evalDd = make('dd'); 1207 const testList = make('ul'); 1208 data._eval.forEach( (test) => { 1209 console.log(test); 1210 1211 const tObj = BotMon.live.data.rules.getRuleInfo(test); 1212 const tDesc = tObj ? tObj.desc : test; 1213 1214 const tstLi = make('li'); 1215 tstLi.appendChild(make('span', { 1216 'class': 'test test_' . test 1217 }, ( tObj ? tObj.desc : test ))); 1218 tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') )); 1219 testList.appendChild(tstLi); 1220 }); 1221 1222 const tst2Li = make('li'); 1223 tst2Li.appendChild(make('span', {}, "Total:")); 1224 tst2Li.appendChild(make('span', {}, data._botVal)); 1225 testList.appendChild(tst2Li); 1226 1227 evalDd.appendChild(testList); 1228 dl.appendChild(evalDd); 1229 1230 details.appendChild(dl); 1231 1232 li.appendChild(details); 1233 return li; 1234 } 1235 1236 } 1237 } 1238}; 1239 1240/* launch only if the BotMon admin panel is open: */ 1241if (document.getElementById('botmon__admin')) { 1242 BotMon.init(); 1243}