Rename some internal functions; Update db and start work on message filters

This commit is contained in:
2024-10-21 00:26:08 +02:00
parent 5ba0df43c4
commit 9220c95636
28 changed files with 213 additions and 62 deletions

View File

@ -14,19 +14,64 @@ class BaseModel(Model):
class EntitySettings(BaseModel):
language = CharField(null=True)
#country = ...
#timezone = ...
class Entity(BaseModel):
id = CharField(null=True)
id_hash = CharField()
settings = ForeignKeyField(EntitySettings, backref="entity", null=True)
class User(Entity):
pass
class File(BaseModel):
path = CharField()
content = BlobField()
owner = ForeignKeyField(Entity, backref="files")
class Meta:
indexes = (
(('path', 'owner'), True),
)
#class BaseFilter(BaseModel):
# name = CharField(null=True)
# owner = ForeignKeyField(Entity, backref="filters")
#class ScriptFilter(BaseFilter):
# script = TextField()
#class StaticFilter(BaseFilter):
# response = TextField()
class Filter(BaseModel):
name = CharField(null=True)
trigger = CharField(null=True)
output = CharField(null=True)
owner = ForeignKeyField(Entity, backref="filters")
class Room(Entity):
pass
Db.create_tables([EntitySettings, User, Room], safe=True)
UserToRoomDeferred = DeferredThroughModel()
FilterToRoomDeferred = DeferredThroughModel()
class User(Entity):
rooms = ManyToManyField(Room, backref="users", through_model=UserToRoomDeferred)
class UserToRoom(BaseModel):
user = ForeignKeyField(User, backref="room_links")
room = ForeignKeyField(Room, backref="user_links")
class FilterToRoom(BaseModel):
filter = ForeignKeyField(Filter, backref="room_links")
room = ForeignKeyField(Room, backref="filter_links")
UserToRoomDeferred.set_model(UserToRoom)
FilterToRoomDeferred.set_model(FilterToRoom)
Db.create_tables([
EntitySettings, File, Filter,
User, Room,
FilterToRoom, UserToRoom,
], safe=True)
class UserSettingsData():
def __new__(cls, user_id:str=None) -> SafeNamespace:

View File

@ -46,7 +46,7 @@ def MastodonMakeInputMessageData(status:dict) -> InputMessageData:
def MastodonHandler(event, Mastodon):
if event["type"] == "mention":
data = MastodonMakeInputMessageData(event["status"])
OnInputMessageParsed(data)
on_input_message_parsed(data)
call_endpoint(EventContext(platform="mastodon", event=event, manager=Mastodon), data)
def MastodonSender(context:EventContext, data:OutputMessageData) -> None:
@ -67,5 +67,5 @@ def MastodonSender(context:EventContext, data:OutputMessageData) -> None:
visibility=('direct' if context.event['status']['visibility'] == 'direct' else 'unlisted'),
)
RegisterPlatform(name="Mastodon", main=MastodonMain, sender=MastodonSender, manager_class=mastodon.Mastodon)
register_platform(name="Mastodon", main=MastodonMain, sender=MastodonSender, manager_class=mastodon.Mastodon)

View File

@ -83,7 +83,7 @@ async def MatrixMessageHandler(room:nio.MatrixRoom, event:nio.RoomMessage) -> No
if MatrixUsername == event.sender:
return # ignore messages that come from the bot itself
data = MatrixMakeInputMessageData(room, event)
OnInputMessageParsed(data)
on_input_message_parsed(data)
call_endpoint(EventContext(platform="matrix", event=SafeNamespace(room=room, event=event), manager=MatrixClient), data)
def MatrixSender(context:EventContext, data:OutputMessageData):
@ -97,5 +97,5 @@ def MatrixSender(context:EventContext, data:OutputMessageData):
message_type="m.room.message",
content={"msgtype": "m.text", "body": data.text_plain}))
RegisterPlatform(name="Matrix", main=MatrixMain, sender=MatrixSender, manager_class=(lambda:MatrixClient))
register_platform(name="Matrix", main=MatrixMain, sender=MatrixSender, manager_class=(lambda:MatrixClient))

