Compare commits

..

No commits in common. "master" and "v1.1.1" have entirely different histories.

12 changed files with 47 additions and 287 deletions

14
.gitignore vendored
View file

@ -1,4 +1,12 @@
# python stuff
__pycache__/
*.egg-info/
/.mypy_cache/
/.venv/
# venv stuff
bin/
include/
lib/
lib64
pyvenv.cfg
# mypy stuff
.mypy_cache/

View file

@ -2,34 +2,6 @@
## 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
@ -39,7 +11,7 @@
- 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
@ -51,9 +23,9 @@
## 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)

View file

@ -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.2.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.
@ -39,17 +39,7 @@ class EchoBot(yaboli.Bot):
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 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.
@ -62,9 +52,6 @@ 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?)

View file

@ -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

View file

@ -1,6 +1,5 @@
[general]
nick = EchoBot
cookie_file = bot.cookie
[rooms]
test

View file

@ -8,14 +8,13 @@ class EchoBot(yaboli.Bot):
"!echo <text> reply with exactly <text>",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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):
text = args.raw.strip() # ignoring leading and trailing whitespace
await message.reply(text)
await message.reply(args.raw)
if __name__ == "__main__":

View file

@ -8,10 +8,6 @@ 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.
# config files
*.conf
*.cookie
cookie_jar

View file

@ -1,13 +1,11 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
from setuptools import setup
[project]
name = "yaboli"
version = "1.2.0"
dependencies = [
"websockets >=10.3, <11"
]
setup(
name="yaboli",
version="1.1.1",
packages=["yaboli"],
install_requires=["websockets==7.0"],
)
# When updating the version, also:
# - update the README.md installation instructions

View file

@ -17,7 +17,7 @@ from .session import *
from .util import *
__all__ = ["STYLE", "FORMAT", "DATE_FORMAT", "FORMATTER", "enable_logging",
"run", "run_modulebot"]
"run"]
__all__ += bot.__all__
__all__ += client.__all__
@ -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()

View file

@ -14,43 +14,13 @@ 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!"
HELP_GENERAL: Optional[str] = None
HELP_SPECIFIC: Optional[List[str]] = None
KILL_REPLY: Optional[str] = "/me dies"
RESTART_REPLY: Optional[str] = "/me restarts"
KILL_REPLY: str = "/me dies"
RESTART_REPLY: str = "/me restarts"
GENERAL_SECTION = "general"
ROOMS_SECTION = "rooms"
@ -80,26 +50,10 @@ 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)
@ -109,12 +63,6 @@ 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,
@ -122,23 +70,6 @@ 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)
@ -147,21 +78,6 @@ 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)
@ -172,13 +88,6 @@ 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)
@ -188,31 +97,11 @@ 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,
@ -230,36 +119,6 @@ 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)
@ -285,10 +144,6 @@ 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,
@ -296,10 +151,6 @@ 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]))
@ -308,10 +159,6 @@ 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))
@ -320,15 +167,6 @@ 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})"
@ -339,18 +177,8 @@ 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:
await message.reply(self.KILL_REPLY)
await message.reply(self.KILL_REPLY)
await self.part(room)
async def cmd_restart(self,
@ -358,20 +186,8 @@ 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:
await message.reply(self.RESTART_REPLY)
await message.reply(self.RESTART_REPLY)
await self.stop()
BotConstructor = Callable[[configparser.ConfigParser, str], Bot]

View file

@ -82,9 +82,6 @@ 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
@ -186,12 +183,8 @@ class Connection:
try:
logger.debug(f"Creating ws connection to {self._url!r}")
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}")
ws = await websockets.connect(self._url,
extra_headers=self._cookie_jar.get_cookies_as_headers())
self._ws = ws
self._awaiting_replies = {}
@ -207,7 +200,7 @@ class Connection:
return True
except (websockets.InvalidHandshake, websockets.InvalidStatusCode,
OSError, asyncio.TimeoutError):
socket.gaierror):
logger.debug("Connection failed")
return False

View file

@ -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 "snapshot" event
# Send "session" event
messages = [LiveMessage.from_data(self, msg_data)
for msg_data in data["log"]]
self._events.fire("snapshot", messages)
self._events.fire("session", messages)
self._snapshot_received = True
await self._try_set_connected()
@ -191,11 +191,8 @@ class Room:
async def _on_bounce_event(self, packet: Any) -> None:
data = packet["data"]
# 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"]):
# Can we even authenticate?
if not "passcode" in data.get("auth_options", []):
self._set_connected_failed()
return