Exemple #1
0
    def etym(self, irc, msg, args, etym):
        """queries etymonline.com for word etymology"""

        try:
            req = requests.get(
                "https://www.etymonline.com/word/{}".format(etym))

            soup = bs4.BeautifulSoup(req.text, 'html.parser')

            word_soup = soup.find('div', {'class': 'word--C9UPa'})
            if not word_soup:
                irc.reply("'{}' not found".format(etym))
                return

            irc_reply = ""
            for word in word_soup:
                irc_reply += "{}: {} ".format(word.div.h1.text,
                                              word.div.section.text)
            log.info(irc_reply)

            irc.reply(irc_reply)

        except Exception as e:
            log.exception()
            irc.reply("Error! Send the log to Druid@Freenode")
Exemple #2
0
 def _runCommandFunction(self, irc, msg, command):
     """Run a command from message, as if command was sent over IRC."""
     tokens = callbacks.tokenize(command)        
     try:
         self.Proxy(irc.irc, msg, tokens)
     except Exception, e:
         log.exception('Uncaught exception in function called by MessageParser:')
Exemple #3
0
def flush():
    """Flushes all the registered flushers."""
    for (i, f) in enumerate(flushers):
        try:
            f()
        except Exception, e:
            log.exception('Uncaught exception in flusher #%s (%s):', i, f)
Exemple #4
0
    def __init__(self, *args, **kwargs):
        IBugtracker.__init__(self, *args, **kwargs)
        self.lp = None

        # A word to the wise:
        # The Launchpad API is much better than the /+text interface we currently use,
        # it's faster and easier to get the information we need.
        # The current /+text interface is not really maintained by Launchpad and most,
        # or all, of the Launchpad developers hate it. For this reason, we are dropping
        # support for /+text in the future in favour of launchpadlib.
        # Terence Simpson (tsimpson) 2010-04-20

        try: # Attempt to use launchpadlib, python bindings for the Launchpad API
            from launchpadlib.launchpad import Launchpad
            cachedir = os.path.join(conf.supybot.directories.data.tmp(), 'lpcache')
            if hasattr(Launchpad, 'login_anonymously'):
                self.lp = Launchpad.login_anonymously("Ubuntu Bots - Bugtracker", 'production', cachedir)
            else: #NOTE: Most people should have a launchpadlib new enough for .login_anonymously
                self.lp = Launchpad.login("Ubuntu Bots - Bugtracker", '', '', 'production', cahedir)
        except ImportError:
            # Ask for launchpadlib to be installed
            supylog.warning("Please install python-launchpadlib, the old interface is deprecated")
        except Exception: # Something unexpected happened
            self.lp = None
            supylog.exception("Unknown exception while accessing the Launchpad API")
Exemple #5
0
    def doPost(self, handler, path, form):
        if path != "/":
            self.send_response(404)
            self.send_header("Content-type", "text/plain")
            self.end_headers()
            self.wfile.write(b"404: page not found")
            return

        headers = {
            key.lower(): value
            for key, value in dict(self.headers).items()
        }

        try:
            asyncio.run(github.dispatch(headers, form))

            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"200: OK")
        except Exception:
            log.exception("Failed to handle GitHub event")
            self.send_response(403)
            self.send_header("Content-type", "text/plain")
            self.end_headers()
            self.wfile.write(b"403: failed processing event")
Exemple #6
0
def flush():
    """Flushes all the registered flushers."""
    for (i, f) in enumerate(flushers):
        try:
            f()
        except Exception, e:
            log.exception("Uncaught exception in flusher #%s (%s):", i, f)
Exemple #7
0
    def get_bug(self, id): # This is still a little rough, but it works :)
        url = "%s/%d" % (self.url, id)
        try:
            raw = utils.web.getUrl("%s?format=tab" % url).decode('utf-8')
        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):
                raise BugNotFoundError
            raise BugtrackerError(self.errget % (self.description, e, url))
        raw = raw.replace('\r\n', '\n')
        (headers, rest) = raw.split('\n', 1)
        headers = headers.strip().split('\t')
        rest = rest.strip().split('\t')

        title = status = package = severity = assignee = ""
        if "summary" in headers:
            title = rest[headers.index("summary")]
        if "status" in headers:
            status = rest[headers.index("status")]
        if "component" in headers:
            package = rest[headers.index("component")]
        if "severity" in headers:
            severity = rest[headers.index("severity")]
        elif "priority" in headers:
            severity = rest[headers.index("priority")]
        if "owner" in headers:
            assignee = rest[headers.index("owner")]
        return (id, package, title, severity, status, assignee, url, [], [])
Exemple #8
0
 def _loadPlugins(self, irc):
     self.log.info('Loading plugins (connecting to %s).', irc.network)
     alwaysLoadImportant = conf.supybot.plugins.alwaysLoadImportant()
     important = conf.supybot.commands.defaultPlugins.importantPlugins()
     for (name, value) in conf.supybot.plugins.getValues(fullNames=False):
         if irc.getCallback(name) is None:
             load = value()
             if not load and name in important:
                 if alwaysLoadImportant:
                     s = '%s is configured not to be loaded, but is being '\
                         'loaded anyway because ' \
                         'supybot.plugins.alwaysLoadImportant is True.'
                     self.log.warning(s, name)
                     load = True
             if load:
                 # We don't load plugins that don't start with a capital
                 # letter.
                 if name[0].isupper() and not irc.getCallback(name):
                     # This is debug because each log logs its beginning.
                     self.log.debug('Loading %s.', name)
                     try:
                         m = plugin.loadPluginModule(name,
                                                     ignoreDeprecation=True)
                         plugin.loadPluginClass(irc, m)
                     except callbacks.Error, e:
                         # This is just an error message.
                         log.warning(str(e))
                     except (plugins.NoSuitableDatabase, ImportError), e:
                         s = 'Failed to load %s: %s' % (name, e)
                         if not s.endswith('.'):
                             s += '.'
                         log.warning(s)
                     except Exception, e:
                         log.exception('Failed to load %s:', name)
Exemple #9
0
def startServer():
    """Starts the HTTP server. Shouldn't be called from other modules.
    The callback should be an instance of a child of SupyHTTPServerCallback."""
    global http_servers
    addresses4 = [(4, (x, configGroup.port())) for x in configGroup.hosts4()
                  if x != '']
    addresses6 = [(6, (x, configGroup.port())) for x in configGroup.hosts6()
                  if x != '']
    http_servers = []
    for protocol, address in (addresses4 + addresses6):
        try:
            server = SupyHTTPServer(address, protocol, SupyHTTPRequestHandler)
        except OSError as e:
            log.error(
                'Failed to start HTTP server with protocol %s at address: %s',
                protocol, address, e)
            if e.args[0] == 98:
                log.error(
                    'This means the port (and address) is already in use by an '
                    'other process. Either find the process using the port '
                    'and stop it, or change the port configured in '
                    'supybot.servers.http.port.')
            continue
        except:
            log.exception(
                "Failed to start HTTP server with protocol %s at address",
                protocol, address)
            continue
        Thread(target=server.serve_forever, name='HTTP Server').start()
        http_servers.append(server)
        log.info('Starting HTTP server: %s' % str(server))
 def implementation():
     try:
         source.update()
     except Exception as e:
         log.exception('Failed to update {}: {}'.format(
             source.NAME, e))
     self._topic_callback()
Exemple #11
0
 def _loadPlugins(self, irc):
     self.log.info('Loading plugins (connecting to %s).', irc.network)
     alwaysLoadImportant = conf.supybot.plugins.alwaysLoadImportant()
     important = conf.supybot.commands.defaultPlugins.importantPlugins()
     for (name, value) in conf.supybot.plugins.getValues(fullNames=False):
         if irc.getCallback(name) is None:
             load = value()
             if not load and name in important:
                 if alwaysLoadImportant:
                     s = '%s is configured not to be loaded, but is being '\
                         'loaded anyway because ' \
                         'supybot.plugins.alwaysLoadImportant is True.'
                     self.log.warning(s, name)
                     load = True
             if load:
                 # We don't load plugins that don't start with a capital
                 # letter.
                 if name[0].isupper() and not irc.getCallback(name):
                     # This is debug because each log logs its beginning.
                     self.log.debug('Loading %s.', name)
                     try:
                         m = plugin.loadPluginModule(name,
                                                     ignoreDeprecation=True)
                         plugin.loadPluginClass(irc, m)
                     except callbacks.Error, e:
                         # This is just an error message.
                         log.warning(str(e))
                     except (plugins.NoSuitableDatabase, ImportError), e:
                         s = 'Failed to load %s: %s' % (name, e)
                         if not s.endswith('.'):
                             s += '.'
                         log.warning(s)
                     except Exception, e:
                         log.exception('Failed to load %s:', name)
Exemple #12
0
 def _runCommandFunction(self, irc, msg, command):
     """Run a command from message, as if command was sent over IRC."""
     tokens = callbacks.tokenize(command)        
     try:
         self.Proxy(irc.irc, msg, tokens)
     except Exception, e:
         log.exception('Uncaught exception in function called by MessageParser:')
Exemple #13
0
    def get_bug_new(self, id): #TODO: Rename this method to 'get_bug'
        try:
            bugdata = self.lp.bugs[id]
            if bugdata.private:
                raise BugtrackerError, "This bug is private"
            dup = bugdata.duplicate_of
            summary_prefix = '' # Used to made dups easier
            while dup:
                summary_prefix = 'duplicate for #%d ' % id
                bugdata = dup
                dup = bugdata.duplicate_of

            affected = bugdata.users_affected_count_with_dupes
            heat = bugdata.heat
            tasks = bugdata.bug_tasks

            if tasks.total_size != 1:
                tasks = list(tasks)
                try:
                    tasks.sort(self._sort)
                    taskdata = tasks[-1]
                except ValueError:
                    tasks = [_ for _ in tasks if _.bug_target_name.endswith(u'(Ubuntu)')]
                    if tasks:
                        if len(tasks) != 1:
                            try:
                                tasks.sort(self._sort)
                                taskdata = tasks[-1]
                            except ValueError:
                                taskdata = bugdata.bug_tasks[bugdata.bug_tasks.total_size - 1]
                        else:
                            taskdata = tasks[-1]
                    else:
                        taskdata = tasks[-1]
            else:
                taskdata = tasks[0]

            assignee = taskdata.assignee
            t = taskdata.bug_target_display_name #task name

            if assignee: # "Diaplay Name (Launchpad ID)"
                assignee = u"%s (%s)" % (assignee.display_name, assignee.name)
            else:
                assignee = ''

        except Exception, e:
            if type(e).__name__ == 'HTTPError': # messy, but saves trying to import lazr.restfulclient.errors.HTPError
                if e.response.status == 404:
                    bugNo = e.content.split(None)[-1][2:-1] # extract the real bug number
                    if bugNo != str(id): # A duplicate of a private bug, at least we know it exists
                        raise BugtrackerError, 'Bug #%s is a duplicate of bug #%s, but it is private (%s/bugs/%s)' % (id, bugNo, self.url, bugNo)
                    raise BugtrackerError, "Bug #%s (%s/bugs/%d) is private or doesn't exist" % (id, self.url, id) # Could be private, could just not exist

                supylog.exception("Error gathering bug data for %s bug #%d" % (self.description, id))
                raise BugtrackerError, "Could not gather data from %s for bug #%s (%s/bugs/%s). The error has been logged" % (self.description, id, self.url, id)
            elif isinstance(e, KeyError):
                raise BugNotFoundError
            supylog.exception("Error gathering bug data for %s bug %d" % (self.description, id))
            raise BugtrackerError, "Could not gather data from %s for bug #%s (%s/bugs/%s). The error has been logged" % (self.description, id, self.url, id)
Exemple #14
0
 def newf(*args, **kwargs):
     for x in xrange(0, 3):
         try:
             return f(*args, **kwargs)
         except Exception:
             log.exception('Shrinking URL failed. Trying again.')
             time.sleep(1)
     return f(*args, **kwargs)
Exemple #15
0
 def newf(*args, **kwargs):
     for x in range(0, 3):
         try:
             return f(*args, **kwargs)
         except Exception:
             log.exception('Shrinking URL failed. Trying again.')
             time.sleep(1)
     return f(*args, **kwargs)