View File

@ -68,7 +68,7 @@ def TelegramHandler(update:telegram.Update, context:CallbackContext=None) -> Non
data = TelegramMakeInputMessageData(update.message)
if (quoted := update.message.reply_to_message):
data.quoted = TelegramMakeInputMessageData(quoted)
OnInputMessageParsed(data)
on_input_message_parsed(data)
call_endpoint(EventContext(platform="telegram", event=update, manager=context), data)
Thread(target=handler).start()
@ -107,7 +107,7 @@ def TelegramLinker(data:InputMessageData) -> SafeNamespace:
linked.message = f"https://t.me/c/{room_id}/{message_id}"
return linked
RegisterPlatform(
register_platform(
name="Telegram",
main=TelegramMain,
sender=TelegramSender,

View File

@ -55,7 +55,7 @@ class WebServerClass(BaseHTTPRequestHandler):
def init_new_room(self, room_id:str=None):
if not room_id:
room_id = str(uuid7().hex)
WebQueues[room_id] = {}#{"0": queue.Queue()}
WebQueues[room_id] = {}
#WebPushEvent(room_id, ".start", self.headers)
#Thread(target=lambda:WebAntiDropEnqueue(room_id)).start()
self.do_redirect(f"/{room_id}")
@ -65,24 +65,25 @@ class WebServerClass(BaseHTTPRequestHandler):
self.send_header("Content-Type", "text/html; charset=UTF-8")
self.send_header("Content-Encoding", "chunked")
self.end_headers()
target = f"/{room_id}/{user_id}?page-target=1#load-target"
if not is_redirected:
return self.wfile.write(f'''{web_html_prefix(head_extra=f'<meta http-equiv="refresh" content="0; url={target}">')}
target = f"/{room_id}/{user_id}?page-target=1#load-target"
self.wfile.write(f'''{web_html_prefix(head_extra=f'<meta http-equiv="refresh" content="0; url={target}">')}
<h3><a href="/" target="_parent">WinDog 🐶️</a></h3>
<p>Initializing... <a href="{target}">Click here</a> if you are not automatically redirected.</p>'''.encode())
self.wfile.write(f'''{web_html_prefix()}
else:
self.wfile.write(f'''{web_html_prefix()}
<h3><a href="/" target="_parent">WinDog 🐶️</a></h3>
<div class="sticky-box">
<p id="load-target" style="display: none;"><span style="color: red;">Background loading seems to have stopped...</span> Please open a new chat or <a href="{target}">reload this one</a> if you can't send new messages.</p>
<p id="load-target" style="display: none;"><span style="color: red;">Background loading seems to have stopped...</span> Please open a new chat or <a href="/{room_id}">reload this current one</a> if you can't send new messages.</p>
<div class="input-frame"><iframe src="/form/{room_id}/{user_id}"></iframe></div>
</div>
<div style="display: flex; flex-direction: column-reverse;">'''.encode())
while True:
# TODO this apparently makes us lose threads, we should handle dropped connections?
try:
self.wfile.write(WebMakeMessageHtml(WebQueues[room_id][user_id].get(block=False), user_id).encode())
except queue.Empty:
time.sleep(0.01)
while True:
# TODO this apparently makes us lose threads, we should handle dropped connections?
try:
self.wfile.write(WebMakeMessageHtml(WebQueues[room_id][user_id].get(block=False), user_id).encode())
except queue.Empty:
time.sleep(0.01)
def send_form_html(self, room_id:str, user_id:str):
self.send_text_content((f'''{web_html_prefix("form no-margin")}
@ -158,7 +159,7 @@ def WebPushEvent(room_id:str, user_id:str, text:str, headers:dict[str:str]):
settings = UserSettingsData(),
),
)
OnInputMessageParsed(data)
on_input_message_parsed(data)
WebSender(context, ObjectUnion(data, {"from_user": True}))
call_endpoint(context, data)
@ -183,7 +184,6 @@ def WebMain(path:str) -> bool:
return True
def WebSender(context:EventContext, data:OutputMessageData) -> None:
#WebQueues[context.event.room_id][context.event.user_id].put(data)
for user_id in (room := WebQueues[context.event.room_id]):
room[user_id].put(data)
@ -194,5 +194,5 @@ def WebAntiDropEnqueue(room_id:str, user_id:str):
WebQueues[room_id][user_id].put(OutputMessageData())
time.sleep(WebConfig["anti_drop_interval"])
RegisterPlatform(name="Web", main=WebMain, sender=WebSender)
register_platform(name="Web", main=WebMain, sender=WebSender)

0
LibWinDog/Platforms/Web/requirements.txt Normal file → Executable file
View File

0
LibWinDog/Platforms/Web/windog.css Normal file → Executable file
View File

0
LibWinDog/Platforms/Web/windog.js Normal file → Executable file
View File

View File

@ -52,7 +52,7 @@ def cPing(context:EventContext, data:InputMessageData):
#def cEval(context:EventContext, data:InputMessageData) -> None:
# send_message(context, {"Text": choice(Locale.__('eval'))})
RegisterModule(name="Base", endpoints=[
register_module(name="Base", endpoints=[
SafeNamespace(names=["source"], handler=cSource),
SafeNamespace(names=["config", "settings"], handler=cConfig, body=False, arguments={
"get": True,

View File

@ -12,10 +12,10 @@ def cBroadcast(context:EventContext, data:InputMessageData):
if not (destination and text):
return send_status_400(context, language)
result = send_message(context, {"text_plain": text, "room": SafeNamespace(id=destination)})
send_message(context, {"text_plain": "Executed."})
send_status(context, 201, language)
return result
RegisterModule(name="Broadcast", endpoints=[
register_module(name="Broadcast", endpoints=[
SafeNamespace(names=["broadcast"], handler=cBroadcast, body=True, arguments={
"destination": True,
}),

View File

@ -41,7 +41,7 @@ def mCodings(context:EventContext, data:InputMessageData):
except Exception:
return send_status_error(context, language)
RegisterModule(name="Codings", group="Geek", endpoints=[
register_module(name="Codings", group="Geek", endpoints=[
SafeNamespace(names=["encode", "decode"], handler=mCodings, body=False, quoted=False, arguments={
"algorithm": True,
}, help_extra=(lambda endpoint, lang: f'{endpoint.module.get_string("algorithms", lang)}: <code>{"</code>, <code>".join(CodingsAlgorithms)}</code>.')),

View File

@ -6,13 +6,14 @@
from json import dumps as json_dumps
# TODO work with links to messages
# TODO remove "wrong" objects like callables
def cDump(context:EventContext, data:InputMessageData):
if not (message := data.quoted):
return send_status_400(context, data.user.settings.language)
text = json_dumps(message, default=(lambda obj: obj.__dict__), indent=" ")
text = json_dumps(message, default=(lambda obj: (obj.__dict__ if not callable(obj) else None)), indent=" ")
return send_message(context, {"text_html": f'<pre>{html_escape(text)}</pre>'})
RegisterModule(name="Dumper", group="Geek", endpoints=[
register_module(name="Dumper", group="Geek", endpoints=[
SafeNamespace(names=["dump"], handler=cDump, quoted=True),
])

View File

@ -20,7 +20,7 @@ def cEcho(context:EventContext, data:InputMessageData):
prefix = ''
return send_message(context, {"text_html": (prefix + html_escape(text))})
RegisterModule(name="Echo", endpoints=[
register_module(name="Echo", endpoints=[
SafeNamespace(names=["echo"], handler=cEcho, body=True),
])

69
ModWinDog/Filters/Filters.py Executable file
View File

@ -0,0 +1,69 @@
# ==================================== #
# WinDog multi-purpose chatbot #
# Licensed under AGPLv3 by OctoSpacc #
# ==================================== #
def cFilters(context:EventContext, data:InputMessageData):
language = data.user.settings.language
if not check_room_admin(data.user):
return send_status(context, 403, language)
# action: create, delete, toggle <chatid, name/filterid>
# * (input) handle, ignore <...>
# * (output) setscript <..., script>
# * (output) insert, remove <..., groupid, message>
#arguments = data.command.parse_arguments(4)
if not (action := data.command.arguments.action) or (action not in ["list", "create", "delete"]):
return send_status_400(context, language)
[room_id, filter_id, command_data] = ((None,) * 3)
for token in data.command.tokens[2:]:
if (not room_id) and (':' in token):
room_id = token
elif (not filter_id):
filter_id = token
elif (not command_data):
command_data = token
if not room_id:
room_id = data.room.id
if (action in ["delete"]) and (not filter_id):
return send_status_400(context, language)
match action:
case "list":
filters_list = ""
for filter in Filter.select().where(Filter.owner == room_id).namedtuples():
filters_list += f"\n* <code>{filter.id}</code> — {filter.name or '[?]'}"
if filters_list:
return send_message(context, {"text_html": f"Filters for room <code>{room_id}</code>:{filters_list}"})
else:
return send_status(context, 404, language, f"No filters found for the room <code>{room_id}</code>.", summary=False)
case "create":
...
# TODO error message on name constraint violation
# TODO filter name validation (no spaces or special symbols, no only numbers)
if filter_id and (len(Filter.select().where((Filter.owner == room_id) & (Filter.name == filter_id)).tuples()) > 0):
return
else:
filter_id = Filter.create(name=filter_id, owner=room_id)
return send_status(context, 201, language, f"Filter with id <code>{filter_id}</code> in room <code>{room_id}</code> created successfully.", summary=False)
case "delete":
#try:
Filter.delete().where((Filter.owner == room_id) & ((Filter.id == filter_id) | (Filter.name == filter_id))).execute()
return send_status(context, 200, language)
#return send_status(context, 200, language, f"Filter <code>{filter_id}</code> for room <code>{room_id}</code> deleted successfully.", summary=False)
#except Exception:
# ... # TODO error and success message, actually check for if the item to delete existed
case "insert": # TODO
#output = Filter.select().where((Filter.owner == room_id) & ((Filter.id == filter_id) | (Filter.name == filter_id))).namedtuples().get().output
Filter.update(output=data.quoted).where((Filter.owner == room_id) & ((Filter.id == filter_id) | (Filter.name == filter_id))).execute()
case "remove":
...
case "handle":
...
case "ignore":
...
register_module(name="Filters", endpoints=[
SafeNamespace(names=["filters"], handler=cFilters, quoted=False, arguments={
"action": True,
}),
])

3
ModWinDog/Filters/Filters.yaml Executable file
View File

@ -0,0 +1,3 @@
summary:
en: Tools for triggering actions on received messages.

View File

@ -17,7 +17,7 @@ def cGpt(context:EventContext, data:InputMessageData):
output += (completion.choices[0].delta.content or "")
return send_message(context, {"text_plain": f"[🤖️ GPT]\n\n{output}"})
RegisterModule(name="GPT", endpoints=[
register_module(name="GPT", endpoints=[
SafeNamespace(names=["gpt", "chatgpt"], handler=cGpt, body=True),
])

View File

@ -13,7 +13,7 @@ def cHash(context:EventContext, data:InputMessageData):
return send_message(context, {
"text_html": f"<pre>{html_escape(hashlib.new(algorithm, text_input.encode()).hexdigest())}</pre>"})
RegisterModule(name="Hashing", group="Geek", endpoints=[
register_module(name="Hashing", group="Geek", endpoints=[
SafeNamespace(names=["hash"], handler=cHash, body=False, quoted=False, arguments={
"algorithm": True,
}, help_extra=(lambda endpoint, lang: f'{endpoint.get_string("algorithms", lang)}: <code>{"</code>, <code>".join(hashlib.algorithms_available)}</code>.')),

View File

@ -25,7 +25,7 @@ def cHelp(context:EventContext, data:InputMessageData) -> None:
text = text.strip()
return send_message(context, {"text_html": text})
RegisterModule(name="Help", group="Basic", endpoints=[
register_module(name="Help", group="Basic", endpoints=[
SafeNamespace(names=["help"], handler=cHelp, arguments={
"endpoint": False,
}),

View File

@ -142,7 +142,7 @@ def cSafebooru(context:EventContext, data:InputMessageData):
except Exception:
return send_status_error(context, language)
RegisterModule(name="Internet", endpoints=[
register_module(name="Internet", endpoints=[
SafeNamespace(names=["embedded"], handler=cEmbedded, body=False, quoted=False),
SafeNamespace(names=["web"], handler=cWeb, body=True),
SafeNamespace(names=["translate"], handler=cTranslate, body=False, quoted=False, arguments={

View File

@ -21,7 +21,7 @@ def mMultifun(context:EventContext, data:InputMessageData):
text = choice(fun_strings["empty"])
return send_message(context, {"text_html": text, "ReplyTo": reply_to})
RegisterModule(name="Multifun", endpoints=[
register_module(name="Multifun", endpoints=[
SafeNamespace(names=["hug", "pat", "poke", "cuddle", "hands", "floor", "sessocto"], handler=mMultifun),
])

View File

@ -15,7 +15,7 @@ def mPercenter(context:EventContext, data:InputMessageData):
) or context.endpoint.get_help_text(data.user.settings.language)
).format(RandomPercentString(), data.command.body)})
RegisterModule(name="Percenter", endpoints=[
register_module(name="Percenter", endpoints=[
SafeNamespace(names=["wish", "level"], handler=mPercenter, body=True),
])

View File

@ -123,7 +123,7 @@ def cCraiyonSelenium(context:EventContext, data:InputMessageData):
closeSelenium(driver_index, driver)
return result
RegisterModule(name="Scrapers", endpoints=[
register_module(name="Scrapers", endpoints=[
SafeNamespace(names=["dalle"], handler=cDalleSelenium, body=True),
SafeNamespace(names=["craiyon", "crayion"], handler=cCraiyonSelenium, body=True),
])

View File

@ -50,7 +50,7 @@ end)()"""))})
except (LuaError, LuaSyntaxError):
return send_status_error(context, data.user.settings.language)
RegisterModule(name="Scripting", group="Geek", endpoints=[
register_module(name="Scripting", group="Geek", endpoints=[
SafeNamespace(names=["lua"], handler=cLua, body=False, quoted=False),
])

View File

@ -8,7 +8,7 @@ def cStart(context:EventContext, data:InputMessageData):
text_html=context.endpoint.get_string(
"start", data.user.settings.language).format(data.user.name)))
RegisterModule(name="Start", endpoints=[
register_module(name="Start", endpoints=[
SafeNamespace(names=["start"], handler=cStart),
])

View File

@ -28,12 +28,12 @@ def cExec(context:EventContext, data:InputMessageData):
return send_message(context, {"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):
if not check_bot_admin(data.user):
return send_status(context, 403, data.user.settings.language)
open("./.WinDog.Restart.lock", 'w').close()
return send_message(context, {"text_plain": "Bot restart queued."})
RegisterModule(name="System", endpoints=[
register_module(name="System", endpoints=[
SafeNamespace(names=["exec"], handler=cExec, body=True),
SafeNamespace(names=["restart"], handler=cRestart),
])

View File

@ -11,6 +11,7 @@ The officially-hosted instances of this bot are, respectively:
* [@WinDogBot](https://t.me/WinDogBot) on Telegram
* [@windog:matrix.org](https://matrix.to/#/@windog:matrix.org) on Matrix
* [@WinDog@botsin.space](https://botsin.space/@WinDog) on Mastodon (can also be used from any other Fediverse platform)
* [WinDog.octt.eu.org](https://windog.octt.eu.org) as a web chat
In case you want to run your own instance:

View File

@ -127,6 +127,22 @@ def strip_url_scheme(url:str) -> str:
tokens = urlparse.urlparse(url)
return f"{tokens.netloc}{tokens.path}"
def parse_command_arguments(command, endpoint, count:int=None):
arguments = SafeNamespace()
body = command.body
index = 1
for key in (endpoint.arguments or range(count)):
if (not count) and (endpoint.body != None) and (endpoint.arguments[key] == False):
continue # skip optional (False) arguments for now if command expects a body, they will be implemented later
try:
value = command.tokens[index]
body = body[len(value):].strip()
except IndexError:
value = None
arguments[str(key)] = value
index += 1
return [arguments, body]
def TextCommandData(text:str, platform:str) -> CommandData|None:
if not text:
return None
@ -145,27 +161,17 @@ def TextCommandData(text:str, platform:str) -> CommandData|None:
command.body = text[len(command.tokens[0]):].strip()
if not (endpoint := obj_get(Endpoints, command.name)):
return command # TODO shouldn't this return None?
if (endpoint.arguments):
command.arguments = SafeNamespace()
index = 1
for key in endpoint.arguments:
if (endpoint.body != None) and (endpoint.arguments[key] == False):
continue # skip optional (False) arguments for now if command expects a body, they will be implemented later
try:
value = command.tokens[index]
command.body = command.body[len(value):].strip()
except IndexError:
value = None
command.arguments[key] = value
index += 1
command.parse_arguments = (lambda count=None: parse_command_arguments(command, endpoint, count))
if endpoint.arguments:
[command.arguments, command.body] = command.parse_arguments()
return command
def OnInputMessageParsed(data:InputMessageData) -> None:
def on_input_message_parsed(data:InputMessageData) -> None:
dump_message(data, prefix='> ')
handle_bridging(send_message, data, from_sent=False)
update_user_db(data.user)
def OnOutputMessageSent(output_data:OutputMessageData, input_data:InputMessageData, from_sent:bool) -> None:
def on_output_message_sent(output_data:OutputMessageData, input_data:InputMessageData, from_sent:bool) -> None:
if (not from_sent) and input_data:
output_data = ObjectUnion(output_data, {"room": input_data.room})
dump_message(output_data, prefix=f'<{"*" if from_sent else " "}')
@ -189,6 +195,14 @@ def handle_bridging(method:callable, data:MessageData, from_sent:bool):
ObjectUnion(data, {"room": SafeNamespace(id=room_id)}, ({"text_plain": text_plain, "text_markdown": None, "text_html": text_html} if data.user else None)),
from_sent=True)
def check_bot_admin(data:InputMessageData|UserData) -> bool:
user = (data.user or data)
return ((user.id in AdminIds) or (user.tag in AdminIds))
# TODO make this real
def check_room_admin(data:InputMessageData|UserData) -> bool:
return check_bot_admin(data)
def update_user_db(user:SafeNamespace) -> None:
if not (user and user.id):
return
@ -255,23 +269,23 @@ def send_message(context:EventContext, data:OutputMessageData, *, from_sent:bool
if (not context.manager) and (manager := platform.manager_class):
context.manager = call_or_return(manager)
result = platform.sender(context, data)
OnOutputMessageSent(data, context.data, from_sent)
on_output_message_sent(data, context.data, from_sent)
return result
def send_notice(context:EventContext, data):
pass
...
def edit_message(context:EventContext, data:MessageData):
pass
...
def delete_message(context:EventContext, data:MessageData):
pass
...
def RegisterPlatform(name:str, main:callable, sender:callable, linker:callable=None, *, event_class=None, manager_class=None, agent_info=None) -> None:
def register_platform(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)
app_log(f"{name}, ", inline=True)
def RegisterModule(name:str, endpoints:dict, *, group:str|None=None) -> None:
def register_module(name:str, endpoints:dict, *, group:str|None=None) -> None:
module = SafeNamespace(group=group, endpoints=endpoints, get_string=(lambda query, lang=None: None))
if isfile(file := f"./ModWinDog/{name}/{name}.yaml"):
module.strings = good_yaml_load(open(file, 'r').read())

View File

@ -17,7 +17,25 @@ statuses:
102:
title:
en: Processing
summary:
en: Your request has been accepted and is currently being processed.
it: La tua richiesta è stata accolta e sta venendo processata.
icon: ⚙️
200:
title:
en: OK
summary:
en: Your request has completed successfully.
it: La tua richiesta è terminata con successo.
icon: 👌️
201:
title:
en: Created
it: Creato
summary:
en: The specified resource has been successfully created.
it: La risorsa specificata è stata creata con successo.
icon: 🪙️
403:
title:
en: Forbidden