mirror of
https://gitlab.com/octospacc/WinDog.git
synced 2025-02-08 15:48:43 +01:00
287 lines
9.5 KiB
Python
Executable File
287 lines
9.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# ==================================== #
|
|
# WinDog multi-purpose chatbot #
|
|
# Licensed under AGPLv3 by OctoSpacc #
|
|
# ==================================== #
|
|
|
|
import json, time
|
|
from binascii import hexlify
|
|
from glob import glob
|
|
from magic import Magic
|
|
from os import listdir
|
|
from os.path import isfile, isdir
|
|
from random import choice, randint
|
|
from types import SimpleNamespace
|
|
from traceback import format_exc
|
|
from bs4 import BeautifulSoup
|
|
from html import unescape as HtmlUnescape
|
|
from markdown import markdown
|
|
from LibWinDog.Config import *
|
|
from LibWinDog.Database import *
|
|
|
|
# <https://daringfireball.net/projects/markdown/syntax#backslash>
|
|
MdEscapes = '\\`*_{}[]()<>#+-.!|='
|
|
|
|
def Log(text:str, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None:
|
|
endline = '\n'
|
|
if newline == False or (inline and newline == None):
|
|
endline = ''
|
|
text = (text if inline else f"[{level}] [{time.ctime()}] [{int(time.time())}] {text}")
|
|
if LogToConsole:
|
|
print(text, end=endline)
|
|
if LogToFile:
|
|
open("./Log.txt", 'a').write(text + endline)
|
|
|
|
def SetupLocales() -> None:
|
|
global Locale
|
|
for file in listdir('./Locale'):
|
|
lang = file.split('.')[0]
|
|
try:
|
|
with open(f'./Locale/{file}') as file:
|
|
Locale[lang] = json.load(file)
|
|
except Exception:
|
|
Log(f'Cannot load {lang} locale, exiting.')
|
|
raise
|
|
exit(1)
|
|
for key in Locale[DefaultLang]:
|
|
Locale['Fallback'][key] = Locale[DefaultLang][key]
|
|
for lang in Locale:
|
|
for key in Locale[lang]:
|
|
if not key in Locale['Fallback']:
|
|
Locale['Fallback'][key] = Locale[lang][key]
|
|
def querier(query:str, lang:str=DefaultLang):
|
|
value = None
|
|
query = query.split('.')
|
|
try:
|
|
value = Locale.Locale[lang]
|
|
for key in query:
|
|
value = value[key]
|
|
except Exception:
|
|
value = Locale.Locale['Fallback']
|
|
for key in query:
|
|
value = value[key]
|
|
return value
|
|
Locale['__'] = querier
|
|
Locale['Locale'] = Locale
|
|
Locale = SimpleNamespace(**Locale)
|
|
|
|
def SureArray(array:any) -> list|tuple:
|
|
return (array if type(array) in [list, tuple] else [array])
|
|
|
|
def InDict(dikt:dict, key:str, /) -> any:
|
|
return (dikt[key] if key in dikt else None)
|
|
|
|
def DictGet(dikt:dict, key:str, /) -> any:
|
|
return (dikt[key] if key in dikt else None)
|
|
|
|
def isinstanceSafe(clazz:any, instance:any) -> bool:
|
|
if instance != None:
|
|
return isinstance(clazz, instance)
|
|
return False
|
|
|
|
def CharEscape(String:str, Escape:str='') -> str:
|
|
if Escape == 'MARKDOWN':
|
|
return escape_markdown(String, version=2)
|
|
else:
|
|
if Escape == 'MARKDOWN_SPEECH':
|
|
Escape = '+-_.!()[]{}<>'
|
|
elif Escape == 'MARKDOWN_SPEECH_FORMAT':
|
|
Escape = '+-_.!()[]<>'
|
|
for c in Escape:
|
|
String = String.replace(c, '\\'+c)
|
|
return String
|
|
|
|
def InferMdEscape(raw:str, plain:str) -> str:
|
|
chars = ''
|
|
for char in MdEscapes:
|
|
if char in raw and char in plain:
|
|
chars += char
|
|
return chars
|
|
|
|
def MarkdownCode(text:str, block:bool) -> str:
|
|
return ('```\n' + text.strip().replace('`', '\`') + '\n```')
|
|
|
|
def MdToTxt(md:str) -> str:
|
|
return BeautifulSoup(markdown(md), 'html.parser').get_text(' ')
|
|
|
|
def HtmlEscapeFull(Raw:str) -> str:
|
|
New = ''
|
|
Hex = hexlify(Raw.encode()).decode()
|
|
for i in range(0, len(Hex), 2):
|
|
New += f'&#x{Hex[i] + Hex[i+1]};'
|
|
return New
|
|
|
|
def GetRawTokens(text:str) -> list:
|
|
return text.strip().replace('\t', ' ').replace(' ', ' ').replace(' ', ' ').split(' ')
|
|
|
|
def ParseCmd(msg) -> SimpleNamespace|None:
|
|
#if not len(msg) or msg[1] not in CmdPrefixes:
|
|
# return
|
|
name = msg.replace('\n', ' ').replace('\t', ' ').replace(' ', ' ').replace(' ', ' ').split(' ')[0][1:].split('@')[0]
|
|
#if not name:
|
|
# return
|
|
return SimpleNamespace(**{
|
|
"Name": name.lower(),
|
|
"Body": name.join(msg.split(name)[1:]).strip(),
|
|
"Tokens": GetRawTokens(msg),
|
|
"User": None,
|
|
"Quoted": None,
|
|
})
|
|
|
|
def GetWeightedText(*texts) -> str|None:
|
|
for text in texts:
|
|
if text:
|
|
return text
|
|
|
|
def RandPercent() -> int:
|
|
num = randint(0,100)
|
|
return (f'{num}.00' if num == 100 else f'{num}.{randint(0,9)}{randint(0,9)}')
|
|
|
|
def RandHexStr(length:int) -> str:
|
|
hexa = ''
|
|
for char in range(length):
|
|
hexa += choice('0123456789abcdef')
|
|
return hexa
|
|
|
|
def ParseCommand(text:str) -> SimpleNamespace|None:
|
|
text = text.strip()
|
|
try: # ensure command is not empty
|
|
if not (text[0] in CmdPrefixes and text[1:].strip()):
|
|
return
|
|
except IndexError:
|
|
return
|
|
command = SimpleNamespace(**{})
|
|
command.tokens = text.replace("\r", " ").replace("\n", " ").replace("\t", " ").replace(" ", " ").replace(" ", " ").split(" ")
|
|
command.name = command.tokens[0][1:].lower()
|
|
command.body = text[len(command.tokens[0]):].strip()
|
|
if (endpoint_arguments := Endpoints[command.name]["arguments"]):
|
|
command.arguments = {}
|
|
# TODO differences between required (True) and optional (False) args
|
|
for index, key in enumerate(endpoint_arguments):
|
|
try:
|
|
value = command.tokens[index + 1]
|
|
command.body = command.body[len(value):].strip()
|
|
except IndexError:
|
|
value = None
|
|
command.arguments[key] = value
|
|
return command
|
|
|
|
def OnMessageParsed(data:SimpleNamespace) -> None:
|
|
if Debug and (DumpToFile or DumpToConsole):
|
|
text = (data.text_auto.replace('\n', '\\n') if data.text_auto else '')
|
|
text = f"[{int(time.time())}] [{time.ctime()}] [{data.room_id}] [{data.message_id}] [{data.user.id}] {text}"
|
|
if DumpToConsole:
|
|
print(text)
|
|
if DumpToFile:
|
|
open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Dump.txt"), 'a').write(text + '\n')
|
|
|
|
def SendMsg(context, data, destination=None) -> None:
|
|
return SendMessage(context, data, destination)
|
|
|
|
def SendMessage(context, data, destination=None) -> None:
|
|
if type(context) == dict:
|
|
event = context['Event'] if 'Event' in context else None
|
|
manager = context['Manager'] if 'Manager' in context else None
|
|
else:
|
|
[event, manager] = [context, context]
|
|
if InDict(data, 'TextPlain') or InDict(data, 'TextMarkdown'):
|
|
textPlain = InDict(data, 'TextPlain')
|
|
textMarkdown = InDict(data, 'TextMarkdown')
|
|
if not textPlain:
|
|
textPlain = textMarkdown
|
|
elif InDict(data, 'Text'):
|
|
# our old system attempts to always receive Markdown and retransform when needed
|
|
textPlain = MdToTxt(data['Text'])
|
|
textMarkdown = CharEscape(HtmlUnescape(data['Text']), InferMdEscape(HtmlUnescape(data['Text']), textPlain))
|
|
for platform in Platforms:
|
|
platform = Platforms[platform]
|
|
if isinstanceSafe(event, InDict(platform, "eventClass")) or isinstanceSafe(manager, InDict(platform, "managerClass")):
|
|
platform["sender"](event, manager, data, destination, textPlain, textMarkdown)
|
|
|
|
def SendReaction() -> None:
|
|
pass
|
|
|
|
def RegisterPlatform(name:str, main:callable, sender:callable, *, eventClass=None, managerClass=None) -> None:
|
|
Platforms[name] = {"main": main, "sender": sender, "eventClass": eventClass, "managerClass": managerClass}
|
|
Log(f"{name}, ", inline=True)
|
|
|
|
def RegisterModule(name:str, endpoints:dict, *, group:str|None=None, summary:str|None=None) -> None:
|
|
Modules[name] = {"group": group, "summary": summary, "endpoints": endpoints}
|
|
Log(f"{name}, ", inline=True)
|
|
for endpoint in endpoints:
|
|
endpoint = endpoints[endpoint]
|
|
for name in endpoint["names"]:
|
|
Endpoints[name] = endpoint
|
|
|
|
# TODO register endpoint with this instead of RegisterModule
|
|
def CreateEndpoint(names:list[str], handler:callable, arguments:dict[str, bool]|None=None, *, summary:str|None=None) -> dict:
|
|
return {"names": names, "summary": summary, "handler": handler, "arguments": arguments}
|
|
|
|
def Main() -> None:
|
|
#SetupDb()
|
|
SetupLocales()
|
|
Log(f"📨️ Initializing Platforms... ", newline=False)
|
|
for platform in Platforms:
|
|
if Platforms[platform]["main"]():
|
|
Log(f"{platform}, ", inline=True)
|
|
Log("...Done. ✅️", inline=True, newline=True)
|
|
Log("🐶️ WinDog Ready!")
|
|
while True:
|
|
time.sleep(9**9)
|
|
|
|
if __name__ == '__main__':
|
|
Log("🌞️ WinDog Starting...")
|
|
#Db = {"Rooms": {}, "Users": {}}
|
|
Locale = {"Fallback": {}}
|
|
Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {}
|
|
|
|
for folder in ("LibWinDog/Platforms", "ModWinDog"):
|
|
match folder:
|
|
case "LibWinDog/Platforms":
|
|
Log("📩️ Loading Platforms... ", newline=False)
|
|
case "ModWinDog":
|
|
Log("🔩️ Loading Modules... ", newline=False)
|
|
for name in listdir(f"./{folder}"):
|
|
path = f"./{folder}/{name}"
|
|
if isfile(path):
|
|
exec(open(path, 'r').read())
|
|
elif isdir(path):
|
|
files = listdir(path)
|
|
if f"{name}.py" in files:
|
|
files.remove(f"{name}.py")
|
|
exec(open(f"{path}/{name}.py", 'r').read())
|
|
for file in files:
|
|
if file.endswith(".py"):
|
|
exec(open(f"{path}/{name}.py", 'r').read())
|
|
# TODO load locales
|
|
#for name in listdir(path):
|
|
# if name.lower().endswith('.json'):
|
|
#
|
|
Log("...Done. ✅️", inline=True, newline=True)
|
|
|
|
Log("💽️ Loading Configuration... ", newline=False)
|
|
#exec(open("./LibWinDog/Config.py", 'r').read())
|
|
from Config import *
|
|
if isfile("./Config.py"):
|
|
from Config import *
|
|
else:
|
|
Log("💾️ No configuration found! Generating and writing to `./Config.py`... ", inline=True)
|
|
with open("./Config.py", 'w') as configFile:
|
|
opening = '# windog config start #'
|
|
closing = '# end windog config #'
|
|
for folder in ("LibWinDog", "ModWinDog"):
|
|
for file in glob(f"./{folder}/**/*.py", recursive=True):
|
|
try:
|
|
name = '/'.join(file.split('/')[1:-1])
|
|
heading = f"# ==={'=' * len(name)}=== #"
|
|
source = open(file, 'r').read().replace(f"''' {opening}", f'""" {opening}').replace(f"{closing} '''", f'{closing} """')
|
|
content = '\n'.join(content.split(f'""" {opening}')[1].split(f'{closing} """')[0].split('\n')[1:-1])
|
|
configFile.write(f"{heading}\n# 🔽️ {name} 🔽️ #\n{heading}\n{content}\n\n")
|
|
except IndexError:
|
|
pass
|
|
Log("Done. ✅️", inline=True, newline=True)
|
|
|
|
Main()
|
|
Log("🌚️ WinDog Stopping...")
|
|
|