Exemple #16
0
    def _email_callback(self, fileobj):
        try:
            email = parse_mail(fileobj)
            msg = get_message(email, new_queue=self.new_queue)

            if not msg:
                return

            txt = colourise(msg.for_irc())

            # Simple flood/duplicate detection
            if txt in self.last_n_messages:
                return
            self.last_n_messages.insert(0, txt)
            self.last_n_messages = self.last_n_messages[:20]

            for channel in self.irc.state.channels:
                package_regex = self.registryValue(
                    'package_regex',
                    channel,
                ) or 'a^' # match nothing by default

                package_match = re.search(package_regex, msg.package)

                maintainer_match = False
                maintainer_regex = self.registryValue(
                    'maintainer_regex',
                    channel)
                if maintainer_regex:
                    info = Maintainer().get_maintainer(msg.package)
                    if info:
                        maintainer_match = re.search(maintainer_regex, info['email'])

                if not package_match and not maintainer_match:
                    continue

                distribution_regex = self.registryValue(
                    'distribution_regex',
                    channel,
                )

                if distribution_regex:
                    if not hasattr(msg, 'distribution'):
                        # If this channel has a distribution regex, don't
                        # bother continuing unless the message actually has a
                        # distribution. This filters security messages, etc.
                        continue

                    if not re.search(distribution_regex, msg.distribution):
                        # Distribution doesn't match regex; don't send this
                        # message.
                        continue

                ircmsg = supybot.ircmsgs.privmsg(channel, txt)
                self.irc.queueMsg(ircmsg)

        except Exception as e:
            log.exception('Uncaught exception: %s ' % e)
 def _loadPlugins(self, irc):
     self.log.info('Loading plugins (connecting to %s).', irc.network)
     alwaysLoadImportant = conf.supybot.plugins.alwaysLoadImportant()
     important = conf.supybot.commands.defaultPlugins.importantPlugins()
     for (name, value) in conf.supybot.plugins.getValues(fullNames=False):
         if irc.getCallback(name) is None:
             load = value()
             if not load and name in important:
                 if alwaysLoadImportant:
                     s = '%s is configured not to be loaded, but is being '\
                         'loaded anyway because ' \
                         'supybot.plugins.alwaysLoadImportant is True.'
                     self.log.warning(s, name)
                     load = True
             if load:
                 # We don't load plugins that don't start with a capital
                 # letter.
                 if name[0].isupper() and not irc.getCallback(name):
                     # This is debug because each log logs its beginning.
                     self.log.debug('Loading %s.', name)
                     try:
                         m = plugin.loadPluginModule(name,
                                                     ignoreDeprecation=True)
                         plugin.loadPluginClass(irc, m)
                     except callbacks.Error as e:
                         # This is just an error message.
                         log.warning(str(e))
                     except plugins.NoSuitableDatabase as e:
                         s = 'Failed to load %s: no suitable database(%s).' % (
                             name, e)
                         log.warning(s)
                     except ImportError as e:
                         e = str(e)
                         if e.endswith(name):
                             s = 'Failed to load {0}: No plugin named {0} exists.'.format(
                                 utils.str.dqrepr(name))
                         elif "No module named 'config'" in e:
                             s = (
                                 "Failed to load %s: This plugin may be incompatible "
                                 "with your current Python version. If this error is appearing "
                                 "with stock Supybot plugins, remove the stock plugins directory "
                                 "(usually ~/Limnoria/plugins) from 'config directories.plugins'." %
                                 name)
                         else:
                             s = 'Failed to load %s: import error (%s).' % (
                                 name, e)
                         log.warning(s)
                     except Exception as e:
                         log.exception('Failed to load %s:', name)
             else:
                 # Let's import the module so configuration is preserved.
                 try:
                     _ = plugin.loadPluginModule(name)
                 except Exception as e:
                     log.debug('Attempted to load %s to preserve its '
                               'configuration, but load failed: %s',
                               name, e)
     world.starting = False
Exemple #18
0
 def _loadPlugins(self, irc):
     self.log.debug('Loading plugins (connecting to %s).', irc.network)
     alwaysLoadImportant = conf.supybot.plugins.alwaysLoadImportant()
     important = conf.supybot.commands.defaultPlugins.importantPlugins()
     for (name, value) in conf.supybot.plugins.getValues(fullNames=False):
         if irc.getCallback(name) is None:
             load = value()
             if not load and name in important:
                 if alwaysLoadImportant:
                     s = '%s is configured not to be loaded, but is being '\
                         'loaded anyway because ' \
                         'supybot.plugins.alwaysLoadImportant is True.'
                     self.log.warning(s, name)
                     load = True
             if load:
                 # We don't load plugins that don't start with a capital
                 # letter.
                 if name[0].isupper() and not irc.getCallback(name):
                     # This is debug because each log logs its beginning.
                     self.log.debug('Loading %s.', name)
                     try:
                         m = plugin.loadPluginModule(name,
                                                     ignoreDeprecation=True)
                         plugin.loadPluginClass(irc, m)
                     except callbacks.Error as e:
                         # This is just an error message.
                         log.warning(str(e))
                     except plugins.NoSuitableDatabase as e:
                         s = 'Failed to load %s: no suitable database(%s).' % (
                             name, e)
                         log.warning(s)
                     except ImportError as e:
                         e = str(e)
                         if e.endswith(name):
                             s = 'Failed to load {0}: No plugin named {0} exists.'.format(
                                 utils.str.dqrepr(name))
                         elif "No module named 'config'" in e:
                             s = (
                                 "Failed to load %s: This plugin may be incompatible "
                                 "with your current Python version. If this error is appearing "
                                 "with stock Supybot plugins, remove the stock plugins directory "
                                 "(usually ~/Limnoria/plugins) from 'config directories.plugins'."
                                 % name)
                         else:
                             s = 'Failed to load %s: import error (%s).' % (
                                 name, e)
                         log.warning(s)
                     except Exception as e:
                         log.exception('Failed to load %s:', name)
             else:
                 # Let's import the module so configuration is preserved.
                 try:
                     _ = plugin.loadPluginModule(name)
                 except Exception as e:
                     log.debug(
                         'Attempted to load %s to preserve its '
                         'configuration, but load failed: %s', name, e)
     world.starting = False
Exemple #19
0
 def flush(self):
     """Exports the database to a file."""
     tmp_filename = self.filename + '.tmp'
     try:
         with open(tmp_filename, 'wb') as f:
             pickle.dump(self.db, f, 2)
         os.rename(tmp_filename, self.filename)
     except Exception as e:
         log.exception('%s: Unable to write database: %s', self._plugin_name, e)
Exemple #20
0
 def _dispatch(self, name, args):
     if name not in self.connector.rpcMethods:
         raise TypeError('no such method (%s)' % name)
     try:
         return self.connector.rpcMethods[name](*args)
     except Fault:
         raise
     except:
         log.exception('Unhandled exception in Trac XMLRPC:')
         raise
