Commits

Mike Crute  committed 9dd8173

MBH Ticket 7 and MBH Ticket 8: Adding support for RSS and Wordpress style Urls

  • Participants
  • Parent commits 6a61694

Comments (0)

Files changed (4)

File zine/feeds.py

+"""
+Syndication feed generation library -- used for generating RSS, etc.
+Implementation lifted from Django and modified to remove Djano
+dependencies.
+
+For definitions of the different versions of RSS, see:
+http://diveintomark.org/archives/2004/02/04/incompatible-rss
+"""
+
+import types
+import datetime
+import urllib
+import urlparse
+from decimal import Decimal
+from xml.sax.saxutils import XMLGenerator
+
+
+def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'):
+    """
+    Returns a bytestring version of 's', encoded as specified in 'encoding'.
+
+    If strings_only is True, don't convert (some) non-string-like objects.
+    """
+    if strings_only and isinstance(s, (types.NoneType, int)):
+        return s
+    if not isinstance(s, basestring):
+        try:
+            return str(s)
+        except UnicodeEncodeError:
+            if isinstance(s, Exception):
+                # An Exception subclass containing non-ASCII data that doesn't
+                # know how to print itself properly. We shouldn't raise a
+                # further exception.
+                return ' '.join([smart_str(arg, encoding, strings_only,
+                        errors) for arg in s])
+            return unicode(s).encode(encoding, errors)
+    elif isinstance(s, unicode):
+        return s.encode(encoding, errors)
+    elif s and encoding != 'utf-8':
+        return s.decode('utf-8', errors).encode(encoding, errors)
+    else:
+        return s
+
+
+def is_protected_type(obj):
+    """Determine if the object instance is of a protected type.
+
+    Objects of protected types are preserved as-is when passed to
+    force_unicode(strings_only=True).
+    """
+    return isinstance(obj, (
+        types.NoneType,
+        int, long,
+        datetime.datetime, datetime.date, datetime.time,
+        float, Decimal)
+    )
+
+
+def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'):
+    """
+    Similar to smart_unicode, except that lazy instances are resolved to
+    strings, rather than kept as lazy objects.
+
+    If strings_only is True, don't convert (some) non-string-like objects.
+    """
+    if strings_only and is_protected_type(s):
+        return s
+    try:
+        if not isinstance(s, basestring,):
+            if hasattr(s, '__unicode__'):
+                s = unicode(s)
+            else:
+                try:
+                    s = unicode(str(s), encoding, errors)
+                except UnicodeEncodeError:
+                    if not isinstance(s, Exception):
+                        raise
+                    # If we get to here, the caller has passed in an Exception
+                    # subclass populated with non-ASCII data without special
+                    # handling to display as a string. We need to handle this
+                    # without raising a further exception. We do an
+                    # approximation to what the Exception's standard str()
+                    # output should be.
+                    s = ' '.join([force_unicode(arg, encoding, strings_only,
+                            errors) for arg in s])
+        elif not isinstance(s, unicode):
+            # Note: We use .decode() here, instead of unicode(s, encoding,
+            # errors), so that if s is a SafeString, it ends up being a
+            # SafeUnicode at the end.
+            s = s.decode(encoding, errors)
+    except UnicodeDecodeError, e:
+        if not isinstance(s, Exception):
+            raise
+        else:
+            # If we get to here, the caller has passed in an Exception
+            # subclass populated with non-ASCII bytestring data without a
+            # working unicode method. Try to handle this without raising a
+            # further exception by individually forcing the exception args
+            # to unicode.
+            s = ' '.join([force_unicode(arg, encoding, strings_only,
+                    errors) for arg in s])
+    return s
+
+def iri_to_uri(iri):
+    """
+    Convert an Internationalized Resource Identifier (IRI) portion to a URI
+    portion that is suitable for inclusion in a URL.
+
+    This is the algorithm from section 3.1 of RFC 3987.  However, since we are
+    assuming input is either UTF-8 or unicode already, we can simplify things a
+    little from the full method.
+
+    Returns an ASCII string containing the encoded result.
+    """
+    # The list of safe characters here is constructed from the "reserved" and
+    # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986:
+    #     reserved    = gen-delims / sub-delims
+    #     gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+    #     sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
+    #                   / "*" / "+" / "," / ";" / "="
+    #     unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
+    # Of the unreserved characters, urllib.quote already considers all but
+    # the ~ safe.
+    # The % character is also added to the list of safe characters here, as the
+    # end of section 3.1 of RFC 3987 specifically mentions that % must not be
+    # converted.
+    if iri is None:
+        return iri
+    return urllib.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~")
+
+
+class SimplerXMLGenerator(XMLGenerator):
+
+    def addQuickElement(self, name, contents=None, attrs=None):
+        "Convenience method for adding an element with no children"
+        if attrs is None: attrs = {}
+        self.startElement(name, attrs)
+        if contents is not None:
+            self.characters(contents)
+        self.endElement(name)
+
+
+def rfc2822_date(date):
+    # We do this ourselves to be timezone aware, email.Utils is not tz aware.
+    if date.tzinfo:
+        time_str = date.strftime('%a, %d %b %Y %H:%M:%S ')
+        offset = date.tzinfo.utcoffset(date)
+        timezone = (offset.days * 24 * 60) + (offset.seconds / 60)
+        hour, minute = divmod(timezone, 60)
+        return time_str + "%+03d%02d" % (hour, minute)
+    else:
+        return date.strftime('%a, %d %b %Y %H:%M:%S -0000')
+
+def rfc3339_date(date):
+    if date.tzinfo:
+        time_str = date.strftime('%Y-%m-%dT%H:%M:%S')
+        offset = date.tzinfo.utcoffset(date)
+        timezone = (offset.days * 24 * 60) + (offset.seconds / 60)
+        hour, minute = divmod(timezone, 60)
+        return time_str + "%+03d:%02d" % (hour, minute)
+    else:
+        return date.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+def get_tag_uri(url, date):
+    """
+    Creates a TagURI.
+
+    See http://diveintomark.org/archives/2004/05/28/howto-atom-id
+    """
+    url_split = urlparse.urlparse(url)
+
+    # Python 2.4 didn't have named attributes on split results or the hostname.
+    hostname = getattr(url_split, 'hostname', url_split[1].split(':')[0])
+    path = url_split[2]
+    fragment = url_split[5]
+
+    d = ''
+    if date is not None:
+        d = ',%s' % date.strftime('%Y-%m-%d')
+    return u'tag:%s%s:%s/%s' % (hostname, d, path, fragment)
+
+class SyndicationFeed(object):
+    "Base class for all syndication feeds. Subclasses should provide write()"
+    def __init__(self, title, link, description, language=None, author_email=None,
+            author_name=None, author_link=None, subtitle=None, categories=None,
+            feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs):
+        to_unicode = lambda s: force_unicode(s, strings_only=True)
+        if categories:
+            categories = [force_unicode(c) for c in categories]
+        if ttl is not None:
+            # Force ints to unicode
+            ttl = force_unicode(ttl)
+        self.feed = {
+            'title': to_unicode(title),
+            'link': iri_to_uri(link),
+            'description': to_unicode(description),
+            'language': to_unicode(language),
+            'author_email': to_unicode(author_email),
+            'author_name': to_unicode(author_name),
+            'author_link': iri_to_uri(author_link),
+            'subtitle': to_unicode(subtitle),
+            'categories': categories or (),
+            'feed_url': iri_to_uri(feed_url),
+            'feed_copyright': to_unicode(feed_copyright),
+            'id': feed_guid or link,
+            'ttl': ttl,
+        }
+        self.feed.update(kwargs)
+        self.items = []
+
+    def add_item(self, title, link, description, author_email=None,
+        author_name=None, author_link=None, pubdate=None, comments=None,
+        unique_id=None, enclosure=None, categories=(), item_copyright=None,
+        ttl=None, **kwargs):
+        """
+        Adds an item to the feed. All args are expected to be Python Unicode
+        objects except pubdate, which is a datetime.datetime object, and
+        enclosure, which is an instance of the Enclosure class.
+        """
+        to_unicode = lambda s: force_unicode(s, strings_only=True)
+        if categories:
+            categories = [to_unicode(c) for c in categories]
+        if ttl is not None:
+            # Force ints to unicode
+            ttl = force_unicode(ttl)
+        item = {
+            'title': to_unicode(title),
+            'link': iri_to_uri(link),
+            'description': to_unicode(description),
+            'author_email': to_unicode(author_email),
+            'author_name': to_unicode(author_name),
+            'author_link': iri_to_uri(author_link),
+            'pubdate': pubdate,
+            'comments': to_unicode(comments),
+            'unique_id': to_unicode(unique_id),
+            'enclosure': enclosure,
+            'categories': categories or (),
+            'item_copyright': to_unicode(item_copyright),
+            'ttl': ttl,
+        }
+        item.update(kwargs)
+        self.items.append(item)
+
+    def num_items(self):
+        return len(self.items)
+
+    def root_attributes(self):
+        """
+        Return extra attributes to place on the root (i.e. feed/channel) element.
+        Called from write().
+        """
+        return {}
+
+    def add_root_elements(self, handler):
+        """
+        Add elements in the root (i.e. feed/channel) element. Called
+        from write().
+        """
+        pass
+
+    def item_attributes(self, item):
+        """
+        Return extra attributes to place on each item (i.e. item/entry) element.
+        """
+        return {}
+
+    def add_item_elements(self, handler, item):
+        """
+        Add elements on each item (i.e. item/entry) element.
+        """
+        pass
+
+    def write(self, outfile, encoding):
+        """
+        Outputs the feed in the given encoding to outfile, which is a file-like
+        object. Subclasses should override this.
+        """
+        raise NotImplementedError
+
+    def writeString(self, encoding):
+        """
+        Returns the feed in the given encoding as a string.
+        """
+        from StringIO import StringIO
+        s = StringIO()
+        self.write(s, encoding)
+        return s.getvalue()
+
+    def latest_post_date(self):
+        """
+        Returns the latest item's pubdate. If none of them have a pubdate,
+        this returns the current date/time.
+        """
+        updates = [i['pubdate'] for i in self.items if i['pubdate'] is not None]
+        if len(updates) > 0:
+            updates.sort()
+            return updates[-1]
+        else:
+            return datetime.datetime.now()
+
+class Enclosure(object):
+    "Represents an RSS enclosure"
+    def __init__(self, url, length, mime_type):
+        "All args are expected to be Python Unicode objects"
+        self.length, self.mime_type = length, mime_type
+        self.url = iri_to_uri(url)
+
+class RssFeed(SyndicationFeed):
+    mime_type = 'application/rss+xml'
+    def write(self, outfile, encoding):
+        handler = SimplerXMLGenerator(outfile, encoding)
+        handler.startDocument()
+        handler.startElement(u"rss", self.rss_attributes())
+        handler.startElement(u"channel", self.root_attributes())
+        self.add_root_elements(handler)
+        self.write_items(handler)
+        self.endChannelElement(handler)
+        handler.endElement(u"rss")
+
+    def rss_attributes(self):
+        return {u"version": self._version,
+                u"xmlns:atom": u"http://www.w3.org/2005/Atom"}
+
+    def write_items(self, handler):
+        for item in self.items:
+            handler.startElement(u'item', self.item_attributes(item))
+            self.add_item_elements(handler, item)
+            handler.endElement(u"item")
+
+    def add_root_elements(self, handler):
+        handler.addQuickElement(u"title", self.feed['title'])
+        handler.addQuickElement(u"link", self.feed['link'])
+        handler.addQuickElement(u"description", self.feed['description'])
+        handler.addQuickElement(u"atom:link", None, {u"rel": u"self", u"href": self.feed['feed_url']})
+        if self.feed['language'] is not None:
+            handler.addQuickElement(u"language", self.feed['language'])
+        for cat in self.feed['categories']:
+            handler.addQuickElement(u"category", cat)
+        if self.feed['feed_copyright'] is not None:
+            handler.addQuickElement(u"copyright", self.feed['feed_copyright'])
+        handler.addQuickElement(u"lastBuildDate", rfc2822_date(self.latest_post_date()).decode('utf-8'))
+        if self.feed['ttl'] is not None:
+            handler.addQuickElement(u"ttl", self.feed['ttl'])
+
+    def endChannelElement(self, handler):
+        handler.endElement(u"channel")
+
+class RssUserland091Feed(RssFeed):
+    _version = u"0.91"
+    def add_item_elements(self, handler, item):
+        handler.addQuickElement(u"title", item['title'])
+        handler.addQuickElement(u"link", item['link'])
+        if item['description'] is not None:
+            handler.addQuickElement(u"description", item['description'])
+
+class Rss201rev2Feed(RssFeed):
+    # Spec: http://blogs.law.harvard.edu/tech/rss
+    _version = u"2.0"
+    def add_item_elements(self, handler, item):
+        handler.addQuickElement(u"title", item['title'])
+        handler.addQuickElement(u"link", item['link'])
+        if item['description'] is not None:
+            handler.addQuickElement(u"description", item['description'])
+
+        # Author information.
+        if item["author_name"] and item["author_email"]:
+            handler.addQuickElement(u"author", "%s (%s)" % \
+                (item['author_email'], item['author_name']))
+        elif item["author_email"]:
+            handler.addQuickElement(u"author", item["author_email"])
+        elif item["author_name"]:
+            handler.addQuickElement(u"dc:creator", item["author_name"], {u"xmlns:dc": u"http://purl.org/dc/elements/1.1/"})
+
+        if item['pubdate'] is not None:
+            handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8'))
+        if item['comments'] is not None:
+            handler.addQuickElement(u"comments", item['comments'])
+        if item['unique_id'] is not None:
+            handler.addQuickElement(u"guid", item['unique_id'])
+        if item['ttl'] is not None:
+            handler.addQuickElement(u"ttl", item['ttl'])
+
+        # Enclosure.
+        if item['enclosure'] is not None:
+            handler.addQuickElement(u"enclosure", '',
+                {u"url": item['enclosure'].url, u"length": item['enclosure'].length,
+                    u"type": item['enclosure'].mime_type})
+
+        # Categories.
+        for cat in item['categories']:
+            handler.addQuickElement(u"category", cat)
+
+class Atom1Feed(SyndicationFeed):
+    # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html
+    mime_type = 'application/atom+xml'
+    ns = u"http://www.w3.org/2005/Atom"
+
+    def write(self, outfile, encoding):
+        handler = SimplerXMLGenerator(outfile, encoding)
+        handler.startDocument()
+        handler.startElement(u'feed', self.root_attributes())
+        self.add_root_elements(handler)
+        self.write_items(handler)
+        handler.endElement(u"feed")
+
+    def root_attributes(self):
+        if self.feed['language'] is not None:
+            return {u"xmlns": self.ns, u"xml:lang": self.feed['language']}
+        else:
+            return {u"xmlns": self.ns}
+
+    def add_root_elements(self, handler):
+        handler.addQuickElement(u"title", self.feed['title'])
+        handler.addQuickElement(u"link", "", {u"rel": u"alternate", u"href": self.feed['link']})
+        if self.feed['feed_url'] is not None:
+            handler.addQuickElement(u"link", "", {u"rel": u"self", u"href": self.feed['feed_url']})
+        handler.addQuickElement(u"id", self.feed['id'])
+        handler.addQuickElement(u"updated", rfc3339_date(self.latest_post_date()).decode('utf-8'))
+        if self.feed['author_name'] is not None:
+            handler.startElement(u"author", {})
+            handler.addQuickElement(u"name", self.feed['author_name'])
+            if self.feed['author_email'] is not None:
+                handler.addQuickElement(u"email", self.feed['author_email'])
+            if self.feed['author_link'] is not None:
+                handler.addQuickElement(u"uri", self.feed['author_link'])
+            handler.endElement(u"author")
+        if self.feed['subtitle'] is not None:
+            handler.addQuickElement(u"subtitle", self.feed['subtitle'])
+        for cat in self.feed['categories']:
+            handler.addQuickElement(u"category", "", {u"term": cat})
+        if self.feed['feed_copyright'] is not None:
+            handler.addQuickElement(u"rights", self.feed['feed_copyright'])
+
+    def write_items(self, handler):
+        for item in self.items:
+            handler.startElement(u"entry", self.item_attributes(item))
+            self.add_item_elements(handler, item)
+            handler.endElement(u"entry")
+
+    def add_item_elements(self, handler, item):
+        handler.addQuickElement(u"title", item['title'])
+        handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"alternate"})
+        if item['pubdate'] is not None:
+            handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8'))
+
+        # Author information.
+        if item['author_name'] is not None:
+            handler.startElement(u"author", {})
+            handler.addQuickElement(u"name", item['author_name'])
+            if item['author_email'] is not None:
+                handler.addQuickElement(u"email", item['author_email'])
+            if item['author_link'] is not None:
+                handler.addQuickElement(u"uri", item['author_link'])
+            handler.endElement(u"author")
+
+        # Unique ID.
+        if item['unique_id'] is not None:
+            unique_id = item['unique_id']
+        else:
+            unique_id = get_tag_uri(item['link'], item['pubdate'])
+        handler.addQuickElement(u"id", unique_id)
+
+        # Summary.
+        if item['description'] is not None:
+            handler.addQuickElement(u"summary", item['description'], {u"type": u"html"})
+
+        # Enclosure.
+        if item['enclosure'] is not None:
+            handler.addQuickElement(u"link", '',
+                {u"rel": u"enclosure",
+                 u"href": item['enclosure'].url,
+                 u"length": item['enclosure'].length,
+                 u"type": item['enclosure'].mime_type})
+
+        # Categories.
+        for cat in item['categories']:
+            handler.addQuickElement(u"category", u"", {u"term": cat})
+
+        # Rights.
+        if item['item_copyright'] is not None:
+            handler.addQuickElement(u"rights", item['item_copyright'])
+
+# This isolates the decision of what the system default is, so calling code can
+# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed".
+DefaultFeed = Rss201rev2Feed

