Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
17 changed files with 67 additions and 483 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -1,4 +1,12 @@
|
||||||
|
# python stuff
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.egg-info/
|
|
||||||
/.mypy_cache/
|
# venv stuff
|
||||||
/.venv/
|
bin/
|
||||||
|
include/
|
||||||
|
lib/
|
||||||
|
lib64
|
||||||
|
pyvenv.cfg
|
||||||
|
|
||||||
|
# mypy stuff
|
||||||
|
.mypy_cache/
|
||||||
|
|
|
||||||
41
CHANGELOG.md
41
CHANGELOG.md
|
|
@ -2,44 +2,7 @@
|
||||||
|
|
||||||
## Next version
|
## Next version
|
||||||
|
|
||||||
## 1.2.0 (2022-08-21)
|
# 1.0.0 (2019-04-13)
|
||||||
|
|
||||||
- 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 fancy argument parsing
|
||||||
- add login and logout command to room
|
- add login and logout command to room
|
||||||
|
|
@ -51,9 +14,9 @@
|
||||||
|
|
||||||
## 0.2.0 (2019-04-12)
|
## 0.2.0 (2019-04-12)
|
||||||
|
|
||||||
|
- change config file format
|
||||||
- add `ALIASES` variable to `Bot`
|
- add `ALIASES` variable to `Bot`
|
||||||
- add `on_connected` function to `Client`
|
- add `on_connected` function to `Client`
|
||||||
- change config file format
|
|
||||||
|
|
||||||
## 0.1.0 (2019-04-12)
|
## 0.1.0 (2019-04-12)
|
||||||
|
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -10,9 +10,8 @@ creating bots for [euphoria.io](https://euphoria.io).
|
||||||
|
|
||||||
Ensure that you have at least Python 3.7 installed.
|
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.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
The use of [venv](https://docs.python.org/3/library/venv.html) is recommended.
|
The use of [venv](https://docs.python.org/3/library/venv.html) is recommended.
|
||||||
|
|
@ -39,17 +38,7 @@ class EchoBot(yaboli.Bot):
|
||||||
await message.reply(args.raw)
|
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`
|
The help command from the botrulez uses the `HELP_GENERAL` and `HELP_SPECIFIC`
|
||||||
fields.
|
fields.
|
||||||
|
|
@ -62,9 +51,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,
|
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.
|
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
|
## TODOs
|
||||||
|
|
||||||
- [ ] document yaboli (markdown files in a "docs" folder?)
|
- [ ] document yaboli (markdown files in a "docs" folder?)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# Setting up and running a bot
|
|
||||||
|
|
||||||
## Installing yaboli
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Configuring the bot
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Running the bot
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
# Index for yaboli docs
|
# Index for yaboli docs
|
||||||
|
|
||||||
- [Setting up and running a bot](bot_setup.md)
|
Links to specific sections will be added here.
|
||||||
- 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
|
## Library structure overview
|
||||||
|
|
||||||
|
|
|
||||||
5
examples/echo/.gitignore
vendored
5
examples/echo/.gitignore
vendored
|
|
@ -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
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
[general]
|
[general]
|
||||||
nick = EchoBot
|
nick = EchoBot
|
||||||
cookie_file = bot.cookie
|
|
||||||
|
|
||||||
[rooms]
|
[rooms]
|
||||||
test
|
test
|
||||||
|
|
@ -8,14 +8,13 @@ class EchoBot(yaboli.Bot):
|
||||||
"!echo <text> – reply with exactly <text>",
|
"!echo <text> – reply with exactly <text>",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, config_file):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(config_file)
|
||||||
self.register_botrulez(kill=True)
|
self.register_botrulez(kill=True)
|
||||||
self.register_general("echo", self.cmd_echo)
|
self.register_general("echo", self.cmd_echo)
|
||||||
|
|
||||||
async def cmd_echo(self, room, message, args):
|
async def cmd_echo(self, room, message, args):
|
||||||
text = args.raw.strip() # ignoring leading and trailing whitespace
|
await message.reply(args.raw)
|
||||||
await message.reply(text)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
[build-system]
|
from setuptools import setup
|
||||||
requires = ["setuptools"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[project]
|
setup(
|
||||||
name = "yaboli"
|
name="yaboli",
|
||||||
version = "1.2.0"
|
version="1.0.0",
|
||||||
dependencies = [
|
packages=["yaboli"],
|
||||||
"websockets >=10.3, <11"
|
install_requires=["websockets==7.0"],
|
||||||
]
|
)
|
||||||
|
|
||||||
# When updating the version, also:
|
# When updating the version, also:
|
||||||
# - update the README.md installation instructions
|
# - update the README.md installation instructions
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import configparser
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable, Dict
|
from typing import Callable
|
||||||
|
|
||||||
from .bot import *
|
from .bot import *
|
||||||
from .client import *
|
from .client import *
|
||||||
from .command import *
|
from .command import *
|
||||||
from .connection import *
|
from .connection import *
|
||||||
from .database import *
|
|
||||||
from .events import *
|
from .events import *
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .message import *
|
from .message import *
|
||||||
|
|
@ -17,13 +15,12 @@ from .session import *
|
||||||
from .util import *
|
from .util import *
|
||||||
|
|
||||||
__all__ = ["STYLE", "FORMAT", "DATE_FORMAT", "FORMATTER", "enable_logging",
|
__all__ = ["STYLE", "FORMAT", "DATE_FORMAT", "FORMATTER", "enable_logging",
|
||||||
"run", "run_modulebot"]
|
"run"]
|
||||||
|
|
||||||
__all__ += bot.__all__
|
__all__ += bot.__all__
|
||||||
__all__ += client.__all__
|
__all__ += client.__all__
|
||||||
__all__ += command.__all__
|
__all__ += command.__all__
|
||||||
__all__ += connection.__all__
|
__all__ += connection.__all__
|
||||||
__all__ += database.__all__
|
|
||||||
__all__ += events.__all__
|
__all__ += events.__all__
|
||||||
__all__ += exceptions.__all__
|
__all__ += exceptions.__all__
|
||||||
__all__ += message.__all__
|
__all__ += message.__all__
|
||||||
|
|
@ -51,33 +48,12 @@ def enable_logging(name: str = "yaboli", level: int = logging.INFO) -> None:
|
||||||
logger.addHandler(handler)
|
logger.addHandler(handler)
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
bot_constructor: BotConstructor,
|
client: Callable[[str], Client],
|
||||||
config_file: str = "bot.conf",
|
config_file: str = "bot.conf"
|
||||||
) -> None:
|
) -> None:
|
||||||
async def _run() -> None:
|
async def _run() -> None:
|
||||||
while True:
|
while True:
|
||||||
# Load the config file
|
client_ = client(config_file)
|
||||||
config = configparser.ConfigParser(allow_no_value=True)
|
await client_.run()
|
||||||
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())
|
asyncio.run(_run())
|
||||||
|
|
|
||||||
207
yaboli/bot.py
207
yaboli/bot.py
|
|
@ -1,7 +1,7 @@
|
||||||
import configparser
|
import configparser
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable, List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from .client import Client
|
from .client import Client
|
||||||
from .command import *
|
from .command import *
|
||||||
|
|
@ -11,57 +11,26 @@ from .util import *
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
__all__ = ["Bot", "BotConstructor"]
|
__all__ = ["Bot"]
|
||||||
|
|
||||||
class Bot(Client):
|
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] = []
|
ALIASES: List[str] = []
|
||||||
|
|
||||||
PING_REPLY: str = "Pong!"
|
PING_REPLY: str = "Pong!"
|
||||||
HELP_GENERAL: Optional[str] = None
|
HELP_GENERAL: Optional[str] = None
|
||||||
HELP_SPECIFIC: Optional[List[str]] = None
|
HELP_SPECIFIC: Optional[List[str]] = None
|
||||||
KILL_REPLY: Optional[str] = "/me dies"
|
KILL_REPLY: str = "/me dies"
|
||||||
RESTART_REPLY: Optional[str] = "/me restarts"
|
RESTART_REPLY: str = "/me restarts"
|
||||||
|
|
||||||
GENERAL_SECTION = "general"
|
GENERAL_SECTION = "general"
|
||||||
ROOMS_SECTION = "rooms"
|
ROOMS_SECTION = "rooms"
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, config_file: str) -> None:
|
||||||
config: configparser.ConfigParser,
|
|
||||||
config_file: str,
|
|
||||||
) -> None:
|
|
||||||
self.config = config
|
|
||||||
self.config_file = config_file
|
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")
|
nick = self.config[self.GENERAL_SECTION].get("nick")
|
||||||
if nick is None:
|
if nick is None:
|
||||||
logger.warn(("'nick' not set in config file. Defaulting to empty"
|
logger.warn(("'nick' not set in config file. Defaulting to empty"
|
||||||
|
|
@ -80,26 +49,10 @@ class Bot(Client):
|
||||||
self.start_time = datetime.datetime.now()
|
self.start_time = datetime.datetime.now()
|
||||||
|
|
||||||
def save_config(self) -> None:
|
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:
|
with open(self.config_file, "w") as f:
|
||||||
self.config.write(f)
|
self.config.write(f)
|
||||||
|
|
||||||
async def started(self) -> None:
|
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():
|
for room, password in self.config[self.ROOMS_SECTION].items():
|
||||||
if password is None:
|
if password is None:
|
||||||
await self.join(room)
|
await self.join(room)
|
||||||
|
|
@ -109,12 +62,6 @@ class Bot(Client):
|
||||||
# Registering commands
|
# Registering commands
|
||||||
|
|
||||||
def register(self, command: Command) -> None:
|
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)
|
self._commands.append(command)
|
||||||
|
|
||||||
def register_general(self,
|
def register_general(self,
|
||||||
|
|
@ -122,23 +69,6 @@ class Bot(Client):
|
||||||
cmdfunc: GeneralCommandFunction,
|
cmdfunc: GeneralCommandFunction,
|
||||||
args: bool = True
|
args: bool = True
|
||||||
) -> None:
|
) -> 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)
|
command = GeneralCommand(name, cmdfunc, args)
|
||||||
self.register(command)
|
self.register(command)
|
||||||
|
|
||||||
|
|
@ -147,21 +77,6 @@ class Bot(Client):
|
||||||
cmdfunc: SpecificCommandFunction,
|
cmdfunc: SpecificCommandFunction,
|
||||||
args: bool = True
|
args: bool = True
|
||||||
) -> None:
|
) -> 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)
|
command = SpecificCommand(name, cmdfunc, args)
|
||||||
self.register(command)
|
self.register(command)
|
||||||
|
|
||||||
|
|
@ -172,13 +87,6 @@ class Bot(Client):
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
aliases: List[str] = []
|
aliases: List[str] = []
|
||||||
) -> None:
|
) -> 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
|
nicks = [room.session.nick] + aliases
|
||||||
data = CommandData.from_string(message.content)
|
data = CommandData.from_string(message.content)
|
||||||
|
|
||||||
|
|
@ -188,31 +96,11 @@ class Bot(Client):
|
||||||
await command.run(room, message, nicks, data)
|
await command.run(room, message, nicks, data)
|
||||||
|
|
||||||
async def on_send(self, room: Room, message: LiveMessage) -> None:
|
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)
|
await self.process_commands(room, message, aliases=self.ALIASES)
|
||||||
|
|
||||||
# Help util
|
# Help util
|
||||||
|
|
||||||
def format_help(self, room: Room, lines: List[str]) -> str:
|
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)
|
text = "\n".join(lines)
|
||||||
params = {
|
params = {
|
||||||
"nick": room.session.nick,
|
"nick": room.session.nick,
|
||||||
|
|
@ -230,36 +118,6 @@ class Bot(Client):
|
||||||
kill: bool = False,
|
kill: bool = False,
|
||||||
restart: bool = False,
|
restart: bool = False,
|
||||||
) -> None:
|
) -> 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:
|
if ping:
|
||||||
self.register_general("ping", self.cmd_ping, args=False)
|
self.register_general("ping", self.cmd_ping, args=False)
|
||||||
self.register_specific("ping", self.cmd_ping, args=False)
|
self.register_specific("ping", self.cmd_ping, args=False)
|
||||||
|
|
@ -285,10 +143,6 @@ class Bot(Client):
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
args: ArgumentData
|
args: ArgumentData
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
Reply with self.PING_REPLY.
|
|
||||||
"""
|
|
||||||
|
|
||||||
await message.reply(self.PING_REPLY)
|
await message.reply(self.PING_REPLY)
|
||||||
|
|
||||||
async def cmd_help_general(self,
|
async def cmd_help_general(self,
|
||||||
|
|
@ -296,10 +150,6 @@ class Bot(Client):
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
args: ArgumentData
|
args: ArgumentData
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
Reply with self.HELP_GENERAL, if it is not None. Uses format_help().
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.HELP_GENERAL is not None:
|
if self.HELP_GENERAL is not None:
|
||||||
await message.reply(self.format_help(room, [self.HELP_GENERAL]))
|
await message.reply(self.format_help(room, [self.HELP_GENERAL]))
|
||||||
|
|
||||||
|
|
@ -308,10 +158,6 @@ class Bot(Client):
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
args: SpecificArgumentData
|
args: SpecificArgumentData
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
Reply with self.HELP_SPECIFIC, if it is not None. Uses format_help().
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.HELP_SPECIFIC is not None:
|
if self.HELP_SPECIFIC is not None:
|
||||||
await message.reply(self.format_help(room, self.HELP_SPECIFIC))
|
await message.reply(self.format_help(room, self.HELP_SPECIFIC))
|
||||||
|
|
||||||
|
|
@ -320,15 +166,6 @@ class Bot(Client):
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
args: SpecificArgumentData
|
args: SpecificArgumentData
|
||||||
) -> None:
|
) -> 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)
|
time = format_time(self.start_time)
|
||||||
delta = format_delta(datetime.datetime.now() - self.start_time)
|
delta = format_delta(datetime.datetime.now() - self.start_time)
|
||||||
text = f"/me has been up since {time} UTC ({delta})"
|
text = f"/me has been up since {time} UTC ({delta})"
|
||||||
|
|
@ -339,18 +176,8 @@ class Bot(Client):
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
args: SpecificArgumentData
|
args: SpecificArgumentData
|
||||||
) -> None:
|
) -> 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}")
|
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)
|
await self.part(room)
|
||||||
|
|
||||||
async def cmd_restart(self,
|
async def cmd_restart(self,
|
||||||
|
|
@ -358,20 +185,6 @@ class Bot(Client):
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
args: SpecificArgumentData
|
args: SpecificArgumentData
|
||||||
) -> None:
|
) -> 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}")
|
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()
|
await self.stop()
|
||||||
|
|
||||||
BotConstructor = Callable[[configparser.ConfigParser, str], Bot]
|
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,6 @@ class Connection:
|
||||||
"part-event" and "ping".
|
"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
|
# Maximum duration between euphoria's ping messages. Euphoria usually sends
|
||||||
# ping messages every 20 to 30 seconds.
|
# ping messages every 20 to 30 seconds.
|
||||||
PING_TIMEOUT = 40 # seconds
|
PING_TIMEOUT = 40 # seconds
|
||||||
|
|
@ -186,12 +183,8 @@ class Connection:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.debug(f"Creating ws connection to {self._url!r}")
|
logger.debug(f"Creating ws connection to {self._url!r}")
|
||||||
ws = await asyncio.wait_for(
|
ws = await websockets.connect(self._url,
|
||||||
websockets.connect(self._url,
|
extra_headers=self._cookie_jar.get_cookies_as_headers())
|
||||||
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._ws = ws
|
||||||
self._awaiting_replies = {}
|
self._awaiting_replies = {}
|
||||||
|
|
@ -207,7 +200,7 @@ class Connection:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except (websockets.InvalidHandshake, websockets.InvalidStatusCode,
|
except (websockets.InvalidHandshake, websockets.InvalidStatusCode,
|
||||||
OSError, asyncio.TimeoutError):
|
socket.gaierror):
|
||||||
logger.debug("Connection failed")
|
logger.debug("Connection failed")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import configparser
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable, Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from .bot import Bot
|
from .bot import Bot
|
||||||
from .command import *
|
from .command import *
|
||||||
|
|
@ -11,77 +10,49 @@ from .util import *
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
__all__ = ["Module", "ModuleConstructor", "ModuleBot", "ModuleBotConstructor"]
|
__all__ = ["Module", "ModuleBot"]
|
||||||
|
|
||||||
class Module(Bot):
|
class Module(Bot):
|
||||||
DESCRIPTION: Optional[str] = None
|
DESCRIPTION: Optional[str] = None
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, config_file: str, standalone: bool) -> None:
|
||||||
config: configparser.ConfigParser,
|
super().__init__(config_file)
|
||||||
config_file: str,
|
|
||||||
standalone: bool = True,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(config, config_file)
|
|
||||||
|
|
||||||
self.standalone = standalone
|
self.standalone = standalone
|
||||||
|
|
||||||
ModuleConstructor = Callable[[configparser.ConfigParser, str, bool], Module]
|
|
||||||
|
|
||||||
class ModuleBot(Bot):
|
class ModuleBot(Bot):
|
||||||
HELP_PRE: Optional[List[str]] = [
|
HELP_PRE: Optional[List[str]] = [
|
||||||
"This bot contains the following modules:"
|
"This bot contains the following modules:"
|
||||||
]
|
]
|
||||||
HELP_POST: Optional[List[str]] = [
|
HELP_POST: Optional[List[str]] = [
|
||||||
"",
|
""
|
||||||
"For module-specific help, try \"!help {atmention} <module>\".",
|
"Use \"!help {atmention} <module>\" to get more information on a"
|
||||||
|
" specific module."
|
||||||
]
|
]
|
||||||
MODULE_HELP_LIMIT = 5
|
MODULE_HELP_LIMIT = 5
|
||||||
|
|
||||||
MODULES_SECTION = "modules"
|
def __init__(self, config_file: str) -> None:
|
||||||
|
super().__init__(config_file)
|
||||||
|
|
||||||
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.modules: Dict[str, Module] = {}
|
||||||
|
|
||||||
# Load initial modules
|
self.register_botrulez(help_=False)
|
||||||
for module_name in self.config[self.MODULES_SECTION]:
|
self.register_general("help", self.cmd_help_general, args=False)
|
||||||
module_constructor = self.module_constructors.get(module_name)
|
self.register_specific("help", self.cmd_help_specific, args=True)
|
||||||
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 load_module(self, name: str, module: Module) -> None:
|
def register_module(self, name: str, module: Module) -> None:
|
||||||
if name in self.modules:
|
if name in self.modules:
|
||||||
logger.warn(f"Module {name!r} is already registered, overwriting...")
|
logger.warn(f"Module {name!r} is already registered, overwriting...")
|
||||||
self.modules[name] = module
|
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]:
|
def compile_module_overview(self) -> List[str]:
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
if self.HELP_PRE is not None:
|
if self.HELP_PRE is not None:
|
||||||
lines.extend(self.HELP_PRE)
|
lines.extend(self.HELP_PRE)
|
||||||
|
|
||||||
any_modules = False
|
|
||||||
|
|
||||||
modules_without_desc: List[str] = []
|
modules_without_desc: List[str] = []
|
||||||
for module_name in sorted(self.modules):
|
for module_name in sorted(self.modules):
|
||||||
any_modules = True
|
|
||||||
|
|
||||||
module = self.modules[module_name]
|
module = self.modules[module_name]
|
||||||
|
|
||||||
if module.DESCRIPTION is None:
|
if module.DESCRIPTION is None:
|
||||||
|
|
@ -91,10 +62,7 @@ class ModuleBot(Bot):
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
|
|
||||||
if modules_without_desc:
|
if modules_without_desc:
|
||||||
lines.append("\t" + ", ".join(modules_without_desc))
|
lines.append(", ".join(modules_without_desc))
|
||||||
|
|
||||||
if not any_modules:
|
|
||||||
lines.append("No modules loaded.")
|
|
||||||
|
|
||||||
if self.HELP_POST is not None:
|
if self.HELP_POST is not None:
|
||||||
lines.extend(self.HELP_POST)
|
lines.extend(self.HELP_POST)
|
||||||
|
|
@ -111,7 +79,8 @@ class ModuleBot(Bot):
|
||||||
|
|
||||||
return module.HELP_SPECIFIC
|
return module.HELP_SPECIFIC
|
||||||
|
|
||||||
async def cmd_modules_help(self,
|
# Overwriting the botrulez help function
|
||||||
|
async def cmd_help_specific(self,
|
||||||
room: Room,
|
room: Room,
|
||||||
message: LiveMessage,
|
message: LiveMessage,
|
||||||
args: SpecificArgumentData
|
args: SpecificArgumentData
|
||||||
|
|
@ -131,12 +100,6 @@ class ModuleBot(Bot):
|
||||||
|
|
||||||
# Sending along all kinds of events
|
# 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:
|
async def on_snapshot(self, room: Room, messages: List[LiveMessage]) -> None:
|
||||||
await super().on_snapshot(room, messages)
|
await super().on_snapshot(room, messages)
|
||||||
|
|
||||||
|
|
@ -178,18 +141,6 @@ class ModuleBot(Bot):
|
||||||
for module in self.modules.values():
|
for module in self.modules.values():
|
||||||
await module.on_edit(room, message)
|
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,
|
async def on_pm(self,
|
||||||
room: Room,
|
room: Room,
|
||||||
from_id: str,
|
from_id: str,
|
||||||
|
|
@ -207,8 +158,3 @@ class ModuleBot(Bot):
|
||||||
|
|
||||||
for module in self.modules.values():
|
for module in self.modules.values():
|
||||||
await module.on_disconnect(room, reason)
|
await module.on_disconnect(room, reason)
|
||||||
|
|
||||||
ModuleBotConstructor = Callable[
|
|
||||||
[configparser.ConfigParser, str, Dict[str, ModuleConstructor]],
|
|
||||||
Bot
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -180,10 +180,10 @@ class Room:
|
||||||
if nick is not None and self._session is not None:
|
if nick is not None and self._session is not None:
|
||||||
self._session = self.session.with_nick(nick)
|
self._session = self.session.with_nick(nick)
|
||||||
|
|
||||||
# Send "snapshot" event
|
# Send "session" event
|
||||||
messages = [LiveMessage.from_data(self, msg_data)
|
messages = [LiveMessage.from_data(self, msg_data)
|
||||||
for msg_data in data["log"]]
|
for msg_data in data["log"]]
|
||||||
self._events.fire("snapshot", messages)
|
self._events.fire("session", messages)
|
||||||
|
|
||||||
self._snapshot_received = True
|
self._snapshot_received = True
|
||||||
await self._try_set_connected()
|
await self._try_set_connected()
|
||||||
|
|
@ -191,11 +191,8 @@ class Room:
|
||||||
async def _on_bounce_event(self, packet: Any) -> None:
|
async def _on_bounce_event(self, packet: Any) -> None:
|
||||||
data = packet["data"]
|
data = packet["data"]
|
||||||
|
|
||||||
# Can we even authenticate? (Assuming that passcode authentication is
|
# Can we even authenticate?
|
||||||
# available if no authentication options are given: Euphoria doesn't
|
if not "passcode" in data.get("auth_options", []):
|
||||||
# (always) send authentication options, even when passcode
|
|
||||||
# authentication works.)
|
|
||||||
if not "passcode" in data.get("auth_options", ["passcode"]):
|
|
||||||
self._set_connected_failed()
|
self._set_connected_failed()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,8 @@
|
||||||
import asyncio
|
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
|
||||||
import re
|
import re
|
||||||
from typing import Any, Callable
|
|
||||||
|
|
||||||
__all__ = ["asyncify", "mention", "atmention", "normalize", "similar",
|
__all__ = ["mention", "atmention", "normalize", "similar", "plural",
|
||||||
"plural", "format_time", "format_delta"]
|
"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
|
# Name/nick related functions
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue