Start Matrix support, more refactoring, cleaner /help and /translate

This commit is contained in:
2024-06-17 00:32:03 +02:00
parent d687cbd51e
commit 09cf925850
19 changed files with 236 additions and 189 deletions

5
.gitignore vendored
View File

@ -1,4 +1,5 @@
Database.json
Dump.txt
/Config.py /Config.py
/Database.sqlite
/Dump.txt
/session.txt
*.pyc *.pyc

View File

@ -4,16 +4,18 @@
# ================================== # # ================================== #
# 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 = ""
MastodonUrl = '' # Only for the platforms you want to use, uncomment the below credentials and fill with your own:
MastodonToken = ''
TelegramId = 1637713483 # MastodonUrl = "https://mastodon.example.com"
TelegramToken = "0123456789:abcdefghijklmnopqrstuvwxyz123456789" # MastodonToken = ""
TelegramAdmins = [ 123456789, 634314973, ]
TelegramWhitelist = [ 123456789, 634314973, ] # MatrixUrl = "https://matrix.example.com"
TelegramRestrict = False # MatrixUsername = "username"
# MatrixPassword = "hunter2"
# TelegramToken = "1234567890:abcdefghijklmnopqrstuvwxyz123456789"
AdminIds = [ "123456789@telegram", "634314973@telegram", "admin@activitypub@mastodon.example.com", ] AdminIds = [ "123456789@telegram", "634314973@telegram", "admin@activitypub@mastodon.example.com", ]
@ -25,29 +27,8 @@ CmdPrefixes = ".!/"
ExecAllowed = {"date": False, "fortune": False, "neofetch": True, "uptime": False} ExecAllowed = {"date": False, "fortune": False, "neofetch": True, "uptime": False}
WebUserAgent = "WinDog v.Staging" WebUserAgent = "WinDog v.Staging"
# TODO deprecate this in favour of new module API ModuleGroups = (ModuleGroups | {
Endpoints = (Endpoints | { "Basic": "",
"start": cStart, "Geek": "",
#"config": cConfig,
"source": cSource,
"ping": cPing,
"echo": cEcho,
"broadcast": cBroadcast,
#"repeat": cRepeat,
"wish": percenter,
"level": percenter,
"hug": multifun,
"pat": multifun,
"poke": multifun,
"cuddle": multifun,
"floor": multifun,
"hands": multifun,
"sessocto": multifun,
#"encode": cEncode,
#"decode": cDecode,
#"time": cTime,
"eval": cEval,
"exec": cExec,
#"format": cFormat,
#"frame": cFrame,
}) })

8
LibWinDog/Database.py Normal file
View File

@ -0,0 +1,8 @@
from peewee import *
Db = SqliteDatabase("Database.sqlite")
class BaseModel(Model):
class Meta:
database = Db

View File

@ -1,36 +1,45 @@
# ================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ================================== #
MastodonUrl, MastodonToken = None, None
import mastodon import mastodon
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
def MastodonSender(event, manager, Data, Destination, TextPlain, TextMarkdown) -> None: def MastodonMain() -> bool:
if InDict(Data, 'Media'):
Media = manager.media_post(Data['Media'], Magic(mime=True).from_buffer(Data['Media']))
while Media['url'] == 'null':
Media = manager.media(Media)
if TextPlain:
manager.status_post(
status=(TextPlain + '\n\n@' + event['account']['acct']),
media_ids=(Media if InDict(Data, 'Media') else None),
in_reply_to_id=event['status']['id'],
visibility=('direct' if event['status']['visibility'] == 'direct' else 'unlisted'),
)
def MastodonMain() -> None:
if not (MastodonUrl and MastodonToken): if not (MastodonUrl and MastodonToken):
return return False
Mastodon = mastodon.Mastodon(api_base_url=MastodonUrl, access_token=MastodonToken) Mastodon = mastodon.Mastodon(api_base_url=MastodonUrl, access_token=MastodonToken)
class MastodonListener(mastodon.StreamListener): class MastodonListener(mastodon.StreamListener):
def on_notification(self, event): def on_notification(self, event):
if event['type'] == 'mention': if event['type'] == 'mention':
Msg = BeautifulSoup(event['status']['content'], 'html.parser').get_text(' ').strip().replace('\t', ' ') OnMessageReceived()
if not Msg.split('@')[0]: message = BeautifulSoup(event['status']['content'], 'html.parser').get_text(' ').strip().replace('\t', ' ')
Msg = ' '.join('@'.join(Msg.split('@')[1:]).strip().split(' ')[1:]).strip() if not message.split('@')[0]:
if Msg[0] in CmdPrefixes: message = ' '.join('@'.join(message.split('@')[1:]).strip().split(' ')[1:]).strip()
cmd = ParseCmd(Msg) if message[0] in CmdPrefixes:
if cmd: command = ParseCmd(message)
cmd.messageId = event['status']['id'] if command:
if cmd.Name in Endpoints: command.messageId = event['status']['id']
Endpoints[cmd.Name]({"Event": event, "Manager": Mastodon}, cmd) if command.Name in Endpoints:
Endpoints[command.Name]({"Event": event, "Manager": Mastodon}, command)
Mastodon.stream_user(MastodonListener(), run_async=True) Mastodon.stream_user(MastodonListener(), run_async=True)
return True
def MastodonSender(event, manager, data, destination, textPlain, textMarkdown) -> None:
if InDict(data, 'Media'):
Media = manager.media_post(data['Media'], Magic(mime=True).from_buffer(data['Media']))
while Media['url'] == 'null':
Media = manager.media(Media)
if textPlain or Media:
manager.status_post(
status=(textPlain + '\n\n@' + event['account']['acct']),
media_ids=(Media if InDict(data, 'Media') else None),
in_reply_to_id=event['status']['id'],
visibility=('direct' if event['status']['visibility'] == 'direct' else 'unlisted'),
)
RegisterPlatform(name="Mastodon", main=MastodonMain, sender=MastodonSender, managerClass=mastodon.Mastodon) RegisterPlatform(name="Mastodon", main=MastodonMain, sender=MastodonSender, managerClass=mastodon.Mastodon)

