Add the ircstatus command, which allows an Evennia-ide user to ping the status of the IRC bot connection, get list of IRC nicks and also (if having the right privilege) force-reconnect the bot if the connection has died.

This commit is contained in:
Griatch 2017-02-12 00:43:19 +01:00
parent 1d181d8259
commit 65a4e507f7
4 changed files with 242 additions and 22 deletions

View file

@ -69,4 +69,5 @@ class PlayerCmdSet(CmdSet):
self.add(comms.CmdCdesc())
self.add(comms.CmdPage())
self.add(comms.CmdIRC2Chan())
self.add(comms.CmdIRCStatus())
self.add(comms.CmdRSS2Chan())

View file

@ -796,6 +796,26 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
self.msg("You paged %s with: '%s'." % (", ".join(received), message))
def _list_bots():
"""
Helper function to produce a list of all IRC bots.
Returns:
bots (str): A table of bots or an error message.
"""
ircbots = [bot for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")]
if ircbots:
from evennia.utils.evtable import EvTable
table = EvTable("{w#dbref{n", "{wbotname{n", "{wev-channel{n", "{wirc-channel{n", "{wSSL{n", maxwidth=_DEFAULT_WIDTH)
for ircbot in ircbots:
ircinfo = "%s (%s:%s)" % (ircbot.db.irc_channel, ircbot.db.irc_network, ircbot.db.irc_port)
table.add_row("#%i" % ircbot.id, ircbot.db.irc_botname, ircbot.db.ev_channel, ircinfo, ircbot.db.irc_ssl)
return table
else:
return "No irc bots found."
return
class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
"""
link an evennia channel to an external IRC channel
@ -841,19 +861,9 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
if 'list' in self.switches:
# show all connections
ircbots = [bot for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")]
if ircbots:
from evennia.utils.evtable import EvTable
table = EvTable("{wdbid{n", "{wbotname{n", "{wev-channel{n", "{wirc-channel{n", "{wSSL{n", maxwidth=_DEFAULT_WIDTH)
for ircbot in ircbots:
ircinfo = "%s (%s:%s)" % (ircbot.db.irc_channel, ircbot.db.irc_network, ircbot.db.irc_port)
table.add_row("#%i" % ircbot.id, ircbot.db.irc_botname, ircbot.db.ev_channel, ircinfo, ircbot.db.irc_ssl)
self.msg(table)
else:
self.msg("No irc bots found.")
self.msg(_list_bots())
return
if('disconnect' in self.switches or 'remove' in self.switches or
'delete' in self.switches):
botname = "ircbot-%s" % self.lhs
@ -911,6 +921,74 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
irc_network=irc_network, irc_port=irc_port, irc_ssl=irc_ssl)
self.msg("Connection created. Starting IRC bot.")
class CmdIRCStatus(COMMAND_DEFAULT_CLASS):
"""
Check and reboot IRC bot.
Usage:
ircstatus [#dbref ping||nicklist||reconnect]
If not given arguments, will return a list of all bots (like
@irc2chan/list). The 'ping' argument will ping the IRC network to
see if the connection is still responsive. The 'users' argument
will return a list of users on the remote IRC channel. Finally,
'reconnect' will force the client to disconnect and reconnect
again. This may be a last resort if the client has silently lost
connection (this may happen if the remote network experience
network issues). During the reconnection messages sent to either
channel will be lost.
"""
key = "@ircstatus"
locks = "cmd:serversetting(IRC_ENABLED) and perm(ircstatus) or perm(Builders))"
help_category = "Comms"
def func(self):
"Handles the functioning of the command."
if not self.args:
self.msg(_list_bots())
return
# should always be on the form botname option
args = self.args.split()
if len(args) != 2:
self.msg("Usage: @ircstatus [#dbref ping||nicklist||reconnect]")
return
botname, option = args
if option not in ("ping", "users", "reconnect", "nicklist"):
self.msg("Not a valid option.")
return
matches = None
if utils.dbref(botname):
matches = PlayerDB.objects.filter(db_is_bot=True, id=utils.dbref(botname))
else:
self.msg("No matching IRC-bot was found.")
return
ircbot = matches[0]
channel = ircbot.db.irc_channel
network = ircbot.db.irc_network
port = ircbot.db.irc_port
chtext = "IRC bot '%s' on channel %s (%s:%s)" % (ircbot.db.irc_botname, channel, network, port)
if option == "ping":
# check connection by sending outself a ping through the server.
self.caller.msg("Pinging through %s." % chtext)
ircbot.ping(self.caller)
elif option in ("users", "nicklist"):
# retrieve user list. The bot must handles the echo since it's
# an asynchronous call.
self.caller.msg("Requesting nicklist from %s (%s:%s)." % (channel, network, port))
ircbot.get_nicklist(self.caller)
elif self.caller.locks.check_lockstring(self.caller, "dummy:perm(ircstatus) or perm(Immortals)"):
# reboot the client
self.caller.msg("Forcing a disconnect + reconnect of %s." % chtext)
ircbot.reconnect()
else:
self.caller.msg("You don't have permission to force-reload the IRC bot.")
# RSS connection
class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
"""

View file

@ -194,6 +194,47 @@ class IRCBot(Bot):
"ssl": self.db.irc_ssl}
_SESSIONS.start_bot_session("evennia.server.portal.irc.IRCBotFactory", configdict)
def get_nicklist(self, caller):
"""
Retrive the nick list from the connected channel.
Args:
caller (Object or Player): The requester of the list. This will
be stored and echoed to when the irc network replies with the
requested info.
Notes: Since the return is asynchronous, the caller is stored internally
in a list; all callers in this list will get the nick info once it
returns (it is a custom OOB inputfunc option). The callback will not
survive a reload (which should be fine, it's very quick).
"""
if not hasattr(self, "_nicklist_callers"):
self._nicklist_callers = []
self._nicklist_callers.append(caller)
super(IRCBot, self).msg(request_nicklist="")
return
def ping(self, caller):
"""
Fire a ping to the IRC server.
Args:
caller (Object or Player): The requester of the ping.
"""
if not hasattr(self, "_ping_callers"):
self._ping_callers = []
self._ping_callers.append(caller)
super(IRCBot, self).msg(ping="")
def reconnect(self):
"""
Force a protocol-side reconnect of the client without
having to destroy/recreate the bot "player".
"""
super(IRCBot, self).msg(reconnect="")
def msg(self, text=None, **kwargs):
"""
Takes text from connected channel (only).
@ -204,7 +245,7 @@ class IRCBot(Bot):
Kwargs:
options (dict): Options dict with the following allowed keys:
- from_channel (str): dbid of a channel this text originated from.
- from_obj (str): dbid of an object sending this text.
- from_obj (list): list of objects this text.
"""
from_obj = kwargs.get("from_obj", None)
@ -224,17 +265,43 @@ class IRCBot(Bot):
Args:
session (Session, optional): Session responsible for this
command.
text (str, optional): Command string.
kwargs (dict, optional): Additional Information passed from bot.
Typically information is only passed by IRCbot including:
user (str): The name of the user who sent the message.
channel (str): The name of channel the message was sent to.
type (str): Nature of message. Either 'msg' or 'action'.
txt (str, optional): Command string.
Kwargs:
user (str): The name of the user who sent the message.
channel (str): The name of channel the message was sent to.
type (str): Nature of message. Either 'msg', 'action', 'nicklist' or 'ping'.
nicklist (list, optional): Set if `type='nicklist'`. This is a list of nicks returned by calling
the `self.get_nicklist`. It must look for a list `self._nicklist_callers`
which will contain all callers waiting for the nicklist.
timings (float, optional): Set if `type='ping'`. This is the return (in seconds) of a
ping request triggered with `self.ping`. The return must look for a list
`self._ping_callers` which will contain all callers waiting for the ping return.
"""
if kwargs["type"] == "action":
if kwargs["type"] == "nicklist":
# the return of a nicklist request
if hasattr(self, "_nicklist_callers") and self._nicklist_callers:
chstr = "%s (%s:%s)" % (self.db.irc_channel, self.db.irc_network, self.db.irc_port)
for obj in self._nicklist_callers:
obj.msg("Nicks at %s:\n %s" % (chstr, ", ".join(kwargs["nicklist"])))
self._nicklist_callers = []
return
elif kwargs["type"] == "ping":
# the return of a ping
if hasattr(self, "_ping_callers") and self._ping_callers:
chstr = "%s (%s:%s)" % (self.db.irc_channel, self.db.irc_network, self.db.irc_port)
for obj in self._ping_callers:
obj.msg("IRC ping return from %s took %ss." % (chstr, kwargs["timing"]))
self._ping_callers = []
return
elif kwargs["type"] == "action":
# An action (irc pose)
text = "%s@%s %s" % (kwargs["user"], kwargs["channel"], txt)
else:
# A normal channel message
text = "%s@%s: %s" % (kwargs["user"], kwargs["channel"], txt)
if not self.ndb.ev_channel and self.db.ev_channel:

View file

@ -134,6 +134,7 @@ class IRCBot(irc.IRCClient, Session):
logger = None
factory = None
channel = None
sourceURL = "http://code.evennia.com"
def signedOn(self):
"""
@ -194,6 +195,42 @@ class IRCBot(irc.IRCClient, Session):
user = user.split('!', 1)[0]
self.data_in(text=msg, type="action", user=user, channel=channel)
def get_nicklist(self):
"""
Retrieve name list from the channel. The return
is handled by the catch methods below.
"""
if not self.nicklist:
self.sendLine("NAMES %s" % self.channel)
def irc_RPL_NAMREPLY(self, prefix, params):
"Handles IRC NAME request returns (nicklist)"
channel = params[2].lower()
if channel != self.channel.lower():
return
self.nicklist += params[3].split(' ')
def irc_RPL_ENDOFNAMES(self, prefix, params):
"Called when the nicklist has finished being returned."
channel = params[1].lower()
if channel != self.channel.lower():
return
self.data_in(text="", type="nicklist", user="server", channel=channel, nicklist=self.nicklist)
self.nicklist = []
def pong(self, user, time):
"""
Called with the return timing from a PING.
Args:
user (str): Njame of user
time (float): Ping time in secs.
"""
self.data_in(text="", type="ping", user="server", channel=self.channel, timing=time)
def data_in(self, text=None, **kwargs):
"""
Data IRC -> Server.
@ -221,6 +258,28 @@ class IRCBot(irc.IRCClient, Session):
text = parse_irc_colors(text)
self.say(self.channel, text)
def send_request_nicklist(self, *args, **kwargs):
"""
Send a request for the channel nicklist. The return (handled
by `self.irc_RPL_ENDOFNAMES`) will be sent back as a message
with type `nicklist'.
"""
self.get_nicklist()
def send_ping(self, *args, **kwargs):
"""
Send a ping. The return (handled by `self.pong`) will be sent
back as a message of type 'ping'.
"""
self.ping(self.nickname)
def send_reconnect(self, *args, **kwargs):
"""
The server instructs us to rebuild the connection by force,
probably because the client silently lost connection.
"""
self.factory.reconnect()
def send_default(self, *args, **kwargs):
"""
Ignore other types of sends.
@ -231,7 +290,7 @@ class IRCBot(irc.IRCClient, Session):
class IRCBotFactory(protocol.ReconnectingClientFactory):
"""
Creates instances of AnnounceBot, connecting with a staggered
Creates instances of IRCBot, connecting with a staggered
increase in delay
"""
@ -264,6 +323,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
self.port = port
self.ssl = ssl
self.bot = None
self.nicklists = {}
def buildProtocol(self, addr):
"""
@ -280,6 +340,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
protocol.network = self.network
protocol.port = self.port
protocol.ssl = self.ssl
protocol.nicklist = []
return protocol
def startedConnecting(self, connector):
@ -312,9 +373,22 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
reason (str): The reason for the failure.
"""
if not self.bot or (self.bot and self.bot.stopping):
if not (self.bot or (self.bot and self.bot.stopping)):
self.retry(connector)
def reconnect(self):
"""
Force a reconnection of the bot protocol. This requires
de-registering the session and then reattaching a new one,
otherwise you end up with an ever growing number of bot
sessions.
"""
self.bot.stopping = True
self.bot.transport.loseConnection()
self.sessionhandler.server_disconnect(self.bot)
self.start()
def start(self):
"""
Connect session to sessionhandler.
@ -326,7 +400,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
from twisted.internet import ssl
service = reactor.connectSSL(self.network, int(self.port), self, ssl.ClientContextFactory())
except ImportError:
self.caller.msg("To use SSL, the PyOpenSSL module must be installed.")
logger.log_err("To use SSL, the PyOpenSSL module must be installed.")
else:
service = internet.TCPClient(self.network, int(self.port), self)
self.sessionhandler.portal.services.addService(service)