summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/pythonapp.yml30
-rw-r--r--MANIFEST.in3
-rw-r--r--README.md1
-rw-r--r--TSGroupAssigner/__init__.py9
-rw-r--r--TSGroupAssigner/exceptions.py8
-rw-r--r--TSGroupAssigner/group_assign.py187
-rw-r--r--setup.py39
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/test_group_assign.py55
9 files changed, 225 insertions, 107 deletions
diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml
new file mode 100644
index 0000000..213ae39
--- /dev/null
+++ b/.github/workflows/pythonapp.yml
@@ -0,0 +1,30 @@
+name: Python application
+
+on: [push]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v1
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v1
+ with:
+ python-version: 3.8
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ - name: Lint with flake8
+ run: |
+ pip install flake8
+ # stop the build if there are Python syntax errors or undefined names
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+ - name: Test with pytest
+ run: |
+ pip install pytest
+ pytest
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..4ac345f
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include README.md
+include LICENCE.md
+include requirements.txt \ No newline at end of file
diff --git a/README.md b/README.md
index fbb4ee1..bd5729e 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
# TeamSpeak GroupAssigner
+[![CodeFactor](https://www.codefactor.io/repository/github/mightybroccoli/tsgroupassigner/badge)](https://www.codefactor.io/repository/github/mightybroccoli/tsgroupassigner)
## Overview
TSGroupAssigner is a module which allows to automatically assign server groups to voice clients, if they connect within
diff --git a/TSGroupAssigner/__init__.py b/TSGroupAssigner/__init__.py
index a41cd05..6feef67 100644
--- a/TSGroupAssigner/__init__.py
+++ b/TSGroupAssigner/__init__.py
@@ -1,3 +1,10 @@
# -*- coding: utf-8 -*-
-from TSGroupAssigner.group_assign import GroupAssigner
+# version
+__version__ = "0.1"
+
+# modules
+from .group_assign import GroupAssigner
+
+# utils
+from .exceptions import DateException
diff --git a/TSGroupAssigner/exceptions.py b/TSGroupAssigner/exceptions.py
new file mode 100644
index 0000000..a0bb55b
--- /dev/null
+++ b/TSGroupAssigner/exceptions.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+__all__ = ['DateException']
+
+
+class DateException(Exception):
+ """Exceptions thrown if configured date is out of range"""
diff --git a/TSGroupAssigner/group_assign.py b/TSGroupAssigner/group_assign.py
index 073ba45..589b820 100644
--- a/TSGroupAssigner/group_assign.py
+++ b/TSGroupAssigner/group_assign.py
@@ -4,12 +4,13 @@ import datetime as dt
import logging
import sys
import time
+from contextlib import suppress
import ts3
+from .exceptions import DateException
-class DateException(Exception):
- """raise this if the date delta exceeds the configured range"""
+__all__ = ['GroupAssigner']
class GroupAssigner:
@@ -20,42 +21,32 @@ class GroupAssigner:
self.port = port
self.user = user
self.pw = password
- self.sid = sid
- # group
+ # server and group id
+ self.sid = sid
self.gid = gid
# start date and delta
self.sleepstart = date - dt.timedelta(days=1)
self.startdate = date
self.enddate = date + delta
- self.delta = delta
- def __connect(self):
- """
- establish query connection and return connection handler
- """
- try:
- # connect to the telnet interface
- self.conn = ts3.query.TS3Connection(self.host, self.port)
+ # init connection handler
+ self.conn = None
- # login
- self.conn.login(client_login_name=self.user, client_login_password=self.pw)
+ def __connect(self):
+ """ establish query connection and return connection handler """
+ # connect to the telnet interface
+ self.conn = ts3.query.TS3Connection(self.host, self.port)
- # select specified sid
- self.conn.use(sid=self.sid)
+ # login
+ self.conn.login(client_login_name=self.user, client_login_password=self.pw)
- # break if credentials are invalid
- except ts3.query.TS3QueryError as TS3QueryError:
- # log error line and reraise
- logging.error(TS3QueryError)
- raise TS3QueryError
+ # select specified sid
+ self.conn.use(sid=self.sid)
def __disconnect(self):
- """
- method to gracefully logout and disconnect the connection
- this should only be called if the exit is intentional
- """
+ """ gracefully logout and disconnect the connection, this should only be called if the exit is intentional """
try:
self.conn.logout()
self.conn.quit()
@@ -65,41 +56,33 @@ class GroupAssigner:
pass
# broad exception if something unexpected happens
- except ts3.TS3Error as TS3Error:
- # log error and reraise exception
- logging.error(TS3Error)
- raise TS3Error
+ except ts3.TS3Error as err:
+ # log exception
+ logging.error(err)
# exit
sys.exit()
- def __datecheck(self):
- """
- method to check if the current date is still in the configured date range
- """
+ def __checkdate(self):
+ """ method to check if the current date is still in the configured date range """
now = dt.date.today()
- # check if target date is in the configured range
- if self.startdate <= now <= self.enddate:
- logging.debug('target date within configured date range')
- return True
-
- # if date range is exceeded shutdown gracefully
- else:
- # terminate possible open connections
+ # check if still in the configured range
+ if not self.startdate <= now <= self.enddate:
+ # if date range is exceeded shutdown gracefully
logging.info('the current date exceeds the configured date range -- exiting')
self.__disconnect()
- def __sleepstart(self):
- """
- method to check if the process is eligible to sleepstart
- """
+ # else continue
+ logging.debug('heartbeat - target date within configured date range')
+
+ def __start_sleepstart(self):
+ """ method to check if the process is eligible to sleepstart """
now = dt.date.today()
# start date already reached proceed
if self.startdate <= now:
logging.debug('start date is already reached -- not eligible to sleepstart continue')
- return
# if startdate within the next 24h proceed to sleepstart
elif now >= self.startdate - dt.timedelta(days=1):
@@ -109,26 +92,78 @@ class GroupAssigner:
# calculate remaining time delta
remaindelta = starttime - now
- logging.debug('target date will be reached in {sec} seconds -- sleeping'.format(sec=remaindelta.seconds))
+ logging.debug(f'target date will be reached in {remaindelta.seconds} seconds -- sleeping')
time.sleep(remaindelta.seconds + 1)
- # if the date is too far back raise DateException
else:
+ # if the date is too far back raise DateException
raise DateException('target date is too far in the future')
+ def __notifycliententerview(self, data: dict):
+ """
+ event thrown if a client connects to the server
+ :param data: dictionary containing users info
+ """
+ # return all non voice clients without reasonid 0
+ if data['client_type'] != '0' or data['reasonid'] != '0':
+ return
+
+ cldbid = data['client_database_id']
+ user_grps = data['client_servergroups'].split(sep=',')
+
+ msg = '{client_nickname}:{client_database_id} connected - member of {client_servergroups}'
+ logging.debug(msg.format(**data))
+
+ # only try to add nonmembers to group
+ if str(self.gid) not in user_grps:
+ logging.debug(f'{data["client_nickname"]} is not member of {self.gid}')
+
+ try:
+ # Usage: servergroupaddclient sgid={groupID} cldbid={clientDBID}
+ cmd = self.conn.servergroupaddclient(sgid=self.gid, cldbid=cldbid)
+
+ if cmd.error['id'] != '0':
+ logging.error(cmd.data[0].decode("utf-8"))
+
+ # log process
+ logging.info('add {client_nickname}:{client_database_id} to {gid}'.format(**data, gid=self.gid))
+
+ # log possible key errors while the teamspeak 5 client is not fully released
+ except KeyError as err:
+ logging.error([err, data])
+
+ def __handle_event(self, event: str, data: dict):
+ """ event handler which separates events to their specific handlers """
+ # check if event is still eligible
+ self.__checkdate()
+
+ # client enter events
+ if event == "notifycliententerview":
+ self.__notifycliententerview(data)
+
def start(self):
+ """ main entry point to start the bot """
# eol to start process ahead of time
- self.__sleepstart()
+ self.__start_sleepstart()
# proceed only if target date is inside the date range
- if self.__datecheck():
+ self.__checkdate()
+
+ try:
# init connection
self.__connect()
- # start processing
- self.__main()
+ # break if credentials are invalid
+ except ts3.query.TS3QueryError as err:
+ # log error and disconnect
+ logging.error(err)
+ self.__disconnect()
+
+ # start processing
+ self.__main()
def __main(self):
+ """ main loop """
# register for "server" notify event
self.conn.servernotifyregister(event="server")
@@ -136,48 +171,10 @@ class GroupAssigner:
while True:
self.conn.send_keepalive()
- try:
- # wait for an event to be thrown
+ # suppress TimeoutError exceptions
+ with suppress(ts3.query.TS3TimeoutError):
+ # wait for events
event = self.conn.wait_for_event(timeout=60)
- # process TeamSpeak Telnet timeout
- except ts3.query.TS3TimeoutError:
- pass
-
- else:
- # only parse entering clients info
- if event.event == "notifycliententerview":
- # is the event still eligible
- self.__datecheck()
-
- # skip query clients -- query client = 1 , voice client = 0
- if event[0]['client_type'] == '0':
-
- # reasonid should be 0 not sure why though
- if event[0]["reasonid"] == "0":
-
- cldbid = event.parsed[0]['client_database_id']
- user_grps = event.parsed[0]['client_servergroups'].split(sep=',')
-
- msg = '{client_nickname}:{client_database_id} connected - member of {client_servergroups}'
- logging.debug(msg.format(**event[0]))
-
- # only try to add nonmembers to group
- if str(self.gid) not in user_grps:
-
- # https://yat.qa/ressourcen/server-query-kommentare/
- # Usage: servergroupaddclient sgid={groupID} cldbid={clientDBID}
- try:
- cmd = self.conn.servergroupaddclient(sgid=self.gid, cldbid=cldbid)
-
- if cmd.error['id'] != '0':
- logging.error(cmd.data[0].decode("utf-8"))
-
- # log process
- msg = '{client_nickname}:{client_database_id} added to {gid}'
- logging.info(msg.format(**event[0], gid=self.gid))
-
- # log possible key errors while the teamspeak 5 client is not fully working
- except KeyError:
- logging.error(str(event.parsed))
- pass
+ # handover event to eventhandler
+ self.__handle_event(event.event, event.parsed[0])
diff --git a/setup.py b/setup.py
index f296015..2bccdd9 100644
--- a/setup.py
+++ b/setup.py
@@ -1,26 +1,43 @@
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages
+from TSGroupAssigner import __version__
+
+# long readme
with open("README.md", "r") as fh:
long_description = fh.read()
-
setup(
name='TSGroupAssigner',
- version='0.0.1',
- packages=find_packages(exclude=['tests', 'tests.*']),
- keywords='automation TeamSpeak teamspeak ts3 ts3server ts',
+ version=__version__,
url='https://github.com/mightyBroccoli/TSGroupAssigner',
- license='GPLv3',
author='nico wellpott',
author_email='nico@magicbroccoli.de',
- description='date based TeamSpeak Group Assigner',
- long_description=long_description,
- python_requires='>=3.7',
classifiers=[
- 'Programming Language :: Python :: 3',
'Intended Audience :: System Administrators',
+ 'Natural Language :: English',
+ 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Operating System :: Unix',
- 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)'
- ]
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Topic :: Communications',
+ 'Topic :: Internet'
+ ],
+ license='GPLv3',
+ description='date based TeamSpeak Group Assigner',
+ long_description=long_description,
+ long_description_content_type='text/markdown',
+ keywords='automation TeamSpeak teamspeak ts3 ts3server ts',
+ install_requires=[
+ 'ts3>=1.0.11,<2'
+ ],
+ packages=find_packages(exclude=('tests',)),
+ python_requires='>=3.6',
+ project_urls={
+ 'Source': 'https://github.com/mightyBroccoli/TSGroupAssigner',
+ 'Issue-Tracker': 'https://github.com/mightyBroccoli/TSGroupAssigner/issues'
+ }
)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/test_group_assign.py b/tests/test_group_assign.py
new file mode 100644
index 0000000..150aa7b
--- /dev/null
+++ b/tests/test_group_assign.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import datetime as dt
+
+import pytest
+
+from TSGroupAssigner import GroupAssigner, DateException
+
+# sample input
+creds = {
+ 'host': 'localhost',
+ 'port': 10011,
+ 'user': 'serveradmin',
+ 'password': '5up3r_53cr37',
+ 'sid': 1,
+ 'gid': 24
+}
+
+
+class TestGroupAssigner:
+ def test_missing_input(self):
+ # the main class is missing arguments and should fail with a TypeError
+
+ with pytest.raises(TypeError):
+ GroupAssigner().start()
+
+ def test_sleepstart_startdate(self):
+ # startdate is too far in the future sleepstart should produce a DateException
+
+ # start date 3 days in the future
+ startdate = dt.date.today() + dt.timedelta(days=3)
+ duration = dt.timedelta()
+
+ with pytest.raises(DateException):
+ GroupAssigner(date=startdate, delta=duration, **creds).start()
+
+ def test_datecheck_enddate(self):
+ # this should produce a exit code 0 SystemExit as the end date is in the past
+
+ # start date 2 days in the past
+ startdate = dt.date.today() + dt.timedelta(days=-2)
+ duration = dt.timedelta()
+
+ with pytest.raises(SystemExit):
+ GroupAssigner(date=startdate, delta=duration, **creds).start()
+
+ def test_connect_noconnection(self):
+ # connect should fail with ConnectionRefusedError
+
+ # start date is today
+ startdate = dt.date.today()
+ duration = dt.timedelta()
+
+ with pytest.raises(ConnectionRefusedError):
+ GroupAssigner(date=startdate, delta=duration, **creds).start()