538 lines
14 KiB
Python
538 lines
14 KiB
Python
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):
|
|
"""
|
|
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
|
|
|
|
# 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.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."))
|
|
|
|
self.add_command("help", self.help_command, "Show 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("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 -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 ]\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):
|
|
"""
|
|
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.mentionable().lower():
|
|
return
|
|
|
|
name = self.room.mentionable(name).lower()
|
|
|
|
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(), name)
|
|
if self.manager.get_id(self) == min(bots): # only one bot should display the messages
|
|
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)
|
|
|
|
for bot_id in sorted(bots):
|
|
bot = bots[bot_id]
|
|
msg += "\n{} - @{} ({})".format(bot_id, bot.nick(), bot.creation_info())
|
|
|
|
self.room.send_message(msg, parent=message.id)
|
|
|
|
else: # name is unique
|
|
self.commands.call(command, message, arguments, flags, options)
|
|
|
|
def roomname(self):
|
|
"""
|
|
roomname() -> roomname
|
|
|
|
The room the bot is connected to.
|
|
"""
|
|
|
|
return self.room.room
|
|
|
|
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
|
|
"""
|
|
|
|
ftime = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(self.start_time))
|
|
info = "created {}".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 uptime(self):
|
|
"""
|
|
uptime() -> str
|
|
|
|
Formatted uptime
|
|
"""
|
|
|
|
delta = int(time.time() - self.start_time)
|
|
uptime = ""
|
|
|
|
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, name, argpart
|
|
|
|
Parse the "!command[ bot_id] @botname[ argpart]" part of a 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.
|
|
"""
|
|
|
|
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, 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 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)
|
|
except exceptions.CreateBotException:
|
|
self.room.send_message("Bot could not be cloned.", parent=message.id)
|
|
else:
|
|
bot.created_in = self.roomname()
|
|
bot.created_by = self.room.mentionable(message.sender.name)
|
|
|
|
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 = self.bot_description
|
|
msg += "\n\nThis bot supports the following commands:"
|
|
|
|
for command in sorted(self.helptexts):
|
|
helptext = self.helptexts[command]
|
|
msg += "\n!{} - {}".format(command, helptext)
|
|
|
|
msg += ("\n\nFor help on the command syntax, try: !help @{0} -s\n"
|
|
"For detailed help on a command, try: !help @{0} <command>")
|
|
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.
|
|
"""
|
|
|
|
msg = "uptime: {}".format(self.uptime())
|
|
|
|
if "i" in flags:
|
|
msg += "\nid: {}".format(self.manager.get_id(self))
|
|
msg += "\n{}".format(self.creation_info())
|
|
|
|
self.room.send_message(msg, message.id)
|