Exemple #21
0
 def _dispatch(self, name, args):
     if name not in self.connector.rpcMethods:
         raise TypeError('no such method (%s)' % name)
     try:
         return self.connector.rpcMethods[name](*args)
     except Fault:
         raise
     except:
         log.exception('Unhandled exception in Trac XMLRPC:')
         raise
Exemple #22
0
 def __del__(self):
     try:
         Connection.__del__(self)
     except AttributeError:
         pass
     except Exception, e:
         try:
             log.exception('Uncaught exception in __del__:')
         except:
             pass
Exemple #23
0
 def __del__(self):
     try:
         Connection.__del__(self)
     except AttributeError:
         pass
     except Exception, e:
         try:
             log.exception('Uncaught exception in __del__:')
         except:
             pass
Exemple #24
0
 def _runCommandFunction(self, irc, msg, command):
     """Run a command from message, as if command was sent over IRC."""
     try:
         tokens = callbacks.tokenize(command,
             channel=msg.channel, network=irc.network)
     except SyntaxError as e:
         # Emulate what callbacks.py does
         self.log.debug('Error return: %s', utils.exnToString(e))
         irc.error(str(e))
     try:
         self.Proxy(irc.irc, msg, tokens)
     except Exception as e:
         log.exception('Uncaught exception in function called by MessageParser:')
Exemple #25
0
 def run(self):
     if len(drivers._drivers) == 1 and not world.testing:
         log.error('Schedule is the only remaining driver, '
                   'why do we continue to live?')
         time.sleep(1)  # We're the only driver; let's pause to think.
     while self.schedule and self.schedule[0][0] < time.time():
         (t, name) = heapq.heappop(self.schedule)
         f = self.events[name]
         del self.events[name]
         try:
             f()
         except Exception, e:
             log.exception('Uncaught exception in scheduled function:')
 def run(self):
     if len(drivers._drivers) == 1 and not world.testing:
         log.error('Schedule is the only remaining driver, '
                   'why do we continue to live?')
         time.sleep(1) # We're the only driver; let's pause to think.
     while self.schedule and self.schedule[0][0] < time.time():
         (t, name) = heapq.heappop(self.schedule)
         f = self.events[name]
         del self.events[name]
         try:
             f()
         except Exception, e:
             log.exception('Uncaught exception in scheduled function:')
Exemple #27
0
 def open(self, filename):
     self.filename = filename
     reader = unpreserve.Reader(IrcUserCreator, self)
     try:
         self.noFlush = True
         try:
             reader.readFile(filename)
             self.noFlush = False
             self.flush()
         except EnvironmentError, e:
             log.error('Invalid user dictionary file, resetting to empty.')
             log.error('Exact error: %s', utils.exnToString(e))
         except Exception, e:
             log.exception('Exact error:')
Exemple #28
0
 def open(self, filename):
     self.filename = filename
     reader = unpreserve.Reader(IrcUserCreator, self)
     try:
         self.noFlush = True
         try:
             reader.readFile(filename)
             self.noFlush = False
             self.flush()
         except EnvironmentError, e:
             log.error('Invalid user dictionary file, resetting to empty.')
             log.error('Exact error: %s', utils.exnToString(e))
         except Exception, e:
             log.exception('Exact error:')
Exemple #29
0
 def open(self, filename):
     self.noFlush = True
     try:
         self.filename = filename
         reader = unpreserve.Reader(IrcChannelCreator, self)
         try:
             reader.readFile(filename)
             self.noFlush = False
             self.flush()
         except EnvironmentError, e:
             log.error("Invalid channel database, resetting to empty.")
             log.error("Exact error: %s", utils.exnToString(e))
         except Exception, e:
             log.error("Invalid channel database, resetting to empty.")
             log.exception("Exact error:")
Exemple #30
0
 def get_bug_old(self, id): # Deprecated
     url = "%s/view.php?id=%d" % (self.url, id)
     try:
         raw = self.soap_client.mc_issue_get(username='', password='', issue_id=id)
     except Exception as e:
         if 'Issue #%d not found' % id in str(e):
             raise BugNotFoundError
         # 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))
     if not hasattr(raw, 'id'):
         raise BugNotFoundError
     try:
         return (id, str(raw.project.name), str(raw.summary), str(raw.severity.name), str(raw.resolution.name), '', url, [], [])
     except Exception as e:
         raise BugtrackerError(self.errparse % (self.description, e, url))
Exemple #31
0
    def __init__(self, filename):
        self.dbs = {}
        cdb = conf.supybot.databases.types.cdb

        services = list(conf.supybot.plugins.ShrinkUrl.default.validStrings)
        services.append('Expand')
        for service in services:
            dbname = filename.replace('.db', service.capitalize() + '.db')
            try:
                self.dbs[service] = cdb.connect(dbname)
            except OSError as e:
                log.error('ShrinkUrl: Can not open database %s: %s', dbname, e)
                raise KeyError("Could not open %s" % dbname)
            except:
                log.exception(
                    'ShrinkUrl: Can not read database %s (data corruption?)',
                    dbname)
                raise KeyError("Could not open %s" % dbname)
