evennia/lib/server/portal/imc2.py

399 lines
14 KiB
Python

"""
IMC2 client module. Handles connecting to and communicating with an IMC2 server.
"""
from time import time
from twisted.internet import task
from twisted.application import internet
from twisted.internet import protocol
from twisted.conch import telnet
from src.server.session import Session
from src.utils import logger, utils
from src.server.portal.imc2lib import imc2_ansi
from src.server.portal.imc2lib import imc2_packets as pck
from django.utils.translation import ugettext as _
# storage containers for IMC2 muds and channels
class IMC2Mud(object):
"""
Stores information about other games connected to our current IMC2 network.
"""
def __init__(self, packet):
self.name = packet.origin
self.versionid = packet.optional_data.get('versionid', None)
self.networkname = packet.optional_data.get('networkname', None)
self.url = packet.optional_data.get('url', None)
self.host = packet.optional_data.get('host', None)
self.port = packet.optional_data.get('port', None)
self.sha256 = packet.optional_data.get('sha256', None)
# This is used to determine when a Mud has fallen into inactive status.
self.last_updated = time()
class IMC2MudList(dict):
"""
Keeps track of other MUDs connected to the IMC network.
"""
def get_mud_list(self):
"""
Returns a sorted list of connected Muds.
"""
muds = self.items()
muds.sort()
return [value for key, value in muds]
def update_mud_from_packet(self, packet):
"""
This grabs relevant info from the packet and stuffs it in the
Mud list for later retrieval.
"""
mud = IMC2Mud(packet)
self[mud.name] = mud
def remove_mud_from_packet(self, packet):
"""
Removes a mud from the Mud list when given a packet.
"""
mud = IMC2Mud(packet)
try:
del self[mud.name]
except KeyError:
# No matching entry, no big deal.
pass
class IMC2Channel(object):
"""
Stores information about channels available on the network.
"""
def __init__(self, packet):
self.localname = packet.optional_data.get('localname', None)
self.name = packet.optional_data.get('channel', None)
self.level = packet.optional_data.get('level', None)
self.owner = packet.optional_data.get('owner', None)
self.policy = packet.optional_data.get('policy', None)
self.last_updated = time()
class IMC2ChanList(dict):
"""
Keeps track of Channels on the IMC network.
"""
def get_channel_list(self):
"""
Returns a sorted list of cached channels.
"""
channels = self.items()
channels.sort()
return [value for key, value in channels]
def update_channel_from_packet(self, packet):
"""
This grabs relevant info from the packet and stuffs it in the
channel list for later retrieval.
"""
channel = IMC2Channel(packet)
self[channel.name] = channel
def remove_channel_from_packet(self, packet):
"""
Removes a channel from the Channel list when given a packet.
"""
channel = IMC2Channel(packet)
try:
del self[channel.name]
except KeyError:
# No matching entry, no big deal.
pass
#
# IMC2 protocol
#
class IMC2Bot(telnet.StatefulTelnetProtocol, Session):
"""
Provides the abstraction for the IMC2 protocol. Handles connection,
authentication, and all necessary packets.
"""
def __init__(self):
self.is_authenticated = False
# only support plaintext passwords
self.auth_type = "plaintext"
self.sequence = None
self.imc2_mudlist = IMC2MudList()
self.imc2_chanlist = IMC2ChanList()
def _send_packet(self, packet):
"Helper function to send packets across the wire"
packet.imc2_protocol = self
packet_str = utils.to_str(packet.assemble(self.factory.mudname,
self.factory.client_pwd, self.factory.server_pwd))
self.sendLine(packet_str)
def _isalive(self):
"Send an isalive packet"
self._send_packet(pck.IMC2PacketIsAlive())
def _keepalive(self):
"Send a keepalive packet"
# send to channel?
self._send_packet(pck.IMC2PacketKeepAliveRequest())
def _channellist(self):
"Sync the network channel list"
checked_networks = []
if not self.network in checked_networks:
self._send_packet(pck.IMC2PacketIceRefresh())
checked_networks.append(self.network)
def _prune(self):
"Prune active channel list"
t0 = time()
for name, mudinfo in self.imc2_mudlist.items():
if t0 - mudinfo.last_updated > 3599:
del self.imc2_mudlist[name]
def _whois_reply(self, packet):
"handle reply from server from an imcwhois request"
# packet.target potentially contains the id of an character to target
# not using that here
response_text = imc2_ansi.parse_ansi(packet.optional_data.get('text', 'Unknown'))
string = _('Whois reply from %(origin)s: %(msg)s') % {"origin":packet.origin, "msg":response_text}
# somehow pass reply on to a given player, for now we just send to channel
self.data_in(string)
def _format_tell(self, packet):
"""
Handle tells over IMC2 by formatting the text properly
"""
return _("{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s") % {"sender": packet.sender,
"origin": packet.origin,
"msg": packet.optional_data.get('text', 'ERROR: No text provided.')}
def _imc_login(self, line):
"Connect and identify to imc network"
if self.auth_type == "plaintext":
# Only support Plain text passwords.
# SERVER Sends: PW <servername> <serverpw> version=<version#> <networkname>
logger.log_infomsg("IMC2: AUTH< %s" % line)
line_split = line.split(' ')
pw_present = line_split[0] == 'PW'
autosetup_present = line_split[0] == 'autosetup'
if "reject" in line_split:
auth_message = _("IMC2 server rejected connection.")
logger.log_infomsg(auth_message)
return
if pw_present:
self.server_name = line_split[1]
self.network_name = line_split[4]
elif autosetup_present:
logger.log_infomsg(_("IMC2: Autosetup response found."))
self.server_name = line_split[1]
self.network_name = line_split[3]
self.is_authenticated = True
self.sequence = int(time())
# Log to stdout and notify over MUDInfo.
logger.log_infomsg('IMC2: Authenticated to %s' % self.factory.network)
# Ask to see what other MUDs are connected.
self._send_packet(pck.IMC2PacketKeepAliveRequest())
# IMC2 protocol states that KeepAliveRequests should be followed
# up by the requester sending an IsAlive packet.
self._send_packet(pck.IMC2PacketIsAlive())
# Get a listing of channels.
self._send_packet(pck.IMC2PacketIceRefresh())
def connectionMade(self):
"""
Triggered after connecting to the IMC2 network.
"""
self.stopping = False
self.factory.bot = self
address = "%s@%s" % (self.mudname, self.network)
self.init_session("ircbot", address, self.factory.sessionhandler)
# link back and log in
self.uid = int(self.factory.uid)
self.logged_in = True
self.factory.sessionhandler.connect(self)
logger.log_infomsg("IMC2 bot connected to %s." % self.network)
# Send authentication packet. The reply will be caught by lineReceived
self._send_packet(pck.IMC2PacketAuthPlaintext())
def lineReceived(self, line):
"""
IMC2 -> Evennia
Triggered when text is received from the IMC2 network. Figures out
what to do with the packet. This deals with the following
"""
line = line.strip()
if not self.is_authenticated:
# we are not authenticated yet. Deal with this.
self._imc_login(line)
return
#logger.log_infomsg("IMC2: RECV> %s" % line)
# Parse the packet and encapsulate it for easy access
packet = pck.IMC2Packet(self.mudname, packet_str=line)
# Figure out what kind of packet we're dealing with and hand it
# off to the correct handler.
if packet.packet_type == 'is-alive':
self.imc2_mudlist.update_mud_from_packet(packet)
elif packet.packet_type == 'keepalive-request':
# Don't need to check the destination, we only receive these
# packets when they are intended for us.
self.send_packet(pck.IMC2PacketIsAlive())
elif packet.packet_type == 'ice-msg-b':
self.data_out(text=line, packettype="broadcast")
elif packet.packet_type == 'whois-reply':
# handle eventual whois reply
self._whois_reply(packet)
elif packet.packet_type == 'close-notify':
self.imc2_mudlist.remove_mud_from_packet(packet)
elif packet.packet_type == 'ice-update':
self.imc2_chanlist.update_channel_from_packet(packet)
elif packet.packet_type == 'ice-destroy':
self.imc2_chanlist.remove_channel_from_packet(packet)
elif packet.packet_type == 'tell':
# send message to identified player
pass
def data_in(self, text=None, **kwargs):
"""
Data IMC2 -> Evennia
"""
text = "bot_data_in " + text
self.sessionhandler.data_in(self, text=text, **kwargs)
def data_out(self, text=None, **kwargs):
"""
Evennia -> IMC2
Keywords
packet_type:
broadcast - send to everyone on IMC channel
tell - send a tell (see target keyword)
whois - get whois information (see target keyword)
sender - used by tell to identify the sender
target - key identifier of target to tells or whois. If not
given "Unknown" will be used.
destination - used by tell to specify mud destination to send to
"""
if self.sequence:
# This gets incremented with every command.
self.sequence += 1
packet_type = kwargs.get("packet_type", "imcbroadcast")
if packet_type == "broadcast":
# broadcast to everyone on IMC channel
if text.startswith("bot_data_out"):
text = text.split(" ", 1)[1]
else:
return
# we remove the extra channel info since imc2 supplies this anyway
if ":" in text:
header, message = [part.strip() for part in text.split(":", 1)]
# Create imc2packet and send it
self._send_packet(pck.IMC2PacketIceMsgBroadcasted(self.servername,
self.channel,
header, text))
elif packet_type == "tell":
# send an IMC2 tell
sender = kwargs.get("sender", self.mudname)
target = kwargs.get("target", "Unknown")
destination = kwargs.get("destination", "Unknown")
self._send_packet(pck.IMC2PacketTell(sender, target, destination, text))
elif packet_type == "whois":
# send a whois request
sender = kwargs.get("sender", self.mudname)
target = kwargs.get("target", "Unknown")
self._send_packet(pck.IMC2PacketWhois(sender, target))
class IMC2BotFactory(protocol.ReconnectingClientFactory):
"""
Creates instances of the IMC2Protocol. Should really only ever
need to create one connection. Tied in via src/server.py.
"""
initialDelay = 1
factor = 1.5
maxDelay = 60
def __init__(self, sessionhandler, uid=None, network=None, channel=None,
port=None, mudname=None, client_pwd=None, server_pwd=None):
self.uid = uid
self.network = network
sname, host = network.split(".", 1)
self.servername = sname.strip()
self.channel = channel
self.port = port
self.mudname = mudname
self.protocol_version = '2'
self.client_pwd = client_pwd
self.server_pwd = server_pwd
self.bot = None
self.task_isalive = None
self.task_keepalive = None
self.task_prune = None
self.task_channellist = None
def buildProtocol(self, addr):
"Build the protocol"
protocol = IMC2Bot()
protocol.factory = self
protocol.network = self.network
protocol.servername = self.servername
protocol.channel = self.channel
protocol.mudname = self.mudname
protocol.port = self.port
return protocol
def clientConnectionFailed(self, connector, reason):
self.retry(connector)
def clientConnectionLost(self, connector, reason):
if not self.bot.stopping:
self.retry(connector)
def start(self):
"Connect session to sessionhandler"
def errback(fail):
logger.log_errmsg(fail.value)
if self.port:
service = internet.TCPClient(self.network, int(self.port), self)
self.sessionhandler.portal.services.addService(service)
# start tasks
self.task_isalive = task.LoopingCall(self.bot._isalive)
self.task_keepalive = task.LoopingCall(self.bot._keepalive)
self.task_prune = task.LoopingCall(self.bot._prune)
self.task_channellist = task.LoopingCall(self.bot._channellist)
self.task_isalive.start(900, now=False)
self.task_keepalive.start(3500, now=False)
self.task_prune.start(1800, now=False)
self.task_channellist.start(3600 * 24, now=False)