mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
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.CmdPage())
|
||||
self.add(comms.CmdIRC2Chan())
|
||||
self.add(comms.CmdIRCStatus())
|
||||
self.add(comms.CmdRSS2Chan())
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue