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.warn("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 this._rulesList = json.rules; 687 } 688 689 if (json.threshold) { 690 this._threshold = json.threshold; 691 } 692 693 this._ready = true; 694 695 } catch (error) { 696 BotMon.live.gui.status.setError("Error while loading the ‘rules’ file: " + error.message); 697 } finally { 698 BotMon.live.gui.status.hideBusy("Status: Done."); 699 BotMon.live.data._dispatch('rules') 700 } 701 }, 702 703 _rulesList: [], // list of rules to find out if a visitor is a bot 704 _threshold: 100, // above this, it is considered a bot. 705 706 // returns a descriptive text for a rule id 707 getRuleInfo: function(ruleId) { 708 // console.info('getRuleInfo', ruleId); 709 710 // shortcut for neater code: 711 const me = BotMon.live.data.rules; 712 713 for (let i=0; i<me._rulesList.length; i++) { 714 const rule = me._rulesList[i]; 715 if (rule.id == ruleId) { 716 return rule; 717 } 718 } 719 return null; 720 721 }, 722 723 // evaluate a visitor for lkikelihood of being a bot 724 evaluate: function(visitor) { 725 726 // shortcut for neater code: 727 const me = BotMon.live.data.rules; 728 729 let r = { // evaluation result 730 'val': 0, 731 'rules': [], 732 'isBot': false 733 }; 734 735 for (let i=0; i<me._rulesList.length; i++) { 736 const rule = me._rulesList[i]; 737 const params = ( rule.params ? rule.params : [] ); 738 739 if (rule.func) { // rule is calling a function 740 if (me.func[rule.func]) { 741 if(me.func[rule.func](visitor, ...params)) { 742 r.val += rule.bot; 743 r.rules.push(rule.id) 744 } 745 } else { 746 //console.warn("Unknown rule function: “${rule.func}”. Ignoring rule.") 747 } 748 } 749 } 750 751 // is a bot? 752 r.isBot = (r.val >= me._threshold); 753 754 return r; 755 }, 756 757 // list of functions that can be called by the rules list to evaluate a visitor: 758 func: { 759 760 // check if client is one of the obsolete ones: 761 obsoleteClient: function(visitor, ...clients) { 762 763 const clientId = ( visitor._client ? visitor._client.id : ''); 764 return clients.includes(clientId); 765 }, 766 767 // check if OS/Platform is one of the obsolete ones: 768 obsoletePlatform: function(visitor, ...platforms) { 769 770 const pId = ( visitor._platform ? visitor._platform.id : ''); 771 return platforms.includes(pId); 772 }, 773 774 // client does not use JavaScript: 775 noJavaScript: function(visitor) { 776 return (visitor._jsClient === false); 777 }, 778 779 // are there at lest num pages loaded? 780 smallPageCount: function(visitor, num) { 781 return (visitor._pageViews.length <= Number(num)); 782 }, 783 784 // there are no ticks recorded for a visitor 785 // note that this will also trigger the "noJavaScript" rule: 786 noTicks: function(visitor) { 787 return !visitor._seenBy.includes('tck'); 788 }, 789 790 // there are no references in any of the page visits: 791 noReferences: function(visitor) { 792 return (visitor._hasReferrer === true); 793 } 794 } 795 796 }, 797 798 loadLogFile: async function(type, onLoaded = undefined) { 799 //console.info('BotMon.live.data.loadLogFile(',type,')'); 800 801 let typeName = ''; 802 let columns = []; 803 804 switch (type) { 805 case "srv": 806 typeName = "Server"; 807 columns = ['ts','ip','pg','id','typ','usr','agent','ref']; 808 break; 809 case "log": 810 typeName = "Page load"; 811 columns = ['ts','ip','pg','id','usr','lt','ref','agent']; 812 break; 813 case "tck": 814 typeName = "Ticker"; 815 columns = ['ts','ip','pg','id','agent']; 816 break; 817 default: 818 console.warn(`Unknown log type ${type}.`); 819 return; 820 } 821 822 // Show the busy indicator and set the visible status: 823 BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`); 824 825 // compose the URL from which to load: 826 const url = BotMon._baseDir + `logs/${BotMon._today}.${type}.txt`; 827 //console.log("Loading:",url); 828 829 // fetch the data: 830 try { 831 const response = await fetch(url); 832 if (!response.ok) { 833 throw new Error(`${response.status} ${response.statusText}`); 834 } 835 836 const logtxt = await response.text(); 837 838 logtxt.split('\n').forEach((line) => { 839 if (line.trim() === '') return; // skip empty lines 840 const cols = line.split('\t'); 841 842 // assign the columns to an object: 843 const data = {}; 844 cols.forEach( (colVal,i) => { 845 colName = columns[i] || `col${i}`; 846 const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim()); 847 data[colName] = colValue; 848 }); 849 850 // register the visit in the model: 851 switch(type) { 852 case 'srv': 853 BotMon.live.data.model.registerVisit(data, type); 854 break; 855 case 'log': 856 data.typ = 'js'; 857 BotMon.live.data.model.updateVisit(data); 858 break; 859 case 'tck': 860 data.typ = 'js'; 861 BotMon.live.data.model.updateTicks(data); 862 break; 863 default: 864 console.warn(`Unknown log type ${type}.`); 865 return; 866 } 867 }); 868 869 if (onLoaded) { 870 onLoaded(); // callback after loading is finished. 871 } 872 873 } catch (error) { 874 BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); 875 } finally { 876 BotMon.live.gui.status.hideBusy("Status: Done."); 877 } 878 } 879 }, 880 881 gui: { 882 init: function() { 883 // init the lists view: 884 this.lists.init(); 885 }, 886 887 overview: { 888 make: function() { 889 890 const data = BotMon.live.data.analytics.data; 891 const parent = document.getElementById('botmon__today__content'); 892 893 // shortcut for neater code: 894 const makeElement = BotMon.t._makeElement; 895 896 if (parent) { 897 898 const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 100); 899 900 jQuery(parent).prepend(jQuery(` 901 <details id="botmon__today__overview" open> 902 <summary>Overview</summary> 903 <div class="grid-3-columns"> 904 <dl> 905 <dt>Web metrics</dt> 906 <dd><span>Total page views:</span><strong>${data.totalPageViews}</strong></dd> 907 <dd><span>Total visitors (est.):</span><span>${data.totalVisits}</span></dd> 908 <dd><span>Bounce rate (est.):</span><span>${bounceRate}%</span></dd> 909 </dl> 910 <dl> 911 <dt>Bots vs. Humans (page views)</dt> 912 <dd><span>Registered users:</span><strong>${data.bots.users}</strong></dd> 913 <dd><span>Probably humans:</span><strong>${data.bots.human}</strong></dd> 914 <dd><span>Suspected bots:</span><strong>${data.bots.suspected}</strong></dd> 915 <dd><span>Known bots:</span><strong>${data.bots.known}</strong></dd> 916 </dl> 917 <dl id="botmon__botslist"></dl> 918 </div> 919 </details> 920 `)); 921 922 // update known bots list: 923 const block = document.getElementById('botmon__botslist'); 924 block.innerHTML = "<dt>Top known bots (page views)</dt>"; 925 926 let bots = BotMon.live.data.analytics.groups.knownBots.toSorted( (a, b) => { 927 return b._pageViews.length - a._pageViews.length; 928 }); 929 930 for (let i=0; i < Math.min(bots.length, 4); i++) { 931 const dd = makeElement('dd'); 932 dd.appendChild(makeElement('span', {'class': 'bot bot_' + bots[i]._bot.id}, bots[i]._bot.n)); 933 dd.appendChild(makeElement('strong', undefined, bots[i]._pageViews.length)); 934 block.appendChild(dd); 935 } 936 } 937 } 938 }, 939 940 status: { 941 setText: function(txt) { 942 const el = document.getElementById('botmon__today__status'); 943 if (el && BotMon.live.gui.status._errorCount <= 0) { 944 el.innerText = txt; 945 } 946 }, 947 948 setTitle: function(html) { 949 const el = document.getElementById('botmon__today__title'); 950 if (el) { 951 el.innerHTML = html; 952 } 953 }, 954 955 setError: function(txt) { 956 console.error(txt); 957 BotMon.live.gui.status._errorCount += 1; 958 const el = document.getElementById('botmon__today__status'); 959 if (el) { 960 el.innerText = "An error occured. See the browser log for details!"; 961 el.classList.add('error'); 962 } 963 }, 964 _errorCount: 0, 965 966 showBusy: function(txt = null) { 967 BotMon.live.gui.status._busyCount += 1; 968 const el = document.getElementById('botmon__today__busy'); 969 if (el) { 970 el.style.display = 'inline-block'; 971 } 972 if (txt) BotMon.live.gui.status.setText(txt); 973 }, 974 _busyCount: 0, 975 976 hideBusy: function(txt = null) { 977 const el = document.getElementById('botmon__today__busy'); 978 BotMon.live.gui.status._busyCount -= 1; 979 if (BotMon.live.gui.status._busyCount <= 0) { 980 if (el) el.style.display = 'none'; 981 if (txt) BotMon.live.gui.status.setText(txt); 982 } 983 } 984 }, 985 986 lists: { 987 init: function() { 988 989 // function shortcut: 990 const makeElement = BotMon.t._makeElement; 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 let group = BotMon.live.data.analytics.groups[listId]; 1021 // todo: add group information! 1022 1023 const details = makeElement('details', { 1024 'data-group': listId, 1025 'data-loaded': false 1026 }); 1027 const title = details.appendChild(makeElement('summary')); 1028 title.appendChild(makeElement('span', {'class':'title'}, '–')) 1029 title.appendChild(makeElement('span', {'class':'counter'}, gCount)) 1030 details.addEventListener("toggle", this._onDetailsToggle); 1031 1032 parent.appendChild(details); 1033 1034 } 1035 } 1036 }, 1037 1038 _onDetailsToggle: function(e) { 1039 //console.info('BotMon.live.gui.lists._onDetailsToggle()'); 1040 1041 const target = e.target; 1042 1043 if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet 1044 target.setAttribute('data-loaded', 'loading'); 1045 1046 const fillType = target.getAttribute('data-group'); 1047 const fillList = BotMon.live.data.analytics.groups[fillType]; 1048 if (fillList && fillList.length > 0) { 1049 1050 const ul = BotMon.t._makeElement('ul'); 1051 1052 fillList.forEach( (it) => { 1053 ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType)); 1054 }); 1055 1056 target.appendChild(ul); 1057 target.setAttribute('data-loaded', 'true'); 1058 } else { 1059 target.setAttribute('data-loaded', 'false'); 1060 } 1061 1062 } 1063 }, 1064 1065 _makeVisitorItem: function(data, type) { 1066 1067 // shortcut for neater code: 1068 const make = BotMon.t._makeElement; 1069 1070 let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); 1071 1072 const li = make('li'); // root list item 1073 const details = make('details'); 1074 const summary = make('summary'); 1075 details.appendChild(summary); 1076 1077 const span1 = make('span'); /* left-hand group */ 1078 1079 const platformName = (data._platform ? data._platform.n : 'Unknown'); 1080 const clientName = (data._client ? data._client.n: 'Unknown'); 1081 1082 if (data._type == BM_USERTYPE.KNOWN_BOT) { /* Bot only */ 1083 1084 const botName = ( data._bot && data._bot.n ? data._bot.n : "Unknown"); 1085 span1.appendChild(make('span', { /* Bot */ 1086 'class': 'bot bot_' + (data._bot ? data._bot.id : 'unknown'), 1087 'title': "Bot: " + botName 1088 }, botName)); 1089 1090 } else if (data._type == BM_USERTYPE.KNOWN_USER) { /* User only */ 1091 1092 span1.appendChild(make('span', { /* User */ 1093 'class': 'user_known', 1094 'title': "User: " + data.usr 1095 }, data.usr)); 1096 1097 } else { /* others */ 1098 1099 if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; 1100 span1.appendChild(make('span', { /* IP-Address */ 1101 'class': 'ipaddr ip' + ipType, 1102 'title': "IP-Address: " + data.ip 1103 }, data.ip)); 1104 1105 } 1106 1107 if (data._type !== BM_USERTYPE.KNOWN_BOT) { /* Not for bots */ 1108 span1.appendChild(make('span', { /* Platform */ 1109 'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'), 1110 'title': "Platform: " + platformName 1111 }, platformName)); 1112 1113 span1.appendChild(make('span', { /* Client */ 1114 'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'), 1115 'title': "Client: " + clientName 1116 }, clientName)); 1117 } 1118 1119 summary.appendChild(span1); 1120 const span2 = make('span'); /* right-hand group */ 1121 1122 span2.appendChild(make('span', { /* page views */ 1123 'class': 'pageviews' 1124 }, data._pageViews.length)); 1125 1126 summary.appendChild(span2); 1127 1128 // create expanable section: 1129 1130 const dl = make('dl', {'class': 'visitor_details'}); 1131 1132 if (data._type == BM_USERTYPE.KNOWN_BOT) { 1133 1134 dl.appendChild(make('dt', {}, "Bot name:")); /* bot info */ 1135 dl.appendChild(make('dd', {'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown')}, 1136 (data._bot ? data._bot.n : 'Unknown'))); 1137 1138 if (data._bot && data._bot.url) { 1139 dl.appendChild(make('dt', {}, "Bot info:")); /* bot info */ 1140 const botInfoDd = dl.appendChild(make('dd')); 1141 botInfoDd.appendChild(make('a', { 1142 'href': data._bot.url, 1143 'target': '_blank' 1144 }, data._bot.url)); /* bot info link*/ 1145 1146 } 1147 1148 } else { /* not for bots */ 1149 1150 dl.appendChild(make('dt', {}, "Client:")); /* client */ 1151 dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')}, 1152 clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) )); 1153 1154 dl.appendChild(make('dt', {}, "Platform:")); /* platform */ 1155 dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')}, 1156 platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) )); 1157 1158 dl.appendChild(make('dt', {}, "IP-Address:")); 1159 dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip)); 1160 1161 dl.appendChild(make('dt', {}, "ID:")); 1162 dl.appendChild(make('dd', {'class': 'has_icon ip' + data.typ}, data.id)); 1163 } 1164 1165 if ((data._lastSeen - data._firstSeen) < 1) { 1166 dl.appendChild(make('dt', {}, "Seen:")); 1167 dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString())); 1168 } else { 1169 dl.appendChild(make('dt', {}, "First seen:")); 1170 dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString())); 1171 dl.appendChild(make('dt', {}, "Last seen:")); 1172 dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString())); 1173 } 1174 1175 dl.appendChild(make('dt', {}, "User-Agent:")); 1176 dl.appendChild(make('dd', {'class': 'agent' + ipType}, data.agent)); 1177 1178 dl.appendChild(make('dt', {}, "Visitor Type:")); 1179 dl.appendChild(make('dd', undefined, data._type )); 1180 1181 dl.appendChild(make('dt', {}, "Seen by:")); 1182 dl.appendChild(make('dd', undefined, data._seenBy.join(', ') )); 1183 1184 dl.appendChild(make('dt', {}, "Visited pages:")); 1185 const pagesDd = make('dd', {'class': 'pages'}); 1186 const pageList = make('ul'); 1187 data._pageViews.forEach( (page) => { 1188 const pgLi = make('li'); 1189 1190 let visitTimeStr = "Bounce"; 1191 const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); 1192 if (visitDuration > 0) { 1193 visitTimeStr = Math.floor(visitDuration / 1000) + "s"; 1194 } 1195 1196 console.log(page); 1197 1198 pgLi.appendChild(make('span', {}, page.pg)); 1199 // pgLi.appendChild(make('span', {}, page.ref)); 1200 pgLi.appendChild(make('span', {}, ( page._seenBy ? page._seenBy.join(', ') : '—') + '; ' + page._tickCount)); 1201 pgLi.appendChild(make('span', {}, page._firstSeen.toLocaleString())); 1202 pgLi.appendChild(make('span', {}, page._lastSeen.toLocaleString())); 1203 pageList.appendChild(pgLi); 1204 }); 1205 pagesDd.appendChild(pageList); 1206 dl.appendChild(pagesDd); 1207 1208 if (data._eval) { 1209 dl.appendChild(make('dt', {}, "Evaluation:")); 1210 const evalDd = make('dd'); 1211 const testList = make('ul',{ 1212 'class': 'eval' 1213 }); 1214 data._eval.forEach( (test) => { 1215 1216 const tObj = BotMon.live.data.rules.getRuleInfo(test); 1217 const tDesc = tObj ? tObj.desc : test; 1218 1219 const tstLi = make('li'); 1220 tstLi.appendChild(make('span', { 1221 'class': 'test test_' . test 1222 }, ( tObj ? tObj.desc : test ))); 1223 tstLi.appendChild(make('span', {}, ( tObj ? tObj.bot : '—') )); 1224 testList.appendChild(tstLi); 1225 }); 1226 1227 const tst2Li = make('li', { 1228 'class': 'total' 1229 }); 1230 tst2Li.appendChild(make('span', {}, "Total:")); 1231 tst2Li.appendChild(make('span', {}, data._botVal)); 1232 testList.appendChild(tst2Li); 1233 1234 evalDd.appendChild(testList); 1235 dl.appendChild(evalDd); 1236 } 1237 1238 details.appendChild(dl); 1239 1240 li.appendChild(details); 1241 return li; 1242 } 1243 1244 } 1245 } 1246}; 1247 1248/* launch only if the BotMon admin panel is open: */ 1249if (document.getElementById('botmon__admin')) { 1250 BotMon.init(); 1251}