|
| 1 | +#!/usr/bin/python |
| 2 | + |
| 3 | +# |
| 4 | +# RSS writer Roundup reactor |
| 5 | +# Mark Paschal <[email protected]> |
| 6 | +# |
| 7 | + |
| 8 | +import os |
| 9 | + |
| 10 | +import logging |
| 11 | +logger = logging.getLogger('detector') |
| 12 | + |
| 13 | +import sys |
| 14 | + |
| 15 | +# How many <item>s to have in the feed, at most. |
| 16 | +MAX_ITEMS = 30 |
| 17 | + |
| 18 | +# |
| 19 | +# Module metadata |
| 20 | +# |
| 21 | + |
| 22 | +__author__ = "Mark Paschal <[email protected]>" |
| 23 | +__copyright__ = "Copyright 2003 Mark Paschal" |
| 24 | +__version__ = "1.2" |
| 25 | + |
| 26 | +__changes__ = """ |
| 27 | +1.1 29 Aug 2003 Produces valid pubDates. Produces pubDates and authors for |
| 28 | + change notes. Consolidates a message and change note into one |
| 29 | + item. Uses TRACKER_NAME in filename to produce one feed per |
| 30 | + tracker. Keeps to MAX_ITEMS limit more efficiently. |
| 31 | +1.2 5 Sep 2003 Fixes bug with programmatically submitted issues having |
| 32 | + messages without summaries (?!). |
| 33 | +x.x 26 Feb 2017 John Rouillard try to deal with truncation of rss |
| 34 | + file cause by error in parsing 8'bit characcters in |
| 35 | + input message. Further attempts to fix issue by |
| 36 | + modifying message bail on 0 length rss file. Delete |
| 37 | + it and retry. |
| 38 | +""" |
| 39 | + |
| 40 | +__license__ = 'MIT' |
| 41 | + |
| 42 | +# |
| 43 | +# Copyright 2003 Mark Paschal |
| 44 | +# |
| 45 | +# Permission is hereby granted, free of charge, to any person obtaining a copy |
| 46 | +# of this software and associated documentation files (the "Software"), to deal |
| 47 | +# in the Software without restriction, including without limitation the rights |
| 48 | +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 49 | +# copies of the Software, and to permit persons to whom the Software is |
| 50 | +# furnished to do so, subject to the following conditions: |
| 51 | +# |
| 52 | +# The above copyright notice and this permission notice shall be included in all |
| 53 | +# copies or substantial portions of the Software. |
| 54 | +# |
| 55 | +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 56 | +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 57 | +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 58 | +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 59 | +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 60 | +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 61 | +# SOFTWARE. |
| 62 | +# |
| 63 | + |
| 64 | + |
| 65 | +# The strftime format to use for <pubDate>s. |
| 66 | +RSS20_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S %z' |
| 67 | + |
| 68 | + |
| 69 | +def newRss(title, link, description): |
| 70 | + """Returns an XML Document containing an RSS 2.0 feed with no items.""" |
| 71 | + import xml.dom.minidom |
| 72 | + rss = xml.dom.minidom.Document() |
| 73 | + |
| 74 | + root = rss.appendChild(rss.createElement("rss")) |
| 75 | + root.setAttribute("version", "2.0") |
| 76 | + root.setAttribute("xmlns:atom","http://www.w3.org/2005/Atom") |
| 77 | + |
| 78 | + channel = root.appendChild(rss.createElement("channel")) |
| 79 | + addEl = lambda tag,value: channel.appendChild(rss.createElement(tag)).appendChild(rss.createTextNode(value)) |
| 80 | + def addElA(tag,attr): |
| 81 | + node=rss.createElement(tag) |
| 82 | + for attr, val in attr.items(): |
| 83 | + node.setAttribute(attr, val) |
| 84 | + channel.appendChild(node) |
| 85 | + |
| 86 | + addEl("title", title) |
| 87 | + addElA('atom:link', attr={"rel": "self", |
| 88 | + "type": "application/rss+xml", "href": link + "@@file/rss.xml"}) |
| 89 | + addEl("link", link) |
| 90 | + addEl("description", description) |
| 91 | + |
| 92 | + return rss # has no items |
| 93 | + |
| 94 | + |
| 95 | +def writeRss(db, cl, nodeid, olddata): |
| 96 | + """ |
| 97 | + Reacts to a created or changed issue. Puts new messages and the change note |
| 98 | + in items in the RSS feed, as determined by the rsswriter.py FILENAME setting. |
| 99 | + If no RSS feed exists where FILENAME specifies, a new feed is created with |
| 100 | + rsswriter.newRss. |
| 101 | + """ |
| 102 | + |
| 103 | + # The filename of a tracker's RSS feed. Tracker config variables |
| 104 | + # are placed with the standard '%' operator syntax. |
| 105 | + |
| 106 | + FILENAME = "%s/rss.xml"%db.config['TEMPLATES'] |
| 107 | + |
| 108 | + # i.e., roundup.cgi/projects/_file/rss.xml |
| 109 | + # FILENAME = "/home/markpasc/public_html/%(TRACKER_NAME)s.xml" |
| 110 | + |
| 111 | + filename = FILENAME % db.config.__dict__ |
| 112 | + |
| 113 | + # return if issue is private |
| 114 | + if ( db.issue.get(nodeid, 'private') ): |
| 115 | + if __debug__: |
| 116 | + logger.debug("rss: Private issue. not generating rss") |
| 117 | + return |
| 118 | + |
| 119 | + if __debug__: |
| 120 | + logger.debug("rss: generating rss for issue %s", nodeid) |
| 121 | + |
| 122 | + # open the RSS |
| 123 | + import xml.dom.minidom |
| 124 | + from xml.parsers.expat import ExpatError |
| 125 | + |
| 126 | + try: |
| 127 | + rss = xml.dom.minidom.parse(filename) |
| 128 | + except IOError as e: |
| 129 | + if 2 != e.errno: raise |
| 130 | + # File not found |
| 131 | + rss = newRss( |
| 132 | + "%s tracker" % (db.config.TRACKER_NAME,), |
| 133 | + db.config.TRACKER_WEB, |
| 134 | + "Recent changes to the %s Roundup issue tracker" % (db.config.TRACKER_NAME,) |
| 135 | + ) |
| 136 | + except ExpatError as e: |
| 137 | + if os.path.getsize(filename) == 0: |
| 138 | + # delete the file, it's broke |
| 139 | + os.remove(filename) |
| 140 | + # create new rss file |
| 141 | + rss = newRss( |
| 142 | + "%s tracker" % (db.config.TRACKER_NAME,), |
| 143 | + db.config.TRACKER_WEB, |
| 144 | + "Recent changes to the %s Roundup issue tracker" % (db.config.TRACKER_NAME,) |
| 145 | + ) |
| 146 | + else: |
| 147 | + raise |
| 148 | + |
| 149 | + channel = rss.documentElement.getElementsByTagName('channel')[0] |
| 150 | + addEl = lambda parent,tag,value: parent.appendChild(rss.createElement(tag)).appendChild(rss.createTextNode(value)) |
| 151 | + issuelink = '%sissue%s' % (db.config.TRACKER_WEB, nodeid) |
| 152 | + |
| 153 | + |
| 154 | + if olddata: |
| 155 | + chg = cl.generateChangeNote(nodeid, olddata) |
| 156 | + else: |
| 157 | + chg = cl.generateCreateNote(nodeid) |
| 158 | + |
| 159 | + def addItem(desc, date, userid): |
| 160 | + """ |
| 161 | + Adds an RSS item to the RSS document. The title, link, and comments |
| 162 | + link are those of the current issue. |
| 163 | + |
| 164 | + desc: the description text to use |
| 165 | + date: an appropriately formatted string for pubDate |
| 166 | + userid: a Roundup user ID to use as author |
| 167 | + """ |
| 168 | + |
| 169 | + item = rss.createElement('item') |
| 170 | + |
| 171 | + addEl(item, 'title', db.issue.get(nodeid, 'title')) |
| 172 | + addEl(item, 'link', issuelink) |
| 173 | + addEl(item, 'guid', issuelink + '#' + date.replace(' ','+')) |
| 174 | + addEl(item, 'comments', issuelink) |
| 175 | + addEl(item, 'description', desc.replace('&','&').replace('<','<').replace('\n', '<br>\n')) |
| 176 | + addEl(item, 'pubDate', date) |
| 177 | + addEl(item, 'author', |
| 178 | + '%s (%s)' % ( |
| 179 | + db.user.get(userid, 'address'), |
| 180 | + db.user.get(userid, 'username') |
| 181 | + ) |
| 182 | + ) |
| 183 | + |
| 184 | + channel.appendChild(item) |
| 185 | + |
| 186 | + # add detectors directory to path if it's not there. |
| 187 | + # FIXME - see if this pollutes the sys.path for other |
| 188 | + # trackers. |
| 189 | + detector_path="%s/detectors"%(db.config.TRACKER_HOME) |
| 190 | + if ( sys.path.count(detector_path) == 0 ): |
| 191 | + sys.path.insert(0,detector_path) |
| 192 | + |
| 193 | + from nosyreaction import determineNewMessages |
| 194 | + for msgid in determineNewMessages(cl, nodeid, olddata): |
| 195 | + logger.debug("Processing new message msg%s for issue%s", msgid, nodeid) |
| 196 | + desc = db.msg.get(msgid, 'content') |
| 197 | + |
| 198 | + if desc and chg: |
| 199 | + desc += chg |
| 200 | + elif chg: |
| 201 | + desc = chg |
| 202 | + chg = None |
| 203 | + |
| 204 | + addItem(desc or '', db.msg.get(msgid, 'date').pretty(RSS20_DATE_FORMAT), db.msg.get(msgid, 'author')) |
| 205 | + |
| 206 | + if chg: |
| 207 | + from time import strftime |
| 208 | + addItem(chg.replace('\n----------\n', ''), strftime(RSS20_DATE_FORMAT), db.getuid()) |
| 209 | + |
| 210 | + |
| 211 | + for c in channel.getElementsByTagName('item')[0:-MAX_ITEMS]: # leaves at most MAX_ITEMS at the end |
| 212 | + channel.removeChild(c) |
| 213 | + |
| 214 | + # write the RSS |
| 215 | + out = open(filename, 'w') |
| 216 | + |
| 217 | + try: |
| 218 | + out.write(rss.toxml()) |
| 219 | + except Exception as e: |
| 220 | + # record the falure This should not happen. |
| 221 | + logger.error(e) |
| 222 | + out.close() # create 0 length file maybe?? But we handle above. |
| 223 | + raise # let the user know something went wrong. |
| 224 | + |
| 225 | + out.close() |
| 226 | + |
| 227 | + |
| 228 | +def init(db): |
| 229 | + db.issue.react('create', writeRss) |
| 230 | + db.issue.react('set', writeRss) |
| 231 | +#SHA: f4c0ccb5d0d9a6ef7829696333b33bc0619b0167 |
0 commit comments