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:
parent
1d181d8259
commit
65a4e507f7
4 changed files with 242 additions and 22 deletions
|
|
@ -69,4 +69,5 @@ class PlayerCmdSet(CmdSet):
|
||||||
self.add(comms.CmdCdesc())
|
self.add(comms.CmdCdesc())
|
||||||
self.add(comms.CmdPage())
|
self.add(comms.CmdPage())
|
||||||
self.add(comms.CmdIRC2Chan())
|
self.add(comms.CmdIRC2Chan())
|
||||||
|
self.add(comms.CmdIRCStatus())
|
||||||
self.add(comms.CmdRSS2Chan())
|
self.add(comms.CmdRSS2Chan())
|
||||||
|
|
|
||||||
|
|
@ -796,6 +796,26 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
||||||
self.msg("You paged %s with: '%s'." % (", ".join(received), message))
|
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):
|
class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
link an evennia channel to an external IRC channel
|
link an evennia channel to an external IRC channel
|
||||||
|
|
@ -841,19 +861,9 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
if 'list' in self.switches:
|
if 'list' in self.switches:
|
||||||
# show all connections
|
# show all connections
|
||||||
ircbots = [bot for bot in PlayerDB.objects.filter(db_is_bot=True, username__startswith="ircbot-")]
|
self.msg(_list_bots())
|
||||||
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.")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
if('disconnect' in self.switches or 'remove' in self.switches or
|
if('disconnect' in self.switches or 'remove' in self.switches or
|
||||||
'delete' in self.switches):
|
'delete' in self.switches):
|
||||||
botname = "ircbot-%s" % self.lhs
|
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)
|
irc_network=irc_network, irc_port=irc_port, irc_ssl=irc_ssl)
|
||||||
self.msg("Connection created. Starting IRC bot.")
|
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
|
# RSS connection
|
||||||
class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
|
class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,47 @@ class IRCBot(Bot):
|
||||||
"ssl": self.db.irc_ssl}
|
"ssl": self.db.irc_ssl}
|
||||||
_SESSIONS.start_bot_session("evennia.server.portal.irc.IRCBotFactory", configdict)
|
_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):
|
def msg(self, text=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Takes text from connected channel (only).
|
Takes text from connected channel (only).
|
||||||
|
|
@ -204,7 +245,7 @@ class IRCBot(Bot):
|
||||||
Kwargs:
|
Kwargs:
|
||||||
options (dict): Options dict with the following allowed keys:
|
options (dict): Options dict with the following allowed keys:
|
||||||
- from_channel (str): dbid of a channel this text originated from.
|
- 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)
|
from_obj = kwargs.get("from_obj", None)
|
||||||
|
|
@ -224,17 +265,43 @@ class IRCBot(Bot):
|
||||||
Args:
|
Args:
|
||||||
session (Session, optional): Session responsible for this
|
session (Session, optional): Session responsible for this
|
||||||
command.
|
command.
|
||||||
text (str, optional): Command string.
|
txt (str, optional): Command string.
|
||||||
kwargs (dict, optional): Additional Information passed from bot.
|
Kwargs:
|
||||||
Typically information is only passed by IRCbot including:
|
user (str): The name of the user who sent the message.
|
||||||
user (str): The name of the user who sent the message.
|
channel (str): The name of channel the message was sent to.
|
||||||
channel (str): The name of channel the message was sent to.
|
type (str): Nature of message. Either 'msg', 'action', 'nicklist' or 'ping'.
|
||||||
type (str): Nature of message. Either 'msg' or 'action'.
|
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)
|
text = "%s@%s %s" % (kwargs["user"], kwargs["channel"], txt)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# A normal channel message
|
||||||
text = "%s@%s: %s" % (kwargs["user"], kwargs["channel"], txt)
|
text = "%s@%s: %s" % (kwargs["user"], kwargs["channel"], txt)
|
||||||
|
|
||||||
if not self.ndb.ev_channel and self.db.ev_channel:
|
if not self.ndb.ev_channel and self.db.ev_channel:
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ class IRCBot(irc.IRCClient, Session):
|
||||||
logger = None
|
logger = None
|
||||||
factory = None
|
factory = None
|
||||||
channel = None
|
channel = None
|
||||||
|
sourceURL = "http://code.evennia.com"
|
||||||
|
|
||||||
def signedOn(self):
|
def signedOn(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -194,6 +195,42 @@ class IRCBot(irc.IRCClient, Session):
|
||||||
user = user.split('!', 1)[0]
|
user = user.split('!', 1)[0]
|
||||||
self.data_in(text=msg, type="action", user=user, channel=channel)
|
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):
|
def data_in(self, text=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Data IRC -> Server.
|
Data IRC -> Server.
|
||||||
|
|
@ -221,6 +258,28 @@ class IRCBot(irc.IRCClient, Session):
|
||||||
text = parse_irc_colors(text)
|
text = parse_irc_colors(text)
|
||||||
self.say(self.channel, 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):
|
def send_default(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Ignore other types of sends.
|
Ignore other types of sends.
|
||||||
|
|
@ -231,7 +290,7 @@ class IRCBot(irc.IRCClient, Session):
|
||||||
|
|
||||||
class IRCBotFactory(protocol.ReconnectingClientFactory):
|
class IRCBotFactory(protocol.ReconnectingClientFactory):
|
||||||
"""
|
"""
|
||||||
Creates instances of AnnounceBot, connecting with a staggered
|
Creates instances of IRCBot, connecting with a staggered
|
||||||
increase in delay
|
increase in delay
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -264,6 +323,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
|
||||||
self.port = port
|
self.port = port
|
||||||
self.ssl = ssl
|
self.ssl = ssl
|
||||||
self.bot = None
|
self.bot = None
|
||||||
|
self.nicklists = {}
|
||||||
|
|
||||||
def buildProtocol(self, addr):
|
def buildProtocol(self, addr):
|
||||||
"""
|
"""
|
||||||
|
|
@ -280,6 +340,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
|
||||||
protocol.network = self.network
|
protocol.network = self.network
|
||||||
protocol.port = self.port
|
protocol.port = self.port
|
||||||
protocol.ssl = self.ssl
|
protocol.ssl = self.ssl
|
||||||
|
protocol.nicklist = []
|
||||||
return protocol
|
return protocol
|
||||||
|
|
||||||
def startedConnecting(self, connector):
|
def startedConnecting(self, connector):
|
||||||
|
|
@ -312,9 +373,22 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
|
||||||
reason (str): The reason for the failure.
|
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)
|
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):
|
def start(self):
|
||||||
"""
|
"""
|
||||||
Connect session to sessionhandler.
|
Connect session to sessionhandler.
|
||||||
|
|
@ -326,7 +400,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
|
||||||
from twisted.internet import ssl
|
from twisted.internet import ssl
|
||||||
service = reactor.connectSSL(self.network, int(self.port), self, ssl.ClientContextFactory())
|
service = reactor.connectSSL(self.network, int(self.port), self, ssl.ClientContextFactory())
|
||||||
except ImportError:
|
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:
|
else:
|
||||||
service = internet.TCPClient(self.network, int(self.port), self)
|
service = internet.TCPClient(self.network, int(self.port), self)
|
||||||
self.sessionhandler.portal.services.addService(service)
|
self.sessionhandler.portal.services.addService(service)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue