mirror of
https://gitlab.com/octospacc/WinDog.git
synced 2025-02-12 09:30:37 +01:00
More API refactoring, add HTML text for Telegram, start work on /config
This commit is contained in:
parent
8f1b80ab14
commit
2c73846554
@ -1,4 +1,5 @@
|
||||
from peewee import *
|
||||
from LibWinDog.Types import *
|
||||
|
||||
Db = SqliteDatabase("Database.sqlite")
|
||||
|
||||
@ -6,11 +7,13 @@ class BaseModel(Model):
|
||||
class Meta:
|
||||
database = Db
|
||||
|
||||
class EntitySettings(BaseModel):
|
||||
language = CharField(null=True)
|
||||
|
||||
class Entity(BaseModel):
|
||||
id = CharField(null=True)
|
||||
id_hash = CharField()
|
||||
#settings = ForeignKeyField(EntitySettings, backref="entity")
|
||||
#language = CharField(null=True)
|
||||
settings = ForeignKeyField(EntitySettings, backref="entity", null=True)
|
||||
|
||||
class User(Entity):
|
||||
pass
|
||||
@ -18,5 +21,5 @@ class User(Entity):
|
||||
class Room(Entity):
|
||||
pass
|
||||
|
||||
Db.create_tables([User, Room], safe=True)
|
||||
Db.create_tables([EntitySettings, User, Room], safe=True)
|
||||
|
||||
|
@ -39,15 +39,19 @@ def MastodonHandler(event):
|
||||
if command.Name in Endpoints:
|
||||
CallEndpoint(command.Name, EventContext(platform="mastodon", event=event, manager=Mastodon), command)
|
||||
|
||||
def MastodonSender(context:EventContext, data:OutputMessageData, destination, textPlain, textMarkdown) -> None:
|
||||
if InDict(data, 'Media'):
|
||||
Media = context.manager.media_post(data['Media'], Magic(mime=True).from_buffer(data['Media']))
|
||||
while Media['url'] == 'null':
|
||||
Media = context.manager.media(Media)
|
||||
if textPlain or Media:
|
||||
def MastodonSender(context:EventContext, data:OutputMessageData, destination) -> None:
|
||||
media_results = None
|
||||
if data.media:
|
||||
media_results = []
|
||||
for medium in data.media[:4]: # Mastodon limits posts to 4 attachments
|
||||
medium_result = context.manager.media_post(medium, Magic(mime=True).from_buffer(medium))
|
||||
while medium_result["url"] == "null":
|
||||
medium_result = context.manager.media(medium_result)
|
||||
media_results.append(medium_result)
|
||||
if data.text_plain or media_results:
|
||||
context.manager.status_post(
|
||||
status=(textPlain + '\n\n@' + context.event['account']['acct']),
|
||||
media_ids=(Media if InDict(data, 'Media') else None),
|
||||
status=(data.text_plain + '\n\n@' + context.event['account']['acct']),
|
||||
media_ids=media_results,
|
||||
in_reply_to_id=context.event['status']['id'],
|
||||
visibility=('direct' if context.event['status']['visibility'] == 'direct' else 'unlisted'),
|
||||
)
|
||||
|
@ -31,6 +31,8 @@ def TelegramMain() -> bool:
|
||||
return True
|
||||
|
||||
def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
|
||||
#if not message:
|
||||
# return None
|
||||
data = InputMessageData(
|
||||
message_id = f"telegram:{message.message_id}",
|
||||
text_plain = message.text,
|
||||
@ -48,8 +50,7 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
|
||||
tag = message.from_user.username,
|
||||
name = message.from_user.first_name,
|
||||
)
|
||||
#if (db_user := GetUserData(data.user.id)):
|
||||
# data.user.language = db_user.language
|
||||
data.user.settings = GetUserSettings(data.user.id)
|
||||
return data
|
||||
|
||||
def TelegramHandlerWrapper(update:telegram.Update, context:CallbackContext=None) -> None:
|
||||
@ -64,8 +65,11 @@ def TelegramHandlerCore(update:telegram.Update, context:CallbackContext=None) ->
|
||||
OnMessageParsed(data)
|
||||
cmd = ParseCmd(update.message.text)
|
||||
if cmd:
|
||||
# TODO remove old cmd and just pass the data object
|
||||
cmd.command = data.command
|
||||
cmd.quoted = data.quoted
|
||||
cmd.user = data.user
|
||||
cmd.message_id = data.message_id
|
||||
cmd.messageId = update.message.message_id
|
||||
cmd.TextPlain = cmd.Body
|
||||
cmd.TextMarkdown = update.message.text_markdown_v2
|
||||
@ -91,25 +95,25 @@ def TelegramHandlerCore(update:telegram.Update, context:CallbackContext=None) ->
|
||||
})
|
||||
CallEndpoint(cmd.Name, EventContext(platform="telegram", event=update, manager=context), cmd)
|
||||
|
||||
def TelegramSender(context:EventContext, data:OutputMessageData, destination, textPlain, textMarkdown):
|
||||
def TelegramSender(context:EventContext, data:OutputMessageData, destination):
|
||||
result = None
|
||||
if destination:
|
||||
result = context.manager.bot.send_message(destination, text=textPlain)
|
||||
result = context.manager.bot.send_message(destination, text=data.text_plain)
|
||||
else:
|
||||
replyToId = (data["ReplyTo"] if ("ReplyTo" in data and data["ReplyTo"]) else context.event.message.message_id)
|
||||
if InDict(data, "Media") and not InDict(data, "media"):
|
||||
data["media"] = {"bytes": data["Media"]}
|
||||
if InDict(data, "media"):
|
||||
for medium in SureArray(data["media"]):
|
||||
replyToId = (data.ReplyTo or context.event.message.message_id)
|
||||
if data.media:
|
||||
for medium in data.media:
|
||||
result = context.event.message.reply_photo(
|
||||
(DictGet(medium, "bytes") or DictGet(medium, "url")),
|
||||
caption=(textMarkdown if textMarkdown else textPlain if textPlain else None),
|
||||
parse_mode=("MarkdownV2" if textMarkdown else None),
|
||||
caption=(data.text_html or data.text_markdown or data.text_plain),
|
||||
parse_mode=("HTML" if data.text_html else "MarkdownV2" if data.text_markdown else None),
|
||||
reply_to_message_id=replyToId)
|
||||
elif textMarkdown:
|
||||
result = context.event.message.reply_markdown_v2(textMarkdown, reply_to_message_id=replyToId)
|
||||
elif textPlain:
|
||||
result = context.event.message.reply_text(textPlain, reply_to_message_id=replyToId)
|
||||
elif data.text_html:
|
||||
result = context.event.message.reply_html(data.text_html, reply_to_message_id=replyToId)
|
||||
elif data.text_markdown:
|
||||
result = context.event.message.reply_markdown_v2(data.text_markdown, reply_to_message_id=replyToId)
|
||||
elif data.text_plain:
|
||||
result = context.event.message.reply_text(data.text_plain, reply_to_message_id=replyToId)
|
||||
return TelegramMakeInputMessageData(result)
|
||||
|
||||
def TelegramLinker(data:InputMessageData) -> SafeNamespace:
|
||||
|
18
LibWinDog/Types.py
Normal file
18
LibWinDog/Types.py
Normal file
@ -0,0 +1,18 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
class SafeNamespace(SimpleNamespace):
|
||||
def __getattribute__(self, value):
|
||||
try:
|
||||
return super().__getattribute__(value)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
class EventContext(SafeNamespace):
|
||||
pass
|
||||
|
||||
class InputMessageData(SafeNamespace):
|
||||
pass
|
||||
|
||||
class OutputMessageData(SafeNamespace):
|
||||
pass
|
||||
|
@ -17,14 +17,16 @@ def cSource(context:EventContext, data:InputMessageData) -> None:
|
||||
def cGdpr(context:EventContext, data:InputMessageData) -> None:
|
||||
pass
|
||||
|
||||
# Module: Config
|
||||
# ...
|
||||
#def cConfig(update:telegram.Update, context:CallbackContext) -> None:
|
||||
# Cmd = TelegramHandleCmd(update)
|
||||
# if not Cmd: return
|
||||
# # ... area: eu, us, ...
|
||||
# # ... language: en, it, ...
|
||||
# # ... userdata: import, export, delete
|
||||
def cConfig(context:EventContext, data:InputMessageData) -> None:
|
||||
if not (settings := GetUserSettings(data.user.id)):
|
||||
User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute()
|
||||
if (get := ObjGet(data, "command.arguments.get")):
|
||||
SendMessage(context, OutputMessageData(text_plain=str(ObjGet(data.user.settings, get))))
|
||||
#Cmd = TelegramHandleCmd(update)
|
||||
#if not Cmd: return
|
||||
# ... area: eu, us, ...
|
||||
# ... language: en, it, ...
|
||||
# ... userdata: import, export, delete
|
||||
|
||||
def cPing(context:EventContext, data:InputMessageData) -> None:
|
||||
SendMessage(context, {"Text": "*Pong!*"})
|
||||
@ -54,6 +56,9 @@ def cExec(context:EventContext, data:InputMessageData) -> None:
|
||||
RegisterModule(name="Misc", endpoints=[
|
||||
SafeNamespace(names=["start"], summary="Salutes the user, hinting that the bot is working and providing basic quick help.", handler=cStart),
|
||||
SafeNamespace(names=["source"], summary="Provides a copy of the bot source codes and/or instructions on how to get it.", handler=cSource),
|
||||
SafeNamespace(names=["config"], handler=cConfig, arguments={
|
||||
"get": True,
|
||||
}),
|
||||
#SafeNamespace(names=["gdpr"], summary="Operations for european citizens regarding your personal data.", handler=cGdpr),
|
||||
SafeNamespace(names=["ping"], summary="Responds pong, useful for testing messaging latency.", handler=cPing),
|
||||
SafeNamespace(names=["eval"], summary="Execute a Python command (or safe literal operation) in the current context. Currently not implemented.", handler=cEval),
|
||||
|
@ -7,7 +7,7 @@ from json import dumps as json_dumps
|
||||
|
||||
def cDump(context:EventContext, data:InputMessageData):
|
||||
if not (message := ObjGet(data, "quoted")):
|
||||
pass
|
||||
pass # TODO send error message
|
||||
SendMessage(context, {"TextPlain": json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ")})
|
||||
|
||||
RegisterModule(name="Dumper", group="Geek", endpoints=[
|
||||
|
@ -17,9 +17,9 @@ def cEcho(context:EventContext, data:InputMessageData) -> None:
|
||||
if nonascii:
|
||||
# text is not ascii, probably an emoji (altough not necessarily), so just pass as is (useful for Telegram emojis)
|
||||
prefix = ''
|
||||
SendMessage(context, {"Text": (prefix + text)})
|
||||
SendMessage(context, OutputMessageData(text=(prefix + text)))
|
||||
else:
|
||||
SendMessage(context, {"Text": choice(Locale.__('echo.empty'))}) #context.endpoint.get_string('empty')
|
||||
SendMessage(context, OutputMessageData(text_html=context.endpoint.get_string('empty')))
|
||||
|
||||
RegisterModule(name="Echo", endpoints=[
|
||||
SafeNamespace(names=["echo"], handler=cEcho),
|
||||
|
@ -92,6 +92,7 @@ def cTranslate(context:EventContext, data:InputMessageData) -> None:
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
# unsplash source appears to be deprecated! <https://old.reddit.com/r/unsplash/comments/s13x4h/what_happened_to_sourceunsplashcom/l65epl8/>
|
||||
def cUnsplash(context:EventContext, data:InputMessageData) -> None:
|
||||
try:
|
||||
Req = HttpReq(f'https://source.unsplash.com/random/?{UrlParse.quote(data.Body)}')
|
||||
@ -144,7 +145,7 @@ RegisterModule(name="Internet", summary="Tools and toys related to the Internet.
|
||||
"language_to": True,
|
||||
"language_from": False,
|
||||
}),
|
||||
SafeNamespace(names=["unsplash"], summary="Sends a picture sourced from Unsplash.", handler=cUnsplash),
|
||||
#SafeNamespace(names=["unsplash"], summary="Sends a picture sourced from Unsplash.", handler=cUnsplash),
|
||||
SafeNamespace(names=["safebooru"], summary="Sends a picture sourced from Safebooru.", handler=cSafebooru),
|
||||
])
|
||||
|
||||
|
@ -123,6 +123,6 @@ def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None:
|
||||
|
||||
RegisterModule(name="Scrapers", endpoints=[
|
||||
SafeNamespace(names=["dalle"], summary="Sends an AI-generated picture from DALL-E 3 via Microsoft Bing.", handler=cDalleSelenium),
|
||||
SafeNamespace(names=["craiyon"], summary="Sends an AI-generated picture from Craiyon.com.", handler=cCraiyonSelenium),
|
||||
SafeNamespace(names=["craiyon", "crayion"], summary="Sends an AI-generated picture from Craiyon.com.", handler=cCraiyonSelenium),
|
||||
])
|
||||
|
||||
|
97
WinDog.py
97
WinDog.py
@ -11,35 +11,18 @@ from hashlib import new as hashlib_new
|
||||
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 yaml import load as yaml_load, BaseLoader as yaml_BaseLoader
|
||||
#from xml.etree import ElementTree
|
||||
from bs4 import BeautifulSoup
|
||||
from html import unescape as HtmlUnescape
|
||||
from markdown import markdown
|
||||
from LibWinDog.Types import *
|
||||
from LibWinDog.Config import *
|
||||
from LibWinDog.Database import *
|
||||
|
||||
# <https://daringfireball.net/projects/markdown/syntax#backslash>
|
||||
MdEscapes = '\\`*_{}[]()<>#+-.!|='
|
||||
|
||||
class SafeNamespace(SimpleNamespace):
|
||||
def __getattribute__(self, value):
|
||||
try:
|
||||
return super().__getattribute__(value)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
class EventContext(SafeNamespace):
|
||||
pass
|
||||
|
||||
class InputMessageData(SafeNamespace):
|
||||
pass
|
||||
|
||||
class OutputMessageData(SafeNamespace):
|
||||
pass
|
||||
|
||||
def NamespaceUnion(namespaces:list|tuple, clazz=SimpleNamespace):
|
||||
dikt = {}
|
||||
for namespace in namespaces:
|
||||
@ -113,12 +96,6 @@ def ObjGet(node:object, query:str, /) -> any:
|
||||
node = node.__getattribute__(method)(key)
|
||||
except exception:
|
||||
return None
|
||||
#try:
|
||||
# node = node[key]
|
||||
#except TypeError:
|
||||
# node = node.__getattribute__(key)
|
||||
#except (TypeError, KeyError, AttributeError):
|
||||
# return None
|
||||
return node
|
||||
|
||||
def isinstanceSafe(clazz:any, instance:any) -> bool:
|
||||
@ -196,10 +173,10 @@ def RandHexStr(length:int) -> str:
|
||||
hexa += choice('0123456789abcdef')
|
||||
return hexa
|
||||
|
||||
def GetUserData(user_id:str) -> SafeNamespace|None:
|
||||
def GetUserSettings(user_id:str) -> SafeNamespace|None:
|
||||
try:
|
||||
return User.get(User.id == user_id)
|
||||
except User.DoesNotExist:
|
||||
return SafeNamespace(**EntitySettings.select().join(User).where(User.id == user_id).dicts().get())
|
||||
except EntitySettings.DoesNotExist:
|
||||
return None
|
||||
|
||||
def ParseCommand(text:str) -> SafeNamespace|None:
|
||||
@ -243,7 +220,7 @@ def UpdateUserDb(user:SafeNamespace) -> None:
|
||||
user_hash = ("sha256:" + hashlib_new("sha256", user.id.encode()).hexdigest())
|
||||
try:
|
||||
User.get(User.id_hash == user_hash)
|
||||
User.update(id=user.id).where(User.id_hash == user_hash)
|
||||
User.update(id=user.id).where(User.id_hash == user_hash).execute()
|
||||
except User.DoesNotExist:
|
||||
User.create(id=user.id, id_hash=user_hash)
|
||||
|
||||
@ -258,19 +235,31 @@ def DumpMessage(data:InputMessageData) -> None:
|
||||
open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Dump.txt"), 'a').write(text + '\n')
|
||||
|
||||
def SendMessage(context:EventContext, data:OutputMessageData, destination=None) -> None:
|
||||
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'):
|
||||
data = (OutputMessageData(**data) if type(data) == dict else data)
|
||||
|
||||
# TODO remove this after all modules are changed
|
||||
if data.Text and not data.text:
|
||||
data.text = data.Text
|
||||
if data.TextPlain and not data.text_plain:
|
||||
data.text_plain = data.TextPlain
|
||||
if data.TextMarkdown and not data.text_markdown:
|
||||
data.text_markdown = data.TextMarkdown
|
||||
|
||||
if data.text_plain or data.text_markdown or data.text_html:
|
||||
if data.text_html and not data.text_plain:
|
||||
data.text_plain = data.text_html # TODO flatten the HTML to plaintext
|
||||
elif data.text_markdown and not data.text_plain:
|
||||
data.text_plain = data.text_markdown
|
||||
elif 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]
|
||||
data.text_plain = MdToTxt(data.text)
|
||||
data.text_markdown = CharEscape(HtmlUnescape(data.text), InferMdEscape(HtmlUnescape(data.text), data.text_plain))
|
||||
#data.text_html = ???
|
||||
if data.media:
|
||||
data.media = SureArray(data.media)
|
||||
for platform in Platforms.values():
|
||||
if isinstanceSafe(context.event, platform.eventClass) or isinstanceSafe(context.manager, platform.managerClass):
|
||||
return platform.sender(context, data, destination, textPlain, textMarkdown)
|
||||
return platform.sender(context, data, destination)
|
||||
|
||||
def SendNotice(context:EventContext, data) -> None:
|
||||
pass
|
||||
@ -278,37 +267,12 @@ def SendNotice(context:EventContext, data) -> None:
|
||||
def DeleteMessage(context:EventContext, data) -> None:
|
||||
pass
|
||||
|
||||
#def ParseModuleTree(module:ElementTree.Element):
|
||||
# def parseTexts(tree:ElementTree.Element):
|
||||
# texts = {}
|
||||
# for text in tree:
|
||||
# texts[text.tag] = text.text
|
||||
# return texts
|
||||
# if module.tag != "module":
|
||||
# raise Exception(f"Unknown root element <{module.tag}> in {FILE}; it should be <module>.")
|
||||
# for option in module:
|
||||
# match option.tag:
|
||||
# case _:
|
||||
# parseTexts(option)
|
||||
# case "endpoints":
|
||||
# for endpoint in option:
|
||||
# for option in endpoint:
|
||||
# match option.tag:
|
||||
# case _:
|
||||
# parseTexts(option)
|
||||
# case "arguments":
|
||||
# for argument in option:
|
||||
# parseTexts(argument)
|
||||
|
||||
def RegisterPlatform(name:str, main:callable, sender:callable, linker:callable=None, *, eventClass=None, managerClass=None) -> None:
|
||||
Platforms[name] = SafeNamespace(main=main, sender=sender, linker=linker, eventClass=eventClass, managerClass=managerClass)
|
||||
Log(f"{name}, ", inline=True)
|
||||
|
||||
def RegisterModule(name:str, endpoints:dict, *, group:str|None=None, summary:str|None=None) -> None:
|
||||
module = SafeNamespace(group=group, endpoints=(SafeNamespace(**endpoints) if type(endpoints) == dict else endpoints))
|
||||
# TODO load XML data, add to both module and locale objects
|
||||
#if isfile(file := f"./ModWinDog/{name}/{name}.xml"):
|
||||
# ParseModuleTree(ElementTree.parse(file).getroot())
|
||||
module = SafeNamespace(group=group, endpoints=endpoints)
|
||||
if isfile(file := f"./ModWinDog/{name}/{name}.yaml"):
|
||||
module.strings = yaml_load(open(file, 'r').read().replace("\t", " "), Loader=yaml_BaseLoader)
|
||||
module.get_string = (lambda query, lang=None: GetString(module.strings, query, lang))
|
||||
@ -316,7 +280,6 @@ def RegisterModule(name:str, endpoints:dict, *, group:str|None=None, summary:str
|
||||
Log(f"{name}, ", inline=True)
|
||||
for endpoint in endpoints:
|
||||
endpoint.module = module
|
||||
endpoint.get_string = (lambda query, lang=None: module.get_string(f"endpoints.{endpoint.names[0]}.{query}", lang))
|
||||
for name in endpoint.names:
|
||||
Endpoints[name] = endpoint
|
||||
|
||||
@ -324,6 +287,7 @@ def CallEndpoint(name:str, context:EventContext, data:InputMessageData):
|
||||
endpoint = Endpoints[name]
|
||||
context.endpoint = endpoint
|
||||
context.module = endpoint.module
|
||||
context.endpoint.get_string = (lambda query, lang=None: endpoint.module.get_string(f"endpoints.{data.command.name}.{query}", lang))
|
||||
return endpoint.handler(context, data)
|
||||
|
||||
def WriteNewConfig() -> None:
|
||||
@ -356,7 +320,6 @@ def Main() -> None:
|
||||
|
||||
if __name__ == '__main__':
|
||||
Log("🌞️ WinDog Starting...")
|
||||
#Db = {"Rooms": {}, "Users": {}}
|
||||
Locale = {"Fallback": {}}
|
||||
Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user