diff --git a/.gitignore b/.gitignore index 7ce48d0..001826c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ -__pycache__/ -*.egg-info/ -/.mypy_cache/ -/.venv/ +yaboli/__pycache__/ +*.txt diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e0f1801..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,60 +0,0 @@ -# Changelog - -## Next version - -## 1.2.0 (2022-08-21) - -- update websockets dependency -- switch to pyproject.toml style setuptools config - -## 1.1.5 (2020-01-26) - -- more stability (I think) - -## 1.1.4 (2019-06-21) - -- 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 - -## 1.1.3 (2019-04-19) - -- add timeout for creating ws connections -- fix config file not reloading when restarting bots - -## 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) - -- add database class for easier sqlite3 access - -## 1.1.0 (2019-04-14) - -- 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 -- add login and logout command to room -- add pm command to room -- add cookie support -- add !restart to botrulez -- add Bot config file saving -- fix the Room not setting its nick correctly upon reconnecting - -## 0.2.0 (2019-04-12) - -- add `ALIASES` variable to `Bot` -- add `on_connected` function to `Client` -- change config file format - -## 0.1.0 (2019-04-12) - -- use setuptools diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f2fd14f..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 - 2019 Garmelon - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 2cd4eb1..0000000 --- a/README.md +++ /dev/null @@ -1,85 +0,0 @@ -# Yaboli - -Yaboli (**Y**et **A**nother **Bo**t **Li**brary) is a Python library for -creating bots for [euphoria.io](https://euphoria.io). - -- [Documentation](docs/index.md) -- [Changelog](CHANGELOG.md) - -## Installation - -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.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 -[botrulez](https://github.com/jedevc/botrulez) can be written like so: - -```python -class EchoBot(yaboli.Bot): - HELP_GENERAL = "/me echoes back what you said" - HELP_SPECIFIC = [ - "This bot only has one command:", - "!echo – reply with exactly ", - ] - - def __init__(self, config_file): - super().__init__(config_file) - 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) -``` - -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. - -In the `__init__` function, the bot's commands are registered. The required -botrulez commands (!ping, !help, !uptime) are enabled by default. Other -commands like !kill need to be enabled explicitly. - -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?) -- [ ] document new classes (docstrings, maybe comments) -- [ ] write examples -- [ ] make yaboli package play nice with mypy -- [x] implement !uptime for proper botrulez conformity -- [x] implement !kill -- [x] untruncate LiveMessage-s -- [x] config file support for bots, used by default -- [x] make it easier to enable log messages -- [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 -- [x] write project readme -- [x] cookie support -- [x] fancy argument parsing diff --git a/docs/bot_setup.md b/docs/bot_setup.md deleted file mode 100644 index cf6722d..0000000 --- a/docs/bot_setup.md +++ /dev/null @@ -1,13 +0,0 @@ -# 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 deleted file mode 100644 index 9f4835f..0000000 --- a/docs/index.md +++ /dev/null @@ -1,89 +0,0 @@ -# Index for yaboli docs - - - [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 - -### 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 -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. - -### 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`. diff --git a/examples/echo/.gitignore b/examples/echo/.gitignore deleted file mode 100644 index da78a19..0000000 --- a/examples/echo/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# 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.default b/examples/echo/bot.conf.default deleted file mode 100644 index 940e8e4..0000000 --- a/examples/echo/bot.conf.default +++ /dev/null @@ -1,6 +0,0 @@ -[general] -nick = EchoBot -cookie_file = bot.cookie - -[rooms] -test diff --git a/examples/echo/echobot.py b/examples/echo/echobot.py deleted file mode 100644 index e404f3c..0000000 --- a/examples/echo/echobot.py +++ /dev/null @@ -1,23 +0,0 @@ -import yaboli - - -class EchoBot(yaboli.Bot): - HELP_GENERAL = "/me echoes back what you said" - HELP_SPECIFIC = [ - "This bot only has one command:", - "!echo – reply with exactly ", - ] - - 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): - text = args.raw.strip() # ignoring leading and trailing whitespace - await message.reply(text) - - -if __name__ == "__main__": - yaboli.enable_logging() - yaboli.run(EchoBot) diff --git a/examples/gitignore_with_venv b/examples/gitignore_with_venv deleted file mode 100644 index f69b963..0000000 --- a/examples/gitignore_with_venv +++ /dev/null @@ -1,17 +0,0 @@ -# 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/mypy.ini b/mypy.ini deleted file mode 100644 index 6fd0e6a..0000000 --- a/mypy.ini +++ /dev/null @@ -1,4 +0,0 @@ -[mypy] -disallow_untyped_defs = True -disallow_incomplete_defs = True -no_implicit_optional = True diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 79ad530..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,30 +0,0 @@ -[build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" - -[project] -name = "yaboli" -version = "1.2.0" -dependencies = [ - "websockets >=10.3, <11" -] - -# When updating the version, also: -# - update the README.md installation instructions -# - update the changelog -# - set a tag to the update commit - -# Meanings of version numbers -# -# Format: a.b.c -# -# a - increased when: major change such as a rewrite -# b - increased when: changes breaking backwards compatibility -# c - increased when: minor changes preserving backwards compatibility -# -# To specify version requirements for yaboli, the following format is -# recommended if you need version a.b.c: -# -# yaboli >=a.b.c, None: - handler = logging.StreamHandler() - handler.setFormatter(FORMATTER) - - logger = logging.getLogger(name) - logger.setLevel(level) - logger.addHandler(handler) - -def run( - bot_constructor: BotConstructor, - config_file: str = "bot.conf", - ) -> None: - 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() - - asyncio.run(_run()) - -def run_modulebot( - modulebot_constructor: ModuleBotConstructor, - module_constructors: Dict[str, ModuleConstructor], - config_file: str = "bot.conf", - ) -> None: - 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() - - asyncio.run(_run()) +from .basic_types import Message, SessionView +from .callbacks import Callbacks +from .connection import Connection +from .session import Session diff --git a/yaboli/basic_types.py b/yaboli/basic_types.py new file mode 100644 index 0000000..5ba0110 --- /dev/null +++ b/yaboli/basic_types.py @@ -0,0 +1,163 @@ +import time + +class SessionView(): + """ + This class keeps track of session details. + http://api.euphoria.io/#sessionview + """ + + def __init__(self, id, name, server_id, server_era, session_id, is_staff=None, is_manager=None): + """ + id - agent/account id + name - name of the client when the SessionView was captured + server_id - id of the server + server_era - era of the server + session_id - session id (unique across euphoria) + is_staff - client is staff + is_manager - client is manager + """ + + self.id = id + self.name = name + self.server_id = server_id + self.server_era = server_era + self.session_id = session_id + self.staff = is_staff + self.manager = is_manager + + @classmethod + def from_data(cls, data): + """ + Creates and returns a session created from the data. + + data - a euphoria SessionView + """ + + view = cls(None, None, None, None, None) + view.read_data(data) + return view + + def read_data(self, data): + if "id" in data: self.id = data.get("id") + if "name" in data: self.name = data.get("name") + if "server_id" in data: self.server_id = data.get("server_id") + if "server_era" in data: self.server_era = data.get("server_era") + if "session_id" in data: self.session_id = data.get("session_id") + if "is_staff" in data: self.is_staff = data.get("is_staff") + if "is_manager" in data: self.is_manager = data.get("is_manager") + + def session_type(self): + """ + session_type() -> str + + The session's type (bot, account, agent). + """ + + return self.id.split(":")[0] if ":" in self.id else None + +class Message(): + """ + This class represents a single euphoria message. + http://api.euphoria.io/#message + """ + + def __init__(self, id, time, sender, content, parent=None, edited=None, previous_edit_id=None, + deleted=None, truncated=None, encryption_key_id=None): + """ + id - message id + time - time the message was sent (epoch) + sender - SessionView of the sender + content - content of the message + parent - id of the parent message, or None + edited - time of last edit (epoch) + previous_edit_id - edit id of the most recent edit of this message + deleted - time of deletion (epoch) + truncated - message was truncated + encryption_key_id - id of the key that encrypts the message in storage + """ + + self.id = id + self.time = time + self.sender = sender + self.content = content + self.parent = parent + self.edited = edited + self.previous_edit_id = previous_edit_id + self.deleted = deleted + self.truncated = truncated + self.encryption_key_id = encryption_key_id + + @classmethod + def from_data(self, data): + """ + Creates and returns a message created from the data. + NOTE: This also creates a session object using the data in "sender". + + data - a euphoria message: http://api.euphoria.io/#message + """ + + sender = SessionView.from_data(data.get("sender")) + + return self( + data.get("id"), + data.get("time"), + sender, + data.get("content"), + parent=data.get("parent"), + edited=data.get("edited"), + deleted=data.get("deleted"), + truncated=data.get("truncated"), + previous_edit_id=data.get("previous_edit_id"), + encryption_key_id=data.get("encryption_key_id") + ) + + def time_formatted(self, date=True): + """ + time_formatted(date=True) -> str + + date - include date in format + + Time in a readable format: + With date: YYYY-MM-DD HH:MM:SS + Without date: HH:MM:SS + """ + + if date: + return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(self.time)) + else: + return time.strftime("%H:%M:%S", time.gmtime(self.time)) + + def formatted(self, show_time=False, date=True, insert_string=None, repeat_insert_string=True): + """ + formatted() -> strftime + + The message contents in the following format (does not end on a newline): +