From b726d8f9f0612dc213a6a76f3b57b4a2b5a132ca Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 12 Apr 2019 23:14:16 +0000 Subject: [PATCH 01/33] Add !restart command to botrulez --- CHANGELOG.md | 2 ++ README.md | 2 +- yaboli/__init__.py | 5 +++-- yaboli/bot.py | 14 ++++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f754075..0437e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +- add !restart to botrulez + ## 0.2.0 (2019-04-12) - change config file format diff --git a/README.md b/README.md index 0c0b0bd..e779c40 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,6 @@ i. e. the text between the end of the "!echo" and the end of the whole message. ## TODOs -- [ ] implement !restart - [ ] document yaboli (markdown files in a "docs" folder?) - [ ] cookie support - [ ] fancy argument parsing @@ -74,3 +73,4 @@ i. e. the text between the end of the "!echo" and the end of the whole message. - [x] make it easier to run bots - [x] package in a distutils-compatible way (users should be able to install yaboli using `pip install git+https://github.com/Garmelon/yaboli`) +- [x] implement !restart diff --git a/yaboli/__init__.py b/yaboli/__init__.py index ac1c244..a964c61 100644 --- a/yaboli/__init__.py +++ b/yaboli/__init__.py @@ -52,7 +52,8 @@ def run( config_file: str = "bot.conf" ) -> None: async def _run() -> None: - client_ = client(config_file) - await client_.run() + while True: + client_ = client(config_file) + await client_.run() asyncio.run(_run()) diff --git a/yaboli/bot.py b/yaboli/bot.py index 7c96f29..de6af0e 100644 --- a/yaboli/bot.py +++ b/yaboli/bot.py @@ -20,6 +20,7 @@ class Bot(Client): HELP_GENERAL: Optional[str] = None HELP_SPECIFIC: Optional[List[str]] = None KILL_REPLY: str = "/me dies" + RESTART_REPLY: str = "/me restarts" GENERAL_SECTION = "general" ROOMS_SECTION = "rooms" @@ -102,6 +103,7 @@ class Bot(Client): help_: bool = True, uptime: bool = True, kill: bool = False, + restart: bool = False, ) -> None: if ping: self.register_general("ping", self.cmd_ping, args=False) @@ -120,6 +122,9 @@ class Bot(Client): if kill: self.register_specific("kill", self.cmd_kill, args=False) + if restart: + self.register_specific("restart", self.cmd_restart, args=False) + async def cmd_ping(self, room: Room, message: LiveMessage, @@ -161,3 +166,12 @@ class Bot(Client): logger.info(f"Killed in &{room.name} by {message.sender.atmention}") await message.reply(self.KILL_REPLY) await self.part(room) + + async def cmd_restart(self, + room: Room, + message: LiveMessage, + args: SpecificArgumentData + ) -> None: + logger.info(f"Restarted in &{room.name} by {message.sender.atmention}") + await message.reply(self.RESTART_REPLY) + await self.stop() From ac70f45229bf4f340c5ff13a47dd95f44e0fdec9 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 13 Apr 2019 00:22:42 +0000 Subject: [PATCH 02/33] Add some basic documentation --- README.md | 6 ++-- docs/index.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 docs/index.md diff --git a/README.md b/README.md index e779c40..5e2e933 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Yaboli (**Y**et **A**nother **Bo**t **Li**brary) is a Python library for creating bots for [euphoria.io](https://euphoria.io). - [Changelog](CHANGELOG.md) +- [Documentation](docs/index.md) -Soon, markdown files containing documentation and troubleshooting info will be -available. +Soon, markdown files containing troubleshooting info will be available. ## Installation @@ -62,7 +62,6 @@ i. e. the text between the end of the "!echo" and the end of the whole message. - [ ] cookie support - [ ] fancy argument parsing - [ ] document new classes (docstrings, maybe comments) -- [ ] write project readme - [ ] write examples - [ ] make yaboli package play nice with mypy - [x] implement !uptime for proper botrulez conformity @@ -74,3 +73,4 @@ i. e. the text between the end of the "!echo" and the end of the whole message. - [x] package in a distutils-compatible way (users should be able to install yaboli using `pip install git+https://github.com/Garmelon/yaboli`) - [x] implement !restart +- [x] write project readme diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..97d28a3 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,79 @@ +# Index for yaboli docs + +Links to specific sections will be added here. + +## Library structure overview + +### Message, Session + +A `Message` represents a single message. It contains all the fields [specified +in the API](http://api.euphoria.io/#message), in addition to a few utility +functions. + +Similar to a `Message`, a `Session` represents a session and also contains +almost all the fields [specified in the +API](http://api.euphoria.io/#sessionview), in addition to a few utility +functions. + +`Message`s and `Session`s also both contain the name of the room they +originated from. + +### Room + +A `Room` represents a single connection to a room on euphoria. It tries to keep +connected and reconnects if it loses connection. When connecting and +reconnecting, it automatically authenticates and sets a nick. + +In addition, a `Room` also keeps track of its own session and the sessions of +all other people and bots connected to the room. It doesn't remember any +messages though, since no "correct" solution to do that exists and the method +depends on the design of the bot using the `Room` (keeping the last few +messages in memory, storing messages in a database etc.). + +### LiveMessage, LiveSession + +`LiveMessage`s and `LiveSession`s function the same as `Message`s and +`Session`s, with the difference that they contain the `Room` object they +originated from, instead of just a room name. This allows them to also include +a few convenience functions, like `Message.reply`. + +Usually, `Room`s and `Client`s (and thus `Bot`s) will pass `LiveMessage`s and +`LiveSession`s instead of their `Message` and `Session` counterparts. + +### Client + +A `Client` may be connected to a few rooms on euphoria and thus manages a few +`Room` objects. It has functions for joining and leaving rooms on euphoria, and +it can also be connected to the same room multiple times (resulting in multiple +`Room` objects). + +The `Client` has a few `on_` functions (e. g. `on_message`, `on_join`) +that are triggered by events in any of the `Room` objects it manages. This +allows a `Client` to react to various things happening in its `Room`s. + +### Bot + +A `Bot` is a client that: + +- is configured using a config file +- reacts to commands using a command system +- implements most commands specified in the + [botrulez](https://github.com/jedevc/botrulez) + +The config file includes the bot's default nick, initial rooms and bot-specific +configuration. Upon starting a `Bot`, it joins the rooms specified in the +config, setting its nick to the default nick. + +The command system can react to general and specific commands as specified in +the botrulez, and can parse command arguments with or without bash-style string +escaping, and with or without unix-like syntax (flags and optional arguments). + +### Module, ModuleBot + +A `Module` is a `Bot` that can also be used as a module in a `ModuleBot`. This +is like combining multiple bots into a single bot. + +The most notable differences are the new `DESCRIPTION` and `standalone` fields. +The `DESCRIPTION` field contains a short description of the module, whereas the +`standalone` field answers the question whether the `Module` is being run as +standalone bot or part of a `ModuleBot`. From 7b7ddaa0d1a847b876b3c7288bfc1dbb9c5b79be Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 13 Apr 2019 00:29:49 +0000 Subject: [PATCH 03/33] Save bot config file --- CHANGELOG.md | 1 + yaboli/bot.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0437e49..7adfa30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Next version - add !restart to botrulez +- save (overwrite) `Bot` config file ## 0.2.0 (2019-04-12) diff --git a/yaboli/bot.py b/yaboli/bot.py index de6af0e..d08bcdd 100644 --- a/yaboli/bot.py +++ b/yaboli/bot.py @@ -26,8 +26,10 @@ class Bot(Client): ROOMS_SECTION = "rooms" def __init__(self, config_file: str) -> None: + self.config_file = config_file + self.config = configparser.ConfigParser(allow_no_value=True) - self.config.read(config_file) + self.config.read(self.config_file) nick = self.config[self.GENERAL_SECTION].get("nick") if nick is None: @@ -39,6 +41,10 @@ class Bot(Client): self.start_time = datetime.datetime.now() + def save_config(self) -> None: + with open(self.config_file, "w") as f: + self.config.write(f) + async def started(self) -> None: for room, password in self.config[self.ROOMS_SECTION].items(): if password is None: From e09e2d215f5847e58cad48717b1e943f97cedab9 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 13 Apr 2019 15:32:58 +0000 Subject: [PATCH 04/33] Add cookie support --- CHANGELOG.md | 1 + README.md | 2 +- yaboli/bot.py | 11 +++++-- yaboli/client.py | 30 ++++++++++++++--- yaboli/connection.py | 12 +++++-- yaboli/cookiejar.py | 77 ++++++++++++++++++++++++++++++++++++++++++++ yaboli/room.py | 5 +-- 7 files changed, 127 insertions(+), 11 deletions(-) create mode 100644 yaboli/cookiejar.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7adfa30..e42a1fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next version +- add cookie support - add !restart to botrulez - save (overwrite) `Bot` config file diff --git a/README.md b/README.md index 5e2e933..94bef94 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ i. e. the text between the end of the "!echo" and the end of the whole message. ## TODOs - [ ] document yaboli (markdown files in a "docs" folder?) -- [ ] cookie support - [ ] fancy argument parsing - [ ] document new classes (docstrings, maybe comments) - [ ] write examples @@ -74,3 +73,4 @@ i. e. the text between the end of the "!echo" and the end of the whole message. yaboli using `pip install git+https://github.com/Garmelon/yaboli`) - [x] implement !restart - [x] write project readme +- [x] cookie support diff --git a/yaboli/bot.py b/yaboli/bot.py index d08bcdd..3006773 100644 --- a/yaboli/bot.py +++ b/yaboli/bot.py @@ -33,9 +33,16 @@ class Bot(Client): nick = self.config[self.GENERAL_SECTION].get("nick") if nick is None: - logger.warn("No nick set in config file. Defaulting to empty nick") + logger.warn(("'nick' not set in config file. Defaulting to empty" + " nick")) nick = "" - super().__init__(nick) + + cookie_file = self.config[self.GENERAL_SECTION].get("cookie_file") + if cookie_file is None: + logger.warn(("'cookie_file' not set in config file. Using no cookie" + " file.")) + + super().__init__(nick, cookie_file=cookie_file) self._commands: List[Command] = [] diff --git a/yaboli/client.py b/yaboli/client.py index 5117c45..46777b0 100644 --- a/yaboli/client.py +++ b/yaboli/client.py @@ -1,7 +1,7 @@ import asyncio import functools import logging -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union from .message import LiveMessage from .room import Room @@ -12,8 +12,12 @@ logger = logging.getLogger(__name__) __all__ = ["Client"] class Client: - def __init__(self, default_nick: str) -> None: + def __init__(self, + default_nick: str, + cookie_file: Optional[str] = None, + ) -> None: self._default_nick = default_nick + self._cookie_file = cookie_file self._rooms: Dict[str, List[Room]] = {} self._stop = asyncio.Event() @@ -48,13 +52,31 @@ class Client: async def join(self, room_name: str, password: Optional[str] = None, - nick: Optional[str] = None + nick: Optional[str] = None, + cookie_file: Union[str, bool] = True, ) -> Optional[Room]: + """ + cookie_file is the name of the file to store the cookies in. If it is + True, the client default is used. If it is False, no cookie file name + will be used. + """ + logger.info(f"Joining &{room_name}") if nick is None: nick = self._default_nick - room = Room(room_name, password=password, target_nick=nick) + + this_cookie_file: Optional[str] + + if isinstance(cookie_file, str): # This way, mypy doesn't complain + this_cookie_file = cookie_file + elif cookie_file: + this_cookie_file = self._cookie_file + else: + this_cookie_file = None + + room = Room(room_name, password=password, target_nick=nick, + cookie_file=this_cookie_file) room.register_event("connected", functools.partial(self.on_connected, room)) diff --git a/yaboli/connection.py b/yaboli/connection.py index fbb354f..af31d1c 100644 --- a/yaboli/connection.py +++ b/yaboli/connection.py @@ -6,6 +6,7 @@ from typing import Any, Awaitable, Callable, Dict, Optional import websockets +from .cookiejar import CookieJar from .events import Events from .exceptions import * @@ -97,8 +98,9 @@ class Connection: # Initialising - def __init__(self, url: str) -> None: + def __init__(self, url: str, cookie_file: Optional[str] = None) -> None: self._url = url + self._cookie_jar = CookieJar(cookie_file) self._events = Events() self._packet_id = 0 @@ -181,7 +183,8 @@ class Connection: try: logger.debug(f"Creating ws connection to {self._url!r}") - ws = await websockets.connect(self._url) + ws = await websockets.connect(self._url, + extra_headers=self._cookie_jar.get_cookies_as_headers()) self._ws = ws self._awaiting_replies = {} @@ -189,6 +192,11 @@ class Connection: self._ping_check = asyncio.create_task( self._disconnect_in(self.PING_TIMEOUT)) + # Put received cookies into cookie jar + for set_cookie in ws.response_headers.get_all("Set-Cookie"): + self._cookie_jar.add_cookie(set_cookie) + self._cookie_jar.save() + return True except (websockets.InvalidHandshake, websockets.InvalidStatusCode, diff --git a/yaboli/cookiejar.py b/yaboli/cookiejar.py new file mode 100644 index 0000000..833dbcb --- /dev/null +++ b/yaboli/cookiejar.py @@ -0,0 +1,77 @@ +import contextlib +import http.cookies as cookies +import logging +from typing import List, Optional, Tuple + +logger = logging.getLogger(__name__) + +__all__ = ["CookieJar"] + +class CookieJar: + """ + Keeps your cookies in a file. + + CookieJar doesn't attempt to discard old cookies, but that doesn't appear + to be necessary for keeping euphoria session cookies. + """ + + def __init__(self, filename: Optional[str] = None) -> None: + self._filename = filename + self._cookies = cookies.SimpleCookie() + + if not self._filename: + logger.warning("Could not load cookies, no filename given.") + return + + with contextlib.suppress(FileNotFoundError): + logger.info(f"Loading cookies from {self._filename!r}") + with open(self._filename, "r") as f: + for line in f: + self._cookies.load(line) + + def get_cookies(self) -> List[str]: + return [morsel.OutputString(attrs=[]) + for morsel in self._cookies.values()] + + def get_cookies_as_headers(self) -> List[Tuple[str, str]]: + """ + Return all stored cookies as tuples in a list. The first tuple entry is + always "Cookie". + """ + + return [("Cookie", cookie) for cookie in self.get_cookies()] + + def add_cookie(self, cookie: str) -> None: + """ + Parse cookie and add it to the jar. + + Example cookie: "a=bcd; Path=/; Expires=Wed, 24 Jul 2019 14:57:52 GMT; + HttpOnly; Secure" + """ + + logger.debug(f"Adding cookie {cookie!r}") + self._cookies.load(cookie) + + def save(self) -> None: + """ + Saves all current cookies to the cookie jar file. + """ + + if not self._filename: + logger.warning("Could not save cookies, no filename given.") + return + + logger.info(f"Saving cookies to {self._filename!r}") + + with open(self._filename, "w") as f: + for morsel in self._cookies.values(): + cookie_string = morsel.OutputString() + f.write(f"{cookie_string}\n") + + def clear(self) -> None: + """ + Removes all cookies from the cookie jar. + """ + + logger.debug("OMNOMNOM, cookies are all gone!") + self._cookies = cookies.SimpleCookie() diff --git a/yaboli/room.py b/yaboli/room.py index 4196d5f..053e398 100644 --- a/yaboli/room.py +++ b/yaboli/room.py @@ -60,7 +60,8 @@ class Room: name: str, password: Optional[str] = None, target_nick: str = "", - url_format: str = URL_FORMAT + url_format: str = URL_FORMAT, + cookie_file: Optional[str] = None, ) -> None: self._name = name self._password = password @@ -78,7 +79,7 @@ class Room: # Connected management self._url = self._url_format.format(self._name) - self._connection = Connection(self._url) + self._connection = Connection(self._url, cookie_file=cookie_file) self._events = Events() self._connected = asyncio.Event() From 0d58f616527f45682891bb36779bd873ab26bd85 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 13 Apr 2019 15:36:02 +0000 Subject: [PATCH 05/33] Clean up readme --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 94bef94..ca1c982 100644 --- a/README.md +++ b/README.md @@ -3,24 +3,19 @@ Yaboli (**Y**et **A**nother **Bo**t **Li**brary) is a Python library for creating bots for [euphoria.io](https://euphoria.io). -- [Changelog](CHANGELOG.md) - [Documentation](docs/index.md) - -Soon, markdown files containing troubleshooting info will be available. +- [Changelog](CHANGELOG.md) ## Installation -Ensure that you have at least Python 3.7 installed. The commands below assume -that `python` points this version of Python. - -In your project directory, run: +Ensure that you have at least Python 3.7 installed. ``` -$ python -m venv . -$ . bin/activate $ pip install git+https://github.com/Garmelon/yaboli@v0.2.0 ``` +The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. + ## Example echo bot A simple echo bot that conforms to the @@ -43,7 +38,7 @@ class EchoBot(yaboli.Bot): await message.reply(args.raw) ``` -The bot's nick and default rooms are specified in a config file. +The bot's nick, cookie file and default rooms are specified in a config file. The help command from the botrulez uses the `HELP_GENERAL` and `HELP_SPECIFIC` fields. From 135640ca443f83f2378fa3c072acf9f02e6efd92 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 13 Apr 2019 20:23:02 +0000 Subject: [PATCH 06/33] Log in/out and pm --- CHANGELOG.md | 2 + yaboli/client.py | 6 +++ yaboli/room.py | 122 ++++++++++++++++++++++++++++++++++++++-------- yaboli/session.py | 10 +++- 4 files changed, 117 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42a1fe..812a225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +- add login and logout command to room +- add pm command to room - add cookie support - add !restart to botrulez - save (overwrite) `Bot` config file diff --git a/yaboli/client.py b/yaboli/client.py index 46777b0..75806fb 100644 --- a/yaboli/client.py +++ b/yaboli/client.py @@ -152,6 +152,12 @@ class Client: async def on_edit(self, room: Room, message: LiveMessage) -> None: pass + async def on_login(self, room: Room, account_id: str) -> None: + pass + + async def on_logout(self, room: Room) -> None: + pass + async def on_pm(self, room: Room, from_id: str, diff --git a/yaboli/room.py b/yaboli/room.py index 053e398..905162f 100644 --- a/yaboli/room.py +++ b/yaboli/room.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Any, Awaitable, Callable, List, Optional, TypeVar +from typing import Any, Awaitable, Callable, List, Optional, Tuple, TypeVar from .connection import Connection from .events import Events @@ -43,6 +43,12 @@ class Room: "edit" - a message in the room has been modified or deleted message: LiveMessage + "login" - this session has been logged in from another session + account_id: str + + "logout" - this session has been logged out from another session + no parameters + "pm" - another session initiated a pm with you from: str - the id of the user inviting the client to chat privately from_nick: str - the nick of the inviting user @@ -117,11 +123,22 @@ class Room: # Connecting, reconnecting and disconnecting - def _set_connected(self) -> None: + async def _try_set_connected(self) -> None: packets_received = self._hello_received and self._snapshot_received if packets_received and not self._connected.is_set(): - self._connected_successfully = True - self._connected.set() + await self._set_nick_if_necessary() + self._set_connected() + + async def _set_nick_if_necessary(self) -> None: + nick_needs_updating = (self._session is None + or self._target_nick != self._session.nick) + + if self._target_nick and nick_needs_updating: + await self._nick(self._target_nick) + + def _set_connected(self) -> None: + self._connected_successfully = True + self._connected.set() def _set_connected_failed(self) -> None: if not self._connected.is_set(): @@ -148,7 +165,7 @@ class Room: self._account = Account.from_data(data) self._hello_received = True - self._set_connected() + await self._try_set_connected() async def _on_snapshot_event(self, packet: Any) -> None: data = packet["data"] @@ -169,7 +186,7 @@ class Room: self._events.fire("session", messages) self._snapshot_received = True - self._set_connected() + await self._try_set_connected() async def _on_bounce_event(self, packet: Any) -> None: data = packet["data"] @@ -207,11 +224,6 @@ class Room: if not self._connected_successfully: return False - nick_needs_updating = (self._session is None - or self._target_nick != self._session.nick) - if self._target_nick and nick_needs_updating: - await self._nick(self._target_nick) - self._events.fire("connected") return True @@ -243,14 +255,34 @@ class Room: session = LiveSession.from_data(self, data) self._users = self.users.with_join(session) - logger.info(f"{session.atmention} joined") + logger.info(f"&{self.name}: {session.atmention} joined") self._events.fire("join", session) async def _on_login_event(self, packet: Any) -> None: - pass # TODO implement once cookie support is here + """ + Just reconnect, see + https://github.com/euphoria-io/heim/blob/master/client/lib/stores/chat.js#L275-L276 + """ + + data = packet["data"] + + account_id = data["account_id"] + + self._events.fire("login", account_id) + logger.info(f"&{self.name}: Got logged in to {account_id}, reconnecting") + + await self._connection.reconnect() async def _on_logout_event(self, packet: Any) -> None: - pass # TODO implement once cookie support is here + """ + Just reconnect, see + https://github.com/euphoria-io/heim/blob/master/client/lib/stores/chat.js#L275-L276 + """ + + self._events.fire("logout") + logger.info(f"&{self.name}: Got logged out, reconnecting") + + await self._connection.reconnect() async def _on_network_event(self, packet: Any) -> None: data = packet["data"] @@ -264,7 +296,7 @@ class Room: for user in self.users: if user.server_id == server_id and user.server_era == server_era: users = users.with_part(user) - logger.info(f"{user.atmention} left") + logger.info(f"&{self.name}: {user.atmention} left") self._events.fire("part", user) self._users = users @@ -281,7 +313,7 @@ class Room: else: await self.who() # recalibrating self._users - logger.info(f"{atmention(nick_from)} is now called {atmention(nick_to)}") + logger.info(f"&{self.name}: {atmention(nick_from)} is now called {atmention(nick_to)}") self._events.fire("nick", session, nick_from, nick_to) async def _on_edit_message_event(self, packet: Any) -> None: @@ -297,7 +329,7 @@ class Room: session = LiveSession.from_data(self, data) self._users = self.users.with_part(session) - logger.info(f"{session.atmention} left") + logger.info(f"&{self.name}: {session.atmention} left") self._events.fire("part", session) async def _on_pm_initiate_event(self, packet: Any) -> None: @@ -374,10 +406,6 @@ class Room: # Functionality - # These functions require cookie support and are thus not implemented yet: - # - # login, logout, pm - def _extract_data(self, packet: Any) -> Any: error = packet.get("error") if error is not None: @@ -477,3 +505,55 @@ class Room: self._users = users return self._users + + async def login(self, email: str, password: str) -> Tuple[bool, str]: + """ + Since euphoria appears to only support email authentication, this way + of logging in is hardcoded here. + + Returns whether the login was successful. If it was, the second + parameter is the account id. If it wasn't, the second parameter is the + reason why the login failed. + """ + + data: Any = { + "namespace": "email", + "id": email, + "password": password, + } + + reply = await self._connection.send("login", data) + data = self._extract_data(reply) + + success: bool = data["success"] + account_id_or_reason = data.get("account_id") or data["reason"] + + if success: + logger.info(f"&{self.name}: Logged in as {account_id_or_reason}") + else: + logger.info(f"&{self.name}: Failed to log in with {email} because {account_id_or_reason}") + + await self._connection.reconnect() + + return success, account_id_or_reason + + async def logout(self) -> None: + await self._connection.send("logout", {}) + + logger.info(f"&{self.name}: Logged out") + + await self._connection.reconnect() + + async def pm(self, user_id: str) -> Tuple[str, str]: + """ + Returns the pm_id of the pm and the nick of the person being pinged. + """ + + data = {"user_id": user_id} + + reply = await self._connection.send("pm-initiate", data) + data = self._extract_data(reply) + + pm_id = data["pm_id"] + to_nick = data["to_nick"] + return pm_id, to_nick diff --git a/yaboli/session.py b/yaboli/session.py index 5adcbcb..e59c81a 100644 --- a/yaboli/session.py +++ b/yaboli/session.py @@ -1,5 +1,6 @@ import re -from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional +from typing import (TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, + Optional, Tuple) from .util import mention, normalize @@ -238,7 +239,12 @@ class LiveSession(Session): # Live stuff - # TODO pm, once pm support is there. + async def pm(self) -> Tuple[str, str]: + """ + See Room.pm + """ + + return await self.room.pm(self.user_id) class LiveSessionListing: def __init__(self, room: "Room", sessions: Iterable[LiveSession]) -> None: From 24128a460a5ddb4939899ed4206ee596f9c3e690 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 13 Apr 2019 21:47:27 +0000 Subject: [PATCH 07/33] Add fancy argument parsing --- CHANGELOG.md | 1 + README.md | 2 +- yaboli/command.py | 94 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 812a225..eea3778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next version +- add fancy argument parsing - add login and logout command to room - add pm command to room - add cookie support diff --git a/README.md b/README.md index ca1c982..5cc43d8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,6 @@ i. e. the text between the end of the "!echo" and the end of the whole message. ## TODOs - [ ] document yaboli (markdown files in a "docs" folder?) -- [ ] fancy argument parsing - [ ] document new classes (docstrings, maybe comments) - [ ] write examples - [ ] make yaboli package play nice with mypy @@ -69,3 +68,4 @@ i. e. the text between the end of the "!echo" and the end of the whole message. - [x] implement !restart - [x] write project readme - [x] cookie support +- [x] fancy argument parsing diff --git a/yaboli/command.py b/yaboli/command.py index e355bb0..08ac3f7 100644 --- a/yaboli/command.py +++ b/yaboli/command.py @@ -23,9 +23,70 @@ __all__ = ["FancyArgs", "ArgumentData", "SpecificArgumentData", "CommandData", "SpecificCommandFunction", "SpecificCommand"] class FancyArgs(NamedTuple): + """ + The fancy argument parser supports arguments of the following formats: + + + FLAGS: + + These are one or more characters preceded by a single dash. Examples: + + -a, -fghf, -vv + + The fancy argument parser counts how often each character (also called + flag) appears. Each flag that appears once or more gets an entry in the + "flags" dict of the form: flags[flag] = amount + + Exception: A single dash ("-") is interpreted as a positional argument. + + + OPTIONAL: + + These are arguments of the form -- or --=, where + is the name of the optional argument and is its (optional) value. + + Due to this syntax, the may not include any "=" signs. + + The optional arguments are collected in a dict of the form: + + optional[name] = value or None + + If the optional argument included a "=" after the name, but no further + characters, its value is the empty string. If it didn't include a "=" after + the name, its value is None. + + If more than one optional argument appears with the same name, the last + argument's value is kept and all previous values discarded. + + + POSITIONAL: + + Positional arguments are all arguments that don't start with "-" or "--". + They are compiled in a list and ordered in the same order they appeared in + after the command. + + + RAW: + + At any time, a single "--" argument may be inserted. This separates the + positional and optional arguments and the flags from the raw arguments. All + arguments after the "--" are interpreted as raw arguments, even flags, + optional arguments and further "--"s. + + For example, consider the following arguments: + + ab -cd -c --ef=g --h i -- j --klm -nop -- qr + + positional: ["ab", "i"] + optional: {"ef": "g", "h": None} + flags: {"c": 2, "d": 1} + raw: ["j", "--klm", "-nop", "--", "qr"] + """ + positional: List[str] optional: Dict[str, Optional[str]] flags: Dict[str, int] + raw: List[str] class ArgumentData: def __init__(self, raw: str) -> None: @@ -94,7 +155,38 @@ class ArgumentData: return text.split() def _parse_fancy(self, args: List[str]) -> FancyArgs: - raise NotImplementedError # TODO + positional: List[str] = [] + optional: Dict[str, Optional[str]] = {} + flags: Dict[str, int] = {} + raw: List[str] = [] + + is_raw = False + + for arg in args: + # raw arguments + if is_raw: + raw.append(arg) + # raw arguments separator + elif arg == "--": + is_raw = True + # optional arguments + elif arg[:2] == "--": + split = arg[2:].split("=", maxsplit=1) + name = split[0] + value = split[1] if len(split) == 2 else None + optional[name] = value + # the "-" exception + elif arg == "-": + positional.append(arg) + # flags + elif arg[:1] == "-": + for char in arg[1:]: + flags[char] = flags.get(char, 0) + 1 + # positional arguments + else: + positional.append(arg) + + return FancyArgs(positional, optional, flags, raw) @property def raw(self) -> str: From 7e56de60da7e70627af76499ce390e17bb9ef1c3 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 13 Apr 2019 21:49:00 +0000 Subject: [PATCH 08/33] Clean up changelog and readme --- CHANGELOG.md | 3 ++- docs/index.md | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eea3778..fc01070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ - add pm command to room - add cookie support - add !restart to botrulez -- save (overwrite) `Bot` config file +- add Bot config file saving +- fix the Room not setting its nick correctly upon reconnecting ## 0.2.0 (2019-04-12) diff --git a/docs/index.md b/docs/index.md index 97d28a3..3311e56 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,10 +10,9 @@ A `Message` represents a single message. It contains all the fields [specified in the API](http://api.euphoria.io/#message), in addition to a few utility functions. -Similar to a `Message`, a `Session` represents a session and also contains -almost all the fields [specified in the -API](http://api.euphoria.io/#sessionview), in addition to a few utility -functions. +Similar to a `Message`, a `Session` represents a [session +view](http://api.euphoria.io/#sessionview) and also contains almost all the +fields specified in the API, in addition to a few utility functions. `Message`s and `Session`s also both contain the name of the room they originated from. From 2f7502723bcaaa36584582e7e054d349010e9aab Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 13 Apr 2019 22:09:58 +0000 Subject: [PATCH 09/33] Bump version to 1.0.0 --- CHANGELOG.md | 2 ++ README.md | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc01070..a77a9bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +# 1.0.0 (2019-04-13) + - add fancy argument parsing - add login and logout command to room - add pm command to room diff --git a/README.md b/README.md index 5cc43d8..c42a5d6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ creating bots for [euphoria.io](https://euphoria.io). Ensure that you have at least Python 3.7 installed. ``` -$ pip install git+https://github.com/Garmelon/yaboli@v0.2.0 +$ pip install git+https://github.com/Garmelon/yaboli@v1.0.0 ``` The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. diff --git a/setup.py b/setup.py index eb4d419..0ae307f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="yaboli", - version="0.2.0", + version="1.0.0", packages=["yaboli"], install_requires=["websockets==7.0"], ) From 86472afb3f6eeddfc6b22e8b76c89a98d92c238b Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Apr 2019 16:07:37 +0000 Subject: [PATCH 10/33] Pass along ConfigParsers instead of file names --- CHANGELOG.md | 3 ++ yaboli/__init__.py | 32 ++++++++++++++--- yaboli/bot.py | 15 ++++---- yaboli/module.py | 86 +++++++++++++++++++++++++++++++++++++--------- 4 files changed, 109 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a77a9bd..e8064c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Next version +- change how config files are passed along +- change module system to support config file changes + # 1.0.0 (2019-04-13) - add fancy argument parsing diff --git a/yaboli/__init__.py b/yaboli/__init__.py index a964c61..a9df9c2 100644 --- a/yaboli/__init__.py +++ b/yaboli/__init__.py @@ -1,6 +1,7 @@ import asyncio +import configparser import logging -from typing import Callable +from typing import Callable, Dict from .bot import * from .client import * @@ -48,12 +49,33 @@ def enable_logging(name: str = "yaboli", level: int = logging.INFO) -> None: logger.addHandler(handler) def run( - client: Callable[[str], Client], - config_file: str = "bot.conf" + bot_constructor: BotConstructor, + config_file: str = "bot.conf", ) -> None: + # Load the config file + config = configparser.ConfigParser(allow_no_value=True) + config.read(config_file) + async def _run() -> None: while True: - client_ = client(config_file) - await client_.run() + bot = bot_constructor(config, config_file) + await bot.run() + + asyncio.run(_run()) + +def run_modulebot( + modulebot_constructor: ModuleBotConstructor, + module_constructors: Dict[str, ModuleConstructor], + config_file: str = "bot.conf", + ) -> None: + # Load the config file + config = configparser.ConfigParser(allow_no_value=True) + config.read(config_file) + + async def _run() -> None: + while True: + modulebot = modulebot_constructor(config, config_file, + module_constructors) + await modulebot.run() asyncio.run(_run()) diff --git a/yaboli/bot.py b/yaboli/bot.py index 3006773..c696820 100644 --- a/yaboli/bot.py +++ b/yaboli/bot.py @@ -1,7 +1,7 @@ import configparser import datetime import logging -from typing import List, Optional +from typing import Callable, List, Optional from .client import Client from .command import * @@ -11,7 +11,7 @@ from .util import * logger = logging.getLogger(__name__) -__all__ = ["Bot"] +__all__ = ["Bot", "BotConstructor"] class Bot(Client): ALIASES: List[str] = [] @@ -25,12 +25,13 @@ class Bot(Client): GENERAL_SECTION = "general" ROOMS_SECTION = "rooms" - def __init__(self, config_file: str) -> None: + def __init__(self, + config: configparser.ConfigParser, + config_file: str, + ) -> None: + self.config = config self.config_file = config_file - self.config = configparser.ConfigParser(allow_no_value=True) - self.config.read(self.config_file) - nick = self.config[self.GENERAL_SECTION].get("nick") if nick is None: logger.warn(("'nick' not set in config file. Defaulting to empty" @@ -188,3 +189,5 @@ class Bot(Client): logger.info(f"Restarted in &{room.name} by {message.sender.atmention}") await message.reply(self.RESTART_REPLY) await self.stop() + +BotConstructor = Callable[[configparser.ConfigParser, str], Bot] diff --git a/yaboli/module.py b/yaboli/module.py index 2dc9b0f..ac750bf 100644 --- a/yaboli/module.py +++ b/yaboli/module.py @@ -1,5 +1,6 @@ +import configparser import logging -from typing import Dict, List, Optional +from typing import Callable, Dict, List, Optional from .bot import Bot from .command import * @@ -10,49 +11,77 @@ from .util import * logger = logging.getLogger(__name__) -__all__ = ["Module", "ModuleBot"] +__all__ = ["Module", "ModuleConstructor", "ModuleBot", "ModuleBotConstructor"] class Module(Bot): DESCRIPTION: Optional[str] = None - def __init__(self, config_file: str, standalone: bool) -> None: - super().__init__(config_file) + def __init__(self, + config: configparser.ConfigParser, + config_file: str, + standalone: bool = True, + ) -> None: + super().__init__(config, config_file) self.standalone = standalone +ModuleConstructor = Callable[[configparser.ConfigParser, str, bool], Module] + class ModuleBot(Bot): HELP_PRE: Optional[List[str]] = [ "This bot contains the following modules:" ] HELP_POST: Optional[List[str]] = [ - "" - "Use \"!help {atmention} \" to get more information on a" - " specific module." + "", + "For module-specific help, try \"!help {atmention} \".", ] MODULE_HELP_LIMIT = 5 - def __init__(self, config_file: str) -> None: - super().__init__(config_file) + MODULES_SECTION = "modules" + def __init__(self, + config: configparser.ConfigParser, + config_file: str, + module_constructors: Dict[str, ModuleConstructor], + ) -> None: + super().__init__(config, config_file) + + self.module_constructors = module_constructors self.modules: Dict[str, Module] = {} - self.register_botrulez(help_=False) - self.register_general("help", self.cmd_help_general, args=False) - self.register_specific("help", self.cmd_help_specific, args=True) + # Load initial modules + for module_name in self.config[self.MODULES_SECTION]: + module_constructor = self.module_constructors.get(module_name) + if module_constructor is None: + logger.warn(f"Module {module_name} not found") + continue + # standalone is set to False + module = module_constructor(self.config, self.config_file, False) + self.load_module(module_name, module) - def register_module(self, name: str, module: Module) -> None: + def load_module(self, name: str, module: Module) -> None: if name in self.modules: logger.warn(f"Module {name!r} is already registered, overwriting...") self.modules[name] = module + def unload_module(self, name: str) -> None: + if name in self.modules: + del self.modules[name] + + # Better help messages + def compile_module_overview(self) -> List[str]: lines = [] if self.HELP_PRE is not None: lines.extend(self.HELP_PRE) + any_modules = False + modules_without_desc: List[str] = [] for module_name in sorted(self.modules): + any_modules = True + module = self.modules[module_name] if module.DESCRIPTION is None: @@ -62,7 +91,10 @@ class ModuleBot(Bot): lines.append(line) if modules_without_desc: - lines.append(", ".join(modules_without_desc)) + lines.append("\t" + ", ".join(modules_without_desc)) + + if not any_modules: + lines.append("No modules loaded.") if self.HELP_POST is not None: lines.extend(self.HELP_POST) @@ -79,8 +111,7 @@ class ModuleBot(Bot): return module.HELP_SPECIFIC - # Overwriting the botrulez help function - async def cmd_help_specific(self, + async def cmd_modules_help(self, room: Room, message: LiveMessage, args: SpecificArgumentData @@ -100,6 +131,12 @@ class ModuleBot(Bot): # Sending along all kinds of events + async def on_connected(self, room: Room) -> None: + await super().on_connected(room) + + for module in self.modules.values(): + await module.on_connected(room) + async def on_snapshot(self, room: Room, messages: List[LiveMessage]) -> None: await super().on_snapshot(room, messages) @@ -141,6 +178,18 @@ class ModuleBot(Bot): for module in self.modules.values(): await module.on_edit(room, message) + async def on_login(self, room: Room, account_id: str) -> None: + await super().on_login(room, account_id) + + for module in self.modules.values(): + await module.on_login(room, account_id) + + async def on_logout(self, room: Room) -> None: + await super().on_logout(room) + + for module in self.modules.values(): + await module.on_logout(room) + async def on_pm(self, room: Room, from_id: str, @@ -158,3 +207,8 @@ class ModuleBot(Bot): for module in self.modules.values(): await module.on_disconnect(room, reason) + +ModuleBotConstructor = Callable[ + [configparser.ConfigParser, str, Dict[str, ModuleConstructor]], + Bot +] From 7780cb92dea9d305d2ce33e847227190a9075225 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Apr 2019 16:10:40 +0000 Subject: [PATCH 11/33] Update the docs --- docs/bot_setup.md | 13 +++++++++++++ docs/index.md | 13 ++++++++++++- examples/gitignore_with_venv | 13 +++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 docs/bot_setup.md create mode 100644 examples/gitignore_with_venv diff --git a/docs/bot_setup.md b/docs/bot_setup.md new file mode 100644 index 0000000..cf6722d --- /dev/null +++ b/docs/bot_setup.md @@ -0,0 +1,13 @@ +# Setting up and running a bot + +## Installing yaboli + +TODO + +## Configuring the bot + +TODO + +## Running the bot + +TODO diff --git a/docs/index.md b/docs/index.md index 3311e56..9f4835f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,17 @@ # Index for yaboli docs -Links to specific sections will be added here. + - [Setting up and running a bot](bot_setup.md) + - Classes + - [Bot](bot.md) + +## Getting started + +First, read the [overview](#library-structure-overview) below. + +To set up your project, follow the [setup guide](bot_setup.md). + +To get a feel for how bots are structured, have a look at the example bots or +read through the docstrings in the `Bot` class. ## Library structure overview diff --git a/examples/gitignore_with_venv b/examples/gitignore_with_venv new file mode 100644 index 0000000..191feb7 --- /dev/null +++ b/examples/gitignore_with_venv @@ -0,0 +1,13 @@ +# python stuff +__pycache__/ + +# venv stuff +bin/ +include/ +lib/ +lib64 +pyvenv.cfg + +# config files +*.conf +cookie_jar From 7e74499f8197ec48d58e0ed8c0172f9298d55708 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Apr 2019 16:12:33 +0000 Subject: [PATCH 12/33] Bump version to 1.1.0 --- CHANGELOG.md | 2 ++ README.md | 3 ++- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8064c5..5e18410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +## 1.1.0 (2019-04-14) + - change how config files are passed along - change module system to support config file changes diff --git a/README.md b/README.md index c42a5d6..c7ef69a 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,9 @@ creating bots for [euphoria.io](https://euphoria.io). Ensure that you have at least Python 3.7 installed. +To install yaboli or update your installation to the latest version, run: ``` -$ pip install git+https://github.com/Garmelon/yaboli@v1.0.0 +$ pip install git+https://github.com/Garmelon/yaboli@v1.1.0 ``` The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. diff --git a/setup.py b/setup.py index 0ae307f..bf3a4ab 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="yaboli", - version="1.0.0", + version="1.1.0", packages=["yaboli"], install_requires=["websockets==7.0"], ) From c579adca9a265675ae4eaab7201a979d28bd1363 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Apr 2019 19:27:16 +0000 Subject: [PATCH 13/33] Re-add database --- CHANGELOG.md | 2 ++ yaboli/__init__.py | 2 ++ yaboli/database.py | 40 ++++++++++++++++++++++++++++++++++++++++ yaboli/util.py | 12 ++++++++++-- 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 yaboli/database.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e18410..147d068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +- add database class for easier sqlite3 access + ## 1.1.0 (2019-04-14) - change how config files are passed along diff --git a/yaboli/__init__.py b/yaboli/__init__.py index a9df9c2..b138c88 100644 --- a/yaboli/__init__.py +++ b/yaboli/__init__.py @@ -7,6 +7,7 @@ from .bot import * from .client import * from .command import * from .connection import * +from .database import * from .events import * from .exceptions import * from .message import * @@ -22,6 +23,7 @@ __all__ += bot.__all__ __all__ += client.__all__ __all__ += command.__all__ __all__ += connection.__all__ +__all__ += database.__all__ __all__ += events.__all__ __all__ += exceptions.__all__ __all__ += message.__all__ diff --git a/yaboli/database.py b/yaboli/database.py new file mode 100644 index 0000000..84af548 --- /dev/null +++ b/yaboli/database.py @@ -0,0 +1,40 @@ +import asyncio +import logging +import sqlite3 +from typing import Any, Awaitable, Callable, TypeVar + +from .util import asyncify + +logger = logging.getLogger(__name__) + +__all__ = ["Database", "operation"] + +T = TypeVar('T') + +def operation(func: Callable[..., T]) -> Callable[..., Awaitable[T]]: + async def wrapper(self: Any, *args: Any, **kwargs: Any) -> T: + async with self as db: + while True: + try: + return await asyncify(func, self, db, *args, **kwargs) + except sqlite3.OperationalError as e: + logger.warn(f"Operational error encountered: {e}") + await asyncio.sleep(5) + return wrapper + +class Database: + def __init__(self, database: str) -> None: + self._connection = sqlite3.connect(database, check_same_thread=False) + self._lock = asyncio.Lock() + + self.initialize(self._connection) + + def initialize(self, db: Any) -> None: + pass + + async def __aenter__(self) -> Any: + await self._lock.__aenter__() + return self._connection + + async def __aexit__(self, *args: Any, **kwargs: Any) -> Any: + return await self._lock.__aexit__(*args, **kwargs) diff --git a/yaboli/util.py b/yaboli/util.py index 6439799..e8395d9 100644 --- a/yaboli/util.py +++ b/yaboli/util.py @@ -1,8 +1,16 @@ +import asyncio import datetime +import functools import re +from typing import Any, Callable -__all__ = ["mention", "atmention", "normalize", "similar", "plural", - "format_time", "format_delta"] +__all__ = ["asyncify", "mention", "atmention", "normalize", "similar", + "plural", "format_time", "format_delta"] + +async def asyncify(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any: + func_with_args = functools.partial(func, *args, **kwargs) + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, func_with_args) # Name/nick related functions From 838c364066f3b91f9d678456f44b55d3113c28ed Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Apr 2019 19:28:40 +0000 Subject: [PATCH 14/33] Bump version to 1.1.1 --- CHANGELOG.md | 2 ++ README.md | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 147d068..169e87f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +## 1.1.1 (2019-04-14) + - add database class for easier sqlite3 access ## 1.1.0 (2019-04-14) diff --git a/README.md b/README.md index c7ef69a..b51b517 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Ensure that you have at least Python 3.7 installed. To install yaboli or update your installation to the latest version, run: ``` -$ pip install git+https://github.com/Garmelon/yaboli@v1.1.0 +$ pip install git+https://github.com/Garmelon/yaboli@v1.1.1 ``` The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. diff --git a/setup.py b/setup.py index bf3a4ab..a3b8de7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="yaboli", - version="1.1.0", + version="1.1.1", packages=["yaboli"], install_requires=["websockets==7.0"], ) From 1297cf201b24f4ca02976a238074bf74aef28dff Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Apr 2019 19:56:14 +0000 Subject: [PATCH 15/33] Fix room authentication --- CHANGELOG.md | 2 ++ yaboli/room.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 169e87f..399f28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +- fix room authentication + ## 1.1.1 (2019-04-14) - add database class for easier sqlite3 access diff --git a/yaboli/room.py b/yaboli/room.py index 905162f..5ea5e03 100644 --- a/yaboli/room.py +++ b/yaboli/room.py @@ -191,8 +191,11 @@ class Room: async def _on_bounce_event(self, packet: Any) -> None: data = packet["data"] - # Can we even authenticate? - if not "passcode" in data.get("auth_options", []): + # Can we even authenticate? (Assuming that passcode authentication is + # available if no authentication options are given: Euphoria doesn't + # (always) send authentication options, even when passcode + # authentication works.) + if not "passcode" in data.get("auth_options", ["passcode"]): self._set_connected_failed() return From e53ce42e99acc3efc8625b3a2367ae9926df1b9c Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Apr 2019 20:03:29 +0000 Subject: [PATCH 16/33] Bump version to 1.1.2 --- CHANGELOG.md | 3 +++ README.md | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 399f28f..86bd86a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## Next version +## 1.1.2 (2019-04-14) + - fix room authentication +- resolve to test yaboli more thoroughly before publishing a new version ## 1.1.1 (2019-04-14) diff --git a/README.md b/README.md index b51b517..92f1e14 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Ensure that you have at least Python 3.7 installed. To install yaboli or update your installation to the latest version, run: ``` -$ pip install git+https://github.com/Garmelon/yaboli@v1.1.1 +$ pip install git+https://github.com/Garmelon/yaboli@v1.1.2 ``` The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. diff --git a/setup.py b/setup.py index a3b8de7..b3b1208 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="yaboli", - version="1.1.1", + version="1.1.2", packages=["yaboli"], install_requires=["websockets==7.0"], ) From d9f25a04fbc0358dd56c69f2e14ae3d23d060461 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 14 Apr 2019 22:25:42 +0000 Subject: [PATCH 17/33] Time out when creating the ws connections --- CHANGELOG.md | 2 ++ yaboli/connection.py | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86bd86a..f996329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +- add timeout for creating ws connections + ## 1.1.2 (2019-04-14) - fix room authentication diff --git a/yaboli/connection.py b/yaboli/connection.py index af31d1c..8af43c3 100644 --- a/yaboli/connection.py +++ b/yaboli/connection.py @@ -82,6 +82,9 @@ class Connection: "part-event" and "ping". """ + # Timeout for waiting for the ws connection to be established + CONNECT_TIMEOUT = 10 # seconds + # Maximum duration between euphoria's ping messages. Euphoria usually sends # ping messages every 20 to 30 seconds. PING_TIMEOUT = 40 # seconds @@ -183,8 +186,12 @@ class Connection: try: logger.debug(f"Creating ws connection to {self._url!r}") - ws = await websockets.connect(self._url, - extra_headers=self._cookie_jar.get_cookies_as_headers()) + ws = await asyncio.wait_for( + websockets.connect(self._url, + extra_headers=self._cookie_jar.get_cookies_as_headers()), + self.CONNECT_TIMEOUT + ) + logger.debug(f"Established ws connection to {self._url!r}") self._ws = ws self._awaiting_replies = {} @@ -200,7 +207,7 @@ class Connection: return True except (websockets.InvalidHandshake, websockets.InvalidStatusCode, - socket.gaierror): + socket.gaierror, asyncio.TimeoutError): logger.debug("Connection failed") return False From de4ba53de85cfa40cd184023e5ee73e4bda18c9a Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 19 Apr 2019 09:57:09 +0000 Subject: [PATCH 18/33] Fix config file not reloading on bot restart --- CHANGELOG.md | 1 + yaboli/__init__.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f996329..9838fd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Next version - add timeout for creating ws connections +- fix config file not reloading when restarting bots ## 1.1.2 (2019-04-14) diff --git a/yaboli/__init__.py b/yaboli/__init__.py index b138c88..241bf0e 100644 --- a/yaboli/__init__.py +++ b/yaboli/__init__.py @@ -54,12 +54,12 @@ def run( bot_constructor: BotConstructor, config_file: str = "bot.conf", ) -> None: - # Load the config file - config = configparser.ConfigParser(allow_no_value=True) - config.read(config_file) - async def _run() -> None: while True: + # Load the config file + config = configparser.ConfigParser(allow_no_value=True) + config.read(config_file) + bot = bot_constructor(config, config_file) await bot.run() @@ -70,12 +70,12 @@ def run_modulebot( module_constructors: Dict[str, ModuleConstructor], config_file: str = "bot.conf", ) -> None: - # Load the config file - config = configparser.ConfigParser(allow_no_value=True) - config.read(config_file) - async def _run() -> None: while True: + # Load the config file + config = configparser.ConfigParser(allow_no_value=True) + config.read(config_file) + modulebot = modulebot_constructor(config, config_file, module_constructors) await modulebot.run() From 83af4ff9e850c5043eb4ce8f6c6ab41eed8608a9 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 19 Apr 2019 10:01:50 +0000 Subject: [PATCH 19/33] Bump version to 1.1.3 --- CHANGELOG.md | 4 ++++ README.md | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9838fd8..2a2f6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next version +Nothing yet + +## 1.1.3 (2019-04-19) + - add timeout for creating ws connections - fix config file not reloading when restarting bots diff --git a/README.md b/README.md index 92f1e14..becb80c 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Ensure that you have at least Python 3.7 installed. To install yaboli or update your installation to the latest version, run: ``` -$ pip install git+https://github.com/Garmelon/yaboli@v1.1.2 +$ pip install git+https://github.com/Garmelon/yaboli@v1.1.3 ``` The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. diff --git a/setup.py b/setup.py index b3b1208..4ddafc0 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="yaboli", - version="1.1.2", + version="1.1.3", packages=["yaboli"], install_requires=["websockets==7.0"], ) From ca56de710c31c18fafdc0a67a809e5d4c2f4ab53 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 19 Apr 2019 10:04:12 +0000 Subject: [PATCH 20/33] Fix changelog formatting --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2f6ec..1cf5072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ Nothing yet - change how config files are passed along - change module system to support config file changes -# 1.0.0 (2019-04-13) +## 1.0.0 (2019-04-13) - add fancy argument parsing - add login and logout command to room @@ -35,9 +35,9 @@ Nothing yet ## 0.2.0 (2019-04-12) -- change config file format - add `ALIASES` variable to `Bot` - add `on_connected` function to `Client` +- change config file format ## 0.1.0 (2019-04-12) From eb9cc4f9bd2c583ddf77ff1e9f1e506b3ea21c94 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 19 Apr 2019 10:52:39 +0000 Subject: [PATCH 21/33] Make KILL_REPLY and RESTART_REPLY optional --- yaboli/bot.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/yaboli/bot.py b/yaboli/bot.py index c696820..eba8669 100644 --- a/yaboli/bot.py +++ b/yaboli/bot.py @@ -19,8 +19,8 @@ class Bot(Client): PING_REPLY: str = "Pong!" HELP_GENERAL: Optional[str] = None HELP_SPECIFIC: Optional[List[str]] = None - KILL_REPLY: str = "/me dies" - RESTART_REPLY: str = "/me restarts" + KILL_REPLY: Optional[str] = "/me dies" + RESTART_REPLY: Optional[str] = "/me restarts" GENERAL_SECTION = "general" ROOMS_SECTION = "rooms" @@ -178,7 +178,10 @@ class Bot(Client): args: SpecificArgumentData ) -> None: logger.info(f"Killed in &{room.name} by {message.sender.atmention}") - await message.reply(self.KILL_REPLY) + + if self.KILL_REPLY is not None: + await message.reply(self.KILL_REPLY) + await self.part(room) async def cmd_restart(self, @@ -187,7 +190,10 @@ class Bot(Client): args: SpecificArgumentData ) -> None: logger.info(f"Restarted in &{room.name} by {message.sender.atmention}") - await message.reply(self.RESTART_REPLY) + + if self.RESTART_REPLY is not None: + await message.reply(self.RESTART_REPLY) + await self.stop() BotConstructor = Callable[[configparser.ConfigParser, str], Bot] From 6a15e1a9488464322203633ca558ff333e81e706 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 19 Apr 2019 11:06:27 +0000 Subject: [PATCH 22/33] Add docstrings to Bot functions --- CHANGELOG.md | 3 +- yaboli/bot.py | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cf5072..a927e16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## Next version -Nothing yet +- add docstrings to `Bot` +- change `KILL_REPLY` and `RESTART_REPLY` to be optional in `Bot` ## 1.1.3 (2019-04-19) diff --git a/yaboli/bot.py b/yaboli/bot.py index eba8669..97385cb 100644 --- a/yaboli/bot.py +++ b/yaboli/bot.py @@ -14,6 +14,36 @@ logger = logging.getLogger(__name__) __all__ = ["Bot", "BotConstructor"] class Bot(Client): + """ + A Bot is a Client that responds to commands and uses a config file to + automatically set its nick and join rooms. + + The config file is loaded as a ConfigParser by the run() or run_modulebot() + functions and has the following structure: + + A "general" section which contains: + - nick - the default nick of the bot (set to the empty string if you don't + want to set a nick) + - cookie_file (optional) - the file the cookie should be saved in + + A "rooms" section which contains a list of rooms that the bot should + automatically join. This section is optional if you overwrite started(). + The room list should have the format "roomname" or "roomname = password". + + A bot has the following attributes: + - ALIASES - list of alternate nicks the bot responds to (see + process_commands()) + - PING_REPLY - used by cmd_ping() + - HELP_GENERAL - used by cmd_help_general() + - HELP_SPECIFIC - used by cmd_help_specific() + - KILL_REPLY - used by cmd_kill() + - RESTART_REPLY - used by cmd_restart() + - GENERAL_SECTION - the name of the "general" section in the config file + (see above) (default: "general") + - ROOMS_SECTION - the name of the "rooms" section in the config file (see + above) (default: "rooms") + """ + ALIASES: List[str] = [] PING_REPLY: str = "Pong!" @@ -50,10 +80,26 @@ class Bot(Client): self.start_time = datetime.datetime.now() def save_config(self) -> None: + """ + Save the current state of self.config to the file passed in __init__ as + the config_file parameter. + + Usually, this is the file that self.config was loaded from (if you use + run or run_modulebot). + """ + with open(self.config_file, "w") as f: self.config.write(f) async def started(self) -> None: + """ + This Client function is overwritten in order to join all the rooms + listed in the "rooms" section of self.config. + + If you need to overwrite this function but want to keep the auto-join + functionality, make sure to await super().started(). + """ + for room, password in self.config[self.ROOMS_SECTION].items(): if password is None: await self.join(room) @@ -63,6 +109,12 @@ class Bot(Client): # Registering commands def register(self, command: Command) -> None: + """ + Register a Command (from the yaboli.command submodule). + + Usually, you don't have to call this function yourself. + """ + self._commands.append(command) def register_general(self, @@ -70,6 +122,23 @@ class Bot(Client): cmdfunc: GeneralCommandFunction, args: bool = True ) -> None: + """ + Register a function as general bot command (i. e. no @mention of the + bot nick after the !command). This function will be called by + process_commands() when the bot encounters a matching command. + + name - the name of the command (If you want your command to be !hello, + the name is "hello".) + + cmdfunc - the function that is called with the Room, LiveMessage and + ArgumentData when the bot encounters a matching command + + args - whether the command may have arguments (If set to False, the + ArgumentData's has_args() function must also return False for the + command function to be called. If set to True, all ArgumentData is + valid.) + """ + command = GeneralCommand(name, cmdfunc, args) self.register(command) @@ -78,6 +147,21 @@ class Bot(Client): cmdfunc: SpecificCommandFunction, args: bool = True ) -> None: + """ + Register a function as specific bot command (i. e. @mention of the bot + nick after the !command is required). This function will be called by + process_commands() when the bot encounters a matching command. + + name - the name of the command (see register_general() for an + explanation) + + cmdfunc - the function that is called with the Room, LiveMessage and + SpecificArgumentData when the bot encounters a matching command + + args - whether the command may have arguments (see register_general() + for an explanation) + """ + command = SpecificCommand(name, cmdfunc, args) self.register(command) @@ -88,6 +172,13 @@ class Bot(Client): message: LiveMessage, aliases: List[str] = [] ) -> None: + """ + If the message contains a command, call all matching command functions + that were previously registered. + + This function is usually called by the overwritten on_send() function. + """ + nicks = [room.session.nick] + aliases data = CommandData.from_string(message.content) @@ -97,11 +188,31 @@ class Bot(Client): await command.run(room, message, nicks, data) async def on_send(self, room: Room, message: LiveMessage) -> None: + """ + This Client function is overwritten in order to automatically call + process_commands() with self.ALIASES. + + If you need to overwrite this function, make sure to await + process_commands() with self.ALIASES somewhere in your function, or + await super().on_send(). + """ + await self.process_commands(room, message, aliases=self.ALIASES) # Help util def format_help(self, room: Room, lines: List[str]) -> str: + """ + Format a list of strings into a string, replacing certain placeholders + with the actual values. + + This function uses the str.format() function to replace the following: + + - {nick} - the bot's current nick + - {mention} - the bot's current nick, run through mention() + - {atmention} - the bot's current nick, run through atmention() + """ + text = "\n".join(lines) params = { "nick": room.session.nick, @@ -119,6 +230,36 @@ class Bot(Client): kill: bool = False, restart: bool = False, ) -> None: + """ + Register the commands necessary for the bot to conform to the botrulez + (https://github.com/jedevc/botrulez). Also includes a few optional + botrulez commands that are disabled by default. + + - ping - register general and specific cmd_ping() + - help_ - register cmd_help_general() and cmd_help_specific() + - uptime - register specific cmd_uptime + - kill - register specific cmd_kill (disabled by default) + - uptime - register specific cmd_uptime (disabled by default) + + All commands are registered with args=False. + + If you want to implement your own versions of these commands, it is + recommended that you set the respective argument to False in your call + to register_botrulez(), overwrite the existing command functions or + create your own, and then register them manually. + + For help, that might look something like this, if you've written a + custom specific help that takes extra arguments but are using the + botrulez general help: + + self.register_botrulez(help_=False) + self.register_general("help", self.cmd_help_general, args=False) + self.register_specific("help", self.cmd_help_custom) + + In case you're asking, the help_ parameter has an underscore at the end + so it doesn't overlap the help() function. + """ + if ping: self.register_general("ping", self.cmd_ping, args=False) self.register_specific("ping", self.cmd_ping, args=False) @@ -144,6 +285,10 @@ class Bot(Client): message: LiveMessage, args: ArgumentData ) -> None: + """ + Reply with self.PING_REPLY. + """ + await message.reply(self.PING_REPLY) async def cmd_help_general(self, @@ -151,6 +296,10 @@ class Bot(Client): message: LiveMessage, args: ArgumentData ) -> None: + """ + Reply with self.HELP_GENERAL, if it is not None. Uses format_help(). + """ + if self.HELP_GENERAL is not None: await message.reply(self.format_help(room, [self.HELP_GENERAL])) @@ -159,6 +308,10 @@ class Bot(Client): message: LiveMessage, args: SpecificArgumentData ) -> None: + """ + Reply with self.HELP_SPECIFIC, if it is not None. Uses format_help(). + """ + if self.HELP_SPECIFIC is not None: await message.reply(self.format_help(room, self.HELP_SPECIFIC)) @@ -167,6 +320,15 @@ class Bot(Client): message: LiveMessage, args: SpecificArgumentData ) -> None: + """ + Reply with the bot's uptime in the format specified by the botrulez. + + This uses the time that the Bot was first started, not the time the + respective Room was created. A !restart (see register_botrulez()) will + reset the bot uptime, but leaving and re-joining a room or losing + connection won't. + """ + time = format_time(self.start_time) delta = format_delta(datetime.datetime.now() - self.start_time) text = f"/me has been up since {time} UTC ({delta})" @@ -177,6 +339,13 @@ class Bot(Client): message: LiveMessage, args: SpecificArgumentData ) -> None: + """ + Remove the bot from this room. + + If self.KILL_REPLY is not None, replies with that before leaving the + room. + """ + logger.info(f"Killed in &{room.name} by {message.sender.atmention}") if self.KILL_REPLY is not None: @@ -189,6 +358,15 @@ class Bot(Client): message: LiveMessage, args: SpecificArgumentData ) -> None: + """ + Restart the whole Bot. + + This is done by stopping the Bot, since the run() or run_modulebot() + functions start the Bot in a while True loop. + + If self.RESTART_REPLY is not None, replies with that before restarting. + """ + logger.info(f"Restarted in &{room.name} by {message.sender.atmention}") if self.RESTART_REPLY is not None: From 74a8adfa587c9de2193e54f7875b53c507f48edc Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 19 Apr 2019 11:09:08 +0000 Subject: [PATCH 23/33] Fix imports --- CHANGELOG.md | 1 + yaboli/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a927e16..4ad540e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - add docstrings to `Bot` - change `KILL_REPLY` and `RESTART_REPLY` to be optional in `Bot` +- fix imports ## 1.1.3 (2019-04-19) diff --git a/yaboli/__init__.py b/yaboli/__init__.py index 241bf0e..527eaeb 100644 --- a/yaboli/__init__.py +++ b/yaboli/__init__.py @@ -17,7 +17,7 @@ from .session import * from .util import * __all__ = ["STYLE", "FORMAT", "DATE_FORMAT", "FORMATTER", "enable_logging", - "run"] + "run", "run_modulebot"] __all__ += bot.__all__ __all__ += client.__all__ From 1c409601dbe309c2438f0061fdc59f9cffcfddee Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 20 Apr 2019 18:55:47 +0000 Subject: [PATCH 24/33] Update echobot to latest yaboli version --- CHANGELOG.md | 1 + examples/echo/bot.conf | 1 + examples/echo/echobot.py | 7 ++++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ad540e..a0e5c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - add docstrings to `Bot` - change `KILL_REPLY` and `RESTART_REPLY` to be optional in `Bot` +- fix echobot example - fix imports ## 1.1.3 (2019-04-19) diff --git a/examples/echo/bot.conf b/examples/echo/bot.conf index 8d48222..940e8e4 100644 --- a/examples/echo/bot.conf +++ b/examples/echo/bot.conf @@ -1,5 +1,6 @@ [general] nick = EchoBot +cookie_file = bot.cookie [rooms] test diff --git a/examples/echo/echobot.py b/examples/echo/echobot.py index 4804992..e404f3c 100644 --- a/examples/echo/echobot.py +++ b/examples/echo/echobot.py @@ -8,13 +8,14 @@ class EchoBot(yaboli.Bot): "!echo – reply with exactly ", ] - def __init__(self, config_file): - super().__init__(config_file) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.register_botrulez(kill=True) self.register_general("echo", self.cmd_echo) async def cmd_echo(self, room, message, args): - await message.reply(args.raw) + text = args.raw.strip() # ignoring leading and trailing whitespace + await message.reply(text) if __name__ == "__main__": From 7024686ff217986e495230d678bfd98c12b46898 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 20 Apr 2019 19:01:25 +0000 Subject: [PATCH 25/33] Update example gitignore to latest version --- CHANGELOG.md | 3 ++- examples/echo/.gitignore | 17 +++++++++++++++++ examples/echo/{bot.conf => bot.conf.default} | 0 examples/gitignore_with_venv | 8 ++++++-- 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 examples/echo/.gitignore rename examples/echo/{bot.conf => bot.conf.default} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e5c4b..ddb365e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,9 @@ - add docstrings to `Bot` - change `KILL_REPLY` and `RESTART_REPLY` to be optional in `Bot` -- fix echobot example - fix imports +- update echobot example to newest version +- update example gitignore to newest version ## 1.1.3 (2019-04-19) diff --git a/examples/echo/.gitignore b/examples/echo/.gitignore new file mode 100644 index 0000000..f69b963 --- /dev/null +++ b/examples/echo/.gitignore @@ -0,0 +1,17 @@ +# python stuff +__pycache__/ + +# venv stuff +bin/ +include/ +lib/ +lib64 +pyvenv.cfg + +# bot stuff +# +# These files are ignored because they may contain sensitive information you +# wouldn't want in your repo. If you need to have a config file in your repo, +# store a bot.conf.default with default settings. +*.conf +*.cookie diff --git a/examples/echo/bot.conf b/examples/echo/bot.conf.default similarity index 100% rename from examples/echo/bot.conf rename to examples/echo/bot.conf.default diff --git a/examples/gitignore_with_venv b/examples/gitignore_with_venv index 191feb7..f69b963 100644 --- a/examples/gitignore_with_venv +++ b/examples/gitignore_with_venv @@ -8,6 +8,10 @@ lib/ lib64 pyvenv.cfg -# config files +# bot stuff +# +# These files are ignored because they may contain sensitive information you +# wouldn't want in your repo. If you need to have a config file in your repo, +# store a bot.conf.default with default settings. *.conf -cookie_jar +*.cookie From 2215e75c34629b76f178e6db5100315377b4ad1f Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 20 Apr 2019 19:26:46 +0000 Subject: [PATCH 26/33] Add config file to example --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index becb80c..9af43a9 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,17 @@ class EchoBot(yaboli.Bot): await message.reply(args.raw) ``` -The bot's nick, cookie file and default rooms are specified in a config file. +The bot's nick, cookie file and default rooms are specified in a config file, +like so: + +```ini +[general] +nick = EchoBot +cookie_file = bot.cookie + +[rooms] +test +``` The help command from the botrulez uses the `HELP_GENERAL` and `HELP_SPECIFIC` fields. @@ -52,6 +62,9 @@ In the `cmd_echo` function, the echo command is implemented. In this case, the bot replies to the message containing the command with the raw argument string, i. e. the text between the end of the "!echo" and the end of the whole message. +The full version of this echobot can be found [in the +examples](examples/echo/). + ## TODOs - [ ] document yaboli (markdown files in a "docs" folder?) From 66b56a450e7327ddea59d1692e4570b3889cdf75 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 21 Jun 2019 07:21:50 +0000 Subject: [PATCH 27/33] Fix room firing incorrect event --- CHANGELOG.md | 1 + yaboli/room.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb365e..26c1d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - add docstrings to `Bot` - change `KILL_REPLY` and `RESTART_REPLY` to be optional in `Bot` - fix imports +- fix room firing incorrect event - update echobot example to newest version - update example gitignore to newest version diff --git a/yaboli/room.py b/yaboli/room.py index 5ea5e03..d1304ee 100644 --- a/yaboli/room.py +++ b/yaboli/room.py @@ -180,10 +180,10 @@ class Room: if nick is not None and self._session is not None: self._session = self.session.with_nick(nick) - # Send "session" event + # Send "snapshot" event messages = [LiveMessage.from_data(self, msg_data) for msg_data in data["log"]] - self._events.fire("session", messages) + self._events.fire("snapshot", messages) self._snapshot_received = True await self._try_set_connected() From 1b9860ba1ef06c0e5e1e758008e2cbf14b359b70 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 21 Jun 2019 07:23:49 +0000 Subject: [PATCH 28/33] Bump version to 1.1.4 --- CHANGELOG.md | 4 ++++ README.md | 2 +- setup.py | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26c1d86..46bbd89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next version +Nothing yet. + +## 1.1.4 (2019-06-21) + - add docstrings to `Bot` - change `KILL_REPLY` and `RESTART_REPLY` to be optional in `Bot` - fix imports diff --git a/README.md b/README.md index 9af43a9..af5aef5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Ensure that you have at least Python 3.7 installed. To install yaboli or update your installation to the latest version, run: ``` -$ pip install git+https://github.com/Garmelon/yaboli@v1.1.3 +$ pip install git+https://github.com/Garmelon/yaboli@v1.1.4 ``` The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. diff --git a/setup.py b/setup.py index 4ddafc0..56127a0 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="yaboli", - version="1.1.3", + version="1.1.4", packages=["yaboli"], install_requires=["websockets==7.0"], ) From 455d2af251a595915c4053b01e1d199f1c2623c8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 30 Nov 2019 16:30:52 +0000 Subject: [PATCH 29/33] Use IOError to catch more exceptions --- yaboli/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yaboli/connection.py b/yaboli/connection.py index 8af43c3..fcc27fe 100644 --- a/yaboli/connection.py +++ b/yaboli/connection.py @@ -207,7 +207,7 @@ class Connection: return True except (websockets.InvalidHandshake, websockets.InvalidStatusCode, - socket.gaierror, asyncio.TimeoutError): + OSError, asyncio.TimeoutError): logger.debug("Connection failed") return False From 1d25b596bbd84e95314d637787c209f248a7e505 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 26 Jan 2020 22:50:20 +0000 Subject: [PATCH 30/33] Bump version to 1.1.5 --- CHANGELOG.md | 4 +++- README.md | 2 +- setup.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46bbd89..8dd46b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Next version -Nothing yet. +## 1.1.5 (2020-01-26) + +- more stability (I think) ## 1.1.4 (2019-06-21) diff --git a/README.md b/README.md index af5aef5..b02a6a8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Ensure that you have at least Python 3.7 installed. To install yaboli or update your installation to the latest version, run: ``` -$ pip install git+https://github.com/Garmelon/yaboli@v1.1.4 +$ pip install git+https://github.com/Garmelon/yaboli@v1.1.5 ``` The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. diff --git a/setup.py b/setup.py index 56127a0..ebbea69 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="yaboli", - version="1.1.4", + version="1.1.5", packages=["yaboli"], install_requires=["websockets==7.0"], ) From 74caea4e922a249deec2ae33b89a7c3471cd693b Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 21 Aug 2022 14:09:02 +0200 Subject: [PATCH 31/33] Update websockets dependency --- CHANGELOG.md | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dd46b4..60cfd94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +- update websockets dependency + ## 1.1.5 (2020-01-26) - more stability (I think) diff --git a/setup.py b/setup.py index ebbea69..8de8af1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="yaboli", version="1.1.5", packages=["yaboli"], - install_requires=["websockets==7.0"], + install_requires=["websockets >=10.3, <11"], ) # When updating the version, also: From 37c4ba703a89d44d89185ac37888b0060e5637d3 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 21 Aug 2022 14:24:05 +0200 Subject: [PATCH 32/33] Switch to pyproject.toml style setuptools config --- .gitignore | 14 +++----------- CHANGELOG.md | 1 + examples/echo/.gitignore | 12 ------------ setup.py => pyproject.toml | 16 +++++++++------- 4 files changed, 13 insertions(+), 30 deletions(-) rename setup.py => pyproject.toml (78%) diff --git a/.gitignore b/.gitignore index 1d164cd..7ce48d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,4 @@ -# python stuff __pycache__/ - -# venv stuff -bin/ -include/ -lib/ -lib64 -pyvenv.cfg - -# mypy stuff -.mypy_cache/ +*.egg-info/ +/.mypy_cache/ +/.venv/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 60cfd94..8df3702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Next version - update websockets dependency +- switch to pyproject.toml style setuptools config ## 1.1.5 (2020-01-26) diff --git a/examples/echo/.gitignore b/examples/echo/.gitignore index f69b963..da78a19 100644 --- a/examples/echo/.gitignore +++ b/examples/echo/.gitignore @@ -1,15 +1,3 @@ -# python stuff -__pycache__/ - -# venv stuff -bin/ -include/ -lib/ -lib64 -pyvenv.cfg - -# bot stuff -# # These files are ignored because they may contain sensitive information you # wouldn't want in your repo. If you need to have a config file in your repo, # store a bot.conf.default with default settings. diff --git a/setup.py b/pyproject.toml similarity index 78% rename from setup.py rename to pyproject.toml index 8de8af1..dbb7c7e 100644 --- a/setup.py +++ b/pyproject.toml @@ -1,11 +1,13 @@ -from setuptools import setup +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" -setup( - name="yaboli", - version="1.1.5", - packages=["yaboli"], - install_requires=["websockets >=10.3, <11"], -) +[project] +name = "yaboli" +version = "1.1.5" +dependencies = [ + "websockets >=10.3, <11" +] # When updating the version, also: # - update the README.md installation instructions From eba398e5d31ff9da05befb1e98459843b8513023 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 21 Aug 2022 14:26:04 +0200 Subject: [PATCH 33/33] Bump version to 1.2.0 --- CHANGELOG.md | 2 ++ README.md | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df3702..e0f1801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next version +## 1.2.0 (2022-08-21) + - update websockets dependency - switch to pyproject.toml style setuptools config diff --git a/README.md b/README.md index b02a6a8..2cd4eb1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Ensure that you have at least Python 3.7 installed. To install yaboli or update your installation to the latest version, run: ``` -$ pip install git+https://github.com/Garmelon/yaboli@v1.1.5 +$ pip install git+https://github.com/Garmelon/yaboli@v1.2.0 ``` The use of [venv](https://docs.python.org/3/library/venv.html) is recommended. diff --git a/pyproject.toml b/pyproject.toml index dbb7c7e..79ad530 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "yaboli" -version = "1.1.5" +version = "1.2.0" dependencies = [ "websockets >=10.3, <11" ]