From 459496d1984152fc46871a8eecd2416f589559c6 Mon Sep 17 00:00:00 2001 From: cynic Date: Thu, 30 Dec 2021 18:48:17 -0500 Subject: [PATCH] api stuff werks, client will idle and get events. next comes plugin integration! --- .gitignore | 3 ++- README.md | 14 ++++++++++++++ client.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ disc_api.py | 2 +- grim_logger.py | 26 ++++++++++++++++++++++++++ main.py | 18 +++--------------- 6 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 client.py create mode 100644 grim_logger.py diff --git a/.gitignore b/.gitignore index feae5c1..a9a8efc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc -__pycache__ \ No newline at end of file +__pycache__ +.vscode \ No newline at end of file diff --git a/README.md b/README.md index fbd04c2..1b98a90 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # modular-discord-bot-fw framework for discord bots that uses contained files for individual feature plugins that can be modified and reloaded on the fly + +basic structure: +* client class has implementations for handling opcodes + * event response is up to the client (!!) + * this lets us expose events for plugins to hook into, but keeps core API interaction defined in-class + * any and all bot features should be useable via independent feature plugins + * these are dynamically found and run, so they can be hotloaded, modified, etc. without disconnecting or restarting the whole bot session + +todo before it's basically finished: +* basic rate limiting +* plugin manager & hook integration +* make sure broken sockets are properly addressed with a resume or reconnect +* default plugins so the bot does _something_ out of the box + * plugin reloading, user (command) authentication, etc. will also probably be written in plugin form \ No newline at end of file diff --git a/client.py b/client.py new file mode 100644 index 0000000..f5fc131 --- /dev/null +++ b/client.py @@ -0,0 +1,48 @@ +import websockets, asyncio, json +from random import randint +import disc_api +from grim_logger import glog, glog_level +class client(): + def __init__(self, token): + self.logger = glog(level = glog_level.TRACE) + self.ws = None + self.last_sequence = None + self.session_id = None + self.token = token + self.user = None + async def shit(self): + # https://discord.com/developers/docs/topics/gateway#connecting-to-the-gateway + # you're supposed to ask an HTTP endpoint what the ws gateway is but the docs didnt tell me the domain after 1sec so HAHA NOPE + async with websockets.connect("wss://gateway.discord.gg/?encoding=json&v=9") as self.ws:# they seem to be fond of making new versions randomly and still supporting old ones? + while True: # they may or may not drop support for this soon # nevermind we're on v9 now + data = json.loads(await self.ws.recv()) + handler = getattr(self, "op_"+disc_api.gateway_opcodes[data["op"]], None) + self.logger.write(data) + self.last_sequence = data["s"] + if handler: + await handler(data) + async def heartbeat(self, interval): + while True: + self.logger.write(interval/1000) + await self.ws.send(json.dumps({"op":1, "d":self.last_sequence})) + await asyncio.sleep(interval/1000) + self.logger.write("sent heartbeat") + async def op_hello(self, res): + if not(self.last_sequence and self.session_id): + interval = res["d"]["heartbeat_interval"] + asyncio.get_event_loop().create_task(self.heartbeat(interval)) + await asyncio.sleep(1) + # https://discord.com/developers/docs/topics/gateway#identify-example-identify + await self.ws.send(json.dumps({"op":2, "d":{"token":self.token, "intents": 513, "properties":{"$os":"linux", "browser":"mdbf", "device":"mdbf"}}})) + else: + # https://discord.com/developers/docs/topics/gateway#identify-example-identify + await self.ws.send(json.dumps({"op":6, "d":{"token":self.token, "session_id":self.session_id, "seq":self.last_sequence}})) + async def op_invalid_session(self, res): + await asyncio.sleep(randint(1, 5)) # try again + await self.ws.send(json.dumps({"op":2, "d":{"token":self.token, "intents": 513, "properties":{"$os":"linux", "browser":"mdbf", "device":"mdbf"}}})) + async def op_dispatch(self, res): + if res["t"] == "READY": + self.session_id = res["d"]["session_id"] + self.user = res["d"]["user"] + def run(self): + asyncio.run(self.shit()) \ No newline at end of file diff --git a/disc_api.py b/disc_api.py index ab26202..1e171cf 100644 --- a/disc_api.py +++ b/disc_api.py @@ -26,4 +26,4 @@ gateway_close_event_codes = { # https://discord.com/developers/docs/topics/opcod 4012: "You sent an invalid version for the gateway.", 4013: "You sent an invalid intent for a Gateway Intent. You may have incorrectly calculated the bitwise value.", 4014: "You sent a disallowed intent for a Gateway Intent. You may have tried to specify an intent that you have not enabled or are not approved for." -} +} \ No newline at end of file diff --git a/grim_logger.py b/grim_logger.py new file mode 100644 index 0000000..464a619 --- /dev/null +++ b/grim_logger.py @@ -0,0 +1,26 @@ +from enum import Enum +from inspect import stack +from sys import stdout +from datetime import datetime +class glog_flags(Enum): + METHOD_PRINT = 0 + METHOD_FILE = 1 +class glog_level(Enum): + PLAIN = 0 + TRACE = 1 +class glog(): + def __init__(self, flags = [glog_flags.METHOD_PRINT], filename = "bot.log", level = glog_level.PLAIN): + if not flags: raise ValueError("no glog_flags given!") + self.targets = [] + if glog_flags.METHOD_PRINT in flags: + self.targets.append(stdout) + if glog_flags.METHOD_FILE in flags: + self.targets.append(open(filename, "a")) + self.level = level + def __del__(self): + for target in self.targets: + if not(target == stdout): + target.close() + def write(self, message): + for target in self.targets: + target.write("({2})@[{0}]: {1}\n".format(stack()[1].function if self.level == glog_level.TRACE else "glog", message, datetime.now())) \ No newline at end of file diff --git a/main.py b/main.py index f607c0e..392fa5d 100644 --- a/main.py +++ b/main.py @@ -1,15 +1,3 @@ -import websockets, asyncio, json -import disc_api -class client(): - def __init__(self): - pass - async def shit(): - # https://discord.com/developers/docs/topics/gateway#connecting-to-the-gateway - # you're supposed to ask an HTTP endpoint what the ws gateway is but the docs didnt tell me the domain after 1sec so HAHA NOPE - async with websockets.connect("wss://gateway.discord.gg/?encoding=json&v=9") as ws: # they seem to be fond of making new versions randomly and still supporting old ones? - while True: # they may or may not drop support for this soon - data = (await ws.recv()) - print(data) - break - def run(): - asyncio.run(shit()) +from client import client +olive = client("OTIxMDg0ODA3Mjk4MDU2MjAz.YbtxEg.NxS1h63qKDeTn_4voc2Axa_QIFI") +olive.run() \ No newline at end of file