Exemple #32
0
    def __init__(self, *args, **kwargs):
        IBugtracker.__init__(self, *args, **kwargs)
        self.lp = None

        # A word to the wise:
        # The Launchpad API is much better than the /+text interface we currently use,
        # it's faster and easier to get the information we need.
        # The current /+text interface is not really maintained by Launchpad and most,
        # or all, of the Launchpad developers hate it. For this reason, we are dropping
        # support for /+text in the future in favour of launchpadlib.
        # Terence Simpson (tsimpson) 2010-04-20

        try:
            from launchpadlib.launchpad import Launchpad
            cachedir = os.path.join(conf.supybot.directories.data.tmp(), 'launchpadlib')
            self.lp = Launchpad.login_anonymously("Ubuntu Bots - Bugtracker", 'production', cachedir, version='devel')
        except ImportError:
            supylog.warning("Please install python-launchpadlib, the old interface is deprecated")
        except Exception:
            self.lp = None
            supylog.exception("Unknown exception while accessing the Launchpad API")
    def process_mails(self):
        log.info("Mail processing thread started.")
        while not self.quit:
            with self.cv:
                while not self.quit and len(self.messages) == 0:
                    log.debug("Waiting for new mail.")
                    self.cv.wait()

                if self.quit:
                    log.info("Mail processing thread stopeed.")
                    self.thread = None
                    return

                mail = self.messages.popleft()
                log.debug("Got mail.")

            mail = base64.b64decode(mail.encode('ascii'))
            try:
                self.callback(BytesIO(mail))
            except Exception as e:
                log.exception('Uncaught exception: {}'.format(e))

        self.thread = None
    def process_mails(self):
        log.debug("Mail processing thread started.")
        while not self.quit:
            with self.cv:
                while not self.quit and len(self.messages) == 0:
                    log.debug("Waiting for new mail.")
                    self.cv.wait()

                if self.quit:
                    log.debug("Stopping.")
                    self.thread = None
                    return

                mail = self.messages.popleft()
                log.debug("Got mail.")

            mail = base64.b64decode(mail.encode('ascii'))
            try:
                self.callback(BytesIO(mail))
            except Exception as e:
                log.exception('Uncaught exception: {}'.format(e))

        self.thread = None
    def _email_callback(self, fileobj):
        try:
            email = parse_mail(fileobj)
            msg = get_message(email)

            if not msg:
                return

            txt = colourise(msg.for_irc())

            # Simple flood/duplicate detection
            if txt in self.last_n_messages:
                return
            self.last_n_messages.insert(0, txt)
            self.last_n_messages = self.last_n_messages[:20]

            for channel in self.irc.state.channels:
                regex = self.registryValue('package_regex', channel) or 'a^'
                if re.search(regex, msg.package):
                    ircmsg = supybot.ircmsgs.privmsg(channel, txt)
                    self.irc.queueMsg(ircmsg)

        except:
            log.exception('Uncaught exception')
Exemple #36
0
    def np(self, irc, msg, args, user):
        """[<user>]

        Announces the track currently being played by <user>. If <user>
        is not given, defaults to the LastFM user configured for your
        current nick.
        """
        apiKey = self.registryValue("apiKey")
        if not apiKey:
            irc.error("The API Key is not set. Please set it via "
                      "'config plugins.lastfm.apikey' and reload the plugin. "
                      "You can sign up for an API Key using "
                      "http://www.last.fm/api/account/create", Raise=True)
        user = (user or self.db.get(msg.prefix) or msg.nick)

        # see http://www.lastfm.de/api/show/user.getrecenttracks
        url = "%sapi_key=%s&method=user.getrecenttracks&user=%s&format=json" % (self.APIURL, apiKey, user)
        try:
            f = utils.web.getUrl(url).decode("utf-8")
        except utils.web.Error:
            irc.error("Unknown user %s." % user, Raise=True)
        self.log.debug("LastFM.nowPlaying: url %s", url)

        try:
            data = json.loads(f)["recenttracks"]
        except KeyError:
            irc.error("Unknown user %s." % user, Raise=True)

        user = data["@attr"]["user"]
        tracks = data["track"]

        # Work with only the first track.
        try:
            trackdata = tracks[0]
        except IndexError:
            irc.error("%s doesn't seem to have listened to anything." % user, Raise=True)

        artist = trackdata["artist"]["#text"].strip()  # Artist name
        track = trackdata["name"].strip()  # Track name
        # Album name (may or may not be present)
        album = trackdata["album"]["#text"].strip()
        if album:
            album = ircutils.bold("[%s]" % album)

        try:
            time = int(trackdata["date"]["uts"])  # Time of last listen
            # Format this using the preferred time format.
            tformat = conf.supybot.reply.format.time()
            time = "at %s" % datetime.fromtimestamp(time).strftime(tformat)
        except KeyError:  # Nothing given by the API?
            time = "just now"

        public_url = ''
        # If the DDG plugin from this repository is loaded, we can integrate
        # that by finding a YouTube link for the track.
        if self.registryValue("fetchYouTubeLink", msg.args[0]):
            ddg = irc.getCallback("DDG")
            if ddg:
                try:
                    results = ddg.search_core('site:youtube.com "%s - %s"' % (artist, track),
                                              channel_context=msg.args[0], max_results=1, show_snippet=False)
                    if results:
                        public_url = format('%u', results[0][2])
                except:
                    # If something breaks, log the error but don't cause the
                    # entire np request to fail.
                    log.exception("LastFM: failed to get YouTube link for track %s - %s", artist, track)

        ext_info = ''
        if self.registryValue("showExtendedInfo", msg.args[0]):
            # Get extended info via a separate API call.
            ext_info_url = "%sapi_key=%s&method=track.getinfo&user=%s&format=json&artist=%s&track=%s" % (self.APIURL, apiKey, user,
                utils.web.urlquote(artist), utils.web.urlquote(track))
            ext_info_f = utils.web.getUrl(ext_info_url).decode("utf-8")
            self.log.debug("LastFM.nowPlaying: using url %s for extended info", ext_info_url)
            try:
                ext_data = json.loads(ext_info_f)['track']

                # We currently show play count and tags - more could be added in the future...
                userplaycount = ext_data['userplaycount']
                tags = [tag['name'] for tag in ext_data['toptags']['tag']]
                ext_info = ' (Playcount: %s / Tags: %s)' % (userplaycount, ', '.join(tags) or 'N/A')
            except KeyError:
                pass

        s = '%s listened to %s by %s %s %s%s. %s' % (ircutils.bold(user), ircutils.bold(track),
            ircutils.bold(artist), album, time, ext_info, public_url)
        irc.reply(utils.str.normalizeWhitespace(s))
    def _email_callback(self, fileobj):
        try:
            emailmsg = parse_mail(fileobj)
            msg = get_message(emailmsg, new_queue=self.new_queue)

            if not msg:
                return

            txt = colourise(msg.for_irc())

            # Simple flood/duplicate detection
            if txt in self.last_n_messages:
                return
            self.last_n_messages.insert(0, txt)
            self.last_n_messages = self.last_n_messages[:20]

            maintainer_info = None
            if hasattr(msg, 'maintainer'):
                maintainer_info = (split_address(msg.maintainer), )
            else:
                maintainer_info = []
                for package in msg.package.split(','):
                    package = package.strip()
                    try:
                        maintainer_info.append(
                            self.apt_archive.get_maintainer(package))
                    except NewDataSource.DataError as e:
                        log.info("Failed to query maintainer for {}.".format(
                            package))

            for channel in self.irc.state.channels:
                package_regex = self.registryValue(
                    'package_regex',
                    channel,
                ) or 'a^'  # match nothing by default

                package_match = re.search(package_regex, msg.package)

                maintainer_match = False
                maintainer_regex = self.registryValue('maintainer_regex',
                                                      channel)
                if maintainer_regex and maintainer_info is not None and len(
                        maintainer_info) >= 0:
                    for mi in maintainer_info:
                        maintainer_match = re.search(maintainer_regex,
                                                     mi['email'])
                        if maintainer_match:
                            break

                if not package_match and not maintainer_match:
                    continue

                distribution_regex = self.registryValue(
                    'distribution_regex',
                    channel,
                )

                if distribution_regex:
                    if not hasattr(msg, 'distribution'):
                        # If this channel has a distribution regex, don't
                        # bother continuing unless the message actually has a
                        # distribution. This filters security messages, etc.
                        continue

                    if not re.search(distribution_regex, msg.distribution):
                        # Distribution doesn't match regex; don't send this
                        # message.
                        continue

                send_privmsg = self.registryValue('send_privmsg', channel)
                # Send NOTICE per default and if 'send_privmsg' is set for the
                # channel, send PRIVMSG instead.
                if send_privmsg:
                    ircmsg = supybot.ircmsgs.privmsg(channel, txt)
                else:
                    ircmsg = supybot.ircmsgs.notice(channel, txt)

                self.irc.queueMsg(ircmsg)

        except Exception as e:
            log.exception('Uncaught exception: %s ' % e)
    def _email_callback(self, fileobj):
        try:
            emailmsg = parse_mail(fileobj)
            msg = get_message(emailmsg, new_queue=self.new_queue)

            if not msg:
                return

            txt = colourise(msg.for_irc())

            # Simple flood/duplicate detection
            if txt in self.last_n_messages:
                return
            self.last_n_messages.insert(0, txt)
            self.last_n_messages = self.last_n_messages[:20]

            maintainer_info = None
            if hasattr(msg, 'maintainer'):
                maintainer_info = (split_address(msg.maintainer), )
            else:
                maintainer_info = []
                for package in msg.package.split(','):
                    package = package.strip()
                    try:
                        maintainer_info.append(self.apt_archive.get_maintainer(package))
                    except NewDataSource.DataError as e:
                        log.info("Failed to query maintainer for {}.".format(package))

            for channel in self.irc.state.channels:
                package_regex = self.registryValue(
                    'package_regex',
                    channel,
                ) or 'a^'  # match nothing by default

                package_match = re.search(package_regex, msg.package)

                maintainer_match = False
                maintainer_regex = self.registryValue(
                    'maintainer_regex',
                    channel)
                if maintainer_regex and maintainer_info is not None and len(maintainer_info) >= 0:
                    for mi in maintainer_info:
                        maintainer_match = re.search(maintainer_regex, mi['email'])
                        if maintainer_match:
                            break

                if not package_match and not maintainer_match:
                    continue

                distribution_regex = self.registryValue(
                    'distribution_regex',
                    channel,
                )

                if distribution_regex:
                    if not hasattr(msg, 'distribution'):
                        # If this channel has a distribution regex, don't
                        # bother continuing unless the message actually has a
                        # distribution. This filters security messages, etc.
                        continue

                    if not re.search(distribution_regex, msg.distribution):
                        # Distribution doesn't match regex; don't send this
                        # message.
                        continue

                send_privmsg = self.registryValue('send_privmsg', channel)
                # Send NOTICE per default and if 'send_privmsg' is set for the
                # channel, send PRIVMSG instead.
                if send_privmsg:
                    ircmsg = supybot.ircmsgs.privmsg(channel, txt)
                else:
                    ircmsg = supybot.ircmsgs.notice(channel, txt)

                self.irc.queueMsg(ircmsg)

        except Exception as e:
            log.exception('Uncaught exception: %s ' % e)
