mirror of
https://codeberg.org/langfingaz/bbb-status
synced 2024-12-22 00:26:06 +01:00
init
This commit is contained in:
parent
531bf27837
commit
8c30c35128
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/secret/*.txt
|
||||
/data/*.xml
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
10
.idea/bbb-status.iml
generated
Normal 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>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
4
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
10
Dockerfile
Normal 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" ]
|
@ -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
0
data/dummy
Normal file
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal 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
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
requests>=2.25.0
|
0
secret/dummy
Normal file
0
secret/dummy
Normal file
48
src/bbbRequest.py
Normal file
48
src/bbbRequest.py
Normal 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
47
src/logMeetingData.py
Executable 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
89
src/parseMeetings.py
Normal 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
48
src/saveMeetings.py
Normal 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
42
src/util.py
Normal 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
24
src/utilTest.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user