diff --git a/src/ansi.py b/src/ansi.py index 6e0f2a7261..003d3b17a7 100755 --- a/src/ansi.py +++ b/src/ansi.py @@ -144,3 +144,9 @@ def parse_ansi(string, strip_ansi=False, strip_formatting=False, parser=ANSI_PAR """ return parser.parse_ansi(string, strip_ansi=strip_ansi, strip_formatting=strip_formatting) +def clean_ansi(string): + """ + Cleans all ansi symbols from a string + """ + r= re.compile("\033\[[0-9;]+m") + return r.sub("", string) #replace all matches with empty strings diff --git a/src/commands/comsys.py b/src/commands/comsys.py index 508419195e..c67da872af 100644 --- a/src/commands/comsys.py +++ b/src/commands/comsys.py @@ -9,8 +9,10 @@ from src import defines_global from src import ansi from src.util import functions_general from src.cmdtable import GLOBAL_CMD_TABLE -from src.imc2.models import IMC2ChannelMapping -from src.imc2.packets import IMC2PacketIceMsgBroadcasted +#from src.imc2.models import IMC2ChannelMapping +#from src.imc2.packets import IMC2PacketIceMsgBroadcasted +#from src.irc.models import IRCChannelMapping +#from src.irc.connection import IRC_CHANNELS def cmd_addcom(command): """ @@ -233,7 +235,7 @@ def cmd_cemit(command): cname = eq_args[0] cmessage = eq_args[1] - + final_cmessage = cmessage if len(cname) == 0: source_object.emit_to("You must provide a channel name to emit to.") return @@ -247,7 +249,7 @@ def cmd_cemit(command): else: source_object.emit_to("Could not find channel %s." % (cname,)) return - + # If this is False, don't show the channel header before # the message. For example: [Public] Woohoo! show_channel_header = True @@ -274,22 +276,11 @@ def cmd_cemit(command): if not "quiet" in command.command_switches: source_object.emit_to("Sent - %s" % (name_matches[0],)) comsys.send_cmessage(cname_parsed, final_cmessage, - show_header=show_channel_header) + show_header=show_channel_header) - if settings.IMC2_ENABLED: - # Look for IMC2 channel maps. If one is found, send an ice-msg-b - # packet to the network. - try: - from src.imc2.connection import IMC2_PROTOCOL_INSTANCE - map = IMC2ChannelMapping.objects.get(channel__name=cname_parsed) - packet = IMC2PacketIceMsgBroadcasted(map.imc2_server_name, - map.imc2_channel_name, - source_object, - cmessage) - IMC2_PROTOCOL_INSTANCE.send_packet(packet) - except IMC2ChannelMapping.DoesNotExist: - # No map found, do nothing. - pass + #pipe to external channels (IRC, IMC) eventually mapped to this channel + comsys.send_cexternal(cname_parsed, "[%s] %s" % (cname_parsed,final_cmessage)) + GLOBAL_CMD_TABLE.add_command("@cemit", cmd_cemit), def cmd_cwho(command): @@ -362,7 +353,7 @@ def cmd_ccreate(command): new_chan = comsys.create_channel(cname, source_object) source_object.emit_to("Channel %s created." % (new_chan.get_name(),)) GLOBAL_CMD_TABLE.add_command("@ccreate", cmd_ccreate, - priv_tuple=("objects.add_commchannel")), + priv_tuple=("objects.add_commchannel",)) def cmd_cchown(command): """ diff --git a/src/commands/imc2.py b/src/commands/imc2.py index 0b94e77ad4..3fe3a2687d 100644 --- a/src/commands/imc2.py +++ b/src/commands/imc2.py @@ -7,14 +7,17 @@ from src.config.models import ConfigValue from src.objects.models import Object from src import defines_global from src import ansi +from src import comsys from src.util import functions_general from src.cmdtable import GLOBAL_CMD_TABLE from src.ansi import parse_ansi from src.imc2.imc_ansi import IMCANSIParser from src.imc2 import connection as imc2_conn from src.imc2.packets import * +from src.imc2.models import IMC2ChannelMapping from src.imc2.trackers import IMC2_MUDLIST, IMC2_CHANLIST - +from src.channels.models import CommChannel + def cmd_imcwhois(command): """ Shows a player's inventory. @@ -114,4 +117,62 @@ def cmd_imcstatus(command): retval += '-' * 50 source_object.emit_to(retval) -GLOBAL_CMD_TABLE.add_command("imcstatus", cmd_imcstatus) \ No newline at end of file +GLOBAL_CMD_TABLE.add_command("imcstatus", cmd_imcstatus) + + +def cmd_IMC2chan(command): + """ + @imc2chan IMCchannel channel + + Links an IMC channel to an existing + evennia channel. You can link as many existing + evennia channels as you like to the + 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. + """ + source_object = command.source_object + if not settings.IMC2_ENABLED: + s = """IMC 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: @imc2chan 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) + return + else: + imc_channel = filter(lambda c: c.localname==imc_channel,imclist) + if imc_channel: imc_channel = imc_channel[0] + 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 = IMC2ChannelMapping.objects.filter(channel__name=channel) + if mapping: + mapping = mapping[0] + 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.channel = chanobj + mapping.save() + outstring += "Mapping set: %s." % mapping + source_object.emit_to(outstring) + +GLOBAL_CMD_TABLE.add_command("@imc2chan",cmd_IMC2chan,auto_help=True,staff_help=True, + priv_tuple=("objects.add_commchannel",)) + diff --git a/src/comsys.py b/src/comsys.py index a6fd474fb4..a19dddef9a 100644 --- a/src/comsys.py +++ b/src/comsys.py @@ -3,11 +3,16 @@ Comsys functions. """ import time import datetime +from django.conf import settings from django.utils import simplejson from src.channels.models import CommChannel, CommChannelMessage, CommChannelMembership from src import session_mgr from src import ansi from src import logger +from src.imc2.packets import IMC2PacketIceMsgBroadcasted +from src.imc2.models import IMC2ChannelMapping +from src.irc.models import IRCChannelMapping +import src.ansi def plr_get_cdict(session): """ @@ -226,13 +231,18 @@ def load_object_channels(pobject): session.channels_subscribed[membership.user_alias] = [membership.channel.name, membership.is_listening] -def send_cmessage(channel, message, show_header=True): +def send_cmessage(channel, message, show_header=True, from_external=None): """ Sends a message to all players on the specified channel. channel: (string or CommChannel) Name of channel or a CommChannel object. message: (string) Message to send. show_header: (bool) If False, don't prefix message with the channel header. + from_external: (string/None) + Can be None, 'IRC' or 'IMC2'. The sending functions of the + respective protocol sets this flag, otherwise it should + be None; it allows for piping messages between protocols + without accidentally also echoing it back to where it came from. """ if isinstance(channel, unicode) or isinstance(channel, str): # If they've passed a string as the channel argument, look up the @@ -258,6 +268,10 @@ def send_cmessage(channel, message, show_header=True): chan_message.message = message chan_message.save() + #pipe to external protocols + if from_external: + send_cexternal(channel_obj.name, message, from_external) + def get_all_channels(): """ Returns all channel objects. @@ -295,3 +309,63 @@ def cname_search(search_text, exact=False): return CommChannel.objects.filter(name__iexact=search_text) else: return CommChannel.objects.filter(name__istartswith=search_text) + + + + +def send_cexternal(cname, cmessage, from_external=None): + """ + This allows external protocols like IRC and IMC to send to a channel + while also echoing to each other. This used by channel-emit functions + to transparently distribute channel sends to external protocols. + + cname - name of evennia channel sent to + cmessage - message sent (should be pre-formatted already) + from_external - which protocol sent the emit. + Currently supports 'IRC' and 'IMC2' or None + (this avoids emits echoing back to themselves). If + None, it is assumed the message comes from within Evennia + and all mapped external channels will be notified. + """ + + if settings.IMC2_ENABLED and not from_external=="IMC": + #map an IRC emit to the IMC network + + # Look for IMC2 channel maps. If one is found, send an ice-msg-b + # packet to the network. + #handle lack of user, IMC-way. + + try: + from src.imc2.connection import IMC2_PROTOCOL_INSTANCE + map = IMC2ChannelMapping.objects.get(channel__name=cname) + packet = IMC2PacketIceMsgBroadcasted(map.imc2_server_name, + map.imc2_channel_name, + "*", + cmessage) + IMC2_PROTOCOL_INSTANCE.send_packet(packet) + except IMC2ChannelMapping.DoesNotExist: + # No map found, do nothing. + pass + + if settings.IRC_ENABLED and not from_external=="IRC": + # Map an IMC emit to IRC channels + + # Look for IRC channel maps. If found, echo cmessage to the + # IRC channel. + + try: + #this fails with a DoesNotExist if the channel is not mapped. + from src.irc.connection import IRC_CHANNELS + mapping = IRCChannelMapping.objects.filter(channel__name=cname) + #strip the message of ansi characters. + cmessage = ansi.clean_ansi(cmessage) + for mapp in mapping: + mapped_irc = filter(lambda c: c.factory.channel==mapp.irc_channel_name, + IRC_CHANNELS) + for chan in mapped_irc: + chan.send_msg(cmessage.encode("utf-8")) + except IRCChannelMapping.DoesNotExist: + #no mappings. Ignore + pass + + diff --git a/src/config_defaults.py b/src/config_defaults.py index 5c786e3746..123d94809a 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -66,53 +66,80 @@ DATABASE_HOST = '' # Empty string defaults to localhost. Not used with sqlite3. DATABASE_PORT = '' -""" -IMC Configuration -This is static and important enough to put in the server-side settings file. -Copy and paste this section to your game/settings.py file and change the -values to fit your needs. +# Communication channels in-game -Evennia's IMC2 client was developed against MudByte's network. You may -register and join it by going to: -http://www.mudbytes.net/imc2-intermud-join-network +""" +Your names of various default comm channels for emitting +debug- or informative messages. +""" -Choose "Other unsupported IMC2 version" and enter your information there. -You'll want to change the values below to reflect what you entered. -""" -# Make sure this is True in your settings.py. -# CHANGE THIS TO True IF YOU WANT IMC! -IMC2_ENABLED = False -# While True, emit additional debugging info. -IMC2_DEBUG = False -# The hostname/ip address of your IMC2 server of choice. -# CHANGE THIS! -IMC2_SERVER_ADDRESS = None -# The port to connect to on your IMC2 server. -# CHANGE THIS! -IMC2_SERVER_PORT = None -# This is your game's IMC2 name. -# CHANGE THIS TO A SHORT ALPHANUMERIC STRING! -IMC2_MUDNAME = None -# Your IMC2 client-side password. Used to authenticate with your network. -# CHANGE THIS TO A SHORT ALPHANUMERIC STRING! -IMC2_CLIENT_PW = None -# Your IMC2 server-side password. Used to verify your network's identity. -# CHANGE THIS TO A SHORT ALPHANUMERIC STRING! -IMC2_SERVER_PW = None -# This isn't something you should generally change. -IMC2_PROTOCOL_VERSION = '2' - -# MudBytes IMC Information -#IMC2_SERVER_ADDRESS = 'server02.mudbytes.net' -#IMC2_SERVER_PORT = 9000 -""" -Various comm channels for emitting debug or informative messages. -""" COMMCHAN_IMC2_INFO = 'MUDInfo' COMMCHAN_MUD_INFO = 'MUDInfo' COMMCHAN_MUD_CONNECTIONS = 'MUDConnections' + +""" +IMC Configuration + +IMC (Inter-MUD communication) allows for an evennia chat channel that connects +to people on other MUDs also using the IMC. Your evennia server do *not* have +to be open to the public to use IMC; it works as a stand-alone chat client. + +Copy and paste this section to your game/settings.py file and change the +values to fit your needs. + +Evennia's IMC2 client was developed against MudByte's network. You must +register your MUD on the network before you can use it, go to +http://www.mudbytes.net/imc2-intermud-join-network. + +Choose 'Other unsupported IMC2 version' from the choices and +and enter your information there. You have to enter the same +'short mud name', 'client password' and 'server password' as you +define in this file. + +The Evennia discussion channel is on server02.mudbytes.net:9000. +""" + +# Change to True if you want IMC active at all. +IMC2_ENABLED = False +# The hostname/ip address of your IMC2 server of choice. +IMC2_SERVER_ADDRESS = 'server02.mudbytes.net' +#IMC2_SERVER_ADDRESS = None +# The port to connect to on your IMC2 server. +IMC2_SERVER_PORT = 9000 +#IMC2_SERVER_PORT = None +# This is your game's IMC2 name on the network (e.g. "MyMUD"). +IMC2_MUDNAME = None +# Your IMC2 client-side password. Used to authenticate with your network. +IMC2_CLIENT_PW = None +# Your IMC2 server-side password. Used to verify your network's identity. +IMC2_SERVER_PW = None +# Emit additional debugging info to log. +IMC2_DEBUG = False +# This isn't something you should generally change. +IMC2_PROTOCOL_VERSION = '2' + +""" +IRC config. This allows your evennia channels to connect to an external IRC +channel. Evennia will connect under a nickname that then echoes what is +said on the channel to IRC and vice versa. +Obs - make sure the IRC network allows bots. +""" + +#Activate the IRC bot. +IRC_ENABLED = True +#Which IRC network (e.g. irc.freenode.net) +IRC_NETWORK = "irc.freenode.net" +#Which IRC port to connect to (e.g. 6667) +IRC_PORT = 6667 +#Which channel on the network to connect to (including the #) +IRC_CHANNEL = "" +#Under what nickname should Evennia connect to the channel +IRC_NICKNAME = "" + + + # Local time zone for this installation. All choices can be found here: # http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE TIME_ZONE = 'America/New_York' @@ -232,10 +259,11 @@ INSTALLED_APPS = ( 'src.objects', 'src.channels', 'src.imc2', + 'src.irc', 'src.helpsys', 'src.genperms', 'game.web.apps.news', - 'game.web.apps.website', + 'game.web.apps.website', ) """ @@ -270,6 +298,7 @@ COMMAND_MODULES = ( 'src.commands.privileged', 'src.commands.search', 'src.commands.imc2', + 'src.commands.irc', ) """ diff --git a/src/imc2/connection.py b/src/imc2/connection.py index 3757f05382..eef388d85d 100644 --- a/src/imc2/connection.py +++ b/src/imc2/connection.py @@ -124,7 +124,8 @@ class IMC2Protocol(StatefulTelnetProtocol): # Bombs away. for mapping in mappings: if mapping.channel: - comsys.send_cmessage(mapping.channel, message) + comsys.send_cmessage(mapping.channel, message, + from_external="IMC2") except IMC2ChannelMapping.DoesNotExist: # No channel mapping found for this message, ignore it. pass diff --git a/src/imc2/packets.py b/src/imc2/packets.py index f521fd2d5f..91c5fb149f 100644 --- a/src/imc2/packets.py +++ b/src/imc2/packets.py @@ -751,4 +751,4 @@ class IMC2PacketCloseNotify(IMC2Packet): if __name__ == "__main__": packstr = "Kayle@MW 1234567 MW!Server02!Server01 ice-msg-b *@* channel=Server01:ichat text=\"*they're going woot\" emote=0 echo=1" packstr = "*@Lythelian 1234567 Lythelian!Server01 is-alive *@* versionid=\"Tim's LPC IMC2 client 30-Jan-05 / Dead Souls integrated\" networkname=Mudbytes url=http://dead-souls.net host=70.32.76.142 port=6666 sha256=0" - print IMC2Packet(packstr) \ No newline at end of file + print IMC2Packet(packstr) diff --git a/src/irc/__init__.py b/src/irc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/irc/admin.py b/src/irc/admin.py new file mode 100644 index 0000000000..fcd485dd15 --- /dev/null +++ b/src/irc/admin.py @@ -0,0 +1,11 @@ +""" +This sets up a few fields in the admin interface for connecting IRC channels +to evennia channels. +""" +from src.irc.models import IRCChannelMapping +from django.contrib import admin + +class IRCChannelMappingAdmin(admin.ModelAdmin): + list_display = ('channel', 'irc_server_name', + 'irc_channel_name', 'is_enabled') +admin.site.register(IRCChannelMapping, IRCChannelMappingAdmin) diff --git a/src/irc/connection.py b/src/irc/connection.py new file mode 100644 index 0000000000..967c8e13fd --- /dev/null +++ b/src/irc/connection.py @@ -0,0 +1,83 @@ +""" +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.words.protocols import irc +from twisted.internet import protocol +from twisted.internet import reactor +from src.irc.models import IRCChannelMapping +from src import comsys +from src import logger + +#store all irc channels +IRC_CHANNELS = [] + +class IRC_Bot(irc.IRCClient): + + def _get_nickname(self): + "required for correct nickname setting" + return self.factory.nickname + nickname = property(_get_nickname) + + def signedOn(self): + global IRC_CHANNELS + self.join(self.factory.channel) + + # 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. + IRC_CHANNELS.append(self) + + logger.log_infomsg("IRC: Client connecting to %s.'" % (self.factory.channel)) + + def joined(self, channel): + logger.log_infomsg("Joined %s/%s as '%s'." % (self.factory.network,channel,self.factory.nickname)) + + def privmsg(self, user, irc_channel, msg): + "Someone has written something in channel. Echo it to the evennia channel" + + print "got msg: %s" % msg + try: + #find irc->evennia channel mappings + mappings = IRCChannelMapping.objects.filter(irc_channel_name=irc_channel) + if not mappings: + return + #format message: + user = user.split("!")[0] + if user: + user.strip() + msg = "%s@%s: %s" % (user,irc_channel,msg) + + logger.log_infomsg("IRC: " + msg) + +class IRC_BotFactory(protocol.ClientFactory): + protocol = IRC_Bot + def __init__(self, channel, network, nickname): + self.network = network + self.channel = channel + self.nickname = nickname + def clientConnectionLost(self, connector, reason): + logger.log_errmsg("IRC: Lost connection (%s), reconnecting." % reason) + connector.connect() + def clientConnectionFailed(self, connector, reason): + logger.log_errmsg("IRC: Could not connect: %s" % reason) + +def connect_to_IRC(irc_network,irc_port,irc_channel,irc_bot_nick ): + "Create the bot instance and connect to the IRC network and channel." + connect = reactor.connectTCP(irc_network, irc_port, + IRC_BotFactory(irc_channel,irc_network,irc_bot_nick)) + diff --git a/src/irc/models.py b/src/irc/models.py new file mode 100644 index 0000000000..98effd62ae --- /dev/null +++ b/src/irc/models.py @@ -0,0 +1,20 @@ +from django.db import models +from src.channels.models import CommChannel + +class IRCChannelMapping(models.Model): + """ + Each IRCChannelMapping object determines which in-game channel incoming + IRC messages are routed to. + """ + channel = models.ForeignKey(CommChannel) + irc_server_name = models.CharField(max_length=78) + irc_channel_name = models.CharField(max_length=78) + is_enabled = models.BooleanField(default=True) + + class Meta: + verbose_name = "IRC Channel mapping" + verbose_name_plural = "IRC Channel mappings" + + def __str__(self): + return "%s <-> %s (%s)" % (self.channel, self.irc_channel_name, + self.irc_server_name) diff --git a/src/server.py b/src/server.py index 4e839eaeb1..bb33ec538f 100755 --- a/src/server.py +++ b/src/server.py @@ -154,6 +154,15 @@ class EvenniaService(service.Service): svc.setServiceParent(self.service_collection) imc2_events.add_events() + if settings.IRC_ENABLED: + #Connect to the IRC network. + from src.irc.connection import connect_to_IRC + connect_to_IRC(settings.IRC_NETWORK, + settings.IRC_PORT, + settings.IRC_CHANNEL, + settings.IRC_NICKNAME) + + application = service.Application('Evennia') mud_service = EvenniaService() mud_service.start_services(application)