Compare commits

...
32 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
8 changed files with 663 additions and 250 deletions
+2
View File
@@ -141,3 +141,5 @@ cython_debug/
# Pycharm # Pycharm
.idea/ .idea/
prod.toml
log/
+1
View File
@@ -7,6 +7,7 @@ name = "pypi"
ts3 = "*" ts3 = "*"
pycoingecko = "*" pycoingecko = "*"
toml = "*" toml = "*"
requests = "*"
[dev-packages] [dev-packages]
Generated
+17 -17
View File
@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "d9f13c4341b606a12b8b0ef95f6bce67911d3f7866a6c4552c342c4a5ae15fdf" "sha256": "31ecf8cb0f57cd41d5c094347ce8878eddd6f611790259ee671f31d6620a1bee"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -18,26 +18,26 @@
"default": { "default": {
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
], ],
"version": "==2021.5.30" "version": "==2021.10.8"
}, },
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
], ],
"markers": "python_version >= '3'", "markers": "python_version >= '3'",
"version": "==2.0.6" "version": "==2.0.12"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a", "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3" "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
], ],
"markers": "python_version >= '3'", "markers": "python_version >= '3'",
"version": "==3.2" "version": "==3.3"
}, },
"pycoingecko": { "pycoingecko": {
"hashes": [ "hashes": [
@@ -49,11 +49,11 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "index": "pypi",
"version": "==2.26.0" "version": "==2.27.1"
}, },
"toml": { "toml": {
"hashes": [ "hashes": [
@@ -73,11 +73,11 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" "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'", "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": {} "develop": {}
+229 -204
View File
@@ -3,21 +3,21 @@ TBD
""" """
__author__ = "Lukas Mahler" __author__ = "Lukas Mahler"
__version__ = "0.0.0" __version__ = "1.0.0"
__date__ = "27.09.2021" __date__ = "14.04.2022"
__email__ = "m@hler.eu" __email__ = "m@hler.eu"
__status__ = "Development" __status__ = "Development"
# Default # Default
import sys import sys
import time import time
import json import os.path
# Custom # Custom
import ts3 import ts3
# Self # Self
from src import util, gecko from src import util, gecko, commands
class TSbot: class TSbot:
@@ -28,35 +28,45 @@ class TSbot:
start the event loop function. start the event loop function.
""" """
# Initialize from config
self.host = conf["Connection"]["host"] self.host = conf["Connection"]["host"]
self.port = conf["Connection"]["port"] self.port = conf["Connection"]["port"]
self.sid = conf["Connection"]["sid"] self.sid = conf["Connection"]["sid"]
self.user = conf["Authentication"]["user"] self.user = conf["Authentication"]["user"]
self.pwd = conf["Authentication"]["pwd"] 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.nickname = conf["Misc"]["nickname"]
self.whitelisted = conf["Misc"]["whitelisted"] self.whitelisted = conf["Misc"]["whitelisted"]
self.gecko = gecko.GeckoAPI() # Initialize self
self.log = log self.log = log
self.myid = None self.myid = None
self.running = True self.running = True
self.intro = "<Keep this chat open to use commands>" self.version = __version__
self.last_crypto_update = time.time() - 1800 self.started = time.time()
self.gecko = gecko.GeckoAPI()
self.crypto_update = self.started - 1800
self.pipeOut(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: with ts3.query.TS3Connection(self.host, self.port) as self.bot:
try:
self.bot.login(client_login_name=self.user, client_login_password=self.pwd) self.bot.login(client_login_name=self.user, client_login_password=self.pwd)
self.bot.use(sid=self.sid) self.bot.use(sid=self.sid)
except ts3.query.TS3QueryError:
self.pipeOut("Invalid login credentials, please check your '.toml' file.", lvl="CRITICAL")
try: try:
self.bot.clientupdate(client_nickname=self.nickname) self.bot.clientupdate(client_nickname=self.nickname)
except ts3.query.TS3QueryError: except ts3.query.TS3QueryError as e:
self.pipeOut(e, lvl="WARNING")
pass pass
self.pipeOut(f"Successfully connected as: {self.nickname}") self.pipeOut(f"Successfully connected as: {self.nickname}")
# Start the Bot # Start the Bot loop
self.loop() self.loop()
def loop(self): def loop(self):
@@ -68,7 +78,7 @@ class TSbot:
The loop can be stopped setting self.running to False 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) me = self.bot.clientfind(pattern=self.nickname)
if len(me) == 1: if len(me) == 1:
self.myid = me[0]["clid"] self.myid = me[0]["clid"]
@@ -76,7 +86,7 @@ class TSbot:
self.pipeOut("Can't find my own client id. terminating...", lvl="critical") self.pipeOut("Can't find my own client id. terminating...", lvl="critical")
exit(1) 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) # self.bot.clientmove(clid=self.myid,cid=129)
# ----------- SUBSCRIBE TO EVENTS ------------- # ----------- SUBSCRIBE TO EVENTS -------------
@@ -99,20 +109,26 @@ class TSbot:
time.sleep(5) time.sleep(5)
# Notify connected admins # Notify connected admins
self.notifyAdmin() commands.notifyAdmin(self)
# ----------- LOOP HERE ------------- # ----------- LOOP HERE -------------
while self.running: while self.running:
# self.bot.send_keepalive() # self.bot.send_keepalive()
self.pipeOut(f"Waiting for a new Event...") self.pipeOut(f"Waiting for a new Event...", lvl="debug")
self.bot.version() self.bot.version()
# Auto-update crypto price channels every 30 minutes # Auto-update crypto price channels every 30 minutes
if self.last_crypto_update + 1800 < time.time(): if self.crypto:
self.last_crypto_update = time.time() if self.crypto_update + 1800 < time.time():
self.crypto_update = time.time()
self.lookupcommand(".btc", self.myid) commands.ticker(self, "")
self.lookupcommand(".dot", self.myid)
# Hijacked for WoW
commands.rwf(self)
else:
pass
try: try:
# This method blocks, but we must sent the keepalive message at # This method blocks, but we must sent the keepalive message at
@@ -122,6 +138,8 @@ class TSbot:
except ts3.query.TS3TimeoutError: except ts3.query.TS3TimeoutError:
pass pass
# try else!
else: else:
event_type = event.event event_type = event.event
self.pipeOut(f"Got Event | length={len(event[0])} | {event_type}") self.pipeOut(f"Got Event | length={len(event[0])} | {event_type}")
@@ -131,17 +149,22 @@ class TSbot:
if 'client_nickname' in event[0]: if 'client_nickname' in event[0]:
displayname = event[0]['client_nickname'] displayname = event[0]['client_nickname']
else: elif 'clid' in event[0]:
displayname = event[0]['clid'] displayname = event[0]['clid']
else:
self.pipeOut(event[0])
# displayname = "Unresolved"
continue # can't resolve no clid
self.pipeOut(f"Client [{displayname}] connected.") self.pipeOut(f"Client [{displayname}] connected.")
# Check if the connector is a ServerQuery or not # Check if the connector is a ServerQuery or not
if not self.isqueryclient(event[0]["client_unique_identifier"]): if not self.isqueryclient(event[0]["clid"]):
self.pipeOut(f"* {event[0]}", lvl="debug") self.pipeOut(f"* {event[0]}", lvl="debug")
# Check if the connector is an Admin # Check if the connector is an Admin
if self.isadmin(event[0]["client_database_id"]): 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: else:
pass pass
else: else:
@@ -159,17 +182,30 @@ class TSbot:
# New text message # New text message
elif event_type == "notifytextmessage": elif event_type == "notifytextmessage":
msg = event[0]["msg"] msg = event[0]["msg"].replace("\n", " ")
invkr = event[0]["invokername"] invkr_name = event[0]["invokername"]
invkr_id = event[0]["invokerid"] invkr_id = event[0]["invokerid"]
self.pipeOut(f'Message | From: "{invkr}" | Content: "{msg}"') 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("."): if msg.startswith("."):
self.lookupcommand(msg, invkr_id) self.lookupcommand(msg, invoker, permission_level)
# Unknown Event # Unknown Event
else: else:
self.pipeOut(f"Unknown Event: {event.__dict__}", lvl="warning") self.pipeOut(f"Unknown Event: {event.__dict__}", lvl="warning")
# Class Utility --------------------------------------------------------------------------------------------------------
def pipeOut(self, msg, lvl="INFO", reprint=True): def pipeOut(self, msg, lvl="INFO", reprint=True):
""" """
All output pipes through this function. All output pipes through this function.
@@ -177,38 +213,30 @@ class TSbot:
or run in silent log mode. [set reprint to False] or run in silent log mode. [set reprint to False]
""" """
lvln = int(getattr(util.logging, lvl.upper())) lvl = lvl.upper()
lvln = int(getattr(util.logging, lvl))
self.log.log(lvln, msg) self.log.log(lvln, msg)
if reprint: if reprint and lvln >= self.log.getEffectiveLevel():
print(f"[{time.strftime('%H:%M:%S')}][{lvl.upper()}] {msg}") print(f"[{time.strftime('%H:%M:%S')}]{f'[{lvl}]':10s} {msg}")
def stop(self, invkr_id): if not self.whitelisted:
if msg == "error id 524: client is flooding":
time.sleep(30)
if lvl == "CRITICAL":
exit(1)
def stop(self, invoker):
""" """
This stops the bot instance by invalidating the event loop. This stops the bot instance by invalidating the event loop.
""" """
msg = "Shutdown, bye bye!" msg = "Shutdown, bye bye!"
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=msg) self.bot.sendtextmessage(targetmode=1, target=invoker['id'], msg=msg)
self.pipeOut(msg) self.pipeOut(msg)
self.running = False self.running = False
def notifyAdmin(self):
"""
Ping 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):
self.bot.sendtextmessage(targetmode=1, target=clid, msg=self.intro)
if not self.whitelisted:
time.sleep(1)
def kickall(self, msg): def kickall(self, msg):
""" """
TODO TODO
@@ -217,13 +245,10 @@ class TSbot:
clients = self.bot.clientlist() clients = self.bot.clientlist()
clients = [client["clid"] for client in clients if client["client_type"] != "1"] clients = [client["clid"] for client in clients if client["client_type"] != "1"]
for clid in clients: for clid in clients:
try:
self.bot.clientpoke(msg=msg, clid=clid) self.bot.clientpoke(msg=msg, clid=clid)
# TODO # TODO
except:
pass
def poke(self, msg=None, n=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. ping a single or multiple users for n times including a msg.
""" """
@@ -236,15 +261,20 @@ class TSbot:
clients = self.bot.clientlist() clients = self.bot.clientlist()
clients = [client["clid"] for client in clients if client["client_type"] != "1"] clients = [client["clid"] for client in clients if client["client_type"] != "1"]
else: else:
try:
clients = self.bot.clientfind(pattern=usr) clients = self.bot.clientfind(pattern=usr)
clients = [client["clid"] for client in clients] 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
# Ping them # Ping them
if len(clients) > 0: if len(clients) > 0:
for clid in clients: for clid in clients:
data = self.printable_clientinfo(clid) data = self.printable_clientinfo(clid)
self.pipeOut(f"{data}", lvl="debug") self.pipeOut(f"{data}", lvl="DEBUG")
i = 0 i = 0
while n == -1 or i < n: while n == -1 or i < n:
@@ -252,7 +282,7 @@ class TSbot:
try: try:
self.bot.clientpoke(msg=msg, clid=clid) self.bot.clientpoke(msg=msg, clid=clid)
except Exception as e: except Exception as e:
self.pipeOut(e, lvl="warning") self.pipeOut(e, lvl="WARNING")
pass pass
time.sleep(delay) time.sleep(delay)
i += 1 i += 1
@@ -288,202 +318,197 @@ class TSbot:
except Exception as e: except Exception as e:
self.pipeOut(e, lvl="error") self.pipeOut(e, lvl="error")
def list(self, what, invkr_id): def isqueryclient(self, clid):
"""
Message the invoker of the function either a list of channels or a list of clients.
"""
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)
else:
msg = None
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=msg)
def isqueryclient(self, cluid):
""" """
Check if the given client-uid is a query client. Check if the given client-uid is a query client.
""" """
try:
# client = self.bot.clientlist(uid=uid) client = self.bot.clientinfo(clid=clid)
# print(client[0]) except ts3.query.TS3QueryError as e:
# if client[0]["client_type"] == "1": self.pipeOut(f"ISQUERY: given clid {clid} returned: {e}", lvl="WARNING")
if cluid == "ServerQuery": return True
self.pipeOut(f"[{cluid}] ISQUERY: True") try:
if client[0]["client_type"] == "1":
self.pipeOut(f"[{clid}] ISQUERY: True")
return True return True
else: else:
self.pipeOut(f"[{cluid}] ISQUERY: False") self.pipeOut(f"[{clid}] ISQUERY: False")
return 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): 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.
""" """
try:
groups = self.bot.servergroupsbyclientid(cldbid=cldbid) groups = self.bot.servergroupsbyclientid(cldbid=cldbid)
# [print(group["sgid"]) for group in groups] except ts3.query.TS3QueryError as e:
self.pipeOut(e, lvl="ERROR")
return False
self.pipeOut(str(groups.__dict__), lvl="DEBUG") # DEBUG
for group in groups: for group in groups:
# 6 Server Admin/ 13 Operator / 15 Root if any(sgid == group['sgid'] for sgid in self.sgid_admin):
# if (group["sgid"] == "6") or (group["sgid"] == "13") or (group["sgid"] == "15"):
if group["sgid"] == "15":
self.pipeOut(f"[{cldbid}] ISADMIN: True") self.pipeOut(f"[{cldbid}] ISADMIN: True")
return True return True
else:
continue
self.pipeOut(f"[{cldbid}] ISADMIN: False") self.pipeOut(f"[{cldbid}] ISADMIN: False")
return False return False
def lookupcommand(self, msg, invkr_id):
"""
Every message starting with '.' gets passed and evaluated in this function.
Command Parameter 1 Parameter 2
---------------------------------------------------------
.annoy target message
.kickall message
.test
.btc / .eth
.dot / .ada
.info clid
.list channel/clients
.follow
.rename nickname
.stop / .quit / .q
.pingall message
.admin
"""
commandstring = msg.split(" ")
command = commandstring[0]
parameter = commandstring[1:]
self.pipeOut(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" or command == ".eth":
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
elif command == ".dot" or command == ".ada":
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
elif command == ".info":
pass # TODO
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":
try:
self.nickname = parameter[0]
self.bot.clientupdate(client_nickname=self.nickname)
except IndexError:
err = "Please use the command like this: .rename NAME"
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=err)
pass
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
elif command == ".admin":
self.notifyAdmin()
else:
err = f"Unknown Command: [{command}]"
self.bot.sendtextmessage(targetmode=1, target=invkr_id, msg=err)
def printable_clientinfo(self, clid): def printable_clientinfo(self, clid):
""" """
Generate printable clientinfo from clid. Generate printable clientinfo from clid.
""" """
usrd = {}
info = self.bot.clientinfo(clid=clid) info = self.bot.clientinfo(clid=clid)
usrd = info.__dict__ usrd = info.__dict__
return usrd return usrd
def checkgrp(self): def clid2cldbid(self, clid):
""" """
Print all assigned groups for every connected client. Convert a given clid to a cldbid.
""" """
# clients = self.bot.clientlist() cluid = self.bot.clientgetuidfromclid(clid=clid)[0]["cluid"]
# clients_cldbid = [client["client_database_id"] for client in clients if client["client_type"] != "1"] cldbid = self.bot.clientgetdbidfromuid(cluid=cluid)[0]["cldbid"]
clients = self.bot.clientlist(groups=True)
clients_groups = [client["client_servergroups"] for client in clients if client["client_type"] != "1"]
self.pipeOut(f"{clients_groups}") return cldbid
def identifytarget(self, target):
"""
Try to find clid from nickname.
"""
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
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(): def main():
# Start logger # Start logger
log = util.setupLogger() logpath = os.path.dirname(os.path.abspath(__file__)) + r"/log"
log = util.setupLogger(logpath)
# Log unhandled exception # Log unhandled exception
sys.excepthook = util.unhandledException sys.excepthook = util.unhandledException
# Load toml config # Load toml config
conf = util.getconf("prod.toml") conf = util.getConf("prod.toml")
# Change loglevel from config
util.changeLevel(log, conf['Log']['level'])
# Start the Bot Instance # Start the Bot Instance
TSbot(conf, log) TSbot(conf, log)
+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()
+8 -2
View File
@@ -3,12 +3,13 @@ TBD
""" """
__author__ = "Lukas Mahler" __author__ = "Lukas Mahler"
__version__ = "0.0.0" __version__ = "0.0.1"
__date__ = "27.09.2021" __date__ = "04.02.2022"
__email__ = "m@hler.eu" __email__ = "m@hler.eu"
__status__ = "Development" __status__ = "Development"
# Imports # Imports
from datetime import datetime
from pycoingecko import CoinGeckoAPI from pycoingecko import CoinGeckoAPI
@@ -44,7 +45,12 @@ class GeckoAPI:
matching = [x for x in self.coinlist if x["id"] == symbol_id.lower()] matching = [x for x in self.coinlist if x["id"] == symbol_id.lower()]
if len(matching) == 1: if len(matching) == 1:
try:
resp_dict = self.api.get_price(ids=matching[0]["id"], vs_currencies=curr.lower()) 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()] price = resp_dict[matching[0]["id"]][curr.lower()]
if isinstance(price, int): if isinstance(price, int):
price = float(price) # Convert to always have floats price = float(price) # Convert to always have floats
+9 -1
View File
@@ -1,5 +1,5 @@
[Connection] [Connection]
host = "example.com" host = "127.0.0.1"
port = 10011 port = 10011
sid = 1 sid = 1
@@ -7,6 +7,14 @@ sid = 1
user = "exampleuser" user = "exampleuser"
pwd = "password" pwd = "password"
[Groups]
admins = ["1"]
mute = "0"
[Misc] [Misc]
nickname = "myTS3-Bot" nickname = "myTS3-Bot"
whitelisted = false whitelisted = false
crypto = false
[Log]
level = "INFO"
+43 -15
View File
@@ -3,8 +3,8 @@ TBD
""" """
__author__ = "Lukas Mahler" __author__ = "Lukas Mahler"
__version__ = "0.0.0" __version__ = "1.0.0"
__date__ = "27.09.2021" __date__ = "14.04.2022"
__email__ = "m@hler.eu" __email__ = "m@hler.eu"
__status__ = "Development" __status__ = "Development"
@@ -14,7 +14,9 @@ import sys
import shutil import shutil
import os.path import os.path
import logging import logging
from datetime import date import time
from datetime import timedelta
from logging.handlers import RotatingFileHandler
# Custom # Custom
import toml import toml
@@ -32,18 +34,18 @@ def unhandledException(exc_type, exc_value, exc_traceback):
log.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) log.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
def getconf(fname): def getConf(fname):
""" """
""" """
if fname.endswith(".toml"): if fname.endswith(".toml"):
if os.path.isfile(fname): if os.path.isfile(fname):
try: try:
data = toml.load(fname) config = toml.load(fname)
return data checkConf(config)
return config
except ValueError as e: except ValueError as e:
log.critical(e) log.critical(f"The provided '.toml' is probably invalid, returned error:\n{e}")
# log.critical("The provided '.toml' seems to be invalid.")
exit(1) exit(1)
else: else:
log.critical(f"Couldn't locate the '.toml' file [{fname}].") log.critical(f"Couldn't locate the '.toml' file [{fname}].")
@@ -51,27 +53,53 @@ def getconf(fname):
shutil.copy("src/template.toml", fname) shutil.copy("src/template.toml", fname)
exit(1) exit(1)
else: else:
log.critical("The provided config file is not a '.toml' file.") 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.") log.info("Creating a new '.toml' file from template, please edit and restart.")
shutil.copy("src/template.toml", "prod.toml") shutil.copy("src/template.toml", "prod.toml")
exit(1) exit(1)
def setupLogger(lvl="DEBUG"): 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 global log # Needed to log exceptions in src\util
log = logging.getLogger() log = logging.getLogger("mylog")
now = date.today().strftime("%Y-%m-%d") if not os.path.exists(logpath):
handler = logging.FileHandler(f"myTS3_{now}.log", encoding="utf-8") os.makedirs(logpath)
logformat = logging.Formatter("%(asctime)s %(levelname)7s %(message)s", "%d-%m-%Y %H:%M:%S") 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) handler.setFormatter(logformat)
log.addHandler(handler) log.addHandler(handler)
log.setLevel(lvl) log.setLevel(logging.getLevelName(lvl))
return log return log
def changeLevel(log, lvl):
"""
Change the loglevel after creation
"""
log.setLevel(logging.getLevelName(lvl))
if __name__ == "__main__": if __name__ == "__main__":
exit() exit()