View File

@ -1,8 +0,0 @@
def MatrixMain() -> None:
pass
def MatrixSender() -> None:
pass
#RegisterPlatform(name="Matrix", main=MatrixMain, sender=MatrixSender)

View File

@ -0,0 +1,34 @@
# ================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ================================== #
MatrixUrl, MatrixUsername, MatrixPassword = None, None, None
import nio
import simplematrixbotlib as MatrixBotLib
from threading import Thread
def MatrixMain() -> bool:
if not (MatrixUrl and MatrixUsername and MatrixPassword):
return False
MatrixBot = MatrixBotLib.Bot(MatrixBotLib.Creds(MatrixUrl, MatrixUsername, MatrixPassword))
@MatrixBot.listener.on_message_event
@MatrixBot.listener.on_custom_event(nio.events.room_events.RoomMessageFile)
async def MatrixMessageListener(room, message) -> None:
pass
#print(message)
#match = MatrixBotLib.MessageMatch(room, message, MatrixBot)
#OnMessageReceived()
#if match.is_not_from_this_bot() and match.command("windogtest"):
# pass #await MatrixBot.api.send_text_message(room.room_id, " ".join(arg for arg in match.args()))
def runMatrixBot() -> None:
MatrixBot.run()
Thread(target=runMatrixBot).start()
return True
def MatrixSender() -> None:
pass
#RegisterPlatform(name="Matrix", main=MatrixMain, sender=MatrixSender)

View File

@ -0,0 +1 @@
simplematrixbotlib

View File

@ -1,26 +1,28 @@
# ================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ================================== #
TelegramToken = None
import telegram, telegram.ext import telegram, telegram.ext
from telegram import ForceReply, Bot from telegram import ForceReply, Bot
from telegram.utils.helpers import escape_markdown from telegram.utils.helpers import escape_markdown
from telegram.ext import CommandHandler, MessageHandler, Filters, CallbackContext from telegram.ext import CommandHandler, MessageHandler, Filters, CallbackContext
def TelegramCmdAllowed(update:telegram.Update) -> bool: def TelegramMain() -> bool:
if not TelegramRestrict: if not TelegramToken:
return True
if TelegramRestrict.lower() == 'whitelist':
if update.message.chat.id in TelegramWhitelist:
return True
return False
def TelegramHandleCmd(update:telegram.Update):
TelegramQueryHandle(update)
if TelegramCmdAllowed(update):
return ParseCmd(update.message.text)
else:
return False return False
updater = telegram.ext.Updater(TelegramToken)
dispatcher = updater.dispatcher
dispatcher.add_handler(MessageHandler(Filters.text | Filters.command, TelegramHandler))
updater.start_polling()
return True
def TelegramQueryHandle(update:telegram.Update, context:CallbackContext=None) -> None: def TelegramHandler(update:telegram.Update, context:CallbackContext=None) -> None:
if not (update and update.message): if not (update and update.message):
return return
OnMessageReceived()
cmd = ParseCmd(update.message.text) cmd = ParseCmd(update.message.text)
if cmd: if cmd:
cmd.messageId = update.message.message_id cmd.messageId = update.message.message_id
@ -70,13 +72,5 @@ def TelegramSender(event, manager, Data, Destination, TextPlain, TextMarkdown) -
elif TextPlain: elif TextPlain:
event.message.reply_text(TextPlain, reply_to_message_id=replyToId) event.message.reply_text(TextPlain, reply_to_message_id=replyToId)
def TelegramMain() -> None:
if not TelegramToken:
return
updater = telegram.ext.Updater(TelegramToken)
dispatcher = updater.dispatcher
dispatcher.add_handler(MessageHandler(Filters.text | Filters.command, TelegramQueryHandle))
updater.start_polling()
RegisterPlatform(name="Telegram", main=TelegramMain, sender=TelegramSender, eventClass=telegram.Update) RegisterPlatform(name="Telegram", main=TelegramMain, sender=TelegramSender, eventClass=telegram.Update)