File zine/urls.py

     blog_urls = [
         Rule('/', defaults={'page': 1}, endpoint='blog/index'),
         Rule('/feed.atom', endpoint='blog/atom_feed'),
+        Submount('/feed', [
+            Rule('/', endpoint='blog/rss_feed'),
+            Rule('/rss', endpoint='blog/rss_feed'),
+            Rule('/rss2', endpoint='blog/rss_feed'),
+            Rule('/atom', endpoint='blog/atom_feed'),
+        ]),
         Rule('/page/<int:page>', endpoint='blog/index'),
         Rule('/archive', endpoint='blog/archive'),
         Submount(app.cfg['profiles_url_prefix'], [
             Rule('/<string:username>', defaults={'page': 1}, endpoint='blog/show_author'),
             Rule('/<string:username>/page/<int:page>', endpoint='blog/show_author'),
             Rule('/<string:author>/feed.atom', endpoint='blog/atom_feed'),
+            Submount('/<string:author>/feed', [
+                Rule('/', endpoint='blog/rss_feed'),
+                Rule('/rss', endpoint='blog/rss_feed'),
+                Rule('/rss2', endpoint='blog/rss_feed'),
+                Rule('/atom', endpoint='blog/atom_feed'),
+            ]),
         ]),
         Submount(app.cfg['category_url_prefix'], [
             Rule('/<string:slug>', defaults={'page': 1}, endpoint='blog/show_category'),
             Rule('/<string:slug>/page/<int:page>', endpoint='blog/show_category'),
-            Rule('/<string:category>/feed.atom', endpoint='blog/atom_feed')
+            Rule('/<string:category>/feed.atom', endpoint='blog/atom_feed'),
+            Submount('/<string:category>/feed', [
+                Rule('/', endpoint='blog/rss_feed'),
+                Rule('/rss', endpoint='blog/rss_feed'),
+                Rule('/rss2', endpoint='blog/rss_feed'),
+                Rule('/atom', endpoint='blog/atom_feed'),
+            ]),
         ]),
         Submount(app.cfg['tags_url_prefix'], [
             Rule('/', endpoint='blog/tags'),
             Rule('/<string:slug>', defaults={'page': 1}, endpoint='blog/show_tag'),
             Rule('/<string:slug>/page/<int:page>', endpoint='blog/show_tag'),
-            Rule('/<string:tag>/feed.atom', endpoint='blog/atom_feed')
+            Rule('/<string:tag>/feed.atom', endpoint='blog/atom_feed'),
+            Submount('/<string:tag>/feed', [
+                Rule('/', endpoint='blog/rss_feed'),
+                Rule('/rss', endpoint='blog/rss_feed'),
+                Rule('/rss2', endpoint='blog/rss_feed'),
+                Rule('/atom', endpoint='blog/atom_feed'),
+            ]),
         ]),
         Submount(app.cfg['account_url_prefix'], [
             Rule('/', endpoint='account/index'),

File zine/views/__init__.py

     'blog/json_service':        blog.json_service,
     'blog/xml_service':         blog.xml_service,
     'blog/atom_feed':           blog.atom_feed,
+    'blog/rss_feed':            blog.rss_feed,
     'blog/serve_translations':  i18n.serve_javascript,
 
     # account views

File zine/views/blog.py

 from zine.models import Post, Category, User, Tag
 from zine.utils import dump_json, log
 from zine.utils.text import build_tag_uri
-from zine.utils.xml import generate_rsd, dump_xml, AtomFeed
+from zine.utils.xml import generate_rsd, dump_xml
 from zine.utils.http import redirect_to, redirect
 from zine.utils.redirects import lookup_redirect
 from zine.forms import NewCommentForm
+from zine.feeds import Rss201rev2Feed as RssFeed, Atom1Feed
 from werkzeug.exceptions import NotFound, Forbidden
 
 
 @cache.response(vary=('user',))
 def atom_feed(req, author=None, year=None, month=None, day=None,
               category=None, tag=None, post=None):
+    feed = Atom1Feed(req.app.cfg['blog_title'], req.app.cfg['blog_url'],
+                    "", # Description not supported
+                    subtitle=req.app.cfg['blog_tagline'], feed_url=req.url)
+
+    results = populate_feed(req, feed, author, year, month, day, category,
+                         tag, post)
+
+    return Response(results, mimetype="application/atom+xml")
+
+
+@cache.response(vary=('user',))
+def rss_feed(req, author=None, year=None, month=None, day=None,
+              category=None, tag=None, post=None):
+    feed = RssFeed(req.app.cfg['blog_title'], req.app.cfg['blog_url'],
+                    "", # Description not supported
+                    subtitle=req.app.cfg['blog_tagline'], feed_url=req.url)
+
+    results = populate_feed(req, feed, author, year, month, day, category,
+                         tag, post)
+
+    return Response(results, mimetype="application/rss+xml")
+
+
+def populate_feed(req, feed, author=None, year=None, month=None, day=None,
+              category=None, tag=None, post=None):
     """Renders an atom feed requested.
 
     :URL endpoint: ``blog/atom_feed``
     """
-    feed = AtomFeed(req.app.cfg['blog_title'], feed_url=req.url,
-                    url=req.app.cfg['blog_url'],
-                    subtitle=req.app.cfg['blog_tagline'])
-
     # the feed only contains published items
     query = Post.query.lightweight(lazy=('comments',)).published()
 
     if post is None:
         for post in query.for_index().order_by(Post.pub_date.desc()) \
                          .limit(15).all():
-            links = [link.as_dict() for link in post.links]
-            feed.add(post.title or '%s @ %s' % (post.author.display_name,
-                     post.pub_date), unicode(post.body), content_type='html',
-                     author=post.author.display_name, links=links,
-                     url=url_for(post, _external=True), id=post.uid,
-                     updated=post.last_update, published=post.pub_date)
+            alt_title = '%s @ %s' % (post.author.display_name, post.pub_date)
+            feed.add_item(post.title or alt_title,
+                          url_for(post, _external=True), unicode(post.body),
+                          author_name=post.author.display_name,
+                          pubdate=post.pub_date, unique_id=post.uid)
 
     # otherwise we create a feed for all the comments of a post.
     # the function is called this way by `dispatch_content_type`.
             author = {'name': comment.author}
             if comment.www:
                 author['uri'] = comment.www
-            feed.add(title, unicode(comment.body), content_type='html',
-                     author=author, url=url_for(comment, _external=True),
-                     id=uid, updated=comment.pub_date)
+            feed.add_item(title, url_for(comment, _external=True),
+                          unicode(comment.body), author_name=author,
+                          pubdate=comment.pub_date, unique_id=uid)
             comment_num += 1
 
-    return feed.get_response()
+    return feed.writeString('utf-8')
 
 
 @cache.response(vary=('user',))