More API refactoring, add HTML text for Telegram, start work on /config

This commit is contained in:
2024-06-26 02:02:55 +02:00
parent 8f1b80ab14
commit 2c73846554
10 changed files with 104 additions and 106 deletions

View File

@ -1,4 +1,5 @@
from peewee import * from peewee import *
from LibWinDog.Types import *
Db = SqliteDatabase("Database.sqlite") Db = SqliteDatabase("Database.sqlite")
@ -6,11 +7,13 @@ class BaseModel(Model):
class Meta: class Meta:
database = Db database = Db
class EntitySettings(BaseModel):
language = CharField(null=True)
class Entity(BaseModel): class Entity(BaseModel):
id = CharField(null=True) id = CharField(null=True)
id_hash = CharField() id_hash = CharField()
#settings = ForeignKeyField(EntitySettings, backref="entity") settings = ForeignKeyField(EntitySettings, backref="entity", null=True)
#language = CharField(null=True)
class User(Entity): class User(Entity):
pass pass
@ -18,5 +21,5 @@ class User(Entity):
class Room(Entity): class Room(Entity):
pass pass
Db.create_tables([User, Room], safe=True) Db.create_tables([EntitySettings, User, Room], safe=True)

View File

@ -39,15 +39,19 @@ def MastodonHandler(event):
if command.Name in Endpoints: if command.Name in Endpoints:
CallEndpoint(command.Name, EventContext(platform="mastodon", event=event, manager=Mastodon), command) CallEndpoint(command.Name, EventContext(platform="mastodon", event=event, manager=Mastodon), command)
def MastodonSender(context:EventContext, data:OutputMessageData, destination, textPlain, textMarkdown) -> None: def MastodonSender(context:EventContext, data:OutputMessageData, destination) -> None:
if InDict(data, 'Media'): media_results = None
Media = context.manager.media_post(data['Media'], Magic(mime=True).from_buffer(data['Media'])) if data.media:
while Media['url'] == 'null': media_results = []
Media = context.manager.media(Media) for medium in data.media[:4]: # Mastodon limits posts to 4 attachments
if textPlain or Media: 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( context.manager.status_post(
status=(textPlain + '\n\n@' + context.event['account']['acct']), status=(data.text_plain + '\n\n@' + context.event['account']['acct']),
media_ids=(Media if InDict(data, 'Media') else None), media_ids=media_results,
in_reply_to_id=context.event['status']['id'], in_reply_to_id=context.event['status']['id'],
visibility=('direct' if context.event['status']['visibility'] == 'direct' else 'unlisted'), visibility=('direct' if context.event['status']['visibility'] == 'direct' else 'unlisted'),
) )

View File

@ -31,6 +31,8 @@ def TelegramMain() -> bool:
return True return True
def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData: def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
#if not message:
# return None
data = InputMessageData( data = InputMessageData(
message_id = f"telegram:{message.message_id}", message_id = f"telegram:{message.message_id}",
text_plain = message.text, text_plain = message.text,
@ -48,8 +50,7 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
tag = message.from_user.username, tag = message.from_user.username,
name = message.from_user.first_name, name = message.from_user.first_name,
) )
#if (db_user := GetUserData(data.user.id)): data.user.settings = GetUserSettings(data.user.id)
# data.user.language = db_user.language
return data return data
def TelegramHandlerWrapper(update:telegram.Update, context:CallbackContext=None) -> None: def TelegramHandlerWrapper(update:telegram.Update, context:CallbackContext=None) -> None:
@ -64,8 +65,11 @@ def TelegramHandlerCore(update:telegram.Update, context:CallbackContext=None) ->
OnMessageParsed(data) OnMessageParsed(data)
cmd = ParseCmd(update.message.text) cmd = ParseCmd(update.message.text)
if cmd: if cmd:
# TODO remove old cmd and just pass the data object
cmd.command = data.command cmd.command = data.command
cmd.quoted = data.quoted cmd.quoted = data.quoted
cmd.user = data.user
cmd.message_id = data.message_id
cmd.messageId = update.message.message_id cmd.messageId = update.message.message_id
cmd.TextPlain = cmd.Body cmd.TextPlain = cmd.Body
cmd.TextMarkdown = update.message.text_markdown_v2 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) 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 result = None
if destination: if destination:
result = context.manager.bot.send_message(destination, text=textPlain) result = context.manager.bot.send_message(destination, text=data.text_plain)
else: else:
replyToId = (data["ReplyTo"] if ("ReplyTo" in data and data["ReplyTo"]) else context.event.message.message_id) replyToId = (data.ReplyTo or context.event.message.message_id)
if InDict(data, "Media") and not InDict(data, "media"): if data.media:
data["media"] = {"bytes": data["Media"]} for medium in data.media:
if InDict(data, "media"):
for medium in SureArray(data["media"]):
result = context.event.message.reply_photo( result = context.event.message.reply_photo(
(DictGet(medium, "bytes") or DictGet(medium, "url")), (DictGet(medium, "bytes") or DictGet(medium, "url")),
caption=(textMarkdown if textMarkdown else textPlain if textPlain else None), caption=(data.text_html or data.text_markdown or data.text_plain),
parse_mode=("MarkdownV2" if textMarkdown else None), parse_mode=("HTML" if data.text_html else "MarkdownV2" if data.text_markdown else None),
reply_to_message_id=replyToId) reply_to_message_id=replyToId)
elif textMarkdown: elif data.text_html:
result = context.event.message.reply_markdown_v2(textMarkdown, reply_to_message_id=replyToId) result = context.event.message.reply_html(data.text_html, reply_to_message_id=replyToId)
elif textPlain: elif data.text_markdown:
result = context.event.message.reply_text(textPlain, reply_to_message_id=replyToId) 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) return TelegramMakeInputMessageData(result)
def TelegramLinker(data:InputMessageData) -> SafeNamespace: def TelegramLinker(data:InputMessageData) -> SafeNamespace:

