diff --git a/src/commands/imc2.py b/src/commands/imc2.py index 3fe3a2687d..e1555ac6a5 100644 --- a/src/commands/imc2.py +++ b/src/commands/imc2.py @@ -122,7 +122,7 @@ GLOBAL_CMD_TABLE.add_command("imcstatus", cmd_imcstatus) def cmd_IMC2chan(command): """ - @imc2chan IMCchannel channel + @imc2chan IMCServer:IMCchannel channel Links an IMC channel to an existing evennia channel. You can link as many existing @@ -130,7 +130,8 @@ def cmd_IMC2chan(command): IMC channel this way. Running the command with an existing mapping will re-map the channels. - Use 'imcchanlist' to get a list of IMC channels. + Use 'imcchanlist' to get a list of IMC channels and servers. + Note that both are case sensitive. """ source_object = command.source_object if not settings.IMC2_ENABLED: @@ -139,16 +140,22 @@ def cmd_IMC2chan(command): return args = command.command_argument if not args or len(args.split()) != 2 : - source_object.emit_to("Usage: @imc2chan IMCchannel channel") + source_object.emit_to("Usage: @imc2chan IMCServer:IMCchannel channel") return - imc_channel, channel = args.split() - imclist = IMC2_CHANLIST.get_channel_list() - if imc_channel not in [c.localname for c in imclist]: - source_object.emit_to("IMC channel '%s' not found." % imc_channel) + #identify the server-channel pair + imcdata, channel = args.split() + if not ":" in imcdata: + source_object.emit_to("You need to supply an IMC Server:Channel pair.") + return + imclist = IMC2_CHANLIST.get_channel_list() + imc_channels = filter(lambda c: c.name == imcdata, imclist) + if not imc_channels: + source_object.emit_to("IMC server and channel '%s' not found." % imcdata) return else: - imc_channel = filter(lambda c: c.localname==imc_channel,imclist) - if imc_channel: imc_channel = imc_channel[0] + imc_server_name, imc_channel_name = imcdata.split(":") + + #find evennia channel try: chanobj = comsys.get_cobj_from_name(channel) except CommChannel.DoesNotExist: @@ -163,11 +170,9 @@ def cmd_IMC2chan(command): outstring = "Replacing %s. New " % mapping else: mapping = IMC2ChannelMapping() - - server,name = imc_channel.name.split(":") - mapping.imc2_server_name = server.strip() #settings.IMC2_SERVER_ADDRESS - mapping.imc2_channel_name = name.strip() #imc_channel.name + mapping.imc2_server_name = imc_server_name + mapping.imc2_channel_name = imc_channel_name mapping.channel = chanobj mapping.save() outstring += "Mapping set: %s." % mapping diff --git a/src/commands/irc.py b/src/commands/irc.py new file mode 100644 index 0000000000..04d9d744f4 --- /dev/null +++ b/src/commands/irc.py @@ -0,0 +1,97 @@ +""" +IRC-related functions +""" + +from django.conf import settings +from src.irc.connection import IRC_CHANNELS +from src.irc.connection import connect_to_IRC +from src.irc.models import IRCChannelMapping +from src import comsys +from src.cmdtable import GLOBAL_CMD_TABLE + +def cmd_IRC2chan(command): + """ + @irc2chan IRCchannel channel + + Links an IRC channel (including #) to an existing + evennia channel. You can link as many existing + evennia channels as you like to the + IRC channel this way. Running the command with an + existing mapping will re-map the channels. + + """ + source_object = command.source_object + if not settings.IRC_ENABLED: + s = """IRC is not enabled. You need to activate it in game/settings.py.""" + source_object.emit_to(s) + return + args = command.command_argument + if not args or len(args.split()) != 2 : + source_object.emit_to("Usage: @irc2chan IRCchannel channel") + return + irc_channel, channel = args.split() + if irc_channel not in [o.factory.channel for o in IRC_CHANNELS]: + source_object.emit_to("IRC channel '%s' not found." % irc_channel) + return + try: + chanobj = comsys.get_cobj_from_name(channel) + except CommChannel.DoesNotExist: + source_object.emit_to("Local channel '%s' not found (use real name, not alias)." % channel) + return + + #create the mapping. + outstring = "" + mapping = IRCChannelMapping.objects.filter(channel__name=channel) + if mapping: + mapping = mapping[0] + outstring = "Replacing %s. New " % mapping + else: + mapping = IRCChannelMapping() + + mapping.irc_server_name = settings.IRC_NETWORK + mapping.irc_channel_name = irc_channel + mapping.channel = chanobj + mapping.save() + outstring += "Mapping set: %s." % mapping + source_object.emit_to(outstring) + +GLOBAL_CMD_TABLE.add_command("@irc2chan",cmd_IRC2chan,auto_help=True,staff_help=True, + priv_tuple=("objects.add_commchannel",)) + +def cmd_IRCjoin(command): + """ + @ircjoin IRCchannel + + Attempts to connect a bot to a new IRC channel (don't forget that + IRC channels begin with a #). + The bot uses the connection details defined in the main settings. + + Observe that channels added using this command does not survive a reboot. + """ + source_object = command.source_object + arg = command.command_argument + if not arg: + source_object.emit_to("Usage: @ircjoin irc_channel") + return + channel = arg.strip() + if channel[0] != "#": channel = "#%s" % channel + + connect_to_IRC(settings.IRC_NETWORK, + settings.IRC_PORT, + channel,settings.IRC_NICKNAME) +GLOBAL_CMD_TABLE.add_command("@ircjoin",cmd_IRCjoin,auto_help=True, + staff_help=True, + priv_tuple=("objects.add_commchannel",)) + +def cmd_IRCchanlist(command): + """ + ircchanlist + + Lists all externally available IRC channels. + """ + source_object = command.source_object + s = "Available IRC channels:" + for c in IRC_CHANNELS: + s += "\n %s \t(nick '%s') on %s" % (c.factory.channel,c.factory.nickname,c.factory.network,) + source_object.emit_to(s) +GLOBAL_CMD_TABLE.add_command("ircchanlist", cmd_IRCchanlist, auto_help=True) diff --git a/src/imc2/connection.py b/src/imc2/connection.py index eef388d85d..77bf4fc8de 100644 --- a/src/imc2/connection.py +++ b/src/imc2/connection.py @@ -1,199 +1,199 @@ -""" -IMC2 client module. Handles connecting to and communicating with an IMC2 server. -""" -import telnetlib -from time import time -from twisted.internet.protocol import ClientFactory -from twisted.protocols.basic import LineReceiver -from twisted.internet import reactor, task -from twisted.conch.telnet import StatefulTelnetProtocol -from django.conf import settings -from src import logger -from src import session_mgr -from src.imc2.packets import * -from src.imc2.trackers import * -from src.imc2 import reply_listener -from src.imc2.models import IMC2ChannelMapping -from src import comsys - -# The active instance of IMC2Protocol. Set at server startup. -IMC2_PROTOCOL_INSTANCE = None - -def cemit_info(message): - """ - Channel emits info to the appropriate info channel. By default, this - is MUDInfo. - """ - comsys.send_cmessage(settings.COMMCHAN_IMC2_INFO, 'IMC: %s' % message) - -class IMC2Protocol(StatefulTelnetProtocol): - """ - Provides the abstraction for the IMC2 protocol. Handles connection, - authentication, and all necessary packets. - """ - def __init__(self): - message = "Client connecting to %s:%s..." % ( - settings.IMC2_SERVER_ADDRESS, - settings.IMC2_SERVER_PORT) - logger.log_infomsg('IMC2: %s' % message) - cemit_info(message) - global IMC2_PROTOCOL_INSTANCE - IMC2_PROTOCOL_INSTANCE = self - self.is_authenticated = False - self.auth_type = None - self.server_name = None - self.network_name = None - self.sequence = None - - def connectionMade(self): - """ - Triggered after connecting to the IMC2 network. - """ - logger.log_infomsg("IMC2: Connected to network server.") - self.auth_type = "plaintext" - logger.log_infomsg("IMC2: Sending authentication packet.") - self.send_packet(IMC2PacketAuthPlaintext()) - - def send_packet(self, packet): - """ - Given a sub-class of IMC2Packet, assemble the packet and send it - on its way. - """ - if self.sequence: - # This gets incremented with every command. - self.sequence += 1 - - packet.imc2_protocol = self - packet_str = str(packet.assemble()) - logger.log_infomsg("IMC2: SENT> %s" % packet_str) - self.sendLine(packet_str) - - def _parse_auth_response(self, line): - """ - Parses the IMC2 network authentication packet. - """ - if self.auth_type == "plaintext": - """ - SERVER Sends: PW version= - """ - line_split = line.split(' ') - pw_present = line_split[0] == 'PW' - autosetup_present = line_split[0] == 'autosetup' - - 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. - auth_message = "Successfully authenticated to the '%s' network." % self.network_name - logger.log_infomsg('IMC2: %s' % auth_message) - cemit_info(auth_message) - - # Ask to see what other MUDs are connected. - self.send_packet(IMC2PacketKeepAliveRequest()) - # IMC2 protocol states that KeepAliveRequests should be followed - # up by the requester sending an IsAlive packet. - self.send_packet(IMC2PacketIsAlive()) - # Get a listing of channels. - self.send_packet(IMC2PacketIceRefresh()) - - def _handle_channel_mappings(self, packet): - """ - Received a message. Look for an IMC2 channel mapping and - route it accordingly. - """ - chan_name = packet.optional_data.get('channel', None) - # If the packet lacks the 'echo' key, don't bother with it. - has_echo = packet.optional_data.get('echo', None) - if chan_name and has_echo: - # The second half of this is the channel name: Server:Channel - chan_name = chan_name.split(':', 1)[1] - try: - # Look for matching IMC2 channel maps. - mappings = IMC2ChannelMapping.objects.filter(imc2_channel_name=chan_name) - # Format the message to cemit to the local channel. - message = '%s@%s: %s' % (packet.sender, - packet.origin, - packet.optional_data.get('text')) - # Bombs away. - for mapping in mappings: - if mapping.channel: - comsys.send_cmessage(mapping.channel, message, - from_external="IMC2") - except IMC2ChannelMapping.DoesNotExist: - # No channel mapping found for this message, ignore it. - pass - - def lineReceived(self, line): - """ - Triggered when text is received from the IMC2 network. Figures out - what to do with the packet. - """ - if not self.is_authenticated: - self._parse_auth_response(line) - else: - if settings.IMC2_DEBUG and 'is-alive' not in line: - # if IMC2_DEBUG mode is on, print the contents of the packet - # to stdout. - logger.log_infomsg("PACKET: %s" % line) - - # Parse the packet and encapsulate it for easy access - packet = IMC2Packet(packet_str = line) - - if settings.IMC2_DEBUG and packet.packet_type not in ['is-alive', 'keepalive-request']: - # Print the parsed packet's __str__ representation. - # is-alive and keepalive-requests happen pretty frequently. - # Don't bore us with them in stdout. - logger.log_infomsg(packet) - - """ - Figure out what kind of packet we're dealing with and hand it - off to the correct handler. - """ - if packet.packet_type == 'is-alive': - 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(IMC2PacketIsAlive()) - elif packet.packet_type == 'ice-msg-b': - self._handle_channel_mappings(packet) - elif packet.packet_type == 'whois-reply': - reply_listener.handle_whois_reply(packet) - elif packet.packet_type == 'close-notify': - IMC2_MUDLIST.remove_mud_from_packet(packet) - elif packet.packet_type == 'ice-update': - IMC2_CHANLIST.update_channel_from_packet(packet) - elif packet.packet_type == 'ice-destroy': - IMC2_CHANLIST.remove_channel_from_packet(packet) - elif packet.packet_type == 'tell': - sessions = session_mgr.find_sessions_from_username(packet.target) - for session in sessions: - session.msg("%s@%s IMC tells: %s" % - (packet.sender, - packet.origin, - packet.optional_data.get('text', - 'ERROR: No text provided.'))) - -class IMC2ClientFactory(ClientFactory): - """ - Creates instances of the IMC2Protocol. Should really only ever create one - in our particular instance. Tied in via src/server.py. - """ - protocol = IMC2Protocol - - def clientConnectionFailed(self, connector, reason): - message = 'Connection failed: %s' % reason.getErrorMessage() - cemit_info(message) - logger.log_errmsg('IMC2: %s' % message) - - def clientConnectionLost(self, connector, reason): - message = 'Connection lost: %s' % reason.getErrorMessage() - cemit_info(message) - logger.log_errmsg('IMC2: %s' % message) +""" +IMC2 client module. Handles connecting to and communicating with an IMC2 server. +""" +import telnetlib +from time import time +from twisted.internet.protocol import ClientFactory +from twisted.protocols.basic import LineReceiver +from twisted.internet import reactor, task +from twisted.conch.telnet import StatefulTelnetProtocol +from django.conf import settings +from src import logger +from src import session_mgr +from src.imc2.packets import * +from src.imc2.trackers import * +from src.imc2 import reply_listener +from src.imc2.models import IMC2ChannelMapping +from src import comsys + +# The active instance of IMC2Protocol. Set at server startup. +IMC2_PROTOCOL_INSTANCE = None + +def cemit_info(message): + """ + Channel emits info to the appropriate info channel. By default, this + is MUDInfo. + """ + comsys.send_cmessage(settings.COMMCHAN_IMC2_INFO, 'IMC: %s' % message,from_external="IMC2") + +class IMC2Protocol(StatefulTelnetProtocol): + """ + Provides the abstraction for the IMC2 protocol. Handles connection, + authentication, and all necessary packets. + """ + def __init__(self): + message = "Client connecting to %s:%s..." % ( + settings.IMC2_SERVER_ADDRESS, + settings.IMC2_SERVER_PORT) + logger.log_infomsg('IMC2: %s' % message) + cemit_info(message) + global IMC2_PROTOCOL_INSTANCE + IMC2_PROTOCOL_INSTANCE = self + self.is_authenticated = False + self.auth_type = None + self.server_name = None + self.network_name = None + self.sequence = None + + def connectionMade(self): + """ + Triggered after connecting to the IMC2 network. + """ + logger.log_infomsg("IMC2: Connected to network server.") + self.auth_type = "plaintext" + logger.log_infomsg("IMC2: Sending authentication packet.") + self.send_packet(IMC2PacketAuthPlaintext()) + + def send_packet(self, packet): + """ + Given a sub-class of IMC2Packet, assemble the packet and send it + on its way. + """ + if self.sequence: + # This gets incremented with every command. + self.sequence += 1 + + packet.imc2_protocol = self + packet_str = str(packet.assemble()) + logger.log_infomsg("IMC2: SENT> %s" % packet_str) + self.sendLine(packet_str) + + def _parse_auth_response(self, line): + """ + Parses the IMC2 network authentication packet. + """ + if self.auth_type == "plaintext": + """ + SERVER Sends: PW version= + """ + line_split = line.split(' ') + pw_present = line_split[0] == 'PW' + autosetup_present = line_split[0] == 'autosetup' + + 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. + auth_message = "Successfully authenticated to the '%s' network." % self.network_name + logger.log_infomsg('IMC2: %s' % auth_message) + cemit_info(auth_message) + + # Ask to see what other MUDs are connected. + self.send_packet(IMC2PacketKeepAliveRequest()) + # IMC2 protocol states that KeepAliveRequests should be followed + # up by the requester sending an IsAlive packet. + self.send_packet(IMC2PacketIsAlive()) + # Get a listing of channels. + self.send_packet(IMC2PacketIceRefresh()) + + def _handle_channel_mappings(self, packet): + """ + Received a message. Look for an IMC2 channel mapping and + route it accordingly. + """ + chan_name = packet.optional_data.get('channel', None) + # If the packet lacks the 'echo' key, don't bother with it. + has_echo = packet.optional_data.get('echo', None) + if chan_name and has_echo: + # The second half of this is the channel name: Server:Channel + chan_name = chan_name.split(':', 1)[1] + try: + # Look for matching IMC2 channel maps. + mappings = IMC2ChannelMapping.objects.filter(imc2_channel_name=chan_name) + # Format the message to cemit to the local channel. + message = '%s@%s: %s' % (packet.sender, + packet.origin, + packet.optional_data.get('text')) + # Bombs away. + for mapping in mappings: + if mapping.channel: + comsys.send_cmessage(mapping.channel, message, + from_external="IMC2") + except IMC2ChannelMapping.DoesNotExist: + # No channel mapping found for this message, ignore it. + pass + + def lineReceived(self, line): + """ + Triggered when text is received from the IMC2 network. Figures out + what to do with the packet. + """ + if not self.is_authenticated: + self._parse_auth_response(line) + else: + if settings.IMC2_DEBUG and 'is-alive' not in line: + # if IMC2_DEBUG mode is on, print the contents of the packet + # to stdout. + logger.log_infomsg("PACKET: %s" % line) + + # Parse the packet and encapsulate it for easy access + packet = IMC2Packet(packet_str = line) + + if settings.IMC2_DEBUG and packet.packet_type not in ['is-alive', 'keepalive-request']: + # Print the parsed packet's __str__ representation. + # is-alive and keepalive-requests happen pretty frequently. + # Don't bore us with them in stdout. + logger.log_infomsg(packet) + + """ + Figure out what kind of packet we're dealing with and hand it + off to the correct handler. + """ + if packet.packet_type == 'is-alive': + 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(IMC2PacketIsAlive()) + elif packet.packet_type == 'ice-msg-b': + self._handle_channel_mappings(packet) + elif packet.packet_type == 'whois-reply': + reply_listener.handle_whois_reply(packet) + elif packet.packet_type == 'close-notify': + IMC2_MUDLIST.remove_mud_from_packet(packet) + elif packet.packet_type == 'ice-update': + IMC2_CHANLIST.update_channel_from_packet(packet) + elif packet.packet_type == 'ice-destroy': + IMC2_CHANLIST.remove_channel_from_packet(packet) + elif packet.packet_type == 'tell': + sessions = session_mgr.find_sessions_from_username(packet.target) + for session in sessions: + session.msg("%s@%s IMC tells: %s" % + (packet.sender, + packet.origin, + packet.optional_data.get('text', + 'ERROR: No text provided.'))) + +class IMC2ClientFactory(ClientFactory): + """ + Creates instances of the IMC2Protocol. Should really only ever create one + in our particular instance. Tied in via src/server.py. + """ + protocol = IMC2Protocol + + def clientConnectionFailed(self, connector, reason): + message = 'Connection failed: %s' % reason.getErrorMessage() + cemit_info(message) + logger.log_errmsg('IMC2: %s' % message) + + def clientConnectionLost(self, connector, reason): + message = 'Connection lost: %s' % reason.getErrorMessage() + cemit_info(message) + logger.log_errmsg('IMC2: %s' % message)