Exemple #39
0
	def _ticker(self, irc, ticker):
		prev = dict()
		while True:
			if self.stop:
				return
			try:
				log.debug("refreshing ticker %r", ticker)
				data = dict()
				for key, url in TICKERS[ticker]['urls'].iteritems():
					log.debug("fetching url %r", url)
					try:
						t0 = time.time()
						data[key] = json.load(urllib2.urlopen(url, timeout=30))
					except Exception, e:
						log.exception("error fetching %r", url)
						continue
					log.info("fetched %r in %.2f s", url, time.time() - t0)
					log.debug("data = %r", data[key])
				if not data:
					raise Exception("no data for ticker %s" % repr(ticker))
				for output, o in TICKERS[ticker]['outputs'].iteritems():
					log.debug("processing output %r: %r", output, o)
					values = []
					diffstring = ''
					report = False
					low = high = None
					for metric, m in o['metrics'].iteritems():
						value = d(getvalue(data, m[1:])).quantize(m[0])
						if metric == 'volume': # volume is in base units
							currency = o['unit']
						else: # others are in currency
							currency = o['currency']
						log.debug("output %r metric %r has value %r", output, metric, value)
						if metric in o['reportchange']:
							if output+metric in prev:
								vdiff = value - prev[output+metric]
							else:
								vdiff = Decimal(0)
								prev[output+metric] = value
								report = True
							if vdiff.copy_abs() >= o['reportchange'][metric]:
								log.debug("output %r metric %r value diff %r exceeds %r, reporting change",
									output, metric, vdiff.copy_abs(), o['reportchange'][metric])
								prev[output+metric] = value
								report = True
							values.append("%12s %-3s" % (
									value, currency))
							diffstring = updown(vdiff)
						else:
							values.append("%12s %-3s" % (
								value, currency))
						if low is None or value < low:
							low = value
						if high is None or value > high:
							high = value
					diffstring=updown(vdiff, '(%s%s %s)' % (
						'-' if vdiff.is_signed() else '+',
						vdiff.copy_abs(), currency))
					out = "%s %s %s %s" % (
						'['+TICKERS[ticker]['label']+']',
						currencycolor(o['unit']),
						" ".join(values),
						diffstring)
					if report:
						for chan in self.registryValue('channels'):
							irc.queueMsg(ircmsgs.privmsg(chan,
								ircutils.bold(out)))
						
						nicks = self.lowvalues.keys()
						for nick in nicks:
							user_value = self.lowvalues[nick]
							if low is not None and low <= user_value:
								out = "%s: OMG, it went below %s to %s!" % (nick, user_value, low)
								irc.queueMsg(ircmsgs.privmsg(chan, ircutils.bold(out)))
								del self.lowvalues[nick]

						nicks = self.highvalues.keys()
						for nick in nicks:
							user_value = self.highvalues[nick]
							if high is not None and high >= user_value:
								out = "%s: OMG, it went above %s to %s!" % (nick, user_value, high)
								irc.queueMsg(ircmsgs.privmsg(chan, ircutils.bold(out)))
								del self.highvalues[nick]
			except Exception, e:
				log.exception("in ticker thread %r", ticker)
