From 29d60f3cb96cf73cb0168f1c0bc124b2b93ce6b4 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Sat, 20 Jun 2020 11:17:55 +0200 Subject: [PATCH] =?UTF-8?q?D=C3=A9but=20de=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Pipfile | 1 + Pipfile.lock | 4 +- src/backends/IRC/channel.py | 13 +++ src/backends/IRC/irc.py | 51 ++++++++++++ src/backends/IRC/message.py | 18 +++++ src/backends/abc/__init__.py | 5 ++ src/backends/abc/channel.py | 15 ++++ src/backends/abc/client.py | 12 +++ src/backends/abc/messagable.py | 6 ++ src/backends/abc/message.py | 19 +++++ src/backends/abc/server.py | 2 + src/backends/abc/status.py | 36 +++++++++ src/backends/abc/user.py | 5 ++ src/backends/discord/__init__.py | 3 + src/backends/discord/channel.py | 20 +++++ src/backends/discord/discord.py | 131 +++++++++++++++++++++++++++++++ src/backends/discord/gateway.py | 38 +++++++++ src/backends/discord/intents.py | 24 ++++++ src/backends/discord/message.py | 52 ++++++++++++ src/backends/discord/user.py | 12 +++ src/bot_base/bot_base.py | 35 ++++++--- src/main.py | 17 ++-- 22 files changed, 494 insertions(+), 25 deletions(-) create mode 100644 src/backends/IRC/channel.py create mode 100644 src/backends/IRC/irc.py create mode 100644 src/backends/IRC/message.py create mode 100644 src/backends/abc/__init__.py create mode 100644 src/backends/abc/channel.py create mode 100644 src/backends/abc/client.py create mode 100644 src/backends/abc/messagable.py create mode 100644 src/backends/abc/message.py create mode 100644 src/backends/abc/server.py create mode 100644 src/backends/abc/status.py create mode 100644 src/backends/abc/user.py create mode 100644 src/backends/discord/__init__.py create mode 100644 src/backends/discord/channel.py create mode 100644 src/backends/discord/discord.py create mode 100644 src/backends/discord/gateway.py create mode 100644 src/backends/discord/intents.py create mode 100644 src/backends/discord/message.py create mode 100644 src/backends/discord/user.py diff --git a/Pipfile b/Pipfile index dd876ab..db1645a 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,7 @@ name = "pypi" [packages] packaging = "*" toml = "*" +websockets = "*" "discord.py" = {version = "*",extras = ["voice",]} [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 3269d33..1f331b5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "267e2db73eae9033f8531715235a968fe4ddd2ca4f09ccc266d5f6da869acb23" + "sha256": "1988f2a813c0947b137f1ac7e09d95507de3ca9c6a57ebfbd333414839a281e5" }, "pipfile-spec": 6, "requires": { @@ -223,7 +223,7 @@ "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" ], - "markers": "python_full_version >= '3.6.1'", + "index": "pypi", "version": "==8.1" }, "yarl": { diff --git a/src/backends/IRC/channel.py b/src/backends/IRC/channel.py new file mode 100644 index 0000000..d41e4cf --- /dev/null +++ b/src/backends/IRC/channel.py @@ -0,0 +1,13 @@ +from backends import abc + + +class Channel(abc.Channel): + id: str + + async def _send(self, message): + print(f"send: {message}") + print(type(message)) + await self.client.send_raw(f"PRIVMSG {self.id} :{message.content}\r\n".encode()) + + def from_irc_id(self, id): + self.id = id.decode() \ No newline at end of file diff --git a/src/backends/IRC/irc.py b/src/backends/IRC/irc.py new file mode 100644 index 0000000..0f979c0 --- /dev/null +++ b/src/backends/IRC/irc.py @@ -0,0 +1,51 @@ +import asyncio + +from backends.IRC.message import Message +from backends.abc.client import Client + + +class IRCHandler(asyncio.Protocol): + def __init__(self, client): + self.transport = None + self.client = client + + def connection_made(self, transport): + self.transport = transport + self.login() + + def data_received(self, data): + print(data.decode()) + if data.startswith(b"PING"): + self.transport.write(b"data".replace(b"PING", b"PONG")) + if b"PRIVMSG" in data: + message = Message(self.client) + message.from_irc(data) + self.client.dispatch("message", message) + + def eof_received(self): + pass + + def login(self): + self.transport.write(f"USER bot 0 * :BOTOX\r\n".encode()) + self.transport.write(f"NICK {self.client.nick}\r\n".encode()) + self.transport.write(f"JOIN #general\r\n".encode()) + + def send(self, data): + print(data) + self.transport.write(data) + + +class IRC(Client): + def __init__(self, server, port, password=None, nick="I_AM_A_BOT", user=None, mode=None, unused=None, realname=None, loop=asyncio.get_event_loop()): + self.server = server + self.port = port + self.password = password + self.nick = nick + self.connection_handler = IRCHandler(self) + self.loop = loop + + async def run(self, loop=asyncio.get_event_loop()): + await loop.create_connection(lambda: self.connection_handler, self.server, self.port) + + async def send_raw(self, data): + self.loop.run_in_executor(None, lambda: self.connection_handler.send(data)) \ No newline at end of file diff --git a/src/backends/IRC/message.py b/src/backends/IRC/message.py new file mode 100644 index 0000000..7a1bb77 --- /dev/null +++ b/src/backends/IRC/message.py @@ -0,0 +1,18 @@ +from backends import abc +from backends.IRC.channel import Channel +from backends.abc import User + + +class Message(abc.Message): + def from_irc(self,data): + # :Fomys!~fomys@192.168.0.14 PRIVMSG #general :Pouet + message = data.split(b":")[-1] + self.content = message.decode() + channel = data.split(b" ")[2] + print(channel) + if channel.decode() == self.client.nick: + channel = data.split(b" ")[0][1:].split(b"!")[0] + self.channel = Channel(self.client) + self.channel.from_irc_id(channel) + self.author = User(self.client) + diff --git a/src/backends/abc/__init__.py b/src/backends/abc/__init__.py new file mode 100644 index 0000000..abf4d48 --- /dev/null +++ b/src/backends/abc/__init__.py @@ -0,0 +1,5 @@ +from .client import Client +from .message import Message +from .server import Server +from .channel import Channel +from .user import User \ No newline at end of file diff --git a/src/backends/abc/channel.py b/src/backends/abc/channel.py new file mode 100644 index 0000000..24b90d1 --- /dev/null +++ b/src/backends/abc/channel.py @@ -0,0 +1,15 @@ +from backends.abc import Message + + +class Channel: + def __init__(self, client): + self.client = client + + async def send(self, message): + if type(message) == str: + await self._send(Message(self.client, content=message)) + return + await self._send(message) + + async def _send(self, message): + pass diff --git a/src/backends/abc/client.py b/src/backends/abc/client.py new file mode 100644 index 0000000..6b9c7a8 --- /dev/null +++ b/src/backends/abc/client.py @@ -0,0 +1,12 @@ +from backends.abc.status import Status + + +class Client: + async def run(self): + pass + + async def set_status(self, status: Status): + pass + + def set_dispatch_handler(self, handler): + self.dispatch = handler diff --git a/src/backends/abc/messagable.py b/src/backends/abc/messagable.py new file mode 100644 index 0000000..d8b1dfa --- /dev/null +++ b/src/backends/abc/messagable.py @@ -0,0 +1,6 @@ +from backends.abc import Message + + +class Messagable: + async def send(self, message: Message): + pass \ No newline at end of file diff --git a/src/backends/abc/message.py b/src/backends/abc/message.py new file mode 100644 index 0000000..0ceae50 --- /dev/null +++ b/src/backends/abc/message.py @@ -0,0 +1,19 @@ +from .server import Server +from .user import User + + +class Message: + server: Server + channel = None + author: User + + content: str + timestamp: int + + def __init__(self, client, server=None, channel=None, author=None, content=None, timestamp=None): + self.client = client + self.server = server + self.channel = channel + self.author = author + self.content = content + self.timestamp = timestamp diff --git a/src/backends/abc/server.py b/src/backends/abc/server.py new file mode 100644 index 0000000..5bbd429 --- /dev/null +++ b/src/backends/abc/server.py @@ -0,0 +1,2 @@ +class Server: + pass \ No newline at end of file diff --git a/src/backends/abc/status.py b/src/backends/abc/status.py new file mode 100644 index 0000000..c2575a5 --- /dev/null +++ b/src/backends/abc/status.py @@ -0,0 +1,36 @@ +import typing + + +class StatusType: + ONLINE = 0 + DO_NOT_DISTURB = 1 + IDLE = 2 + INVISIBLE = 3 + OFFLINE = 4 + + +class ActivityType: + GAME = 0 + STREAMING = 1 + LISTENING = 2 + CUSTOM = 4 + + +class Activity: + name: str + type: ActivityType + + def __init__(self, name, activity_type): + self.name = name + self.type = activity_type + + +class Status: + activity: Activity + status: StatusType + afk: bool + + def __init__(self, status, activity=None, afk=False): + self.activity = activity + self.status = status + self.afk = afk diff --git a/src/backends/abc/user.py b/src/backends/abc/user.py new file mode 100644 index 0000000..a1b33ef --- /dev/null +++ b/src/backends/abc/user.py @@ -0,0 +1,5 @@ +class User: + bot: bool = False + + def __init__(self, client): + self.client = client diff --git a/src/backends/discord/__init__.py b/src/backends/discord/__init__.py new file mode 100644 index 0000000..f62e42b --- /dev/null +++ b/src/backends/discord/__init__.py @@ -0,0 +1,3 @@ +from .discord import Discord +from .intents import Intents +from .gateway import Gateway \ No newline at end of file diff --git a/src/backends/discord/channel.py b/src/backends/discord/channel.py new file mode 100644 index 0000000..d507870 --- /dev/null +++ b/src/backends/discord/channel.py @@ -0,0 +1,20 @@ +import json + +import aiohttp + +from .. import abc + + +class Channel(abc.Channel): + filled: bool = False + id: int + populated: bool + + def from_discord_id(self, id_): + self.id = id_ + self.populated = False + + async def _send(self, message): + form = aiohttp.FormData() + form.add_field('payload_json', json.dumps({"content": message.content})) + await self.client.api_call(f"/channels/{self.id}/messages", method="POST", data=form) diff --git a/src/backends/discord/discord.py b/src/backends/discord/discord.py new file mode 100644 index 0000000..d9b07dd --- /dev/null +++ b/src/backends/discord/discord.py @@ -0,0 +1,131 @@ +import asyncio + +import time + +import aiohttp + +from .message import Message +from ..abc import Client +from ..abc.status import Activity, Status, StatusType, ActivityType + +from .intents import Intents +from .gateway import Gateway + + +class Discord(Client): + def __init__(self, token, intents=Intents.DEFAULTS, loop=asyncio.get_event_loop()): + self.token = token + self.api_root = "https://discord.com/api" + self.intents = intents + self.loop = loop + self.gateway_root = None + self.gateway = None + self.heartbeat_interval = None + self.heartbeat = None + self.dispatch = lambda *x, **y: None + self.last_seq = 0 + + async def api_call(self, path, method="GET", **kwargs): + headers = { + "Authorization": f"Bot {self.token}", + "User-Agent": "Bot" + } + async with aiohttp.ClientSession() as session: + async with session.request(method, self.api_root + path, headers=headers, **kwargs) as response: + try: + assert 200 <= response.status < 300 + if response.status in [200, 201]: + return await response.json() + except Exception as e: + if response.status == 400: + raise Exception("Status = 400") + elif response.status == 403: + raise Exception("Status = 403") + else: + raise Exception("Chépa") + + async def get_gateway_root(self): + return (await self.api_call("/gateway"))["url"] + + async def run(self): + self.gateway_root = await self.get_gateway_root() + self.gateway = Gateway(self.gateway_root, loop=self.loop) + self.heartbeat = asyncio.create_task(self.__heartbeat()) + async for data in self.gateway.run(): + self.last_seq = data.get("s") or self.last_seq + await self.handle_receive(data) + + async def handle_receive(self, data): + if data.get("op") == 0: + self.handle_event(data) + elif data.get("op") == 1: + await self.heartbeat_ack() + elif data.get("op") == 10: + self.heartbeat_interval = data.get("d", {}).get("heartbeat_interval", None) + await self.identify() + elif data.get("op") == 11: + # Ping ack + pass + else: + pass + + async def set_status(self, status: Status): + await self.gateway.send({ + "op": 3, + "d": self._to_discord_status(status) + }) + + async def identify(self): + await self.gateway.send({ + "op": 2, + "d": { + "token": f"{self.token}", + "properties": { + "$os": "linux", + "$browser": "PBA", + "$device": "PBA" + }, + "large_threshold": 250, + "guild_subscriptions": True, + "intents": self.intents + } + }) + + async def __heartbeat(self): + while True: + await asyncio.sleep((self.heartbeat_interval or 1000) / 1000) + if not self.gateway.closed: + await self.gateway.send({"op": 1, "d": self.last_seq}) + + async def heartbeat_ack(self): + await self.gateway.send({"op": 11}) + + def handle_event(self, data): + self.last_seq = data.get("s") + event_name = data.get("t") + print(f"Event: {event_name}") + if event_name == "MESSAGE_CREATE": + message = Message(self) + message.from_raw_discord(data.get("d")) + self.dispatch("message", message) + + @staticmethod + def _to_discord_status(status): + data = {} + status_name = "" + if status.status == StatusType.ONLINE: + status_name = "online" + elif status.status == StatusType.DO_NOT_DISTURB: + status_name = "dnd" + elif status.status == StatusType.IDLE: + status_name = "idle" + elif status.status == StatusType.INVISIBLE: + status_name = "invisible" + elif status.status == StatusType.OFFLINE: + status_name = "offline" + data.update({"status": status_name}) + data.update({"afk": status.afk}) + if status.activity: + data.update( + {"game": {"name": status.activity.name, "type": status.activity.type}, "since": time.time()}) + return data diff --git a/src/backends/discord/gateway.py b/src/backends/discord/gateway.py new file mode 100644 index 0000000..5b143d3 --- /dev/null +++ b/src/backends/discord/gateway.py @@ -0,0 +1,38 @@ +import asyncio +import json +import traceback + +import websockets + + +class Gateway: + websocket: websockets.WebSocketClientProtocol + + def __init__(self, root, version=6, encoding="json", loop=asyncio.get_event_loop()): + self.root = root + self.version = version + self.encoding = encoding + self.loop = loop + self.websocket = None + + @property + def url(self): + return f"{self.root}?v={self.version}&encoding={self.encoding}" + + async def run(self): + self.websocket = await websockets.connect(self.url) + while True: + message = await self.websocket.recv() + data = json.loads(message) + yield data + + async def send(self, content): + try: + if self.websocket is not None: + await self.websocket.send(json.dumps(content)) + except Exception: + traceback.print_exc() + + @property + def closed(self): + return self.websocket.closed diff --git a/src/backends/discord/intents.py b/src/backends/discord/intents.py new file mode 100644 index 0000000..880e0ba --- /dev/null +++ b/src/backends/discord/intents.py @@ -0,0 +1,24 @@ +class Intents: + GUILDS = 1 << 0 + GUILD_MEMBERS = 1 << 1 + GUILD_BANS = 1 << 2 + GUILD_EMOJIS = 1 << 3 + GUILD_INTEGRATIONS = 1 << 4 + GUILD_WEBHOOKS = 1 << 5 + GUILD_INVITES = 1 << 6 + GUILD_VOICE_STATES = 1 << 7 + GUILD_PRESENCES = 1 << 8 + GUILD_MESSAGES = 1 << 9 + GUILD_MESSAGE_REACTIONS = 1 << 10 + GUILD_MESSAGE_TYPING = 1 << 11 + DIRECT_MESSAGES = 1 << 12 + DIRECT_MESSAGE_REACTIONS = 1 << 13 + DIRECT_MESSAGE_TYPING = 1 << 14 + + ALL = GUILDS | GUILD_MEMBERS | GUILD_BANS | GUILD_EMOJIS | GUILD_INTEGRATIONS | GUILD_WEBHOOKS | GUILD_INVITES | \ + GUILD_VOICE_STATES | GUILD_PRESENCES | GUILD_MESSAGES | GUILD_MESSAGE_REACTIONS | GUILD_MESSAGE_TYPING | \ + DIRECT_MESSAGES | DIRECT_MESSAGE_REACTIONS | DIRECT_MESSAGE_TYPING + + DEFAULTS = GUILDS | GUILD_BANS | GUILD_EMOJIS | GUILD_INTEGRATIONS | GUILD_WEBHOOKS | GUILD_INVITES | \ + GUILD_VOICE_STATES | GUILD_MESSAGES | GUILD_MESSAGE_REACTIONS | GUILD_MESSAGE_TYPING | \ + DIRECT_MESSAGES | DIRECT_MESSAGE_REACTIONS | DIRECT_MESSAGE_TYPING diff --git a/src/backends/discord/message.py b/src/backends/discord/message.py new file mode 100644 index 0000000..bea4c80 --- /dev/null +++ b/src/backends/discord/message.py @@ -0,0 +1,52 @@ +from pprint import pprint + +from .channel import Channel +from .user import User +from .. import abc + + +class Message(abc.Message): + populated: bool = False + id: int + tts: bool + mention_everyone: bool + mentions: list + mention_roles: list + mention_channels: list + attachments: list + embeds: list + reactions: list + nonce: list + pinned: bool + webhook_id: int + type: int + activity: int + application: int + message_reference: int + flags: int + + def from_raw_discord(self, data): + self.id = data.get("id") + self.author = User(self.client) + self.author.from_discord_raw(data.get("author")) + self.channel = Channel(self.client) + self.channel.from_discord_id(data.get("channel_id")) + self.content = data.get("content") + self.timestamp = data.get("timestamp") + self.tts = data.get("tts") + self.mention_everyone = data.get("mention_everyone") + self.mentions = data.get("mentions") + self.mention_roles = data.get("mention_roles") + self.mention_channels = data.get("mention_channels") + self.attachments = data.get("attachments") + self.embeds = data.get("embeds") + self.reactions = data.get("reactions") + self.nonce = data.get("nonce") + self.pinned = data.get("pinned") + self.webhook_id = data.get("webhook_id") + self.type = data.get("type") + self.activity = data.get("activity") + self.application = data.get("application") + self.message_reference = data.get("message_reference") + self.flags = data.get("flags") + self.populated = True \ No newline at end of file diff --git a/src/backends/discord/user.py b/src/backends/discord/user.py new file mode 100644 index 0000000..c761535 --- /dev/null +++ b/src/backends/discord/user.py @@ -0,0 +1,12 @@ +from .. import abc + + +class User(abc.User): + id: int + bot: bool + discriminator: str + + def from_discord_raw(self, data): + self.id = data.get("id") + self.bot = data.get("bot") + self.discriminator = data.get("discriminator") diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index d191a94..55cc380 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -1,34 +1,28 @@ from __future__ import annotations -import importlib -import inspect +import asyncio import logging import os -import sys import traceback import discord -import toml -from packaging.specifiers import SpecifierSet, InvalidSpecifier from bot_base.modules import ModuleManager from config import Config, config_types from config.config_types import factory -import errors __version__ = "0.2.0" -class BotBase(discord.Client): +class BotBase(): log = None - def __init__(self, data_folder: str = "data", modules_folder: str = "modules", *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, data_folder: str = "datas", modules_folder: str = "modules", loop = asyncio.get_event_loop()): # Create folders os.makedirs(modules_folder, exist_ok=True) os.makedirs(data_folder, exist_ok=True) - # Add module folder to search path - # TODO: Vérifier que ca ne casse rien + self.backends = [] + # Setup logging self.log = logging.getLogger('bot_base') @@ -46,14 +40,19 @@ class BotBase(discord.Client): self.modules = ModuleManager(self) + self.loop = loop + self.modules.load_modules() + + def is_ready(self): + return False + async def on_ready(self): self.info("Bot ready.") - self.modules.load_modules() def dispatch(self, event, *args, **kwargs): """Dispatch event""" - super().dispatch(event, *args, **kwargs) for module in self.modules: + print(f"Dispatched: {event}\n{args}{kwargs}") module.dispatch(event, *args, **kwargs) async def on_error(self, event_method, *args, **kwargs): @@ -66,6 +65,7 @@ class BotBase(discord.Client): self.dispatch("log_info", info, *args, **kwargs) def error(self, e, *args, **kwargs): + print(e) if self.log: self.log.error(e, *args, **kwargs) self.dispatch("log_error", e, *args, **kwargs) @@ -84,3 +84,12 @@ class BotBase(discord.Client): path: config }) return config + + def register(self, backend): + self.backends.append(backend) + backend.set_dispatch_handler(self.dispatch) + + def run(self): + for back in self.backends: + asyncio.ensure_future(back.run()) + self.loop.run_forever() diff --git a/src/main.py b/src/main.py index 87604e5..d97a695 100644 --- a/src/main.py +++ b/src/main.py @@ -4,10 +4,12 @@ import json import logging import os +from backends.IRC.irc import IRC +from backends.discord.discord import Discord from bot_base.bot_base import BotBase -def setup_logging(default_path='data/log_config.json', default_level=logging.INFO, env_key='BOT_BASE_LOG_CONFIG'): +def setup_logging(default_path='datas/log_config.json', default_level=logging.INFO, env_key='BOT_BASE_LOG_CONFIG'): """Setup logging configuration """ path = default_path @@ -24,15 +26,10 @@ def setup_logging(default_path='data/log_config.json', default_level=logging.INF def main(): setup_logging() - print(os.environ.get("LOCAL_MODULES", "modules")) - client = BotBase(max_messages=500000, data_folder="datas") - - async def start_bot(): - await client.start(os.environ.get("DISCORD_TOKEN")) - - loop = asyncio.get_event_loop() - loop.create_task(start_bot()) - loop.run_forever() + client = BotBase() + client.register(Discord("NDcwNzI4NjAzMDEzNzQyNjAy.XuSCkg.8A6DEqpDj9pghFDefp9PEHlASnc")) + client.register(IRC("192.168.0.1", 6667, "toto")) + client.run() if __name__ == "__main__":