diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dcac78b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/secret/*.txt
+/data/*.xml
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..73f69e0
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/bbb-status.iml b/.idea/bbb-status.iml
new file mode 100644
index 0000000..371977c
--- /dev/null
+++ b/.idea/bbb-status.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..9727bbc
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..5989892
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ed59cdb
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,10 @@
+FROM python:3.8
+
+WORKDIR /usr/src/
+
+COPY requirements.txt ./
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY ./src/ ./
+
+CMD [ "python", "./logMeetingData.py" ]
\ No newline at end of file
diff --git a/README.md b/README.md
index e69de29..183eb0d 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1,9 @@
+# README
+
+Execute `bbb-conf --secret` on your server and place your
+
+This gives you your **API-secret** as well as a link to **API-Mate** wheren
+you can test new requests with your secret prefilled.
+
+* URL inside `secrets/url.txt`
+* secret inside `secret/secret.txt`
diff --git a/data/dummy b/data/dummy
new file mode 100644
index 0000000..e69de29
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..55164f8
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,10 @@
+version: '3.7'
+services:
+ dk-gen:
+ build: .
+ environment:
+ - PYTHONPATH=/usr/src/
+ volumes:
+# - ./src/:/usr/src/
+ - ./secret:/usr/secret
+ - ./data:/usr/data
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..eed6988
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+requests>=2.25.0
\ No newline at end of file
diff --git a/secret/dummy b/secret/dummy
new file mode 100644
index 0000000..e69de29
diff --git a/src/bbbRequest.py b/src/bbbRequest.py
new file mode 100644
index 0000000..5490b0a
--- /dev/null
+++ b/src/bbbRequest.py
@@ -0,0 +1,48 @@
+import hashlib
+import requests
+from xml.etree import ElementTree
+
+import util
+
+
+def requestMeetingData() -> str:
+ response = requests.get(getRequestUrl())
+ if not response.ok:
+ raise ValueError("error during request, got status code {}".format(response.status_code))
+
+ tree = ElementTree.fromstring(response.content)
+ if tree.find('returncode').text != 'SUCCESS':
+ raise ValueError('error getting API data')
+
+ return str(response.content, encoding=response.encoding)
+
+
+def getRequestUrl(api_method: str = 'getMeetings', query_string: str = '') -> str:
+ url = getUrl()
+ api_url = url + "api/"
+ secret = getSecret()
+ h = hashlib.sha1((api_method + query_string + secret).encode('utf-8'))
+ checksum = h.hexdigest()
+
+ if len(query_string) > 0:
+ query_string += '&'
+
+ return api_url + api_method + '?' + query_string + 'checksum=' + checksum
+
+
+def getUrl() -> str:
+ filepath = util.getWorkingDir().joinpath("../secret").joinpath("url.txt")
+ url = util.readFirstLine(filepath).strip()
+ if not url.endswith("/"):
+ raise ValueError("url should end with '/'")
+ return url
+
+
+def getSecret() -> str:
+ filepath = util.getWorkingDir().joinpath("../secret").joinpath("secret.txt")
+ secret = util.readFirstLine(filepath).strip()
+ min_length = 12
+ if len(secret) <= min_length:
+ raise ValueError("secret should be longer than {} characters!".format(min_length))
+ return secret
+
diff --git a/src/logMeetingData.py b/src/logMeetingData.py
new file mode 100755
index 0000000..f1a6da5
--- /dev/null
+++ b/src/logMeetingData.py
@@ -0,0 +1,47 @@
+#!/usr/bin/python3
+
+import time
+
+import saveMeetings
+import bbbRequest
+import parseMeetings
+import util
+
+
+def sleepFiveMin(verbose=False):
+ minute = 60
+ fiveMinutes = 5 * minute
+
+ if verbose:
+ print("> Sleeping for five minutes ...")
+ time.sleep(fiveMinutes)
+
+
+def v2():
+ print("BBB meetingData logger started!")
+
+ while True:
+ meetingsStr = bbbRequest.requestMeetingData()
+ saveDir = util.getWorkingDir().joinpath("../data")
+ savedFile = saveMeetings.saveMeetingsData(meetingsStr, saveDir)
+ print("Saved meetings at {}".format(savedFile))
+
+ meetings = parseMeetings.parseMeetingsData(meetingsStr)
+ bbbStatus = parseMeetings.BbbStatus(meetings)
+ bbbStatusStr = str(bbbStatus)
+ print(util.indentMultilineStr(bbbStatusStr))
+
+ sleepFiveMin(verbose=True)
+
+
+def v1():
+ while True:
+ saveDir = util.getWorkingDir().joinpath("../data")
+ saveMeetings.requestAndSaveMeetingData(saveDir)
+ print('.', end='')
+ sleepFiveMin()
+
+
+if __name__ == '__main__':
+ v2()
+ exit(1)
diff --git a/src/parseMeetings.py b/src/parseMeetings.py
new file mode 100644
index 0000000..b987b49
--- /dev/null
+++ b/src/parseMeetings.py
@@ -0,0 +1,89 @@
+from typing import List
+from xml.etree import ElementTree
+
+import util
+
+
+class Meeting(object):
+ def __init__(self, xml_meeting: ElementTree.Element):
+ self.meetingName: str = xml_meeting.find('meetingName').text
+ self.meetingID: str = xml_meeting.find('meetingID').text
+ self.internalMeetingID: str = xml_meeting.find('internalMeetingID').text
+
+ self.isRunning: bool = util.asBoolean(xml_meeting.find('running').text)
+ self.startTime = xml_meeting.find('startTime').text
+ if self.isRunning:
+ self.endTime = None
+ else:
+ self.endTime = xml_meeting.find('endTime').text
+
+ # TODO: disable default meeting recordings (!)
+ self.isRecording: bool = util.asBoolean(xml_meeting.find('recording').text)
+ self.isBreakout: bool = util.asBoolean(xml_meeting.find('isBreakout').text)
+
+ self.participantCount: int = int(xml_meeting.find('participantCount').text)
+ self.listenerCount: int = int(xml_meeting.find('listenerCount').text)
+ self.voiceParticipantCount: int = int(xml_meeting.find('voiceParticipantCount').text)
+ self.videoCount: int = int(xml_meeting.find('videoCount').text)
+ self.maxUsers: int = int(xml_meeting.find('maxUsers').text)
+ self.moderatorCount: int = int(xml_meeting.find('moderatorCount').text)
+
+ self.attendees: List[Attendee] = [Attendee(xml_attendee) for xml_attendee in
+ xml_meeting.find('attendees').iter('attendee')]
+
+ def __str__(self):
+ return util.asString(self)
+
+
+class Attendee(object):
+ def __init__(self, xml_attendee: ElementTree.Element):
+ # self.userID = xml_attendee.find('userID').text
+ # self.fullName = xml_attendee.find('fullName').text
+ self.role = xml_attendee.find('role').text
+ self.isPresenter: bool = util.asBoolean(xml_attendee.find('isPresenter').text)
+ self.isListeningOnly: bool = util.asBoolean(xml_attendee.find('isListeningOnly').text)
+ self.hasJoinedVoice: bool = util.asBoolean(xml_attendee.find('hasJoinedVoice').text)
+ self.hasVideo: bool = util.asBoolean(xml_attendee.find('hasVideo').text)
+
+ def __str__(self):
+ return util.asString(self)
+
+
+class BbbStatus(object):
+ """
+ Represents the status of one BBB server at one point of time.
+ It contains statistics about the running meetings on the server
+ and (TODO) it's CPU utilization and network usage.
+ """
+
+ def __init__(self, meetings: List[Meeting]):
+ """
+ :param meetings: All current meetings
+ """
+ self.meetings = meetings
+
+ self.recordingCount = 0
+ self.participantCount = 0
+ self.listenerCount = 0
+ self.voiceParticipantCount = 0
+ self.videoCount = 0
+ self.moderatorCount = 0
+
+ for meeting in meetings:
+ self.participantCount += meeting.participantCount
+ self.listenerCount += meeting.listenerCount
+ self.voiceParticipantCount += meeting.voiceParticipantCount
+ self.videoCount += meeting.videoCount
+ self.moderatorCount += meeting.moderatorCount
+ if meeting.isRecording:
+ self.recordingCount += 1
+
+ def __str__(self):
+ return util.asString(self)
+
+
+def parseMeetingsData(dataStr: str) -> List[Meeting]:
+ tree = ElementTree.fromstring(dataStr)
+ xml_meetings = tree.find('meetings')
+
+ return [Meeting(xml_meeting) for xml_meeting in xml_meetings.iter('meeting')]
diff --git a/src/saveMeetings.py b/src/saveMeetings.py
new file mode 100644
index 0000000..f0e987a
--- /dev/null
+++ b/src/saveMeetings.py
@@ -0,0 +1,48 @@
+from datetime import datetime
+from pathlib import Path
+
+import bbbRequest
+
+
+def requestAndSaveMeetingData(folder: Path) -> Path:
+ """
+ save a new xml file in the given folder
+
+ :param dataStr:
+ :param folder:
+ :return: Path to created file
+ """
+
+ return saveMeetingsData(bbbRequest.requestMeetingData(), folder)
+
+
+def saveMeetingsData(dataStr: str, folder: Path) -> Path:
+ """
+ save a new xml file in the given folder
+
+ :param dataStr:
+ :param folder:
+ :return: Path to created file
+ """
+
+ return doSaveData(dataStr, folder, 'meetings')
+
+
+def doSaveData(dataStr: str, folder: Path, dataType: str = 'meetings') -> Path:
+ """
+ save a new xml file in the given folder
+
+ :param dataStr:
+ :param folder:
+ :param dataType: e.g. "meetings" for meeting XML data
+ :return: Path to created file
+ """
+
+ # time_str = datetime.utcnow().strftime('%Y%m%d_%H%M%S')
+ time_str = datetime.now().strftime('%Y%m%d_%H%M%S')
+ filename = '{}_{}.xml'.format(time_str, dataType)
+ filepath = folder.joinpath(filename)
+
+ with open(filepath, "w") as xml_file:
+ xml_file.write(dataStr)
+ return filepath
diff --git a/src/util.py b/src/util.py
new file mode 100644
index 0000000..98cdece
--- /dev/null
+++ b/src/util.py
@@ -0,0 +1,42 @@
+from pathlib import Path
+import os
+
+def getWorkingDir() -> Path:
+ return Path(os.path.dirname(os.path.realpath(__file__)))
+
+def readFirstLine(file: Path) -> str:
+ """
+ :param file: Path to file
+ :return: first line of file
+ """
+ with open(file, "r") as f:
+ return f.readline()
+
+
+def indentMultilineStr(s: str, indentWith='\t'):
+ indented = indentWith + s.replace('\n', '\n' + indentWith)
+ if s.endswith('\n'):
+ indented = indented[:len(indented)-len(indentWith)]
+ return indented
+
+def asString(o: object):
+ attrs = vars(o) # attributes and their values
+ return '\n'.join("%s: %s" % item for item in attrs.items())
+
+
+def asBoolean(booleanValue: str):
+ """
+ :param booleanValue: Some boolean value in string representation
+ :return: the boolean value as boolean object
+ :raise ValueError: If booleanValue could not be parsed as boolean object
+ """
+ if booleanValue.lower() in ['true', 'yes']:
+ return True
+ elif booleanValue.lower() in ['false', 'no']:
+ return False
+ else:
+ raise ValueError("booleanValue (string) could not be parsed to boolean object!")
+
+# def listAsString(l: list):
+# x = ["(", ")"]
+# return ", ".join(l).join(x)
diff --git a/src/utilTest.py b/src/utilTest.py
new file mode 100644
index 0000000..5a1c00b
--- /dev/null
+++ b/src/utilTest.py
@@ -0,0 +1,24 @@
+import unittest
+import util
+
+
+class UtilTestCase(unittest.TestCase):
+ def test_indentString(self):
+ unindented = "Hello\nWorld!"
+ expected = "\tHello\n\tWorld!"
+
+ actual = util.indentMultilineStr(unindented)
+
+ self.assertEqual(expected, actual)
+
+ def test_indentString2(self):
+ unindented = "Hello\nWorld!\n"
+ expected = "\tHello\n\tWorld!\n"
+
+ actual = util.indentMultilineStr(unindented)
+
+ self.assertEqual(expected, actual)
+
+
+if __name__ == '__main__':
+ unittest.main()