diff --git a/LibWinDog/Database.py b/LibWinDog/Database.py index a57640a..0302538 100755 --- a/LibWinDog/Database.py +++ b/LibWinDog/Database.py @@ -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: diff --git a/LibWinDog/Platforms/Mastodon/Mastodon.py b/LibWinDog/Platforms/Mastodon/Mastodon.py index e94b7ea..db5b793 100755 --- a/LibWinDog/Platforms/Mastodon/Mastodon.py +++ b/LibWinDog/Platforms/Mastodon/Mastodon.py @@ -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) diff --git a/LibWinDog/Platforms/Matrix/Matrix.py b/LibWinDog/Platforms/Matrix/Matrix.py index f7ad5d5..c31b7a9 100755 --- a/LibWinDog/Platforms/Matrix/Matrix.py +++ b/LibWinDog/Platforms/Matrix/Matrix.py @@ -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)) diff --git a/LibWinDog/Platforms/Telegram/Telegram.py b/LibWinDog/Platforms/Telegram/Telegram.py index 8c9a066..7503b99 100755 --- a/LibWinDog/Platforms/Telegram/Telegram.py +++ b/LibWinDog/Platforms/Telegram/Telegram.py @@ -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, diff --git a/LibWinDog/Platforms/Web/Web.py b/LibWinDog/Platforms/Web/Web.py index ad313a9..a08cbb0 100755 --- a/LibWinDog/Platforms/Web/Web.py +++ b/LibWinDog/Platforms/Web/Web.py @@ -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'')} + target = f"/{room_id}/{user_id}?page-target=1#load-target" + self.wfile.write(f'''{web_html_prefix(head_extra=f'')}
Initializing... Click here if you are not automatically redirected.
'''.encode()) - self.wfile.write(f'''{web_html_prefix()} + else: + self.wfile.write(f'''{web_html_prefix()}{"
, ".join(CodingsAlgorithms)}
.')),
diff --git a/ModWinDog/Dumper/Dumper.py b/ModWinDog/Dumper/Dumper.py
index f7905e9..340a043 100755
--- a/ModWinDog/Dumper/Dumper.py
+++ b/ModWinDog/Dumper/Dumper.py
@@ -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'{html_escape(text)}'}) -RegisterModule(name="Dumper", group="Geek", endpoints=[ +register_module(name="Dumper", group="Geek", endpoints=[ SafeNamespace(names=["dump"], handler=cDump, quoted=True), ]) diff --git a/ModWinDog/Echo/Echo.py b/ModWinDog/Echo/Echo.py index 898cd16..eedad07 100755 --- a/ModWinDog/Echo/Echo.py +++ b/ModWinDog/Echo/Echo.py @@ -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), ]) diff --git a/ModWinDog/Filters/Filters.py b/ModWinDog/Filters/Filters.py new file mode 100755 index 0000000..cebf7d2 --- /dev/null +++ b/ModWinDog/Filters/Filters.py @@ -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
{filter.id}
— {filter.name or '[?]'}"
+ if filters_list:
+ return send_message(context, {"text_html": f"Filters for room {room_id}
:{filters_list}"})
+ else:
+ return send_status(context, 404, language, f"No filters found for the room {room_id}
.", 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 {filter_id}
in room {room_id}
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 {filter_id}
for room {room_id}
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,
+ }),
+])
+
diff --git a/ModWinDog/Filters/Filters.yaml b/ModWinDog/Filters/Filters.yaml
new file mode 100755
index 0000000..139c451
--- /dev/null
+++ b/ModWinDog/Filters/Filters.yaml
@@ -0,0 +1,3 @@
+summary:
+ en: Tools for triggering actions on received messages.
+
diff --git a/ModWinDog/GPT/GPT.py b/ModWinDog/GPT/GPT.py
index bc284c2..7016756 100755
--- a/ModWinDog/GPT/GPT.py
+++ b/ModWinDog/GPT/GPT.py
@@ -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),
])
diff --git a/ModWinDog/Hashing/Hashing.py b/ModWinDog/Hashing/Hashing.py
index 7a3e512..171d0ca 100755
--- a/ModWinDog/Hashing/Hashing.py
+++ b/ModWinDog/Hashing/Hashing.py
@@ -13,7 +13,7 @@ def cHash(context:EventContext, data:InputMessageData):
return send_message(context, {
"text_html": f"{html_escape(hashlib.new(algorithm, text_input.encode()).hexdigest())}"}) -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)}:
{"
, ".join(hashlib.algorithms_available)}
.')),
diff --git a/ModWinDog/Help/Help.py b/ModWinDog/Help/Help.py
index 2285c8e..153bebe 100755
--- a/ModWinDog/Help/Help.py
+++ b/ModWinDog/Help/Help.py
@@ -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,
}),
diff --git a/ModWinDog/Internet/Internet.py b/ModWinDog/Internet/Internet.py
index 1d5e2bd..b20112c 100755
--- a/ModWinDog/Internet/Internet.py
+++ b/ModWinDog/Internet/Internet.py
@@ -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={
diff --git a/ModWinDog/Multifun/Multifun.py b/ModWinDog/Multifun/Multifun.py
index c2c255f..761e1eb 100755
--- a/ModWinDog/Multifun/Multifun.py
+++ b/ModWinDog/Multifun/Multifun.py
@@ -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),
])
diff --git a/ModWinDog/Percenter/Percenter.py b/ModWinDog/Percenter/Percenter.py
index 1859632..a76dc6c 100755
--- a/ModWinDog/Percenter/Percenter.py
+++ b/ModWinDog/Percenter/Percenter.py
@@ -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),
])
diff --git a/ModWinDog/Scrapers/Scrapers.py b/ModWinDog/Scrapers/Scrapers.py
index 9879090..a52ccab 100755
--- a/ModWinDog/Scrapers/Scrapers.py
+++ b/ModWinDog/Scrapers/Scrapers.py
@@ -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),
])
diff --git a/ModWinDog/Scripting/Scripting.py b/ModWinDog/Scripting/Scripting.py
index 99fd80c..e1f753b 100755
--- a/ModWinDog/Scripting/Scripting.py
+++ b/ModWinDog/Scripting/Scripting.py
@@ -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),
])
diff --git a/ModWinDog/Start/Start.py b/ModWinDog/Start/Start.py
index 12df3f4..68ee5ac 100755
--- a/ModWinDog/Start/Start.py
+++ b/ModWinDog/Start/Start.py
@@ -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),
])
diff --git a/ModWinDog/System/System.py b/ModWinDog/System/System.py
index 01527ba..9dc402b 100755
--- a/ModWinDog/System/System.py
+++ b/ModWinDog/System/System.py
@@ -28,12 +28,12 @@ def cExec(context:EventContext, data:InputMessageData):
return send_message(context, {"text_html": f'{html_escape(text)}'}) 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), ]) diff --git a/README.md b/README.md index 68a0d72..de3bf6f 100755 --- a/README.md +++ b/README.md @@ -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: diff --git a/WinDog.py b/WinDog.py index 3bdb8fc..76887f3 100755 --- a/WinDog.py +++ b/WinDog.py @@ -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()) diff --git a/WinDog.yaml b/WinDog.yaml index b5d2b2b..a7ca34c 100755 --- a/WinDog.yaml +++ b/WinDog.yaml @@ -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