Bugtracker: Update trackers.
* Add Bugzilla JSON, SourceForge, GitHub. * Make Mantis and Trac getable. * Deprecate Bugzilla XML.
This commit is contained in:
@ -24,7 +24,7 @@ import supybot.world as world
|
|||||||
|
|
||||||
from imp import reload
|
from imp import reload
|
||||||
|
|
||||||
__version__ = "2.8.0"
|
__version__ = "2.9.0"
|
||||||
__author__ = supybot.Author("Krytarik Raido", "krytarik", "krytarik@tuxgarage.com")
|
__author__ = supybot.Author("Krytarik Raido", "krytarik", "krytarik@tuxgarage.com")
|
||||||
__contributors__ = {
|
__contributors__ = {
|
||||||
supybot.Author("Dennis Kaarsemaker", "Seveas", "dennis@kaarsemaker.net"): ['Original Author'],
|
supybot.Author("Dennis Kaarsemaker", "Seveas", "dennis@kaarsemaker.net"): ['Original Author'],
|
||||||
|
@ -24,7 +24,7 @@ import supybot.conf as conf
|
|||||||
import supybot.registry as registry
|
import supybot.registry as registry
|
||||||
import supybot.log as supylog
|
import supybot.log as supylog
|
||||||
|
|
||||||
import re, os, sys, time
|
import re, os, sys, time, json
|
||||||
import xml.dom.minidom as minidom
|
import xml.dom.minidom as minidom
|
||||||
from email.parser import FeedParser
|
from email.parser import FeedParser
|
||||||
if sys.version_info < (3,0):
|
if sys.version_info < (3,0):
|
||||||
@ -131,7 +131,7 @@ class Bugtracker(callbacks.PluginRegexp):
|
|||||||
registerBugtracker(name)
|
registerBugtracker(name)
|
||||||
group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False)
|
group = self.registryValue('bugtrackers.%s' % name.replace('.','\\.'), value=False)
|
||||||
if group.trackertype() in defined_bugtrackers:
|
if group.trackertype() in defined_bugtrackers:
|
||||||
self.db[name] = defined_bugtrackers[group.trackertype()](name, group.url(), group.description())
|
self.db[name] = defined_bugtrackers[group.trackertype()](name, group.url(), group.description(), group.trackertype())
|
||||||
else:
|
else:
|
||||||
supylog.warning("Bugtracker: Unknown trackertype: %s (%s)" % (group.trackertype(), name))
|
supylog.warning("Bugtracker: Unknown trackertype: %s (%s)" % (group.trackertype(), name))
|
||||||
self.shorthand = utils.abbrev(list(self.db.keys()))
|
self.shorthand = utils.abbrev(list(self.db.keys()))
|
||||||
@ -151,8 +151,8 @@ class Bugtracker(callbacks.PluginRegexp):
|
|||||||
def add(self, irc, msg, args, name, trackertype, url, description):
|
def add(self, irc, msg, args, name, trackertype, url, description):
|
||||||
"""<name> <type> <url> [<description>]
|
"""<name> <type> <url> [<description>]
|
||||||
|
|
||||||
Add a bugtracker to the list of defined bugtrackers. Currently
|
Add a bugtracker to the list of defined bugtrackers. Currently supported
|
||||||
supported types are Launchpad, Debbugs, Bugzilla, Mantis, and Trac.
|
types are Launchpad, Debbugs, Bugzilla, SourceForge, Github, Mantis, and Trac.
|
||||||
<name> will be used to reference the bugtracker in all commands.
|
<name> will be used to reference the bugtracker in all commands.
|
||||||
Unambiguous abbreviations of it will also be accepted.
|
Unambiguous abbreviations of it will also be accepted.
|
||||||
<description> will be used to reference the bugtracker in the
|
<description> will be used to reference the bugtracker in the
|
||||||
@ -165,7 +165,7 @@ class Bugtracker(callbacks.PluginRegexp):
|
|||||||
url = url[:-1]
|
url = url[:-1]
|
||||||
trackertype = trackertype.lower()
|
trackertype = trackertype.lower()
|
||||||
if trackertype in defined_bugtrackers:
|
if trackertype in defined_bugtrackers:
|
||||||
self.db[name] = defined_bugtrackers[trackertype](name, url, description)
|
self.db[name] = defined_bugtrackers[trackertype](name, url, description, trackertype)
|
||||||
else:
|
else:
|
||||||
irc.error("Bugtrackers of type '%s' are not understood" % trackertype)
|
irc.error("Bugtrackers of type '%s' are not understood" % trackertype)
|
||||||
return
|
return
|
||||||
@ -323,7 +323,7 @@ class Bugtracker(callbacks.PluginRegexp):
|
|||||||
irc.reply(r)
|
irc.reply(r)
|
||||||
|
|
||||||
def turlSnarfer(self, irc, msg, match):
|
def turlSnarfer(self, irc, msg, match):
|
||||||
r"(https?://)?((bugs\.debian\.org|pad\.lv)/|\S+/(show_bug\.cgi\?id=|bugreport\.cgi\?bug=|view\.php\?id=|bug=|bugs/|\+bug/|ticket/))(?P<bug>\d+)"
|
r"(https?://)?((bugs\.debian\.org|pad\.lv)/|\S+/(show_bug\.cgi\?id=|bugreport\.cgi\?bug=|view\.php\?id=|bug=|bugs/|\+bug/|ticket/|feature-requests/|patches/|todo/|issues/|pulls?/))(?P<bug>\d+)/?"
|
||||||
channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None
|
channel = msg.args[0] if ircutils.isChannel(msg.args[0]) else None
|
||||||
if checkAddressed(msg.args[1].strip(), channel):
|
if checkAddressed(msg.args[1].strip(), channel):
|
||||||
return
|
return
|
||||||
@ -396,6 +396,9 @@ class Bugtracker(callbacks.PluginRegexp):
|
|||||||
|
|
||||||
#TODO: As we will depend on launchpadlib, we should consider using lazr.uri.URI to do URL parsing
|
#TODO: As we will depend on launchpadlib, we should consider using lazr.uri.URI to do URL parsing
|
||||||
def get_tracker(self, snarfurl):
|
def get_tracker(self, snarfurl):
|
||||||
|
# SourceForge short domain
|
||||||
|
snarfurl = snarfurl.replace('sf.net', 'sourceforge.net', 1)
|
||||||
|
|
||||||
# Launchpad URL shortening
|
# Launchpad URL shortening
|
||||||
snarfurl = re.sub(r'pad\.lv/(bug=)?(?P<bug>[0-9]+)', r'launchpad.net/bugs/\g<bug>', snarfurl)
|
snarfurl = re.sub(r'pad\.lv/(bug=)?(?P<bug>[0-9]+)', r'launchpad.net/bugs/\g<bug>', snarfurl)
|
||||||
|
|
||||||
@ -411,10 +414,21 @@ class Bugtracker(callbacks.PluginRegexp):
|
|||||||
# No tracker found, bummer. Let's try and get one
|
# No tracker found, bummer. Let's try and get one
|
||||||
if 'show_bug.cgi' in snarfurl:
|
if 'show_bug.cgi' in snarfurl:
|
||||||
tracker = Bugzilla().get_tracker(snarfurl)
|
tracker = Bugzilla().get_tracker(snarfurl)
|
||||||
if tracker:
|
elif 'sourceforge.net' in snarfurl:
|
||||||
self.db[tracker.name] = tracker
|
tracker = SourceForge().get_tracker(snarfurl)
|
||||||
self.shorthand = utils.abbrev(list(self.db.keys()))
|
elif 'github.com' in snarfurl:
|
||||||
return tracker
|
tracker = GitHub().get_tracker(snarfurl)
|
||||||
|
elif 'view.php' in snarfurl:
|
||||||
|
tracker = Mantis().get_tracker(snarfurl)
|
||||||
|
elif '/ticket/' in snarfurl:
|
||||||
|
tracker = Trac().get_tracker(snarfurl)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if tracker:
|
||||||
|
self.db[tracker.name] = tracker
|
||||||
|
self.shorthand = utils.abbrev(list(self.db.keys()))
|
||||||
|
return tracker
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_bug(self, channel, tracker, id, do_assignee, do_extinfo, do_url=True, do_tracker=True):
|
def get_bug(self, channel, tracker, id, do_assignee, do_extinfo, do_url=True, do_tracker=True):
|
||||||
@ -430,10 +444,20 @@ class Bugtracker(callbacks.PluginRegexp):
|
|||||||
if duplicate and not self.is_ok(channel, tracker, bid):
|
if duplicate and not self.is_ok(channel, tracker, bid):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if do_tracker:
|
bugtype = re.match(r'.*/(feature-)?(?P<type>request|patch|todo|issue|pull|ticket)(e?s)?/[0-9]+/?$', url)
|
||||||
report = '%s bug %d' % (tracker.description, bid)
|
if do_tracker and tracker.trackertype != 'github':
|
||||||
|
if re.match(r'.*/(bugs|feature-requests|patches|todo|issues|pulls?|ticket)/?$', tracker.description):
|
||||||
|
report = '%s %d' % (tracker.description, bid)
|
||||||
|
else:
|
||||||
|
if bugtype:
|
||||||
|
report = '%s %s %d' % (tracker.description, bugtype.group('type'), bid)
|
||||||
|
else:
|
||||||
|
report = '%s bug %d' % (tracker.description, bid)
|
||||||
else:
|
else:
|
||||||
report = 'Bug %d' % bid
|
if bugtype:
|
||||||
|
report = '%s %d' % (bugtype.group('type').title(), bid)
|
||||||
|
else:
|
||||||
|
report = 'Bug %d' % bid
|
||||||
|
|
||||||
if product:
|
if product:
|
||||||
report += ' in %s' % product
|
report += ' in %s' % product
|
||||||
@ -472,10 +496,11 @@ class Bugtracker(callbacks.PluginRegexp):
|
|||||||
|
|
||||||
# Define all bugtrackers
|
# Define all bugtrackers
|
||||||
class IBugtracker:
|
class IBugtracker:
|
||||||
def __init__(self, name=None, url=None, description=None):
|
def __init__(self, name=None, url=None, description=None, trackertype=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.url = url
|
self.url = url
|
||||||
self.description = description
|
self.description = description
|
||||||
|
self.trackertype = trackertype
|
||||||
self.errget = 'Could not get data from %s: %s (%s)'
|
self.errget = 'Could not get data from %s: %s (%s)'
|
||||||
self.errparse = 'Could not parse data from %s: %s (%s)'
|
self.errparse = 'Could not parse data from %s: %s (%s)'
|
||||||
|
|
||||||
@ -504,11 +529,36 @@ class Bugzilla(IBugtracker):
|
|||||||
name = desc = match.group('name')
|
name = desc = match.group('name')
|
||||||
url = 'https://%s' % match.group('url')
|
url = 'https://%s' % match.group('url')
|
||||||
# registerBugtracker(name, url, desc, 'bugzilla')
|
# registerBugtracker(name, url, desc, 'bugzilla')
|
||||||
return Bugzilla(name, url, desc)
|
return Bugzilla(name, url, desc, 'bugzilla')
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_bug(self, id):
|
def get_bug(self, id):
|
||||||
|
url = "%s/rest/bug/%d" % (self.url, id)
|
||||||
|
try:
|
||||||
|
bugjson = utils.web.getUrl(url)
|
||||||
|
bug = json.loads(bugjson.decode('utf-8'))['bugs'][0]
|
||||||
|
except Exception as e:
|
||||||
|
# For old-stable Bugzilla
|
||||||
|
if 'HTTP Error 404' in str(e):
|
||||||
|
return self.get_bug_old(id)
|
||||||
|
raise BugtrackerError(self.errget % (self.description, e, url))
|
||||||
|
try:
|
||||||
|
status = bug['status']
|
||||||
|
if bug['resolution']:
|
||||||
|
status += ': %s' % bug['resolution']
|
||||||
|
if bug['assigned_to_detail']:
|
||||||
|
assignee = bug['assigned_to_detail']['real_name']
|
||||||
|
if not assignee:
|
||||||
|
assignee = bug['assigned_to_detail']['name']
|
||||||
|
else:
|
||||||
|
assignee = ''
|
||||||
|
return [(id, bug['product'], bug['summary'], bug['severity'], status, assignee,
|
||||||
|
"%s/show_bug.cgi?id=%d" % (self.url, id), [], [])]
|
||||||
|
except Exception as e:
|
||||||
|
raise BugtrackerError(self.errparse % (self.description, e, url))
|
||||||
|
|
||||||
|
def get_bug_old(self, id): # Deprecated
|
||||||
url = "%s/show_bug.cgi?id=%d&ctype=xml" % (self.url, id)
|
url = "%s/show_bug.cgi?id=%d&ctype=xml" % (self.url, id)
|
||||||
try:
|
try:
|
||||||
bugxml = utils.web.getUrl(url)
|
bugxml = utils.web.getUrl(url)
|
||||||
@ -741,6 +791,71 @@ class Debbugs(IBugtracker):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BugtrackerError(self.errparse % (self.description, e, url))
|
raise BugtrackerError(self.errparse % (self.description, e, url))
|
||||||
|
|
||||||
|
class SourceForge(IBugtracker):
|
||||||
|
def get_tracker(self, url):
|
||||||
|
try:
|
||||||
|
match = re.match(r'sourceforge\.net/p/[^\s/]+/(bugs|feature-requests|patches|todo)', url)
|
||||||
|
name = desc = match.group(0)
|
||||||
|
url = 'https://%s' % name
|
||||||
|
# registerBugtracker(name, url, desc, 'sourceforge')
|
||||||
|
return SourceForge(name, url, desc, 'sourceforge')
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_bug(self, id):
|
||||||
|
url = "%s/%d/" % (self.url.replace('sourceforge.net', 'sourceforge.net/rest'), id)
|
||||||
|
try:
|
||||||
|
bugjson = utils.web.getUrl(url)
|
||||||
|
bug = json.loads(bugjson.decode('utf-8'))['ticket']
|
||||||
|
except Exception as e:
|
||||||
|
raise BugtrackerError(self.errget % (self.description, e, url))
|
||||||
|
try:
|
||||||
|
product = severity = ''
|
||||||
|
if bug['labels']:
|
||||||
|
product = bug['labels'][0]
|
||||||
|
if '_priority' in bug['custom_fields']:
|
||||||
|
severity = 'Pri: %s' % bug['custom_fields']['_priority']
|
||||||
|
return [(id, product, bug['summary'], severity, ': '.join(bug['status'].split('-')),
|
||||||
|
bug['assigned_to'], "%s/%d/" % (self.url, id), [], [])]
|
||||||
|
except Exception as e:
|
||||||
|
raise BugtrackerError(self.errparse % (self.description, e, url))
|
||||||
|
|
||||||
|
class GitHub(IBugtracker):
|
||||||
|
def get_tracker(self, url):
|
||||||
|
try:
|
||||||
|
match = re.match(r'github\.com/[^\s/]+/[^\s/]+/(issues|pulls?)', url)
|
||||||
|
name = desc = match.group(0)
|
||||||
|
url = 'https://%s' % name
|
||||||
|
# Pulls are inconsistent in main and single page URLs
|
||||||
|
name = desc = re.sub(r'/pull$', r'/pulls', name)
|
||||||
|
# registerBugtracker(name, url, desc, 'github')
|
||||||
|
return GitHub(name, url, desc, 'github')
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_bug(self, id):
|
||||||
|
url = "%s/%d" % (self.url.replace('github.com', 'api.github.com/repos'), id)
|
||||||
|
# Pulls are inconsistent in web and API URLs
|
||||||
|
url = url.replace('/pull/', '/pulls/')
|
||||||
|
try:
|
||||||
|
bugjson = utils.web.getUrl(url)
|
||||||
|
bug = json.loads(bugjson.decode('utf-8'))
|
||||||
|
except Exception as e:
|
||||||
|
raise BugtrackerError(self.errget % (self.description, e, url))
|
||||||
|
try:
|
||||||
|
product = '/'.join(url.split('/')[-4:-2])
|
||||||
|
if 'merged' in bug and bug['merged']:
|
||||||
|
status = 'Merged'
|
||||||
|
else:
|
||||||
|
status = bug['state']
|
||||||
|
if bug['assignee']:
|
||||||
|
assignee = bug['assignee']['login']
|
||||||
|
else:
|
||||||
|
assignee = ''
|
||||||
|
return [(id, product, bug['title'], '', status, assignee, bug['html_url'], [], [])]
|
||||||
|
except Exception as e:
|
||||||
|
raise BugtrackerError(self.errparse % (self.description, e, url))
|
||||||
|
|
||||||
class Mantis(IBugtracker):
|
class Mantis(IBugtracker):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
if not sys.version_info < (3,0):
|
if not sys.version_info < (3,0):
|
||||||
@ -749,11 +864,25 @@ class Mantis(IBugtracker):
|
|||||||
IBugtracker.__init__(self, *args, **kwargs)
|
IBugtracker.__init__(self, *args, **kwargs)
|
||||||
self.soap_proxy = SOAPProxy("%s/api/soap/mantisconnect.php" % self.url, namespace="http://futureware.biz/mantisconnect")
|
self.soap_proxy = SOAPProxy("%s/api/soap/mantisconnect.php" % self.url, namespace="http://futureware.biz/mantisconnect")
|
||||||
|
|
||||||
|
def get_tracker(self, url):
|
||||||
|
try:
|
||||||
|
match = re.match(r'(?P<url>(?P<name>[^\s/]+).*)/view\.php', url)
|
||||||
|
name = desc = match.group('name')
|
||||||
|
url = 'https://%s' % match.group('url')
|
||||||
|
# registerBugtracker(name, url, desc, 'mantis')
|
||||||
|
return Mantis(name, url, desc, 'mantis')
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_bug(self, id):
|
def get_bug(self, id):
|
||||||
url = "%s/view.php?id=%d" % (self.url, id)
|
url = "%s/view.php?id=%d" % (self.url, id)
|
||||||
try:
|
try:
|
||||||
raw = self.soap_proxy.mc_issue_get('', '', id)
|
raw = self.soap_proxy.mc_issue_get('', '', id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Often SOAP is not enabled
|
||||||
|
if '.' in self.name:
|
||||||
|
supylog.exception(self.errget % (self.description, e, url))
|
||||||
|
return
|
||||||
raise BugtrackerError(self.errget % (self.description, e, url))
|
raise BugtrackerError(self.errget % (self.description, e, url))
|
||||||
if not raw:
|
if not raw:
|
||||||
raise BugNotFoundError
|
raise BugNotFoundError
|
||||||
@ -767,11 +896,25 @@ class Mantis(IBugtracker):
|
|||||||
# has commas, things get tricky.
|
# has commas, things get tricky.
|
||||||
# This should be more robust than the screen scraping done previously.
|
# This should be more robust than the screen scraping done previously.
|
||||||
class Trac(IBugtracker):
|
class Trac(IBugtracker):
|
||||||
|
def get_tracker(self, url):
|
||||||
|
try:
|
||||||
|
match = re.match(r'(?P<name>[^\s/]+).*/ticket', url)
|
||||||
|
name = desc = match.group('name')
|
||||||
|
url = 'https://%s' % match.group(0)
|
||||||
|
# registerBugtracker(name, url, desc, 'trac')
|
||||||
|
return Trac(name, url, desc, 'trac')
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_bug(self, id): # This is still a little rough, but it works :)
|
def get_bug(self, id): # This is still a little rough, but it works :)
|
||||||
url = "%s/%d" % (self.url, id)
|
url = "%s/%d" % (self.url, id)
|
||||||
try:
|
try:
|
||||||
raw = utils.web.getUrl("%s?format=tab" % url).decode('utf-8')
|
raw = utils.web.getUrl("%s?format=tab" % url).decode('utf-8')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Due to unreliable matching
|
||||||
|
if '.' in self.name:
|
||||||
|
supylog.exception(self.errget % (self.description, e, url))
|
||||||
|
return
|
||||||
if 'HTTP Error 500' in str(e):
|
if 'HTTP Error 500' in str(e):
|
||||||
raise BugNotFoundError
|
raise BugNotFoundError
|
||||||
raise BugtrackerError(self.errget % (self.description, e, url))
|
raise BugtrackerError(self.errget % (self.description, e, url))
|
||||||
@ -807,6 +950,7 @@ registerBugtracker('gnome', 'https://bugzilla.gnome.org', 'Gnome', 'bugzilla')
|
|||||||
registerBugtracker('gnome2', 'https://bugs.gnome.org', 'Gnome', 'bugzilla')
|
registerBugtracker('gnome2', 'https://bugs.gnome.org', 'Gnome', 'bugzilla')
|
||||||
registerBugtracker('kde', 'https://bugs.kde.org', 'KDE', 'bugzilla')
|
registerBugtracker('kde', 'https://bugs.kde.org', 'KDE', 'bugzilla')
|
||||||
registerBugtracker('xfce', 'https://bugzilla.xfce.org', 'Xfce', 'bugzilla')
|
registerBugtracker('xfce', 'https://bugzilla.xfce.org', 'Xfce', 'bugzilla')
|
||||||
|
registerBugtracker('lxde', 'https://sourceforge.net/p/lxde/bugs', 'LXDE', 'sourceforge')
|
||||||
registerBugtracker('freedesktop', 'https://bugzilla.freedesktop.org', 'Freedesktop', 'bugzilla')
|
registerBugtracker('freedesktop', 'https://bugzilla.freedesktop.org', 'Freedesktop', 'bugzilla')
|
||||||
registerBugtracker('freedesktop2', 'https://bugs.freedesktop.org', 'Freedesktop', 'bugzilla')
|
registerBugtracker('freedesktop2', 'https://bugs.freedesktop.org', 'Freedesktop', 'bugzilla')
|
||||||
registerBugtracker('openoffice', 'https://bz.apache.org/ooo', 'OpenOffice', 'bugzilla')
|
registerBugtracker('openoffice', 'https://bz.apache.org/ooo', 'OpenOffice', 'bugzilla')
|
||||||
@ -815,7 +959,9 @@ registerBugtracker('ubottu', 'https://launchpad.net', 'Ubottu', 'launchpad')
|
|||||||
registerBugtracker('launchpad', 'https://launchpad.net', 'Launchpad', 'launchpad')
|
registerBugtracker('launchpad', 'https://launchpad.net', 'Launchpad', 'launchpad')
|
||||||
registerBugtracker('lp', 'https://launchpad.net', 'Launchpad', 'launchpad')
|
registerBugtracker('lp', 'https://launchpad.net', 'Launchpad', 'launchpad')
|
||||||
registerBugtracker('debian', 'https://bugs.debian.org', 'Debian', 'debbugs')
|
registerBugtracker('debian', 'https://bugs.debian.org', 'Debian', 'debbugs')
|
||||||
|
registerBugtracker('supybot', 'https://sourceforge.net/p/supybot/bugs', 'Supybot', 'sourceforge')
|
||||||
|
registerBugtracker('irssi', 'https://github.com/irssi/irssi/issues', 'irssi/irssi', 'github')
|
||||||
registerBugtracker('mantis', 'https://www.mantisbt.org/bugs', 'Mantis', 'mantis')
|
registerBugtracker('mantis', 'https://www.mantisbt.org/bugs', 'Mantis', 'mantis')
|
||||||
registerBugtracker('trac', 'https://trac.edgewall.org/ticket', 'Trac', 'trac')
|
registerBugtracker('trac', 'https://trac.edgewall.org/ticket', 'Trac', 'trac')
|
||||||
registerBugtracker('django', 'https://code.djangoproject.com/ticket', 'Django', 'trac')
|
registerBugtracker('pidgin', 'https://developer.pidgin.im/ticket', 'Pidgin', 'trac')
|
||||||
Class = Bugtracker
|
Class = Bugtracker
|
||||||
|
Reference in New Issue
Block a user