Added RSS feed support to Evennia. This uses the @rss2chan command to tie a feed to an in-game channel. Updates to the feed will henceforth be echoed to the channel. The reader requires the python-feedreader package to be installed.

This commit is contained in:
Griatch 2012-01-28 20:12:59 +01:00
parent b2d7f37e9c
commit cdab5a240b
8 changed files with 286 additions and 25 deletions

View file

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

View file

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

View file

@ -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] <evennia_channel> = <rss_url>
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] <evennia_channel> = <rss url>"
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.")

View file

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

156
src/comms/rss.py Normal file
View file

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

View file

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

View file

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

View file

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