mirror of
https://codeberg.org/langfingaz/bbb-status
synced 2024-11-21 20:23:17 +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
vendored
Normal file
8
.idea/.gitignore
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
Normal file
10
.idea/bbb-status.iml
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
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
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
Normal file
4
.idea/misc.xml
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
Normal file
8
.idea/modules.xml
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
Normal file
6
.idea/vcs.xml
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