View File

@ -1,3 +1,8 @@
# ================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ================================== #
def WebMain() -> None: def WebMain() -> None:
pass pass

View File

@ -113,7 +113,7 @@
}, },
"hands": { "hands": {
"empty": [ "empty": [
"*Le @manineuwu? 😳️*", "*Le t.me/manineuwu? 😳️*",
"*A chi vuoi dare le manine? Rispondi a qualcuno.*" "*A chi vuoi dare le manine? Rispondi a qualcuno.*"
], ],
"bot": [ "bot": [

View File

@ -1 +1,7 @@
import base64 import base64
#RegisterModule(name="Codings", group="Geek", endpoints={
# "Encode": CreateEndpoint(["encode"], summary="", handler=cEncode),
# "Decode": CreateEndpoint(["decode"], summary="", handler=cDecode),
#})

View File

@ -1,3 +1,8 @@
# ================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ================================== #
import hashlib import hashlib
def cHash(context, data) -> None: def cHash(context, data) -> None:

View File

@ -1,7 +1,11 @@
# TODO: implement /help <commandname> feature # ================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ================================== #
# TODO: implement /help <commandname> feature
def cHelp(context, data=None) -> None: def cHelp(context, data=None) -> None:
moduleList, commands = '', '' moduleList = ''
for module in Modules: for module in Modules:
summary = Modules[module]["summary"] summary = Modules[module]["summary"]
endpoints = Modules[module]["endpoints"] endpoints = Modules[module]["endpoints"]
@ -9,9 +13,7 @@ def cHelp(context, data=None) -> None:
for endpoint in endpoints: for endpoint in endpoints:
summary = endpoints[endpoint]["summary"] summary = endpoints[endpoint]["summary"]
moduleList += (f"\n* /{', /'.join(endpoints[endpoint]['names'])}" + (f": {summary}" if summary else '')) moduleList += (f"\n* /{', /'.join(endpoints[endpoint]['names'])}" + (f": {summary}" if summary else ''))
for cmd in Endpoints.keys(): SendMsg(context, {"Text": f"[ Available Modules ]{moduleList}"})
commands += f'* /{cmd}\n'
SendMsg(context, {"Text": f"[ Available Modules ]{moduleList}\n\nFull Endpoints List:\n{commands}"})
RegisterModule(name="Help", group="Basic", endpoints={ RegisterModule(name="Help", group="Basic", endpoints={
"Help": CreateEndpoint(["help"], summary="Provides help for the bot. For now, it just lists the commands.", handler=cHelp), "Help": CreateEndpoint(["help"], summary="Provides help for the bot. For now, it just lists the commands.", handler=cHelp),

View File

@ -1,3 +1,8 @@
# ================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ================================== #
from urlextract import URLExtract from urlextract import URLExtract
from urllib import parse as UrlParse from urllib import parse as UrlParse
from urllib.request import urlopen, Request from urllib.request import urlopen, Request
@ -63,14 +68,20 @@ def cWeb(context, data) -> None:
else: else:
pass pass
def cImages(context, data) -> None:
pass
def cNews(context, data) -> None:
pass
def cTranslate(context, data) -> None: def cTranslate(context, data) -> None:
if len(data.Tokens) < 3: if len(data.Tokens) < 3:
return return
try: try:
Lang = data.Tokens[1] toLang = data.Tokens[1]
# TODO: Use many different public Lingva instances in rotation to avoid overloading a specific one # TODO: Use many different public Lingva instances in rotation to avoid overloading a specific one
Result = json.loads(HttpGet(f'https://lingva.ml/api/v1/auto/{Lang}/{UrlParse.quote(Lang.join(data.Body.split(Lang)[1:]))}').read())["translation"] result = json.loads(HttpGet(f'https://lingva.ml/api/v1/auto/{toLang}/{UrlParse.quote(toLang.join(data.Body.split(toLang)[1:]))}').read())
SendMsg(context, {"TextPlain": Result}) SendMsg(context, {"TextPlain": f"[{result['info']['detectedSource']} (auto) -> {toLang}]\n\n{result['translation']}"})
except Exception: except Exception:
raise raise
@ -119,7 +130,7 @@ def cSafebooru(context, data) -> None:
except Exception: except Exception:
raise raise
RegisterModule(name="Internet", group="Internet", summary="Tools and toys related to the Internet.", endpoints={ RegisterModule(name="Internet", summary="Tools and toys related to the Internet.", endpoints={
"Embedded": CreateEndpoint(["embedded"], summary="Rewrites a link, trying to bypass embed view protection.", handler=cEmbedded), "Embedded": CreateEndpoint(["embedded"], summary="Rewrites a link, trying to bypass embed view protection.", handler=cEmbedded),
"Web": CreateEndpoint(["web"], summary="Provides results of a DuckDuckGo search.", handler=cWeb), "Web": CreateEndpoint(["web"], summary="Provides results of a DuckDuckGo search.", handler=cWeb),
"Translate": CreateEndpoint(["translate"], summary="Returns the received message after translating it in another language.", handler=cTranslate), "Translate": CreateEndpoint(["translate"], summary="Returns the received message after translating it in another language.", handler=cTranslate),

View File

@ -3,21 +3,19 @@
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ==================================== # # ==================================== #
# Module: Percenter import re, subprocess
# Provides fun trough percentage-based toys.
def percenter(context, data) -> None: def mPercenter(context, data) -> None:
SendMsg(context, {"Text": choice(Locale.__(f'{data.Name}.{"done" if data.Body else "empty"}')).format( SendMsg(context, {"Text": choice(Locale.__(f'{data.Name}.{"done" if data.Body else "empty"}')).format(
Cmd=data.Tokens[0], Percent=RandPercent(), Thing=data.Body)}) Cmd=data.Tokens[0], Percent=RandPercent(), Thing=data.Body)})
# Module: Multifun def mMultifun(context, data) -> None:
# Provides fun trough preprogrammed-text-based toys.
def multifun(context, data) -> None:
cmdkey = data.Name cmdkey = data.Name
replyToId = None replyToId = None
if data.Quoted: if data.Quoted:
replyFromUid = data.Quoted.User.Id replyFromUid = data.Quoted.User.Id
# TODO work on all platforms for the bot id # TODO work on all platforms for the bot id
if int(replyFromUid.split('@')[0]) == int(TelegramId) and 'bot' in Locale.__(cmdkey): if replyFromUid.split('@')[0] == TelegramToken.split(':')[0] and 'bot' in Locale.__(cmdkey):
Text = choice(Locale.__(f'{cmdkey}.bot')) Text = choice(Locale.__(f'{cmdkey}.bot'))
elif replyFromUid == data.User.Id and 'self' in Locale.__(cmdkey): elif replyFromUid == data.User.Id and 'self' in Locale.__(cmdkey):
Text = choice(Locale.__(f'{cmdkey}.self')).format(data.User.Name) Text = choice(Locale.__(f'{cmdkey}.self')).format(data.User.Name)
@ -30,13 +28,9 @@ def multifun(context, data) -> None:
Text = choice(Locale.__(f'{cmdkey}.empty')) Text = choice(Locale.__(f'{cmdkey}.empty'))
SendMsg(context, {"Text": Text, "ReplyTo": replyToId}) SendMsg(context, {"Text": Text, "ReplyTo": replyToId})
# Module: Start
# Salutes the user, hinting that the bot is working and providing basic quick help.
def cStart(context, data) -> None: def cStart(context, data) -> None:
SendMsg(context, {"Text": choice(Locale.__('start')).format(data.User.Name)}) SendMsg(context, {"Text": choice(Locale.__('start')).format(data.User.Name)})
# Module: Source
# Provides a copy of the bot source codes and/or instructions on how to get it.
def cSource(context, data=None) -> None: def cSource(context, data=None) -> None:
SendMsg(context, {"TextPlain": ("""\ SendMsg(context, {"TextPlain": ("""\
* Original Code: {https://gitlab.com/octospacc/WinDog} * Original Code: {https://gitlab.com/octospacc/WinDog}
@ -52,13 +46,9 @@ def cSource(context, data=None) -> None:
# # ... language: en, it, ... # # ... language: en, it, ...
# # ... userdata: import, export, delete # # ... userdata: import, export, delete
# Module: Ping
# Responds pong, useful for testing messaging latency.
def cPing(context, data=None) -> None: def cPing(context, data=None) -> None:
SendMsg(context, {"Text": "*Pong!*"}) SendMsg(context, {"Text": "*Pong!*"})
# Module: Echo
# Responds back with the original text of the received message.
def cEcho(context, data) -> None: def cEcho(context, data) -> None:
if data.Body: if data.Body:
prefix = "🗣️ " prefix = "🗣️ "
@ -75,8 +65,6 @@ def cEcho(context, data) -> None:
else: else:
SendMsg(context, {"Text": choice(Locale.__('echo.empty'))}) SendMsg(context, {"Text": choice(Locale.__('echo.empty'))})
# Module: Broadcast
# Sends an admin message over to another destination
def cBroadcast(context, data) -> None: def cBroadcast(context, data) -> None:
if data.User.Id not in AdminIds: if data.User.Id not in AdminIds:
return SendMsg(context, {"Text": choice(Locale.__('eval'))}) return SendMsg(context, {"Text": choice(Locale.__('eval'))})
@ -92,13 +80,9 @@ def cBroadcast(context, data) -> None:
# CharEscape(choice(Locale.__('time')).format(time.ctime().replace(' ', ' ')), 'MARKDOWN_SPEECH'), # CharEscape(choice(Locale.__('time')).format(time.ctime().replace(' ', ' ')), 'MARKDOWN_SPEECH'),
# reply_to_message_id=update.message.message_id) # reply_to_message_id=update.message.message_id)
# Module: Eval
# Execute a Python command (or safe literal operation) in the current context. Currently not implemented.
def cEval(context, data=None) -> None: def cEval(context, data=None) -> None:
SendMsg(context, {"Text": choice(Locale.__('eval'))}) SendMsg(context, {"Text": choice(Locale.__('eval'))})
# Module: Exec
# Execute a system command from the allowed ones and return stdout/stderr.
def cExec(context, data) -> None: def cExec(context, data) -> None:
if len(data.Tokens) >= 2 and data.Tokens[1].lower() in ExecAllowed: if len(data.Tokens) >= 2 and data.Tokens[1].lower() in ExecAllowed:
cmd = data.Tokens[1].lower() cmd = data.Tokens[1].lower()
@ -113,13 +97,18 @@ def cExec(context, data) -> None:
else: else:
SendMsg(context, {"Text": choice(Locale.__('eval'))}) SendMsg(context, {"Text": choice(Locale.__('eval'))})
# Module: Format RegisterModule(name="Misc", endpoints={
# Reformat text using an handful of rules. Currently not implemented. "Percenter": CreateEndpoint(["wish", "level"], summary="Provides fun trough percentage-based toys.", handler=mPercenter),
def cFormat(context, data=None) -> None: "Multifun": CreateEndpoint(["hug", "pat", "poke", "cuddle", "hands", "floor", "sessocto"], summary="Provides fun trough preprogrammed-text-based toys.", handler=mMultifun),
pass "Start": CreateEndpoint(["start"], summary="Salutes the user, hinting that the bot is working and providing basic quick help.", handler=cStart),
"Source": CreateEndpoint(["source"], summary="Provides a copy of the bot source codes and/or instructions on how to get it.", handler=cSource),
# Module: Frame "Ping": CreateEndpoint(["ping"], summary="Responds pong, useful for testing messaging latency.", handler=cPing),
# Frame someone's message into a platform-styled image. Currently not implemented. "Echo": CreateEndpoint(["echo"], summary="Responds back with the original text of the received message.", handler=cEcho),
def cFrame(context, data=None) -> None: "Broadcast": CreateEndpoint(["broadcast"], summary="Sends an admin message over to any chat destination.", handler=cBroadcast),
pass "Eval": CreateEndpoint(["eval"], summary="Execute a Python command (or safe literal operation) in the current context. Currently not implemented.", handler=cEval),
"Exec": CreateEndpoint(["exec"], summary="Execute a system command from the allowed ones and return stdout+stderr.", handler=cExec),
#"Format": CreateEndpoint(["format"], summary="Reformat text using an handful of rules. Not yet implemented.", handler=cFormat),
#"Frame": CreateEndpoint(["frame"], summary="Frame someone's message into a platform-styled image. Not yet implemented.", handler=cFrame),
#"Repeat": CreateEndpoint(["repeat"], summary="I had this planned but I don't remember what this should have done. Not yet implemented.", handler=cRepeat),
})

View File

@ -1,24 +1,18 @@
# ================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ================================== #
luaCycleLimit = 10000 luaCycleLimit = 10000
luaMemoryLimit = (512 * 1024) # 512 KB luaMemoryLimit = (512 * 1024) # 512 KB
luaCrashMessage = f"Lua Error: Script has been forcefully terminated due to having exceeded the max cycle count limit ({luaCycleLimit})." luaCrashMessage = f"Script has been forcefully terminated due to having exceeded the max cycle count limit ({luaCycleLimit})."
from lupa import LuaRuntime as NewLuaRuntime, LuaError, LuaSyntaxError # Use specific Lua version; always using the latest is risky due to possible new APIs and using JIT is vulnerable
from lupa.lua54 import LuaRuntime as NewLuaRuntime, LuaError, LuaSyntaxError
def luaAttributeFilter(obj, attr_name, is_setting): def luaAttributeFilter(obj, attr_name, is_setting):
raise AttributeError("Access Denied.") raise AttributeError("Access Denied.")
#LuaRuntime = NewLuaRuntime(max_memory=(16 * 1024**2), register_eval=False, register_builtins=False, attribute_filter=luaAttributeFilter)
#for key in LuaRuntime.globals():
# if key not in ("error", "assert", "math", "type"):
# del LuaRuntime.globals()[key]
#luaGlobalsCopy = dict(LuaRuntime.globals()) # should this manually handle nested stuff?
# this way to prevent overwriting of global fields is flawed since this doesn't protect concurrent scripts
# better to use the currently active solution of a dedicated instance for each new script running
#def luaFunctionRunner(userFunction:callable):
# for key in luaGlobalsCopy:
# LuaRuntime.globals()[key] = luaGlobalsCopy[key]
# return userFunction()
# 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, data=None) -> None: def cLua(context, data=None) -> None:
scriptText = (data.Body or (data.Quoted and data.Quoted.Body)) scriptText = (data.Body or (data.Quoted and data.Quoted.Body))
@ -27,12 +21,12 @@ def cLua(context, data=None) -> None:
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 = "" }}
function print (text, endl) _windog.stdout = _windog.stdout .. text .. (endl ~= false and "\\n" or "") end function print (text, endl) _windog.stdout = _windog.stdout .. tostring(text) .. (endl ~= false and "\\n" or "") end
function luaCrashHandler () return error("{luaCrashMessage}") end function luaCrashHandler () return error("{luaCrashMessage}") end
debug.sethook(luaCrashHandler, "", {luaCycleLimit}) debug.sethook(luaCrashHandler, "", {luaCycleLimit})
end)()""") end)()""")
for key in luaRuntime.globals(): for key in luaRuntime.globals():
if key not in ("error", "assert", "math", "string", "print", "_windog"): if key not in ["error", "assert", "math", "string", "tostring", "print", "_windog"]:
del luaRuntime.globals()[key] del luaRuntime.globals()[key]
try: try:
textOutput = ("[ʟᴜᴀ ꜱᴛᴅᴏᴜᴛ]\n\n" + luaRuntime.eval(f"""(function() textOutput = ("[ʟᴜᴀ ꜱᴛᴅᴏᴜᴛ]\n\n" + luaRuntime.eval(f"""(function()
@ -40,7 +34,7 @@ _windog.scriptout = (function()\n{scriptText}\nend)()
return _windog.stdout .. (_windog.scriptout or '') return _windog.stdout .. (_windog.scriptout or '')
end)()""")) end)()"""))
except (LuaError, LuaSyntaxError) as error: except (LuaError, LuaSyntaxError) as error:
Log(textOutput := str("Lua Error: " + error)) Log(textOutput := ("Lua Error: " + str(error)))
SendMsg(context, {"TextPlain": textOutput}) SendMsg(context, {"TextPlain": textOutput})
RegisterModule(name="Scripting", group="Geek", summary="Tools for programming the bot and expanding its features.", endpoints={ RegisterModule(name="Scripting", group="Geek", summary="Tools for programming the bot and expanding its features.", endpoints={

View File

@ -15,6 +15,12 @@ In case you want to run your own instance:
1. `git clone --depth 1 https://gitlab.com/octospacc/WinDog && cd ./WinDog` to get the code. 1. `git clone --depth 1 https://gitlab.com/octospacc/WinDog && cd ./WinDog` to get the code.
2. `find -type f -name requirements.txt -exec python3 -m pip install -U -r {} \;` to install the full package of dependencies. 2. `find -type f -name requirements.txt -exec python3 -m pip install -U -r {} \;` to install the full package of dependencies.
3. `cp ./LibWinDog/Config.py ./` and, in the new file, edit essential fields like user credentials, then delete the unmodified fields. 3. `cp ./LibWinDog/Config.py ./` and, in the new file, edit essential fields (like user credentials), uncommenting them where needed, then delete the unmodified fields.
4. `sh ./StartWinDog.sh` to start the bot every time. 4. `sh ./StartWinDog.sh` to start the bot every time.
All my source code mirrors for the bot:
* GitLab (primary): <https://gitlab.com/octospacc/WinDog>
* GitHub: <https://github.com/octospacc/WinDog>
* Gitea.it: <https://gitea.it/octospacc/WinDog>

View File

@ -4,7 +4,7 @@
# Licensed under AGPLv3 by OctoSpacc # # Licensed under AGPLv3 by OctoSpacc #
# ==================================== # # ==================================== #
import json, re, time, subprocess import json, time
from binascii import hexlify from binascii import hexlify
from magic import Magic from magic import Magic
from os import listdir from os import listdir
@ -15,12 +15,16 @@ from traceback import format_exc
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.Database import *
# <https://daringfireball.net/projects/markdown/syntax#backslash> # <https://daringfireball.net/projects/markdown/syntax#backslash>
MdEscapes = '\\`*_{}[]()<>#+-.!|=' MdEscapes = '\\`*_{}[]()<>#+-.!|='
def Log(text:str, level:str="?") -> None: def Log(text:str, level:str="?", *, newline:bool|None=None, inline:bool=False) -> None:
print(f"[{level}] [{int(time.time())}] {text}") endline = '\n'
if newline == False or (inline and newline == None):
endline = ''
print((text if inline else f"[{level}] [{int(time.time())}] {text}"), end=endline)
def SetupLocales() -> None: def SetupLocales() -> None:
global Locale global Locale
@ -55,14 +59,6 @@ def SetupLocales() -> None:
Locale['Locale'] = Locale Locale['Locale'] = Locale
Locale = SimpleNamespace(**Locale) Locale = SimpleNamespace(**Locale)
def SetupDb() -> None:
global Db
try:
with open('Database.json', 'r') as file:
Db = json.load(file)
except Exception:
pass
def InDict(Dict:dict, Key:str) -> any: def InDict(Dict:dict, Key:str) -> any:
if Key in Dict: if Key in Dict:
return Dict[Key] return Dict[Key]
@ -94,7 +90,7 @@ def InferMdEscape(raw:str, plain:str) -> str:
return chars return chars
def MarkdownCode(text:str, block:bool) -> str: def MarkdownCode(text:str, block:bool) -> str:
return '```\n' + text.strip().replace('`', '\`') + '\n```' return ('```\n' + text.strip().replace('`', '\`') + '\n```')
def MdToTxt(md:str) -> str: def MdToTxt(md:str) -> str:
return BeautifulSoup(markdown(md), 'html.parser').get_text(' ') return BeautifulSoup(markdown(md), 'html.parser').get_text(' ')
@ -135,58 +131,68 @@ def RandHexStr(length:int) -> str:
hexa += choice('0123456789abcdef') hexa += choice('0123456789abcdef')
return hexa return hexa
def SendMsg(Context, Data, Destination=None) -> None: def OnMessageReceived() -> None:
if type(Context) == dict: pass
Event = Context['Event'] if 'Event' in Context else None
Manager = Context['Manager'] if 'Manager' in Context else None def SendMsg(context, data, destination=None) -> None:
if type(context) == dict:
event = context['Event'] if 'Event' in context else None
manager = context['Manager'] if 'Manager' in context else None
else: else:
[Event, Manager] = [Context, Context] [event, manager] = [context, context]
if InDict(Data, 'TextPlain') or InDict(Data, 'TextMarkdown'): if InDict(data, 'TextPlain') or InDict(data, 'TextMarkdown'):
TextPlain = InDict(Data, 'TextPlain') textPlain = InDict(data, 'TextPlain')
TextMarkdown = InDict(Data, 'TextMarkdown') textMarkdown = InDict(data, 'TextMarkdown')
if not TextPlain: if not textPlain:
TextPlain = TextMarkdown textPlain = textMarkdown
elif InDict(Data, 'Text'): elif InDict(data, 'Text'):
# our old system attemps to always receive Markdown and retransform when needed # our old system attempts to always receive Markdown and retransform when needed
TextPlain = MdToTxt(Data['Text']) textPlain = MdToTxt(data['Text'])
TextMarkdown = CharEscape(HtmlUnescape(Data['Text']), InferMdEscape(HtmlUnescape(Data['Text']), TextPlain)) textMarkdown = CharEscape(HtmlUnescape(data['Text']), InferMdEscape(HtmlUnescape(data['Text']), textPlain))
for platform in Platforms: for platform in Platforms:
platform = Platforms[platform] platform = Platforms[platform]
if isinstanceSafe(Event, InDict(platform, "eventClass")) or isinstanceSafe(Manager, InDict(platform, "managerClass")): if isinstanceSafe(event, InDict(platform, "eventClass")) or isinstanceSafe(manager, InDict(platform, "managerClass")):
platform["sender"](Event, Manager, Data, Destination, TextPlain, TextMarkdown) platform["sender"](event, manager, data, destination, textPlain, textMarkdown)
def RegisterPlatform(name:str, main:callable, sender:callable, *, eventClass=None, managerClass=None) -> None: def RegisterPlatform(name:str, main:callable, sender:callable, *, eventClass=None, managerClass=None) -> None:
Platforms[name] = {"main": main, "sender": sender, "eventClass": eventClass, "managerClass": managerClass} Platforms[name] = {"main": main, "sender": sender, "eventClass": eventClass, "managerClass": managerClass}
Log(f"Registered Platform: {name}.") Log(f"{name}, ", inline=True)
def RegisterModule(name:str, endpoints:dict, *, group:str=None, summary:str=None) -> None: def RegisterModule(name:str, endpoints:dict, *, group:str|None=None, summary:str|None=None) -> None:
Modules[name] = {"group": group, "summary": summary, "endpoints": endpoints} Modules[name] = {"group": group, "summary": summary, "endpoints": endpoints}
Log(f"Registered Module: {name}.") Log(f"{name}, ", inline=True)
for endpoint in endpoints: for endpoint in endpoints:
endpoint = endpoints[endpoint] endpoint = endpoints[endpoint]
for name in endpoint["names"]: for name in endpoint["names"]:
Endpoints[name] = endpoint["handler"] Endpoints[name] = endpoint["handler"]
def CreateEndpoint(names:list[str]|tuple[str], handler:callable, *, summary:str=None) -> dict: def CreateEndpoint(names:list[str], handler:callable, arguments:dict[str, dict]={}, *, summary:str|None=None) -> dict:
return {"names": names, "summary": summary, "handler": handler} return {"names": names, "summary": summary, "handler": handler, "arguments": arguments}
def Main() -> None: def Main() -> None:
SetupDb() #SetupDb()
SetupLocales() SetupLocales()
Log(f"📨️ Initializing Platforms... ", newline=False)
for platform in Platforms: for platform in Platforms:
Platforms[platform]["main"]() if Platforms[platform]["main"]():
Log(f"Initialized Platform: {platform}.") Log(f"{platform}, ", inline=True)
Log('WinDog Ready!') Log("...Done. ✅️", inline=True, newline=True)
Log("🐶️ WinDog Ready!")
while True: while True:
time.sleep(9**9) time.sleep(9**9)
if __name__ == '__main__': if __name__ == '__main__':
Log('Starting WinDog...') Log("🌞️ WinDog Starting...")
Db = {"Rooms": {}, "Users": {}} #Db = {"Rooms": {}, "Users": {}}
Locale = {"Fallback": {}} Locale = {"Fallback": {}}
Platforms, Modules, Endpoints = {}, {}, {} Platforms, Modules, ModuleGroups, Endpoints = {}, {}, {}, {}
for dir in ("LibWinDog/Platforms", "ModWinDog"): for dir in ("LibWinDog/Platforms", "ModWinDog"):
match dir:
case "LibWinDog/Platforms":
Log("📩️ Loading Platforms... ", newline=False)
case "ModWinDog":
Log("🔩️ Loading Modules... ", newline=False)
for name in listdir(f"./{dir}"): for name in listdir(f"./{dir}"):
path = f"./{dir}/{name}" path = f"./{dir}/{name}"
if isfile(path): if isfile(path):
@ -197,14 +203,16 @@ if __name__ == '__main__':
#for name in listdir(path): #for name in listdir(path):
# if name.lower().endswith('.json'): # if name.lower().endswith('.json'):
# #
Log("...Done. ✅️", inline=True, newline=True)
Log('Loading Configuration...') Log("💽️ Loading Configuration", newline=False)
exec(open("./LibWinDog/Config.py", 'r').read()) exec(open("./LibWinDog/Config.py", 'r').read())
try: try:
from Config import * from Config import *
except Exception: except Exception:
Log(format_exc()) Log(format_exc())
Log("...Done. ✅️", inline=True, newline=True)
Main() Main()
Log('Closing WinDog...') Log("🌚️ WinDog Stopping...")

View File

@ -1,2 +1,3 @@
beautifulsoup4 beautifulsoup4
Markdown Markdown
peewee