modular-bot-framework-for-t.../main.py

261 lines
8.9 KiB
Python
Raw Normal View History

2022-10-23 17:20:32 +03:00
from telegram.ext import Updater, MessageHandler, Filters
import datetime
2022-10-23 17:20:32 +03:00
import codecs
import time
import json
import sys
import os
import threading
2023-05-03 21:06:05 +03:00
import importlib
2022-10-23 17:20:32 +03:00
# global variables
STOP_REQUESTED = False
2023-05-08 12:45:02 +03:00
2022-10-23 17:20:32 +03:00
# some functions that increase readability of the code
def readfile(filename):
try:
2023-05-08 12:45:02 +03:00
return codecs.open(filename, encoding="utf-8").read()
2022-10-23 17:20:32 +03:00
except FileNotFoundError:
return False
except Exception as e:
2023-05-08 12:45:02 +03:00
print(f"[ERROR] Unexpected error occurred in readfile() ({e})")
2022-10-23 17:20:32 +03:00
return False
2023-05-08 12:45:02 +03:00
2022-10-23 17:20:32 +03:00
# module object classes
class ModuleV1:
def __init__(self, path, code, enabled, alias, predefine):
2022-10-23 17:20:32 +03:00
self.version = 1
self.enabled = enabled
self.code = code
self.alias = alias
self.path = path
self.predefine = predefine
if self.predefine:
self.set_predefine()
2022-10-23 17:20:32 +03:00
# set environmental variables
def set_env(self):
self.RESPONSE = ""
2022-10-23 17:20:32 +03:00
def set_predefine(self):
try:
exec(self.predefine)
except Exception as e:
2023-05-08 12:45:02 +03:00
print(f"[ERROR] module v1: module \"{self.path}\" ({self.alias}) raised exception \"{e}\" "
f"during predefine stage, disabling it...")
2022-10-23 17:20:32 +03:00
# running the module
def process(self, msg):
self.set_env()
self.MESSAGE = msg
try:
exec(self.code)
return self.RESPONSE
2022-10-23 17:20:32 +03:00
except Exception as e:
2023-05-08 12:45:02 +03:00
print(f"[ERROR] module v1: module \"{self.path}\" ({self.alias}) raised exception \"{e}\"")
return ""
2022-10-23 17:20:32 +03:00
2023-05-08 12:45:02 +03:00
2023-05-03 21:06:05 +03:00
class ModuleV2:
def __init__(self, path, index_file, enabled, alias):
self.version = 2
self.enabled = enabled
self.alias = alias
self.path = path
self.index_file = index_file[:-3]
self.obj = importlib.import_module((path + self.index_file).replace("/", "."))
# running the module
def process(self, msg):
try:
return self.obj.process(msg, self.path)
2023-05-03 21:06:05 +03:00
except Exception as e:
print(f"[ERROR] module v2: module \"{self.path}\" ({self.alias}) raised exception \"{e}\"")
return None
2022-10-23 17:20:32 +03:00
# module control unit
class ModuleControlUnit:
def __init__(self):
self.modules = []
self.reload_modules()
print("[INFO] ModuleControlUnit: initialized successfully")
def reload_modules(self):
for folder in os.listdir("modules/"):
try:
meta_raw = readfile("modules/{}/meta.json".format(folder))
if not meta_raw:
2023-05-08 12:45:02 +03:00
print(f"[WARN] module_loader: no meta.json found in module folder \"{folder}\"")
2022-10-23 17:20:32 +03:00
continue
meta = json.loads( meta_raw )
if "version" in meta:
if meta["version"] == 1:
if "index_file" in meta:
index_file = meta["index_file"]
else:
index_file = "index.py"
code = readfile( "modules/{}/{}".format(folder, index_file) )
if not code: # False both when readfile() returns False and when the code string is empty
print("[WARN] reload_modules: module {} does not have any code, skipping...".format(folder))
continue
2023-05-08 12:45:02 +03:00
2022-10-23 17:20:32 +03:00
if "start_on_boot" in meta:
enabled = meta["start_on_boot"]
else:
enabled = False
if "alias" in meta:
alias = meta["alias"]
else:
alias = None
if "predefine" in meta:
predefine = readfile("modules/{}/{}".format(folder, meta["predefine"]))
else:
predefine = False
2023-05-08 12:45:02 +03:00
self.modules.append(ModuleV1(f"modules/{folder}/", code, enabled, alias, predefine))
2022-10-23 17:20:32 +03:00
2023-05-08 12:45:02 +03:00
print(f"[INFO] reload_modules: successfully loaded {folder} as {alias} "
f"(start_on_boot: {enabled})")
2022-10-23 17:20:32 +03:00
2023-05-03 21:06:05 +03:00
elif meta["version"] == 2:
if "index_file" in meta:
index_file = meta["index_file"]
else:
index_file = "index.py"
2022-10-23 17:20:32 +03:00
2023-05-03 21:06:05 +03:00
if "start_on_boot" in meta:
enabled = meta["start_on_boot"]
else:
enabled = False
if "alias" in meta:
alias = meta["alias"]
else:
alias = None
2022-10-23 17:20:32 +03:00
2023-05-03 21:06:05 +03:00
self.modules.append(ModuleV2(f"modules/{folder}/", index_file, enabled, alias))
2022-10-23 17:20:32 +03:00
2023-05-08 12:45:02 +03:00
print(f"[INFO] reload_modules: successfully loaded {folder} as {alias} "
f"(start_on_boot: {enabled})")
2023-05-03 21:06:05 +03:00
else:
2023-05-08 12:45:02 +03:00
print(f"[WARN] reload_modules: module {folder} requires unsupported version "
f"({meta['version']} > 2), skipping...")
2023-05-03 21:06:05 +03:00
except Exception as e:
2023-05-08 12:45:02 +03:00
print(f"[ERROR] module_loader: error while loading module \"{folder}\" ({e})")
2022-10-23 17:20:32 +03:00
# synchronous message processor
def queue_processor():
while True:
try:
if len(message_queue) > 0:
msg = message_queue[0]
print("[DEBUG] queue_processor: {}".format(msg)) # debug
2023-05-08 12:45:02 +03:00
# check for control commands
if msg["chat"]["id"] == 575246355:
if msg["text"][0] == "$":
command = msg["text"][1:].split(" ")
2023-05-08 12:45:02 +03:00
if len(command) >= 2 and command[0] == "module":
if command[1] == "reload":
print("[INFO] Module reloading triggered by a command")
2023-05-03 21:44:46 +03:00
# properly reload all v2 modules
for mod in mcu.modules:
if mod.version == 2:
importlib.reload(mod.obj)
2023-05-03 21:44:46 +03:00
del mcu.modules[:]
mcu.reload_modules()
2023-05-08 12:45:02 +03:00
del message_queue[0]
continue
2023-05-08 12:45:02 +03:00
# modules are used in here
for mod in mcu.modules:
if mod.enabled:
2023-05-03 21:06:05 +03:00
if mod.version == 1 or mod.version == 2:
response = mod.process(msg)
2023-09-04 21:33:23 +03:00
if response:
2023-09-04 21:33:23 +03:00
# protecting output
symbols_to_escape = ['[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
for symbol in symbols_to_escape:
response = response.replace(symbol, f"\\{symbol}")
2023-09-04 21:33:23 +03:00
updater.bot.send_message(chat_id=msg.chat.id, text=response,
2023-09-04 21:33:23 +03:00
disable_web_page_preview=True,
parse_mode="MarkdownV2")
print(f"Responded using module {mod.path} ({mod.alias}) with text: {response}")
break
2023-05-08 12:45:02 +03:00
del message_queue[0]
time.sleep(0.1)
2022-10-23 17:20:32 +03:00
else:
if STOP_REQUESTED:
break
else:
time.sleep(1)
2023-09-04 21:33:23 +03:00
except Exception as e:
2023-05-08 12:45:02 +03:00
print(f"[ERROR] queue_processor: current message queue: {message_queue}")
2023-09-04 21:33:23 +03:00
print(f"[ERROR] queue_processor: error while processing message ({e}), trying to delete it...")
try:
del message_queue[0]
print("[INFO] queue_processor: deleted broken message from the queue")
except:
print("[WARN] queue_processor: message seems absent, whatever")
2022-10-23 17:20:32 +03:00
print("[INFO] queue_processor thread stops successfully")
# telegram bot processor
def message_handler(update, context):
2023-05-08 12:45:02 +03:00
print("[DEBUG] Received new message") # just for testing
2022-10-23 17:20:32 +03:00
message_queue.append(update.message)
# --- Final stage ---
# initializing services and queues
message_queue = []
mcu = ModuleControlUnit()
2023-05-08 12:45:02 +03:00
processor_thread = threading.Thread(target=queue_processor, args=[])
2022-10-23 17:20:32 +03:00
processor_thread.start()
# connecting to Telegram servers and listening for messages
TOKEN = readfile("config/token")
if not TOKEN:
print("[CRIT] Token has not been defined, quitting")
sys.exit(1)
# connect to Telegram servers
2023-05-08 12:45:02 +03:00
updater = Updater(TOKEN, use_context=True)
2022-10-23 17:20:32 +03:00
dispatcher = updater.dispatcher
# assign the handler for messages
dispatcher.add_handler(MessageHandler(Filters.text, message_handler))
# run the bot
updater.start_polling()
updater.idle()