Added new IRC protocol implementation. Not tested yet.

This commit is contained in:
Griatch 2014-02-23 21:07:16 +01:00
parent 2ae5d56928
commit 2108506a8a
7 changed files with 362 additions and 217 deletions

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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("<IRC: " + msg)
for conn in conns:
if conn.channel:
conn.to_channel(msg, sender_strings=sender_strings)
def privmsg(self, user, channel, msg):
"A message was sent to channel"
if not msg.startswith('***'):
user = user.split('!', 1)[0]
self.data_in
def action(self, user, irc_channel, msg):
"Someone has performed an action, e.g. using /me <pose>"
#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("<IRC: " + msg)
for conn in conns:
if conn.channel:
conn.to_channel(msg, sender_strings=sender_strings)
def action(self, user, channel, msg):
"An action was done in channel"
if not msg.startswith('**'):
user = user.split('!', 1)[0]
def msg_irc(self, msg, senders=None):
"""
Called by evennia when sending something to mapped IRC channel.
def data_in(self, text=None, **kwargs):
"Data IRC -> 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("<IRC: " + msg)
# for conn in conns:
# if conn.channel:
# conn.to_channel(msg, sender_strings=sender_strings)
#
# def action(self, user, irc_channel, msg):
# "Someone has performed an action, e.g. using /me <pose>"
# #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("<IRC: " + msg)
# for conn in conns:
# if conn.channel:
# conn.to_channel(msg, sender_strings=sender_strings)
#
# def msg_irc(self, msg, senders=None):
# """
# Called by evennia when sending something to mapped IRC channel.
#
# 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))
#
#
#class IRCbotFactory(protocol.ClientFactory):
# protocol = IRC_Bot
#
# def __init__(self, key, channel, network, port, nickname, evennia_channel):
# self.key = key
# self.pretty_key = "%s:%s%s ('%s')" % (network, port, channel, nickname)
# self.network = network
# self.port = port
# self.channel = channel
# self.nickname = nickname
# self.evennia_channel = evennia_channel
#
# 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 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 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)
#
#

View file

@ -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=""):
"""

View file

@ -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