This is `roundup-feeder', an RSS issue feeder for Roundup. It creates a new issue for each new item in its RSS feed, and keeps track of items already entered using a pickled list.
I owe quite a bit of roundup-feeder to the chaps who built `spycyroll' (http://spycyroll.sourceforge.net/). It's a neat program. Rather than have multiple webpages to look at, though, I thought it would be nice if everything were entered into my Roundup tracker.
You'll need to install Mark Pilgrim's `feedparser' (http://feedparser.sourceforge.net), and of course you'll need to have roundup installed too.
Setup is fairly simple. Set the roundup_* variables in config.ini (see below), list the RSS feeds you want to watch, and go. I threw together roundup-feeder to track security advisories & news, hence the selection of feeds you see in the sample config.ini.
Modifying the program to handle your custom Roundup database should be trivial - have a look at the enterNewsItem method of the RoundupFeeder class.
You should probably set this up to run every 30 minutes or so via cron.
- Cian
config.ini ::
## # File: config.ini # Desc: A roundup-feeder sample config. I threw together roundup-feeder to # track security advisories & news, hence the selection of feeds you see # here. # Auth: Cian Synnott <cian@dmz.ie> # $Id: config.ini,v 1.2 2004/09/06 16:28:41 cian Exp $ ## [DEFAULT] # The file in which to save our pickled list of items already entered statefile = itemlist.sav # Set the location of your roundup tracker roundup-home = /path/to/roundup/tracker # Set the user you want to enter issues into the tracker as roundup-user = feeder # Set the class of roundup issues - most likely `issue' :o) roundup-class = issue # Each feed has this layout - the feed link is the section header, and we can # specify a name in the section. [http://www.securityfocus.com/rss/vulnerabilities.xml] name = SecurityFocus [http://www.osvdb.org/backend/rss.php] name = OSVDB [http://www.packetstormsecurity.nl/whatsnew50.xml] name = PacketStorm [http://www.k-otik.com/advisories.xml] name = k-otik advisories [http://www.k-otik.com/exploits.xml] name = k-otik exploits [http://www.microsoft.com/technet/security/bulletin/secrss.aspx] name = MS Bulletins [http://www.djeaux.com/rss/insecure-vulnwatch.rss] name = Vulnwatch [http://msdn.microsoft.com/security/rss.xml] name = MS Developer [http://www.nwfusion.com/rss/security.xml] name = nwfusion [http://www.us-cert.gov/channels/techalerts.rdf] name = US-CERT tech alerts [http://www.us-cert.gov/channels/bulletins.rdf] name = US-CERT bulletins
roundup-feeder ::
1 #!/usr/bin/env python
2
3 ##
4 # File: roundup-feeder
5 # Desc: RSS issue feeder for roundup. It is partially derived from the
6 # `spycyroll' RSS aggregator, from http://spycyroll.sourceforge.net/.
7 #
8 # I've made it as straightforward as possible.
9 #
10 # Creates new roundup issues for each new item in its RSS feed, and keeps
11 # track of those already entered in the roundup DB in a pickled list.
12 #
13 # Auth: Cian Synnott <cian@dmz.ie>
14 # $Id: roundup-feeder.py,v 1.2 2004/09/06 21:30:12 cian Exp $
15 ##
16
17 import sys
18 import pickle
19
20 # Mark Pilgrim's rather relaxed RSS parser (http://feedparser.sourceforge.net/)
21 import feedparser
22
23 # Ease of configuration
24 from ConfigParser import ConfigParser
25
26 # The all-important roundup stuff
27 import roundup.instance
28 from roundup import date
29
30 DEFAULT_CONFIG_FILE = "config.ini"
31
32 class Channel:
33 """An RSS channel.
34
35 Allows an RSS channel to be loaded and stored in memory
36
37 rss : url to the RSS or RDF file. From this url, it figures out the rest;
38 title : Title as specified in RSS file UNLESS you specify it while creating
39 the object
40 link : url for the site, as specified in the RSS file
41 description : optional textual description.
42 items[]: collection of type NewsItem for the items in the feed
43 """
44
45 def __init__(self, rss, title=None):
46 self.rss = rss
47 self.title = title
48 self.link = None
49 self.description = None
50 self.items = []
51
52 def load(self, rss=None):
53 """Downloads and parses a channel
54
55 sets the feed's title, link and description.
56 sets and returns items[], for NewsItems defined
57 """
58 if rss is None:
59 rss = self.rss
60 prss = feedparser.parse(rss)
61 channel = prss['channel']
62 items = prss['items']
63 if 'link' in channel.keys():
64 self.link = channel['link']
65 else:
66 self.link = ''
67 title = channel['title']
68 self.description = channel['description']
69 if self.title is None:
70 self.title = title
71 self.items = []
72 for item in items:
73 self.items.append(NewsItem(item))
74 return self.items
75
76 class NewsItem:
77 """Each item in a channel"""
78
79 def __init__(self, dict):
80 self.link = dict.get('link', '')
81 self.title = dict['title']
82 self.description = dict['description']
83 if 'date' in dict.keys():
84 self.date = dict['date']
85 else:
86 self.date = None
87
88 def loadChannels(config):
89 """Loads all channels in a configuration
90 """
91 feeds = {}
92
93 for f in config.sections():
94 if config.has_option(f, 'name'):
95 feeds[f] = config.get(f, 'name')
96 else:
97 feeds[f] = None
98
99 channels = []
100
101 for f in feeds.keys():
102 c = Channel(f, feeds[f])
103 try:
104 c.load()
105 except:
106 continue
107
108 channels.append(c)
109
110 return channels
111
112
113 class RoundupFeeder:
114 """A roundup RSS feeder class
115
116 A class for maintaining a `connection' to the roundup database and feeding
117 issues into it.
118
119 instance : An instance of the roundup tracker we're dealing with
120 db : The roundup database belonging to that instance
121 cl : The roundup class we enter issues as
122 uid : The user id we've opend the db as
123 """
124
125 def __init__(self, home, user, klass):
126 """Initialise the RoundupFeeder
127
128 home : The location of the roundup tracker on the filesystem
129 user : The user to open the database as
130 klass : The name of the `issue' class in the database
131 """
132
133 # Connect to our roundup database
134 self.instance = roundup.instance.open(home)
135 self.db = self.instance.open('admin')
136
137 # First lookup and reconnect as this user
138 try:
139 self.uid = self.db.user.lookup(user)
140 username = self.db.user.get(self.uid, 'username')
141
142 self.db.close()
143 self.db = self.instance.open(username)
144 except:
145 print '''
146 Cannot open the tracker "%s" with username "%s".
147 Are you sure this user exists in the database?
148 '''%(home, user)
149 sys.exit(1)
150
151 try:
152 self.cl = self.db.getclass(klass)
153 except:
154 print '''
155 It appears that the configured Roundup class "%s" does not exist in the
156 database. Valid class names are: %s
157 '''%(klass, ', '.join(self.db.getclasses()))
158 sys.exit(1)
159
160 def cleanup(self):
161 """Cleans up the RoundupFeeder
162
163 Closes the database we've been dealing with. This will need to be called
164 once you have created a RoundupFeeder; perhaps use a try: finally:
165 structure.
166 """
167 self.db.close()
168
169 def enterNewsItem(self, channel, item):
170 """Enter a news item as a roundup issue
171
172 Each news item should be a new issue in roundup.
173
174 channel : The channel this item is from
175 item : The item to enter as an issue
176 """
177
178 # Setup issue title and issue content
179 title = item.title
180
181 if item.description:
182 content = '''
183 From: %s (%s)
184
185 %s
186
187 Link: %s
188 '''%(channel.title, channel.link, item.description, item.link)
189
190 else:
191 content = '''
192 From: %s (%s)
193
194 Link: %s
195 '''%(channel.title, channel.link, item.link)
196
197 # Set up the issue
198 issue = {}
199 issue['title'] = title
200 issue['files'] = []
201 issue['nosy'] = []
202 issue['superseder'] = []
203
204 # Set up the message
205 msg = {}
206 msg['author'] = self.uid
207 msg['date'] = date.Date('.')
208 msg['summary'] = title
209 msg['content'] = content
210 msg['files'] = []
211 msg['recipients'] = []
212 msg['messageid'] = ''
213 msg['inreplyto'] = ''
214
215 # My issues have a 'type' property for the issue type - I set that to
216 # roundup
217 if self.cl.getprops().has_key('type'):
218 issue['type'] = 'rssfeed'
219
220 # My issues have a 'status' property - I set that to unread
221 if self.cl.getprops().has_key('type'):
222 issue['status'] = 'unread'
223
224 # Now create new message & issue for this item
225 try:
226 message_id = self.db.msg.create(**msg)
227 issue['messages'] = [message_id]
228 nodeid = self.cl.create(**issue)
229 except Exception, inst:
230 print '''
231 Error creating issue for item '%s'.
232 '''%(item.link)
233 print inst
234
235 self.db.commit()
236
237 if __name__ == '__main__':
238
239 if len(sys.argv) > 1:
240 config_file = sys.argv[1]
241 else:
242 config_file = DEFAULT_CONFIG_FILE
243
244 config = ConfigParser()
245 config.readfp(open(config_file))
246
247 statefile = config.get('DEFAULT', 'statefile' )
248 roundup_home = config.get('DEFAULT', 'roundup-home' )
249 roundup_user = config.get('DEFAULT', 'roundup-user' )
250 roundup_class = config.get('DEFAULT', 'roundup-class')
251
252 channels = loadChannels(config)
253
254 try:
255 fp = open(statefile, 'r')
256 olditems = pickle.load(fp)
257 fp.close()
258 except:
259 olditems = []
260
261 newitems = []
262
263 feeder = RoundupFeeder(roundup_home, roundup_user, roundup_class)
264
265 # Now use try: finally: to make sure the database gets closed
266 try:
267 for c in channels:
268 for i in c.items:
269 if i.link not in newitems:
270 newitems.append(i.link)
271 if i.link not in olditems:
272 feeder.enterNewsItem(c, i)
273
274 finally:
275 feeder.cleanup()
276
277 # Now pickle our list of RSS items for the next run.
278 try:
279 fp = open(statefile, 'w')
280 pickle.dump(newitems, fp)
281 fp.close()
282 except:
283 print "Couldn't dump statefile!"