From fdbe866a70bcdb5b04352670eeedddb6d7d3c5a4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sun, 18 Sep 2016 17:53:56 +0000 Subject: [PATCH 01/10] Rewrite BotManager and add module-wide logging --- yaboli/__init__.py | 17 +- yaboli/bot.py | 616 ++---------------------------------------- yaboli/botmanager.py | 209 ++++++++------- yaboli/callbacks.py | 62 ----- yaboli/connection.py | 229 ---------------- yaboli/exceptions.py | 41 --- yaboli/message.py | 99 ------- yaboli/messages.py | 154 ----------- yaboli/room.py | 617 ------------------------------------------- yaboli/session.py | 71 ----- yaboli/sessions.py | 146 ---------- 11 files changed, 147 insertions(+), 2114 deletions(-) delete mode 100644 yaboli/callbacks.py delete mode 100644 yaboli/connection.py delete mode 100644 yaboli/exceptions.py delete mode 100644 yaboli/message.py delete mode 100644 yaboli/messages.py delete mode 100644 yaboli/room.py delete mode 100644 yaboli/session.py delete mode 100644 yaboli/sessions.py diff --git a/yaboli/__init__.py b/yaboli/__init__.py index 8aed845..846b148 100644 --- a/yaboli/__init__.py +++ b/yaboli/__init__.py @@ -1,10 +1,11 @@ +import logging +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +formatter = logging.Formatter('%(asctime)s|%(name)s|%(levelname)s| %(message)s') +sh = logging.StreamHandler() +sh.setFormatter(formatter) +logger.addHandler(sh) + 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 diff --git a/yaboli/bot.py b/yaboli/bot.py index 68ee2f8..8bf0f2a 100644 --- a/yaboli/bot.py +++ b/yaboli/bot.py @@ -1,600 +1,40 @@ -import time +# PLACEHOLDER BOT CLASS -from . import callbacks -from . import exceptions -from . import room - -class Bot(): - """ - Empty bot class that can be built upon. - Takes care of extended botrulez. - """ +class Bot: + def __init__(self, name, room, pw=None, creator=None, create_room=None, create_time=None): + self.name = name + self.room = room + self.pw = pw + self.creator = creator + self.create_room = create_room + self.create_time = create_time - 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 [ [ --pw= ] ]\n" - " : 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 | ]\n" - "-s : general syntax help\n" - "-c : only list the commands\n" - " : 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 [ --pw= ]\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 run(self, bot_id): + pass def stop(self): - """ - stop() -> None - - Kill this bot. - """ - - self.room.stop() + pass - 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 get_name(self): + return self.name - 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" - "!{} @{} [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 get_roomname(self): + return self.room - def roomname(self): - """ - roomname() -> roomname - - The room the bot is connected to. - """ - - return self.room.room + def get_roompw(self): + return self.pw - def password(self): - """ - password() -> password - - The current room's password. - """ - - return self.room.password + def get_creator(self): + return self.creator - def nick(self): - """ - nick() -> nick - - The bot's full nick. - """ - - return self.room.nick + def get_create_room(self): + return self.create_room - def mentionable(self): - """ - mentionable() -> nick - - The bot's nick in a mentionable format. - """ - - return self.room.mentionable() + def get_create_time(self): + return self.create_time - 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 save(self): + return [1, 2, 3] - 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: - [- ][d ][h ][m ]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} \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) + def load(self, data): + pass diff --git a/yaboli/botmanager.py b/yaboli/botmanager.py index 1a91b91..e20ef32 100644 --- a/yaboli/botmanager.py +++ b/yaboli/botmanager.py @@ -1,149 +1,160 @@ import json +import logging +logger = logging.getLogger(__name__) -from . import bot -from . import exceptions - -class BotManager(): +class BotManager: """ Keep track of multiple bots in different rooms. + Save and load bots from a file. """ - 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 + def __init__(self, bot_class, bot_limit=None): """ + """ 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() + self.bot_limit = bot_limit + self.bot_id_counter = 0 # no two bots can have the same id + self.bots = {} # each bot has an unique id - def create(self, room, password=None, nick=None, created_in=None, created_by=None): + def create(self, name, room, pw=None, creator=None, create_room=None, create_time=None): """ - create(room, password, nick) -> bot + create(name, room, pw, creator, create_room, create_time) -> bot - Create a new bot in room. + Create a bot of type self.bot_class. + Starts the bot and returns it. """ - if nick is None: - nick = self.default_nick + bot_id = self.bot_id_counter + self.bot_id_counter += 1 - 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 + bot = self.bot_class(name=name, room=room, pw=pw, creator=creator, + create_room=create_room, create_time=create_time) + + self.bots[bot_id] = bot + bot.run(self) + + logger.info("Created {}: {} in room {}".format(bot_id, name, room)) + return bot def remove(self, bot_id): """ remove(bot_id) -> None - Kill a bot and remove it from the list of bots. + Remove a bot from the manager and stop it. """ - if bot_id in self._bots: - self._bots[bot_id].stop() - self._bots.pop(bot_id) - - self._save_bots() + bot = self.get(bot_id) + if not bot: return + + # for logging purposes + name = bot.get_name() + room = bot.get_roomname() + + bot.stop() + del self.bots[bot_id] + + logger.info("Removed {}: {} in room {}".format(bot_id, name, room)) def get(self, bot_id): """ - get(self, bot_id) -> bot + get(bot_id) -> bot - Return bot with that id, if found. + Get a bot by its id. + Returns None if no bot was found. """ - if bot_id in self._bots: - return self._bots[bot_id] + return self.bots.get(bot_id) def get_id(self, bot): """ get_id(bot) -> bot_id - Return the bot id, if the bot is known. + Get a bot's id. + Returns None if id not found. """ - for bot_id, own_bot in self._bots.items(): - if bot == own_bot: + for bot_id, lbot in self.bots.items(): + if lbot == bot: return bot_id - def get_similar(self, room, nick): + def similar(self, roomname, mention): """ - get_by_room(room, nick) -> dict + in_room(roomname, mention) -> [bot_id] - Collect all bots that are connected to the room and have that nick. + Get all bots that are connected to a room and match the mention. + The bot ids are sorted from small to big. """ - return {bot_id: bot for bot_id, bot in self._bots.items() - if bot.roomname() == room and bot.mentionable().lower() == nick.lower()} + l = [] + + for bot_id, bot in sorted(self.bots.items()): + if bot.get_roomname() == roomname and mention.equals(bot.get_name()): + l.append(bot_id) + + return l - def _load_bots(self): + def save(self, filename): """ - _load_bots() -> None + save(filename) -> None - Load and create bots from self.bots_file. + Save all current bots to a 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 + logger.info("Saving bots to {}".format(filename)) bots = [] + for bot in self.bots.values(): + bots.append({ + "name": bot.get_name(), + "room": bot.get_roomname(), + "pw": bot.get_roompw(), + "creator": bot.get_creator(), + "create_room": bot.get_create_room(), + "create_time": bot.get_create_time(), + "data": bot.save() + }) + logger.debug("Bot info: {}".format(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) + logger.debug("Writing to file") + with open(filename, "w") as f: + json.dump(bots, f, sort_keys=True, indent=4) - with open(self.bots_file, "w") as f: - json.dump(bots, f) + logger.info("Saved bots.") + + def load(self, filename): + """ + load(filename) -> None + + Load bots from a file. + Creates the bots and starts them. + """ + + logger.info("Loading bots from {}".format(filename)) + + try: + logger.debug("Reading file") + with open(filename) as f: + bots = json.load(f) + + except FileNotFoundError: + logger.warning("File {} not found.".format(filename)) + + else: + logger.debug("Bot info: {}".format(bots)) + for bot_info in bots: + self.create(bot_info["name"], bot_info["room"], bot_info["pw"], bot_info["creator"], + bot_info["create_room"], bot_info["create_time"]).load(bot_info["data"]) + + logger.info("Loaded bots.") + + def interactive(self): + """ + interactive() -> None + + Start interactive mode that allows you to manage bots using commands. + Command list: + [NYI] + """ + + pass diff --git a/yaboli/callbacks.py b/yaboli/callbacks.py deleted file mode 100644 index 04c36b8..0000000 --- a/yaboli/callbacks.py +++ /dev/null @@ -1,62 +0,0 @@ -class Callbacks(): - """ - Manage callbacks - """ - - def __init__(self): - self._callbacks = {} - - def add(self, event, callback, *args, **kwargs): - """ - add(event, callback, *args, **kwargs) -> None - - Add a function to be called on event. - The function will be called with *args and **kwargs. - Certain arguments might be added, depending on the event. - """ - - if not event in self._callbacks: - self._callbacks[event] = [] - - callback_info = { - "callback": callback, - "args": args, - "kwargs": kwargs - } - - self._callbacks[event].append(callback_info) - - def remove(self, event): - """ - remove(event) -> None - - Remove all callbacks attached to that event. - """ - - if event in self._callbacks: - del self._callbacks[event] - - def call(self, event, *args): - """ - call(event) -> None - - Call all callbacks subscribed to the event with *args and the arguments specified when the - callback was added. - """ - - if event in self._callbacks: - for c_info in self._callbacks[event]: - c = c_info["callback"] - args = c_info["args"] + args - kwargs = c_info["kwargs"] - - c(*args, **kwargs) - - def exists(self, event): - """ - exists(event) -> bool - - Are any functions subscribed to this event? - """ - - return event in self._callbacks diff --git a/yaboli/connection.py b/yaboli/connection.py deleted file mode 100644 index 57731b8..0000000 --- a/yaboli/connection.py +++ /dev/null @@ -1,229 +0,0 @@ -import json -import time -import threading -import websocket -from websocket import WebSocketException as WSException - -from . import callbacks - -class Connection(): - """ - Stays connected to a room in its own thread. - Callback functions are called when a packet is received. - - Callbacks: - - all the message types from api.euphoria.io - These pass the packet data as argument to the called functions. - The other callbacks don't pass any special arguments. - - "connect" - - "disconnect" - - "stop" - """ - - ROOM_FORMAT = "wss://euphoria.io/room/{}/ws" - - def __init__(self, room, url_format=None): - """ - room - name of the room to connect to - - """ - - self.room = room - - if not url_format: - url_format = self.ROOM_FORMAT - self._url = url_format.format(self.room) - - self._stopping = False - - self._ws = None - self._thread = None - self._send_id = 0 - self._callbacks = callbacks.Callbacks() - self._id_callbacks = callbacks.Callbacks() - - def _connect(self, tries=-1, delay=10): - """ - _connect(tries, delay) -> bool - - tries - maximum number of retries - -1 -> retry indefinitely - - Returns True on success, False on failure. - - Connect to the room. - """ - - while tries != 0: - try: - self._ws = websocket.create_connection( - self._url, - enable_multithread=True - ) - - self._callbacks.call("connect") - - return True - except WSException: - if tries > 0: - tries -= 1 - if tries != 0: - time.sleep(delay) - return False - - def disconnect(self): - """ - disconnect() -> None - - Reconnect to the room. - WARNING: To completely disconnect, use stop(). - """ - - if self._ws: - self._ws.close() - self._ws = None - - self._callbacks.call("disconnect") - - def launch(self): - """ - launch() -> Thread - - Connect to the room and spawn a new thread running run. - """ - - if self._connect(tries=1): - self._thread = threading.Thread(target=self._run, - name="{}-{}".format(self.room, int(time.time()))) - self._thread.start() - return self._thread - else: - self.stop() - - def _run(self): - """ - _run() -> None - - Receive messages. - """ - - while not self._stopping: - try: - self._handle_json(self._ws.recv()) - except (WSException, ConnectionResetError): - if not self._stopping: - self.disconnect() - self._connect() - - def stop(self): - """ - stop() -> None - - Close the connection to the room. - Joins the thread launched by self.launch(). - """ - - self._stopping = True - self.disconnect() - - self._callbacks.call("stop") - - if self._thread and self._thread != threading.current_thread(): - self._thread.join() - - def next_id(self): - """ - next_id() -> id - - Returns the id that will be used for the next package. - """ - - return str(self._send_id) - - def add_callback(self, ptype, callback, *args, **kwargs): - """ - add_callback(ptype, callback, *args, **kwargs) -> None - - Add a function to be called when a packet of type ptype is received. - """ - - self._callbacks.add(ptype, callback, *args, **kwargs) - - def add_id_callback(self, pid, callback, *args, **kwargs): - """ - add_id_callback(pid, callback, *args, **kwargs) -> None - - Add a function to be called when a packet with id pid is received. - """ - - self._id_callbacks.add(pid, callback, *args, **kwargs) - - def add_next_callback(self, callback, *args, **kwargs): - """ - add_next_callback(callback, *args, **kwargs) -> None - - Add a function to be called when the answer to the next message sent is received. - """ - - self._id_callbacks.add(self.next_id(), callback, *args, **kwargs) - - def _handle_json(self, data): - """ - handle_json(data) -> None - - Handle incoming 'raw' data. - """ - - packet = json.loads(data) - self._handle_packet(packet) - - def _handle_packet(self, packet): - """ - _handle_packet(ptype, data) -> None - - Handle incoming packets - """ - - if "data" in packet: - data = packet["data"] - else: - data = None - - if "error" in packet: - error = packet["error"] - else: - error = None - - self._callbacks.call(packet["type"], data, error) - - if "id" in packet: - self._id_callbacks.call(packet["id"], data, error) - self._id_callbacks.remove(packet["id"]) - - def _send_json(self, data): - """ - _send_json(data) -> None - - Send 'raw' json. - """ - - if self._ws: - try: - self._ws.send(json.dumps(data)) - except WSException: - self.disconnect() - - def send_packet(self, ptype, **kwargs): - """ - send_packet(ptype, **kwargs) -> None - - Send a formatted packet. - """ - - packet = { - "type": ptype, - "data": kwargs or None, - "id": str(self._send_id) - } - self._send_id += 1 - self._send_json(packet) diff --git a/yaboli/exceptions.py b/yaboli/exceptions.py deleted file mode 100644 index f48993d..0000000 --- a/yaboli/exceptions.py +++ /dev/null @@ -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 diff --git a/yaboli/message.py b/yaboli/message.py deleted file mode 100644 index 7e64bc5..0000000 --- a/yaboli/message.py +++ /dev/null @@ -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 diff --git a/yaboli/messages.py b/yaboli/messages.py deleted file mode 100644 index 8382a69..0000000 --- a/yaboli/messages.py +++ /dev/null @@ -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 diff --git a/yaboli/room.py b/yaboli/room.py deleted file mode 100644 index 360e0e4..0000000 --- a/yaboli/room.py +++ /dev/null @@ -1,617 +0,0 @@ -import time - -from . import connection -from . import message -from . import messages -from . import session -from . import sessions -from . import callbacks - -class Room(): - """ - Connects to and provides more abstract access to a room on euphoria. - - callback (values passed) - description - ---------------------------------------------------------------------------------- - delete (message) - message has been deleted - edit (message) - message has been edited - identity - own session or nick has changed - join (session) - user has joined the room - message (message) - message has been sent - messages - message data has changed - nick (session, old, new) - user has changed their nick - part (session) - user has left the room - ping - ping event has happened - room - room info has changed - sessions - session data has changed - change - room has been changed - """ - - def __init__(self, room=None, nick=None, password=None, message_limit=500): - """ - room - name of the room to connect to - nick - nick to assume, None -> no nick - password - room password (in case the room is private) - message_limit - maximum amount of messages that will be stored at a time - None - no limit - """ - - self.room = room - self.password = password - self.room_is_private = None - self.pm_with_nick = None - self.pm_with_user = None - - self.nick = nick - self.session = None - self.message_limit = message_limit - - self.ping_last = 0 - self.ping_next = 0 - self.ping_offset = 0 # difference between server and local time - - self._messages = None - self._sessions = None - - self._callbacks = callbacks.Callbacks() - - self._con = None - - if self.room: - self.change(self.room, password=self.password) - - def launch(self): - """ - launch() -> Thread - - Open connection in a new thread (see connection.Connection.launch). - """ - - return self._con.launch() - - def stop(self): - """ - stop() -> None - - Close connection to room. - """ - - self._con.stop() - - def change(self, room, password=None): - """ - change(room) -> None - - Leave current room (if already connected) and join new room. - Clears all messages and sessions. - A call to launch() is necessary to start a new thread again. - """ - - if self._con: - self._con.stop() - - self.room = room - self.password = password - self.room_is_private = None - self.pm_with_nick = None - self.pm_with_user = None - - self.session = None - - self.ping_last = 0 - self.ping_next = 0 - self.ping_offset = 0 # difference between server and local time - - self._messages = messages.Messages(message_limit=self.message_limit) - self._sessions = sessions.Sessions() - - self._con = connection.Connection(self.room) - - self._con.add_callback("bounce-event", self._handle_bounce_event) - self._con.add_callback("disconnect-event", self._handle_disconnect_event) - self._con.add_callback("hello-event", self._handle_hello_event) - self._con.add_callback("join-event", self._handle_join_event) - self._con.add_callback("network-event", self._handle_network_event) - self._con.add_callback("nick-event", self._handle_nick_event) - self._con.add_callback("edit-message-event", self._handle_edit_message_event) - self._con.add_callback("part-event", self._handle_part_event) - self._con.add_callback("ping-event", self._handle_ping_event) - self._con.add_callback("send-event", self._handle_send_event) - self._con.add_callback("snapshot-event", self._handle_snapshot_event) - - self._callbacks.call("change") - - def add_callback(self, event, callback, *args, **kwargs): - """ - add_callback(ptype, callback, *args, **kwargs) -> None - - Add a function to be called when a certain event happens. - """ - - self._callbacks.add(event, callback, *args, **kwargs) - - def get_msg(self, mid): - """ - get_msg(message_id) -> Message - - Returns the message with the given id, if found. - """ - - return self._messages.get(mid) - - def get_msg_parent(self, mid): - """ - get_msg_parent(message_id) -> Message - - Returns the message's parent, if found. - """ - - return self._messages.get_parent(mid) - - def get_msg_children(self, mid): - """ - get_msg_children(message_id) -> list - - Returns a sorted list of children of the given message, if found. - """ - - return self._messages.get_children(mid) - - def get_msg_top_level(self): - """ - get_msg_top_level() -> list - - Returns a sorted list of top-level messages. - """ - - return self._messages.get_top_level() - - def get_msg_oldest(self): - """ - get_msg_oldest() -> Message - - Returns the oldest message, if found. - """ - - return self._messages.get_oldest() - - def get_msg_youngest(self): - """ - get_msg_youngest() -> Message - - Returns the youngest message, if found. - """ - - return self._messages.get_youngest() - - def get_session(self, sid): - """ - get_session(session_id) -> Session - - Returns the session with that id. - """ - - return self._sessions.get(sid) - - def get_sessions(self): - """ - get_sessions() -> list - - Returns the full list of sessions. - """ - - return self._sessions.get_all() - - def get_people(self): - """ - get_people() -> list - - Returns a list of all non-bot and non-lurker sessions. - """ - - return self._sessions.get_people() - - def get_accounts(self): - """ - get_accounts() -> list - - Returns a list of all logged-in sessions. - """ - - return self._sessions.get_accounts() - - 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._sessions.get_agents() - - def get_bots(self): - """ - get_bots() -> list - - Returns a list of all bot sessions. - """ - - return self._sessions.get_bots() - - def get_lurkers(self): - """ - get_lurkers() -> list - - Returns a list of all lurker sessions. - """ - - return self._sessions.get_lurkers() - - def set_nick(self, nick): - """ - set_nick(nick) -> None - - Change your nick. - """ - - self.nick = nick - - if not self.session or self.session.name != self.nick: - self._con.add_next_callback(self._handle_nick_reply) - self._con.send_packet("nick", name=nick) - - def mentionable(self, nick=None): - """ - mentionable() - - A mentionable version of the nick. - The nick defaults to the bot's nick. - """ - - if nick is None: - nick = self.nick - - return "".join(c for c in nick if not c in ".!?;&<'\"" and not c.isspace()) - - def send_message(self, content, parent=None): - """ - send_message(content, parent) -> None - - Send a message. - """ - - self._con.add_next_callback(self._handle_send_reply) - self._con.send_packet("send", content=content, parent=parent) - - def authenticate(self, password=None): - """ - authenticate(passsword) -> None - - Try to authenticate so you can enter the room. - """ - - self.password = password - - self._con.add_next_callback(self._handle_auth_reply) - self._con.send_packet("auth", type="passcode", passcode=self.password) - - def update_sessions(self): - """ - update_sessions() -> None - - Resets and then updates the list of sessions. - """ - - self._con.add_next_callback(self._handle_who_reply) - self._con.send_packet("who") - - def load_msgs(self, number=50): - """ - load_msgs(number) -> None - - Request a certain number of older messages from the server. - """ - - self._con.add_next_callback(self._handle_log_reply) - self._con.send_packet("log", n=number, before=self.get_msg_oldest().id) - - def load_msg(self, mid): - """ - load_msg(message_id) -> None - - Request an untruncated version of the message with that id. - """ - - self._con.add_next_callback(self._handle_get_message_reply) - self._con.send_packet("get-message", id=mid) - - # ----- HANDLING OF EVENTS ----- - - def _handle_connect(self): - """ - TODO - """ - - self._callbacks.call("connect") - - def _handle_disconnect(self): - """ - TODO - """ - - self._callbacks.call("disconnect") - - def _handle_stop(self): - """ - TODO - """ - - self._callbacks.call("stop") - - def _handle_bounce_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "bounce-event", error) - self.stop() - return - - if self.password is not None: - self.authenticate(self.password) - else: - self.stop() - - def _handle_disconnect_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "disconnect-event", error) - self.stop() - return - - self._con.disconnect() - - def _handle_hello_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "hello-event", error) - self.stop() - return - - self.session = session.Session.from_data(data["session"]) - self._sessions.add(self.session) - self._callbacks.call("identity") - self._callbacks.call("sessions") - - self.room_is_private = data["room_is_private"] - self._callbacks.call("room") - - def _handle_join_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "join-event", error) - self.update_sessions() - return - - ses = session.Session.from_data(data) - self._sessions.add(ses) - self._callbacks.call("join", ses) - self._callbacks.call("sessions") - - def _handle_network_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "network-event", error) - return - - if data["type"] == "partition": - self._sessions.remove_on_network_partition(data["server_id"], data["server_era"]) - self._callbacks.call("sessions") - - def _handle_nick_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "nick-event", error) - self.update_sessions() - return - - ses = self.get_session(data["session_id"]) - if ses: - ses.name = data["to"] - self._callbacks.call("nick", ses, data["from"], data["to"]) - self._callbacks.call("sessions") - - def _handle_edit_message_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "edit-message-event", error) - return - - msg = message.Message.from_data(data) - if msg: - self._messages.add(msg) - - if msg.deleted: - self._callbacks.call("delete", msg) - elif msg.edited: - self._callbacks.call("edit", msg) - - self._callbacks.call("messages") - - def _handle_part_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "part-event", error) - self.update_sessions() - return - - ses = session.Session.from_data(data) - if ses: - self._sessions.remove(ses.session_id) - - self._callbacks.call("part", ses) - self._callbacks.call("sessions") - - def _handle_ping_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "ping-event", error) - return - - self.ping_last = data["time"] - self.ping_next = data["next"] - self.ping_offset = self.ping_last - time.time() - - self._con.send_packet("ping-reply", time=self.ping_last) - self._callbacks.call("ping") - - def _handle_send_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "send-event", error) - return - - msg = message.Message.from_data(data) - self._callbacks.call("message", msg) - - self._messages.add(msg) - self._callbacks.call("messages") - - def _handle_snapshot_event(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "snapshot-event", error) - self.stop() - return - - self.set_nick(self.nick) - - if "pm_with_nick" in data or "pm_with_user_id" in data: - if "pm_with_nick" in data: - self.pm_with_nick = data["pm_with_nick"] - if "pm_with_user_id" in data: - self.pm_with_user_id = data["pm_with_user_id"] - self._callbacks.call("room") - - self._sessions.remove_all() - for sesdata in data["listing"]: - self._sessions.add_from_data(sesdata) - self._callbacks.call("sessions") - - self._messages.remove_all() - for msgdata in data["log"]: - self._messages.add_from_data(msgdata) - self._callbacks.call("messages") - - # ----- HANDLING OF REPLIES ----- - - def _handle_auth_reply(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "auth-reply", error) - self.stop() - return - - if not data["success"]: - self._con.stop() - - def _handle_get_message_reply(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "get-message-reply", error) - return - - self._messages.add_from_data(data) - self._callbacks.call("messages") - - def _handle_log_reply(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "log-reply", error) - return - - for msgdata in data["log"]: - self._messages.add_from_data(msgdata) - self._callbacks.call("messages") - - def _handle_nick_reply(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "nick-reply", error) - return - - if "to" in data: - self.session.name = self.nick - self._callbacks.call("identity") - - if data["to"] != self.nick: - self.set_nick(self.nick) - - def _handle_send_reply(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "send-reply", error) - return - - self._messages.add_from_data(data) - self._callbacks.call("messages") - - def _handle_who_reply(self, data, error): - """ - TODO - """ - - if error: - self._callbacks.call("error", "who-reply", error) - return - - self._sessions.remove_all() - for sesdata in data["listing"]: - self._sessions.add_from_data(sesdata) - self._callbacks.call("sessions") diff --git a/yaboli/session.py b/yaboli/session.py deleted file mode 100644 index e649655..0000000 --- a/yaboli/session.py +++ /dev/null @@ -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 diff --git a/yaboli/sessions.py b/yaboli/sessions.py deleted file mode 100644 index b00cf75..0000000 --- a/yaboli/sessions.py +++ /dev/null @@ -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] From 8b1a10c396576bc17900687adb50321a90cd25f2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 20 Sep 2016 20:46:41 +0000 Subject: [PATCH 02/10] Use exceptions and mentions --- yaboli/botmanager.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/yaboli/botmanager.py b/yaboli/botmanager.py index e20ef32..06030ee 100644 --- a/yaboli/botmanager.py +++ b/yaboli/botmanager.py @@ -1,7 +1,11 @@ import json +import time import logging logger = logging.getLogger(__name__) +from .exceptions import CreateBotException +from .mention import Mention + class BotManager: """ Keep track of multiple bots in different rooms. @@ -17,24 +21,30 @@ class BotManager: self.bot_id_counter = 0 # no two bots can have the same id self.bots = {} # each bot has an unique id - def create(self, name, room, pw=None, creator=None, create_room=None, create_time=None): + def create(self, name, roomname, pw=None, creator=None, create_room=None, create_time=None): """ - create(name, room, pw, creator, create_room, create_time) -> bot + create(name, roomname, pw, creator, create_room, create_time) -> bot Create a bot of type self.bot_class. Starts the bot and returns it. """ + if self.bot_limit and len(self.bots) >= self.bot_limit: + raise CreateBotException("Bot limit hit ({} bots)".format(self.bot_limit)) + bot_id = self.bot_id_counter self.bot_id_counter += 1 - bot = self.bot_class(name=name, room=room, pw=pw, creator=creator, - create_room=create_room, create_time=create_time) + if create_time is None: + create_time = time.time() + + bot = self.bot_class(name, roomname, pw=pw, creator=creator, create_room=create_room, + create_time=create_time, manager=self) self.bots[bot_id] = bot - bot.run(self) + bot.launch() - logger.info("Created {}: {} in room {}".format(bot_id, name, room)) + logger.info("Created {} - {} in room {}".format(bot_id, name, roomname)) return bot def remove(self, bot_id): @@ -49,12 +59,12 @@ class BotManager: # for logging purposes name = bot.get_name() - room = bot.get_roomname() + roomname = bot.get_roomname() bot.stop() del self.bots[bot_id] - logger.info("Removed {}: {} in room {}".format(bot_id, name, room)) + logger.info("Removed {} - {} in room {}".format(bot_id, name, roomname)) def get(self, bot_id): """ @@ -89,7 +99,7 @@ class BotManager: l = [] for bot_id, bot in sorted(self.bots.items()): - if bot.get_roomname() == roomname and mention.equals(bot.get_name()): + if bot.get_roomname() == roomname and mention == Mention(bot.get_name()): l.append(bot_id) return l @@ -143,8 +153,12 @@ class BotManager: else: logger.debug("Bot info: {}".format(bots)) for bot_info in bots: - self.create(bot_info["name"], bot_info["room"], bot_info["pw"], bot_info["creator"], - bot_info["create_room"], bot_info["create_time"]).load(bot_info["data"]) + try: + self.create(bot_info["name"], bot_info["room"], bot_info["pw"], + bot_info["creator"], bot_info["create_room"], + bot_info["create_time"]).load(bot_info["data"]) + except CreateBotException as err: + logger.warning("Creating bot failed: {}.".format(err)) logger.info("Loaded bots.") From cc5a342f91d79aa2c34b096a65c40799b713cf20 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 20 Sep 2016 20:47:40 +0000 Subject: [PATCH 03/10] Add documentation --- yaboli/botmanager.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/yaboli/botmanager.py b/yaboli/botmanager.py index 06030ee..443761e 100644 --- a/yaboli/botmanager.py +++ b/yaboli/botmanager.py @@ -10,12 +10,22 @@ class BotManager: """ Keep track of multiple bots in different rooms. Save and load bots from a file. + + If you've created a bot from the Bot class, you can easily host it by adding: + + if __name__ == "__main__": + manager = BotManager(YourBotClass, bot_limit=10) + manager.interactive() + + to your file and then executing it. """ def __init__(self, bot_class, bot_limit=None): """ - + bot_class - bot class you want to run + bot_limit - maximum amount of bots to exist simultaneously """ + self.bot_class = bot_class self.bot_limit = bot_limit self.bot_id_counter = 0 # no two bots can have the same id From 19de0177d58bb02f7f14657c2e2cfc8423dfe38d Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 20 Sep 2016 20:49:20 +0000 Subject: [PATCH 04/10] Add exceptions --- yaboli/exceptions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 yaboli/exceptions.py diff --git a/yaboli/exceptions.py b/yaboli/exceptions.py new file mode 100644 index 0000000..3564d6b --- /dev/null +++ b/yaboli/exceptions.py @@ -0,0 +1,11 @@ +class YaboliException(Exception): + """ + Generic yaboli exception class. + """ + pass + +class CreateBotException(YaboliException): + """ + Raised when a bot could not be created. + """ + pass From 9082d0a40411a85856cc2ea6e37830d5eb61eec3 Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 20 Sep 2016 20:49:33 +0000 Subject: [PATCH 05/10] Add mention --- yaboli/mention.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 yaboli/mention.py diff --git a/yaboli/mention.py b/yaboli/mention.py new file mode 100644 index 0000000..eda86ff --- /dev/null +++ b/yaboli/mention.py @@ -0,0 +1,43 @@ +class Mention(str): + """ + A class to compare @mentions to nicks and other @mentions + """ + + def mentionable(nick): + """ + A mentionable version of the nick. + Add an "@" in front to mention someone on euphoria. + """ + + # return "".join(c for c in nick if not c in ".!?;&<'\"" and not c.isspace()) + return "".join(filter(lambda c: c not in ".!?;&<'\"" and not c.isspace(), nick)) + + def __new__(cls, nick): + return str.__new__(cls, Mention.mentionable(nick)) + + def __add__(self, other): + return Mention(str(self) + other) + + def __mod__(self, other): + return Mention(str(self) % other) + + def __mul__(self, other): + return Mention(str(self)*other) + + def __repr__(self): + return "@" + super().__repr__() + + def __radd__(self, other): + return Mention(other + str(self)) + + def __rmul__(self, other): + return Mention(other*str(self)) + + def format(self, *args, **kwargs): + return Mention(str(self).format(*args, **kwargs)) + + def format_map(self, *args, **kwargs): + return Mention(str(self).format_map(*args, **kwargs)) + + def replace(self, *args, **kwargs): + return Mention(str(self).replace(*args, **kwargs)) From ef2d9eba50cf17799bad82855947da282a15700b Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 21 Sep 2016 16:35:37 +0000 Subject: [PATCH 06/10] Add callbacks --- yaboli/callbacks.py | 50 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 yaboli/callbacks.py diff --git a/yaboli/callbacks.py b/yaboli/callbacks.py new file mode 100644 index 0000000..f17f214 --- /dev/null +++ b/yaboli/callbacks.py @@ -0,0 +1,50 @@ +class Callbacks(): + """ + Manage callbacks + """ + + def __init__(self): + self._callbacks = {} + + def add(self, event, callback): + """ + add(event, callback) -> None + + Add a function to be called on event. + Certain arguments might be added on call, depending on the event. + """ + + if not event in self._callbacks: + self._callbacks[event] = [] + + self._callbacks[event].append(callback) + + def remove(self, event): + """ + remove(event) -> None + + Remove all callbacks added to that event. + """ + + if event in self._callbacks: + del self._callbacks[event] + + def call(self, event, *args, **kwargs): + """ + call(event) -> None + + Call all callbacks subscribed to the event with the arguments passed to this function. + """ + + if event in self._callbacks: + for c in self._callbacks: + c(*args, **kwargs) + + def exists(self, event): + """ + exists(event) -> bool + + Are any functions subscribed to this event? + """ + + return event in self._callbacks From a4b9e016b0c256ebe6bdbf71c4dfa9f0f488da76 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 21 Sep 2016 17:00:04 +0000 Subject: [PATCH 07/10] Refer to client nick as "nick", not "name" --- yaboli/botmanager.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/yaboli/botmanager.py b/yaboli/botmanager.py index 443761e..a0bea1f 100644 --- a/yaboli/botmanager.py +++ b/yaboli/botmanager.py @@ -31,9 +31,9 @@ class BotManager: self.bot_id_counter = 0 # no two bots can have the same id self.bots = {} # each bot has an unique id - def create(self, name, roomname, pw=None, creator=None, create_room=None, create_time=None): + def create(self, nick, roomname, pw=None, creator=None, create_room=None, create_time=None): """ - create(name, roomname, pw, creator, create_room, create_time) -> bot + create(nick, roomname, pw, creator, create_room, create_time) -> bot Create a bot of type self.bot_class. Starts the bot and returns it. @@ -48,13 +48,13 @@ class BotManager: if create_time is None: create_time = time.time() - bot = self.bot_class(name, roomname, pw=pw, creator=creator, create_room=create_room, + bot = self.bot_class(nick, roomname, pw=pw, creator=creator, create_room=create_room, create_time=create_time, manager=self) self.bots[bot_id] = bot bot.launch() - logger.info("Created {} - {} in room {}".format(bot_id, name, roomname)) + logger.info("Created {} - {} in room {}".format(bot_id, nick, roomname)) return bot def remove(self, bot_id): @@ -68,13 +68,13 @@ class BotManager: if not bot: return # for logging purposes - name = bot.get_name() + nick = bot.get_nick() roomname = bot.get_roomname() bot.stop() del self.bots[bot_id] - logger.info("Removed {} - {} in room {}".format(bot_id, name, roomname)) + logger.info("Removed {} - {} in room {}".format(bot_id, nick, roomname)) def get(self, bot_id): """ @@ -109,7 +109,7 @@ class BotManager: l = [] for bot_id, bot in sorted(self.bots.items()): - if bot.get_roomname() == roomname and mention == Mention(bot.get_name()): + if bot.get_roomname() == roomname and mention == Mention(bot.get_nick()): l.append(bot_id) return l @@ -126,7 +126,7 @@ class BotManager: bots = [] for bot in self.bots.values(): bots.append({ - "name": bot.get_name(), + "nick": bot.get_nick(), "room": bot.get_roomname(), "pw": bot.get_roompw(), "creator": bot.get_creator(), @@ -164,7 +164,7 @@ class BotManager: logger.debug("Bot info: {}".format(bots)) for bot_info in bots: try: - self.create(bot_info["name"], bot_info["room"], bot_info["pw"], + self.create(bot_info["nick"], bot_info["room"], bot_info["pw"], bot_info["creator"], bot_info["create_room"], bot_info["create_time"]).load(bot_info["data"]) except CreateBotException as err: From b4eacde6ca0ba6465dfb220ca2424bc19657a2e4 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 21 Sep 2016 17:03:13 +0000 Subject: [PATCH 08/10] Import existing modules --- yaboli/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yaboli/__init__.py b/yaboli/__init__.py index 846b148..0c32ba3 100644 --- a/yaboli/__init__.py +++ b/yaboli/__init__.py @@ -9,3 +9,6 @@ logger.addHandler(sh) from .bot import Bot from .botmanager import BotManager +from .callbacks import Callbacks +from .exceptions import YaboliException, CreateBotException +from .mention import Mention From 65cad2cdf33464ea4244da165e1abdc57d14b0d2 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 21 Sep 2016 18:51:47 +0000 Subject: [PATCH 09/10] Refer to password as "password", not "pw" --- yaboli/botmanager.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yaboli/botmanager.py b/yaboli/botmanager.py index a0bea1f..dfa61a2 100644 --- a/yaboli/botmanager.py +++ b/yaboli/botmanager.py @@ -31,9 +31,9 @@ class BotManager: self.bot_id_counter = 0 # no two bots can have the same id self.bots = {} # each bot has an unique id - def create(self, nick, roomname, pw=None, creator=None, create_room=None, create_time=None): + def create(self, nick, roomname, password=None, creator=None, create_room=None, create_time=None): """ - create(nick, roomname, pw, creator, create_room, create_time) -> bot + create(nick, roomname, password, creator, create_room, create_time) -> bot Create a bot of type self.bot_class. Starts the bot and returns it. @@ -48,7 +48,7 @@ class BotManager: if create_time is None: create_time = time.time() - bot = self.bot_class(nick, roomname, pw=pw, creator=creator, create_room=create_room, + bot = self.bot_class(nick, roomname, password=password, creator=creator, create_room=create_room, create_time=create_time, manager=self) self.bots[bot_id] = bot @@ -128,7 +128,7 @@ class BotManager: bots.append({ "nick": bot.get_nick(), "room": bot.get_roomname(), - "pw": bot.get_roompw(), + "password": bot.get_roompassword(), "creator": bot.get_creator(), "create_room": bot.get_create_room(), "create_time": bot.get_create_time(), @@ -164,7 +164,7 @@ class BotManager: logger.debug("Bot info: {}".format(bots)) for bot_info in bots: try: - self.create(bot_info["nick"], bot_info["room"], bot_info["pw"], + self.create(bot_info["nick"], bot_info["room"], bot_info["password"], bot_info["creator"], bot_info["create_room"], bot_info["create_time"]).load(bot_info["data"]) except CreateBotException as err: From 8f02d05b5a52104c729972490fc39f824d2733ba Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 21 Sep 2016 18:52:16 +0000 Subject: [PATCH 10/10] Add more functions --- yaboli/bot.py | 135 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 120 insertions(+), 15 deletions(-) diff --git a/yaboli/bot.py b/yaboli/bot.py index 8bf0f2a..aa504e0 100644 --- a/yaboli/bot.py +++ b/yaboli/bot.py @@ -1,28 +1,57 @@ -# PLACEHOLDER BOT CLASS +import time + +from . import callbacks +from . import room class Bot: - def __init__(self, name, room, pw=None, creator=None, create_room=None, create_time=None): - self.name = name - self.room = room - self.pw = pw - self.creator = creator + def __init__(self, nick, roomname, password=None, creator=None, create_room=None, + create_time=None, manager=None): + """ + nick - nick to assume, None -> no nick + roomname - name of the room to connect to + password - room password (in case the room is private) + creator - nick of the person the bot was created by + create_room - room the bot was created in + create_time - time/date the bot was created at (used when listing bots) + """ + + self.manager = manager + self.start_time = time.time() + + self.creator = creator self.create_room = create_room self.create_time = create_time + + + self.room = room.Room(nick, roomname, password=password) + #self.room.add_callback("message", self.on_message) + + # description used on general "!help", and in the !help text. (None - no reaction) + self.short_description = None + self.description = ("This bot complies with the botrulez™", + "(https://github.com/jedevc/botrulez),\n" + "plus a few extra commands.") + + self._commands = callbacks.Callbacks() + self._general_commands = [] # without @mention after the !nick + self._specific_commands = [] # need to choose certain bot if multiple are present + self._helptexts = {} + self._detailed_helptexts = {} - def run(self, bot_id): - pass + def launch(self): + self.room.launch() def stop(self): - pass + self.room.stop() - def get_name(self): - return self.name + def get_nick(self): + return self.room.nick def get_roomname(self): - return self.room + return self.room.roomname - def get_roompw(self): - return self.pw + def get_roompassword(self): + return self.room.password def get_creator(self): return self.creator @@ -33,8 +62,84 @@ class Bot: def get_create_time(self): return self.create_time + @staticmethod + def format_date(seconds, omit_date=False, omit_time=False): + """ + format_date(seconds) -> string + + Convert a date (Unix/POSIX/Epoch time) into a YYYY-MM-DD hh:mm:ss format. + """ + + f = "" + if not omit_date: + f += "%Y-%m-%d" + + if not omit_time: + if not omit_date: f += " " + f += "%H:%M:%S" + + return time.strftime(f, time.gmtime(seconds)) + + @staticmethod + def format_delta(seconds): + """ + format_delta(seconds) -> string + + Convert a time difference into the following format (where x is an integer): + [-] [[[xd ]xh ]xm ]xs + """ + + seconds = int(seconds) + delta = "" + + if seconds < 0: + delta += "- " + seconds = -seconds + + if seconds >= 24*60*60: + delta +="{}d ".format(seconds//(24*60*60)) + seconds %= 24*60*60 + + if seconds >= 60*60: + delta += "{}h ".format(seconds//(60*60)) + seconds %= 60*60 + + if seconds >= 60: + delta += "{}m ".format(seconds//60) + seconds %= 60 + + delta += "{}s".format(seconds) + + return delta + + def uptime(self): + """ + uptime() -> string + + The bot's uptime since it was last started, in the following format: + date time (delta) + """ + + date = self.format_date(self.start_time) + delta = self.format_delta(time.time() - self.start_time) + + return "{} ({})".format(date, delta) + def save(self): - return [1, 2, 3] + """ + Overwrite if your bot should save when BotManager is shut down. + Make sure to also overwrite load(). + + The data returned will be converted to json and back for using the json module, so + make sure your data can handle that (i.e. don't use numbers as dict keys etc.) + """ + + pass def load(self, data): + """ + Overwrite to load data from save(). + See save() for more details. + """ + pass