Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eba398e5d3 | |||
| 37c4ba703a | |||
| 74caea4e92 | |||
| 1d25b596bb | |||
| 455d2af251 | |||
| 1b9860ba1e | |||
| 66b56a450e | |||
| 2215e75c34 | |||
| 7024686ff2 | |||
| 1c409601db | |||
| 74a8adfa58 | |||
| 6a15e1a948 | |||
| eb9cc4f9bd | |||
| ca56de710c |
12 changed files with 254 additions and 36 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -1,12 +1,4 @@
|
|||
# python stuff
|
||||
__pycache__/
|
||||
|
||||
# venv stuff
|
||||
bin/
|
||||
include/
|
||||
lib/
|
||||
lib64
|
||||
pyvenv.cfg
|
||||
|
||||
# mypy stuff
|
||||
.mypy_cache/
|
||||
*.egg-info/
|
||||
/.mypy_cache/
|
||||
/.venv/
|
||||
|
|
|
|||
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -2,7 +2,23 @@
|
|||
|
||||
## Next version
|
||||
|
||||
Nothing yet
|
||||
## 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)
|
||||
|
||||
|
|
@ -23,7 +39,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 +51,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)
|
||||
|
||||
|
|
|
|||
17
README.md
17
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.2.0
|
||||
```
|
||||
|
||||
The use of [venv](https://docs.python.org/3/library/venv.html) is recommended.
|
||||
|
|
@ -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?)
|
||||
|
|
|
|||
5
examples/echo/.gitignore
vendored
Normal file
5
examples/echo/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# 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
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
[general]
|
||||
nick = EchoBot
|
||||
cookie_file = bot.cookie
|
||||
|
||||
[rooms]
|
||||
test
|
||||
|
|
@ -8,13 +8,14 @@ class EchoBot(yaboli.Bot):
|
|||
"!echo <text> – reply with exactly <text>",
|
||||
]
|
||||
|
||||
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__":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
from setuptools import setup
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
setup(
|
||||
name="yaboli",
|
||||
version="1.1.3",
|
||||
packages=["yaboli"],
|
||||
install_requires=["websockets==7.0"],
|
||||
)
|
||||
[project]
|
||||
name = "yaboli"
|
||||
version = "1.2.0"
|
||||
dependencies = [
|
||||
"websockets >=10.3, <11"
|
||||
]
|
||||
|
||||
# When updating the version, also:
|
||||
# - update the README.md installation instructions
|
||||
|
|
@ -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__
|
||||
|
|
|
|||
188
yaboli/bot.py
188
yaboli/bot.py
|
|
@ -14,13 +14,43 @@ 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: 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"
|
||||
|
|
@ -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,8 +339,18 @@ 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 self.part(room)
|
||||
|
||||
async def cmd_restart(self,
|
||||
|
|
@ -186,8 +358,20 @@ 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 self.stop()
|
||||
|
||||
BotConstructor = Callable[[configparser.ConfigParser, str], Bot]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue