subDesTagesMitExtraKaese 4 years ago
commit
4b236bd4e7
8 changed files with 489 additions and 0 deletions
  1. 2 0
      .gitignore
  2. 23 0
      README.md
  3. 163 0
      channel-statistics.js
  4. 107 0
      createTables.sql
  5. BIN
      ts3-stats.png
  6. 24 0
      ts3-viewer/ajax.php
  7. 130 0
      ts3-viewer/index.php
  8. 40 0
      ts3-viewer/stylesheet.css

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+connection.php
+secrets.js

+ 23 - 0
README.md

@@ -0,0 +1,23 @@
+# Sinusbot Channel Statistics
+
+## log number of clients per channel to mysql
+
+sinusbot script `channel-statistics.js`
+
+## display graphs on website
+
+`ts3-viewer/`
+
+needs `ts3-viewer/connection.php`
+```php
+<?php
+$dbhost="localhost";
+$db="<db>";
+$dbuser="<user>";
+$dbpass="<pass>";
+
+$serverUid = "<ts3 server uid>";
+?>
+```
+
+![demo](ts3-stats.png)

+ 163 - 0
channel-statistics.js

@@ -0,0 +1,163 @@
+registerPlugin({
+  name: 'Client Statistics Script',
+  version: '1.0',
+  description: 'log client events to db',
+  author: 'mcj201',
+  vars: [
+    {
+      name: 'host',
+      title: 'MySQL Host',
+      type: 'string'
+    },
+    {
+      name: 'username',
+      title: 'MySQL User',
+      type: 'string'
+    },
+    {
+      name: 'password',
+      title: 'MySQL Password',
+      type: 'password'
+    },
+    {
+      name: 'database',
+      title: 'MySQL Database',
+      type: 'string'
+    },
+  ],
+  autorun: false,
+  requiredModules: [
+    'db'
+  ]
+}, function(sinusbot, config, meta) {
+  const db = require('db');
+  const engine = require('engine');
+  const backend = require('backend');
+  const event = require('event');
+  const helpers = require('helpers');
+  const dbc = db.connect({ driver: 'mysql', host: config.host, username: config.username, password: config.password, database: config.database }, function(err) {
+      if (err) {
+           engine.log(err);
+      } else {
+        engine.log('connection successful');
+      }
+  });
+  
+  let channelIdMap = {};
+  let serverId = null;
+  event.on('connect', () => {
+    const serverinfo = backend.extended().getServerInfo();
+    updateServer(serverinfo, function(id) {
+      serverId = id;
+      engine.log('serverId:', serverId);
+      backend.getChannels().forEach(channel => updateChannel(serverId, channel, function(id) {
+        channelIdMap[channel.id()] = id;
+        updateChannelEvent(id, channel);
+      }));
+    });
+  });
+  
+  event.on('clientMove', ({ client, fromChannel, toChannel }) => {
+    if(fromChannel) updateChannelEvent(channelIdMap[fromChannel.id()], fromChannel);
+    if(toChannel)   updateChannelEvent(channelIdMap[toChannel.id()], toChannel);
+  });
+
+  event.on('channelCreate', (channel, client) => {
+    updateChannel(serverId, channel, function(id) {
+      channelIdMap[channel.id()] = id;
+      updateChannelEvent(id, channel);
+    });
+  });
+
+  event.on('channelUpdate', (channel, client) => {
+    updateChannel(serverId, channel, function(id) {
+      channelIdMap[channel.id()] = id;
+      updateChannelEvent(id, channel);
+    });
+  });
+
+
+  function updateServer(serverinfo, cb) {
+    if(!dbc || !serverinfo) {
+      engine.log('error on server update');
+      return;
+    }
+    dbc.query("SELECT id, name FROM server WHERE uid = ?", serverinfo.UID(), function(err, res) {
+      if (!err) {
+        if(res.length > 0) {
+          const serverId = res[0].id;
+          dbc.exec("UPDATE server SET name = ? WHERE id = ?", serverinfo.name(), serverId);
+
+          cb(serverId);
+        } else {
+          dbc.exec("INSERT INTO server (uid, name) VALUES (?, ?)", serverinfo.UID(), serverinfo.name(), function() {
+            dbc.query("SELECT id FROM server WHERE uid = ?", serverinfo.UID(), function(err, res) {
+              if(!err && res.length > 0) {
+                cb(res[0].id);
+              } else {
+                console.log(err, res);
+              }
+            });
+          });
+        }
+      } else {
+        console.log(err, res);
+      }
+    });
+  }
+
+  function updateChannel(serverId, channel, cb) {
+    if(!dbc || !serverId) {
+      return;
+    }
+    const parentId = channel.parent() ? channel.parent().id() : null;
+    dbc.query("SELECT * FROM channel WHERE channelId = ? AND serverId = ?", channel.id(), serverId, function(err, res) {
+      if (!err) {
+        if(res.length > 0) {
+          const id = res[0].id;
+          dbc.exec("UPDATE channel SET name = ?, parentId = ?, position = ?, description = ? WHERE id = ?", 
+                    channel.name(), parentId, channel.position(), channel.description(), id);
+          cb(id);
+        } else {
+          dbc.exec("INSERT INTO channel (channelId, name, serverId, parentId, position, description) VALUES (?, ?, ?, ?, ?, ?)", 
+            channel.id(), channel.name(), serverId, parentId, channel.position(), channel.description(), function() {
+              dbc.query("SELECT id FROM channel WHERE channelId = ? AND serverId = ?", channel.id(), serverId, function(err, res) {
+                if(!err && res.length > 0) {
+                  cb(res[0].id);
+                } else {
+                  console.log(err, res);
+                }
+              });
+          });
+        }
+      } else {
+        console.log(err, res);
+      }
+    });
+  }
+
+  function updateChannelEvent(id, channel) {
+    if(!dbc || !id) {
+      return;
+    }
+    const clients = channel.getClientCount();
+    dbc.query("SELECT * FROM channelEvent WHERE channelId = ? AND date > DATE_SUB(NOW(), INTERVAL 1 MINUTE) ORDER BY date DESC LIMIT 1", id, function(err, res) {
+      if(!err) {
+        if(res.length > 0) {
+          engine.log('channel ' + channel.name() + ' updated to ' + clients + ' clients');
+          dbc.exec("UPDATE channelEvent SET clientCount = ? WHERE id = ?", clients, res[0].id);
+        } else {
+          dbc.query("SELECT * FROM channelEvent WHERE channelId = ? ORDER BY date DESC LIMIT 1", id, function(err, res) {
+            if(!err && res.length > 0 && res[0].clientCount === clients)
+              return;
+            if(clients > 0)
+              engine.log('channel ' + channel.name() + ' has now ' + clients + ' clients');
+            dbc.exec("INSERT INTO channelEvent (channelId, clientCount) VALUES (?, ?)", id, clients);
+          });
+        }
+      } else {
+        console.log(err, res);
+      }
+    });
+  }
+});