Exemple #40
0
    def np(self, irc, msg, args, user):
        """[<user>]

        Announces the track currently being played by <user>. If <user>
        is not given, defaults to the LastFM user configured for your
        current nick.
        """
        apiKey = self.registryValue("apiKey")
        if not apiKey:
            irc.error(
                "The API Key is not set. Please set it via "
                "'config plugins.lastfm.apikey' and reload the plugin. "
                "You can sign up for an API Key using "
                "http://www.last.fm/api/account/create",
                Raise=True)
        user = (user or self.db.get(msg.prefix) or msg.nick)

        # see http://www.lastfm.de/api/show/user.getrecenttracks
        url = "%sapi_key=%s&method=user.getrecenttracks&user=%s&format=json" % (
            self.APIURL, apiKey, user)
        try:
            f = utils.web.getUrl(url).decode("utf-8")
        except utils.web.Error:
            irc.error("Unknown user %s." % user, Raise=True)
        self.log.debug("LastFM.nowPlaying: url %s", url)

        try:
            data = json.loads(f)["recenttracks"]
        except KeyError:
            irc.error("Unknown user %s." % user, Raise=True)

        user = data["@attr"]["user"]
        tracks = data["track"]

        # Work with only the first track.
        try:
            trackdata = tracks[0]
        except IndexError:
            irc.error("%s doesn't seem to have listened to anything." % user,
                      Raise=True)

        artist = trackdata["artist"]["#text"].strip()  # Artist name
        track = trackdata["name"].strip()  # Track name
        # Album name (may or may not be present)
        album = trackdata["album"]["#text"].strip()
        if album:
            album = ircutils.bold("[%s]" % album)

        try:
            time = int(trackdata["date"]["uts"])  # Time of last listen
            # Format this using the preferred time format.
            tformat = conf.supybot.reply.format.time()
            time = "at %s" % datetime.fromtimestamp(time).strftime(tformat)
        except KeyError:  # Nothing given by the API?
            time = "right now"

        public_url = ''
        # If the DDG plugin from this repository is loaded, we can integrate
        # that by finding a YouTube link for the track.
        if self.registryValue("fetchYouTubeLink"):
            ddg = irc.getCallback("DDG")
            if ddg:
                # Each valid result has a preceding heading in the format
                # '<td valign="top">1.&nbsp;</td>', etc.
                try:
                    search = [
                        td for td in ddg._ddgurl('site:youtube.com "%s - %s"' %
                                                 (artist, track))
                        if "1." in td.text
                    ]
                    res = search[0].next_sibling.next_sibling
                    public_url = format('%u', res.a.get('href'))
                except:
                    # If something breaks, log the error but don't cause the
                    # entire np request to fail.
                    log.exception(
                        "LastFM: failed to get YouTube link for track %s - %s",
                        artist, track)

        s = '%s listened to %s by %s %s %s. %s' % (
            ircutils.bold(user), ircutils.bold(track), ircutils.bold(artist),
            album, time, public_url)
        irc.reply(utils.str.normalizeWhitespace(s))
 def implementation():
     try:
         source.update()
     except Exception as e:
         log.exception('Failed to update {}: {}'.format(source.NAME, e))
     self._topic_callback()
Exemple #42
0
 def _ticker(self, irc, ticker):
     prev = dict()
     while True:
         if self.stop:
             return
         try:
             log.debug("refreshing ticker %r", ticker)
             data = dict()
             for key, url in TICKERS[ticker]["urls"].iteritems():
                 log.debug("fetching url %r", url)
                 try:
                     t0 = time.time()
                     data[key] = json.load(urllib2.urlopen(url, timeout=30))
                 except Exception, e:
                     log.exception("error fetching %r", url)
                     continue
                 log.info("fetched %r in %.2f s", url, time.time() - t0)
                 log.debug("data = %r", data[key])
             if not data:
                 raise Exception("no data for ticker %s" % repr(ticker))
             for output, o in TICKERS[ticker]["outputs"].iteritems():
                 log.debug("processing output %r: %r", output, o)
                 values = []
                 diffstring = ""
                 report = False
                 for metric, m in o["metrics"].iteritems():
                     value = d(getvalue(data, m[1:])).quantize(m[0])
                     if metric == "volume":  # volume is in base units
                         currency = o["unit"]
                     else:  # others are in currency
                         currency = o["currency"]
                     log.debug("output %r metric %r has value %r", output, metric, value)
                     if metric in o["reportchange"]:
                         if output + metric in prev:
                             vdiff = value - prev[output + metric]
                         else:
                             vdiff = Decimal(0)
                             prev[output + metric] = value
                             report = True
                         if vdiff.copy_abs() >= o["reportchange"][metric]:
                             log.debug(
                                 "output %r metric %r value diff %r exceeds %r, reporting change",
                                 output,
                                 metric,
                                 vdiff.copy_abs(),
                                 o["reportchange"][metric],
                             )
                             prev[output + metric] = value
                             report = True
                         values.append("%12s %-3s" % (value, currency))
                         diffstring = updown(vdiff)
                     else:
                         values.append("%12s %-3s" % (value, currency))
                 diffstring = updown(
                     vdiff, "(%s%s %s)" % ("-" if vdiff.is_signed() else "+", vdiff.copy_abs(), currency)
                 )
                 out = "%s %s %s %s" % (
                     "[" + TICKERS[ticker]["label"] + "]",
                     currencycolor(o["unit"]),
                     " ".join(values),
                     diffstring,
                 )
                 if report:
                     for chan in self.registryValue("channels"):
                         irc.queueMsg(ircmsgs.privmsg(chan, ircutils.bold(out)))
         except Exception, e:
             log.exception("in ticker thread %r", ticker)
