Clean up module structure
This commit is contained in:
parent
dfad3241fb
commit
6cc8094e0d
14 changed files with 39 additions and 1315 deletions
|
|
@ -1,47 +0,0 @@
|
|||
import asyncio
|
||||
#from controller import Bot
|
||||
from controller import Controller
|
||||
from utils import *
|
||||
|
||||
|
||||
|
||||
#class TestBot(Bot):
|
||||
class TestBot(Controller):
|
||||
def __init__(self, roomname):
|
||||
super().__init__(roomname)
|
||||
|
||||
async def on_snapshot(self, user_id, session_id, version, listing, log, nick=None,
|
||||
pm_with_nick=None, pm_with_user_id=None):
|
||||
await self.room.nick("TestBot")
|
||||
|
||||
async def on_send(self, message):
|
||||
await self.room.send("Hey, a message!", message.message_id)
|
||||
|
||||
async def on_join(self, session):
|
||||
if session.nick != "":
|
||||
await self.room.send(f"Hey, a @{mention(session.nick)}!")
|
||||
else:
|
||||
await self.room.send("Hey, a lurker!")
|
||||
|
||||
async def on_nick(self, session_id, user_id, from_nick, to_nick):
|
||||
if from_nick != "" and to_nick != "":
|
||||
if from_nick == to_nick:
|
||||
await self.room.send(f"You didn't even change your nick, @{mention(to_nick)} :(")
|
||||
else:
|
||||
await self.room.send(f"Bye @{mention(from_nick)}, hi @{mention(to_nick)}")
|
||||
elif from_nick != "":
|
||||
await self.room.send(f"Bye @{mention(from_nick)}? This message should never appear...")
|
||||
elif to_nick != "":
|
||||
await self.room.send(f"Hey, a @{mention(to_nick)}!")
|
||||
else:
|
||||
await self.room.send("I have no idea how you did that. This message should never appear...")
|
||||
|
||||
async def on_part(self, session):
|
||||
if session.nick != "":
|
||||
await self.room.send(f"Bye, you @{mention(session.nick)}!")
|
||||
else:
|
||||
await self.room.send("Bye, you lurker!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
bot = TestBot("test")
|
||||
asyncio.get_event_loop().run_until_complete(bot.run())
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
from .bot import Bot
|
||||
from .botmanager import BotManager
|
||||
from .callbacks import Callbacks
|
||||
from .connection import Connection
|
||||
from .exceptions import *
|
||||
from .session import Session
|
||||
from .message import Message
|
||||
from .sessions import Sessions
|
||||
from .messages import Messages
|
||||
from .room import Room
|
||||
from .connection import *
|
||||
from .room import *
|
||||
from .controller import *
|
||||
from .utils import *
|
||||
|
||||
__all__ = connection.__all__ + room.__all__ + controller.__all__ + utils.__all__
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import asyncio
|
||||
|
||||
async def create():
|
||||
await asyncio.sleep(3.0)
|
||||
print("(1) create file")
|
||||
|
||||
async def write():
|
||||
await asyncio.sleep(1.0)
|
||||
print("(2) write into file")
|
||||
|
||||
async def close():
|
||||
print("(3) close file")
|
||||
|
||||
async def test():
|
||||
await create()
|
||||
await write()
|
||||
await close()
|
||||
await asyncio.sleep(2.0)
|
||||
loop.stop()
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
asyncio.ensure_future(test())
|
||||
loop.run_forever()
|
||||
print("Pending tasks at exit: %s" % asyncio.Task.all_tasks(loop))
|
||||
loop.close()
|
||||
600
yaboli/bot.py
600
yaboli/bot.py
|
|
@ -1,600 +0,0 @@
|
|||
import time
|
||||
|
||||
from . import callbacks
|
||||
from . import exceptions
|
||||
from . import room
|
||||
|
||||
class Bot():
|
||||
"""
|
||||
Empty bot class that can be built upon.
|
||||
Takes care of extended botrulez.
|
||||
"""
|
||||
|
||||
def __init__(self, roomname, nick="yaboli", password=None, manager=None,
|
||||
created_in=None, created_by=None):
|
||||
"""
|
||||
roomname - name of the room to connect to
|
||||
nick - nick to assume, None -> no nick
|
||||
password - room password (in case the room is private)
|
||||
created_in - room the bot was created in
|
||||
created_by - nick of the person the bot was created by
|
||||
"""
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
self.created_by = created_by
|
||||
self.created_in = created_in
|
||||
|
||||
self.manager = manager
|
||||
|
||||
# modify/customize this in your __init__() function (or anywhere else you want, for that matter)
|
||||
self.bot_description = ("This bot complies with the botrulez™ (https://github.com/jedevc/botrulez),\n"
|
||||
"plus a few extra commands.")
|
||||
|
||||
self.helptexts = {}
|
||||
self.detailed_helptexts = {}
|
||||
|
||||
self.room = room.Room(roomname, nick=nick, password=password)
|
||||
self.room.add_callback("message", self.on_message)
|
||||
|
||||
self.commands = callbacks.Callbacks()
|
||||
self.bot_specific_commands = []
|
||||
|
||||
self.add_command("clone", self.clone_command, "Clone this bot to another room.", # possibly add option to set nick?
|
||||
("!clone @bot [ <room> [ --pw=<password> ] ]\n"
|
||||
"<room> : the name of the room to clone the bot to\n"
|
||||
"--pw : the room's password\n\n"
|
||||
"Clone this bot to the room specified.\n"
|
||||
"If the target room is passworded, you can use the --pw option to set\n"
|
||||
"a password for the bot to use.\n"
|
||||
"If no room is specified, this will use the current room and password."),
|
||||
bot_specific=False)
|
||||
|
||||
self.add_command("help", self.help_command, "Show help information about the bot.",
|
||||
("!help @bot [ -s | -c | <command> ]\n"
|
||||
"-s : general syntax help\n"
|
||||
"-c : only list the commands\n"
|
||||
"<command> : any command from !help @bot -c\n\n"
|
||||
"Shows detailed help for a command if you specify a command name.\n"
|
||||
"Shows a list of commands and short description if no arguments are given."))
|
||||
|
||||
self.add_command("kill", self.kill_command, "Kill (stop) the bot.",
|
||||
("!kill @bot [ -r ]\n"
|
||||
"-r : restart the bot (will change the id)\n\n"
|
||||
"The bot disconnects from the room and stops."))
|
||||
|
||||
self.add_command("ping", self.ping_command, "Replies 'Pong!'.",
|
||||
("!ping @bot\n\n"
|
||||
"This command was originally used to help distinguish bots from\n"
|
||||
"people. Since the Great UI Change, this is no longer necessary as\n"
|
||||
"bots and people are displayed separately."))
|
||||
|
||||
self.add_command("restart", self.restart_command, "Restart the bot (shorthand for !kill @bot -r).",
|
||||
("!restart @bot\n\n"
|
||||
"Restart the bot.\n"
|
||||
"Short for !kill @bot -r"))
|
||||
|
||||
self.add_command("send", self.send_command, "Send the bot to another room.",
|
||||
("!send @bot <room> [ --pw=<password> ]\n"
|
||||
"--pw : the room's password\n\n"
|
||||
"Sends this bot to the room specified. If the target room is passworded,\n"
|
||||
"you can use the --pw option to set a password for the bot to use."))
|
||||
|
||||
self.add_command("uptime", self.uptime_command, "Show bot uptime since last (re-)start.",
|
||||
("!uptime @bot [ -i s]\n"
|
||||
"-i : show more detailed information\n\n"
|
||||
"Shows the bot's uptime since the last start or restart.\n"
|
||||
"Shows additional information (i.e. id) if the -i flag is set."))
|
||||
|
||||
|
||||
self.add_command("show", self.show_command, detailed_helptext="You've found a hidden command! :)")
|
||||
|
||||
self.room.launch()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
stop() -> None
|
||||
|
||||
Kill this bot.
|
||||
"""
|
||||
|
||||
self.room.stop()
|
||||
|
||||
def add_command(self, command, function, helptext=None, detailed_helptext=None,
|
||||
bot_specific=True):
|
||||
"""
|
||||
add_command(command, function, helptext, detailed_helptext, bot_specific) -> None
|
||||
|
||||
Subscribe a function to a command and add a help text.
|
||||
If no help text is provided, the command won't be displayed by the !help command.
|
||||
This overwrites any previously added command.
|
||||
|
||||
You can "hide" commands by specifying only the detailed helptext,
|
||||
or no helptext at all.
|
||||
|
||||
If the command is not bot specific, no id has to be specified if there are multiple bots
|
||||
with the same nick in a room.
|
||||
"""
|
||||
|
||||
command = command.lower()
|
||||
|
||||
self.commands.remove(command)
|
||||
self.commands.add(command, function)
|
||||
|
||||
if helptext and not command in self.helptexts:
|
||||
self.helptexts[command] = helptext
|
||||
elif not helptext and command in self.helptexts:
|
||||
self.helptexts.pop(command)
|
||||
|
||||
if detailed_helptext and not command in self.detailed_helptexts:
|
||||
self.detailed_helptexts[command] = detailed_helptext
|
||||
elif not detailed_helptext and command in self.detailed_helptexts:
|
||||
self.detailed_helptexts.pop(command)
|
||||
|
||||
if bot_specific and not command in self.bot_specific_commands:
|
||||
self.bot_specific_commands.append(command)
|
||||
elif not bot_specific and command in self.bot_specific_commands:
|
||||
self.bot_specific_commands.remove(command)
|
||||
|
||||
def call_command(self, message):
|
||||
"""
|
||||
call_command(message) -> None
|
||||
|
||||
Calls all functions subscribed to the command with the arguments supplied in the message.
|
||||
Deals with the situation that multiple bots of the same type and nick are in the same room.
|
||||
"""
|
||||
|
||||
try:
|
||||
command, bot_id, nick, arguments, flags, options = self.parse(message.content)
|
||||
except exceptions.ParseMessageException:
|
||||
return
|
||||
else:
|
||||
command = command.lower()
|
||||
nick = self.room.mentionable(nick).lower()
|
||||
|
||||
if not self.commands.exists(command):
|
||||
return
|
||||
|
||||
if not nick == self.mentionable().lower():
|
||||
return
|
||||
|
||||
if bot_id is not None: # id specified
|
||||
if self.manager.get(bot_id) == self:
|
||||
self.commands.call(command, message, arguments, flags, options)
|
||||
else:
|
||||
return
|
||||
|
||||
else: # no id specified
|
||||
bots = self.manager.get_similar(self.roomname(), nick)
|
||||
if self.manager.get_id(self) == min(bots): # only one bot should act
|
||||
# either the bot is unique or the command is not bot-specific
|
||||
if not command in self.bot_specific_commands or len(bots) == 1:
|
||||
self.commands.call(command, message, arguments, flags, options)
|
||||
|
||||
else: # user has to select a bot
|
||||
msg = ("There are multiple bots with that nick in this room. To select one,\n"
|
||||
"please specify its id (from the list below) as follows:\n"
|
||||
"!{} <id> @{} [your arguments...]\n").format(command, nick)
|
||||
|
||||
for bot_id in sorted(bots):
|
||||
bot = bots[bot_id]
|
||||
msg += "\n{} - @{} ({})".format(bot_id, bot.mentionable(), bot.creation_info())
|
||||
|
||||
self.room.send_message(msg, parent=message.id)
|
||||
|
||||
def roomname(self):
|
||||
"""
|
||||
roomname() -> roomname
|
||||
|
||||
The room the bot is connected to.
|
||||
"""
|
||||
|
||||
return self.room.room
|
||||
|
||||
def password(self):
|
||||
"""
|
||||
password() -> password
|
||||
|
||||
The current room's password.
|
||||
"""
|
||||
|
||||
return self.room.password
|
||||
|
||||
def nick(self):
|
||||
"""
|
||||
nick() -> nick
|
||||
|
||||
The bot's full nick.
|
||||
"""
|
||||
|
||||
return self.room.nick
|
||||
|
||||
def mentionable(self):
|
||||
"""
|
||||
mentionable() -> nick
|
||||
|
||||
The bot's nick in a mentionable format.
|
||||
"""
|
||||
|
||||
return self.room.mentionable()
|
||||
|
||||
def creation_info(self):
|
||||
"""
|
||||
creation_info() -> str
|
||||
|
||||
Formatted info about the bot's creation
|
||||
"""
|
||||
|
||||
info = "created {}".format(self.format_date())
|
||||
|
||||
if self.created_by:
|
||||
info += " by @{}".format(self.room.mentionable(self.created_by))
|
||||
|
||||
if self.created_in:
|
||||
info += " in &{}".format(self.created_in)
|
||||
|
||||
return info
|
||||
|
||||
def format_date(self, seconds=None):
|
||||
"""
|
||||
format_date(seconds) -> str
|
||||
|
||||
Format a time in epoch format to the format specified in self.date_format.
|
||||
Defaults to self.start_time.
|
||||
"""
|
||||
|
||||
if seconds is None:
|
||||
seconds = self.start_time
|
||||
|
||||
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(seconds))
|
||||
|
||||
def format_delta(self, delta=None):
|
||||
"""
|
||||
format_delta(delta) -> str
|
||||
|
||||
Format a difference in seconds to the following format:
|
||||
[- ][<days>d ][<hours>h ][<minutes>m ]<seconds>s
|
||||
Defaults to the current uptime if no delta is specified.
|
||||
"""
|
||||
|
||||
if not delta:
|
||||
delta = time.time() - self.start_time
|
||||
|
||||
delta = int(delta)
|
||||
uptime = ""
|
||||
|
||||
if delta < 0:
|
||||
uptime += "- "
|
||||
delta = -delta
|
||||
|
||||
if delta >= 24*60*60:
|
||||
uptime +="{}d ".format(delta//(24*60*60))
|
||||
delta %= 24*60*60
|
||||
|
||||
if delta >= 60*60:
|
||||
uptime += "{}h ".format(delta//(60*60))
|
||||
delta %= 60*60
|
||||
|
||||
if delta >= 60:
|
||||
uptime += "{}m ".format(delta//60)
|
||||
delta %= 60
|
||||
|
||||
uptime += "{}s".format(delta)
|
||||
|
||||
return uptime
|
||||
|
||||
def parse_command(self, message):
|
||||
"""
|
||||
parse_command(message_content) -> command, bot_id, nick, argpart
|
||||
|
||||
Parse the "!command[ bot_id] @botname[ argpart]" part of a command.
|
||||
"""
|
||||
|
||||
# command name (!command)
|
||||
split = message.split(maxsplit=1)
|
||||
|
||||
if len(split) < 2:
|
||||
raise exceptions.ParseMessageException("Not enough arguments")
|
||||
elif split[0][:1] != "!":
|
||||
raise exceptions.ParseMessageException("Not a command")
|
||||
|
||||
command = split[0][1:]
|
||||
message = split[1]
|
||||
split = message.split(maxsplit=1)
|
||||
|
||||
# bot id
|
||||
try:
|
||||
bot_id = int(split[0])
|
||||
except ValueError:
|
||||
bot_id = None
|
||||
else:
|
||||
if len(split) <= 1:
|
||||
raise exceptions.ParseMessageException("No bot nick")
|
||||
|
||||
message = split[1]
|
||||
split = message.split(maxsplit=1)
|
||||
|
||||
# bot nick (@mention)
|
||||
if split[0][:1] != "@":
|
||||
raise exceptions.ParseMessageException("No bot nick")
|
||||
|
||||
nick = split[0][1:]
|
||||
|
||||
# arguments to the command
|
||||
if len(split) > 1:
|
||||
argpart = split[1]
|
||||
else:
|
||||
argpart = None
|
||||
|
||||
return command, bot_id, nick, argpart
|
||||
|
||||
def parse_arguments(self, argstr):
|
||||
"""
|
||||
parse_arguments(argstr) -> arguments, flags, options
|
||||
|
||||
Parse the argument part of a command.
|
||||
"""
|
||||
|
||||
argstr += " " # so the last argument will also be captured
|
||||
|
||||
escaping = False
|
||||
quot_marks = None
|
||||
type_signs = 0
|
||||
option = None
|
||||
word = ""
|
||||
|
||||
arguments = []
|
||||
flags = ""
|
||||
options = {}
|
||||
|
||||
for char in argstr:
|
||||
|
||||
# backslash-escaping
|
||||
if escaping:
|
||||
word += char
|
||||
escaping = False
|
||||
elif char == "\\":
|
||||
escaping = True
|
||||
|
||||
# quotation mark escaped strings
|
||||
elif quot_marks:
|
||||
if char == quot_marks:
|
||||
quot_marks = None
|
||||
else:
|
||||
word += char
|
||||
elif char in ['"', "'"]:
|
||||
quot_marks = char
|
||||
|
||||
# type signs
|
||||
elif char == "-":
|
||||
if type_signs < 2 and not word:
|
||||
type_signs += 1
|
||||
else:
|
||||
word += char
|
||||
|
||||
# "=" in options
|
||||
elif char == "=" and type_signs == 2 and word and not option:
|
||||
option = word
|
||||
word = ""
|
||||
|
||||
# space - evaluate information collected so far
|
||||
elif char == " ":
|
||||
if word:
|
||||
if type_signs == 0: # argument
|
||||
arguments.append(word)
|
||||
elif type_signs == 1: # flag(s)
|
||||
for flag in word:
|
||||
if not flag in flags:
|
||||
flags += flag
|
||||
elif type_signs == 2: # option
|
||||
if option:
|
||||
options[option] = word
|
||||
else:
|
||||
options[word] = True
|
||||
|
||||
# reset all state variables
|
||||
escaping = False
|
||||
quot_marks = None
|
||||
type_signs = 0
|
||||
option = None
|
||||
word = ""
|
||||
|
||||
# all other chars and situations
|
||||
else:
|
||||
word += char
|
||||
|
||||
return arguments, flags, options
|
||||
|
||||
def parse(self, message):
|
||||
"""
|
||||
parse(message_content) -> bool
|
||||
|
||||
Parse a message.
|
||||
"""
|
||||
|
||||
command, bot_id, nick, argpart = self.parse_command(message)
|
||||
|
||||
if argpart:
|
||||
arguments, flags, options = self.parse_arguments(argpart)
|
||||
else:
|
||||
arguments = []
|
||||
flags = ""
|
||||
options = {}
|
||||
|
||||
return command, bot_id, nick, arguments, flags, options
|
||||
|
||||
# ----- HANDLING OF EVENTS -----
|
||||
|
||||
def on_message(self, message):
|
||||
"""
|
||||
on_message(message) -> None
|
||||
|
||||
Gets called when a message is received (see __init__).
|
||||
If you want to add a command to your bot, consider using add_command instead of overwriting
|
||||
this function.
|
||||
"""
|
||||
|
||||
self.call_command(message)
|
||||
|
||||
# ----- COMMANDS -----
|
||||
|
||||
def clone_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
clone_command(message, *arguments, flags, options) -> None
|
||||
|
||||
Create a new bot.
|
||||
"""
|
||||
|
||||
if not arguments:
|
||||
room = self.roomname()
|
||||
password = self.room.password
|
||||
else:
|
||||
room = arguments[0]
|
||||
|
||||
if room[:1] == "&":
|
||||
room = room[1:]
|
||||
|
||||
if "pw" in options and options["pw"] is not True:
|
||||
password = options["pw"]
|
||||
else:
|
||||
password = None
|
||||
|
||||
try:
|
||||
bot = self.manager.create(room, password=password, created_in=self.roomname(),
|
||||
created_by=message.sender.name)
|
||||
except exceptions.CreateBotException:
|
||||
self.room.send_message("Bot could not be cloned.", parent=message.id)
|
||||
else:
|
||||
self.room.send_message("Cloned @{} to &{}.".format(bot.mentionable(), room),
|
||||
parent=message.id)
|
||||
|
||||
def help_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
help_command(message, *arguments, flags, options) -> None
|
||||
|
||||
Show help about the bot.
|
||||
"""
|
||||
|
||||
if arguments: # detailed help for one command
|
||||
command = arguments[0]
|
||||
if command[:1] == "!":
|
||||
command = command[1:]
|
||||
|
||||
if command in self.detailed_helptexts:
|
||||
msg = "Detailed help for !{}:\n".format(command)
|
||||
msg += self.detailed_helptexts[command]
|
||||
else:
|
||||
msg = "No detailed help text found for !{}.".format(command)
|
||||
if command in self.helptexts:
|
||||
msg += "\n\n" + self.helptexts[command]
|
||||
|
||||
elif "s" in flags: # detailed syntax help
|
||||
msg = ("SYNTAX HELP PLACEHOLDER")
|
||||
|
||||
else: # just list all commands
|
||||
msg = ""
|
||||
|
||||
if not "c" in flags:
|
||||
msg += self.bot_description + "\n\n"
|
||||
|
||||
msg += "This bot supports the following commands:"
|
||||
for command in sorted(self.helptexts):
|
||||
helptext = self.helptexts[command]
|
||||
msg += "\n!{} - {}".format(command, helptext)
|
||||
|
||||
if not "c" in flags:
|
||||
msg += ("\n\nFor help on the command syntax, try: !help @{0} -s\n"
|
||||
"For detailed help on a command, try: !help @{0} <command>\n"
|
||||
"(Hint: Most commands have extra functionality, which is listed in their detailed help.)")
|
||||
msg = msg.format(self.mentionable())
|
||||
|
||||
self.room.send_message(msg, parent=message.id)
|
||||
|
||||
def kill_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
kill_command(message, *arguments, flags, options) -> None
|
||||
|
||||
stop the bot.
|
||||
"""
|
||||
|
||||
if "r" in flags:
|
||||
bot = self.manager.create(self.roomname())
|
||||
bot.created_by = self.created_by
|
||||
bot.created_in = self.created_in
|
||||
|
||||
self.room.send_message("/me exits.", message.id)
|
||||
|
||||
self.manager.remove(self.manager.get_id(self))
|
||||
|
||||
def ping_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
ping_command(message, *arguments, flags, options) -> None
|
||||
|
||||
Send a "Pong!" reply on a !ping command.
|
||||
"""
|
||||
|
||||
self.room.send_message("Pong!", parent=message.id)
|
||||
|
||||
def restart_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
restart_command(message, *arguments, flags, options) -> None
|
||||
|
||||
Restart the bot (shorthand for !kill @bot -r).
|
||||
"""
|
||||
|
||||
self.commands.call("kill", message, [], "r", {})
|
||||
|
||||
def send_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
_command(message, *arguments, flags, options) -> None
|
||||
|
||||
Send this bot to another room.
|
||||
"""
|
||||
|
||||
if not arguments:
|
||||
return
|
||||
else:
|
||||
room = arguments[0]
|
||||
|
||||
if room[:1] == "&":
|
||||
room = room[1:]
|
||||
|
||||
if "pw" in options and options["pw"] is not True:
|
||||
password = options["pw"]
|
||||
else:
|
||||
password = None
|
||||
|
||||
self.room.send_message("/me moves to &{}.".format(room), parent=message.id)
|
||||
|
||||
self.room.change(room, password=password)
|
||||
self.room.launch()
|
||||
|
||||
def show_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
show_command(message, arguments, flags, options) -> None
|
||||
|
||||
Show arguments, flags and options.
|
||||
"""
|
||||
|
||||
msg = "arguments: {}\nflags: {}\noptions: {}".format(arguments, repr(flags), options)
|
||||
self.room.send_message(msg, parent=message.id)
|
||||
|
||||
def uptime_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
uptime_command(message, arguments, flags, options) -> None
|
||||
|
||||
Show uptime and other info.
|
||||
"""
|
||||
|
||||
stime = self.format_date()
|
||||
utime = self.format_delta()
|
||||
|
||||
if "i" in flags:
|
||||
msg = "uptime: {} ({})".format(stime, utime)
|
||||
msg += "\nid: {}".format(self.manager.get_id(self))
|
||||
msg += "\n{}".format(self.creation_info())
|
||||
|
||||
else:
|
||||
msg = "/me is up since {} ({}).".format(stime, utime)
|
||||
|
||||
self.room.send_message(msg, message.id)
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
import json
|
||||
|
||||
from . import bot
|
||||
from . import exceptions
|
||||
|
||||
class BotManager():
|
||||
"""
|
||||
Keep track of multiple bots in different rooms.
|
||||
"""
|
||||
|
||||
def __init__(self, bot_class, default_nick="yaboli", max_bots=100,
|
||||
bots_file="bots.json", data_file="data.json"):
|
||||
"""
|
||||
bot_class - class to create instances of
|
||||
default_nick - default nick for all bots to assume when no nick is specified
|
||||
max_bots - maximum number of bots allowed to exist simultaneously
|
||||
None or 0 - no limit
|
||||
bots_file - file the bot backups are saved to
|
||||
None - no bot backups
|
||||
data_file - file the bot data is saved to
|
||||
- None - bot data isn't saved
|
||||
"""
|
||||
|
||||
self.bot_class = bot_class
|
||||
self.max_bots = max_bots
|
||||
self.default_nick = default_nick
|
||||
|
||||
self.bots_file = bots_file
|
||||
self.data_file = data_file
|
||||
|
||||
self._bots = {}
|
||||
self._bot_id = 0
|
||||
self._bot_data = {}
|
||||
|
||||
self._load_bots()
|
||||
|
||||
def create(self, room, password=None, nick=None, created_in=None, created_by=None):
|
||||
"""
|
||||
create(room, password, nick) -> bot
|
||||
|
||||
Create a new bot in room.
|
||||
"""
|
||||
|
||||
if nick is None:
|
||||
nick = self.default_nick
|
||||
|
||||
if self.max_bots and len(self._bots) >= self.max_bots:
|
||||
raise exceptions.CreateBotException("max_bots limit hit")
|
||||
else:
|
||||
bot = self.bot_class(room, nick=nick, password=password, manager=self,
|
||||
created_in=created_in, created_by=created_by)
|
||||
self._bots[self._bot_id] = bot
|
||||
self._bot_id += 1
|
||||
|
||||
self._save_bots()
|
||||
|
||||
return bot
|
||||
|
||||
def remove(self, bot_id):
|
||||
"""
|
||||
remove(bot_id) -> None
|
||||
|
||||
Kill a bot and remove it from the list of bots.
|
||||
"""
|
||||
|
||||
if bot_id in self._bots:
|
||||
self._bots[bot_id].stop()
|
||||
self._bots.pop(bot_id)
|
||||
|
||||
self._save_bots()
|
||||
|
||||
def get(self, bot_id):
|
||||
"""
|
||||
get(self, bot_id) -> bot
|
||||
|
||||
Return bot with that id, if found.
|
||||
"""
|
||||
|
||||
if bot_id in self._bots:
|
||||
return self._bots[bot_id]
|
||||
|
||||
def get_id(self, bot):
|
||||
"""
|
||||
get_id(bot) -> bot_id
|
||||
|
||||
Return the bot id, if the bot is known.
|
||||
"""
|
||||
|
||||
for bot_id, own_bot in self._bots.items():
|
||||
if bot == own_bot:
|
||||
return bot_id
|
||||
|
||||
def get_similar(self, room, nick):
|
||||
"""
|
||||
get_by_room(room, nick) -> dict
|
||||
|
||||
Collect all bots that are connected to the room and have that nick.
|
||||
"""
|
||||
|
||||
return {bot_id: bot for bot_id, bot in self._bots.items()
|
||||
if bot.roomname() == room and bot.mentionable().lower() == nick.lower()}
|
||||
|
||||
def _load_bots(self):
|
||||
"""
|
||||
_load_bots() -> None
|
||||
|
||||
Load and create bots from self.bots_file.
|
||||
"""
|
||||
|
||||
if not self.bots_file:
|
||||
return
|
||||
|
||||
try:
|
||||
with open(self.bots_file) as f:
|
||||
bots = json.load(f)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
for bot_info in bots:
|
||||
bot = self.create(bot_info["room"], password=bot_info["password"],
|
||||
nick=bot_info["nick"])
|
||||
bot.created_in = bot_info["created_in"]
|
||||
bot.created_by = bot_info["created_by"]
|
||||
|
||||
def _save_bots(self):
|
||||
"""
|
||||
_save_bots() -> None
|
||||
|
||||
Save all current bots to self.bots_file.
|
||||
"""
|
||||
|
||||
if not self.bots_file:
|
||||
return
|
||||
|
||||
bots = []
|
||||
|
||||
for bot_id, bot in self._bots.items():
|
||||
bot_info = {}
|
||||
|
||||
bot_info["room"] = bot.roomname()
|
||||
bot_info["password"] = bot.password()
|
||||
bot_info["nick"] = bot.nick()
|
||||
bot_info["created_in"] = bot.created_in
|
||||
bot_info["created_by"] = bot.created_by
|
||||
|
||||
bots.append(bot_info)
|
||||
|
||||
with open(self.bots_file, "w") as f:
|
||||
json.dump(bots, f)
|
||||
|
|
@ -5,7 +5,9 @@ asyncio.get_event_loop().set_debug(True)
|
|||
|
||||
import json
|
||||
import websockets
|
||||
from websockets import ConnectionClosed
|
||||
#from websockets import ConnectionClosed
|
||||
|
||||
__all__ = ["Connection"]
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
from room import Room
|
||||
from .room import Room
|
||||
|
||||
__all__ = ["Controller"]
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
class YaboliException(Exception):
|
||||
"""
|
||||
Generic yaboli exception class.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class BotManagerException(YaboliException):
|
||||
"""
|
||||
Generic BotManager exception class.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class CreateBotException(BotManagerException):
|
||||
"""
|
||||
This exception will be raised when BotManager could not create a bot.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class BotNotFoundException(BotManagerException):
|
||||
"""
|
||||
This exception will be raised when BotManager could not find a bot.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class BotException(YaboliException):
|
||||
"""
|
||||
Generic Bot exception class.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class ParseMessageException(BotException):
|
||||
"""
|
||||
This exception will be raised when a failure parsing a message occurs.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
import time
|
||||
|
||||
from . import session
|
||||
|
||||
class Message():
|
||||
"""
|
||||
This class keeps track of message details.
|
||||
"""
|
||||
|
||||
def __init__(self, id, time, sender, content, parent=None, edited=None, deleted=None,
|
||||
truncated=None):
|
||||
"""
|
||||
id - message id
|
||||
time - time the message was sent (epoch)
|
||||
sender - session of the sender
|
||||
content - content of the message
|
||||
parent - id of the parent message, or None
|
||||
edited - time of last edit (epoch)
|
||||
deleted - time of deletion (epoch)
|
||||
truncated - message was truncated
|
||||
"""
|
||||
|
||||
self.id = id
|
||||
self.time = time
|
||||
self.sender = sender
|
||||
self.content = content
|
||||
self.parent = parent
|
||||
self.edited = edited
|
||||
self.deleted = deleted
|
||||
self.truncated = truncated
|
||||
|
||||
@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 = session.Session.from_data(data["sender"])
|
||||
parent = data["parent"] if "parent" in data else None
|
||||
edited = data["edited"] if "edited" in data else None
|
||||
deleted = data["deleted"] if "deleted" in data else None
|
||||
truncated = data["truncated"] if "truncated" in data else None
|
||||
|
||||
return self(
|
||||
data["id"],
|
||||
data["time"],
|
||||
sender,
|
||||
data["content"],
|
||||
parent=parent,
|
||||
edited=edited,
|
||||
deleted=deleted,
|
||||
truncated=truncated
|
||||
)
|
||||
|
||||
def time_formatted(self, date=False):
|
||||
"""
|
||||
time_formatted(date=False) -> 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 is_edited(self):
|
||||
"""
|
||||
is_edited() -> bool
|
||||
|
||||
Has this message been edited?
|
||||
"""
|
||||
|
||||
return True if self.edited is not None else False
|
||||
|
||||
def is_deleted(self):
|
||||
"""
|
||||
is_deleted() -> bool
|
||||
|
||||
Has this message been deleted?
|
||||
"""
|
||||
|
||||
return True if self.deleted is not None else False
|
||||
|
||||
def is_truncated(self):
|
||||
"""
|
||||
is_truncated() -> bool
|
||||
|
||||
Has this message been truncated?
|
||||
"""
|
||||
|
||||
return True if self.truncated is not None else False
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
import operator
|
||||
|
||||
from . import message
|
||||
|
||||
class Messages():
|
||||
"""
|
||||
Message storage class which preserves thread hierarchy.
|
||||
"""
|
||||
|
||||
def __init__(self, message_limit=500):
|
||||
"""
|
||||
message_limit - maximum amount of messages that will be stored at a time
|
||||
None - no limit
|
||||
"""
|
||||
|
||||
self.message_limit = message_limit
|
||||
|
||||
self._by_id = {}
|
||||
self._by_parent = {}
|
||||
|
||||
def _sort(self, msgs):
|
||||
"""
|
||||
_sort(messages) -> None
|
||||
|
||||
Sorts a list of messages by their id, in place.
|
||||
"""
|
||||
|
||||
msgs.sort(key=operator.attrgetter("id"))
|
||||
|
||||
def add_from_data(self, data):
|
||||
"""
|
||||
add_from_data(data) -> None
|
||||
|
||||
Create a message from "raw" data and add it.
|
||||
"""
|
||||
|
||||
mes = message.Message.from_data(data)
|
||||
|
||||
self.add(mes)
|
||||
|
||||
def add(self, mes):
|
||||
"""
|
||||
add(message) -> None
|
||||
|
||||
Add a message to the structure.
|
||||
"""
|
||||
|
||||
self.remove(mes.id)
|
||||
|
||||
self._by_id[mes.id] = mes
|
||||
|
||||
if mes.parent:
|
||||
if not mes.parent in self._by_parent:
|
||||
self._by_parent[mes.parent] = []
|
||||
self._by_parent[mes.parent].append(mes)
|
||||
|
||||
if self.message_limit and len(self._by_id) > self.message_limit:
|
||||
self.remove(self.get_oldest().id)
|
||||
|
||||
def remove(self, mid):
|
||||
"""
|
||||
remove(message_id) -> None
|
||||
|
||||
Remove a message from the structure.
|
||||
"""
|
||||
|
||||
mes = self.get(mid)
|
||||
if mes:
|
||||
if mes.id in self._by_id:
|
||||
self._by_id.pop(mes.id)
|
||||
|
||||
parent = self.get_parent(mes.id)
|
||||
if parent and mes in self.get_children(parent.id):
|
||||
self._by_parent[mes.parent].remove(mes)
|
||||
|
||||
def remove_all(self):
|
||||
"""
|
||||
remove_all() -> None
|
||||
|
||||
Removes all messages.
|
||||
"""
|
||||
|
||||
self._by_id = {}
|
||||
self._by_parent = {}
|
||||
|
||||
def get(self, mid):
|
||||
"""
|
||||
get(message_id) -> Message
|
||||
|
||||
Returns the message with the given id, if found.
|
||||
"""
|
||||
|
||||
if mid in self._by_id:
|
||||
return self._by_id[mid]
|
||||
|
||||
def get_oldest(self):
|
||||
"""
|
||||
get_oldest() -> Message
|
||||
|
||||
Returns the oldest message, if found.
|
||||
"""
|
||||
|
||||
oldest = None
|
||||
for mid in self._by_id:
|
||||
if oldest is None or mid < oldest:
|
||||
oldest = mid
|
||||
return self.get(oldest)
|
||||
|
||||
def get_youngest(self):
|
||||
"""
|
||||
get_youngest() -> Message
|
||||
|
||||
Returns the youngest message, if found.
|
||||
"""
|
||||
|
||||
youngest = None
|
||||
for mid in self._by_id:
|
||||
if youngest is None or mid > youngest:
|
||||
youngest = mid
|
||||
return self.get(youngest)
|
||||
|
||||
def get_parent(self, mid):
|
||||
"""
|
||||
get_parent(message_id) -> str
|
||||
|
||||
Returns the message's parent, if found.
|
||||
"""
|
||||
|
||||
mes = self.get(mid)
|
||||
if mes:
|
||||
return self.get(mes.parent)
|
||||
|
||||
def get_children(self, mid):
|
||||
"""
|
||||
get_children(message_id) -> list
|
||||
|
||||
Returns a sorted list of children of the given message, if found.
|
||||
"""
|
||||
|
||||
if mid in self._by_parent:
|
||||
children = self._by_parent[mid][:]
|
||||
self._sort(children)
|
||||
return children
|
||||
|
||||
def get_top_level(self):
|
||||
"""
|
||||
get_top_level() -> list
|
||||
|
||||
Returns a sorted list of top-level messages.
|
||||
"""
|
||||
|
||||
msgs = [self.get(mid) for mid in self._by_id if not self.get(mid).parent]
|
||||
self._sort(msgs)
|
||||
return msgs
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
import asyncio
|
||||
from connection import Connection
|
||||
import utils
|
||||
from .connection import *
|
||||
from .utils import *
|
||||
|
||||
__all__ = ["Room"]
|
||||
|
||||
|
||||
|
||||
class Room:
|
||||
ROOM_FORMAT = "wss://euphoria.io/room/{}/ws"
|
||||
|
|
@ -17,7 +21,7 @@ class Room:
|
|||
# If you need to keep track of messages, use utils.Log.
|
||||
self.session = None
|
||||
self.account = None
|
||||
self.listing = utils.Listing()
|
||||
self.listing = Listing()
|
||||
|
||||
# Various room information
|
||||
self.account_has_access = None
|
||||
|
|
@ -118,7 +122,7 @@ class Room:
|
|||
response = await self._conn.send("send", data)
|
||||
self._check_for_errors(response)
|
||||
|
||||
message = utils.Message.from_dict(response.get("data"))
|
||||
message = Message.from_dict(response.get("data"))
|
||||
return message
|
||||
|
||||
async def who(self):
|
||||
|
|
@ -170,7 +174,7 @@ class Room:
|
|||
# TODO: log throttled
|
||||
|
||||
if "error" in packet:
|
||||
raise utils.ResponseError(response.get("error"))
|
||||
raise ResponseError(response.get("error"))
|
||||
|
||||
async def _handle_bounce(self, packet):
|
||||
"""
|
||||
|
|
@ -210,7 +214,7 @@ class Room:
|
|||
"""
|
||||
|
||||
data = packet.get("data")
|
||||
self.session = utils.Session.from_dict(data.get("session"))
|
||||
self.session = Session.from_dict(data.get("session"))
|
||||
self.room_is_private = data.get("room_is_private")
|
||||
self.version = data.get("version")
|
||||
self.account = data.get("account", None)
|
||||
|
|
@ -234,7 +238,7 @@ class Room:
|
|||
"""
|
||||
|
||||
data = packet.get("data")
|
||||
session = utils.Session.from_dict(data)
|
||||
session = Session.from_dict(data)
|
||||
|
||||
# update self.listing
|
||||
self.listing.add(session)
|
||||
|
|
@ -282,7 +286,7 @@ class Room:
|
|||
"""
|
||||
|
||||
data = packet.get("data")
|
||||
session = utils.Session.from_dict(data)
|
||||
session = Session.from_dict(data)
|
||||
|
||||
# update self.listing
|
||||
self.listing.remove(session.session_id)
|
||||
|
|
@ -315,7 +319,7 @@ class Room:
|
|||
"""
|
||||
|
||||
data = packet.get("data")
|
||||
message = utils.Message.from_dict(data)
|
||||
message = Message.from_dict(data)
|
||||
|
||||
await self.controller.on_send(message)
|
||||
|
||||
|
|
@ -328,8 +332,8 @@ class Room:
|
|||
|
||||
data = packet.get("data")
|
||||
|
||||
sessions = [utils.Session.from_dict(d) for d in data.get("listing")]
|
||||
messages = [utils.Message.from_dict(d) for d in data.get("log")]
|
||||
sessions = [Session.from_dict(d) for d in data.get("listing")]
|
||||
messages = [Message.from_dict(d) for d in data.get("log")]
|
||||
|
||||
# update self.listing
|
||||
for session in sessions:
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
class Session():
|
||||
"""
|
||||
This class keeps track of session details.
|
||||
"""
|
||||
|
||||
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(self, data):
|
||||
"""
|
||||
Creates and returns a session created from the data.
|
||||
|
||||
data - a euphoria SessionView: http://api.euphoria.io/#sessionview
|
||||
"""
|
||||
|
||||
is_staff = data["is_staff"] if "is_staff" in data else None
|
||||
is_manager = data["is_manager"] if "is_manager" in data else None
|
||||
|
||||
return self(
|
||||
data["id"],
|
||||
data["name"],
|
||||
data["server_id"],
|
||||
data["server_era"],
|
||||
data["session_id"],
|
||||
is_staff,
|
||||
is_manager
|
||||
)
|
||||
|
||||
def session_type(self):
|
||||
"""
|
||||
session_type() -> str
|
||||
|
||||
The session's type (bot, account, agent).
|
||||
"""
|
||||
|
||||
return self.id.split(":")[0]
|
||||
|
||||
def is_staff(self):
|
||||
"""
|
||||
is_staff() -> bool
|
||||
|
||||
Is a user staff?
|
||||
"""
|
||||
|
||||
return self.staff and True or False
|
||||
|
||||
def is_manager(self):
|
||||
"""
|
||||
is_manager() -> bool
|
||||
|
||||
Is a user manager?
|
||||
"""
|
||||
|
||||
return self.staff and True or False
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
from . import session
|
||||
|
||||
class Sessions():
|
||||
"""
|
||||
Keeps track of sessions.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
TODO
|
||||
"""
|
||||
self._sessions = {}
|
||||
|
||||
def add_from_data(self, data):
|
||||
"""
|
||||
add_raw(data) -> None
|
||||
|
||||
Create a session from "raw" data and add it.
|
||||
"""
|
||||
|
||||
ses = session.Session.from_data(data)
|
||||
|
||||
self._sessions[ses.session_id] = ses
|
||||
|
||||
def add(self, ses):
|
||||
"""
|
||||
add(session) -> None
|
||||
|
||||
Add a session.
|
||||
"""
|
||||
|
||||
self._sessions[ses.session_id] = ses
|
||||
|
||||
def get(self, sid):
|
||||
"""
|
||||
get(session_id) -> Session
|
||||
|
||||
Returns the session with that id.
|
||||
"""
|
||||
|
||||
if sid in self._sessions:
|
||||
return self._sessions[sid]
|
||||
|
||||
def remove(self, sid):
|
||||
"""
|
||||
remove(session) -> None
|
||||
|
||||
Remove a session.
|
||||
"""
|
||||
|
||||
if sid in self._sessions:
|
||||
self._sessions.pop(sid)
|
||||
|
||||
def remove_on_network_partition(self, server_id, server_era):
|
||||
"""
|
||||
remove_on_network_partition(server_id, server_era) -> None
|
||||
|
||||
Removes all sessions matching the server_id/server_era combo.
|
||||
http://api.euphoria.io/#network-event
|
||||
"""
|
||||
|
||||
sesnew = {}
|
||||
for sid in self._sessions:
|
||||
ses = self.get(sid)
|
||||
if not (ses.server_id == server_id and ses.server_era == server_era):
|
||||
sesnew[sid] = ses
|
||||
self._sessions = sesnew
|
||||
|
||||
def remove_all(self):
|
||||
"""
|
||||
remove_all() -> None
|
||||
|
||||
Removes all sessions.
|
||||
"""
|
||||
|
||||
self._sessions = {}
|
||||
|
||||
def get_all(self):
|
||||
"""
|
||||
get_all() -> list
|
||||
|
||||
Returns the full list of sessions.
|
||||
"""
|
||||
|
||||
return [ses for sid, ses in self._sessions.items()]
|
||||
|
||||
def get_people(self):
|
||||
"""
|
||||
get_people() -> list
|
||||
|
||||
Returns a list of all non-bot and non-lurker sessions.
|
||||
"""
|
||||
|
||||
# not a list comprehension because that would span several lines too
|
||||
people = []
|
||||
for sid in self._sessions:
|
||||
ses = self.get(sid)
|
||||
if ses.session_type() in ["agent", "account"] and ses.name:
|
||||
people.append(ses)
|
||||
return people
|
||||
|
||||
def _get_by_type(self, tp):
|
||||
"""
|
||||
_get_by_type(session_type) -> list
|
||||
|
||||
Returns a list of all non-lurker sessions with that type.
|
||||
"""
|
||||
|
||||
return [ses for sid, ses in self._sessions.items()
|
||||
if ses.session_type() == tp and ses.name]
|
||||
|
||||
def get_accounts(self):
|
||||
"""
|
||||
get_accounts() -> list
|
||||
|
||||
Returns a list of all logged-in sessions.
|
||||
"""
|
||||
|
||||
return self._get_by_type("account")
|
||||
|
||||
def get_agents(self):
|
||||
"""
|
||||
get_agents() -> list
|
||||
|
||||
Returns a list of all sessions who are not signed into an account and not bots or lurkers.
|
||||
"""
|
||||
|
||||
return self._get_by_type("agent")
|
||||
|
||||
def get_bots(self):
|
||||
"""
|
||||
get_bots() -> list
|
||||
|
||||
Returns a list of all bot sessions.
|
||||
"""
|
||||
|
||||
return self._get_by_type("bot")
|
||||
|
||||
def get_lurkers(self):
|
||||
"""
|
||||
get_lurkers() -> list
|
||||
|
||||
Returns a list of all lurker sessions.
|
||||
"""
|
||||
|
||||
return [ses for sid, ses in self._sessions.items() if not ses.name]
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import re
|
||||
|
||||
__all__ = ["mention", "mention_reduced", "similar", "Session", "Listing", "Message", "Log"]
|
||||
__all__ = [
|
||||
"mention", "mention_reduced", "similar",
|
||||
"Session", "Listing",
|
||||
"Message", "Log",
|
||||
"ResponseError"
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
|
@ -111,3 +114,6 @@ class Message():
|
|||
|
||||
class Log:
|
||||
pass # TODO
|
||||
|
||||
class ResponseError(Exception):
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue