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 6const BotMon = { 7 8 init: function() { 9 //console.info('BotMon.init()'); 10 11 // find the plugin basedir: 12 this._baseDir = document.currentScript.src.substring(0, document.currentScript.src.indexOf('/exe/')) 13 + '/plugins/botmon/'; 14 15 // read the page language from the DOM: 16 this._lang = document.getRootNode().documentElement.lang || this._lang; 17 18 // get the time offset: 19 this._timeDiff = BotMon.t._getTimeOffset(); 20 21 // init the sub-objects: 22 BotMon.t._callInit(this); 23 }, 24 25 _baseDir: null, 26 _lang: 'en', 27 _today: (new Date()).toISOString().slice(0, 10), 28 _timeDiff: '', 29 30 /* internal tools */ 31 t: { 32 33 /* helper function to call inits of sub-objects */ 34 _callInit: function(obj) { 35 //console.info('BotMon.t._callInit(obj=',obj,')'); 36 37 /* call init / _init on each sub-object: */ 38 Object.keys(obj).forEach( (key,i) => { 39 const sub = obj[key]; 40 let init = null; 41 if (typeof sub === 'object' && sub.init) { 42 init = sub.init; 43 } 44 45 // bind to object 46 if (typeof init == 'function') { 47 const init2 = init.bind(sub); 48 init2(obj); 49 } 50 }); 51 }, 52 53 /* helper function to calculate the time difference to UTC: */ 54 _getTimeOffset: function() { 55 const now = new Date(); 56 let offset = now.getTimezoneOffset(); // in minutes 57 const sign = Math.sign(offset); // +1 or -1 58 offset = Math.abs(offset); // always positive 59 60 let hours = 0; 61 while (offset >= 60) { 62 hours += 1; 63 offset -= 60; 64 } 65 return ( hours > 0 ? sign * hours + ' h' : '') + (offset > 0 ? ` ${offset} min` : ''); 66 }, 67 68 /* helper function to create a new element with all attributes and text content */ 69 _makeElement: function(name, atlist = undefined, text = undefined) { 70 var r = null; 71 try { 72 r = document.createElement(name); 73 if (atlist) { 74 for (let attr in atlist) { 75 r.setAttribute(attr, atlist[attr]); 76 } 77 } 78 if (text) { 79 r.textContent = text.toString(); 80 } 81 } catch(e) { 82 console.error(e); 83 } 84 return r; 85 } 86 } 87}; 88 89/* everything specific to the "Today" tab is self-contained in the "live" object: */ 90BotMon.live = { 91 init: function() { 92 //console.info('BotMon.live.init()'); 93 94 // set the title: 95 const tDiff = '(<abbr title="Coordinated Universal Time">UTC</abbr>' + (BotMon._timeDiff != '' ? `, ${BotMon._timeDiff}` : '' ) + ')'; 96 BotMon.live.gui.status.setTitle(`Data for <time datetime=${BotMon._today}>${BotMon._today}</time> ${tDiff}`); 97 98 // init sub-objects: 99 BotMon.t._callInit(this); 100 }, 101 102 data: { 103 init: function() { 104 //console.info('BotMon.live.data.init()'); 105 106 // call sub-inits: 107 BotMon.t._callInit(this); 108 }, 109 110 // this will be called when the known json files are done loading: 111 _dispatch: function(file) { 112 //console.info('BotMon.live.data._dispatch(,',file,')'); 113 114 // shortcut to make code more readable: 115 const data = BotMon.live.data; 116 117 // set the flags: 118 switch(file) { 119 case 'bots': 120 data._dispatchBotsLoaded = true; 121 break; 122 case 'clients': 123 data._dispatchClientsLoaded = true; 124 break; 125 case 'platforms': 126 data._dispatchPlatformsLoaded = true; 127 break; 128 default: 129 // ignore 130 } 131 132 // are all the flags set? 133 if (data._dispatchBotsLoaded && data._dispatchClientsLoaded && data._dispatchPlatformsLoaded) { 134 // chain the log files loading: 135 BotMon.live.data.loadLogFile('srv', BotMon.live.data._onServerLogLoaded); 136 } 137 }, 138 // flags to track which data files have been loaded: 139 _dispatchBotsLoaded: false, 140 _dispatchClientsLoaded: false, 141 _dispatchPlatformsLoaded: false, 142 143 // event callback, after the server log has been loaded: 144 _onServerLogLoaded: function() { 145 //console.info('BotMon.live.data._onServerLogLoaded()'); 146 147 // chain the client log file to load: 148 BotMon.live.data.loadLogFile('log', BotMon.live.data._onClientLogLoaded); 149 }, 150 151 // event callback, after the client log has been loaded: 152 _onClientLogLoaded: function() { 153 //console.info('BotMon.live.data._onClientLogLoaded()'); 154 155 // chain the ticks file to load: 156 BotMon.live.data.loadLogFile('tck', BotMon.live.data._onTicksLogLoaded); 157 158 }, 159 160 // event callback, after the tiker log has been loaded: 161 _onTicksLogLoaded: function() { 162 //console.info('BotMon.live.data._onTicksLogLoaded()'); 163 164 // analyse the data: 165 BotMon.live.data.analytics.analyseAll(); 166 167 // sort the data: 168 // #TODO 169 170 // display the data: 171 BotMon.live.gui.overview.make(); 172 173 //console.log(BotMon.live.data.model._visitors); 174 175 }, 176 177 model: { 178 // visitors storage: 179 _visitors: [], 180 181 // find an already existing visitor record: 182 findVisitor: function(id) { 183 184 // shortcut to make code more readable: 185 const model = BotMon.live.data.model; 186 187 // loop over all visitors already registered: 188 for (let i=0; i<model._visitors.length; i++) { 189 const v = model._visitors[i]; 190 if (v && v.id == id) return v; 191 } 192 return null; // nothing found 193 }, 194 195 /* if there is already this visit registered, return it (used for updates) */ 196 _getVisit: function(visit, view) { 197 198 // shortcut to make code more readable: 199 const model = BotMon.live.data.model; 200 201 202 for (let i=0; i<visit._pageViews.length; i++) { 203 const pv = visit._pageViews[i]; 204 if (pv.pg == view.pg && // same page id, and 205 view.ts.getTime() - pv._firstSeen.getTime() < 1200000) { // seen less than 20 minutes ago 206 return pv; // it is the same visit. 207 } 208 } 209 return null; // not found 210 }, 211 212 // register a new visitor (or update if already exists) 213 registerVisit: function(dat, type) { 214 console.info('registerVisit', dat); 215 216 // shortcut to make code more readable: 217 const model = BotMon.live.data.model; 218 219 // is it a known bot? 220 const bot = BotMon.live.data.bots.match(dat.agent); 221 222 // which user id to use: 223 let visitorId = dat.id; // default is the session ID 224 if (bot) visitorId = bot.id; // use bot ID if known bot 225 if (dat.usr !== '') visitorId = 'usr'; // use user ID if known user 226 227 // check if it already exists: 228 let visitor = model.findVisitor(dat.id); 229 if (!visitor) { 230 231 // override the visitor type? 232 let visitorType = dat.typ; 233 if (bot) visitorType = 'bot'; 234 235 model._visitors.push(dat); 236 visitor = dat; 237 visitor.id = visitorId; 238 visitor._firstSeen = dat.ts; 239 visitor._lastSeen = dat.ts; 240 visitor._seenBy = [type]; 241 visitor._pageViews = []; // array of page views 242 visitor._hasReferrer = false; // has at least one referrer 243 visitor._jsClient = false; // visitor has been seen logged by client js as well 244 visitor._client = BotMon.live.data.clients.match(dat.agent) ?? null; // client info 245 visitor._bot = bot ?? null; // bot info 246 visitor._platform = BotMon.live.data.platforms.match(dat.agent); // platform info 247 visitor._type = visitorType; 248 } 249 250 // find browser 251 252 // is this visit already registered? 253 let prereg = model._getVisit(visitor, dat); 254 if (!prereg) { 255 // add the page view to the visitor: 256 prereg = { 257 _by: 'srv', 258 ip: dat.ip, 259 pg: dat.pg, 260 ref: dat.ref || '', 261 _firstSeen: dat.ts, 262 _lastSeen: dat.ts, 263 _jsClient: false 264 }; 265 visitor._pageViews.push(prereg); 266 } 267 268 // update referrer state: 269 visitor._hasReferrer = visitor._hasReferrer || 270 (prereg.ref !== undefined && prereg.ref !== ''); 271 272 // update time stamp for last-seen: 273 visitor._lastSeen = dat.ts; 274 275 // if needed: 276 return visitor; 277 }, 278 279 // updating visit data from the client-side log: 280 updateVisit: function(dat) { 281 //console.info('updateVisit', dat); 282 283 // shortcut to make code more readable: 284 const model = BotMon.live.data.model; 285 286 const type = 'log'; 287 288 let visitor = BotMon.live.data.model.findVisitor(dat.id); 289 if (!visitor) { 290 visitor = model.registerVisit(dat, type); 291 visitor._seenBy = [type]; 292 } 293 if (visitor) { 294 295 // prime the "seen by" list: 296 seenBy = ( visitor._seenBy ? ( visitor._seenBy.includes(type) ? visitor._seenBy : [...visitor._seenBy, type] ) : [type] ); 297 298 visitor._lastSeen = dat.ts; 299 visitor._seenBy = seenBy; 300 visitor._jsClient = true; // seen by client js 301 } 302 303 // find the page view: 304 let prereg = BotMon.live.data.model._getVisit(visitor, dat); 305 if (prereg) { 306 // update the page view: 307 prereg._lastSeen = dat.ts; 308 prereg._jsClient = true; // seen by client js 309 } else { 310 // add the page view to the visitor: 311 prereg = { 312 _by: 'log', 313 ip: dat.ip, 314 pg: dat.pg, 315 ref: dat.ref || '', 316 _firstSeen: dat.ts, 317 _lastSeen: dat.ts, 318 _jsClient: true 319 }; 320 visitor._pageViews.push(prereg); 321 } 322 }, 323 324 // updating visit data from the ticker log: 325 updateTicks: function(dat) { 326 //console.info('updateTicks', dat); 327 328 // shortcut to make code more readable: 329 const model = BotMon.live.data.model; 330 331 // find the visit info: 332 let visitor = model.findVisitor(dat.id); 333 if (!visitor) { 334 console.warn(`No visitor with ID ${dat.id}, registering a new one.`); 335 visitor = model.registerVisit(dat, 'tck'); 336 } 337 if (visitor) { 338 // update visitor: 339 if (visitor._lastSeen < dat.ts) visitor._lastSeen = dat.ts; 340 if (!visitor._seenBy.includes('tck')) visitor._seenBy.push('tck'); 341 342 // get the page view info: 343 const pv = model._getVisit(visitor, dat); 344 if (pv) { 345 // update the page view info: 346 if (pv._lastSeen.getTime() < dat.ts.getTime()) pv._lastSeen = dat.ts; 347 } else { 348 console.warn(`No page view for visit ID ${dat.id}, page ${dat.pg}, registering a new one.`); 349 350 // add a new page view to the visitor: 351 const newPv = { 352 _by: 'tck', 353 ip: dat.ip, 354 pg: dat.pg, 355 ref: '', 356 _firstSeen: dat.ts, 357 _lastSeen: dat.ts, 358 _jsClient: false 359 }; 360 visitor._pageViews.push(newPv); 361 } 362 363 } else { 364 console.warn(`No visit with ID ${dat.id}.`); 365 return; 366 } 367 368 } 369 }, 370 371 analytics: { 372 373 init: function() { 374 console.info('BotMon.live.data.analytics.init()'); 375 }, 376 377 // data storage: 378 data: { 379 totalVisits: 0, 380 totalPageViews: 0, 381 bots: { 382 known: 0, 383 suspected: 0, 384 human: 0, 385 users: 0 386 } 387 }, 388 389 // sort the visits by type: 390 groups: { 391 knownBots: [], 392 suspectedBots: [], 393 humans: [], 394 users: [] 395 }, 396 397 // all analytics 398 analyseAll: function() { 399 //console.info('BotMon.live.data.analytics.analyseAll()'); 400 401 // shortcut to make code more readable: 402 const model = BotMon.live.data.model; 403 console.log(model._visitors); 404 405 // loop over all visitors: 406 model._visitors.forEach( (v) => { 407 408 // count visits and page views: 409 this.data.totalVisits += 1; 410 this.data.totalPageViews += v._pageViews.length; 411 412 // check for typical bot aspects: 413 let botScore = 0; 414 415 /*if (v._isBot >= 1.0) { // known bots 416 417 this.data.bots.known += 1; 418 this.groups.knownBots.push(v); 419 420 } if (v.usr && v.usr != '') { // known users */ 421 this.groups.users.push(v); 422 this.data.bots.users += 1; 423 /*} else { 424 // not a known bot, nor a known user; check other aspects: 425 426 // no referrer at all: 427 if (!v._hasReferrer) botScore += 0.2; 428 429 // no js client logging: 430 if (!v._jsClient) botScore += 0.2; 431 432 // average time between page views less than 30s: 433 if (v._pageViews.length > 1) { 434 botScore -= 0.2; // more than one view: good! 435 let totalDiff = 0; 436 for (let i=1; i<v._pageViews.length; i++) { 437 const diff = v._pageViews[i]._firstSeen.getTime() - v._pageViews[i-1]._lastSeen.getTime(); 438 totalDiff += diff; 439 } 440 const avgDiff = totalDiff / (v._pageViews.length - 1); 441 if (avgDiff < 30000) botScore += 0.2; 442 else if (avgDiff < 60000) botScore += 0.1; 443 } 444 445 // decide based on the score: 446 if (botScore >= 0.5) { 447 this.data.bots.suspected += 1; 448 this.groups.suspectedBots.push(v); 449 } else { 450 this.data.bots.human += 1; 451 this.groups.humans.push(v); 452 } 453 }*/ 454 }); 455 456 console.log(this.data); 457 console.log(this.groups); 458 } 459 460 }, 461 462 bots: { 463 // loads the list of known bots from a JSON file: 464 init: async function() { 465 //console.info('BotMon.live.data.bots.init()'); 466 467 // Load the list of known bots: 468 BotMon.live.gui.status.showBusy("Loading known bots …"); 469 const url = BotMon._baseDir + 'data/known-bots.json'; 470 try { 471 const response = await fetch(url); 472 if (!response.ok) { 473 throw new Error(`${response.status} ${response.statusText}`); 474 } 475 476 this._list = await response.json(); 477 this._ready = true; 478 479 } catch (error) { 480 BotMon.live.gui.status.setError("Error while loading the ‘known bots’ file: " + error.message); 481 } finally { 482 BotMon.live.gui.status.hideBusy("Status: Done."); 483 BotMon.live.data._dispatch('bots') 484 } 485 }, 486 487 // returns bot info if the clientId matches a known bot, null otherwise: 488 match: function(agent) { 489 //console.info('BotMon.live.data.bots.match(',agent,')'); 490 491 const BotList = BotMon.live.data.bots._list; 492 493 // default is: not found! 494 let botInfo = null; 495 496 // check for known bots: 497 if (agent) { 498 BotList.find(bot => { 499 let r = false; 500 for (let j=0; j<bot.rx.length; j++) { 501 const rxr = agent.match(new RegExp(bot.rx[j])); 502 if (rxr) { 503 botInfo = { 504 n : bot.n, 505 id: bot.id, 506 url: bot.url, 507 v: (rxr.length > 1 ? rxr[1] : -1) 508 } 509 r = true; 510 break; 511 } 512 } 513 return r; 514 }); 515 } 516 517 //console.log("botInfo:", botInfo); 518 return botInfo; 519 }, 520 521 522 // indicates if the list is loaded and ready to use: 523 _ready: false, 524 525 // the actual bot list is stored here: 526 _list: [] 527 }, 528 529 clients: { 530 // loads the list of known clients from a JSON file: 531 init: async function() { 532 //console.info('BotMon.live.data.clients.init()'); 533 534 // Load the list of known bots: 535 BotMon.live.gui.status.showBusy("Loading known clients"); 536 const url = BotMon._baseDir + 'data/known-clients.json'; 537 try { 538 const response = await fetch(url); 539 if (!response.ok) { 540 throw new Error(`${response.status} ${response.statusText}`); 541 } 542 543 BotMon.live.data.clients._list = await response.json(); 544 BotMon.live.data.clients._ready = true; 545 546 } catch (error) { 547 BotMon.live.gui.status.setError("Error while loading the known clients file: " + error.message); 548 } finally { 549 BotMon.live.gui.status.hideBusy("Status: Done."); 550 BotMon.live.data._dispatch('clients') 551 } 552 }, 553 554 // returns bot info if the user-agent matches a known bot, null otherwise: 555 match: function(agent) { 556 //console.info('BotMon.live.data.clients.match(',agent,')'); 557 558 let match = {"n": "Unknown", "v": -1, "id": null}; 559 560 if (agent) { 561 BotMon.live.data.clients._list.find(client => { 562 let r = false; 563 for (let j=0; j<client.rx.length; j++) { 564 const rxr = agent.match(new RegExp(client.rx[j])); 565 if (rxr) { 566 match.n = client.n; 567 match.v = (rxr.length > 1 ? rxr[1] : -1); 568 match.id = client.id || null; 569 r = true; 570 break; 571 } 572 } 573 return r; 574 }); 575 } 576 577 //console.log(match) 578 return match; 579 }, 580 581 // indicates if the list is loaded and ready to use: 582 _ready: false, 583 584 // the actual bot list is stored here: 585 _list: [] 586 587 }, 588 589 platforms: { 590 // loads the list of known platforms from a JSON file: 591 init: async function() { 592 //console.info('BotMon.live.data.platforms.init()'); 593 594 // Load the list of known bots: 595 BotMon.live.gui.status.showBusy("Loading known platforms"); 596 const url = BotMon._baseDir + 'data/known-platforms.json'; 597 try { 598 const response = await fetch(url); 599 if (!response.ok) { 600 throw new Error(`${response.status} ${response.statusText}`); 601 } 602 603 BotMon.live.data.platforms._list = await response.json(); 604 BotMon.live.data.platforms._ready = true; 605 606 } catch (error) { 607 BotMon.live.gui.status.setError("Error while loading the known platforms file: " + error.message); 608 } finally { 609 BotMon.live.gui.status.hideBusy("Status: Done."); 610 BotMon.live.data._dispatch('platforms') 611 } 612 }, 613 614 // returns bot info if the browser id matches a known platform: 615 match: function(cid) { 616 //console.info('BotMon.live.data.platforms.match(',cid,')'); 617 618 let match = {"n": "Unknown", "id": null}; 619 620 if (cid) { 621 BotMon.live.data.platforms._list.find(platform => { 622 let r = false; 623 for (let j=0; j<platform.rx.length; j++) { 624 const rxr = cid.match(new RegExp(platform.rx[j])); 625 if (rxr) { 626 match.n = platform.n; 627 match.v = (rxr.length > 1 ? rxr[1] : -1); 628 match.id = platform.id || null; 629 r = true; 630 break; 631 } 632 } 633 return r; 634 }); 635 } 636 637 return match; 638 }, 639 640 // indicates if the list is loaded and ready to use: 641 _ready: false, 642 643 // the actual bot list is stored here: 644 _list: [] 645 646 }, 647 648 loadLogFile: async function(type, onLoaded = undefined) { 649 console.info('BotMon.live.data.loadLogFile(',type,')'); 650 651 let typeName = ''; 652 let columns = []; 653 654 switch (type) { 655 case "srv": 656 typeName = "Server"; 657 columns = ['ts','ip','pg','id','typ','usr','agent','ref']; 658 break; 659 case "log": 660 typeName = "Page load"; 661 columns = ['ts','ip','pg','id','usr','lt','ref','agent']; 662 break; 663 case "tck": 664 typeName = "Ticker"; 665 columns = ['ts','ip','pg','id','agent']; 666 break; 667 default: 668 console.warn(`Unknown log type ${type}.`); 669 return; 670 } 671 672 // Show the busy indicator and set the visible status: 673 BotMon.live.gui.status.showBusy(`Loading ${typeName} log file …`); 674 675 // compose the URL from which to load: 676 const url = BotMon._baseDir + `logs/${BotMon._today}.${type}.txt`; 677 //console.log("Loading:",url); 678 679 // fetch the data: 680 try { 681 const response = await fetch(url); 682 if (!response.ok) { 683 throw new Error(`${response.status} ${response.statusText}`); 684 } 685 686 const logtxt = await response.text(); 687 688 logtxt.split('\n').forEach((line) => { 689 if (line.trim() === '') return; // skip empty lines 690 const cols = line.split('\t'); 691 692 // assign the columns to an object: 693 const data = {}; 694 cols.forEach( (colVal,i) => { 695 colName = columns[i] || `col${i}`; 696 const colValue = (colName == 'ts' ? new Date(colVal) : colVal.trim()); 697 data[colName] = colValue; 698 }); 699 700 // register the visit in the model: 701 switch(type) { 702 case 'srv': 703 BotMon.live.data.model.registerVisit(data, type); 704 break; 705 case 'log': 706 data.typ = 'js'; 707 BotMon.live.data.model.updateVisit(data); 708 break; 709 case 'tck': 710 data.typ = 'js'; 711 BotMon.live.data.model.updateTicks(data); 712 break; 713 default: 714 console.warn(`Unknown log type ${type}.`); 715 return; 716 } 717 }); 718 719 if (onLoaded) { 720 onLoaded(); // callback after loading is finished. 721 } 722 723 } catch (error) { 724 BotMon.live.gui.status.setError(`Error while loading the ${typeName} log file: ${error.message}.`); 725 } finally { 726 BotMon.live.gui.status.hideBusy("Status: Done."); 727 } 728 } 729 }, 730 731 gui: { 732 init: function() { 733 // init the lists view: 734 this.lists.init(); 735 }, 736 737 overview: { 738 make: function() { 739 const data = BotMon.live.data.analytics.data; 740 const parent = document.getElementById('botmon__today__content'); 741 if (parent) { 742 743 const bounceRate = Math.round(data.totalVisits / data.totalPageViews * 1000) / 10; 744 745 jQuery(parent).prepend(jQuery(` 746 <details id="botmon__today__overview" open> 747 <summary>Overview</summary> 748 <div class="grid-3-columns"> 749 <dl> 750 <dt>Web metrics</dt> 751 <dd><span>Total visits:</span><span>${data.totalVisits}</span></dd> 752 <dd><span>Total page views:</span><span>${data.totalPageViews}</span></dd> 753 <dd><span>Bounce rate:</span><span>${bounceRate} %</span></dd> 754 <dd><span>∅ load time:</span><span>${data.avgLoadTime} ms</span></dd> 755 </dl> 756 <dl> 757 <dt>Bots vs. Humans</dt> 758 <dd><span>Known bots:</span><span>${data.bots.known}</span></dd> 759 <dd><span>Suspected bots:</span><span>${data.bots.suspected}</span></dd> 760 <dd><span>Probably humans:</span><span>${data.bots.human}</span></dd> 761 <dd><span>Registered users:</span><span>${data.bots.users}</span></dd> 762 </dl> 763 <dl id="botmon__botslist"> 764 <dt>Known bots</dt> 765 </dl> 766 </div> 767 </details> 768 `)); 769 } 770 } 771 }, 772 status: { 773 setText: function(txt) { 774 const el = document.getElementById('botmon__today__status'); 775 if (el && BotMon.live.gui.status._errorCount <= 0) { 776 el.innerText = txt; 777 } 778 }, 779 780 setTitle: function(html) { 781 const el = document.getElementById('botmon__today__title'); 782 if (el) { 783 el.innerHTML = html; 784 } 785 }, 786 787 setError: function(txt) { 788 console.error(txt); 789 BotMon.live.gui.status._errorCount += 1; 790 const el = document.getElementById('botmon__today__status'); 791 if (el) { 792 el.innerText = "An error occured. See the browser log for details!"; 793 el.classList.add('error'); 794 } 795 }, 796 _errorCount: 0, 797 798 showBusy: function(txt = null) { 799 BotMon.live.gui.status._busyCount += 1; 800 const el = document.getElementById('botmon__today__busy'); 801 if (el) { 802 el.style.display = 'inline-block'; 803 } 804 if (txt) BotMon.live.gui.status.setText(txt); 805 }, 806 _busyCount: 0, 807 808 hideBusy: function(txt = null) { 809 const el = document.getElementById('botmon__today__busy'); 810 BotMon.live.gui.status._busyCount -= 1; 811 if (BotMon.live.gui.status._busyCount <= 0) { 812 if (el) el.style.display = 'none'; 813 if (txt) BotMon.live.gui.status.setText(txt); 814 } 815 } 816 }, 817 818 lists: { 819 init: function() { 820 821 const parent = document.getElementById('botmon__today__visitorlists'); 822 if (parent) { 823 824 for (let i=0; i < 4; i++) { 825 826 // change the id and title by number: 827 let listTitle = ''; 828 let listId = ''; 829 switch (i) { 830 case 0: 831 listTitle = "Registered users"; 832 listId = 'users'; 833 break; 834 case 1: 835 listTitle = "Probably humans"; 836 listId = 'humans'; 837 break; 838 case 2: 839 listTitle = "Suspected bots"; 840 listId = 'suspectedBots'; 841 break; 842 case 3: 843 listTitle = "Known bots"; 844 listId = 'knownBots'; 845 break; 846 default: 847 console.warn('Unknwon list number.'); 848 } 849 850 const details = BotMon.t._makeElement('details', { 851 'data-group': listId, 852 'data-loaded': false 853 }); 854 details.appendChild(BotMon.t._makeElement('summary', 855 undefined, 856 listTitle 857 )); 858 details.addEventListener("toggle", this._onDetailsToggle); 859 860 parent.appendChild(details); 861 862 } 863 } 864 }, 865 866 _onDetailsToggle: function(e) { 867 console.info('BotMon.live.gui.lists._onDetailsToggle()'); 868 869 const target = e.target; 870 871 if (target.getAttribute('data-loaded') == 'false') { // only if not loaded yet 872 target.setAttribute('data-loaded', 'loading'); 873 874 const fillType = target.getAttribute('data-group'); 875 const fillList = BotMon.live.data.analytics.groups[fillType]; 876 if (fillList && fillList.length > 0) { 877 878 const ul = BotMon.t._makeElement('ul'); 879 880 fillList.forEach( (it) => { 881 ul.appendChild(BotMon.live.gui.lists._makeVisitorItem(it, fillType)); 882 }); 883 884 target.appendChild(ul); 885 target.setAttribute('data-loaded', 'true'); 886 } else { 887 target.setAttribute('data-loaded', 'false'); 888 } 889 890 } 891 }, 892 893 _makeVisitorItem: function(data, type) { 894 895 // shortcut for neater code: 896 const make = BotMon.t._makeElement; 897 898 let ipType = ( data.ip.indexOf(':') >= 0 ? '6' : '4' ); 899 900 const li = make('li'); // root list item 901 const details = make('details'); 902 const summary = make('summary'); 903 details.appendChild(summary); 904 905 const span1 = make('span'); /* left-hand group */ 906 907 if (data._type == 'bot') { /* Bot only */ 908 909 span1.appendChild(make('span', { /* Bot */ 910 'class': 'bot bot_' + (data._bot ? data._bot.id : 'unknown'), 911 'title': "Bot: " + (data._bot ? data._bot.n : 'Unknown') 912 }, (data._bot ? data._bot.n : 'Unknown'))); 913 914 } else if (data._type == 'usr') { /* User only */ 915 916 span1.appendChild(make('span', { /* User */ 917 'class': 'user' + (data._user ? data._user.id : 'unknown'), 918 'title': "User: " + data.usr 919 }, data.usr)); 920 921 } else { /* others */ 922 923 if (data.ip == '127.0.0.1' || data.ip == '::1' ) ipType = '0'; 924 span1.appendChild(make('span', { /* IP-Address */ 925 'class': 'ipaddr ip' + ipType, 926 'title': "IP-Address: " + data.ip 927 }, data.ip)); 928 929 } 930 931 const platformName = (data._platform ? data._platform.n : 'Unknown'); 932 span1.appendChild(make('span', { /* Platform */ 933 'class': 'icon platform platform_' + (data._platform ? data._platform.id : 'unknown'), 934 'title': "Platform: " + platformName 935 }, platformName)); 936 937 const clientName = (data._client ? data._client.n: 'Unknown'); 938 span1.appendChild(make('span', { /* Client */ 939 'class': 'icon client client_' + (data._client ? data._client.id : 'unknown'), 940 'title': "Client: " + clientName 941 }, clientName)); 942 943 944 945 summary.appendChild(span1); 946 const span2 = make('span'); /* right-hand group */ 947 948 span2.appendChild(make('time', { /* Last seen */ 949 'data-field': 'last-seen', 950 'datetime': (data._lastSeen ? data._lastSeen : 'unknown') 951 }, (data._lastSeen ? data._lastSeen.getHours() + ':' + data._lastSeen.getMinutes() + ':' + data._lastSeen.getSeconds() : 'Unknown'))); 952 953 summary.appendChild(span2); 954 955 // create expanable section: 956 957 const dl = make('dl', {'class': 'visitor_details'}); 958 959 if (data._bot) { 960 dl.appendChild(make('dt', {}, "Bot:")); /* bot info */ 961 dl.appendChild(make('dd', {'class': 'has_icon bot bot_' + (data._bot ? data._bot.id : 'unknown')}, 962 (data._bot ? data._bot.n : 'Unknown'))); 963 } 964 965 dl.appendChild(make('dt', {}, "Client:")); /* client */ 966 dl.appendChild(make('dd', {'class': 'has_icon client_' + (data._client ? data._client.id : 'unknown')}, 967 clientName + ( data._client.v > 0 ? ' (' + data._client.v + ')' : '' ) )); 968 969 dl.appendChild(make('dt', {}, "Platform:")); /* platform */ 970 dl.appendChild(make('dd', {'class': 'has_icon platform_' + (data._platform ? data._platform.id : 'unknown')}, 971 platformName + ( data._platform.v > 0 ? ' (' + data._platform.v + ')' : '' ) )); 972 973 dl.appendChild(make('dt', {}, "IP-Address:")); 974 dl.appendChild(make('dd', {'class': 'has_icon ip' + ipType}, data.ip)); 975 976 if ((data._lastSeen - data._firstSeen) < 1) { 977 dl.appendChild(make('dt', {}, "Seen:")); 978 dl.appendChild(make('dd', {'class': 'seen'}, data._firstSeen.toLocaleString())); 979 } else { 980 dl.appendChild(make('dt', {}, "First seen:")); 981 dl.appendChild(make('dd', {'class': 'firstSeen'}, data._firstSeen.toLocaleString())); 982 dl.appendChild(make('dt', {}, "Last seen:")); 983 dl.appendChild(make('dd', {'class': 'lastSeen'}, data._lastSeen.toLocaleString())); 984 } 985 986 dl.appendChild(make('dt', {}, "User-Agent:")); 987 dl.appendChild(make('dd', {'class': 'agent' + ipType}, data.agent)); 988 989 dl.appendChild(make('dt', {}, "Visited pages:")); 990 const pagesDd = make('dd', {'class': 'pages'}); 991 const pageList = make('ul'); 992 993 data._pageViews.forEach( (page) => { 994 const pgLi = make('li'); 995 996 let visitTimeStr = "Bounce"; 997 const visitDuration = page._lastSeen.getTime() - page._firstSeen.getTime(); 998 if (visitDuration > 0) { 999 visitTimeStr = Math.floor(visitDuration / 1000) + "s"; 1000 } 1001 1002 pgLi.appendChild(make('span', {}, page.pg)); 1003 pgLi.appendChild(make('span', {}, page.ref)); 1004 pgLi.appendChild(make('span', {}, visitTimeStr)); 1005 pageList.appendChild(pgLi); 1006 }); 1007 1008 pagesDd.appendChild(pageList); 1009 dl.appendChild(pagesDd); 1010 1011 details.appendChild(dl); 1012 1013 li.appendChild(details); 1014 return li; 1015 } 1016 } 1017 } 1018}; 1019 1020/* launch only if the BotMon admin panel is open: */ 1021if (document.getElementById('botmon__admin')) { 1022 BotMon.init(); 1023}