Exemple #43
0
    def np(self, irc, msg, args, user):
        """[<user>]

        Announces the track currently being played by <user>. If <user>
        is not given, defaults to the LastFM user configured for your
        current nick.
        """
        apiKey = self.registryValue("apiKey")
        if not apiKey:
            irc.error("The API Key is not set. Please set it via "
                      "'config plugins.lastfm.apikey' and reload the plugin. "
                      "You can sign up for an API Key using "
                      "http://www.last.fm/api/account/create", Raise=True)
        user = (user or self.db.get(msg.prefix) or msg.nick)

        # see http://www.lastfm.de/api/show/user.getrecenttracks
        url = "%sapi_key=%s&method=user.getrecenttracks&user=%s&format=json" % (self.APIURL, apiKey, user)
        try:
            f = utils.web.getUrl(url).decode("utf-8")
        except utils.web.Error:
            irc.error("Unknown user %s." % user, Raise=True)
        self.log.debug("LastFM.nowPlaying: url %s", url)

        try:
            data = json.loads(f)["recenttracks"]
        except KeyError:
            irc.error("Unknown user %s." % user, Raise=True)

        user = data["@attr"]["user"]
        tracks = data["track"]

        # Work with only the first track.
        try:
            trackdata = tracks[0]
        except IndexError:
            irc.error("%s doesn't seem to have listened to anything." % user, Raise=True)

        artist = trackdata["artist"]["#text"].strip()  # Artist name
        track = trackdata["name"].strip()  # Track name
        # Album name (may or may not be present)
        album = trackdata["album"]["#text"].strip()
        if album:
            album = "[%s] " % album

        try:
            time = int(trackdata["date"]["uts"])  # Time of last listen
            # Format this using the preferred time format.
            tformat = conf.supybot.reply.format.time()
            time = datetime.fromtimestamp(time).strftime(tformat)
        except KeyError:  # Nothing given by the API?
            time = "some point in time"

        public_url = ''
        # If the DDG plugin from this repository is loaded, we can integrate
        # that by finding a YouTube link for the track.
        if self.registryValue("fetchYouTubeLink"):
            ddg = irc.getCallback("DDG")
            if ddg:
                # Each valid result has a preceding heading in the format
                # '<td valign="top">1.&nbsp;</td>', etc.
                try:
                    search = [td for td in ddg._ddgurl('site:youtube.com "%s - %s"' % (artist, track))
                              if "1." in td.text]
                    res = search[0].next_sibling.next_sibling
                    public_url = format(' - %u', res.a.get('href'))
                except:
                    # If something breaks, log the error but don't cause the
                    # entire np request to fail.
                    log.exception("LastFM: failed to get YouTube link for track %s - %s", artist, track)

        irc.reply('%s listened to %s by %s %sat %s%s' %
                  (ircutils.bold(user), ircutils.bold(track),
                   ircutils.bold(artist), ircutils.bold(album), time, public_url))
Exemple #44
0
def getvalue(data, keys):
    try:
        for k in keys:
            data = data.get(k)
    except Exception, e:
        log.exception("data=%r keys=%r", data, keys)
Exemple #45
0
    def np(self, irc, msg, args, user):
        """[<user>]

        Announces the track currently being played by <user>. If <user>
        is not given, defaults to the LastFM user configured for your
        current nick.
        """
        apiKey = self.registryValue("apiKey")
        if not apiKey:
            irc.error(
                "The API Key is not set. Please set it via "
                "'config plugins.lastfm.apikey' and reload the plugin. "
                "You can sign up for an API Key using "
                "http://www.last.fm/api/account/create",
                Raise=True)
        user = (user or self.db.get(msg.prefix) or msg.nick)

        # see http://www.lastfm.de/api/show/user.getrecenttracks
        url = "%sapi_key=%s&method=user.getrecenttracks&user=%s&format=json" % (
            self.APIURL, apiKey, user)
        try:
            f = utils.web.getUrl(url).decode("utf-8")
        except utils.web.Error:
            irc.error("Unknown user %s." % user, Raise=True)
        self.log.debug("LastFM.nowPlaying: url %s", url)

        try:
            data = json.loads(f)["recenttracks"]
        except KeyError:
            irc.error("Unknown user %s." % user, Raise=True)

        user = data["@attr"]["user"]
        tracks = data["track"]

        # Work with only the first track.
        try:
            trackdata = tracks[0]
        except IndexError:
            irc.error("%s doesn't seem to have listened to anything." % user,
                      Raise=True)

        artist = trackdata["artist"]["#text"].strip()  # Artist name
        track = trackdata["name"].strip()  # Track name
        # Album name (may or may not be present)
        album = trackdata["album"]["#text"].strip()
        if album:
            album = ircutils.bold("[%s]" % album)

        try:
            time = int(trackdata["date"]["uts"])  # Time of last listen
            # Format this using the preferred time format.
            tformat = conf.supybot.reply.format.time()
            time = "at %s" % datetime.fromtimestamp(time).strftime(tformat)
        except KeyError:  # Nothing given by the API?
            time = "just now"

        public_url = ''
        # If the DDG plugin from this repository is loaded, we can integrate
        # that by finding a YouTube link for the track.
        if self.registryValue("fetchYouTubeLink", msg.args[0]):
            ddg = irc.getCallback("DDG")
            if ddg:
                try:
                    results = ddg.search_core('site:youtube.com "%s - %s"' %
                                              (artist, track),
                                              channel_context=msg.args[0],
                                              max_results=1,
                                              show_snippet=False)
                    if results:
                        public_url = format('%u', results[0][2])
                except:
                    # If something breaks, log the error but don't cause the
                    # entire np request to fail.
                    log.exception(
                        "LastFM: failed to get YouTube link for track %s - %s",
                        artist, track)

        ext_info = ''
        if self.registryValue("showExtendedInfo", msg.args[0]):
            # Get extended info via a separate API call.
            ext_info_url = "%sapi_key=%s&method=track.getinfo&user=%s&format=json&artist=%s&track=%s" % (
                self.APIURL, apiKey, user, utils.web.urlquote(artist),
                utils.web.urlquote(track))
            ext_info_f = utils.web.getUrl(ext_info_url).decode("utf-8")
            self.log.debug("LastFM.nowPlaying: using url %s for extended info",
                           ext_info_url)
            try:
                ext_data = json.loads(ext_info_f)['track']

                # We currently show play count and tags - more could be added in the future...
                userplaycount = ext_data['userplaycount']
                tags = [tag['name'] for tag in ext_data['toptags']['tag']]
                ext_info = ' (Playcount: %s / Tags: %s)' % (
                    userplaycount, ', '.join(tags) or 'N/A')
            except KeyError:
                pass

        s = '%s listened to %s by %s %s %s%s. %s' % (
            ircutils.bold(user), ircutils.bold(track), ircutils.bold(artist),
            album, time, ext_info, public_url)
        irc.reply(utils.str.normalizeWhitespace(s))