Add Bot and BotManager
This commit is contained in:
parent
145cbe5fe2
commit
c075287222
3 changed files with 477 additions and 0 deletions
|
|
@ -1,3 +1,5 @@
|
|||
from .bot import Bot
|
||||
from .botmanager import BotManager
|
||||
from .callbacks import Callbacks
|
||||
from .connection import Connection
|
||||
from .exceptions import *
|
||||
|
|
|
|||
390
yaboli/bot.py
Normal file
390
yaboli/bot.py
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
from . import callbacks
|
||||
import time
|
||||
|
||||
from . import room
|
||||
from . import exceptions
|
||||
|
||||
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):
|
||||
"""
|
||||
roomname - name of the room to connect to
|
||||
nick - nick to assume, None -> no nick
|
||||
password - room password (in case the room is private)
|
||||
"""
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
self.created_by = None
|
||||
self.created_in = None
|
||||
|
||||
self.manager = manager
|
||||
|
||||
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.commands.add("create", create_command)
|
||||
#self.commands.add("kill", kill_command)
|
||||
#self.commands.add("send", send_command)
|
||||
#self.commands.add("uptime", uptime_command)
|
||||
|
||||
self.add_command("help", self.help_command, "Shows help information about the bot.",
|
||||
("!help @bot [ -s | <command> ]\n"
|
||||
"-s : general syntax help\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("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("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):
|
||||
"""
|
||||
add_command(command, function, helptext, detailed_helptext) -> 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.
|
||||
|
||||
You can "hide" commands by specifying only the detailed helptext,
|
||||
or no helptext at all.
|
||||
"""
|
||||
|
||||
command = command.lower()
|
||||
|
||||
self.commands.add(command, function)
|
||||
|
||||
if helptext:
|
||||
self.helptexts[command] = helptext
|
||||
|
||||
if detailed_helptext:
|
||||
self.detailed_helptexts[command] = detailed_helptext
|
||||
|
||||
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 name are in the same room.
|
||||
"""
|
||||
|
||||
try:
|
||||
command, bot_id, name, arguments, flags, options = self.parse(message.content)
|
||||
except exceptions.ParseMessageException:
|
||||
return
|
||||
|
||||
if not self.commands.exists(command):
|
||||
return
|
||||
|
||||
if not name == self.room.session.mentionable():
|
||||
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.get_room(), name)
|
||||
if self.manager.get_id(self) == min(bots):
|
||||
if len(bots) > 1:
|
||||
msg = ("There are multiple bots with that name in this room. To select one,\n"
|
||||
"please specify its id (from the list below) as follows:\n"
|
||||
"!{} <id> @{} [your arguments...]\n").format(command, name)
|
||||
|
||||
print("constructing table")
|
||||
for bot_id in sorted(bots):
|
||||
bot = bots[bot_id]
|
||||
msg += "\n{} - @{} ({})".format(bot_id, bot.get_nick(), bot.creation_info())
|
||||
|
||||
print("sending help message")
|
||||
self.room.send_message(msg, parent=message.id)
|
||||
|
||||
else: # name is unique
|
||||
self.commands.call(command, message, arguments, flags, options)
|
||||
|
||||
def get_room(self):
|
||||
"""
|
||||
get_room() -> roomname
|
||||
|
||||
The room the bot is connected to.
|
||||
"""
|
||||
|
||||
return self.room.room
|
||||
|
||||
def get_mentionable_nick(self):
|
||||
"""
|
||||
get_mentionable_nick() -> nick
|
||||
|
||||
The The bot's nick in a mentionable format.
|
||||
"""
|
||||
|
||||
if self.room.session:
|
||||
return self.room.session.mentionable()
|
||||
|
||||
def get_nick(self):
|
||||
"""
|
||||
get_nick() -> nick
|
||||
|
||||
The bot's nick.
|
||||
"""
|
||||
|
||||
if self.room.session:
|
||||
return self.room.session.name
|
||||
|
||||
def creation_info(self):
|
||||
"""
|
||||
creation_info() -> str
|
||||
|
||||
Formatted info about the bot's creation
|
||||
"""
|
||||
|
||||
ftime = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(self.start_time))
|
||||
info = "created at {}".format(ftime)
|
||||
|
||||
if self.created_by:
|
||||
info += " by @{}".format(self.created_by)
|
||||
|
||||
if self.created_in:
|
||||
info += " in &{}".format(self.created_in)
|
||||
|
||||
return info
|
||||
|
||||
def parse_command(self, message):
|
||||
"""
|
||||
parse_command(message_content) -> command, bot_id, name, argpart
|
||||
|
||||
Parse the "!command[ bot_id] @botname[ argpart]" part of a command.
|
||||
"""
|
||||
|
||||
print("PARSING - COMMAND")
|
||||
|
||||
# command name (!command)
|
||||
split = message.split(maxsplit=1)
|
||||
|
||||
if split[0][:1] != "!":
|
||||
raise exceptions.ParseMessageException("Not a command")
|
||||
elif not len(split) > 1:
|
||||
raise exceptions.ParseMessageException("No bot name")
|
||||
|
||||
command = split[0][1:].lower()
|
||||
message = split[1]
|
||||
split = message.split(maxsplit=1)
|
||||
|
||||
# bot id
|
||||
try:
|
||||
bot_id = int(split[0])
|
||||
except ValueError:
|
||||
bot_id = None
|
||||
else:
|
||||
if not len(split) > 1:
|
||||
raise exceptions.ParseMessageException("No bot name")
|
||||
|
||||
message = split[1]
|
||||
split = message.split(maxsplit=1)
|
||||
|
||||
# bot name (@mention)
|
||||
if split[0][:1] != "@":
|
||||
raise exceptions.ParseMessageException("No bot name")
|
||||
|
||||
name = split[0][1:].lower()
|
||||
|
||||
# arguments to the command
|
||||
if len(split) > 1:
|
||||
argpart = split[1]
|
||||
else:
|
||||
argpart = None
|
||||
|
||||
return command, bot_id, name, argpart
|
||||
|
||||
def parse_arguments(self, argstr):
|
||||
"""
|
||||
parse_arguments(argstr) -> arguments, flags, options
|
||||
|
||||
Parse the argument part of a command.
|
||||
"""
|
||||
|
||||
print("PARSING - ARGPART")
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
print("PARSING")
|
||||
|
||||
command, bot_id, name, argpart = self.parse_command(message)
|
||||
|
||||
if argpart:
|
||||
arguments, flags, options = self.parse_arguments(argpart)
|
||||
else:
|
||||
arguments = []
|
||||
flags = ""
|
||||
options = {}
|
||||
|
||||
return command, bot_id, name, 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 help_command(self, message, arguments, flags, options):
|
||||
"""
|
||||
help_command(message, *arguments, flags, options) -> None
|
||||
|
||||
Show help about the bot.
|
||||
"""
|
||||
|
||||
if "s" in flags: # detailed syntax help
|
||||
msg = "SYNTAX HELP PLACEHOLDER"
|
||||
|
||||
elif arguments: # detailed help for one command
|
||||
command = arguments[0]
|
||||
if command[:1] == "!":
|
||||
command = command[1:]
|
||||
|
||||
if command in self.detailed_helptexts:
|
||||
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]
|
||||
|
||||
else: # just list all commands
|
||||
msg = "This bot supports the following commands:\n"
|
||||
|
||||
for command in sorted(self.helptexts):
|
||||
helptext = self.helptexts[command]
|
||||
msg += "\n!{} - {}".format(command, helptext)
|
||||
|
||||
msg += ("\n\nFor detailed help on the command syntax, try:\n"
|
||||
"!help @{0} -s\n"
|
||||
"For detailed help on a command, try:\n"
|
||||
"!help {0} <command>").format(self.get_mentionable_nick())
|
||||
|
||||
self.room.send_message(msg, parent=message.id)
|
||||
|
||||
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 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)
|
||||
85
yaboli/botmanager.py
Normal file
85
yaboli/botmanager.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
self.bot_class = bot_class
|
||||
self.max_bots = max_bots
|
||||
self.default_nick = default_nick
|
||||
|
||||
self.bots = {}
|
||||
self.bot_id = 0
|
||||
|
||||
def create(self, room, password=None, nick=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)
|
||||
self.bots[self.bot_id] = bot
|
||||
self.bot_id += 1
|
||||
|
||||
return bot
|
||||
|
||||
def remove(self, bot_id):
|
||||
"""
|
||||
remove(bot_id) -> None
|
||||
|
||||
Kill a bot and remove it from the list of bots.
|
||||
"""
|
||||
|
||||
if not bot_id in self.bots:
|
||||
raise exceptions.BotNotFoundException("Bot not in bots list")
|
||||
|
||||
self.bots[bot_id].stop()
|
||||
del self.bots[bot_id]
|
||||
|
||||
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.get_room() == room and bot.get_mentionable_nick() == nick}
|
||||
Loading…
Add table
Add a link
Reference in a new issue