From 2108506a8aa7eaa0ebae4394165710aae4bc3d85 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 23 Feb 2014 21:07:16 +0100 Subject: [PATCH] Added new IRC protocol implementation. Not tested yet. --- src/objects/objects.py | 5 +- src/players/bots.py | 86 +++- src/players/models.py | 6 +- src/players/player.py | 5 +- src/server/portal/irc.py | 465 +++++++++++++--------- src/server/portal/portalsessionhandler.py | 6 +- src/server/serversession.py | 6 +- 7 files changed, 362 insertions(+), 217 deletions(-) diff --git a/src/objects/objects.py b/src/objects/objects.py index aa3b343c52..ce4b57bfb2 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -264,8 +264,9 @@ class Object(TypeClass): def execute_cmd(self, raw_string, sessid=None): """ Do something as this object. This command transparently - lets its typeclass execute the command. Evennia also calls - this method whenever the player sends a command on the command line. + lets its typeclass execute the command. This method is + never called normally, it is only called explicitly in + code. Argument: raw_string (string) - raw command input diff --git a/src/players/bots.py b/src/players/bots.py index 9d73f9655e..06e7e1a9a2 100644 --- a/src/players/bots.py +++ b/src/players/bots.py @@ -9,9 +9,13 @@ from src.scripts.script import Script from src.commands.command import Command from src.commands.cmdset import CmdSet from src.commands.cmdhandler import CMD_NOMATCH +from src.utils import search _SESSIONS = None + +# Bot helper utilities + class BotStarter(Script): """ This non-repeating script has the @@ -63,6 +67,8 @@ class BotCmdSet(CmdSet): self.add(CmdBotListen()) +# Bot base class + class Bot(Player): """ A Bot will start itself when the server @@ -70,20 +76,25 @@ class Bot(Player): on a reload - that will be handled by the normal Portal session resync) """ - def at_player_creation(self): + + def basetype_setup(self): """ - Called when the bot is first created. It sets - up the cmdset and the botstarter script + This sets up the basic properties for the bot. """ + # the text encoding to use. + self.db.encoding = "utf-8" + # A basic security setup + lockstring = "examine:perm(Wizards);edit:perm(Wizards);delete:perm(Wizards);boot:perm(Wizards);msg:all()" + self.locks.add(lockstring) + # set the basics of being a bot self.cmdset.add_default(BotCmdSet) script_key = "botstarter_%s" % self.key self.scripts.add(BotStarter, key=script_key) self.is_bot = True - def start(self): + def start(self, **kwargs): """ - This starts the bot, usually by connecting - to a protocol. + This starts the bot, whatever that may mean. """ pass @@ -100,23 +111,54 @@ class Bot(Player): pass +# Bot implementations + class IRCBot(Bot): """ - Bot for handling IRC connections + Bot for handling IRC connections. """ - def start(self): - "Start by telling the portal to start a new session" - global _SESSIONS + def start(self, ev_channel=None, irc_botname=None, irc_channel=None, irc_network=None, irc_port=None): + """ + Start by telling the portal to start a new session. + + ev_channel - key of the Evennia channel to connect to + irc_botname - name of bot to connect to irc channel. If not set, use self.key + irc_channel - name of channel on the form #channelname + irc_network - url of network, like irc.freenode.net + irc_port - port number of irc network, like 6667 + """ + global _SESSIONS, _CHANNELDB if not _SESSIONS: from src.server.sessionhandler import SESSIONS as _SESSIONS - # instruct the server and portal to create a new session - _SESSIONS.start_bot_session("src.server.portal.irc.IRCClient", self.id) - def connect_to_channel(self, botkey, channelname): - """ - Connect the bot to an Evennia channel - """ - pass + # if keywords are given, store (the BotStarter script + # will not give any keywords, so this should normally only + # happen at initialization) + self.db.irc_botname = irc_botname if irc_botname else self.key + if ev_channel: + # connect to Evennia channel + channel = search.channel_search(ev_channel) + if not channel: + raise RuntimeError("Evennia Channel '%s' not found." % ev_channel) + channel.connect(self) + self.db.ev_channel = channel + if irc_channel: + self.db.irc_channel = irc_channel + if irc_network: + self.db.irc_network = irc_network + if irc_port: + self.db.irc_port = irc_port + + # cache channel + self.ndb.ev_channel = self.db.ev_channel + + # instruct the server and portal to create a new session with + # the stored configuration + configdict = {"botname": self.db.irc_botname, + "channel": self.db.irc_channel , + "network": self.db.irc_network, + "port": self.db.irc_port} + _SESSIONS.start_bot_session("src.server.portal.irc.IRCClient", self.id, configdict) def msg(self, text=None, **kwargs): """ @@ -129,6 +171,10 @@ class IRCBot(Bot): text = "[%s] %s" % (ckey, text) self.dbobj.msg(text=text) - - def execute_cmd(self): - pass + def execute_cmd(self, text=None, sessid=None): + """ + Take incoming data and send it to connected channel. This is triggered + by the CmdListen command in the BotCmdSet. + """ + if self.ndb.channel: + self.ndb.channel.msg(text) diff --git a/src/players/models.py b/src/players/models.py index 51b1716a18..527a0a4eba 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -78,6 +78,7 @@ class PlayerDB(TypedObject, AbstractUser): name - alias for user.username sessions - sessions connected to this player is_superuser - bool if this player is a superuser + is_bot - bool if this player is a bot and not a real player """ @@ -419,12 +420,11 @@ class PlayerDB(TypedObject, AbstractUser): """ Do something as this player. This method is never called normally, but only when the player object itself is supposed to execute the - command. It does not take nicks on eventual puppets into account. + command. It takes player nicks into account, but not nicks of + eventual puppets. raw_string - raw command input coming from the command line. """ - # nick replacement - we require full-word matching. - raw_string = utils.to_unicode(raw_string) raw_string = self.nicks.nickreplace(raw_string, categories=("inputline", "channel"), include_player=False) diff --git a/src/players/player.py b/src/players/player.py index f11787b375..88efdd201c 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -136,8 +136,9 @@ class Player(TypeClass): def execute_cmd(self, raw_string, sessid=None): """ Do something as this object. This command transparently - lets its typeclass execute the command. Evennia also calls - this method whenever the player sends a command on the command line. + lets its typeclass execute the command. This method + is -not- called by Evennia normally, it is here to be + called explicitly in code. Argument: raw_string (string) - raw command input diff --git a/src/server/portal/irc.py b/src/server/portal/irc.py index 1f2f65decc..dbf4b3e01f 100644 --- a/src/server/portal/irc.py +++ b/src/server/portal/irc.py @@ -3,216 +3,309 @@ This connects to an IRC network/channel and launches an 'bot' onto it. The bot then pipes what is being said between the IRC channel and one or more Evennia channels. """ + from twisted.application import internet from twisted.words.protocols import irc from twisted.internet import protocol -from django.conf import settings -from src.comms.models import ExternalChannelConnection, ChannelDB -from src.utils import logger, utils -from src.server.sessionhandler import SESSIONS - -from django.utils.translation import ugettext as _ - -INFOCHANNEL = ChannelDB.objects.channel_search(settings.CHANNEL_MUDINFO[0]) -IRC_CHANNELS = [] +from src.server.session import Session +from src.utils import logger -def msg_info(message): +# IRC bot + +class IRCBot(irc.IRCClient, Session): """ - Send info to default info channel + An IRC bot that tracks actitivity in a channel as well + as sends text to it when prompted """ - message = '[%s][IRC]: %s' % (INFOCHANNEL[0].key, message) - try: - INFOCHANNEL[0].msg(message) - except AttributeError: - logger.log_infomsg("MUDinfo (irc): %s" % message) + lineRate = 1 + # assigned by factory at creation -class IRC_Bot(irc.IRCClient): - """ - This defines an IRC bot that connects to an IRC channel - and relays data to and from an evennia game. - """ - - def _get_nickname(self): - "required for correct nickname setting" - return self.factory.nickname - nickname = property(_get_nickname) + nickname = None + logger = None + factory = None + channel = None def signedOn(self): - # This is the first point the protocol is instantiated. - # add this protocol instance to the global list so we - # can access it later to send data. - global IRC_CHANNELS - self.join(self.factory.channel) - - IRC_CHANNELS.append(self) - #msg_info("Client connecting to %s.'" % (self.factory.channel)) - - def joined(self, channel): - msg = _("joined %s.") % self.factory.pretty_key - msg_info(msg) - logger.log_infomsg(msg) - - def get_mesg_info(self, user, irc_channel, msg): """ - Get basic information about a message posted in IRC. + This is called when we successfully connect to + the network. We make sure to store ourself + on the factory. """ - #find irc->evennia channel mappings - conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key) - if not conns: - return - #format message: - user = user.split("!")[0] - if user: - user.strip() - else: - user = _("Unknown") - msg = msg.strip() - sender_strings = ["%s@%s" % (user, irc_channel)] - return conns, msg, sender_strings + self.join(self.channel) + self.factory.bot = self - def privmsg(self, user, irc_channel, msg): - "Someone has written something in irc channel. Echo it to the evennia channel" - conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg) - #logger.log_infomsg("" - #find irc->evennia channel mappings - conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key) - if not conns: - return - conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg) - # Transform this into a pose. - msg = ':' + msg - #logger.log_infomsg(" Server" + self.sessionhandler.data_in(self, text=text, **kwargs) - Note that this cannot simply be called msg() since that's the - name of of the twisted irc hook as well, this leads to some - initialization messages to be sent without checks, causing loops. - """ - self.msg(utils.to_str(self.factory.channel), utils.to_str(msg)) + def data_out(self, text=None, **kwargs): + "Data from server-> IRC" + self.say(self.channel, text) + def start(self): + "Connect session to sessionhandler" + service = internet.TCPClient(self.network, int(self.port), self) + self.sessionhandler.portal.services.addService(service) + self.sessionhandler.connect(self) -class IRCbotFactory(protocol.ClientFactory): - protocol = IRC_Bot +class IRCBotFactory(protocol.ReconnectingClientFactory): + """ + Creates instances of AnnounceBot, connecting with + a staggered increase in delay + """ + # scaling reconnect time + initialDelay = 1 + factor = 1.5 + maxDelay = 60 - def __init__(self, key, channel, network, port, nickname, evennia_channel): - self.key = key - self.pretty_key = "%s:%s%s ('%s')" % (network, port, channel, nickname) + def __init__(self, botname=None, channel=None, network=None, port=None): + "Storing some important protocol properties" + self.nickname = botname + self.logger = logger + self.channel = channel self.network = network self.port = port - self.channel = channel - self.nickname = nickname - self.evennia_channel = evennia_channel + self.bots = None - def clientConnectionLost(self, connector, reason): - from twisted.internet.error import ConnectionDone - if type(reason.type) == type(ConnectionDone): - msg_info(_("Connection closed.")) - else: - msg_info(_("Lost connection %(key)s. Reason: '%(reason)s'. Reconnecting.") % {"key":self.pretty_key, "reason":reason}) - connector.connect() + def buildProtocol(self, addr): + "Build the protocol and assign it some properties" + protocol = IRCBot() + protocol.factory = self + protocol.nickname = self.nickname + protocol.channel = self.channel + protocol.network = self.network + protocol.port = self.port + return protocol - def clientConnectionFailed(self, connector, reason): - msg = _("Could not connect %(key)s Reason: '%(reason)s'") % {"key":self.pretty_key, "reason":reason} - msg_info(msg) - logger.log_errmsg(msg) + def startedConnecting(self, connector): + "Tracks reconnections for debugging" + logger.log_infomsg("(re)connecting to %s" % self.channel) -def build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick): - "Build an id hash for the connection" - if hasattr(channel, 'key'): - channel = channel.key - return "irc_%s:%s%s(%s)<>%s" % (irc_network, irc_port, - irc_channel, irc_bot_nick, channel) - - -def build_service_key(key): - return "IRCbot:%s" % key - - -def create_connection(channel, irc_network, irc_port, - irc_channel, irc_bot_nick): - """ - This will create a new IRC<->channel connection. - """ - if not type(channel) == ChannelDB: - new_channel = ChannelDB.objects.filter(db_key=channel) - if not new_channel: - logger.log_errmsg(_("Cannot attach IRC<->Evennia: Evennia Channel '%s' not found") % channel) - return False - channel = new_channel[0] - key = build_connection_key(channel, irc_network, irc_port, - irc_channel, irc_bot_nick) - - old_conns = ExternalChannelConnection.objects.filter(db_external_key=key) - if old_conns: - return False - config = "%s|%s|%s|%s" % (irc_network, irc_port, irc_channel, irc_bot_nick) - # how the channel will be able to contact this protocol - send_code = "from src.comms.irc import IRC_CHANNELS\n" - send_code += "matched_ircs = [irc for irc in IRC_CHANNELS if irc.factory.key == '%s']\n" % key - send_code += "[irc.msg_irc(message, senders=[self]) for irc in matched_ircs]\n" - conn = ExternalChannelConnection(db_channel=channel, - db_external_key=key, - db_external_send_code=send_code, - db_external_config=config) - conn.save() - - # connect - connect_to_irc(conn) - return True - -def delete_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick): - "Destroy a connection" - if hasattr(channel, 'key'): - channel = channel.key - - key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick) - service_key = build_service_key(key) - try: - conn = ExternalChannelConnection.objects.get(db_external_key=key) - except Exception: - return False - conn.delete() - - try: - service = SESSIONS.server.services.getServiceNamed(service_key) - except Exception: - return True - if service.running: - SESSIONS.server.services.removeService(service) - return True - -def connect_to_irc(connection): - "Create the bot instance and connect to the IRC network and channel." - # get config - key = utils.to_str(connection.external_key) - service_key = build_service_key(key) - irc_network, irc_port, irc_channel, irc_bot_nick = [utils.to_str(conf) for conf in connection.external_config.split('|')] - # connect - bot = internet.TCPClient(irc_network, int(irc_port), IRCbotFactory(key, irc_channel, irc_network, irc_port, irc_bot_nick, - connection.channel.key)) - bot.setName(service_key) - SESSIONS.server.services.addService(bot) - -def connect_all(): - """ - Activate all irc bots. - """ - for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_'): - connect_to_irc(connection) - +# +#from twisted.application import internet +#from twisted.words.protocols import irc +#from twisted.internet import protocol +#from django.conf import settings +#from src.comms.models import ExternalChannelConnection, ChannelDB +#from src.utils import logger, utils +#from src.server.sessionhandler import SESSIONS +# +#from django.utils.translation import ugettext as _ +# +#INFOCHANNEL = ChannelDB.objects.channel_search(settings.CHANNEL_MUDINFO[0]) +#IRC_CHANNELS = [] +# +#def msg_info(message): +# """ +# Send info to default info channel +# """ +# message = '[%s][IRC]: %s' % (INFOCHANNEL[0].key, message) +# try: +# INFOCHANNEL[0].msg(message) +# except AttributeError: +# logger.log_infomsg("MUDinfo (irc): %s" % message) +# +# +#class IRC_Bot(irc.IRCClient): +# """ +# This defines an IRC bot that connects to an IRC channel +# and relays data to and from an evennia game. +# """ +# +# def _get_nickname(self): +# "required for correct nickname setting" +# return self.factory.nickname +# nickname = property(_get_nickname) +# +# def signedOn(self): +# # This is the first point the protocol is instantiated. +# # add this protocol instance to the global list so we +# # can access it later to send data. +# global IRC_CHANNELS +# self.join(self.factory.channel) +# +# IRC_CHANNELS.append(self) +# #msg_info("Client connecting to %s.'" % (self.factory.channel)) +# +# def joined(self, channel): +# msg = _("joined %s.") % self.factory.pretty_key +# msg_info(msg) +# logger.log_infomsg(msg) +# +# def get_mesg_info(self, user, irc_channel, msg): +# """ +# Get basic information about a message posted in IRC. +# """ +# #find irc->evennia channel mappings +# conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key) +# if not conns: +# return +# #format message: +# user = user.split("!")[0] +# if user: +# user.strip() +# else: +# user = _("Unknown") +# msg = msg.strip() +# sender_strings = ["%s@%s" % (user, irc_channel)] +# return conns, msg, sender_strings +# +# def privmsg(self, user, irc_channel, msg): +# "Someone has written something in irc channel. Echo it to the evennia channel" +# conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg) +# #logger.log_infomsg("" +# #find irc->evennia channel mappings +# conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key) +# if not conns: +# return +# conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg) +# # Transform this into a pose. +# msg = ':' + msg +# #logger.log_infomsg("%s" % (irc_network, irc_port, +# irc_channel, irc_bot_nick, channel) +# +# +#def build_service_key(key): +# return "IRCbot:%s" % key +# +# +#def create_connection(channel, irc_network, irc_port, +# irc_channel, irc_bot_nick): +# """ +# This will create a new IRC<->channel connection. +# """ +# if not type(channel) == ChannelDB: +# new_channel = ChannelDB.objects.filter(db_key=channel) +# if not new_channel: +# logger.log_errmsg(_("Cannot attach IRC<->Evennia: Evennia Channel '%s' not found") % channel) +# return False +# channel = new_channel[0] +# key = build_connection_key(channel, irc_network, irc_port, +# irc_channel, irc_bot_nick) +# +# old_conns = ExternalChannelConnection.objects.filter(db_external_key=key) +# if old_conns: +# return False +# config = "%s|%s|%s|%s" % (irc_network, irc_port, irc_channel, irc_bot_nick) +# # how the channel will be able to contact this protocol +# send_code = "from src.comms.irc import IRC_CHANNELS\n" +# send_code += "matched_ircs = [irc for irc in IRC_CHANNELS if irc.factory.key == '%s']\n" % key +# send_code += "[irc.msg_irc(message, senders=[self]) for irc in matched_ircs]\n" +# conn = ExternalChannelConnection(db_channel=channel, +# db_external_key=key, +# db_external_send_code=send_code, +# db_external_config=config) +# conn.save() +# +# # connect +# connect_to_irc(conn) +# return True +# +#def delete_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick): +# "Destroy a connection" +# if hasattr(channel, 'key'): +# channel = channel.key +# +# key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick) +# service_key = build_service_key(key) +# try: +# conn = ExternalChannelConnection.objects.get(db_external_key=key) +# except Exception: +# return False +# conn.delete() +# +# try: +# service = SESSIONS.server.services.getServiceNamed(service_key) +# except Exception: +# return True +# if service.running: +# SESSIONS.server.services.removeService(service) +# return True +# +#def connect_to_irc(connection): +# "Create the bot instance and connect to the IRC network and channel." +# # get config +# key = utils.to_str(connection.external_key) +# service_key = build_service_key(key) +# irc_network, irc_port, irc_channel, irc_bot_nick = [utils.to_str(conf) for conf in connection.external_config.split('|')] +# # connect +# bot = internet.TCPClient(irc_network, int(irc_port), IRCbotFactory(key, irc_channel, irc_network, irc_port, irc_bot_nick, +# connection.channel.key)) +# bot.setName(service_key) +# SESSIONS.server.services.addService(bot) +# +#def connect_all(): +# """ +# Activate all irc bots. +# """ +# for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_'): +# connect_to_irc(connection) +# +# diff --git a/src/server/portal/portalsessionhandler.py b/src/server/portal/portalsessionhandler.py index 3e9a8db85c..05ff99fb9f 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/src/server/portal/portalsessionhandler.py @@ -78,12 +78,12 @@ class PortalSessionHandler(SessionHandler): protocol_path - full python path to the class factory for the protocol used, eg - 'src.server.portal.irc.IRCClient' + 'src.server.portal.irc.IRCClientFactory' uid - database uid to the connected player-bot config - dictionary of configuration options, fed as **kwarg to protocol class' __init__ method. - The called protocol class must have a method ConnectionMade + The called protocol class must have a method start() that calls the portalsession.connect() as a normal protocol. """ global _MOD_IMPORT @@ -93,7 +93,7 @@ class PortalSessionHandler(SessionHandler): cls = _MOD_IMPORT(path, clsname) protocol = cls(**config) protocol.sessionhandler = self - protocol.connectionMade() + protocol.start() def server_disconnect(self, sessid, reason=""): """ diff --git a/src/server/serversession.py b/src/server/serversession.py index b0637b8a9a..8daca40f81 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -177,7 +177,11 @@ class ServerSession(Session): """ Send User->Evennia. This will in effect execute a command string on the server. - Eventual extra data moves through oob_data_in + + Especially handled keywords: + + oob - this should hold a dictionary of oob command calls from + the oob-supporting protocol. """ if text: # this is treated as a command input