This commit is contained in:
Daniel Langbein 2020-11-25 19:35:39 +01:00
parent 531bf27837
commit 8c30c35128
19 changed files with 372 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/secret/*.txt
/data/*.xml

8
.idea/.gitignore generated vendored Normal file
View File

@ -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/

10
.idea/bbb-status.iml generated Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.8 (xfconf-backup)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

4
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (xfconf-backup)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/bbb-status.iml" filepath="$PROJECT_DIR$/.idea/bbb-status.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

10
Dockerfile Normal file
View File

@ -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" ]

View File

@ -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`

0
data/dummy Normal file
View File

10
docker-compose.yml Normal file
View File

@ -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

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
requests>=2.25.0

0
secret/dummy Normal file
View File

48
src/bbbRequest.py Normal file
View File

@ -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

47
src/logMeetingData.py Executable file
View File

@ -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)

89
src/parseMeetings.py Normal file
View File

@ -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')]

48
src/saveMeetings.py Normal file
View File

@ -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

42
src/util.py Normal file
View File

@ -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)

24
src/utilTest.py Normal file
View File

@ -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()