Compare commits

..
39 Commits
Author SHA1 Message Date
lm a8566dfdde release 1.0 2022-04-14 17:05:39 +02:00
lm b22d16e364 fix rwf containing no 'progression' key 2022-04-14 00:13:30 +02:00
lm 710c570c8d enhance the rwf command 2022-04-08 22:33:23 +02:00
lm f43905b760 fix the rwf command 2022-03-21 23:38:44 +01:00
lm e2702cbfe8 rwf command now adds the number of guilds finished 2022-03-21 18:30:50 +01:00
lm f38fcd8939 rwf command now adds the number of guilds finished 2022-03-21 18:27:24 +01:00
lm bd24e53a3c fixed rwf command 2022-03-21 00:10:22 +01:00
lm 05b11aafc8 fixed rwf command 2022-03-21 00:03:47 +01:00
lm 8cac1a7da4 add rwf command 2022-03-20 23:51:33 +01:00
lm d4922f8772 add rwf command 2022-03-20 23:38:54 +01:00
lm a22905f689 update version for commands.py 2022-02-04 17:27:52 +01:00
lm c85a7520ce Fix connection error to coingecko breaking the loop 2022-02-04 17:23:55 +01:00
lm 5a060342a4 resolved merge conflict 2021-10-30 19:03:07 +02:00
lm dfca0593f5 migrate every command 2021-10-30 18:51:35 +02:00
lm ee3e081c3c fix for the rare occassion that the client has no client_type 2021-10-25 18:52:50 +02:00
lm 712faa876c mute/unmute commands, structual rewrite and ordering 2021-10-24 00:56:40 +02:00
lm c53d0ce4ed add .help command 2021-10-13 21:56:25 +02:00
lm 4d87558e0f fix loop breaking on missing clid 2021-10-11 22:51:53 +02:00
lm 890d2c47a4 start outsourcing commands 2021-10-07 14:39:05 +02:00
lm 78f82d64e5 untested follow example 2021-10-07 13:41:41 +02:00
lm 49e3de14a5 improve code quality 2021-10-07 13:18:55 +02:00
lm 55665db5bb fix isadmin function using any() 2021-10-06 18:20:23 +02:00
lm 31b745d048 fix isadmin debug output 2021-10-01 21:13:53 +02:00
Lukas Mahler 55776e31cd fix functions breaking on clients without clid 2021-09-30 08:48:30 +02:00
lm 35c64cea14 fix isqueryclient and connection errors 2021-09-27 14:24:49 +02:00
lm 71a6aa71ca fix isqueryclient function 2021-09-27 14:09:11 +02:00
lm ed9f9acb11 make connection and login more robust 2021-09-27 13:46:06 +02:00
lm 82053e8131 fix logpath for real 2 2021-09-27 13:32:38 +02:00
lm 954593258c fix logpath for real 2021-09-27 13:21:00 +02:00
lm aed1a85b20 fix logpath 2021-09-27 13:07:39 +02:00
lm c5c265d3a0 add rotating log handler 2021-09-27 13:03:32 +02:00
lm 659dfd551c Rewrite config to use toml instead of .env 2021-09-27 12:11:31 +02:00
lm de05b885c2 Rewrite config to use toml instead of .env 2021-09-27 01:47:47 +02:00
lm 1df3e08bee add unhandled exception logging 2021-09-25 15:28:26 +02:00
lm dddd444e4d remove threading from util 2021-09-25 13:26:31 +02:00
lm a472c4dc06 working crypto channel 2021-09-25 13:25:43 +02:00
lm 14fbc7f827 add logging / add some docstrings 2021-09-25 00:49:51 +02:00
lm 305ad6cffb Merge pull request 'Using functools partial to pass function instead of return value of function' (#1) from kono/myTS3:master into master
Reviewed-on: #1
2021-09-24 14:11:02 +02:00
kono 7540e57c08 Using functools partial to pass function instead of return value of function 2021-09-23 22:54:00 +02:00
9 changed files with 858 additions and 317 deletions
+2
View File
@@ -141,3 +141,5 @@ cython_debug/
# Pycharm
.idea/
prod.toml
log/
+2 -1
View File
@@ -5,8 +5,9 @@ name = "pypi"
[packages]
ts3 = "*"
python-dotenv = "*"
pycoingecko = "*"
toml = "*"
requests = "*"
[dev-packages]
Generated
+25 -25
View File
@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "dea9fe66fc91a3adf33caec6ce10635546f9e81ec613bc3bbba14c257d57029b"
"sha256": "31ecf8cb0f57cd41d5c094347ce8878eddd6f611790259ee671f31d6620a1bee"
},
"pipfile-spec": 6,
"requires": {
@@ -18,26 +18,26 @@
"default": {
"certifi": {
"hashes": [
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
"version": "==2021.5.30"
"version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
"sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6",
"sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_version >= '3'",
"version": "==2.0.6"
"version": "==2.0.12"
},
"idna": {
"hashes": [
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"version": "==3.2"
"version": "==3.3"
},
"pycoingecko": {
"hashes": [
@@ -47,21 +47,21 @@
"index": "pypi",
"version": "==2.2.0"
},
"python-dotenv": {
"hashes": [
"sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1",
"sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"
],
"index": "pypi",
"version": "==0.19.0"
},
"requests": {
"hashes": [
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==2.26.0"
"index": "pypi",
"version": "==2.27.1"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"index": "pypi",
"version": "==0.10.2"
},
"ts3": {
"hashes": [
@@ -73,11 +73,11 @@
},
"urllib3": {
"hashes": [
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.7"
"version": "==1.26.9"
}
},
"develop": {}
+348 -257
View File
@@ -3,73 +3,93 @@ TBD
"""
__author__ = "Lukas Mahler"
__version__ = "0.2.0"
__date__ = "23.09.2021"
__version__ = "1.0.0"
__date__ = "14.04.2022"
__email__ = "m@hler.eu"
__status__ = "Development"
# Default
import os
import json
from time import sleep
import sys
import time
import os.path
# Custom
import ts3
import dotenv # python-dotenv
# Self
import gecko
import util
from src import util, gecko, commands
class MyTeamspeakBot:
class TSbot:
def __init__(self, conf):
def __init__(self, conf, log):
"""
Create a new instance, connect to the server via telnet and
start the event loop function.
"""
"""
# Initialize from config
self.host = conf["Connection"]["host"]
self.port = conf["Connection"]["port"]
self.sid = conf["Connection"]["sid"]
self.user = conf["Authentication"]["user"]
self.pwd = conf["Authentication"]["pwd"]
self.sgid_admin = conf["Groups"]["admins"]
self.sgid_mute = conf["Groups"]["mute"]
self.crypto = conf["Misc"]["crypto"]
self.nickname = conf["Misc"]["nickname"]
self.whitelisted = conf["Misc"]["whitelisted"]
self.host = conf["host"]
self.port = conf["port"]
self.user = conf["user"]
self.pwd = conf["pwd"]
self.sid = conf["sid"]
self.nickname = conf["name"]
self.gecko = gecko.GeckoAPI()
# Initialize self
self.log = log
self.myid = None
self.running = True
self.intro = "<Keep this chat open to use commands>"
self.version = __version__
self.started = time.time()
self.gecko = gecko.GeckoAPI()
self.crypto_update = self.started - 1800
print(f"* Trying to connect to: {self.host}:{self.port}")
self.pipeOut(f"Trying to connect to: {self.host}:{self.port}")
# Starting the Connection
with ts3.query.TS3Connection(self.host, self.port) as self.bot:
self.bot.login(client_login_name=self.user, client_login_password=self.pwd)
self.bot.use(sid=self.sid)
try:
self.bot.login(client_login_name=self.user, client_login_password=self.pwd)
self.bot.use(sid=self.sid)
except ts3.query.TS3QueryError:
self.pipeOut("Invalid login credentials, please check your '.toml' file.", lvl="CRITICAL")
try:
self.bot.clientupdate(client_nickname=self.nickname)
except ts3.query.TS3QueryError:
except ts3.query.TS3QueryError as e:
self.pipeOut(e, lvl="WARNING")
pass
print(f"* Successfully connected as: {self.nickname}")
# Start the Bot
self.pipeOut(f"Successfully connected as: {self.nickname}")
# Start the Bot loop
self.loop()
def loop(self):
"""
Subscribe to event types, ping bot admins and start the event loop.
Every time an event is triggered, the bot will identify its type and
execute the appropriate function.
The loop can be stopped setting self.running to False
"""
# Find my client id
# Find the instances client id
me = self.bot.clientfind(pattern=self.nickname)
if len(me) == 1:
self.myid = me[0]["clid"]
else:
raise ValueError("x Can't find my own client id.")
self.pipeOut("Can't find my own client id. terminating...", lvl="critical")
exit(1)
''' if you want to move the Bot to a certain channel (instead of the defualt channel, you can do: '''
''' if you want to move the Bot to a certain channel (instead of the default channel, you can do: '''
# self.bot.clientmove(clid=self.myid,cid=129)
# ----------- SUBSCRIBE TO EVENTS -------------
# Subscribe to a server movement events
self.bot.servernotifyregister(event="server")
@@ -85,23 +105,31 @@ class MyTeamspeakBot:
# Subscribe to channel movement events
# self.bot.servernotifyregister(event="channel", id_=0)
# Start the timer for auto-updating crypto channels
channelname = f"{'[cspacerBTC]Bitcoin:':<33}" + f"{self.gecko.getSymbol('BTC', decimal=0):>5}"
btc_timer = util.maketimer(60, self.editChannelname(200, channelname))
btc_timer.start()
channelname = f"{'[cspacerETH]Ethereum:':<30}" + f"{self.gecko.getSymbol('ETH', decimal=0):>5}"
eth_timer = util.maketimer(60, self.editChannelname(201, channelname))
eth_timer.start()
if not self.whitelisted:
time.sleep(5)
# Notify connected admins
self.notifyAdmin()
commands.notifyAdmin(self)
# ----------- LOOP HERE -------------
while self.running:
# self.bot.send_keepalive()
print("* Waiting for a new Event...")
self.pipeOut(f"Waiting for a new Event...", lvl="debug")
self.bot.version()
# Auto-update crypto price channels every 30 minutes
if self.crypto:
if self.crypto_update + 1800 < time.time():
self.crypto_update = time.time()
commands.ticker(self, "")
# Hijacked for WoW
commands.rwf(self)
else:
pass
try:
# This method blocks, but we must sent the keepalive message at
# least once in 5 minutes to avoid the sever side idle client
@@ -110,20 +138,33 @@ class MyTeamspeakBot:
except ts3.query.TS3TimeoutError:
pass
# try else!
else:
event_type = event.event
print(f"* Got Event | length={len(event[0])} | {event_type}")
self.pipeOut(f"Got Event | length={len(event[0])} | {event_type}")
# Client Connect
# Client connected
if event_type == "notifycliententerview":
print(f"* Client [{event[0]['client_nickname']}] connected.")
if 'client_nickname' in event[0]:
displayname = event[0]['client_nickname']
elif 'clid' in event[0]:
displayname = event[0]['clid']
else:
self.pipeOut(event[0])
# displayname = "Unresolved"
continue # can't resolve no clid
self.pipeOut(f"Client [{displayname}] connected.")
# Check if the connector is a ServerQuery or not
if not self.isqueryclient(event[0]["client_unique_identifier"]):
print(f"* {event[0]}")
if not self.isqueryclient(event[0]["clid"]):
self.pipeOut(f"* {event[0]}", lvl="debug")
# Check if the connector is an Admin
if self.isadmin(event[0]["client_database_id"]):
self.bot.sendtextmessage(targetmode=1, target=event[0]["clid"], msg=self.intro)
msg = "<Keep this chat open to use commands>"
self.bot.sendtextmessage(targetmode=1, target=event[0]["clid"], msg=msg)
else:
pass
else:
@@ -131,96 +172,127 @@ class MyTeamspeakBot:
# Client disconnected
elif event_type == "notifyclientleftview":
print(f"* Clientid [{event[0]['clid']}] disconnected.")
pass
# Text Message
if 'client_nickname' in event[0]:
displayname = event[0]['client_nickname']
else:
displayname = event[0]['clid']
self.pipeOut(f"Clientid [{displayname}] disconnected.")
# New text message
elif event_type == "notifytextmessage":
msg = event[0]["msg"]
invkr = event[0]["invokername"]
msg = event[0]["msg"].replace("\n", " ")
invkr_name = event[0]["invokername"]
invkr_id = event[0]["invokerid"]
print(f'* From: "{invkr}" | Message: "{msg}"')
self.lookupcommand(msg, invkr_id)
invkr_uid = event[0]["invokeruid"]
invkr_dbid = self.bot.clientgetdbidfromuid(cluid=invkr_uid)[0]["cldbid"]
permission_level = 1 if self.isadmin(invkr_dbid) else 0
invoker = {'name': invkr_name,
'id': invkr_id,
'uid': invkr_uid,
'dbid': invkr_dbid}
self.pipeOut(f'Message | From: "{invkr_name}" | '
f'Permissionlevel: {permission_level} | Content: "{msg}"')
if msg.startswith("."):
self.lookupcommand(msg, invoker, permission_level)
# Unknown Event
else:
print(f"* Unknown Event: {event.__dict__}")
self.pipeOut(f"Unknown Event: {event.__dict__}", lvl="warning")
def stop(self, invkr_id):
# Class Utility --------------------------------------------------------------------------------------------------------
def pipeOut(self, msg, lvl="INFO", reprint=True):
"""
All output pipes through this function.
It's possible to print the output to console [set reprint to True]
or run in silent log mode. [set reprint to False]
"""
lvl = lvl.upper()
lvln = int(getattr(util.logging, lvl))
self.log.log(lvln, msg)
if reprint and lvln >= self.log.getEffectiveLevel():
print(f"[{time.strftime('%H:%M:%S')}]{f'[{lvl}]':10s} {msg}")
if not self.whitelisted:
if msg == "error id 524: client is flooding":
time.sleep(30)
if lvl == "CRITICAL":
exit(1)
def stop(self, invoker):
"""
msg = "I'm out, bye bye!"
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=msg)
This stops the bot instance by invalidating the event loop.
"""
msg = "Shutdown, bye bye!"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=msg)
self.pipeOut(msg)
self.running = False
def notifyAdmin(self):
clients = self.bot.clientlist()
clients = [client for client in clients if client["client_type"] != "1"]
for client in clients:
cldbid = client["client_database_id"]
clid = client["clid"]
if self.isadmin(cldbid):
self.bot.sendtextmessage(targetmode=1, target=clid, msg=self.intro)
sleep(1) # This can be removed if the Query Client is Whitelisted
def kickall(self, msg):
"""
TODO
"""
clients = self.bot.clientlist()
clients = [client["clid"] for client in clients if client["client_type"] != "1"]
for clid in clients:
try:
self.bot.clientpoke(msg=msg, clid=clid)
# TODO
except:
pass
self.bot.clientpoke(msg=msg, clid=clid)
# TODO
def poke(self, msg=None, num=10, delay=0.2, usr='all'):
def poke(self, msg=None, n=1, delay=0.2, usr='all'):
"""
ping a single or multiple users for n times including a msg.
"""
if msg is None:
msg = "-~-~-~-~-~-~-~-~-~-~-~"
msg = "-~-~-~-~-~ Placeholder -~-~-~-~-~-~"
# Get the client ids
if usr == 'all':
clients = self.bot.clientlist()
clients = [client["clid"] for client in clients if client["client_type"] != "1"]
else:
clients = self.bot.clientfind(pattern=usr)
clients = [client["clid"] for client in clients]
try:
clients = self.bot.clientfind(pattern=usr)
clients = [client["clid"] for client in clients]
except ts3.query.TS3QueryError as e:
self.pipeOut(f"Couldnt execute poke, no client found using pattern {usr},"
f"returned error:\n{e}", lvl="ERROR")
return
# Break, if there's no client.
if not clients:
return None
else:
for client in clients:
data = self.printable_clientinfo(client)
print(f"* {data}")
# Ping them
if len(clients) > 0:
# Nopokeatm
# return
# Poke them
i = 0
while num == -1 or i < num:
for clid in clients:
print(f"* {clid}")
try:
self.bot.clientpoke(msg=msg, clid=clid)
except:
pass
sleep(delay)
i += 1
return None
data = self.printable_clientinfo(clid)
self.pipeOut(f"{data}", lvl="DEBUG")
i = 0
while n == -1 or i < n:
for clid in clients:
try:
self.bot.clientpoke(msg=msg, clid=clid)
except Exception as e:
self.pipeOut(e, lvl="WARNING")
pass
time.sleep(delay)
i += 1
def createChannel(self, name, permanent=False):
"""
Create a teamspeak channel, the channel is non persistent by default.
Set permanent to True if you want a persistant channel.
"""
if permanent:
new = self.bot.channelcreate(channel_name=name, channel_flag_permanent="1")
else:
@@ -230,197 +302,216 @@ class MyTeamspeakBot:
def editChannelname(self, cid, name):
"""
"""
self.bot.channeledit(cid=cid, channel_name=name)
sleep(5)
def list(self, what, invkr_id):
Using a channel-id you can set the name of the given channel.
This will fail if the channel name is already in use.
This will fail if the given channel name is > 40 bytes.
"""
# Check name for bytes
n = len(name)
if n > 40:
self.pipeOut(f"Can't change channelname to {name} as [{n}] exceeds the max bytes of 40.")
return
try:
self.bot.channeledit(cid=cid, channel_name=name)
except Exception as e:
self.pipeOut(e, lvl="error")
def isqueryclient(self, clid):
"""
if what == "channel":
mydict = {}
channels = self.bot.channellist()
for channel in channels:
order = channel["channel_order"]
mydict[order] = [channel["channel_name"], channel["cid"]]
mydict = dict(sorted(mydict.items()))
msg = json.dumps(mydict)
elif what == "clients":
msg = self.bot.clientlist()
# TODO
else:
msg = None
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=msg)
@staticmethod
def isqueryclient(cluid):
Check if the given client-uid is a query client.
"""
Check if the given client-uid is a query client
"""
# client = self.bot.clientlist(uid=uid)
# print(client[0])
# if client[0]["client_type"] == "1":
if cluid == "ServerQuery":
print("* ISQUERY: True")
try:
client = self.bot.clientinfo(clid=clid)
except ts3.query.TS3QueryError as e:
self.pipeOut(f"ISQUERY: given clid {clid} returned: {e}", lvl="WARNING")
return True
else:
print("* ISQUERY: False")
return False
try:
if client[0]["client_type"] == "1":
self.pipeOut(f"[{clid}] ISQUERY: True")
return True
else:
self.pipeOut(f"[{clid}] ISQUERY: False")
return False
except KeyError as e:
self.pipeOut(f"Error [{e}] on client [{client.__dict__}] ")
return True # Da wahrscheinlich ein Query Client
def isadmin(self, cldbid):
"""
Check if the given client-databaseid is an admin
Check if the given client-databaseid is an admin.
This is done by resolving the clients groups and check
if any of the groups sgid match a sgid from the sgids from the '.toml' config.
"""
groups = self.bot.servergroupsbyclientid(cldbid=cldbid)
# [print(group["sgid"]) for group in groups]
for group in groups:
# 6 Server Admin/ 13 Operator / 15 Root
# if (group["sgid"] == "6") or (group["sgid"] == "13") or (group["sgid"] == "15"):
if group["sgid"] == "15":
print("* ISADMIN: True")
return True
else:
continue
try:
groups = self.bot.servergroupsbyclientid(cldbid=cldbid)
except ts3.query.TS3QueryError as e:
self.pipeOut(e, lvl="ERROR")
return False
print("* ISADMIN: False")
self.pipeOut(str(groups.__dict__), lvl="DEBUG") # DEBUG
for group in groups:
if any(sgid == group['sgid'] for sgid in self.sgid_admin):
self.pipeOut(f"[{cldbid}] ISADMIN: True")
return True
self.pipeOut(f"[{cldbid}] ISADMIN: False")
return False
def lookupcommand(self, msg, invkr_id):
def printable_clientinfo(self, clid):
"""
Generate printable clientinfo from clid.
"""
"""
if msg.startswith("."):
commandstring = msg.split(" ")
command = commandstring[0]
parameter = commandstring[1:]
print(f"* command: {command} / parameter: {parameter} / invkr_id: {invkr_id}")
if command == ".annoy":
try:
target = parameter[0]
msg = parameter[1]
self.poke(msg=msg, usr=target)
except IndexError:
err = "Please use the command like this: .annoy TARGET MESSAGE"
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=err)
pass
elif command == ".kickall":
self.kickall("test") # TODO
elif command == ".test":
cid = self.createChannel("Test")
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=cid)
elif command == ".btc":
channelname = f"{'[cspacerBTC]Bitcoin:':<33}" + f"{self.gecko.getSymbol('BTC', decimal=0):>5}"
try:
self.editChannelname(200, channelname)
except ts3.query.TS3QueryError:
pass
elif command == ".eth":
channelname = f"{'[cspacerETH]Ethereum:':<30}" + f"{self.gecko.getSymbol('ETH', decimal=0):>5}"
try:
self.editChannelname(201, channelname)
except ts3.query.TS3QueryError:
pass
elif command == ".list":
try:
self.list(parameter[0], invkr_id)
except IndexError:
err = "Please use the command like this: .list channel/clients"
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=err)
pass
elif command == ".follow":
pass # TODO
elif command == ".rename":
pass # TODO
elif command == ".stop" or command == ".quit" or command == ".q":
self.stop(invkr_id)
elif command == ".pingall":
try:
msg = parameter[0]
self.poke(msg=msg)
except IndexError:
err = "Please use the command like this: .pingall MESSAGE"
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=err)
pass
else:
err = f"Unknown Command: [{command}]"
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=err)
def printable_clientinfo(self, client):
"""
"""
usrd = {}
info = self.bot.clientinfo(clid=client)
temp = info._data[0].split()
for t1 in temp:
t2 = t1.decode("utf-8")
t3 = t2.split("=")
try:
t3[1]
except Exception:
t3.append("None")
pass
usrd[t3[0]] = t3[1]
info = self.bot.clientinfo(clid=clid)
usrd = info.__dict__
return usrd
def checkgrp(self):
def clid2cldbid(self, clid):
"""
Convert a given clid to a cldbid.
"""
cluid = self.bot.clientgetuidfromclid(clid=clid)[0]["cluid"]
cldbid = self.bot.clientgetdbidfromuid(cluid=cluid)[0]["cldbid"]
return cldbid
def identifytarget(self, target):
"""
Try to find clid from nickname.
"""
clients = self.bot.clientlist()
clients_cldbid = [client["client_database_id"] for client in clients if client["client_type"] != "1"]
clients = self.bot.clientlist(groups=True)
clients_groups = [client["client_servergroups"] for client in clients if client["client_type"] != "1"]
try:
clients = self.bot.clientfind(pattern=target)
clids = [client["clid"] for client in clients]
if len(clids) == 1:
return clids[0]
else:
self.pipeOut(f"Found multiple matching clients using pattern {target}:{clids}")
return
print(f"* {clients_groups}")
except ts3.query.TS3QueryError as e:
self.pipeOut(f"No client found using pattern [{target}], "
f"returned error:\n{e}", lvl="ERROR")
return
# Commands -------------------------------------------------------------------------------------------------------------
def lookupcommand(self, msg, invoker, permission_level):
"""
Every message starting with '.' gets passed and evaluated in this function.
Commands in this function are sorted alphabetically.
Parameters in brackets are optional.
Currently only 2 permission levels are implemented [0 = Everyone, 1 = Admins]
Command Parameter 1 Parameter 2 Permissionlevel
--------------------------------------------------------------------------------------
.admin / .notifyAdmin 0
.annoy target message 1
.follow target 1
.help / .h 0
.info clid 0
.kickall message 1
.list channel/clients/commands/groups 1
.mute target 1
.pingall message 1
.rename nickname 1
.rmb amount 0
.roll 0
.rwf 0
.stop / .quit / .q 1
.test 1
.ticker (symbol) 0
.unmute target 1
"""
commandstring = msg.split(" ")
command = commandstring[0]
parameter = commandstring[1:]
self.pipeOut(f"command: {command} | parameter: {parameter} | "
f"from: {invoker['name']} | permissionlevel: {permission_level}")
if command == ".admin" or command == ".notifyAdmin":
commands.notifyAdmin(self)
elif command == ".annoy" and permission_level > 0:
commands.annoy(self, invoker, parameter)
elif command == ".follow" and permission_level > 0:
commands.follow(self, invoker, parameter)
elif command == ".help" or command == ".h":
commands.help(self, invoker)
elif command == ".info":
commands.info(self, invoker)
elif command == ".kickall" and permission_level > 0:
self.kickall("test") # TODO
elif command == ".list" and permission_level > 0:
commands.list(self, invoker, parameter)
elif command == ".mute" and permission_level > 0:
commands.mute(self, invoker, parameter)
elif command == ".pingall" and permission_level > 0:
commands.pingall(self, invoker, parameter)
elif command == ".rename" and permission_level > 0:
commands.rename(self, invoker, parameter)
elif command == ".rmb":
commands.rmb(self, invoker, parameter)
elif command == ".roll":
commands.roll(self, invoker)
elif command == ".rwf":
commands.rwf(self)
elif command == ".stop" or command == ".quit" or command == ".q" and permission_level > 0:
commands.quit(self, invoker)
elif command == ".test" and permission_level > 0:
cid = self.createChannel("Test")
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=cid)
elif command == ".ticker":
commands.ticker(self, parameter)
elif command == ".unmute" and permission_level > 0:
commands.unmute(self, invoker, parameter)
else:
err = f"Unknown Command: [{command}]"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
# ----------------------------------------------------------------------------------------------------------------------
def main():
# Load Dotenv
dotenv_file = dotenv.find_dotenv()
if not dotenv_file:
raise FileNotFoundError("could not locate .env file")
dotenv.load_dotenv(dotenv_file)
dotend_keys = dotenv.dotenv_values()
# Start logger
logpath = os.path.dirname(os.path.abspath(__file__)) + r"/log"
log = util.setupLogger(logpath)
if not {"_HOST", "_PORT", "_USER", "_PWD", "_SID"} <= dotend_keys.keys():
raise ValueError("missing keys in your .env file.")
# Log unhandled exception
sys.excepthook = util.unhandledException
# Config
conf = dict(host=os.getenv("_HOST"),
port=os.getenv("_PORT"),
user=os.getenv("_USER"),
pwd=os.getenv("_PWD"),
sid=os.getenv("_SID"),
name=os.getenv("_NAME"))
# Load toml config
conf = util.getConf("prod.toml")
# Change loglevel from config
util.changeLevel(log, conf['Log']['level'])
# Start the Bot Instance
abot = MyTeamspeakBot(conf)
TSbot(conf, log)
if __name__ == "__main__":
+343
View File
@@ -0,0 +1,343 @@
"""
TBD
"""
__author__ = "Lukas Mahler"
__version__ = "1.0.0"
__date__ = "14.04.2022"
__email__ = "m@hler.eu"
__status__ = "Development"
# Default
import sys
import json
import time
import random
import inspect
# Custom
import ts3
import requests
# Self
from src import util
def annoy(self, invoker, parameter):
"""
"""
if not len(parameter) != 1:
err = "Please use the command like this: .annoy TARGET MESSAGE"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
target = parameter[0]
msg = parameter[1]
self.poke(n=10, msg=msg, usr=target)
def follow(self, invoker, parameter):
"""
TODO sticky folgen
"""
if not parameter:
err = "Please use the command like this: .follow TARGET"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
target = parameter[0]
clid = self.identifytarget(target)
cid = self.bot.clientinfo(clid=int(clid))[0]["cid"]
self.bot.clientmove(clid=self.myid, cid=cid)
def help(self, invoker):
"""
"""
# Find all functions in this submodule
cmds = [f[0] for f in inspect.getmembers(sys.modules[__name__], inspect.isfunction)]
msg = f"List of commands:\n---------------------\n.{f'{chr(10)}.'.join(cmds)}"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=msg)
def info(self, invoker):
"""
"""
msg = f"Runtime: {util.getRuntime(self.started)}\n" \
f"Version: {self.version}"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=msg)
def list(self, invoker, parameter):
"""
Message the invoker of the function either a list of channels, clients, commands or server groups.
"""
if not parameter:
err = "Please use the command like this: .list channel/clients/commands/groups/"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
what = parameter[0]
mydict = {}
if what == "channel":
channels = self.bot.channellist()
for channel in channels:
order = channel["channel_order"]
mydict[order] = [channel["channel_name"], channel["cid"]]
mydict = dict(sorted(mydict.items()))
msg = json.dumps(mydict)
elif what == "clients":
clients = self.bot.clientlist()
for client in clients:
mydict[client["client_nickname"]] = {"clid": client["clid"], "cldbid": client["client_database_id"]}
mydict = dict(sorted(mydict.items()))
msg = json.dumps(mydict)
elif what == "commands":
help(self, invoker)
return
elif what == "groups":
groups = self.bot.servergrouplist()
for group in groups:
mydict[group["name"]] = {"sgid": group["sgid"], "sortid": group["sortid"]}
msg = json.dumps(mydict)
else:
msg = f"The parameter [{what}] is not supported."
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=msg)
def mute(self, invoker, parameter):
"""
Assign the mute group to a user.
"""
if not parameter:
err = "Please use the command like this: .mute TARGET"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
target = parameter[0]
clid = self.identifytarget(target)
cldbid = self.clid2cldbid(clid)
try:
self.bot.servergroupaddclient(sgid=self.sgid_mute, cldbid=cldbid)
except ts3.query.TS3QueryError as e:
err = f"Failed to add to group: [{e}]"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
def pingall(self, invoker, parameter):
"""
"""
if not parameter:
err = "Please use the command like this: .pingall MESSAGE"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
msg = parameter[0]
self.poke(msg=msg)
def notifyAdmin(self):
"""
Message every available admin.
"""
clients = self.bot.clientlist()
clients = [client for client in clients if client["client_type"] != "1"]
for client in clients:
cldbid = client["client_database_id"]
clid = client["clid"]
if self.isadmin(cldbid):
msg = "<Keep this chat open to use commands>"
self.bot.sendtextmessage(targetmode=1, target=clid, msg=msg)
if not self.whitelisted:
time.sleep(1)
def quit(self, invoker):
"""
"""
self.stop(invoker)
def rename(self, invoker, parameter):
"""
Rename the Bot to the given Nickname
"""
if not parameter:
err = "Please use the command like this: .rename NICKNAME"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
self.nickname = parameter[0]
self.bot.clientupdate(client_nickname=self.nickname)
def rmb(self, invoker, parameter):
"""
Convert rmb (Chinese Yuan) to eur (Euro)
"""
if not parameter:
err = "Please use the command like this: .rmb AMOUNT"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
url = "https://www.floatrates.com/daily/cny.json"
req = requests.get(url)
if req.status_code != 200:
self.pipeOut(req.status_code, lvl="WARNING")
return
data = json.loads(req.text)
if not data:
return
if "eur" not in data:
self.pipeOut(data, lvl="WARNING")
return
try:
conversion_rate = float(data['eur']['inverseRate'])
except KeyError:
conversion_rate = 6.94 # Static
if "k" in parameter[0]:
rmb_val = float(parameter[0].replace("k", "000"))
else:
rmb_val = float(parameter[0])
eur = rmb_val / conversion_rate
msg = f"[{rmb_val:.0f}] rmb is approx. [{eur:.0f}] eur / conversion rate [{conversion_rate:.2f}]"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=msg)
def roll(self, invoker):
cid_invoker = self.bot.clientinfo(clid=int(invoker['id']))[0]["cid"]
cid_self = self.bot.clientinfo(clid=self.myid)[0]["cid"]
if cid_self != cid_invoker:
self.bot.clientmove(clid=self.myid, cid=cid_invoker)
rolled = random.randint(0, 100)
msg = f"Rolled a [{rolled:3d}] by {invoker['name']}"
self.bot.sendtextmessage(targetmode=2, target=cid_invoker, msg=msg)
def rwf(self):
"""
Refresh the WoW progress channel,
this is very specific to my use, you can disable this in the config.
"""
url = "https://raider.io/api/v1/raiding/progression?raid=sepulcher-of-the-first-ones&difficulty=mythic&region=world"
req = requests.get(url)
if req.status_code != 200:
self.pipeOut(req.status_code, lvl="WARNING")
return
data = json.loads(req.text)
if not data:
return
if "progression" not in data:
self.pipeOut(data, lvl="WARNING")
return
prog_done = 0
prog_max = len(data['progression'])
for i in range(0, prog_max):
if data['progression'][i]['totalGuilds'] > 0:
prog_done = i
guilds_done = data['progression'][prog_done]['totalGuilds']
channelname = f"[cspacerC3]Sepulcher {prog_done+1}/{prog_max} ({guilds_done})"
try:
self.editChannelname(205, channelname)
except ts3.query.TS3QueryError:
pass
def ticker(self, parameter):
"""
Refresh the crypto Ticker channels,
this is very specific to my use, you can disable this in the config.
"""
if parameter:
symbol = parameter[0].lower()
else:
symbol = "all"
if symbol in ["btc", "bitcoin", "eth", "ethereum", "all"]:
channelname = f"[cspacerC1]BTC: {self.gecko.getSymbol('bitcoin', decimal=0)} | " \
f"ETH: {self.gecko.getSymbol('ethereum', decimal=0)}"
try:
self.editChannelname(200, channelname)
except ts3.query.TS3QueryError:
pass
if symbol in ["dot", "polkadot", "ada", "cardano", "all"]:
channelname = f"[cspacerC2]DOT: {self.gecko.getSymbol('polkadot', decimal=2)} | " \
f"ADA: {self.gecko.getSymbol('cardano', decimal=2)}"
try:
self.editChannelname(201, channelname)
except ts3.query.TS3QueryError:
pass
def unmute(self, invoker, parameter):
"""
Remove the mute group to a user.
"""
if not parameter:
err = "Please use the command like this: .unmute TARGET"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
target = parameter[0]
clid = self.identifytarget(target)
cldbid = self.clid2cldbid(clid)
try:
self.bot.servergroupdelclient(sgid=self.sgid_mute, cldbid=cldbid)
except ts3.query.TS3QueryError as e:
err = f"Failed to remove from group: [{e}]"
self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=err)
return
if __name__ == "__main__":
exit()
+13 -5
View File
@@ -4,11 +4,12 @@ TBD
__author__ = "Lukas Mahler"
__version__ = "0.0.1"
__date__ = "23.09.2021"
__date__ = "04.02.2022"
__email__ = "m@hler.eu"
__status__ = "Development"
# Imports
from datetime import datetime
from pycoingecko import CoinGeckoAPI
@@ -37,21 +38,28 @@ class GeckoAPI:
return self.api.get_coins_list()
def getSymbol(self, symbol, curr="EUR", decimal=2):
def getSymbol(self, symbol_id, curr="EUR", decimal=2):
"""
"""
matching = [x for x in self.coinlist if x["symbol"] == symbol.lower()]
matching = [x for x in self.coinlist if x["id"] == symbol_id.lower()]
if len(matching) == 1:
resp_dict = self.api.get_price(ids=matching[0]["id"], vs_currencies=curr.lower())
try:
resp_dict = self.api.get_price(ids=matching[0]["id"], vs_currencies=curr.lower())
except Exception as e:
with open("gecko_error.log", 'a+') as log:
log.write(f"{datetime.now():%Y-%m-%d %H:%M} | {e}\n")
return "Error"
price = resp_dict[matching[0]["id"]][curr.lower()]
if isinstance(price, int):
price = float(price) # Convert to always have floats
price = f"{price:.{decimal}f}"
return price
else:
return "No unique coin found"
print(matching)
# No unique coin found
return "Not found"
if __name__ == "__main__":
+20
View File
@@ -0,0 +1,20 @@
[Connection]
host = "127.0.0.1"
port = 10011
sid = 1
[Authentication]
user = "exampleuser"
pwd = "password"
[Groups]
admins = ["1"]
mute = "0"
[Misc]
nickname = "myTS3-Bot"
whitelisted = false
crypto = false
[Log]
level = "INFO"
+105
View File
@@ -0,0 +1,105 @@
"""
TBD
"""
__author__ = "Lukas Mahler"
__version__ = "1.0.0"
__date__ = "14.04.2022"
__email__ = "m@hler.eu"
__status__ = "Development"
# Default
import sys
import shutil
import os.path
import logging
import time
from datetime import timedelta
from logging.handlers import RotatingFileHandler
# Custom
import toml
def unhandledException(exc_type, exc_value, exc_traceback):
"""
src = https://stackoverflow.com/a/16993115/5593051
"""
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
log.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
def getConf(fname):
"""
"""
if fname.endswith(".toml"):
if os.path.isfile(fname):
try:
config = toml.load(fname)
checkConf(config)
return config
except ValueError as e:
log.critical(f"The provided '.toml' is probably invalid, returned error:\n{e}")
exit(1)
else:
log.critical(f"Couldn't locate the '.toml' file [{fname}].")
log.info("Creating a new '.toml' file from template, please edit and restart.")
shutil.copy("src/template.toml", fname)
exit(1)
else:
log.critical(f"The provided config file [{fname}] is not a '.toml' file.")
log.info("Creating a new '.toml' file from template, please edit and restart.")
shutil.copy("src/template.toml", "prod.toml")
exit(1)
def getRuntime(started):
"""
"""
elapsed = time.time() - started
runtime = str(timedelta(seconds=elapsed)).split(".")[0] # ignore Microseconds
return runtime
def checkConf(config):
"""
TODO check if keys exist
"""
pass
def setupLogger(logpath, lvl="DEBUG"):
"""
Create a rotating log in a log folder
"""
global log # Needed to log exceptions in src\util
log = logging.getLogger("mylog")
if not os.path.exists(logpath):
os.makedirs(logpath)
handler = RotatingFileHandler(logpath + r"/myTS3.log", encoding='utf-8', maxBytes=1*1024*1024, backupCount=10)
logformat = logging.Formatter("%(asctime)s %(levelname)8s %(message)s", "%Y-%m-%d %H:%M:%S")
handler.setFormatter(logformat)
log.addHandler(handler)
log.setLevel(logging.getLevelName(lvl))
return log
def changeLevel(log, lvl):
"""
Change the loglevel after creation
"""
log.setLevel(logging.getLevelName(lvl))
if __name__ == "__main__":
exit()
-29
View File
@@ -1,29 +0,0 @@
"""
TBD
"""
__author__ = "Lukas Mahler"
__version__ = "0.2.0"
__date__ = "23.09.2021"
__email__ = "m@hler.eu"
__status__ = "Development"
# Default
import threading
class MyTimer(threading.Timer):
def run(self):
while not self.finished.wait(self.interval):
self.function(*self.args, **self.kwargs)
def maketimer(cooldown, func):
timer = MyTimer(cooldown, func)
timer.daemon = True
return timer
if __name__ == "__main__":
exit()