+ 107 - 0
createTables.sql

@@ -0,0 +1,107 @@
+-- phpMyAdmin SQL Dump
+-- version 4.6.6deb4
+-- https://www.phpmyadmin.net/
+--
+-- Host: localhost:3306
+-- Erstellungszeit: 31. Mai 2020 um 21:37
+-- Server-Version: 10.1.44-MariaDB-0+deb9u1
+-- PHP-Version: 7.3.18-1+0~20200515.59+debian9~1.gbp12fa4f
+
+SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
+SET time_zone = "+00:00";
+
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8mb4 */;
+
+--
+-- Datenbank: `teamspeak`
+--
+
+-- --------------------------------------------------------
+
+--
+-- Tabellenstruktur für Tabelle `channel`
+--
+
+CREATE TABLE `channel` (
+  `id` int(11) NOT NULL,
+  `channelId` text NOT NULL,
+  `name` text NOT NULL,
+  `serverId` int(11) NOT NULL,
+  `parentId` int(11) DEFAULT NULL,
+  `position` int(11) NOT NULL,
+  `description` text
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
+
+-- --------------------------------------------------------
+
+--
+-- Tabellenstruktur für Tabelle `channelEvent`
+--
+
+CREATE TABLE `channelEvent` (
+  `id` int(11) NOT NULL,
+  `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `channelId` int(11) NOT NULL,
+  `clientCount` int(11) NOT NULL
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
+
+-- --------------------------------------------------------
+
+--
+-- Tabellenstruktur für Tabelle `server`
+--
+
+CREATE TABLE `server` (
+  `id` int(11) NOT NULL,
+  `uid` text NOT NULL,
+  `name` text NOT NULL
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;
+
+--
+-- Indizes der exportierten Tabellen
+--
+
+--
+-- Indizes für die Tabelle `channel`
+--
+ALTER TABLE `channel`
+  ADD PRIMARY KEY (`id`);
+
+--
+-- Indizes für die Tabelle `channelEvent`
+--
+ALTER TABLE `channelEvent`
+  ADD PRIMARY KEY (`id`);
+
+--
+-- Indizes für die Tabelle `server`
+--
+ALTER TABLE `server`
+  ADD PRIMARY KEY (`id`);
+
+--
+-- AUTO_INCREMENT für exportierte Tabellen
+--
+
+--
+-- AUTO_INCREMENT für Tabelle `channel`
+--
+ALTER TABLE `channel`
+  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
+--
+-- AUTO_INCREMENT für Tabelle `channelEvent`
+--
+ALTER TABLE `channelEvent`
+  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
+--
+-- AUTO_INCREMENT für Tabelle `server`
+--
+ALTER TABLE `server`
+  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

BIN
ts3-stats.png


+ 24 - 0
ts3-viewer/ajax.php

@@ -0,0 +1,24 @@
+<?php
+	require("connection.php");
+	// Opens a connection to a MySQL server
+	$pdo = new PDO("mysql:host=$dbhost;dbname=$db;charset=utf8mb4", $dbuser, $dbpass);
+	$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+  $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
+  
+  $stmtEvents = $pdo->prepare("SELECT channelEvent.channelId, UNIX_TIMESTAMP(channelEvent.date), channelEvent.clientCount FROM channelEvent 
+    LEFT JOIN channel on channel.id = channelEvent.channelId 
+    LEFT JOIN server ON server.id = channel.serverId 
+    WHERE server.uid = ? 
+    ORDER BY date DESC LIMIT 10000");
+  $stmtEvents->execute(Array($serverUid));
+  $stmtChannel = $pdo->prepare("SELECT channel.id as id, channel.name as name, channel.channelId as channelId, channel.parentId as parentId, channel.position as position, server.name as serverName, server.id as serverId FROM channel 
+    LEFT JOIN server ON server.id = channel.serverId
+    WHERE server.uid = ?");
+  $stmtChannel->execute(Array($serverUid));
+	$json = Array(
+		"eventColumns" => Array("id", "date", "clientCount"),
+		"events" => $stmtEvents->fetchAll(PDO::FETCH_NUM),
+		"channels" => $stmtChannel->fetchAll(PDO::FETCH_ASSOC)
+  );
+	echo json_encode($json);
+?>

+ 130 - 0
ts3-viewer/index.php

@@ -0,0 +1,130 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+	<head>
+		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+		<title>TS3 channel usage</title>
+		<meta name="viewport" content="width=device-width, initial-scale=1.0">
+		<link href="stylesheet.css" rel="stylesheet" type="text/css">
+		<!--[if lte IE 8]><script language="javascript" type="text/javascript" src="../flot/excanvas.min.js"></script><![endif]-->
+		<script language="javascript" type="text/javascript" src="../flot/jquery.min.js"></script>
+		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.min.js"></script>
+		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.stack.min.js"></script>
+		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.time.min.js"></script>
+		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.selection.min.js"></script>
+		<script language="javascript" type="text/javascript" src="../flot/jquery.flot.resize.min.js"></script>
+
+    </head>
+	<body>
+    <header class="main-head">TS3 channel usage</header>
+    <div id="tree"></div>
+    <div id="graphs"></div>
+    <script>
+    function update() {
+      fetch("ajax.php").then(res=>res.json()).then(function(json) {
+        
+        let channelTree = {channelId: 0, name: 'Server'};
+        makeTree(channelTree, json.channels, json.events);
+        showTree(channelTree, document.getElementById('tree'));
+
+        let div = $("#graphs")[0];
+        div.innerHTML = "";
+        for(const channel of channelTree.children) {
+          if(channel.clientCount == 0)
+            continue;
+
+          let container = document.createElement("div");
+          container.classList.add("demo-container");
+          div.appendChild(container);
+
+          let placeholder = document.createElement("div");
+          placeholder.classList.add("demo-placeholder");
+          container.appendChild(placeholder);
+
+          plotEvents(channel, placeholder);
+        }
+
+        console.log(channelTree);
+      });
+    }
+    update();
+    setInterval(update, 30 * 1000);
+
+    function makeTree(tree, channels, events) {
+      tree.events = events.filter(e => e[0] == tree.id);
+      tree.children = channels.filter((ch) => ch.parentId == tree.channelId).sort((a, b) => a.position - b.position);
+      tree.clientCount = tree.events.reduce((sum, e) => sum+e[2], 0) / (tree.events.length > 0 ? tree.events.length : 1);
+
+      for(const child of tree.children) {
+        tree.clientCount += makeTree(child, channels.filter( ch => ch.parentId != tree.channelId), events.filter( e => e[0] != tree.id));
+      }
+      return tree.clientCount;
+    }
+
+    function showTree(tree, el) {
+      el.innerHTML = `${formatHeadings(tree.name)}`;
+      if(tree.events.length > 0 && (tree.events.length != 1 || tree.events[0][2] != 0))
+        for(const event of tree.events) {
+          el.innerHTML += `<br>${(new Date(event[1]*1000)).toLocaleTimeString('de-DE', {timeZone: 'Europe/Berlin'})}: ${event[2]}`
+        }
+      let ul = document.createElement('ul');
+      el.appendChild(ul);
+      for(const channel of tree.children) {
+        if(channel.clientCount == 0)
+          continue;
+        let li = document.createElement('li');
+        ul.appendChild(li);
+        showTree(channel, li);
+      }
+    }
+
+    function formatHeadings(str, html = true) {
+      const res = /(?:\[(.)spacer[^\]]*\])?(.*)/.exec(str);
+      if(!html)
+        return res[2];
+      switch(res[1]) {
+        case '*':
+          return `<div class='channel'>${res[2].repeat(50)}</div>`;
+        case 'l':
+          return `<div class='channel' align='left'>${res[2]}</div>`;
+        case 'c':
+          return `<div class='channel' align='center'>${res[2]}</div>`;
+        case 'r':
+          return `<div class='channel' align='right'>${res[2]}</div>`;
+        default:
+          return `<div class='channel'>${res[2]}</div>`;
+      }
+    }
+    
+    function plotEvents(tree, div) {
+      let series = [];
+      function iterChilds(tree, prefix="") {
+        const data = tree.events.map(e => [e[1]*1000, e[2]]);
+        const name = prefix + formatHeadings(tree.name, false);
+        if(data.length > 0 && (data.length != 1 || data[0][1] != 0)) {
+          series.push({
+            label: name,
+            data: data,
+            lines: {
+              show: true,
+              fill: true,
+              steps: true
+            }
+          });
+        }
+        for(const channel of tree.children) {
+          iterChilds(channel, name + ' / ');
+        }
+      }
+      iterChilds(tree);
+
+      return $.plot(div, series, {
+        xaxis: {
+          mode: "time",
+          timeBase: "milliseconds",
+          timezone: "browser"
+        }
+      });
+    }
+    </script>
+	</body>
+</html>

+ 40 - 0
ts3-viewer/stylesheet.css

@@ -0,0 +1,40 @@
+.channel {
+  width: 300px;
+  overflow-x: hidden;
+}
+#tree {
+  float: left;
+}
+.demo-container {
+	box-sizing: border-box;
+	width: 850px;
+	height: 450px;
+	padding: 20px 15px 15px 15px;
+	margin: 15px auto 30px auto;
+	border: 1px solid #ddd;
+	background: #fff;
+	background: linear-gradient(#f6f6f6 0, #fff 50px);
+	background: -o-linear-gradient(#f6f6f6 0, #fff 50px);
+	background: -ms-linear-gradient(#f6f6f6 0, #fff 50px);
+	background: -moz-linear-gradient(#f6f6f6 0, #fff 50px);
+	background: -webkit-linear-gradient(#f6f6f6 0, #fff 50px);
+	box-shadow: 0 3px 10px rgba(0,0,0,0.15);
+	-o-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
+	-ms-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
+	-moz-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
+	-webkit-box-shadow: 0 3px 10px rgba(0,0,0,0.1);
+	-webkit-tap-highlight-color: rgba(0,0,0,0);
+	-webkit-tap-highlight-color: transparent;
+	-webkit-touch-callout: none;
+	-webkit-user-select: none;
+	-khtml-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+  user-select: none;
+  float: left;
+}
+.demo-placeholder {
+	width: 100%;
+	height: 100%;
+	font-size: 14px;
+}