2003-10-16 16:53:42 +02:00
|
|
|
#!/usr/bin/env python
|
2003-10-10 15:47:06 +02:00
|
|
|
|
|
|
|
###
|
2003-10-11 20:40:22 +02:00
|
|
|
# Copyright (c) 2003, James Vega
|
2003-10-10 15:47:06 +02:00
|
|
|
# All rights reserved.
|
|
|
|
#
|
|
|
|
# Redistribution and use in source and binary forms, with or without
|
|
|
|
# modification, are permitted provided that the following conditions are met:
|
|
|
|
#
|
|
|
|
# * Redistributions of source code must retain the above copyright notice,
|
|
|
|
# this list of conditions, and the following disclaimer.
|
|
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
|
|
# this list of conditions, and the following disclaimer in the
|
|
|
|
# documentation and/or other materials provided with the distribution.
|
|
|
|
# * Neither the name of the author of this software nor the name of
|
|
|
|
# contributors to this software may be used to endorse or promote products
|
|
|
|
# derived from this software without specific prior written consent.
|
|
|
|
#
|
|
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
###
|
|
|
|
|
|
|
|
"""
|
2003-10-11 20:40:22 +02:00
|
|
|
Accesses Sourceforge.net for various things
|
2003-10-10 15:47:06 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
import re
|
2003-10-11 22:52:35 +02:00
|
|
|
import sets
|
2003-11-19 15:56:21 +01:00
|
|
|
import socket
|
2003-10-10 15:47:06 +02:00
|
|
|
import urllib2
|
|
|
|
|
2003-11-15 05:37:04 +01:00
|
|
|
from itertools import ifilter, imap
|
2003-10-10 15:47:06 +02:00
|
|
|
|
2003-10-20 19:52:09 +02:00
|
|
|
import conf
|
2003-10-10 15:47:06 +02:00
|
|
|
import debug
|
|
|
|
import utils
|
2003-10-29 07:06:56 +01:00
|
|
|
import plugins
|
2003-10-11 20:40:22 +02:00
|
|
|
import ircutils
|
2003-10-10 15:47:06 +02:00
|
|
|
import privmsgs
|
|
|
|
import callbacks
|
|
|
|
|
|
|
|
|
|
|
|
def configure(onStart, afterConnect, advanced):
|
|
|
|
# This will be called by setup.py to configure this module. onStart and
|
|
|
|
# afterConnect are both lists. Append to onStart the commands you would
|
|
|
|
# like to be run when the bot is started; append to afterConnect the
|
|
|
|
# commands you would like to be run when the bot has finished connecting.
|
|
|
|
from questions import expect, anything, something, yn
|
2003-10-21 18:43:02 +02:00
|
|
|
onStart.append('load Sourceforge')
|
2003-11-17 19:10:53 +01:00
|
|
|
print 'The Sourceforge plugin has the functionality to watch for URLs'
|
|
|
|
print 'that match a specific pattern (we call this a snarfer). When'
|
|
|
|
print 'supybot sees such a URL, he will parse the web page for'
|
|
|
|
print 'information and reply with the results.\n'
|
2003-11-22 18:10:41 +01:00
|
|
|
if yn('Do you want this snarfer to be enabled by default?') == 'y':
|
2003-11-17 19:10:53 +01:00
|
|
|
onStart.append('Sourceforge config tracker-snarfer on')
|
2003-10-27 21:24:32 +01:00
|
|
|
|
|
|
|
print 'The bugs and rfes commands of the Sourceforge plugin can be set'
|
|
|
|
print 'to query a default project when no project is specified. If this'
|
|
|
|
print 'project is not set, calling either of those commands will display'
|
|
|
|
print 'the associated help. With the default project set, calling'
|
|
|
|
print 'bugs/rfes with no arguments will find the most recent bugs/rfes'
|
|
|
|
print 'for the default project.\n'
|
|
|
|
if yn('Do you want to specify a default project?') == 'y':
|
|
|
|
project = anything('Project name:')
|
|
|
|
if project:
|
2003-11-08 10:02:30 +01:00
|
|
|
onStart.append('Sourceforge config defaultproject %s' % project)
|
2003-10-10 15:47:06 +02:00
|
|
|
|
2003-11-22 18:10:41 +01:00
|
|
|
print 'Sourceforge is quite the word to type, and it may get annoying'
|
|
|
|
print 'typing it all the time because Supybot makes you use the plugin'
|
|
|
|
print 'name to disambiguate calls to ambiguous commands (i.e., the bug'
|
|
|
|
print 'command is in this plugin and the Bugzilla plugin; if both are'
|
|
|
|
print 'loaded, you\'ll have you type "sourceforge bug ..." to get this'
|
|
|
|
print 'bug command). You may save some time by making an alias for'
|
|
|
|
print '"sourceforge". We like to make it "sf".'
|
|
|
|
if yn('Would you like to add sf as an alias for Sourceforge?') == 'y':
|
2003-11-17 19:10:53 +01:00
|
|
|
if 'load Alias' not in onStart:
|
|
|
|
print 'This depends on the Alias module.'
|
|
|
|
if yn('Would you like to load the Alias plugin now?') == 'y':
|
|
|
|
onStart.append('load Alias')
|
2003-11-20 00:18:34 +01:00
|
|
|
else:
|
|
|
|
print 'Then I can\'t add such an alias.'
|
|
|
|
return
|
|
|
|
onStart.append('alias add sf sourceforge $*')
|
2003-11-17 19:10:53 +01:00
|
|
|
|
2003-11-05 03:13:41 +01:00
|
|
|
class TrackerError(Exception):
|
|
|
|
pass
|
2003-10-10 15:47:06 +02:00
|
|
|
|
2003-11-08 10:02:30 +01:00
|
|
|
class Sourceforge(callbacks.PrivmsgCommandAndRegexp, plugins.Configurable):
|
2003-10-11 20:40:22 +02:00
|
|
|
"""
|
|
|
|
Module for Sourceforge stuff. Currently contains commands to query a
|
|
|
|
project's most recent bugs and rfes.
|
|
|
|
"""
|
2003-10-10 15:47:06 +02:00
|
|
|
threaded = True
|
2003-10-21 23:10:20 +02:00
|
|
|
regexps = ['sfSnarfer']
|
2003-10-10 15:47:06 +02:00
|
|
|
|
2003-10-24 14:38:45 +02:00
|
|
|
_reopts = re.I
|
2003-10-27 21:24:32 +01:00
|
|
|
_infoRe = re.compile(r'<td nowrap>(\d+)</td><td><a href='\
|
|
|
|
'"([^"]+)">([^<]+)</a>', _reopts)
|
|
|
|
_hrefOpts = '&set=custom&_assigned_to=0&_status=1&_category'\
|
|
|
|
'=100&_group=100&order=artifact_id&sort=DESC'
|
2003-11-04 08:03:18 +01:00
|
|
|
_resolution=re.compile(r'<b>(Resolution):</b> <a.+?<br>(.+?)</td>',_reopts)
|
|
|
|
_assigned=re.compile(r'<b>(Assigned To):</b> <a.+?<br>(.+?)</td>', _reopts)
|
2003-11-02 20:46:35 +01:00
|
|
|
_submitted = re.compile(r'<b>(Submitted By):</b><br>([^<]+)</td>', _reopts)
|
|
|
|
_priority = re.compile(r'<b>(Priority):</b> <a.+?<br>(.+?)</td>', _reopts)
|
|
|
|
_status = re.compile(r'<b>(Status):</b> <a.+?<br>(.+?)</td>', _reopts)
|
|
|
|
_res =(_resolution, _assigned, _submitted, _priority, _status)
|
2003-10-18 16:19:06 +02:00
|
|
|
|
2003-11-08 10:02:30 +01:00
|
|
|
configurables = plugins.ConfigurableDictionary(
|
2003-11-17 19:10:53 +01:00
|
|
|
[('tracker-snarfer', plugins.ConfigurableBoolType, False,
|
2003-11-08 10:02:30 +01:00
|
|
|
"""Determines whether the bot will reply to SF.net Tracker URLs in
|
|
|
|
the channel with a nice summary of the tracker item."""),
|
2003-11-11 16:58:20 +01:00
|
|
|
('default-project', plugins.ConfigurableStrType, '',
|
2003-11-08 10:02:30 +01:00
|
|
|
"""Sets the default project (used by the bugs/rfes commands in the
|
|
|
|
case that no explicit project is given).""")]
|
|
|
|
)
|
2003-11-05 03:13:41 +01:00
|
|
|
_projectURL = 'http://sourceforge.net/projects/'
|
2003-11-11 16:58:20 +01:00
|
|
|
def __init__(self):
|
|
|
|
plugins.Configurable.__init__(self)
|
|
|
|
callbacks.PrivmsgCommandAndRegexp.__init__(self)
|
|
|
|
|
|
|
|
def die(self):
|
|
|
|
plugins.Configurable.die(self)
|
|
|
|
callbacks.PrivmsgCommandAndRegexp.die(self)
|
2003-10-29 07:06:56 +01:00
|
|
|
|
2003-11-05 03:13:41 +01:00
|
|
|
def _formatResp(self, text, num=''):
|
2003-10-11 20:40:22 +02:00
|
|
|
"""
|
|
|
|
Parses the Sourceforge query to return a list of tuples that
|
|
|
|
contain the bug/rfe information.
|
|
|
|
"""
|
2003-11-05 03:13:41 +01:00
|
|
|
if num:
|
|
|
|
for item in ifilter(lambda s, n=num: s and n in s,
|
2003-10-11 20:40:22 +02:00
|
|
|
self._infoRe.findall(text)):
|
2003-11-05 03:13:41 +01:00
|
|
|
yield (ircutils.bold(utils.htmlToText(item[2])),
|
|
|
|
utils.htmlToText(item[1]))
|
|
|
|
else:
|
2003-10-11 20:40:22 +02:00
|
|
|
for item in ifilter(None, self._infoRe.findall(text)):
|
2003-11-05 03:13:41 +01:00
|
|
|
yield (item[0], utils.htmlToText(item[2]))
|
2003-10-11 20:40:22 +02:00
|
|
|
|
2003-11-05 03:13:41 +01:00
|
|
|
def _getTrackerURL(self, project, regex):
|
2003-10-10 15:47:06 +02:00
|
|
|
try:
|
2003-11-05 03:13:41 +01:00
|
|
|
fd = urllib2.urlopen('%s%s' % (self._projectURL, project))
|
2003-10-10 15:47:06 +02:00
|
|
|
text = fd.read()
|
|
|
|
fd.close()
|
2003-10-27 21:24:32 +01:00
|
|
|
m = regex.search(text)
|
2003-10-10 15:47:06 +02:00
|
|
|
if m is None:
|
2003-11-05 03:13:41 +01:00
|
|
|
raise TrackerError, 'Invalid Tracker page'
|
2003-10-10 15:47:06 +02:00
|
|
|
else:
|
2003-11-05 03:13:41 +01:00
|
|
|
return 'http://sourceforge.net%s%s' % (utils.htmlToText(
|
|
|
|
m.group(1)), self._hrefOpts)
|
2003-10-11 20:40:22 +02:00
|
|
|
except urllib2.HTTPError, e:
|
2003-11-05 03:13:41 +01:00
|
|
|
raise callbacks.Error, e.msg()
|
2003-10-10 15:47:06 +02:00
|
|
|
|
2003-11-05 03:13:41 +01:00
|
|
|
def _getTrackerList(self, url):
|
2003-10-10 15:47:06 +02:00
|
|
|
try:
|
|
|
|
fd = urllib2.urlopen(url)
|
|
|
|
text = fd.read()
|
|
|
|
fd.close()
|
2003-11-05 03:13:41 +01:00
|
|
|
head = '#%s: %s'
|
|
|
|
resp = [head % entry for entry in self._formatResp(text)]
|
|
|
|
if resp:
|
|
|
|
if len(resp) > 10:
|
2003-11-15 05:37:04 +01:00
|
|
|
resp = imap(lambda s: utils.ellipsisify(s, 50), resp)
|
2003-11-05 03:13:41 +01:00
|
|
|
return '%s' % utils.commaAndify(resp)
|
|
|
|
raise callbacks.Error, 'No Trackers were found. (%s)' %\
|
|
|
|
conf.replyPossibleBug
|
2003-11-11 17:04:27 +01:00
|
|
|
except urllib2.HTTPError, e:
|
|
|
|
raise callbacks.Error, e.msg()
|
2003-11-05 03:13:41 +01:00
|
|
|
|
|
|
|
def _getTrackerInfo(self, irc, msg, url, num):
|
|
|
|
try:
|
|
|
|
fd = urllib2.urlopen(url)
|
|
|
|
text = fd.read()
|
|
|
|
fd.close()
|
|
|
|
head = '%s <http://sourceforge.net%s>'
|
|
|
|
resp = [head % match for match in self._formatResp(text,num)]
|
|
|
|
if resp:
|
|
|
|
irc.reply(msg, resp[0])
|
|
|
|
return
|
2003-11-03 02:12:00 +01:00
|
|
|
irc.error(msg, 'No Trackers were found. (%s)' %
|
|
|
|
conf.replyPossibleBug)
|
2003-11-11 17:04:27 +01:00
|
|
|
except urllib2.HTTPError, e:
|
|
|
|
irc.error(msg, e.msg())
|
2003-10-10 15:47:06 +02:00
|
|
|
|
2003-10-27 21:24:32 +01:00
|
|
|
_bugLink = re.compile(r'"([^"]+)">Bugs')
|
|
|
|
def bugs(self, irc, msg, args):
|
2003-11-05 03:13:41 +01:00
|
|
|
"""[<project>]
|
2003-10-27 21:24:32 +01:00
|
|
|
|
|
|
|
Returns a list of the most recent bugs filed against <project>.
|
2003-11-11 17:04:27 +01:00
|
|
|
<project> is not needed if there is a default project set.
|
2003-10-27 21:24:32 +01:00
|
|
|
"""
|
2003-11-11 14:20:06 +01:00
|
|
|
project = privmsgs.getArgs(args, required=0, optional=1)
|
2003-11-11 17:04:27 +01:00
|
|
|
try:
|
|
|
|
int(project)
|
|
|
|
irc.error(msg, 'Use the bug command to get information about a '\
|
|
|
|
'specific bug.')
|
|
|
|
return
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2003-10-27 21:24:32 +01:00
|
|
|
if not project:
|
2003-11-08 10:02:30 +01:00
|
|
|
project = self.configurables.get('default-project', msg.args[0])
|
|
|
|
if not project:
|
|
|
|
raise callbacks.ArgumentError
|
2003-11-05 03:13:41 +01:00
|
|
|
try:
|
|
|
|
url = self._getTrackerURL(project, self._bugLink)
|
2003-11-11 17:04:27 +01:00
|
|
|
except TrackerError, e:
|
|
|
|
irc.error(msg, '%s. Can\'t find the Bugs link.' % e)
|
2003-11-05 03:13:41 +01:00
|
|
|
return
|
|
|
|
irc.reply(msg, self._getTrackerList(url))
|
|
|
|
|
|
|
|
def bug(self, irc, msg, args):
|
|
|
|
"""[<project>] <num>
|
|
|
|
|
|
|
|
Returns a description of the bug with Tracker id <num> and the
|
2003-11-11 17:04:27 +01:00
|
|
|
corresponding Tracker URL. <project> is not needed if there is a
|
|
|
|
default project set.
|
2003-11-05 03:13:41 +01:00
|
|
|
"""
|
|
|
|
(project, bugnum) = privmsgs.getArgs(args, optional=1)
|
|
|
|
if not bugnum:
|
2003-10-27 21:24:32 +01:00
|
|
|
try:
|
2003-11-05 03:13:41 +01:00
|
|
|
int(project)
|
2003-10-27 21:24:32 +01:00
|
|
|
except ValueError:
|
2003-11-05 03:13:41 +01:00
|
|
|
irc.error(msg, '"%s" is not a proper bugnumber.' % project)
|
2003-11-08 10:02:30 +01:00
|
|
|
return
|
2003-11-05 03:13:41 +01:00
|
|
|
bugnum = project
|
2003-11-08 10:02:30 +01:00
|
|
|
project = self.configurables.get('default-project', msg.args[0])
|
|
|
|
if not project:
|
|
|
|
raise callbacks.ArgumentError
|
2003-11-05 03:13:41 +01:00
|
|
|
try:
|
|
|
|
url = self._getTrackerURL(project, self._bugLink)
|
2003-11-11 17:04:27 +01:00
|
|
|
except TrackerError, e:
|
|
|
|
irc.error(msg, '%s. Can\'t find the Bugs link.' % e)
|
2003-11-05 03:13:41 +01:00
|
|
|
return
|
|
|
|
self._getTrackerInfo(irc, msg, url, bugnum)
|
2003-10-27 21:24:32 +01:00
|
|
|
|
2003-10-10 15:47:06 +02:00
|
|
|
_rfeLink = re.compile(r'"([^"]+)">RFE')
|
|
|
|
def rfes(self, irc, msg, args):
|
2003-11-05 03:13:41 +01:00
|
|
|
"""[<project>]
|
2003-10-10 15:47:06 +02:00
|
|
|
|
2003-10-11 20:40:22 +02:00
|
|
|
Returns a list of the most recent RFEs filed against <project>.
|
2003-11-11 17:04:27 +01:00
|
|
|
<project> is not needed if there is a default project set.
|
2003-10-10 15:47:06 +02:00
|
|
|
"""
|
2003-11-11 14:20:06 +01:00
|
|
|
project = privmsgs.getArgs(args, required=0, optional=1)
|
2003-11-11 17:04:27 +01:00
|
|
|
try:
|
|
|
|
int(project)
|
|
|
|
irc.error(msg, 'Use the rfe command to get information about a '\
|
|
|
|
'specific rfe.')
|
|
|
|
return
|
|
|
|
except ValueError:
|
|
|
|
pass
|
2003-10-10 15:47:06 +02:00
|
|
|
if not project:
|
2003-11-08 10:02:30 +01:00
|
|
|
project = self.configurables.get('default-project', msg.args[0])
|
|
|
|
if not project:
|
|
|
|
raise callbacks.ArgumentError
|
2003-11-05 03:13:41 +01:00
|
|
|
try:
|
|
|
|
url = self._getTrackerURL(project, self._rfeLink)
|
|
|
|
except TrackerError, e:
|
2003-11-11 17:04:27 +01:00
|
|
|
irc.error(msg, '%s. Can\'t find the RFEs link.' % e)
|
2003-11-05 03:13:41 +01:00
|
|
|
return
|
|
|
|
irc.reply(msg, self._getTrackerList(url))
|
|
|
|
|
|
|
|
def rfe(self, irc, msg, args):
|
|
|
|
"""[<project>] <num>
|
|
|
|
|
|
|
|
Returns a description of the bug with Tracker id <num> and the
|
2003-11-11 17:04:27 +01:00
|
|
|
corresponding Tracker URL. <project> is not needed if there is a
|
|
|
|
default project set.
|
2003-11-05 03:13:41 +01:00
|
|
|
"""
|
|
|
|
(project, rfenum) = privmsgs.getArgs(args, optional=1)
|
|
|
|
if not rfenum:
|
2003-10-27 21:24:32 +01:00
|
|
|
try:
|
2003-11-05 03:13:41 +01:00
|
|
|
int(project)
|
2003-10-27 21:24:32 +01:00
|
|
|
except ValueError:
|
2003-11-05 03:13:41 +01:00
|
|
|
irc.error(msg, '"%s" is not a proper rfenumber.' % project)
|
2003-11-08 10:02:30 +01:00
|
|
|
return
|
|
|
|
rfenum = project
|
|
|
|
project = self.configurables.get('default-project', msg.args[0])
|
|
|
|
if not project:
|
|
|
|
raise callbacks.ArgumentError
|
2003-11-05 03:13:41 +01:00
|
|
|
try:
|
|
|
|
url = self._getTrackerURL(project, self._rfeLink)
|
2003-11-11 17:04:27 +01:00
|
|
|
except TrackerError, e:
|
|
|
|
irc.error(msg, '%s. Can\'t find the RFEs link.' % e)
|
2003-11-05 03:13:41 +01:00
|
|
|
return
|
|
|
|
self._getTrackerInfo(irc, msg, url, rfenum)
|
2003-10-10 15:47:06 +02:00
|
|
|
|
2003-11-02 20:46:35 +01:00
|
|
|
_bold = lambda self, m: (ircutils.bold(m[0]),) + m[1:]
|
2003-10-11 22:52:35 +02:00
|
|
|
_sfTitle = re.compile(r'Detail:(\d+) - ([^<]+)</title>', re.I)
|
|
|
|
_linkType = re.compile(r'(\w+ \w+|\w+): Tracker Detailed View', re.I)
|
2003-10-21 14:20:23 +02:00
|
|
|
def sfSnarfer(self, irc, msg, match):
|
2003-10-30 01:13:00 +01:00
|
|
|
r"https?://(?:www\.)?(?:sourceforge|sf)\.net/tracker/" \
|
|
|
|
r".*\?(?:&?func=detail|&?aid=\d+|&?group_id=\d+|&?atid=\d+){4}"
|
2003-11-08 10:02:30 +01:00
|
|
|
if not self.configurables.get('tracker-snarfer', channel=msg.args[0]):
|
2003-10-20 19:52:09 +02:00
|
|
|
return
|
2003-10-11 22:52:35 +02:00
|
|
|
try:
|
2003-11-02 20:46:35 +01:00
|
|
|
url = match.group(0)
|
|
|
|
fd = urllib2.urlopen(url)
|
|
|
|
s = fd.read()
|
|
|
|
fd.close()
|
|
|
|
resp = []
|
|
|
|
head = ''
|
|
|
|
m = self._linkType.search(s)
|
|
|
|
n = self._sfTitle.search(s)
|
|
|
|
if m and n:
|
|
|
|
linktype = m.group(1)
|
|
|
|
linktype = utils.depluralize(linktype)
|
|
|
|
(num, desc) = n.groups()
|
|
|
|
head = '%s #%s:' % (ircutils.bold(linktype), num)
|
|
|
|
resp.append(desc)
|
|
|
|
else:
|
|
|
|
debug.msg('%s does not appear to be a proper Sourceforge '\
|
|
|
|
'Tracker page (%s)' % (url, conf.replyPossibleBug))
|
|
|
|
for r in self._res:
|
|
|
|
m = r.search(s)
|
|
|
|
if m:
|
|
|
|
resp.append('%s: %s' % self._bold(m.groups()))
|
2003-10-18 16:19:06 +02:00
|
|
|
irc.reply(msg, '%s #%s: %s' % (ircutils.bold(linktype),
|
2003-10-21 18:43:02 +02:00
|
|
|
ircutils.bold(num), '; '.join(resp)), prefixName = False)
|
2003-11-02 20:46:35 +01:00
|
|
|
except urllib2.HTTPError, e:
|
|
|
|
debug.msg(e.msg())
|
2003-11-19 15:56:21 +01:00
|
|
|
except socket.error, e:
|
2003-11-14 15:06:19 +01:00
|
|
|
debug.msg(e.msg())
|
2003-11-08 10:02:30 +01:00
|
|
|
sfSnarfer = privmsgs.urlSnarfer(sfSnarfer)
|
2003-10-11 22:52:35 +02:00
|
|
|
|
2003-10-21 18:43:02 +02:00
|
|
|
Class = Sourceforge
|
2003-10-10 15:47:06 +02:00
|
|
|
|
|
|
|
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|