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:
parent
b2d7f37e9c
commit
cdab5a240b
8 changed files with 286 additions and 25 deletions
|
|
@ -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.cmdset import CmdSet
|
||||||
from src.commands.default import general, help, admin, system
|
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
|
from src.commands.default import batchprocess
|
||||||
|
|
||||||
class DefaultCmdSet(CmdSet):
|
class DefaultCmdSet(CmdSet):
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
"""
|
"""
|
||||||
|
|
||||||
This is the cmdset for OutOfCharacter (OOC) commands.
|
This is the cmdset for OutOfCharacter (OOC) commands. These are
|
||||||
These are stored on the Player object and should
|
stored on the Player object and should thus be able to handle getting
|
||||||
thus be able to handle getting a Player object
|
a Player object as caller rather than a Character.
|
||||||
as caller rather than a Character.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from src.commands.cmdset import CmdSet
|
from src.commands.cmdset import CmdSet
|
||||||
|
|
@ -53,3 +52,4 @@ class OOCCmdSet(CmdSet):
|
||||||
self.add(comms.CmdIMC2Chan())
|
self.add(comms.CmdIMC2Chan())
|
||||||
self.add(comms.CmdIMCInfo())
|
self.add(comms.CmdIMCInfo())
|
||||||
self.add(comms.CmdIMCTell())
|
self.add(comms.CmdIMCTell())
|
||||||
|
self.add(comms.CmdRSS2Chan())
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ for easy handling.
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from src.comms.models import Channel, Msg, PlayerChannelConnection, ExternalChannelConnection
|
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.comms.channelhandler import CHANNELHANDLER
|
||||||
from src.utils import create, utils
|
from src.utils import create, utils
|
||||||
from src.commands.default.muxcommand import MuxCommand
|
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)
|
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))
|
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.")
|
||||||
|
|
|
||||||
|
|
@ -174,8 +174,6 @@ def connect_to_irc(connection):
|
||||||
def connect_all():
|
def connect_all():
|
||||||
"""
|
"""
|
||||||
Activate all irc bots.
|
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_'):
|
for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_'):
|
||||||
connect_to_irc(connection)
|
connect_to_irc(connection)
|
||||||
|
|
|
||||||
156
src/comms/rss.py
Normal file
156
src/comms/rss.py
Normal 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)
|
||||||
|
|
@ -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
|
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS and SSH_INTERFACES
|
||||||
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
|
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS and WEBSERVER_INTERFACES
|
||||||
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||||
IMC2_ENABLED = settings.IMC2_ENABLED
|
|
||||||
IRC_ENABLED = settings.IRC_ENABLED
|
|
||||||
|
|
||||||
AMP_HOST = settings.AMP_HOST
|
AMP_HOST = settings.AMP_HOST
|
||||||
AMP_PORT = settings.AMP_PORT
|
AMP_PORT = settings.AMP_PORT
|
||||||
|
|
@ -283,20 +281,6 @@ if WEBSERVER_ENABLED:
|
||||||
webserver.setName('EvenniaWebServer%s' % pstring)
|
webserver.setName('EvenniaWebServer%s' % pstring)
|
||||||
PORTAL.services.addService(webserver)
|
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':
|
if os.name == 'nt':
|
||||||
# Windows only: Set PID file manually
|
# Windows only: Set PID file manually
|
||||||
f = open(os.path.join(settings.GAME_DIR, 'portal.pid'), 'w')
|
f = open(os.path.join(settings.GAME_DIR, 'portal.pid'), 'w')
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,10 @@ AMP_ENABLED = True
|
||||||
AMP_HOST = settings.AMP_HOST
|
AMP_HOST = settings.AMP_HOST
|
||||||
AMP_PORT = settings.AMP_PORT
|
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
|
# Evennia Main Server object
|
||||||
|
|
@ -258,6 +262,27 @@ if AMP_ENABLED:
|
||||||
amp_service.setName("EvenniaPortal")
|
amp_service.setName("EvenniaPortal")
|
||||||
EVENNIA.services.addService(amp_service)
|
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
|
# clear server startup mode
|
||||||
ServerConfig.objects.conf("server_starting_mode", delete=True)
|
ServerConfig.objects.conf("server_starting_mode", delete=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,14 @@ IMC2_NETWORK = "server01.mudbytes.net"
|
||||||
IMC2_PORT = 5000
|
IMC2_PORT = 5000
|
||||||
IMC2_CLIENT_PWD = ""
|
IMC2_CLIENT_PWD = ""
|
||||||
IMC2_SERVER_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
|
# Config for Django web features
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue