diff --git a/src/commands/default/cmdset_default.py b/src/commands/default/cmdset_default.py index 8f0ef062be..41ab8207c3 100644 --- a/src/commands/default/cmdset_default.py +++ b/src/commands/default/cmdset_default.py @@ -1,9 +1,11 @@ """ -This module ties together all the commands of the default command set. +This module ties together all the commands of the default command +set. Note that some commands, such as communication-commands are +instead put in the OOC cmdset. """ from src.commands.cmdset import CmdSet from src.commands.default import general, help, admin, system -from src.commands.default import comms, building +from src.commands.default import building from src.commands.default import batchprocess class DefaultCmdSet(CmdSet): diff --git a/src/commands/default/cmdset_ooc.py b/src/commands/default/cmdset_ooc.py index d6ca8439d2..cb5331351f 100644 --- a/src/commands/default/cmdset_ooc.py +++ b/src/commands/default/cmdset_ooc.py @@ -1,9 +1,8 @@ """ -This is the cmdset for OutOfCharacter (OOC) commands. -These are stored on the Player object and should -thus be able to handle getting a Player object -as caller rather than a Character. +This is the cmdset for OutOfCharacter (OOC) commands. These are +stored on the Player object and should thus be able to handle getting +a Player object as caller rather than a Character. """ from src.commands.cmdset import CmdSet @@ -53,3 +52,4 @@ class OOCCmdSet(CmdSet): self.add(comms.CmdIMC2Chan()) self.add(comms.CmdIMCInfo()) self.add(comms.CmdIMCTell()) + self.add(comms.CmdRSS2Chan()) diff --git a/src/commands/default/comms.py b/src/commands/default/comms.py index 303f91f95b..32e6bb437e 100644 --- a/src/commands/default/comms.py +++ b/src/commands/default/comms.py @@ -9,7 +9,7 @@ for easy handling. """ from django.conf import settings from src.comms.models import Channel, Msg, PlayerChannelConnection, ExternalChannelConnection -from src.comms import irc, imc2 +from src.comms import irc, imc2, rss from src.comms.channelhandler import CHANNELHANDLER from src.utils import create, utils from src.commands.default.muxcommand import MuxCommand @@ -1078,3 +1078,91 @@ class CmdIMCTell(MuxCommand): IMC2_CLIENT.msg_imc2(message, from_obj=self.caller, packet_type="imctell", data=data) self.caller.msg("You paged {c%s@%s{n (over IMC): '%s'." % (target, destination, message)) + + +# RSS connection +class CmdRSS2Chan(MuxCommand): + """ + @rss2chan - link evennia channel to an RSS feed + + Usage: + @rss2chan[/switches] = + + Switches: + /disconnect - this will stop the feed and remove the connection to the channel. + /remove - " + /list - show all rss->evennia mappings + + Example: + @rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic + + This creates an RSS reader that connects to a given RSS feed url. Updates will be + echoed as a title and news link to the given channel. The rate of updating is set + with the RSS_UPDATE_INTERVAL variable in settings (default is every 10 minutes). + + When disconnecting you need to supply both the channel and url again so as to identify + the connection uniquely. + """ + + key = "@rss2chan" + locks = "cmd:serversetting(RSS_ENABLED) and pperm(Immortals)" + help_category = "Comms" + + def func(self): + "Setup the rss-channel mapping" + + if not settings.RSS_ENABLED: + string = """RSS is not enabled. You need to activate it in game/settings.py.""" + self.caller.msg(string) + return + + if 'list' in self.switches: + # show all connections + connections = ExternalChannelConnection.objects.filter(db_external_key__startswith='rss_') + if connections: + cols = [["Evennia-channel"], ["RSS-url"]] + for conn in connections: + cols[0].append(conn.channel.key) + cols[1].append(conn.external_config.split('|')[0]) + ftable = utils.format_table(cols) + string = "" + for ir, row in enumerate(ftable): + if ir == 0: + string += "{w%s{n" % "".join(row) + else: + string += "\n" + "".join(row) + self.caller.msg(string) + else: + self.caller.msg("No connections found.") + return + + if not self.args or not self.rhs: + string = "Usage: @rss2chan[/switches] = " + self.caller.msg(string) + return + channel = self.lhs + url = self.rhs + + if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches: + chanmatch = find_channel(self.caller, channel, silent=True) + if chanmatch: + channel = chanmatch.key + + ok = rss.delete_connection(channel, url) + if not ok: + self.caller.msg("RSS connection/reader could not be removed, does it exist?") + else: + self.caller.msg("RSS connection destroyed.") + return + + channel = find_channel(self.caller, channel) + if not channel: + return + interval = settings.RSS_UPDATE_INTERVAL + if not interval: + interval = 10*60 + ok = rss.create_connection(channel, url, interval) + if not ok: + self.caller.msg("This RSS connection already exists.") + return + self.caller.msg("Connection created. Starting RSS reader.") diff --git a/src/comms/irc.py b/src/comms/irc.py index 4760fbfd36..f66798b8e7 100644 --- a/src/comms/irc.py +++ b/src/comms/irc.py @@ -174,8 +174,6 @@ def connect_to_irc(connection): def connect_all(): """ Activate all irc bots. - - Returns a list of (key, TCPClient) tuples for server to properly set services. """ for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_'): connect_to_irc(connection) diff --git a/src/comms/rss.py b/src/comms/rss.py new file mode 100644 index 0000000000..2a4e638c07 --- /dev/null +++ b/src/comms/rss.py @@ -0,0 +1,156 @@ +""" +RSS parser for Evennia + +This connects an RSS feed to an in-game Evennia channel, sending messages +to the channel whenever the feed updates. + +""" + +import re +from twisted.internet import task +from django.conf import settings +from src.comms.models import ExternalChannelConnection, Channel +from src.utils import logger, utils +from src.scripts.models import ScriptDB + +RSS_ENABLED = settings.RSS_ENABLED +RSS_UPDATE_INTERVAL = settings.RSS_UPDATE_INTERVAL +INFOCHANNEL = Channel.objects.channel_search(settings.CHANNEL_MUDINFO[0]) +RETAG = re.compile(r'<[^>]*?>') + +# holds rss readers they can be shut down at will. +RSS_READERS = {} + +def msg_info(message): + """ + Send info to default info channel + """ + message = '[%s][RSS]: %s' % (INFOCHANNEL[0].key, message) + try: + INFOCHANNEL[0].msg(message) + except AttributeError: + logger.log_infomsg("MUDinfo (rss): %s" % message) + +if RSS_ENABLED: + try: + import feedparser + except ImportError: + raise ImportError("RSS requirs python-feedparser to be installed. Install or set RSS_ENABLED=False.") + +class RSSReader(object): + """ + Reader script used to connect to each individual RSS feed + """ + def __init__(self, key, url, interval): + """ + The reader needs an rss url and It also needs an interval + for how often it is to check for new updates (defaults + to 10 minutes) + """ + self.key = key + self.url = url + self.interval = interval + self.entries = {} # stored feeds + self.task = None + # first we do is to load the feed so we don't resend + # old entries whenever the reader starts. + self.update_feed() + # start runner + self.start() + + def update_feed(self): + "Read the url for new updated data and determine what's new." + feed = feedparser.parse(self.url) + new = [] + for entry in (e for e in feed['entries'] if e['id'] not in self.entries): + txt = "[RSS] %s: %s" % (RETAG.sub("", entry['title']), entry['link'].replace('\n','').encode('utf-8')) + self.entries[entry['id']] = txt + new.append(txt) + return new + + def update(self): + """ + Called every self.interval seconds - tries to get new feed entries, + and if so, uses the appropriate ExternalChannelConnection to send the + data to subscribing channels. + """ + new = self.update_feed() + if not new: + return + conns = ExternalChannelConnection.objects.filter(db_external_key=self.key) + for conn in (conn for conn in conns if conn.channel): + for txt in new: + conn.to_channel("%s:%s" % (conn.channel.key, txt)) + + def start(self): + """ + Starting the update task and store a reference in the + global variable so it can be found and shut down later. + """ + global RSS_READERS + self.task = task.LoopingCall(self.update) + self.task.start(self.interval, now=False) + RSS_READERS[self.key] = self + +def build_connection_key(channel, url): + "This is used to id the connection" + if hasattr(channel, 'key'): + channel = channel.key + return "rss_%s>%s" % (url, channel) + +def create_connection(channel, url, interval): + """ + This will create a new RSS->channel connection + """ + if not type(channel) == Channel: + new_channel = Channel.objects.filter(db_key=channel) + if not new_channel: + logger.log_errmsg("Cannot attach RSS->Evennia: Evennia Channel '%s' not found." % channel) + return False + channel = new_channel[0] + key = build_connection_key(channel, url) + old_conns = ExternalChannelConnection.objects.filter(db_external_key=key) + if old_conns: + return False + config = "%s|%i" % (url, interval) + # There is no sendback from evennia to the rss, so we need not define any sendback code. + conn = ExternalChannelConnection(db_channel=channel, db_external_key=key, db_external_config=config) + conn.save() + + connect_to_rss(conn) + return True + +def delete_connection(channel, url): + """ + Delete rss connection between channel and url + """ + key = build_connection_key(channel, url) + try: + conn = ExternalChannelConnection.objects.get(db_external_key=key) + except Exception: + return False + conn.delete() + reader = RSS_READERS.get(key, None) + if reader and reader.task: + reader.task.stop() + return True + +def connect_to_rss(connection): + """ + Create the parser instance and connect to RSS feed and channel + """ + global RSS_READERS + key = utils.to_str(connection.external_key) + url, interval = [utils.to_str(conf) for conf in connection.external_config.split('|')] + # Create reader (this starts the running task and stores a reference in RSS_TASKS) + RSSReader(key, url, int(interval)) + +def connect_all(): + """ + Activate all rss feed parsers + """ + if not RSS_ENABLED: + return + for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith="rss_"): + print "connecting RSS: %s" % connection + connect_to_rss(connection) diff --git a/src/server/portal.py b/src/server/portal.py index 4642b7e135..0ecdb76457 100644 --- a/src/server/portal.py +++ b/src/server/portal.py @@ -54,8 +54,6 @@ SSL_ENABLED = settings.SSL_ENABLED and SSL_PORTS and SSL_INTERFACES SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED -IMC2_ENABLED = settings.IMC2_ENABLED -IRC_ENABLED = settings.IRC_ENABLED AMP_HOST = settings.AMP_HOST AMP_PORT = settings.AMP_PORT @@ -283,20 +281,6 @@ if WEBSERVER_ENABLED: webserver.setName('EvenniaWebServer%s' % pstring) PORTAL.services.addService(webserver) -if IRC_ENABLED: - - # IRC channel connections - - from src.comms import irc - irc.connect_all() - -if IMC2_ENABLED: - - # IMC2 channel connections - - from src.comms import imc2 - imc2.connect_all() - if os.name == 'nt': # Windows only: Set PID file manually f = open(os.path.join(settings.GAME_DIR, 'portal.pid'), 'w') diff --git a/src/server/server.py b/src/server/server.py index 18dc1f498c..2d4c352564 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -51,6 +51,10 @@ AMP_ENABLED = True AMP_HOST = settings.AMP_HOST AMP_PORT = settings.AMP_PORT +# server-channel mappings +IMC2_ENABLED = settings.IMC2_ENABLED +IRC_ENABLED = settings.IRC_ENABLED +RSS_ENABLED = settings.RSS_ENABLED #------------------------------------------------------------ # Evennia Main Server object @@ -258,6 +262,27 @@ if AMP_ENABLED: amp_service.setName("EvenniaPortal") EVENNIA.services.addService(amp_service) + +if IRC_ENABLED: + + # IRC channel connections + + from src.comms import irc + irc.connect_all() + +if IMC2_ENABLED: + + # IMC2 channel connections + + from src.comms import imc2 + imc2.connect_all() + +if RSS_ENABLED: + + # RSS feed channel connections + from src.comms import rss + rss.connect_all() + # clear server startup mode ServerConfig.objects.conf("server_starting_mode", delete=True) diff --git a/src/settings_default.py b/src/settings_default.py index 33a754c738..31939aeff6 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -307,6 +307,14 @@ IMC2_NETWORK = "server01.mudbytes.net" IMC2_PORT = 5000 IMC2_CLIENT_PWD = "" IMC2_SERVER_PWD = "" +# RSS allows to connect RSS feeds (from forum updates, blogs etc) to +# an in-game channel. The channel will be updated when the rss feed +# updates. Use @rss2chan in game to connect if this setting is +# active. OBS: RSS support requires the python-feedparser package to +# be installed (through package manager or from the website +# http://code.google.com/p/feedparser/) +RSS_ENABLED=False +RSS_UPDATE_INTERVAL = 60*10 # 10 minutes ################################################### # Config for Django web features