18
LibWinDog/Types.py Normal file
View 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

View File

@ -17,14 +17,16 @@ def cSource(context:EventContext, data:InputMessageData) -> None:
def cGdpr(context:EventContext, data:InputMessageData) -> None: def cGdpr(context:EventContext, data:InputMessageData) -> None:
pass pass
# Module: Config def cConfig(context:EventContext, data:InputMessageData) -> None:
# ... if not (settings := GetUserSettings(data.user.id)):
#def cConfig(update:telegram.Update, context:CallbackContext) -> None: 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) #Cmd = TelegramHandleCmd(update)
#if not Cmd: return #if not Cmd: return
# # ... area: eu, us, ... # ... area: eu, us, ...
# # ... language: en, it, ... # ... language: en, it, ...
# # ... userdata: import, export, delete # ... userdata: import, export, delete
def cPing(context:EventContext, data:InputMessageData) -> None: def cPing(context:EventContext, data:InputMessageData) -> None:
SendMessage(context, {"Text": "*Pong!*"}) SendMessage(context, {"Text": "*Pong!*"})
@ -54,6 +56,9 @@ def cExec(context:EventContext, data:InputMessageData) -> None:
RegisterModule(name="Misc", endpoints=[ 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=["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=["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=["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=["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), SafeNamespace(names=["eval"], summary="Execute a Python command (or safe literal operation) in the current context. Currently not implemented.", handler=cEval),

View File

@ -7,7 +7,7 @@ from json import dumps as json_dumps
def cDump(context:EventContext, data:InputMessageData): def cDump(context:EventContext, data:InputMessageData):
if not (message := ObjGet(data, "quoted")): if not (message := ObjGet(data, "quoted")):
pass pass # TODO send error message
SendMessage(context, {"TextPlain": json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ")}) SendMessage(context, {"TextPlain": json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ")})
RegisterModule(name="Dumper", group="Geek", endpoints=[ RegisterModule(name="Dumper", group="Geek", endpoints=[

View File

@ -17,9 +17,9 @@ def cEcho(context:EventContext, data:InputMessageData) -> None:
if nonascii: if nonascii:
# text is not ascii, probably an emoji (altough not necessarily), so just pass as is (useful for Telegram emojis) # text is not ascii, probably an emoji (altough not necessarily), so just pass as is (useful for Telegram emojis)
prefix = '' prefix = ''
SendMessage(context, {"Text": (prefix + text)}) SendMessage(context, OutputMessageData(text=(prefix + text)))
else: 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=[ RegisterModule(name="Echo", endpoints=[
SafeNamespace(names=["echo"], handler=cEcho), SafeNamespace(names=["echo"], handler=cEcho),

View File

@ -92,6 +92,7 @@ def cTranslate(context:EventContext, data:InputMessageData) -> None:
except Exception: except Exception:
raise 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: def cUnsplash(context:EventContext, data:InputMessageData) -> None:
try: try:
Req = HttpReq(f'https://source.unsplash.com/random/?{UrlParse.quote(data.Body)}') 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_to": True,
"language_from": False, "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), SafeNamespace(names=["safebooru"], summary="Sends a picture sourced from Safebooru.", handler=cSafebooru),
]) ])

View File

@ -123,6 +123,6 @@ def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None:
RegisterModule(name="Scrapers", endpoints=[ RegisterModule(name="Scrapers", endpoints=[
SafeNamespace(names=["dalle"], summary="Sends an AI-generated picture from DALL-E 3 via Microsoft Bing.", handler=cDalleSelenium), 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),
]) ])

View File

@ -11,35 +11,18 @@ from hashlib import new as hashlib_new
from os import listdir from os import listdir
from os.path import isfile, isdir from os.path import isfile, isdir
from random import choice, randint from random import choice, randint
from types import SimpleNamespace
from traceback import format_exc from traceback import format_exc
from yaml import load as yaml_load, BaseLoader as yaml_BaseLoader from yaml import load as yaml_load, BaseLoader as yaml_BaseLoader
#from xml.etree import ElementTree
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from html import unescape as HtmlUnescape from html import unescape as HtmlUnescape
from markdown import markdown from markdown import markdown
from LibWinDog.Types import *
from LibWinDog.Config import * from LibWinDog.Config import *
from LibWinDog.Database import * from LibWinDog.Database import *
# <https://daringfireball.net/projects/markdown/syntax#backslash> # <https://daringfireball.net/projects/markdown/syntax#backslash>
MdEscapes = '\\`*_{}[]()<>#+-.!|=' 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): def NamespaceUnion(namespaces:list|tuple, clazz=SimpleNamespace):
dikt = {} dikt = {}
for namespace in namespaces: for namespace in namespaces:
@ -113,12 +96,6 @@ def ObjGet(node:object, query:str, /) -> any:
node = node.__getattribute__(method)(key) node = node.__getattribute__(method)(key)
except exception: except exception:
return None return None
#try:
# node = node[key]
#except TypeError:
# node = node.__getattribute__(key)
#except (TypeError, KeyError, AttributeError):
# return None
return node return node
def isinstanceSafe(clazz:any, instance:any) -> bool: def isinstanceSafe(clazz:any, instance:any) -> bool:
@ -196,10 +173,10 @@ def RandHexStr(length:int) -> str:
hexa += choice('0123456789abcdef') hexa += choice('0123456789abcdef')
return hexa return hexa
def GetUserData(user_id:str) -> SafeNamespace|None: def GetUserSettings(user_id:str) -> SafeNamespace|None:
try: try:
return User.get(User.id == user_id) return SafeNamespace(**EntitySettings.select().join(User).where(User.id == user_id).dicts().get())
except User.DoesNotExist: except EntitySettings.DoesNotExist:
return None return None
def ParseCommand(text:str) -> SafeNamespace|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()) user_hash = ("sha256:" + hashlib_new("sha256", user.id.encode()).hexdigest())
try: try:
User.get(User.id_hash == user_hash) 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: except User.DoesNotExist:
User.create(id=user.id, id_hash=user_hash) 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') open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Dump.txt"), 'a').write(text + '\n')
def SendMessage(context:EventContext, data:OutputMessageData, destination=None) -> None: def SendMessage(context:EventContext, data:OutputMessageData, destination=None) -> None:
if InDict(data, 'TextPlain') or InDict(data, 'TextMarkdown'): data = (OutputMessageData(**data) if type(data) == dict else data)
textPlain = InDict(data, 'TextPlain')
textMarkdown = InDict(data, 'TextMarkdown') # TODO remove this after all modules are changed
if not textPlain: if data.Text and not data.text:
textPlain = textMarkdown data.text = data.Text
elif InDict(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 # our old system attempts to always receive Markdown and retransform when needed
textPlain = MdToTxt(data['Text']) data.text_plain = MdToTxt(data.text)
textMarkdown = CharEscape(HtmlUnescape(data['Text']), InferMdEscape(HtmlUnescape(data['Text']), textPlain)) data.text_markdown = CharEscape(HtmlUnescape(data.text), InferMdEscape(HtmlUnescape(data.text), data.text_plain))
for platform in Platforms: #data.text_html = ???
platform = Platforms[platform] 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): 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: def SendNotice(context:EventContext, data) -> None:
pass pass
@ -278,37 +267,12 @@ def SendNotice(context:EventContext, data) -> None:
def DeleteMessage(context:EventContext, data) -> None: def DeleteMessage(context:EventContext, data) -> None:
pass 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: 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) Platforms[name] = SafeNamespace(main=main, sender=sender, linker=linker, eventClass=eventClass, managerClass=managerClass)
Log(f"{name}, ", inline=True) Log(f"{name}, ", inline=True)
def RegisterModule(name:str, endpoints:dict, *, group:str|None=None, summary:str|None=None) -> None: 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)) module = SafeNamespace(group=group, endpoints=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())
if isfile(file := f"./ModWinDog/{name}/{name}.yaml"): if isfile(file := f"./ModWinDog/{name}/{name}.yaml"):
module.strings = yaml_load(open(file, 'r').read().replace("\t", " "), Loader=yaml_BaseLoader) 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)) 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) Log(f"{name}, ", inline=True)
for endpoint in endpoints: for endpoint in endpoints:
endpoint.module = module 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: for name in endpoint.names:
Endpoints[name] = endpoint Endpoints[name] = endpoint
@ -324,6 +287,7 @@ def CallEndpoint(name:str, context:EventContext, data:InputMessageData):
endpoint = Endpoints[name] endpoint = Endpoints[name]
context.endpoint = endpoint context.endpoint = endpoint
context.module = endpoint.module 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) return endpoint.handler(context, data)
def WriteNewConfig() -> None: def WriteNewConfig() -> None:
@ -356,7 +320,6 @@ def Main() -> None:
if __name__ == '__main__': if __name__ == '__main__':
Log("🌞️ WinDog Starting...") Log("🌞️ WinDog Starting...")
#Db = {"Rooms": {}, "Users": {}}
Locale = {"Fallback": {}} Locale = {"Fallback": {}}
Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {} Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {}