Legacy removals, code restructuring, add send_... functions and better help

This commit is contained in:
2024-08-10 01:36:54 +02:00
parent 183b8c60cd
commit 6ebc68127e
32 changed files with 512 additions and 351 deletions

9
.gitignore vendored
View File

@ -1,8 +1,5 @@
/Config.py /Data/
/Database.sqlite
/Dump.txt
/Log.txt
/Selenium-WinDog/
/downloaded_files/ /downloaded_files/
/session.txt
*.pyc *.pyc
*.lock
/RunWinDog.py

View File

@ -7,7 +7,7 @@
# If you have modified the bot's code, you should set this. # If you have modified the bot's code, you should set this.
ModifiedSourceUrl = "" ModifiedSourceUrl = ""
# Logging of system information and runtime errors. Recommended to be on at least for console. # Logging of system information and runtime errors. Recommended to be on in some way to diagnose errors.
LogToConsole = True LogToConsole = True
LogToFile = True LogToFile = True
@ -21,14 +21,8 @@ BridgesConfig = []
DefaultLanguage = "en" DefaultLanguage = "en"
Debug = False Debug = False
CmdPrefixes = ".!/" CommandPrefixes = ".!/"
WebUserAgent = "WinDog v.Staging" WebUserAgent = "WinDog v.Staging"
#ModuleGroups = (ModuleGroups | {
ModuleGroups = {
"Basic": "",
"Geek": "",
}
# Only for the platforms you want to use, uncomment the below credentials and fill with your own: # Only for the platforms you want to use, uncomment the below credentials and fill with your own:
""" # end windog config # """ """ # end windog config # """

View File

@ -1,12 +1,12 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
from peewee import * from peewee import *
from LibWinDog.Types import * from LibWinDog.Types import *
Db = SqliteDatabase("Database.sqlite") Db = SqliteDatabase("./Data/Database.sqlite")
class BaseModel(Model): class BaseModel(Model):
class Meta: class Meta:
@ -28,3 +28,10 @@ class Room(Entity):
Db.create_tables([EntitySettings, User, Room], safe=True) Db.create_tables([EntitySettings, User, Room], safe=True)
class UserSettingsData():
def __new__(cls, user_id:str) -> SafeNamespace|None:
try:
return SafeNamespace(**EntitySettings.select().join(User).where(User.id == user_id).dicts().get())
except EntitySettings.DoesNotExist:
return None

View File

@ -1,7 +1,7 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
""" # windog config start # """ # windog config start #
@ -35,20 +35,19 @@ def MastodonMakeInputMessageData(status:dict) -> InputMessageData:
command_tokens = data.text_plain.strip().replace("\t", " ").split(" ") command_tokens = data.text_plain.strip().replace("\t", " ").split(" ")
while command_tokens[0].strip().startswith('@') or not command_tokens[0]: while command_tokens[0].strip().startswith('@') or not command_tokens[0]:
command_tokens.pop(0) command_tokens.pop(0)
data.command = ParseCommand(" ".join(command_tokens), "mastodon") data.command = TextCommandData(" ".join(command_tokens), "mastodon")
data.user = UserData( data.user = UserData(
id = ("mastodon:" + strip_url_scheme(status["account"]["uri"])), id = ("mastodon:" + strip_url_scheme(status["account"]["uri"])),
name = status["account"]["display_name"], name = status["account"]["display_name"],
) )
data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace()) data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace())
return data return data
def MastodonHandler(event, Mastodon): def MastodonHandler(event, Mastodon):
if event["type"] == "mention": if event["type"] == "mention":
data = MastodonMakeInputMessageData(event["status"]) data = MastodonMakeInputMessageData(event["status"])
OnInputMessageParsed(data) OnInputMessageParsed(data)
if (command := ObjGet(data, "command.name")): call_endpoint(EventContext(platform="mastodon", event=event, manager=Mastodon), data)
CallEndpoint(command, EventContext(platform="mastodon", event=event, manager=Mastodon), data)
def MastodonSender(context:EventContext, data:OutputMessageData) -> None: def MastodonSender(context:EventContext, data:OutputMessageData) -> None:
media_results = None media_results = None

View File

@ -1,7 +1,7 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
""" # windog config start # """ # windog config start #
@ -41,9 +41,9 @@ def MatrixMain() -> bool:
global MatrixClient global MatrixClient
MatrixClient = nio.AsyncClient(MatrixUrl, MatrixUsername) MatrixClient = nio.AsyncClient(MatrixUrl, MatrixUsername)
login = await MatrixClient.login(password=MatrixPassword, token=MatrixToken) login = await MatrixClient.login(password=MatrixPassword, token=MatrixToken)
if MatrixPassword and (not MatrixToken) and (token := ObjGet(login, "access_token")): if MatrixPassword and (not MatrixToken) and (token := obj_get(login, "access_token")):
open("./Config.py", 'a').write(f'\n# Added automatically #\nMatrixToken = "{token}"\n') open("./Config.py", 'a').write(f'\n# Added automatically #\nMatrixToken = "{token}"\n')
if (bot_id := ObjGet(login, "user_id")): if (bot_id := obj_get(login, "user_id")):
upgrade_username(bot_id) # ensure username is fully qualified for the API upgrade_username(bot_id) # ensure username is fully qualified for the API
await MatrixClient.sync(30000) # resync old messages first to "skip read ones" await MatrixClient.sync(30000) # resync old messages first to "skip read ones"
asyncio.ensure_future(queue_handler()) asyncio.ensure_future(queue_handler())
@ -58,8 +58,8 @@ def MatrixMakeInputMessageData(room:nio.MatrixRoom, event:nio.RoomMessage) -> In
message_id = f"matrix:{event.event_id}", message_id = f"matrix:{event.event_id}",
datetime = event.server_timestamp, datetime = event.server_timestamp,
text_plain = event.body, text_plain = event.body,
text_html = ObjGet(event, "formatted_body"), # this could be unavailable text_html = obj_get(event, "formatted_body"), # this could be unavailable
media = ({"url": event.url} if ObjGet(event, "url") else None), media = ({"url": event.url} if obj_get(event, "url") else None),
room = SafeNamespace( room = SafeNamespace(
id = f"matrix:{room.room_id}", id = f"matrix:{room.room_id}",
name = room.display_name, name = room.display_name,
@ -69,11 +69,11 @@ def MatrixMakeInputMessageData(room:nio.MatrixRoom, event:nio.RoomMessage) -> In
#name = , # TODO name must be get via a separate API request (and so maybe we should cache it) #name = , # TODO name must be get via a separate API request (and so maybe we should cache it)
), ),
) )
if (mxc_url := ObjGet(data, "media.url")) and mxc_url.startswith("mxc://"): if (mxc_url := obj_get(data, "media.url")) and mxc_url.startswith("mxc://"):
_, _, server_name, media_id = mxc_url.split('/') _, _, server_name, media_id = mxc_url.split('/')
data.media["url"] = ("https://" + server_name + nio.Api.download(server_name, media_id)[1]) data.media["url"] = ("https://" + server_name + nio.Api.download(server_name, media_id)[1])
data.command = ParseCommand(data.text_plain, "matrix") data.command = TextCommandData(data.text_plain, "matrix")
data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace()) data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace())
return data return data
async def MatrixInviteHandler(room:nio.MatrixRoom, event:nio.InviteEvent) -> None: async def MatrixInviteHandler(room:nio.MatrixRoom, event:nio.InviteEvent) -> None:
@ -84,8 +84,7 @@ async def MatrixMessageHandler(room:nio.MatrixRoom, event:nio.RoomMessage) -> No
return # ignore messages that come from the bot itself return # ignore messages that come from the bot itself
data = MatrixMakeInputMessageData(room, event) data = MatrixMakeInputMessageData(room, event)
OnInputMessageParsed(data) OnInputMessageParsed(data)
if (command := ObjGet(data, "command.name")): call_endpoint(EventContext(platform="matrix", event=SafeNamespace(room=room, event=event), manager=MatrixClient), data)
CallEndpoint(command, EventContext(platform="matrix", event=SafeNamespace(room=room, event=event), manager=MatrixClient), data)
def MatrixSender(context:EventContext, data:OutputMessageData): def MatrixSender(context:EventContext, data:OutputMessageData):
try: try:
@ -94,7 +93,7 @@ def MatrixSender(context:EventContext, data:OutputMessageData):
MatrixQueue.put((context, data)) MatrixQueue.put((context, data))
return None return None
asyncio.create_task(context.manager.room_send( asyncio.create_task(context.manager.room_send(
room_id=((data.room and data.room.id) or ObjGet(context, "event.room.room_id")), room_id=((data.room and data.room.id) or obj_get(context, "event.room.room_id")),
message_type="m.room.message", message_type="m.room.message",
content={"msgtype": "m.text", "body": data.text_plain})) content={"msgtype": "m.text", "body": data.text_plain}))

View File

@ -1,7 +1,7 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
""" # windog config start # """ # windog config start #
@ -54,8 +54,8 @@ def TelegramMakeInputMessageData(message:telegram.Message) -> InputMessageData:
name = (message.chat.title or message.chat.first_name), name = (message.chat.title or message.chat.first_name),
), ),
) )
data.command = ParseCommand(data.text_plain, "telegram") data.command = TextCommandData(data.text_plain, "telegram")
data.user.settings = (GetUserSettings(data.user.id) or SafeNamespace()) data.user.settings = (UserSettingsData(data.user.id) or SafeNamespace())
linked = TelegramLinker(data) linked = TelegramLinker(data)
data.message_url = linked.message data.message_url = linked.message
data.room.url = linked.room data.room.url = linked.room
@ -69,8 +69,7 @@ def TelegramHandler(update:telegram.Update, context:CallbackContext=None) -> Non
if (quoted := update.message.reply_to_message): if (quoted := update.message.reply_to_message):
data.quoted = TelegramMakeInputMessageData(quoted) data.quoted = TelegramMakeInputMessageData(quoted)
OnInputMessageParsed(data) OnInputMessageParsed(data)
if (command := ObjGet(data, "command.name")): call_endpoint(EventContext(platform="telegram", event=update, manager=context), data)
CallEndpoint(command, EventContext(platform="telegram", event=update, manager=context), data)
Thread(target=handler).start() Thread(target=handler).start()
def TelegramSender(context:EventContext, data:OutputMessageData): def TelegramSender(context:EventContext, data:OutputMessageData):
@ -83,7 +82,7 @@ def TelegramSender(context:EventContext, data:OutputMessageData):
if data.media: if data.media:
for medium in data.media: for medium in data.media:
result = context.event.message.reply_photo( result = context.event.message.reply_photo(
(ObjGet(medium, "bytes") or ObjGet(medium, "url")), (obj_get(medium, "bytes") or obj_get(medium, "url")),
caption=(data.text_html or data.text_markdown or data.text_plain), 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), 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)

View File

@ -1,7 +1,7 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
""" # windog config start # """ """ # windog config start # """
@ -60,13 +60,12 @@ class WebServerClass(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
data = WebMakeInputMessageData(text, uuid) data = WebMakeInputMessageData(text, uuid)
OnInputMessageParsed(data) OnInputMessageParsed(data)
if (command := ObjGet(data, "command.name")): call_endpoint(EventContext(platform="web", event=SafeNamespace(room_id=uuid)), data)
CallEndpoint(command, EventContext(platform="web", event=SafeNamespace(room_id=uuid)), data)
def WebMakeInputMessageData(text:str, uuid:str) -> InputMessageData: def WebMakeInputMessageData(text:str, uuid:str) -> InputMessageData:
return InputMessageData( return InputMessageData(
text_plain = text, text_plain = text,
command = ParseCommand(text, "web"), command = TextCommandData(text, "web"),
room = SafeNamespace( room = SafeNamespace(
id = f"web:{uuid}", id = f"web:{uuid}",
), ),

View File

@ -1,7 +1,7 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
from types import SimpleNamespace from types import SimpleNamespace
@ -22,6 +22,9 @@ class SafeNamespace(DictNamespace):
# we just use these for type hinting and clearer code: # we just use these for type hinting and clearer code:
class CommandData(SafeNamespace):
pass
class EventContext(SafeNamespace): class EventContext(SafeNamespace):
pass pass

View File

@ -1,37 +1,46 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
def cSource(context:EventContext, data:InputMessageData) -> None: def cSource(context:EventContext, data:InputMessageData):
SendMessage(context, {"text_plain": ("""\ send_message(context, {"text_plain": ("""\
* Original Code: {https://gitlab.com/octospacc/WinDog} * Original Code: {https://gitlab.com/octospacc/WinDog}
* Mirror: {https://github.com/octospacc/WinDog} * Mirror: {https://github.com/octospacc/WinDog}
""" + (f"* Modified Code: {{{ModifiedSourceUrl}}}" if ModifiedSourceUrl else ""))}) """ + (f"* Modified Code: {{{ModifiedSourceUrl}}}" if ModifiedSourceUrl else ""))})
def cGdpr(context:EventContext, data:InputMessageData) -> None: def cGdpr(context:EventContext, data:InputMessageData):
pass pass
def cConfig(context:EventContext, data:InputMessageData) -> None: UserSettingsLimits = {
if not (settings := GetUserSettings(data.user.id)): "language": 13,
}
def cConfig(context:EventContext, data:InputMessageData):
language = data.user.settings.language
if not (settings := UserSettingsData(data.user.id)):
User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute() User.update(settings=EntitySettings.create()).where(User.id == data.user.id).execute()
if (to_set := ObjGet(data, "command.arguments.set")): settings = UserSettingsData(data.user.id)
pass # TODO set in db, but first we need to ensure data is handled safely if not (key := data.command.arguments.get) or (key not in UserSettingsLimits):
if (to_get := ObjGet(data, "command.arguments.get")): return send_status_400(context, language)
# TODO show a hint on possible options? if (value := data.command.body):
return SendMessage(context, OutputMessageData(text_plain=str(ObjGet(data.user.settings, to_get)))) if len(value) > UserSettingsLimits[key]:
return send_status(context, 500, language)
EntitySettings.update(**{key: value}).where(EntitySettings.entity == data.user.id).execute()
settings = UserSettingsData(data.user.id)
if (key):
# TODO show a hint on possible options? and add proper text hints for results
return send_message(context, {"text_plain": str(obj_get(settings, key))})
# TODO show general help when no useful parameters are passed # TODO show general help when no useful parameters are passed
#Cmd = TelegramHandleCmd(update)
#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):
# nice experiment, but it won't work with Telegram since time is not to milliseconds (?) # nice experiment, but it won't work with Telegram since time is not to milliseconds (?)
#time_diff = (time_now := int(time.time())) - (time_sent := data.datetime) #time_diff = (time_now := int(time.time())) - (time_sent := data.datetime)
#SendMessage(context, OutputMessageData(text_html=f"<b>Pong!</b>\n\n{time_sent} → {time_now} = {time_diff}")) #send_message(context, OutputMessageData(text_html=f"<b>Pong!</b>\n\n{time_sent} → {time_now} = {time_diff}"))
SendMessage(context, OutputMessageData(text_html="<b>Pong!</b>")) send_message(context, OutputMessageData(text_html="<b>Pong!</b>"))
#def cTime(update:Update, context:CallbackContext) -> None: #def cTime(update:Update, context:CallbackContext) -> None:
# update.message.reply_markdown_v2( # update.message.reply_markdown_v2(
@ -39,7 +48,7 @@ def cPing(context:EventContext, data:InputMessageData) -> None:
# reply_to_message_id=update.message.message_id) # reply_to_message_id=update.message.message_id)
#def cEval(context:EventContext, data:InputMessageData) -> None: #def cEval(context:EventContext, data:InputMessageData) -> None:
# SendMessage(context, {"Text": choice(Locale.__('eval'))}) # send_message(context, {"Text": choice(Locale.__('eval'))})
RegisterModule(name="Base", endpoints=[ RegisterModule(name="Base", endpoints=[
SafeNamespace(names=["source"], handler=cSource), SafeNamespace(names=["source"], handler=cSource),

View File

@ -1,17 +1,19 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
def cBroadcast(context:EventContext, data:InputMessageData) -> None: def cBroadcast(context:EventContext, data:InputMessageData):
language = data.user.settings.language
if (data.user.id not in AdminIds) and (data.user.tag not in AdminIds): if (data.user.id not in AdminIds) and (data.user.tag not in AdminIds):
return SendMessage(context, {"Text": "Permission denied."}) return send_status(context, 403, language)
destination = data.command.arguments["destination"] destination = data.command.arguments.destination
text = data.command.body text = (data.command.body or (data.quoted and data.quoted.text_plain))
if not (destination and text): if not (destination and text):
return SendMessage(context, OutputMessageData(text_plain="Bad usage.")) return send_status_400(context, language)
SendMessage(context, {"text_plain": text, "room": SafeNamespace(id=destination)}) result = send_message(context, {"text_plain": text, "room": SafeNamespace(id=destination)})
SendMessage(context, {"text_plain": "Executed."}) send_message(context, {"text_plain": "Executed."})
return result
RegisterModule(name="Broadcast", endpoints=[ RegisterModule(name="Broadcast", endpoints=[
SafeNamespace(names=["broadcast"], handler=cBroadcast, body=True, arguments={ SafeNamespace(names=["broadcast"], handler=cBroadcast, body=True, arguments={

View File

@ -1,28 +1,49 @@
# ==================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ==================================== #
import base64 import base64
from binascii import Error as binascii_Error import base256
CodingsAlgorithms = []
CodingsMethods = {
"encode:ascii85": base64.a85encode,
"decode:ascii85": base64.a85decode,
"encode:base16": base64.b16encode,
"decode:base16": base64.b16decode,
"encode:base32": base64.b32encode,
"decode:base32": base64.b32decode,
"encode:base32hex": base64.b32hexencode,
"decode:base32hex": base64.b32hexdecode,
"encode:base64": base64.b64encode,
"decode:base64": base64.b64decode,
"encode:base85": base64.b85encode,
"decode:base85": base64.b85decode,
"encode:base256": (lambda decoded: base256.encode_string(decoded.decode()).encode()),
"decode:base256": (lambda encoded: base256.decode_string(encoded.decode()).encode()),
}
for method in dict(CodingsMethods):
method2 = method.replace("ascii", 'a').replace("base", 'b')
CodingsMethods[method2] = CodingsMethods[method]
if (name := method.split(':')[1]) not in CodingsAlgorithms:
CodingsAlgorithms.append(name)
def mCodings(context:EventContext, data:InputMessageData): def mCodings(context:EventContext, data:InputMessageData):
algorithms = ["base64"] language = data.user.settings.language
methods = { method = obj_get(CodingsMethods, f"{data.command.name}:{data.command.arguments.algorithm}")
"encode_base64": base64.b64encode, text = (data.command.body or (data.quoted and data.quoted.text_plain))
"decode_base64": base64.b64decode, if not (method and text):
"encode_b64": base64.b64encode, return send_status_400(context, language)
"decode_b64": base64.b64decode, try:
} return send_message(context, {
if (method := ObjGet(methods, f"{data.command.name}_{data.command.arguments.algorithm}")): "text_html": f"<pre>{html_escape(method(text.encode()).decode())}</pre>"})
try: except Exception:
result = method((data.command.body or (data.quoted and data.quoted.text_plain)).encode()).decode() return send_status_error(context, language)
SendMessage(context, {"text_html": f"<pre>{html_escape(result)}</pre>"})
except binascii_Error:
SendMessage(context, {"text_plain": f"An error occurred."})
else:
language = data.user.settings.language
SendMessage(context, {
"text_html": f'{context.endpoint.help_text(language)}\n\n{context.module.get_string("algorithms", language)}: {algorithms}'})
RegisterModule(name="Codings", group="Geek", endpoints=[ RegisterModule(name="Codings", group="Geek", endpoints=[
SafeNamespace(names=["encode", "decode"], handler=mCodings, body=False, quoted=False, arguments={ SafeNamespace(names=["encode", "decode"], handler=mCodings, body=False, quoted=False, arguments={
"algorithm": True, "algorithm": True,
}), }, help_extra=(lambda endpoint, lang: f'{endpoint.module.get_string("algorithms", lang)}: <code>{"</code>, <code>".join(CodingsAlgorithms)}</code>.')),
]) ])

0
ModWinDog/Codings/Codings.yaml Normal file → Executable file
View File

View File

@ -0,0 +1 @@
base256

View File

@ -1,16 +1,16 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
from json import dumps as json_dumps from json import dumps as json_dumps
# TODO work with links to messages
def cDump(context:EventContext, data:InputMessageData): def cDump(context:EventContext, data:InputMessageData):
if (message := data.quoted): if not (message := data.quoted):
dump_text = json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ") return send_status_400(context, data.user.settings.language)
SendMessage(context, { text = json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ")
"text_html": (f'<pre>{html_escape(dump_text)}</pre>' if message return send_message(context, {"text_html": f'<pre>{html_escape(text)}</pre>'})
else context.endpoint.help_text(data.user.settings.language))})
RegisterModule(name="Dumper", group="Geek", endpoints=[ RegisterModule(name="Dumper", group="Geek", endpoints=[
SafeNamespace(names=["dump"], handler=cDump, quoted=True), SafeNamespace(names=["dump"], handler=cDump, quoted=True),

View File

@ -3,9 +3,9 @@
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ==================================== # # ==================================== #
def cEcho(context:EventContext, data:InputMessageData) -> None: def cEcho(context:EventContext, data:InputMessageData):
if not (text := ObjGet(data, "command.body")): if not (text := data.command.body):
return SendMessage(context, { return send_message(context, {
"text_html": context.endpoint.get_string("empty", data.user.settings.language)}) "text_html": context.endpoint.get_string("empty", data.user.settings.language)})
prefix = f'<a href="{data.message_url}">🗣️</a> ' prefix = f'<a href="{data.message_url}">🗣️</a> '
if len(data.command.tokens) == 2: # text is a single word if len(data.command.tokens) == 2: # text is a single word
@ -18,7 +18,7 @@ def cEcho(context:EventContext, data:InputMessageData) -> None:
# word is not ascii, probably an emoji (altough not necessarily) # word is not ascii, probably an emoji (altough not necessarily)
# so just pass it as is (useful for Telegram emojis) # so just pass it as is (useful for Telegram emojis)
prefix = '' prefix = ''
SendMessage(context, {"text_html": (prefix + html_escape(text))}) return send_message(context, {"text_html": (prefix + html_escape(text))})
RegisterModule(name="Echo", endpoints=[ RegisterModule(name="Echo", endpoints=[
SafeNamespace(names=["echo"], handler=cEcho, body=True), SafeNamespace(names=["echo"], handler=cEcho, body=True),

View File

@ -1,21 +1,21 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
from g4f.client import Client as G4FClient from g4f.client import Client as G4FClient
g4fClient = G4FClient() g4fClient = G4FClient()
def cGpt(context:EventContext, data:InputMessageData) -> None: def cGpt(context:EventContext, data:InputMessageData):
if not (prompt := data.command.body): if not (prompt := data.command.body):
return SendMessage(context, {"text_plain": "You must type some text."}) return send_status_400(context, data.user.settings.language)
output = None output = None
while not output or output.startswith("sorry, 您的ip已由于触发防滥用检测而被封禁,本服务网址是"): # quick fix for a strange ratelimit message while not output or output.startswith("sorry, 您的ip已由于触发防滥用检测而被封禁,本服务网址是"): # quick fix for a strange ratelimit message
output = "" output = ""
for completion in g4fClient.chat.completions.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], stream=True): for completion in g4fClient.chat.completions.create(model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], stream=True):
output += (completion.choices[0].delta.content or "") output += (completion.choices[0].delta.content or "")
return SendMessage(context, {"text_plain": f"[🤖️ GPT]\n\n{output}"}) return send_message(context, {"text_plain": f"[🤖️ GPT]\n\n{output}"})
RegisterModule(name="GPT", endpoints=[ RegisterModule(name="GPT", endpoints=[
SafeNamespace(names=["gpt", "chatgpt"], handler=cGpt, body=True), SafeNamespace(names=["gpt", "chatgpt"], handler=cGpt, body=True),

View File

@ -3,4 +3,6 @@ endpoints:
summary: summary:
en: > en: >
Sends a message to GPT to get back a response. Note: conversations are not yet supported, and this is more standard GPT than ChatGPT, and in general there are many bugs! Sends a message to GPT to get back a response. Note: conversations are not yet supported, and this is more standard GPT than ChatGPT, and in general there are many bugs!
body:
en: Prompt

View File

@ -1,23 +1,21 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
import hashlib import hashlib
def cHash(context:EventContext, data:InputMessageData): def cHash(context:EventContext, data:InputMessageData):
text_input = (data.command.body or (data.quoted and data.quoted.text_plain)) text_input = (data.command.body or (data.quoted and data.quoted.text_plain))
algorithm = data.command.arguments.algorithm algorithm = data.command.arguments.algorithm
language = data.user.settings.language
if not (text_input and (algorithm in hashlib.algorithms_available)): if not (text_input and (algorithm in hashlib.algorithms_available)):
return SendMessage(context, { return send_status_400(context, data.user.settings.language)
"text_html": f'{context.endpoint.help_text(language)}\n\n{context.endpoint.get_string("algorithms", language)}: {hashlib.algorithms_available}'}) return send_message(context, {
hashed = hashlib.new(algorithm, text_input.encode()).hexdigest() "text_html": f"<pre>{html_escape(hashlib.new(algorithm, text_input.encode()).hexdigest())}</pre>"})
return SendMessage(context, {"text_html": f"<pre>{hashed}</pre>"})
RegisterModule(name="Hashing", group="Geek", endpoints=[ RegisterModule(name="Hashing", group="Geek", endpoints=[
SafeNamespace(names=["hash"], handler=cHash, body=False, quoted=False, arguments={ SafeNamespace(names=["hash"], handler=cHash, body=False, quoted=False, arguments={
"algorithm": True, "algorithm": True,
}), }, help_extra=(lambda endpoint, lang: f'{endpoint.get_string("algorithms", lang)}: <code>{"</code>, <code>".join(hashlib.algorithms_available)}</code>.')),
]) ])

View File

@ -1,23 +1,32 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
# TODO: implement /help <commandname> feature
def cHelp(context:EventContext, data:InputMessageData) -> None: def cHelp(context:EventContext, data:InputMessageData) -> None:
text = (context.endpoint.get_string(lang=data.user.settings.language) or '').strip()
language = data.user.settings.language language = data.user.settings.language
for module in Modules: prefix = data.command.prefix
summary = Modules[module].get_string("summary", language) if (endpoint := data.command.arguments.endpoint):
endpoints = Modules[module].endpoints if endpoint[0] in CommandPrefixes:
text += (f"\n\n{module}" + (f": {summary}" if summary else '')) endpoint = endpoint[1:]
for endpoint in endpoints: if endpoint in Endpoints:
summary = Modules[module].get_string(f"endpoints.{endpoint.names[0]}.summary", language) return send_message(context, {"text_html": get_help_text(endpoint, language, prefix)})
text += (f"\n* /{', /'.join(endpoint.names)}" + (f": {summary}" if summary else '')) text = (context.endpoint.get_string(lang=data.user.settings.language) or '').strip()
text = text.strip() for group in ModuleGroups:
SendMessage(context, {"text_html": text}) text += f"\n\n[ {group} ]"
for module in ModuleGroups[group]:
summary = Modules[module].get_string("summary", language)
endpoints = Modules[module].endpoints
text += (f"\n\n{module}" + (f": {summary}" if summary else ''))
for endpoint in endpoints:
summary = Modules[module].get_string(f"endpoints.{endpoint.names[0]}.summary", language)
text += (f"\n* {prefix}{', {prefix}'.join(endpoint.names)}" + (f": {summary}" if summary else ''))
text = text.strip()
return send_message(context, {"text_html": text})
RegisterModule(name="Help", group="Basic", endpoints=[ RegisterModule(name="Help", group="Basic", endpoints=[
SafeNamespace(names=["help"], handler=cHelp), SafeNamespace(names=["help"], handler=cHelp, arguments={
"endpoint": False,
}),
]) ])

View File

@ -1,7 +1,7 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
""" # windog config start # """ """ # windog config start # """
@ -12,10 +12,14 @@ MicrosoftBingSettings = {}
from urlextract import URLExtract from urlextract import URLExtract
from urllib.request import urlopen, Request from urllib.request import urlopen, Request
def RandomHexString(length:int) -> str:
return ''.join([randchoice('0123456789abcdef') for i in range(length)])
def HttpReq(url:str, method:str|None=None, *, body:bytes=None, headers:dict[str, str]={"User-Agent": WebUserAgent}): def HttpReq(url:str, method:str|None=None, *, body:bytes=None, headers:dict[str, str]={"User-Agent": WebUserAgent}):
return urlopen(Request(url, method=method, data=body, headers=headers)) return urlopen(Request(url, method=method, data=body, headers=headers))
def cEmbedded(context:EventContext, data:InputMessageData) -> None: def cEmbedded(context:EventContext, data:InputMessageData):
language = data.user.settings.language
if len(data.command.tokens) >= 2: if len(data.command.tokens) >= 2:
# Find links in command body # Find links in command body
text = (data.text_markdown + ' ' + data.text_plain) text = (data.text_markdown + ' ' + data.text_plain)
@ -23,9 +27,7 @@ def cEmbedded(context:EventContext, data:InputMessageData) -> None:
# Find links in quoted message # Find links in quoted message
text = ((quoted.text_markdown or '') + ' ' + (quoted.text_plain or '') + ' ' + (quoted.text_html or '')) text = ((quoted.text_markdown or '') + ' ' + (quoted.text_plain or '') + ' ' + (quoted.text_html or ''))
else: else:
# TODO Error message return send_status_400(context, language)
return
pass
urls = URLExtract().find_urls(text) urls = URLExtract().find_urls(text)
if len(urls) > 0: if len(urls) > 0:
proto = 'https://' proto = 'https://'
@ -47,55 +49,57 @@ def cEmbedded(context:EventContext, data:InputMessageData) -> None:
elif urlDomain == "vm.tiktok.com": elif urlDomain == "vm.tiktok.com":
urlDomain = "vm.vxtiktok.com" urlDomain = "vm.vxtiktok.com"
url = (urlDomain + '/' + '/'.join(url.split('/')[1:])) url = (urlDomain + '/' + '/'.join(url.split('/')[1:]))
SendMessage(context, {"text_plain": f"{{{proto}{url}}}"}) return send_message(context, {"text_plain": f"{{{proto}{url}}}"})
# else TODO error message? return send_message(context, {"text_plain": "No links found."})
def cWeb(context:EventContext, data:InputMessageData) -> None: def cWeb(context:EventContext, data:InputMessageData):
language = data.user.settings.language
if not (query := data.command.body): if not (query := data.command.body):
return # TODO show message return send_status_400(context, language)
try: try:
QueryUrl = urlparse.quote(query) query_url = urlparse.quote(query)
Req = HttpReq(f'https://html.duckduckgo.com/html?q={QueryUrl}') request = HttpReq(f'https://html.duckduckgo.com/html?q={query_url}')
Caption = f'🦆🔎 "{query}": https://duckduckgo.com/?q={QueryUrl}\n\n' caption = f'🦆🔎 "{query}": https://duckduckgo.com/?q={query_url}\n\n'
Index = 0 index = 0
for Line in Req.read().decode().replace('\t', ' ').splitlines(): for line in request.read().decode().replace('\t', ' ').splitlines():
if ' class="result__a" ' in Line and ' href="//duckduckgo.com/l/?uddg=' in Line: if ' class="result__a" ' in line and ' href="//duckduckgo.com/l/?uddg=' in line:
Index += 1 index += 1
Link = urlparse.unquote(Line.split(' href="//duckduckgo.com/l/?uddg=')[1].split('&amp;rut=')[0]) link = urlparse.unquote(line.split(' href="//duckduckgo.com/l/?uddg=')[1].split('&amp;rut=')[0])
Title = Line.strip().split('</a>')[0].strip().split('</span>')[-1].strip().split('>') title = line.strip().split('</a>')[0].strip().split('</span>')[-1].strip().split('>')
if len(Title) > 1: if len(title) > 1:
Title = html_unescape(Title[1].strip()) title = html_unescape(title[1].strip())
Caption += f'[{Index}] {Title} : {{{Link}}}\n\n' caption += f'[{index}] {title} : {{{link}}}\n\n'
else: else:
continue continue
SendMessage(context, {"TextPlain": f'{Caption}...'}) return send_message(context, {"text_plain": f'{caption}...'})
except Exception: except Exception:
raise return send_status_error(context, language)
def cImages(context:EventContext, data:InputMessageData) -> None: def cImages(context:EventContext, data:InputMessageData):
pass pass
def cNews(context:EventContext, data:InputMessageData) -> None: def cNews(context:EventContext, data:InputMessageData):
pass pass
def cTranslate(context:EventContext, data:InputMessageData) -> None: def cTranslate(context:EventContext, data:InputMessageData):
language = data.user.settings.language
instances = ["lingva.ml", "lingva.lunar.icu"] instances = ["lingva.ml", "lingva.lunar.icu"]
language_to = data.command.arguments["language_to"] language_to = data.command.arguments.language_to
text_input = (data.command.body or (data.quoted and data.quoted.text_plain)) text_input = (data.command.body or (data.quoted and data.quoted.text_plain))
if not (text_input and language_to): if not (text_input and language_to):
return SendMessage(context, {"TextPlain": f"Usage: /translate <to language> <text>"}) return send_status_400(context, language)
try: try:
result = json.loads(HttpReq(f'https://{randchoice(instances)}/api/v1/auto/{language_to}/{urlparse.quote(text_input)}').read()) result = json.loads(HttpReq(f'https://{randchoice(instances)}/api/v1/auto/{language_to}/{urlparse.quote(text_input)}').read())
SendMessage(context, {"TextPlain": f"[{result['info']['detectedSource']} (auto) -> {language_to}]\n\n{result['translation']}"}) return send_message(context, {"text_plain": f"[{result['info']['detectedSource']} (auto) -> {language_to}]\n\n{result['translation']}"})
except Exception: except Exception:
raise return send_status_error(context, language)
# unsplash source appears to be deprecated! <https://old.reddit.com/r/unsplash/comments/s13x4h/what_happened_to_sourceunsplashcom/l65epl8/> # 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.command.body)}') # Req = HttpReq(f'https://source.unsplash.com/random/?{urlparse.quote(data.command.body)}')
# ImgUrl = Req.geturl().split('?')[0] # ImgUrl = Req.geturl().split('?')[0]
# SendMessage(context, { # send_message(context, {
# "TextPlain": f'{{{ImgUrl}}}', # "TextPlain": f'{{{ImgUrl}}}',
# "TextMarkdown": MarkdownCode(ImgUrl, True), # "TextMarkdown": MarkdownCode(ImgUrl, True),
# "Media": Req.read(), # "Media": Req.read(),
@ -103,20 +107,21 @@ def cTranslate(context:EventContext, data:InputMessageData) -> None:
# except Exception: # except Exception:
# raise # raise
def cSafebooru(context:EventContext, data:InputMessageData) -> None: def cSafebooru(context:EventContext, data:InputMessageData):
ApiUrl = 'https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags=' language = data.user.settings.language
api_url = 'https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=100&tags='
try: try:
img_id, img_url = None, None img_id, img_url = None, None
if (query := data.command.body): if (query := data.command.body):
for i in range(7): # retry a bunch of times if we can't find a really random result for i in range(7): # retry a bunch of times if we can't find a really random result
ImgUrls = HttpReq(f'{ApiUrl}md5:{RandHexStr(3)}%20{urlparse.quote(query)}').read().decode().split(' file_url="')[1:] img_urls = HttpReq(f'{api_url}md5:{RandomHexString(3)}%20{urlparse.quote(query)}').read().decode().split(' file_url="')[1:]
if ImgUrls: if img_urls:
break break
if not ImgUrls: # literal search if not img_urls: # literal search
ImgUrls = HttpReq(f'{ApiUrl}{urlparse.quote(query)}').read().decode().split(' file_url="')[1:] img_urls = HttpReq(f'{api_url}{urlparse.quote(query)}').read().decode().split(' file_url="')[1:]
if not ImgUrls: if not img_urls:
return SendMessage(context, {"Text": "Error: Could not get any result from Safebooru."}) return send_status(context, 404, language, "Could not get any result from Safebooru.", summary=False)
ImgXml = choice(ImgUrls) ImgXml = choice(img_urls)
img_url = ImgXml.split('"')[0] img_url = ImgXml.split('"')[0]
img_id = ImgXml.split(' id="')[1].split('"')[0] img_id = ImgXml.split(' id="')[1].split('"')[0]
else: else:
@ -127,14 +132,14 @@ def cSafebooru(context:EventContext, data:InputMessageData) -> None:
img_id = img_url.split('?')[-1] img_id = img_url.split('?')[-1]
break break
if img_url: if img_url:
SendMessage(context, OutputMessageData( return send_message(context, OutputMessageData(
text_plain=f"[{img_id}]\n{{{img_url}}}", text_plain=f"[{img_id}]\n{{{img_url}}}",
text_html=f"[<code>{img_id}</code>]\n<pre>{img_url}</pre>", text_html=f"[<code>{img_id}</code>]\n<pre>{img_url}</pre>",
media={"url": img_url})) media={"url": img_url}))
else: else:
pass return send_status_400(context, language)
except Exception as error: except Exception:
raise return send_status_error(context, language)
RegisterModule(name="Internet", endpoints=[ RegisterModule(name="Internet", endpoints=[
SafeNamespace(names=["embedded"], handler=cEmbedded, body=False, quoted=False), SafeNamespace(names=["embedded"], handler=cEmbedded, body=False, quoted=False),

View File

@ -1,9 +1,9 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
def mMultifun(context:EventContext, data:InputMessageData) -> None: def mMultifun(context:EventContext, data:InputMessageData):
reply_to = None reply_to = None
fun_strings = {} fun_strings = {}
for key in ("empty", "bot", "self", "others"): for key in ("empty", "bot", "self", "others"):
@ -19,7 +19,7 @@ def mMultifun(context:EventContext, data:InputMessageData) -> None:
else: else:
if fun_strings["empty"]: if fun_strings["empty"]:
text = choice(fun_strings["empty"]) text = choice(fun_strings["empty"])
SendMessage(context, {"text_html": text, "ReplyTo": reply_to}) return send_message(context, {"text_html": text, "ReplyTo": reply_to})
RegisterModule(name="Multifun", endpoints=[ RegisterModule(name="Multifun", endpoints=[
SafeNamespace(names=["hug", "pat", "poke", "cuddle", "hands", "floor", "sessocto"], handler=mMultifun), SafeNamespace(names=["hug", "pat", "poke", "cuddle", "hands", "floor", "sessocto"], handler=mMultifun),

View File

@ -3,12 +3,17 @@
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ==================================== # # ==================================== #
def mPercenter(context:EventContext, data:InputMessageData) -> None: # NOTE: with this implementation there is a 1/100 probability (high!) of result 100.00, which is not always ideal
SendMessage(context, {"text_html": (context.endpoint.get_string( def RandomPercentString() -> str:
num = randint(0,100)
return (f'{num}.00' if num == 100 else f'{num}.{randint(0,9)}{randint(0,9)}')
def mPercenter(context:EventContext, data:InputMessageData):
return send_message(context, {"text_html": (context.endpoint.get_string(
("done" if data.command.body else "empty"), ("done" if data.command.body else "empty"),
data.user.settings.language data.user.settings.language
) or context.endpoint.help_text(data.user.settings.language) ) or context.endpoint.get_help_text(data.user.settings.language)
).format(RandPercent(), data.command.body)}) ).format(RandomPercentString(), data.command.body)})
RegisterModule(name="Percenter", endpoints=[ RegisterModule(name="Percenter", endpoints=[
SafeNamespace(names=["wish", "level"], handler=mPercenter, body=True), SafeNamespace(names=["wish", "level"], handler=mPercenter, body=True),

View File

@ -1,7 +1,7 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
""" # windog config start # """ """ # windog config start # """
@ -22,7 +22,7 @@ def getSelenium() -> tuple[int, Driver]|bool:
if index not in currentSeleniumDrivers: if index not in currentSeleniumDrivers:
currentSeleniumDrivers.append(index) currentSeleniumDrivers.append(index)
break break
return (index, Driver(uc=True, headless2=True, user_data_dir=f"./Selenium-WinDog/{index}")) return (index, Driver(uc=True, headless2=True, user_data_dir=f"./Data/Selenium/{index}"))
def closeSelenium(index:int, driver:Driver) -> None: def closeSelenium(index:int, driver:Driver) -> None:
if driver: if driver:
@ -30,19 +30,19 @@ def closeSelenium(index:int, driver:Driver) -> None:
driver.close() driver.close()
driver.quit() driver.quit()
except: except:
Log(format_exc()) app_log(format_exc())
if index: if index:
currentSeleniumDrivers.remove(index) currentSeleniumDrivers.remove(index)
def cDalleSelenium(context:EventContext, data:InputMessageData) -> None: def cDalleSelenium(context:EventContext, data:InputMessageData):
warning_text = "has been blocked by Microsoft because it violates their content policy. Further attempts might lead to a ban on your profile. Please review the Code of Conduct for Image Creator in this picture or at https://www.bing.com/new/termsofuseimagecreator#content-policy." warning_text = "has been blocked by Microsoft because it violates their content policy. Further attempts might lead to a ban on your profile. Please review the Code of Conduct for Image Creator in this picture or at https://www.bing.com/new/termsofuseimagecreator#content-policy."
if not (prompt := data.command.body): if not (prompt := data.command.body):
return SendMessage(context, {"Text": "Please tell me what to generate."}) return send_message(context, {"text_plain": "Please tell me what to generate."})
driver_index, driver = None, None driver_index, driver = None, None
try: try:
driver = getSelenium() driver = getSelenium()
if not driver: if not driver:
return SendMessage(context, {"Text": "Couldn't access a web scraping VM as they are all busy. Please try again later."}) return send_message(context, {"text_plain": "Couldn't access a web scraping VM as they are all busy. Please try again later."})
driver_index, driver = driver driver_index, driver = driver
driver.get("https://www.bing.com/images/create/") driver.get("https://www.bing.com/images/create/")
driver.refresh() driver.refresh()
@ -50,11 +50,11 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
driver.find_element('form a[role="button"]').submit() driver.find_element('form a[role="button"]').submit()
try: try:
driver.find_element('img.gil_err_img[alt="Content warning"]') driver.find_element('img.gil_err_img[alt="Content warning"]')
SendMessage(context, {"Text": f"Content warning: This prompt {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}}) send_message(context, {"text_plain": f"Content warning: This prompt {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}})
return closeSelenium(driver_index, driver) return closeSelenium(driver_index, driver)
except Exception: # warning element was not found, we should be good except Exception: # warning element was not found, we should be good
pass pass
SendMessage(context, {"Text": "Request sent successfully, please wait..."}) send_message(context, {"text_plain": "Request sent successfully, please wait..."})
retry_index = 3 retry_index = 3
while retry_index < 12: while retry_index < 12:
# note that sometimes generation can still fail and we will never get any image! # note that sometimes generation can still fail and we will never get any image!
@ -64,8 +64,9 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
if not len(img_list): if not len(img_list):
try: try:
driver.find_element('img.gil_err_img[alt="Unsafe image content detected"]') driver.find_element('img.gil_err_img[alt="Unsafe image content detected"]')
SendMessage(context, {"Text": f"Unsafe image content detected: This result {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}}) result = send_message(context, {"text_plain": f"Unsafe image content detected: This result {warning_text}", "media": {"bytes": open("./Assets/ImageCreator-CodeOfConduct.png", 'rb').read()}})
return closeSelenium(driver_index, driver) closeSelenium(driver_index, driver)
return result
except: # no error is present, so we just have to wait more for the images except: # no error is present, so we just have to wait more for the images
continue continue
img_array = [] img_array = []
@ -73,30 +74,32 @@ def cDalleSelenium(context:EventContext, data:InputMessageData) -> None:
img_url = img_url.get_attribute("src").split('?')[0] img_url = img_url.get_attribute("src").split('?')[0]
img_array.append({"url": img_url}) #, "bytes": HttpReq(img_url).read()}) img_array.append({"url": img_url}) #, "bytes": HttpReq(img_url).read()})
page_url = driver.current_url.split('?')[0] page_url = driver.current_url.split('?')[0]
SendMessage(context, OutputMessageData( result = send_message(context, OutputMessageData(
text_plain=f'"{prompt}"\n{{{page_url}}}', text_plain=f'"{prompt}"\n{{{page_url}}}',
text_html=f'"<i>{html_escape(prompt)}</i>"\n<pre>{page_url}</pre>', text_html=f'"<i>{html_escape(prompt)}</i>"\n<pre>{page_url}</pre>',
media=img_array)) media=img_array))
return closeSelenium(driver_index, driver) closeSelenium(driver_index, driver)
return result
raise Exception("VM timed out.") raise Exception("VM timed out.")
except Exception as error: except Exception as error:
Log(format_exc()) app_log(format_exc())
SendMessage(context, {"TextPlain": "An unexpected error occurred."}) result = send_message(context, {"text_plain": "An unexpected error occurred."})
closeSelenium(driver_index, driver) closeSelenium(driver_index, driver)
return result
def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None: def cCraiyonSelenium(context:EventContext, data:InputMessageData):
if not (prompt := data.command.body): if not (prompt := data.command.body):
return SendMessage(context, {"Text": "Please tell me what to generate."}) return send_message(context, {"text_plain": "Please tell me what to generate."})
driver_index, driver = None, None driver_index, driver = None, None
try: try:
driver = getSelenium() driver = getSelenium()
if not driver: if not driver:
return SendMessage(context, {"Text": "Couldn't access a web scraping VM as they are all busy. Please try again later."}) return send_message(context, {"text_plain": "Couldn't access a web scraping VM as they are all busy. Please try again later."})
driver_index, driver = driver driver_index, driver = driver
driver.get("https://www.craiyon.com/") driver.get("https://www.craiyon.com/")
driver.find_element('textarea#prompt').send_keys(prompt) driver.find_element('textarea#prompt').send_keys(prompt)
driver.execute_script("arguments[0].click();", driver.find_element('button#generateButton')) driver.execute_script("arguments[0].click();", driver.find_element('button#generateButton'))
SendMessage(context, {"Text": "Request sent successfully, please wait up to 60 seconds..."}) send_message(context, {"text_plain": "Request sent successfully, please wait up to 60 seconds..."})
retry_index = 3 retry_index = 3
while retry_index < 16: while retry_index < 16:
time.sleep(retry_index := retry_index + 1) time.sleep(retry_index := retry_index + 1)
@ -106,17 +109,19 @@ def cCraiyonSelenium(context:EventContext, data:InputMessageData) -> None:
img_array = [] img_array = []
for img_elem in img_list: for img_elem in img_list:
img_array.append({"url": img_elem.get_attribute("src")}) #, "bytes": HttpReq(img_url).read()}) img_array.append({"url": img_elem.get_attribute("src")}) #, "bytes": HttpReq(img_url).read()})
SendMessage(context, { result = send_message(context, {
"text_plain": f'"{prompt}"', "text_plain": f'"{prompt}"',
"text_html": f'"<i>{html_escape(prompt)}</i>"', "text_html": f'"<i>{html_escape(prompt)}</i>"',
"media": img_array, "media": img_array,
}) })
return closeSelenium(driver_index, driver) closeSelenium(driver_index, driver)
return result
raise Exception("VM timed out.") raise Exception("VM timed out.")
except Exception as error: except Exception as error:
Log(format_exc()) app_log(format_exc())
SendMessage(context, {"TextPlain": "An unexpected error occurred."}) result = send_message(context, {"text_plain": "An unexpected error occurred."})
closeSelenium(driver_index, driver) closeSelenium(driver_index, driver)
return result
RegisterModule(name="Scrapers", endpoints=[ RegisterModule(name="Scrapers", endpoints=[
SafeNamespace(names=["dalle"], handler=cDalleSelenium, body=True), SafeNamespace(names=["dalle"], handler=cDalleSelenium, body=True),

View File

@ -1,7 +1,7 @@
# ================================== # # ==================================== #
# WinDog multi-purpose chatbot # # WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ================================== # # ==================================== #
""" # windog config start # """ """ # windog config start # """
@ -23,11 +23,10 @@ def luaAttributeFilter(obj, attr_name, is_setting):
raise AttributeError("Access Denied.") raise AttributeError("Access Denied.")
# TODO make print behave the same as normal Lua, and expose a function for printing without newlines # TODO make print behave the same as normal Lua, and expose a function for printing without newlines
def cLua(context:EventContext, data:InputMessageData) -> None: def cLua(context:EventContext, data:InputMessageData):
# TODO update quoted api getting # TODO update quoted api getting
scriptText = (data.command.body or (data.quoted and data.quoted.text_plain)) if not (script_text := (data.command.body or (data.quoted and data.quoted.text_plain))):
if not scriptText: return send_message(context, {"text_plain": "You must provide some Lua code to execute."})
return SendMessage(context, {"text_plain": "You must provide some Lua code to execute."})
luaRuntime = NewLuaRuntime(max_memory=LuaMemoryLimit, register_eval=False, register_builtins=False, attribute_filter=luaAttributeFilter) luaRuntime = NewLuaRuntime(max_memory=LuaMemoryLimit, register_eval=False, register_builtins=False, attribute_filter=luaAttributeFilter)
luaRuntime.eval(f"""(function() luaRuntime.eval(f"""(function()
_windog = {{ stdout = "" }} _windog = {{ stdout = "" }}
@ -44,13 +43,12 @@ end)()""")
elif key not in LuaGlobalsWhitelist: elif key not in LuaGlobalsWhitelist:
del luaRuntime.globals()[key] del luaRuntime.globals()[key]
try: try:
textOutput = ("[ʟᴜᴀ ꜱᴛᴅᴏᴜᴛ]\n\n" + luaRuntime.eval(f"""(function() return send_message(context, {"text_plain": ("[ʟᴜᴀ ꜱᴛᴅᴏᴜᴛ]\n\n" + luaRuntime.eval(f"""(function()
_windog.scriptout = (function()\n{scriptText}\nend)() _windog.scriptout = (function()\n{script_text}\nend)()
return _windog.stdout .. (_windog.scriptout or '') return _windog.stdout .. (_windog.scriptout or '')
end)()""")) end)()"""))})
except (LuaError, LuaSyntaxError) as error: except (LuaError, LuaSyntaxError):
Log(textOutput := ("Lua Error: " + str(error))) return send_status_error(context, data.user.settings.language)
SendMessage(context, {"TextPlain": textOutput})
RegisterModule(name="Scripting", group="Geek", endpoints=[ RegisterModule(name="Scripting", group="Geek", endpoints=[
SafeNamespace(names=["lua"], handler=cLua, body=False, quoted=False), SafeNamespace(names=["lua"], handler=cLua, body=False, quoted=False),

View File

@ -3,8 +3,8 @@
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ==================================== # # ==================================== #
def cStart(context:EventContext, data:InputMessageData) -> None: def cStart(context:EventContext, data:InputMessageData):
SendMessage(context, OutputMessageData( return send_message(context, OutputMessageData(
text_html=context.endpoint.get_string( text_html=context.endpoint.get_string(
"start", data.user.settings.language).format(data.user.name))) "start", data.user.settings.language).format(data.user.name)))

View File

@ -13,19 +13,28 @@ ExecAllowed = {"date": False, "fortune": False, "neofetch": True, "uptime": Fals
import subprocess import subprocess
from re import compile as re_compile from re import compile as re_compile
def cExec(context:EventContext, data:InputMessageData) -> None: def cExec(context:EventContext, data:InputMessageData):
if not (len(data.command.tokens) >= 2 and data.command.tokens[1].lower() in ExecAllowed): language = data.user.settings.language
return SendMessage(context, {"text_plain": "This feature is not implemented [Security Issue]."}) if not (len(data.command.tokens) >= 2):
return send_status_400(context, language)
if not data.command.tokens[1].lower() in ExecAllowed:
return send_status(context, 404, language, context.endpoint.get_string("statuses.404", language), summary=False)
command = data.command.tokens[1].lower() command = data.command.tokens[1].lower()
output = subprocess.run( output = subprocess.run(
("sh", "-c", f"export PATH=$PATH:/usr/games; {command}"), ("sh", "-c", f"export PATH=$PATH:/usr/games; {command}"),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode() stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.decode()
# <https://stackoverflow.com/a/14693789> # <https://stackoverflow.com/a/14693789>
text = (re_compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").sub('', output)) text = (re_compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])").sub('', output))
SendMessage(context, OutputMessageData( return send_message(context, {"text_html": f'<pre>{html_escape(text)}</pre>'})
text_plain=text, text_html=f"<pre>{html_escape(text)}</pre>"))
def cRestart(context:EventContext, data:InputMessageData):
if (data.user.id not in AdminIds) and (data.user.tag not in AdminIds):
return send_message(context, {"text_plain": "Permission denied."})
open("./.WinDog.Restart.lock", 'w').close()
return send_message(context, {"text_plain": "Bot restart queued."})
RegisterModule(name="System", endpoints=[ RegisterModule(name="System", endpoints=[
SafeNamespace(names=["exec"], handler=cExec, body=True), SafeNamespace(names=["exec"], handler=cExec, body=True),
SafeNamespace(names=["restart"], handler=cRestart),
]) ])

View File

@ -2,4 +2,7 @@ endpoints:
exec: exec:
summary: summary:
en: Execute a system command from the allowed ones and return stdout+stderr. en: Execute a system command from the allowed ones and return stdout+stderr.
statuses:
404:
en: The requested command is not available.

19
RunWinDog.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
windog_start(){
rm -f ./.WinDog.Restart.lock
python3 ./WinDog.py &
}
cd "$( dirname "$( realpath "$0" )" )"
windog_start
while true
do
if [ -f ./.WinDog.Restart.lock ]
then
kill "$!"
windog_start
fi
sleep 5
done

View File

@ -1,3 +0,0 @@
#!/bin/sh
cd "$( dirname "$( realpath "$0" )" )"
python3 ./WinDog.py

218
WinDog.py
View File

@ -11,6 +11,7 @@ from html import escape as html_escape, unescape as html_unescape
from os import listdir from os import listdir
from os.path import isfile, isdir from os.path import isfile, isdir
from random import choice, choice as randchoice, randint from random import choice, choice as randchoice, randint
from sys import exc_info as sys_exc_info
from threading import Thread from threading import Thread
from traceback import format_exc, format_exc as traceback_format_exc from traceback import format_exc, format_exc as traceback_format_exc
from urllib import parse as urlparse, parse as urllib_parse from urllib import parse as urlparse, parse as urllib_parse
@ -32,23 +33,35 @@ def ObjectUnion(*objects:object, clazz:object=None):
dikt[key] = value dikt[key] = value
return (clazz or auto_clazz)(**dikt) return (clazz or auto_clazz)(**dikt)
def Log(text:str, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None: def ObjectClone(obj:object):
endline = '\n' return ObjectUnion(obj, {});
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 call_or_return(obj:any) -> any:
return (obj() if callable(obj) else obj)
def SureArray(array:any) -> list|tuple: def SureArray(array:any) -> list|tuple:
return (array if type(array) in [list, tuple] else [array]) return (array if type(array) in [list, tuple] else [array])
def ObjGet(node:object, query:str, /) -> any: def app_log(text:str=None, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None:
if not text:
text = get_exception_text(full=True)
endline = '\n'
if newline == False or (inline and newline == None):
endline = ''
text = (str(text) if inline else f"[{level}] [{time.ctime()}] [{int(time.time())}] {text}")
if LogToConsole:
print(text, end=endline)
if LogToFile:
open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Data/Log.txt"), 'a').write(text + endline)
def get_exception_text(full:bool=False):
exc_type, exc_value, exc_traceback = sys_exc_info()
text = f'{exc_type.__qualname__}: {exc_value}'
if full:
text = f'@{exc_traceback.tb_frame.f_code.co_name}:{exc_traceback.tb_lineno} {text}'
return text
def call_or_return(obj:any, *args) -> any:
return (obj(*args) if callable(obj) else obj)
def obj_get(node:object, query:str, /) -> any:
for key in query.split('.'): for key in query.split('.'):
if hasattr(node, "__getitem__") and node.__getitem__: if hasattr(node, "__getitem__") and node.__getitem__:
# dicts and such # dicts and such
@ -67,19 +80,30 @@ def ObjGet(node:object, query:str, /) -> any:
def good_yaml_load(text:str): def good_yaml_load(text:str):
return yaml_load(text.replace("\t", " "), Loader=yaml_BaseLoader) return yaml_load(text.replace("\t", " "), Loader=yaml_BaseLoader)
def get_string(bank:dict, query:str|dict, lang:str=None) -> str|list[str]|None: def get_string(bank:dict, query:str, lang:str=None) -> str|list[str]|None:
if not (result := ObjGet(bank, f"{query}.{lang or DefaultLanguage}")): if type(result := obj_get(bank, query)) != str:
if not (result := ObjGet(bank, f"{query}.en")): if not (result := obj_get(bank, f"{query}.{lang or DefaultLanguage}")):
result = ObjGet(bank, query) if not (result := obj_get(bank, f"{query}.en")):
result = obj_get(bank, query)
if result:
result = result.strip()
return result return result
def help_text(endpoint, lang:str=None) -> str: def get_help_text(endpoint, lang:str=None, prefix:str=None) -> str:
if type(endpoint) == str:
endpoint = instanciate_endpoint(endpoint, prefix)
global_string = (lambda query: get_string(GlobalStrings, query, lang)) global_string = (lambda query: get_string(GlobalStrings, query, lang))
text = f'{endpoint.get_string("summary", lang) or ""}\n\n{global_string("usage")}:' text = f'{endpoint.get_string("summary", lang) or ""}\n\n{global_string("usage")}: {prefix or ""}{endpoint.name}'
if endpoint.arguments: if endpoint.arguments:
for argument in endpoint.arguments: for argument in endpoint.arguments:
if endpoint.arguments[argument]: if not ((endpoint.body != None) and (endpoint.arguments[argument] == False)):
text += f' &lt;{endpoint.get_string(f"arguments.{argument}", lang) or endpoint.module.get_string(f"arguments.{argument}", lang) or argument}&gt;' argument_help = (endpoint.get_string(f"arguments.{argument}", lang)
or endpoint.module.get_string(f"arguments.{argument}", lang)
or argument)
if endpoint.arguments[argument] == True:
text += f' &lt;{argument_help}&gt;'
elif endpoint.arguments[argument] == False:
text += f' [{argument_help}]'
body_help = (endpoint.get_string("body", lang) or endpoint.module.get_string("body", lang)) body_help = (endpoint.get_string("body", lang) or endpoint.module.get_string("body", lang))
quoted_help = (global_string("quoted_message") + (f': {body_help}' if body_help else '')) quoted_help = (global_string("quoted_message") + (f': {body_help}' if body_help else ''))
if not body_help: if not body_help:
@ -95,51 +119,38 @@ def help_text(endpoint, lang:str=None) -> str:
text += f' &lt;{quoted_help}&gt;' text += f' &lt;{quoted_help}&gt;'
elif endpoint.quoted == False: elif endpoint.quoted == False:
text += f' [{quoted_help}]' text += f' [{quoted_help}]'
if (extra := call_or_return(endpoint.help_extra, endpoint, lang)):
text += f'\n\n{extra}'
return text return text
def strip_url_scheme(url:str) -> str: def strip_url_scheme(url:str) -> str:
tokens = urlparse.urlparse(url) tokens = urlparse.urlparse(url)
return f"{tokens.netloc}{tokens.path}" return f"{tokens.netloc}{tokens.path}"
def RandPercent() -> int: def TextCommandData(text:str, platform:str) -> CommandData|None:
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 GetUserSettings(user_id:str) -> SafeNamespace|None:
try:
return SafeNamespace(**EntitySettings.select().join(User).where(User.id == user_id).dicts().get())
except EntitySettings.DoesNotExist:
return None
def ParseCommand(text:str, platform:str) -> SafeNamespace|None:
if not text: if not text:
return None return None
text = text.strip() text = text.strip()
try: # ensure text is a non-empty command try: # ensure text is non-empty and an actual command
if not (text[0] in CmdPrefixes and text[1:].strip()): if not (text[0] in CommandPrefixes and text[1:].strip()):
return None return None
except IndexError: except IndexError:
return None return None
command = SafeNamespace() command = SafeNamespace()
command.tokens = text.split() command.tokens = text.split()
command.prefix = command.tokens[0][0]
command.name, command_target = (command.tokens[0][1:].lower().split('@') + [''])[:2] command.name, command_target = (command.tokens[0][1:].lower().split('@') + [''])[:2]
if command_target and not (command_target == call_or_return(Platforms[platform].agent_info).tag.lower()): if command_target and not (command_target == call_or_return(Platforms[platform].agent_info).tag.lower()):
return None return None
command.body = text[len(command.tokens[0]):].strip() command.body = text[len(command.tokens[0]):].strip()
if command.name not in Endpoints: if not (endpoint := obj_get(Endpoints, command.name)):
return command return command # TODO shouldn't this return None?
if (endpoint_arguments := Endpoints[command.name].arguments): if (endpoint.arguments):
command.arguments = SafeNamespace() command.arguments = SafeNamespace()
index = 1 index = 1
for key in endpoint_arguments: for key in endpoint.arguments:
if not endpoint_arguments[key]: if (endpoint.body != None) and (endpoint.arguments[key] == False):
continue # skip optional (False) arguments for now, they will be implemented later continue # skip optional (False) arguments for now if command expects a body, they will be implemented later
try: try:
value = command.tokens[index] value = command.tokens[index]
command.body = command.body[len(value):].strip() command.body = command.body[len(value):].strip()
@ -151,7 +162,7 @@ def ParseCommand(text:str, platform:str) -> SafeNamespace|None:
def OnInputMessageParsed(data:InputMessageData) -> None: def OnInputMessageParsed(data:InputMessageData) -> None:
dump_message(data, prefix='> ') dump_message(data, prefix='> ')
handle_bridging(SendMessage, data, from_sent=False) handle_bridging(send_message, data, from_sent=False)
update_user_db(data.user) update_user_db(data.user)
def OnOutputMessageSent(output_data:OutputMessageData, input_data:InputMessageData, from_sent:bool) -> None: def OnOutputMessageSent(output_data:OutputMessageData, input_data:InputMessageData, from_sent:bool) -> None:
@ -159,14 +170,13 @@ def OnOutputMessageSent(output_data:OutputMessageData, input_data:InputMessageDa
output_data = ObjectUnion(output_data, {"room": input_data.room}) output_data = ObjectUnion(output_data, {"room": input_data.room})
dump_message(output_data, prefix=f'<{"*" if from_sent else " "}') dump_message(output_data, prefix=f'<{"*" if from_sent else " "}')
if not from_sent: if not from_sent:
handle_bridging(SendMessage, output_data, from_sent=True) handle_bridging(send_message, output_data, from_sent=True)
# TODO: fix to send messages to different rooms, this overrides destination data but that gives problems with rebroadcasting the bot's own messages
def handle_bridging(method:callable, data:MessageData, from_sent:bool): def handle_bridging(method:callable, data:MessageData, from_sent:bool):
if data.user: if data.user:
if (text_plain := ObjGet(data, "text_plain")): if (text_plain := obj_get(data, "text_plain")):
text_plain = f"<{data.user.name}>: {text_plain}" text_plain = f"<{data.user.name}>: {text_plain}"
if (text_html := ObjGet(data, "text_html")): if (text_html := obj_get(data, "text_html")):
text_html = (urlparse.quote(f"<{data.user.name}>: ") + text_html) text_html = (urlparse.quote(f"<{data.user.name}>: ") + text_html)
for bridge in BridgesConfig: for bridge in BridgesConfig:
if data.room.id not in bridge: if data.room.id not in bridge:
@ -200,9 +210,28 @@ def dump_message(data:InputMessageData, prefix:str='') -> None:
if DumpToConsole: if DumpToConsole:
print(text, data) print(text, data)
if DumpToFile: if DumpToFile:
open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Dump.txt"), 'a').write(text + '\n') open((DumpToFile if (DumpToFile and type(DumpToFile) == str) else "./Data/Dump.txt"), 'a').write(text + '\n')
def SendMessage(context:EventContext, data:OutputMessageData, from_sent:bool=False) -> None: def send_status(context:EventContext, code:int, lang:str=None, extra:str=None, preamble:bool=True, summary:bool=True):
global_string = (lambda query: get_string(GlobalStrings, query, lang))
summary_text = (global_string(f"statuses.{code}.summary") or '')
return send_message(context, {"text_html": (
(((f'{global_string(f"statuses.{code}.icon")} {global_string("error") if code >= 400 else ""}'.strip()
+ f' {code}: {global_string(f"statuses.{code}.title")}. {summary_text if summary else ""}').strip()) if preamble else '')
+ '\n\n' + (extra or "")).strip()})
def send_status_400(context:EventContext, lang:str=None, extra:str=None):
return send_status(context, 400, lang,
f'{context.endpoint.get_help_text(lang)}\n\n{extra or ""}', preamble=False, summary=False)
def send_status_error(context:EventContext, lang:str=None, code:int=500, extra:str=None):
result = send_status(context, code, lang,
f'{html_escape(get_exception_text())}\n\n{extra or ""}')
app_log()
return result
def send_message(context:EventContext, data:OutputMessageData, *, from_sent:bool=False):
context = ObjectClone(context)
data = (OutputMessageData(**data) if type(data) == dict else data) data = (OutputMessageData(**data) if type(data) == dict else data)
if data.text_html and not data.text_plain: if data.text_html and not data.text_plain:
data.text_plain = BeautifulSoup(data.text_html, "html.parser").get_text() data.text_plain = BeautifulSoup(data.text_html, "html.parser").get_text()
@ -229,15 +258,18 @@ def SendMessage(context:EventContext, data:OutputMessageData, from_sent:bool=Fal
OnOutputMessageSent(data, context.data, from_sent) OnOutputMessageSent(data, context.data, from_sent)
return result return result
def SendNotice(context:EventContext, data) -> None: def send_notice(context:EventContext, data):
pass pass
def DeleteMessage(context:EventContext, data) -> None: def edit_message(context:EventContext, data:MessageData):
pass
def delete_message(context:EventContext, data:MessageData):
pass pass
def RegisterPlatform(name:str, main:callable, sender:callable, linker:callable=None, *, event_class=None, manager_class=None, agent_info=None) -> None: def RegisterPlatform(name:str, main:callable, sender:callable, linker:callable=None, *, event_class=None, manager_class=None, agent_info=None) -> None:
Platforms[name.lower()] = SafeNamespace(name=name, main=main, sender=sender, linker=linker, event_class=event_class, manager_class=manager_class, agent_info=agent_info) Platforms[name.lower()] = SafeNamespace(name=name, main=main, sender=sender, linker=linker, event_class=event_class, manager_class=manager_class, agent_info=agent_info)
Log(f"{name}, ", inline=True) app_log(f"{name}, ", inline=True)
def RegisterModule(name:str, endpoints:dict, *, group:str|None=None) -> None: def RegisterModule(name:str, endpoints:dict, *, group:str|None=None) -> None:
module = SafeNamespace(group=group, endpoints=endpoints, get_string=(lambda query, lang=None: None)) module = SafeNamespace(group=group, endpoints=endpoints, get_string=(lambda query, lang=None: None))
@ -245,27 +277,40 @@ def RegisterModule(name:str, endpoints:dict, *, group:str|None=None) -> None:
module.strings = good_yaml_load(open(file, 'r').read()) module.strings = good_yaml_load(open(file, 'r').read())
module.get_string = (lambda query, lang=None: get_string(module.strings, query, lang)) module.get_string = (lambda query, lang=None: get_string(module.strings, query, lang))
Modules[name] = module Modules[name] = module
Log(f"{name}, ", inline=True) if group not in ModuleGroups:
ModuleGroups[group] = []
ModuleGroups[group].append(name)
app_log(f"{name}, ", inline=True)
for endpoint in endpoints: for endpoint in endpoints:
endpoint.module = module endpoint.module = module
for name in endpoint.names: for name in endpoint.names:
Endpoints[name] = endpoint Endpoints[name] = endpoint
def CallEndpoint(name:str, context:EventContext, data:InputMessageData): def instanciate_endpoint(name:str, prefix:str):
endpoint = Endpoints[name] if not (endpoint := obj_get(Endpoints, name)):
return None
endpoint = ObjectClone(endpoint)
endpoint.name = name
endpoint.get_string = (lambda query=name, lang=None:
endpoint.module.get_string(f"endpoints.{name}.{query}", lang))
endpoint.get_help_text = (lambda lang=None: get_help_text(endpoint, lang, prefix))
return endpoint
def call_endpoint(context:EventContext, data:InputMessageData):
if not ((command := data.command) and (name := command.name)):
return
if not (endpoint := instanciate_endpoint(name, command.prefix)):
return
context.data = data context.data = data
context.module = endpoint.module context.module = endpoint.module
context.endpoint = endpoint context.endpoint = endpoint
context.endpoint.get_string = (lambda query=data.command.name, lang=None:
endpoint.module.get_string(f"endpoints.{data.command.name}.{query}", lang))
context.endpoint.help_text = (lambda lang=None: help_text(endpoint, lang))
if callable(agent_info := Platforms[context.platform].agent_info): if callable(agent_info := Platforms[context.platform].agent_info):
Platforms[context.platform].agent_info = agent_info() Platforms[context.platform].agent_info = agent_info()
return endpoint.handler(context, data) return endpoint.handler(context, data)
def WriteNewConfig() -> None: def write_new_config() -> None:
Log("💾️ No configuration found! Generating and writing to `./Config.py`... ", inline=True) app_log("💾️ No configuration found! Generating and writing to `./Data/Config.py`... ", inline=True)
with open("./Config.py", 'w') as configFile: with open("./Data/Config.py", 'w') as configFile:
opening = '# windog config start #' opening = '# windog config start #'
closing = '# end windog config #' closing = '# end windog config #'
for folder in ("LibWinDog", "ModWinDog"): for folder in ("LibWinDog", "ModWinDog"):
@ -279,50 +324,49 @@ def WriteNewConfig() -> None:
except IndexError: except IndexError:
pass pass
def Main() -> None: def app_main() -> None:
#SetupDb() #SetupDb()
Log(f"📨️ Initializing Platforms... ", newline=False) app_log(f"📨️ Initializing Platforms... ", newline=False)
for platform in Platforms.values(): for platform in Platforms.values():
if platform.main(): if platform.main():
Log(f"{platform.name}, ", inline=True) app_log(f"{platform.name}, ", inline=True)
Log("...Done. ✅️", inline=True, newline=True) app_log("...Done. ✅️", inline=True, newline=True)
Log("🐶️ WinDog Ready!") app_log("🐶️ WinDog Ready!")
while True: while True:
time.sleep(9**9) time.sleep(9**9)
if __name__ == '__main__': if __name__ == '__main__':
Log("🌞️ WinDog Starting...") app_log("🌞️ WinDog Starting...")
GlobalStrings = good_yaml_load(open("./WinDog.yaml", 'r').read()) GlobalStrings = good_yaml_load(open("./WinDog.yaml", 'r').read())
Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {} Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {}
for folder in ("LibWinDog/Platforms", "ModWinDog"): for folder in ("LibWinDog/Platforms", "ModWinDog"):
match folder: match folder:
case "LibWinDog/Platforms": case "LibWinDog/Platforms":
Log("📩️ Loading Platforms... ", newline=False) app_log("📩️ Loading Platforms... ", newline=False)
case "ModWinDog": case "ModWinDog":
Log("🔩️ Loading Modules... ", newline=False) app_log("🔩️ Loading Modules... ", newline=False)
for name in listdir(f"./{folder}"): for name in listdir(f"./{folder}"):
path = f"./{folder}/{name}" path = f"./{folder}/{name}"
if isfile(path): if path.endswith(".py") and isfile(path):
exec(open(path, 'r').read()) exec(open(path).read())
elif isdir(path): elif isdir(path):
files = listdir(path) files = listdir(path)
if f"{name}.py" in files: if f"{name}.py" in files:
files.remove(f"{name}.py") files.remove(f"{name}.py")
exec(open(f"{path}/{name}.py", 'r').read()) exec(open(f"{path}/{name}.py", 'r').read())
for file in files: #for file in files:
if file.endswith(".py"): # if file.endswith(".py"):
exec(open(f"{path}/{name}.py", 'r').read()) # exec(open(f"{path}/{file}", 'r').read())
Log("...Done. ✅️", inline=True, newline=True) app_log("...Done. ✅️", inline=True, newline=True)
Log("💽️ Loading Configuration... ", newline=False) app_log("💽️ Loading Configuration... ", newline=False)
from Config import * if isfile("./Data/Config.py"):
if isfile("./Config.py"): exec(open("./Data/Config.py", 'r').read())
from Config import *
else: else:
WriteNewConfig() write_new_config()
Log("Done. ✅️", inline=True, newline=True) app_log("Done. ✅️", inline=True, newline=True)
Main() app_main()
Log("🌚️ WinDog Stopping...") app_log("🌚️ WinDog Stopping...")

38
WinDog.yaml Normal file → Executable file
View File

@ -1,3 +1,6 @@
error:
en: Error
it: Errore
or: or:
en: or en: or
it: o it: o
@ -10,4 +13,39 @@ text:
usage: usage:
en: Usage en: Usage
it: Uso it: Uso
statuses:
102:
title:
en: Processing
icon: ⚙️
403:
title:
en: Forbidden
it: Vietato
summary:
en: You don't have the necessary permissions to access the requested resource or complete the specified action.
it: Non hai i permessi necessari per accedere alla risorsa richiesta o completare l'azione specificata.
icon: ⛔️
404:
title:
en: Not Found
it: Non Trovato
summary:
en: The requested resource was not found or the specified action cannot be completed.
it: La risorsa richiesta non è stata trovata o non è possibile completare l'azione specificata.
icon: 🕳️
500:
title:
en: Internal Error
it: Errore Interno
icon: 💣️
503:
title:
en: Service Unavailable
it: Servizio non Disponibile
icon: 🪦️
504:
title:
en: Gateway Timeout
icon: 🧻️

View File

@ -1,4 +1,3 @@
beautifulsoup4 beautifulsoup4
Markdown
peewee peewee
PyYAML PyYAML