예제 #1
0
class DarkSkyWeather(callbacks.Plugin):
    """A weather script that uses APIXU's api.
    """

    threaded = True

    def wz(self, irc, msg, args: List, text: str):
        """- Calls the weather"""
        userinfo = {"host": msg.host, "nick": msg.nick}
        userinfos = UserInfo(**userinfo)
        log.info(f"{type(msg)}")
        irc.reply(f"host is {msg}")

    wz = wrap(wz, [optional("text")])

    def setweather(self, irc, msg, args, units: int, text: str):
        """- Sets the weather for a user to the db."""

        userinfo = {
            "host": msg.host,
            "nick": msg.nick,
            "units": units,
            "location": text,
        }

        irc.reply(f"user: {msg.args[0]}")

    setweather = wrap(setweather, ["int", "text"])
예제 #2
0
class SocialGraph(Plugin):
    """Flimsy support for Google's Social Graph API.
    """
    def otherme(self, irc, msg, args, uri):
        """Ask google's social graph api about a particular url or email.
        """
        if not uri:
            irc.reply("please supply a URL or email address!")
            return

        profile = self._get_profile(uri)
        irc.reply(', '.join(profile.keys()))

    otherme = wrap(otherme, [optional('text')])

    def _get_profile(self, uri):
        url = "http://socialgraph.apis.google.com/otherme?" + \
            urllib.urlencode({'q': uri})
        return json.loads(urllib.urlopen(url).read())
예제 #3
0
class HcoopMeetbot(callbacks.Plugin):
    """Helps run IRC meetings."""
    def __init__(self, irc):
        """Initialize the plugin with our custom configuration."""
        super().__init__(irc)
        handler.configure(self.log, "%s" % conf.supybot.directories.conf)

    def doPrivmsg(self, irc, msg):
        """Capture all messages from supybot."""
        channel = msg.args[0]
        topic = irc.state.channels[
            channel].topic if channel in irc.state.channels else ""
        users = irc.state.channels[
            channel].users if channel in irc.state.channels else []
        context = _context(self, irc, msg)
        message = interface.Message(
            id=uuid4().hex,
            timestamp=now(),
            nick=msg.nick,
            channel=channel,
            network=irc.msg.tags["receivedOn"],
            payload=msg.args[1],
            topic=topic,
            channel_nicks=["%s" % n for n in users],
        )
        handler.irc_message(context=context, message=message)

    def outFilter(self, irc, msg):
        """Log outgoing messages from supybot."""
        try:
            if msg.command in ("PRIVMSG", ):
                context = _context(self, irc, msg)
                message = interface.Message(
                    id=uuid4().hex,
                    timestamp=now(),
                    nick=irc.nick,
                    channel=msg.args[0],
                    network=irc.network,
                    payload=msg.args[1],
                )
                handler.outbound_message(context=context, message=message)
        except Exception:
            # Per original MeetBot, catch errors to prevent all output from being clobbered
            self.log.exception("Discarded error in outFilter")
        return msg

    def meetversion(self, irc, msg, args):
        """Get the current plugin version."""
        context = _context(self, irc, msg)
        handler.meetversion(context=context)

    meetversion = wrap(meetversion)

    def listmeetings(self, irc, msg, args):
        """List all currently-active meetings."""
        context = _context(self, irc, msg)
        handler.listmeetings(context=context)

    listmeetings = wrap(listmeetings, ["admin"])

    def savemeetings(self, irc, msg, args):
        """Save all currently active meetings"""
        context = _context(self, irc, msg)
        handler.savemeetings(context=context)

    savemeetings = wrap(savemeetings, ["admin"])

    def addchair(self, irc, msg, args, channel, nick):
        """Add a nickname as a chair to the meeting in this channel: addchair <nick>."""
        context = _context(self, irc, msg)
        network = irc.msg.tags["receivedOn"]
        handler.addchair(context=context,
                         channel=channel,
                         network=network,
                         nick=nick)

    addchair = wrap(addchair, ["admin", "channel", "nick"])

    def deletemeeting(self, irc, msg, args, channel, save):
        """Delete a meeting from the cache: deletemeeting <save=true/false>"""
        context = _context(self, irc, msg)
        network = irc.msg.tags["receivedOn"]
        handler.deletemeeting(context=context,
                              channel=channel,
                              network=network,
                              save=save)

    deletemeeting = wrap(
        deletemeeting,
        ["admin", "channel", optional("boolean", True)])

    def recent(self, irc, msg, args):
        """List recent meetings for admin purposes."""
        context = _context(self, irc, msg)
        handler.recent(context=context)

    recent = wrap(recent, ["admin"])

    def commands(self, irc, msg, args):
        """List available commands."""
        context = _context(self, irc, msg)
        handler.commands(context=context)

    commands = wrap(commands)
예제 #4
0
class Owner(callbacks.Plugin):
    """Owner-only commands for core Supybot. This is a core Supybot module
    that should not be removed!"""

    # This plugin must be first; its priority must be lowest; otherwise odd
    # things will happen when adding callbacks.
    def __init__(self, irc=None):
        if irc is not None:
            assert not irc.getCallback(self.name())
        self.__parent = super(Owner, self)
        self.__parent.__init__(irc)
        # Setup command flood detection.
        self.commands = ircutils.FloodQueue(
            conf.supybot.abuse.flood.interval())
        conf.supybot.abuse.flood.interval.addCallback(
            self.setFloodQueueTimeout)
        # Setup plugins and default plugins for commands.
        #
        # This needs to be done before we connect to any networks so that the
        # children of supybot.plugins (the actual plugins) exist and can be
        # loaded.
        for (name, s) in registry._cache.items():
            if 'alwaysLoadDefault' in name or 'alwaysLoadImportant' in name:
                continue
            if name.startswith('supybot.plugins'):
                try:
                    (_, _, name) = registry.split(name)
                except ValueError:  # unpack list of wrong size.
                    continue
                # This is just for the prettiness of the configuration file.
                # There are no plugins that are all-lowercase, so we'll at
                # least attempt to capitalize them.
                if name == name.lower():
                    name = name.capitalize()
                conf.registerPlugin(name)
            if name.startswith('supybot.commands.defaultPlugins'):
                try:
                    (_, _, _, name) = registry.split(name)
                except ValueError:  # unpack list of wrong size.
                    continue
                registerDefaultPlugin(name, s)
        # Setup Irc objects, connected to networks.  If world.ircs is already
        # populated, chances are that we're being reloaded, so don't do this.
        if not world.ircs:
            for network in conf.supybot.networks():
                try:
                    self._connect(network)
                except socket.error as e:
                    self.log.error('Could not connect to %s: %s.', network, e)
                except Exception as e:
                    self.log.exception('Exception connecting to %s:', network)
                    self.log.error('Could not connect to %s: %s.', network, e)

    def callPrecedence(self, irc):
        return ([], [cb for cb in irc.callbacks if cb is not self])

    def outFilter(self, irc, msg):
        if msg.command == 'PRIVMSG' and not world.testing:
            if ircutils.strEqual(msg.args[0], irc.nick):
                self.log.warning('Tried to send a message to myself: %r.', msg)
                return None
        return msg

    def reset(self):
        # This has to be done somewhere, I figure here is as good place as any.
        callbacks.IrcObjectProxy._mores.clear()
        self.__parent.reset()

    def _connect(self, network, serverPort=None, password='', ssl=False):
        try:
            group = conf.supybot.networks.get(network)
            group.servers()[0]
        except (registry.NonExistentRegistryEntry, IndexError):
            if serverPort is None:
                raise ValueError('connect requires a (server, port) ' \
                                  'if the network is not registered.')
            conf.registerNetwork(network, password, ssl)
            server = '%s:%s' % serverPort
            conf.supybot.networks.get(network).servers.append(server)
            assert conf.supybot.networks.get(network).servers(), \
                   'No servers are set for the %s network.' % network
        self.log.debug('Creating new Irc for %s.', network)
        newIrc = irclib.Irc(network)
        driver = drivers.newDriver(newIrc)
        self._loadPlugins(newIrc)
        return newIrc

    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." % 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

    def do376(self, irc, msg):
        msgs = conf.supybot.networks.get(irc.network).channels.joins()
        if msgs:
            for msg in msgs:
                irc.queueMsg(msg)

    do422 = do377 = do376

    def setFloodQueueTimeout(self, *args, **kwargs):
        self.commands.timeout = conf.supybot.abuse.flood.interval()

    def doBatch(self, irc, msg):
        if not conf.supybot.protocols.irc.experimentalExtensions():
            return

        batch = msg.tagged('batch')  # Always not-None on a BATCH message

        if msg.args[0].startswith('+'):
            # Start of a batch, we're not interested yet.
            return
        if batch.type != 'draft/multiline':
            # This is not a multiline batch, also not interested.
            return

        assert msg.args[0].startswith("-"), (
            "BATCH's first argument should start with either - or +, but "
            "it is %s.") % msg.args[0]
        # End of multiline batch. It may be a long command.

        payloads = []
        first_privmsg = None

        for message in batch.messages:
            if message.command != "PRIVMSG":
                # We're only interested in PRIVMSGs for the payloads.
                # (eg. exclude NOTICE)
                continue
            elif not payloads:
                # This is the first PRIVMSG of the batch
                first_privmsg = message
                payloads.append(message.args[1])
            elif 'draft/multiline-concat' in message.server_tags:
                # This message is not a new line, but the continuation
                # of the previous one.
                payloads.append(message.args[1])
            else:
                # New line; stop here. We're not processing extra lines
                # either as the rest of the command or as new commands.
                # This may change in the future.
                break

        payload = ''.join(payloads)
        if not payload:
            self.log.error(
                'Got empty multiline payload. This is a bug, please '
                'report it along with logs.')
            return

        assert first_privmsg, "This shouldn't be None unless payload is empty"

        # Let's build a synthetic message from the various parts of the
        # batch, to look like the multiline batch was a single (large)
        # PRIVMSG:
        # * copy the tags and server tags of the 'BATCH +' command,
        # * copy the prefix and channel of any of the PRIVMSGs
        #   inside the batch
        # * create a new args[1]
        target = first_privmsg.args[0]
        synthetic_msg = ircmsgs.IrcMsg(
            msg=batch.messages[0],  # tags, server_tags, time
            prefix=first_privmsg.prefix,
            command='PRIVMSG',
            args=(target, payload))

        self._doPrivmsgs(irc, synthetic_msg)

    def doPrivmsg(self, irc, msg):
        if 'batch' in msg.server_tags:
            parent_batches = irc.state.getParentBatches(msg)
            parent_batch_types = [batch.type for batch in parent_batches]
            if 'draft/multiline' in parent_batch_types \
                    and conf.supybot.protocols.irc.experimentalExtensions():
                # We will handle the message in doBatch when the entire
                # batch ends.
                return
            if 'chathistory' in parent_batch_types:
                # Either sent automatically by the server upon join,
                # or triggered by a plugin (why?!)
                # Either way, replying to commands from the history would
                # look weird, because it may have been sent a while ago,
                # and we may have already answered to it.
                return

        self._doPrivmsgs(irc, msg)

    def _doPrivmsgs(self, irc, msg):
        """If the given message is a command, triggers Limnoria's
        command-dispatching for that command.

        Takes the same arguments as ``doPrivmsg`` would, but ``msg`` can
        potentially be an artificial message synthesized in doBatch
        from a multiline batch.

        Usually, a command is a single message, so ``payload=msg.params[0]``
        However, when ``msg`` is part of a multiline message, the payload
        is the concatenation of multiple messages.
        See <https://ircv3.net/specs/extensions/multiline>.
        """
        assert self is irc.callbacks[0], \
               'Owner isn\'t first callback: %r' % irc.callbacks
        if ircmsgs.isCtcp(msg):
            return

        s = callbacks.addressed(irc, msg)
        if s:
            ignored = ircdb.checkIgnored(msg.prefix)
            if ignored:
                self.log.info('Ignoring command from %s.', msg.prefix)
                return
            maximum = conf.supybot.abuse.flood.command.maximum()
            self.commands.enqueue(msg)
            if conf.supybot.abuse.flood.command() \
               and self.commands.len(msg) > maximum \
               and not ircdb.checkCapability(msg.prefix, 'trusted'):
                punishment = conf.supybot.abuse.flood.command.punishment()
                banmask = conf.supybot.protocols.irc.banmask \
                        .makeBanmask(msg.prefix)
                self.log.info(
                    'Ignoring %s for %s seconds due to an apparent '
                    'command flood.', banmask, punishment)
                ircdb.ignores.add(banmask, time.time() + punishment)
                if conf.supybot.abuse.flood.command.notify():
                    irc.reply('You\'ve given me %s commands within the last '
                              '%i seconds; I\'m now ignoring you for %s.' %
                              (maximum, conf.supybot.abuse.flood.interval(),
                               utils.timeElapsed(punishment, seconds=False)))
                return
            try:
                tokens = callbacks.tokenize(s,
                                            channel=msg.channel,
                                            network=irc.network)
                self.Proxy(irc, msg, tokens)
            except SyntaxError as e:
                if conf.supybot.reply.error.detailed():
                    irc.error(str(e))
                else:
                    irc.replyError(msg=msg)
                    self.log.info('Syntax error: %s', e)

    def logmark(self, irc, msg, args, text):
        """<text>

        Logs <text> to the global Supybot log at critical priority.  Useful for
        marking logfiles for later searching.
        """
        self.log.critical(text)
        irc.replySuccess()

    logmark = wrap(logmark, ['text'])

    def announce(self, irc, msg, args, text):
        """<text>

        Sends <text> to all channels the bot is currently on and not
        lobotomized in.
        """
        u = ircdb.users.getUser(msg.prefix)

        template = self.registryValue('announceFormat')

        text = ircutils.standardSubstitute(irc,
                                           msg,
                                           template,
                                           env={
                                               'owner': u.name,
                                               'text': text
                                           })

        for channel in irc.state.channels:
            c = ircdb.channels.getChannel(channel)
            if not c.lobotomized:
                irc.queueMsg(ircmsgs.privmsg(channel, text))

        irc.noReply()

    announce = wrap(announce, ['text'])

    def defaultplugin(self, irc, msg, args, optlist, command, plugin):
        """[--remove] <command> [<plugin>]

        Sets the default plugin for <command> to <plugin>.  If --remove is
        given, removes the current default plugin for <command>.  If no plugin
        is given, returns the current default plugin set for <command>.  See
        also, supybot.commands.defaultPlugins.importantPlugins.
        """
        remove = False
        for (option, arg) in optlist:
            if option == 'remove':
                remove = True
        (_, cbs) = irc.findCallbacksForArgs([command])
        if remove:
            try:
                conf.supybot.commands.defaultPlugins.unregister(command)
                irc.replySuccess()
            except registry.NonExistentRegistryEntry:
                s = 'I don\'t have a default plugin set for that command.'
                irc.error(s)
        elif not cbs:
            irc.errorInvalid('command', command)
        elif plugin:
            if not plugin.isCommand(command):
                irc.errorInvalid('command in the %s plugin' % plugin.name(),
                                 command)
            registerDefaultPlugin(command, plugin.name())
            irc.replySuccess()
        else:
            try:
                irc.reply(conf.supybot.commands.defaultPlugins.get(command)())
            except registry.NonExistentRegistryEntry:
                s = 'I don\'t have a default plugin set for that command.'
                irc.error(s)

    defaultplugin = wrap(
        defaultplugin,
        [getopts({'remove': ''}), 'commandName',
         additional('plugin')])

    def ircquote(self, irc, msg, args, s):
        """<string to be sent to the server>

        Sends the raw string given to the server.
        """
        try:
            m = ircmsgs.IrcMsg(s)
        except Exception as e:
            irc.error(utils.exnToString(e))
        else:
            irc.queueMsg(m)
            irc.noReply()

    ircquote = wrap(ircquote, ['text'])

    def quit(self, irc, msg, args, text):
        """[<text>]

        Exits the bot with the QUIT message <text>.  If <text> is not given,
        the default quit message (supybot.plugins.Owner.quitMsg) will be used.
        If there is no default quitMsg set, your nick will be used. The standard
        substitutions ($version, $nick, etc.) are all handled appropriately.
        """
        text = text or self.registryValue('quitMsg') or msg.nick
        text = ircutils.standardSubstitute(irc, msg, text)
        irc.noReply()
        m = ircmsgs.quit(text)
        world.upkeep()
        for irc in world.ircs[:]:
            irc.queueMsg(m)
            irc.die()

    quit = wrap(quit, [additional('text')])

    def flush(self, irc, msg, args):
        """takes no arguments

        Runs all the periodic flushers in world.flushers.  This includes
        flushing all logs and all configuration changes to disk.
        """
        world.flush()
        irc.replySuccess()

    flush = wrap(flush)

    def upkeep(self, irc, msg, args, level):
        """[<level>]

        Runs the standard upkeep stuff (flushes and gc.collects()).  If given
        a level, runs that level of upkeep (currently, the only supported
        level is "high", which causes the bot to flush a lot of caches as well
        as do normal upkeep stuff).
        """
        L = []
        if level == 'high':
            L.append(
                format('Regexp cache flushed: %n cleared.',
                       (len(re._cache), 'regexp')))
            re.purge()
            L.append(
                format('Pattern cache flushed: %n cleared.',
                       (len(ircutils._patternCache), 'compiled pattern')))
            ircutils._patternCache.clear()
            L.append(
                format('hostmaskPatternEqual cache flushed: %n cleared.',
                       (len(ircutils._hostmaskPatternEqualCache), 'result')))
            ircutils._hostmaskPatternEqualCache.clear()
            L.append(
                format(
                    'ircdb username cache flushed: %n cleared.',
                    (len(ircdb.users._nameCache), 'username to id mapping')))
            ircdb.users._nameCache.clear()
            L.append(
                format('ircdb hostmask cache flushed: %n cleared.', (len(
                    ircdb.users._hostmaskCache), 'hostmask to id mapping')))
            ircdb.users._hostmaskCache.clear()
            L.append(
                format('linecache line cache flushed: %n cleared.',
                       (len(linecache.cache), 'line')))
            linecache.clearcache()
            if minisix.PY2:
                sys.exc_clear()
        collected = world.upkeep()
        if gc.garbage:
            L.append('Garbage!  %r.' % gc.garbage)
        if collected is not None:
            # Some time between 5.2 and 7.1, Pypy (3?) started returning None
            # when gc.collect() is called.
            L.append(format('%n collected.', (collected, 'object')))
        if L:
            irc.reply('  '.join(L))
        else:
            irc.replySuccess()

    upkeep = wrap(upkeep, [additional(('literal', ['high']))])

    def load(self, irc, msg, args, optlist, name):
        """[--deprecated] <plugin>

        Loads the plugin <plugin> from any of the directories in
        conf.supybot.directories.plugins; usually this includes the main
        installed directory and 'plugins' in the current directory.
        --deprecated is necessary if you wish to load deprecated plugins.
        """
        ignoreDeprecation = False
        for (option, argument) in optlist:
            if option == 'deprecated':
                ignoreDeprecation = True
        if name.endswith('.py'):
            name = name[:-3]
        if irc.getCallback(name):
            irc.error('%s is already loaded.' % name.capitalize())
            return
        try:
            module = plugin.loadPluginModule(name, ignoreDeprecation)
        except plugin.Deprecated:
            irc.error('%s is deprecated.  Use --deprecated '
                      'to force it to load.' % name.capitalize())
            return
        except ImportError as e:
            if str(e).endswith(name):
                irc.error('No plugin named %s exists.' %
                          utils.str.dqrepr(name))
            elif "No module named 'config'" in str(e):
                irc.error(
                    'This plugin may be incompatible with your current Python '
                    'version. Try running 2to3 on it.')
            else:
                irc.error(str(e))
            return
        cb = plugin.loadPluginClass(irc, module)
        name = cb.name()  # Let's normalize this.
        conf.registerPlugin(name, True)
        irc.replySuccess()

    load = wrap(load, [getopts({'deprecated': ''}), 'something'])

    def reload(self, irc, msg, args, name):
        """<plugin>

        Unloads and subsequently reloads the plugin by name; use the 'list'
        command to see a list of the currently loaded plugins.
        """
        if ircutils.strEqual(name, self.name()):
            irc.error('You can\'t reload the %s plugin.' % name)
            return
        callbacks = irc.removeCallback(name)
        if callbacks:
            module = sys.modules[callbacks[0].__module__]
            if hasattr(module, 'reload'):
                x = module.reload()
            try:
                module = plugin.loadPluginModule(name)
                if hasattr(module, 'reload') and 'x' in locals():
                    module.reload(x)
                if hasattr(module, 'config'):
                    from importlib import reload
                    reload(module.config)
                for callback in callbacks:
                    callback.die()
                    del callback
                gc.collect()  # This makes sure the callback is collected.
                callback = plugin.loadPluginClass(irc, module)
                irc.replySuccess()
            except ImportError:
                for callback in callbacks:
                    irc.addCallback(callback)
                irc.error('No plugin named %s exists.' % name)
        else:
            irc.error('There was no plugin %s.' % name)

    reload = wrap(reload, ['something'])

    def unload(self, irc, msg, args, name):
        """<plugin>

        Unloads the callback by name; use the 'list' command to see a list
        of the currently loaded plugins.  Obviously, the Owner plugin can't
        be unloaded.
        """
        if ircutils.strEqual(name, self.name()):
            irc.error('You can\'t unload the %s plugin.' % name)
            return
        # Let's do this so even if the plugin isn't currently loaded, it doesn't
        # stay attempting to load.
        old_callback = irc.getCallback(name)
        if old_callback:
            # Normalize the plugin case to prevent duplicate registration
            # entries, https://github.com/progval/Limnoria/issues/1295
            name = old_callback.name()
            conf.registerPlugin(name, False)
            callbacks = irc.removeCallback(name)
            if callbacks:
                for callback in callbacks:
                    callback.die()
                    del callback
                gc.collect()
                irc.replySuccess()
                return
        irc.error('There was no plugin %s.' % name)

    unload = wrap(unload, ['something'])

    def defaultcapability(self, irc, msg, args, action, capability):
        """{add|remove} <capability>

        Adds or removes (according to the first argument) <capability> from the
        default capabilities given to users (the configuration variable
        supybot.capabilities stores these).
        """
        if action == 'add':
            conf.supybot.capabilities().add(capability)
            irc.replySuccess()
        elif action == 'remove':
            try:
                conf.supybot.capabilities().remove(capability)
                irc.replySuccess()
            except KeyError:
                if ircdb.isAntiCapability(capability):
                    irc.error('That capability wasn\'t in '
                              'supybot.capabilities.')
                else:
                    anticap = ircdb.makeAntiCapability(capability)
                    conf.supybot.capabilities().add(anticap)
                    irc.replySuccess()

    defaultcapability = wrap(defaultcapability,
                             [('literal', ['add', 'remove']), 'capability'])

    def disable(self, irc, msg, args, plugin, command):
        """[<plugin>] <command>

        Disables the command <command> for all users (including the owners).
        If <plugin> is given, only disables the <command> from <plugin>.  If
        you want to disable a command for most users but not for yourself, set
        a default capability of -plugin.command or -command (if you want to
        disable the command in all plugins).
        """
        if command in ('enable', 'identify'):
            irc.error('You can\'t disable %s.' % command)
            return
        if plugin:
            if plugin.isCommand(command):
                pluginCommand = '%s.%s' % (plugin.name(), command)
                conf.supybot.commands.disabled().add(pluginCommand)
                plugin._disabled.add(command, plugin.name())
            else:
                irc.error('%s is not a command in the %s plugin.' %
                          (command, plugin.name()))
                return
        else:
            conf.supybot.commands.disabled().add(command)
            self._disabled.add(command)
        irc.replySuccess()

    disable = wrap(disable, [optional('plugin'), 'commandName'])

    def enable(self, irc, msg, args, plugin, command):
        """[<plugin>] <command>

        Enables the command <command> for all users.  If <plugin>
        if given, only enables the <command> from <plugin>.  This command is
        the inverse of disable.
        """
        try:
            if plugin:
                plugin._disabled.remove(command, plugin.name())
                command = '%s.%s' % (plugin.name(), command)
            else:
                self._disabled.remove(command)
            conf.supybot.commands.disabled().remove(command)
            irc.replySuccess()
        except KeyError:
            irc.error('That command wasn\'t disabled.')

    enable = wrap(enable, [optional('plugin'), 'commandName'])

    def rename(self, irc, msg, args, command_plugin, command, newName):
        """<plugin> <command> <new name>

        Renames <command> in <plugin> to the <new name>.
        """
        if not command_plugin.isCommand(command):
            what = 'command in the %s plugin' % command_plugin.name()
            irc.errorInvalid(what, command)
        if hasattr(command_plugin, newName):
            irc.error('The %s plugin already has an attribute named %s.' %
                      (command_plugin, newName))
            return
        plugin.registerRename(command_plugin.name(), command, newName)
        plugin.renameCommand(command_plugin, command, newName)
        irc.replySuccess()

    rename = wrap(rename, ['plugin', 'commandName', 'commandName'])

    def unrename(self, irc, msg, args, plugin):
        """<plugin>

        Removes all renames in <plugin>.  The plugin will be reloaded after
        this command is run.
        """
        try:
            conf.supybot.commands.renames.unregister(plugin.name())
        except registry.NonExistentRegistryEntry:
            irc.errorInvalid('plugin', plugin.name())
        self.reload(irc, msg, [plugin.name()])  # This makes the replySuccess.

    unrename = wrap(unrename, ['plugin'])

    def reloadlocale(self, irc, msg, args):
        """takes no argument

        Reloads the locale of the bot."""
        i18n.reloadLocales()
        irc.replySuccess()
예제 #5
0
class Github(callbacks.Plugin):
    """Add the help for \"@plugin help Github\" here
    This should describe how to use this plugin."""

    threaded = True
    address = Utility.configValue('address')
    port = Utility.configValue('port')
    messages = []
    pass

    add = None
    search = None
    stats = None
    change = None

    def ServerStart(self, httpd):
        try:
            log.info('Server Starts - %s:%s' % (self.address, self.port))
            httpd.serve_forever()
        except:
            return

    def __init__(self, irc):
        self.__parent = super(Github, self)
        self.__parent.__init__(irc)
        server_class = http.server.HTTPServer
        self.httpd = server_class((self.address, self.port),
                                  RequestHandler.GithubHandler)
        t = threading.Thread(target=self.ServerStart, args=(self.httpd, ))
        t.daemon = False
        t.start()

    def __call__(self, irc, msg):
        self.__parent.__call__(irc, msg)

    def die(self):
        self.httpd.server_close()
        self.httpd.shutdown()
        self.__parent.die()
        reload(RequestHandler)
        reload(Utility)

    def get(self, irc, msg, args, order, type, garbage):
        """[<order>] [<type>] [<garbage>]
        Returns the requested message generated by the request handler.
        Examples: get first message, get 2nd last response
        """
        _digits = re.compile(r'\d')  # "r" needed?

        orders = {'first': 1, 'second': 2, 'third': 3, 'fourth': 4}

        if _digits.search(order):
            requestedNum = int(re.sub("[^0-9]", "", order))
        else:
            requestedNum = orders[order] if order in orders else 1

        if order == 'last' or type == 'last':
            requestedNum = len(globals.messageList) - requestedNum + 1

        try:
            irc.reply(globals.messageList[requestedNum - 1])
        except IndexError:
            irc.error('No such message')

    # Debug command
    get = wrap(get,
               ['lowered', optional('lowered'),
                optional('text')]) if world.testing else False

    class secret(callbacks.Commands):
        class DB(plugins.DbiChannelDB):
            class DB(dbi.DB):
                class Record(dbi.Record):
                    __fields__ = ['secret']

                def add(self, secret, **kwargs):
                    record = self.Record(secret=secret, **kwargs)
                    return super(self.__class__, self).add(record)

                def set(self, id, secret, **kwargs):
                    record = self.Record(secret=secret, **kwargs)
                    return super(self.__class__, self).set(id, record)

        def __init__(self, irc):
            super(Github.secret, self).__init__(irc)
            self.db = plugins.DB(("github-secret"), {'flat': self.DB})()
            globals.secretDB = self.db

        def set(self, irc, msg, args, channel, secret):
            """[<channel>] secret

            Sets a Github secret for a channel to a specific value.
            <channel> is only necessary if the message isn't sent in the channel itself.
            """
            self.db.set(channel, 1, secret)
            # record = Github.secret.DB.DB.Record(secret = sec)
            # self.db.set(channel, 1, secret)
            irc.replySuccess()

        set = wrap(set, ['op', 'text'])

        def reset(self, irc, msg, args, channel):
            """[<channel>]

            Removes a Github secret for a channel.
            <channel> is only necessary if the message isn't sent in the channel itself.
            """
            self.db.remove(channel, 1)
            irc.reply("Channel %s no longer has a secret." % channel)

        reset = wrap(reset, ['op'])

        def generate(self, irc, msg, args, channel):
            """<channel>

            Generates a Github secret for a channel.
            <channel> is only necessary if the message isn't sent in the channel itself.
            """
            secret = Utility.randomString(40)
            irc.reply("Setting secret for %s to: %s" % (channel, secret))
            self.db.set(channel, 1, secret)

        generate = wrap(generate, ['op'])
예제 #6
0
class MeetBot(callbacks.Plugin):
    """Add the help for "@plugin help MeetBot" here
    This should describe *how* to use this plugin."""

    def __init__(self, irc):
        self.__parent = super(MeetBot, self)
        self.__parent.__init__(irc)

    # Instead of using real supybot commands, I just listen to ALL
    # messages coming in and respond to those beginning with our
    # prefix char.  I found this helpful from a not duplicating logic
    # standpoint (as well as other things).  Ask me if you have more
    # questions.

    # This captures all messages coming into the bot.
    def doPrivmsg(self, irc, msg):
        nick = msg.nick
        channel = msg.args[0]
        payload = msg.args[1]

        # The following is for debugging.  It's excellent to get an
        # interactive interperter inside of the live bot.  use
        # code.interact instead of my souped-up version if you aren't
        # on my computer:
        # if payload == 'interact':
        #    from rkddp.interact import interact ; interact()

        # Get our Meeting object, if one exists.  Have to keep track
        # of different servers/channels.
        # (channel, network) tuple is our lookup key.
        Mkey = (channel, irc.msg.tags['receivedOn'])
        M = meeting_cache.get(Mkey, None)

        # Start meeting if we are requested
        if payload[:13] == '#startmeeting':
            if M is not None:
                irc.error("Can't start another meeting, one is in progress.")
                return
            # This callback is used to send data to the channel:

            def _setTopic(x):
                irc.sendMsg(ircmsgs.topic(channel, x))

            def _sendReply(x):
                irc.sendMsg(ircmsgs.privmsg(channel, x))

            M = meeting.Meeting(channel=channel, owner=nick,
                                oldtopic=irc.state.channels[channel].topic,
                                writeRawLog=True,
                                setTopic=_setTopic, sendReply=_sendReply,
                                getRegistryValue=self.registryValue,
                                safeMode=True
                                )
            meeting_cache[Mkey] = M
            recent_meetings.append(
                (channel, irc.msg.tags['receivedOn'], time.ctime()))
            if len(recent_meetings) > 10:
                del recent_meetings[0]
        # If there is no meeting going on, then we quit
        if M is None:
            return
        # Add line to our meeting buffer.
        M.addline(nick, payload)
        # End meeting if requested:
        if M._meetingIsOver:
            # M.save()  # now do_endmeeting in M calls the save functions
            del meeting_cache[Mkey]

    def outFilter(self, irc, msg):
        """Log outgoing messages from supybot.
        """
        # Catch supybot's own outgoing messages to log them.  Run the
        # whole thing in a try: block to prevent all output from
        # getting clobbered.
        try:
            if msg.command in ('PRIVMSG'):
                # Note that we have to get our nick and network parameters
                # in a slightly different way here, compared to doPrivmsg.
                nick = irc.nick
                channel = msg.args[0]
                payload = msg.args[1]
                Mkey = (channel, irc.network)
                M = meeting_cache.get(Mkey, None)
                if M is not None:
                    M.addrawline(nick, payload)
        except Exception:
            import traceback
            print(traceback.print_exc())
            print("(above exception in outFilter, ignoring)")
        return msg

    # These are admin commands, for use by the bot owner when there
    # are many channels which may need to be independently managed.
    def listmeetings(self, irc, msg, args):
        """

        List all currently-active meetings."""
        reply = ""
        reply = ", ".join(str(x) for x in sorted(meeting_cache.keys()))
        if reply.strip() == '':
            irc.reply("No currently active meetings.")
        else:
            irc.reply(reply)
    listmeetings = wrap(listmeetings, ['admin'])

    def savemeetings(self, irc, msg, args):
        """

        Save all currently active meetings."""
        numSaved = 0
        for M in meeting_cache.items():
            M.config.save()
        irc.reply("Saved {0} meetings.".format(numSaved))
    savemeetings = wrap(savemeetings, ['admin'])

    def addchair(self, irc, msg, args, channel, network, nick):
        """<channel> <network> <nick>

        Add a nick as a chair to the meeting."""
        Mkey = (channel, network)
        M = meeting_cache.get(Mkey, None)
        if not M:
            irc.reply("Meeting on channel {0}, network {1} not found".format(
                channel, network))
            return
        M.chairs.setdefault(nick, True)
        irc.reply("Chair added: {0} on ({1}, {2}).".format(
            nick, channel, network))
    addchair = wrap(addchair, ['admin', "channel", "something", "nick"])

    def deletemeeting(self, irc, msg, args, channel, network, save):
        """<channel> <network> <saveit=True>

        Delete a meeting from the cache.  If save is given, save the
        meeting first, defaults to saving."""
        Mkey = (channel, network)
        if Mkey not in meeting_cache:
            irc.reply("Meeting on channel {0}, network {1} not found".format(
                channel, network))
            return
        if save:
            M = meeting_cache.get(Mkey, None)
            import time
            M.endtime = time.localtime()
            M.config.save()
        del meeting_cache[Mkey]
        irc.reply("Deleted: meeting on ({0}, {1}).".format(channel, network))
    deletemeeting = wrap(
        deletemeeting, ['admin', "channel", "something",
                        optional("boolean", True)]
    )

    def recent(self, irc, msg, args):
        """

        List recent meetings for admin purposes.
        """
        reply = []
        for channel, network, ctime in recent_meetings:
            Mkey = (channel, network)
            if Mkey in meeting_cache:
                state = ", running"
            else:
                state = ""
            reply.append("({0}, {1}, {2}{3})".format(
                channel, network, ctime, state))
        if reply:
            irc.reply(" ".join(reply))
        else:
            irc.reply("No recent meetings in internal state.")
    recent = wrap(recent, ['admin'])

    def pingall(self, irc, msg, args, message):
        """<text>

        Send a broadcast ping to all users on the channel.

        An message to be sent along with this ping must also be
        supplied for this command to work.
        """
        nick = msg.nick
        channel = msg.args[0]

        # We require a message to go out with the ping, we don't want
        # to waste people's time:
        if channel[0] != '#':
            irc.reply("Not joined to any channel.")
            return
        if message is None:
            irc.reply(
                "You must supply a description with the `pingall` command.  "
                "We don't want to go wasting people's times looking for why "
                "they are pinged."
            )
            return

        # Send announcement message
        irc.sendMsg(ircmsgs.privmsg(channel, message))
        # ping all nicks in lines of about 256
        nickline = ''
        nicks = sorted(irc.state.channels[channel].users,
                       key=lambda x: x.lower())  # Fix lambda
        for nick in nicks:
            nickline = nickline + nick + ' '
            if len(nickline) > 256:
                irc.sendMsg(ircmsgs.privmsg(channel, nickline))
                nickline = ''
        irc.sendMsg(ircmsgs.privmsg(channel, nickline))
        # Send announcement message
        irc.sendMsg(ircmsgs.privmsg(channel, message))

    pingall = wrap(pingall, [optional('text', None)])
예제 #7
0
class NBA(callbacks.Plugin):
    """Get scores from NBA.com."""

    _ENDPOINT_BASE_URL = "https://data.nba.net"

    _SCOREBOARD_ENDPOINT = _ENDPOINT_BASE_URL + "/10s/prod/v2/{}/" + "scoreboard.json"

    _TODAY_ENDPOINT = _ENDPOINT_BASE_URL + "/prod/v3/today.json"

    _FUZZY_DAYS = frozenset(("yesterday", "tonight", "today", "tomorrow"))

    _TEAM_TRICODES = frozenset((
        "CHA",
        "ATL",
        "IND",
        "MEM",
        "DET",
        "UTA",
        "CHI",
        "TOR",
        "CLE",
        "OKC",
        "DAL",
        "MIN",
        "BOS",
        "SAS",
        "MIA",
        "DEN",
        "LAL",
        "PHX",
        "NOP",
        "MIL",
        "HOU",
        "NYK",
        "ORL",
        "SAC",
        "PHI",
        "BKN",
        "POR",
        "GSW",
        "LAC",
        "WAS",
    ))

    def __init__(self, irc):
        self.__parent = super(NBA, self)
        self.__parent.__init__(irc)
        directory = conf.supybot.directories.data.dirize("NBA/")
        self._http = httplib2.Http(directory)

    def nba(self, irc, msg, args, optional_team, optional_date):
        """[<TTT>] [<YYYY-MM-DD>]

        Get games for a given date. If none is specified, return
        games scheduled for today. Optionally add team abbreviation
        to filter for a specific team.
        """

        # Check to see if there's optional input and if there is check
        # if it's a date or a team, or both.
        try:
            team, date = self._parseOptionalArguments(optional_team,
                                                      optional_date)
        except ValueError as error:
            irc.error(str(error))
            return

        try:
            games = (self._getTodayGames()
                     if date is None else self._getGamesForDate(date))
        except ConnectionError as error:
            irc.error("Could not connect to nba.com")
            return
        except:
            irc.error("Something went wrong")
            return

        games = self._filterGamesWithTeam(team, games)

        games_string = self._resultAsString(games)

        # Single game query? We can show some extra info.
        if len(games) == 1:
            game = games[0]

            # If the game has ended, we fetch the recap info from NBA.com:
            if game["ended"]:
                try:
                    recap = self._getRecapInfo(game)
                    games_string += " | {} {}".format(ircutils.bold("Recap:"),
                                                      recap)
                except:
                    pass

            else:
                # Otherwise, when querying a specific game in progress,
                # we show the broadcaster list.
                # Also, if it has a text nugget, and it's not
                # 'Watch live', we show it:
                broadcasters = game["tv_broadcasters"]
                broadcasters_string = self._broadcastersToString(broadcasters)
                games_string += " [{}]".format(broadcasters_string)

                nugget = game["text_nugget"]
                nugget_is_interesting = nugget and "Watch live" not in nugget
                if nugget_is_interesting:
                    games_string += " | {}".format(nugget)

        if date:
            date = pendulum.from_format(date, "YYYYMMDD").to_date_string()
        else:
            date = pendulum.now("US/Pacific").to_date_string()

        irc.reply("{0}: {1}".format(date, games_string))

    nba = wrap(nba, [
        optional("somethingWithoutSpaces"),
        optional("somethingWithoutSpaces")
    ])

    def nbatv(self, irc, msg, args, team):
        """[<TTT>]

        Given a team, if there is a game scheduled for today,
        return where it is being broadcasted.
        """
        try:
            team = self._parseTeamInput(team)
        except ValueError as error:
            irc.error(str(error))
            return

        games = self._filterGamesWithTeam(team, self._getTodayGames())

        if not games:
            irc.reply("{} is not playing today.".format(team))
            return

        game = games[0]
        game_string = self._gameToString(game)
        broadcasters_string = self._broadcastersToString(
            game["tv_broadcasters"])
        irc.reply("{} on: {}".format(game_string, broadcasters_string))

    nbatv = wrap(nbatv, ["somethingWithoutSpaces"])

    def nbanext(self, irc, msg, args, n, team, team2):
        """[<n>] <TTT> [<TTT>]

        Get the next <n> games (1 by default; max. 10) for a given team
        or, if two teams are provided, matchups between them.

        """
        MAX_GAMES_IN_RESULT = 10

        try:
            if team == team2:
                irc.error("Both teams should be different.")
                return

            team = self._parseTeamInput(team)
            if team2 is not None:
                team2 = self._parseTeamInput(team2)

            team_schedule = self._getTeamSchedule(team)
        except ValueError as error:
            irc.error(str(error))
            return

        last_played = team_schedule["lastStandardGamePlayedIndex"]

        # Keeping only the games that haven't been played:
        future_games = team_schedule["standard"][last_played + 1:]

        if n is None:
            n = 1
        end = min(MAX_GAMES_IN_RESULT, n, len(future_games) - 1)

        if team2 is None:
            games = future_games
        else:
            # Filtering matchups between team and team2:
            team2_id = self._tricodeToTeamId(team2)
            games = [
                g for g in future_games
                if team2_id in [g["vTeam"]["teamId"], g["hTeam"]["teamId"]]
            ]

        if not games:
            irc.error("I could not find future games.")
            return

        for game in games[:end]:
            irc.reply(self._upcomingGameToString(game))

    nbanext = wrap(
        nbanext,
        [
            optional("positiveInt"),
            "somethingWithoutSpaces",
            optional("somethingWithoutSpaces"),
        ],
    )

    def nbalast(self, irc, msg, args, n, team, team2):
        """[<n>] <TTT> [<TTT>]

        Get the last <n> games (1 by default; max. 10) for a given team
        or, if two teams are provided, matchups between them.

        """
        MAX_GAMES_IN_RESULT = 10

        try:
            if team == team2:
                irc.error("Both teams should be different.")
                return

            team = self._parseTeamInput(team)
            if team2 is not None:
                team2 = self._parseTeamInput(team2)

            team_schedule = self._getTeamSchedule(team)
        except ValueError as error:
            irc.error(str(error))
            return

        last_played = team_schedule["lastStandardGamePlayedIndex"]

        # Keeping only the games that have been played:
        team_past_games = team_schedule["standard"][:last_played + 1]

        # Making sure the number of games we will show is a valid one:
        if n is None:
            n = 1
        n = min(MAX_GAMES_IN_RESULT, n)

        if team2 is None:
            games = team_past_games
        else:
            # Filtering matchups between team and team2:
            team2_id = self._tricodeToTeamId(team2)
            games = [
                g for g in team_past_games
                if team2_id in [g["vTeam"]["teamId"], g["hTeam"]["teamId"]]
            ]

        if not games:
            irc.error("I could not find past games.")
            return

        for game in reversed(games[-n:]):  # Most-recent game first.
            irc.reply(self._pastGameToString(game))

    nbalast = wrap(
        nbalast,
        [
            optional("positiveInt"),
            "somethingWithoutSpaces",
            optional("somethingWithoutSpaces"),
        ],
    )

    @classmethod
    def _parseOptionalArguments(cls, optional_team, optional_date):
        """Parse the optional arguments, which could be None, and return
        a (team, date) tuple. In case of finding an invalid argument, it
        throws a ValueError exception.
        """
        # No arguments:
        if optional_team is None:
            return (None, None)

        # Both arguments:
        if (optional_date is not None) and (optional_team is not None):
            team = cls._parseTeamInput(optional_team)
            date = cls._parseDateInput(optional_date)
            return (team, date)

        # Only one argument:
        if cls._isPotentialDate(optional_team):
            # Should be a date.
            team = None
            date = cls._parseDateInput(optional_team)
        else:
            # Should be a team.
            team = cls._parseTeamInput(optional_team)
            date = None

        return (team, date)

    def _getTodayGames(self):
        return self._getGames(self._getTodayDate())

    def _getGamesForDate(self, date):
        return self._getGames(date)

    @staticmethod
    def _filterGamesWithTeam(team, games):
        """Given a list of games, return those that involve a given
        team. If team is None, return the list with no modifications.
        """
        if team is None:
            return games

        return [
            g for g in games
            if team == g["home_team"] or team == g["away_team"]
        ]

    ############################
    # Content-getting helpers
    ############################
    def _getTodayJSON(self):
        today_url = self._ENDPOINT_BASE_URL + "/10s/prod/v3/today.json"
        return self._getJSON(today_url)

    def _getGames(self, date):
        """Given a date, populate the url with it and try to download
        its content. If successful, parse the JSON data and extract the
        relevant fields for each game. Returns a list of games.
        """
        url = self._getEndpointURL(date)

        # If asking for today's results, revalidate the cached data.
        # ('If-Mod.-Since' flag.). This allows to get real-time scores.
        revalidate_cache = date == self._getTodayDate()
        response = self._getURL(url, revalidate_cache)

        json_data = self._extractJSON(response)

        return self._parseGames(json_data)

    @classmethod
    def _getEndpointURL(cls, date):
        return cls._SCOREBOARD_ENDPOINT.format(date)

    def _getTeamSchedule(self, tricode):
        """Fetch the json with the given team's schedule"""

        # First we fetch `today.json` to extract the path to teams'
        # schedules and `seasonScheduleYear`:
        today_json = self._getTodayJSON()
        schedule_path = today_json["links"]["teamScheduleYear2"]
        season_year = today_json["seasonScheduleYear"]

        # We also need to convert the `tricode` to a `team_id`:
        team_id = self._tricodeToTeamId(tricode)

        # (The path looks like this:
        # '/prod/v1/{{seasonScheduleYear}}/teams/{{teamId}}/schedule.json')

        # Now we can fill-in the url:
        schedule_path = schedule_path.replace("{{teamId}}", team_id)
        schedule_path = schedule_path.replace("{{seasonScheduleYear}}",
                                              str(season_year))

        return self._getJSON(self._ENDPOINT_BASE_URL + schedule_path)["league"]

    def _tricodeToTeamId(self, tricode):
        """Given a valid team tricode, get the `teamId` used in NBA.com"""

        teams_path = self._getJSON(self._TODAY_ENDPOINT)["links"]["teams"]
        teams_json = self._getJSON(self._ENDPOINT_BASE_URL + teams_path)

        for team in teams_json["league"]["standard"]:
            if team["tricode"] == tricode:
                return team["teamId"]

        raise ValueError("{} is not a valid tricode".format(tricode))

    def _teamIdToTricode(self, team_id):
        """Given a valid teamId, get the team's tricode"""

        teams_path = self._getJSON(self._TODAY_ENDPOINT)["links"]["teams"]
        teams_json = self._getJSON(self._ENDPOINT_BASE_URL + teams_path)

        for team in teams_json["league"]["standard"]:
            if team["teamId"] == team_id:
                return team["tricode"]

        raise ValueError("{} is not a valid teamId".format(team_id))

    def _getURL(self, url, force_revalidation=False):
        """Use httplib2 to download the URL's content.

        The `force_revalidation` parameter forces the data to be
        validated before being returned from the cache.
        In the worst case the data has not changed in the server,
        and we get a '304 - Not Modified' response.
        """
        user_agent = (
            "Mozilla/5.0                       (X11; Ubuntu; Linux x86_64; rv:45.0)    "
            "                   Gecko/20100101 Firefox/45.0")
        header = {"User-Agent": user_agent}

        if force_revalidation:
            header["Cache-Control"] = "max-age=0"

        response, content = self._http.request(url, "GET", headers=header)

        if response.fromcache:
            self.log.debug("%s - 304/Cache Hit", url)

        if response.status == 200:
            return content

        self.log.error("HTTP Error (%s): %s", url, error.code)
        raise ConnectionError("Could not access URL")

    @staticmethod
    def _extractJSON(body):
        return json.loads(body)

    def _getJSON(self, url):
        """Fetch `url` and return its contents decoded as json."""
        return self._extractJSON(self._getURL(url))

    @classmethod
    def _parseGames(cls, json_data):
        """Extract all relevant fields from NBA.com's scoreboard.json
        and return a list of games.
        """
        games = []
        for g in json_data["games"]:
            # Starting times are in UTC. By default, we will show
            # Eastern times.
            # (In the future we could add a user option to select
            # timezones.)
            try:
                starting_time = cls._ISODateToEasternTime(g["startTimeUTC"])
            except:
                starting_time = "TBD" if g["isStartTimeTBD"] else ""

            game_info = {
                "game_id": g["gameId"],
                "home_team": g["hTeam"]["triCode"],
                "away_team": g["vTeam"]["triCode"],
                "home_score": g["hTeam"]["score"],
                "away_score": g["vTeam"]["score"],
                "starting_year": g["startDateEastern"][0:4],
                "starting_month": g["startDateEastern"][4:6],
                "starting_day": g["startDateEastern"][6:8],
                "starting_time": starting_time,
                "starting_time_TBD": g["isStartTimeTBD"],
                "clock": g["clock"],
                "period": g["period"],
                "buzzer_beater": g["isBuzzerBeater"],
                "ended": (g["statusNum"] == 3),
                "text_nugget": g["nugget"]["text"].strip(),
                "tv_broadcasters": cls._extractGameBroadcasters(g),
            }

            games.append(game_info)

        return games

    @staticmethod
    def _extractGameBroadcasters(game_json):
        """Extract the list of broadcasters from the API.
        Return a dictionary of broadcasts:
        (['vTeam', 'hTeam', 'national', 'canadian']) to
        the short name of the broadcaster.
        """
        json_data = game_json["watch"]["broadcast"]["broadcasters"]
        game_broadcasters = dict()

        for category in json_data:
            broadcasters_list = json_data[category]
            if broadcasters_list and "shortName" in broadcasters_list[0]:
                game_broadcasters[category] = broadcasters_list[0]["shortName"]
        return game_broadcasters

    ############################
    # Formatting helpers
    ############################
    @classmethod
    def _resultAsString(cls, games):
        if not games:
            return "No games found"

        # sort games list and put F(inal) games at end
        sorted_games = sorted(games, key=lambda k: k["ended"])
        return " | ".join([cls._gameToString(g) for g in sorted_games])

    @classmethod
    def _gameToString(cls, game):
        """Given a game, format the information into a string
        according to the context.

        For example:
        * "MEM @ CLE 07:00 PM ET" (a game that has not started yet),
        * "HOU 132 GSW 127 F OT2" (a game that ended and went to 2
        overtimes),
        * "POR 36 LAC 42 8:01 Q2" (a game in progress).
        """
        away_team = game["away_team"]
        home_team = game["home_team"]

        if game["period"]["current"] == 0:  # The game hasn't started yet
            starting_time = (game["starting_time"]
                             if not game["starting_time_TBD"] else "TBD")
            return "{} @ {} {}".format(away_team, home_team, starting_time)

        # The game started => It has points:
        away_score = game["away_score"]
        home_score = game["home_score"]

        away_string = "{} {}".format(away_team, away_score)
        home_string = "{} {}".format(home_team, home_score)

        # Bold for the winning team:
        if int(away_score) > int(home_score):
            away_string = ircutils.bold(away_string)
        elif int(home_score) > int(away_score):
            home_string = ircutils.bold(home_string)

        game_string = "{} {} {}".format(
            away_string,
            home_string,
            cls._clockBoardToString(game["clock"], game["period"],
                                    game["ended"]),
        )
        # Highlighting 'buzzer-beaters':
        if game["buzzer_beater"] and not game["ended"]:
            game_string = ircutils.mircColor(game_string,
                                             fg="yellow",
                                             bg="black")

        return game_string

    @classmethod
    def _clockBoardToString(cls, clock, period, game_ended):
        """Get a string with current period and, if the game is still
        in progress, the remaining time in it.
        """
        period_number = period["current"]
        # Game hasn't started => There is no clock yet.
        if period_number == 0:
            return ""

        # Halftime
        if period["isHalftime"]:
            return ircutils.mircColor("Halftime", "orange")

        period_string = cls._periodToString(period_number)

        # Game finished:
        if game_ended:
            if period_number == 4:
                return ircutils.mircColor("F", "red")

            return ircutils.mircColor("F {}".format(period_string), "red")

        # Game in progress:
        if period["isEndOfPeriod"]:
            return ircutils.mircColor("E{}".format(period_string), "blue")

        # Period in progress, show clock:
        return "{} {}".format(clock,
                              ircutils.mircColor(period_string, "green"))

    @staticmethod
    def _periodToString(period):
        """Get a string describing the current period in the game.

        Period is an integer counting periods from 1 (so 5 would be
        OT1).
        The output format is as follows: {Q1...Q4} (regulation);
        {OT, OT2, OT3...} (overtimes).
        """
        if period <= 4:
            return "Q{}".format(period)

        ot_number = period - 4
        if ot_number == 1:
            return "OT"
        return "OT{}".format(ot_number)

    @staticmethod
    def _broadcastersToString(broadcasters):
        """Given a broadcasters dictionary (category->name), where
        category is in ['vTeam', 'hTeam', 'national', 'canadian'],
        return a printable string representation of that list.
        """
        items = []
        for category in ["vTeam", "hTeam", "national", "canadian"]:
            if category in broadcasters:
                items.append(broadcasters[category])
        return ", ".join(items)

    def _upcomingGameToString(self, game):
        """Given a team's upcoming game, return a string with
        the opponent's tricode and the date of the game.
        """

        date = self._ISODateToEasternDatetime(game["startTimeUTC"])

        home_tricode = self._teamIdToTricode(game["hTeam"]["teamId"])
        away_tricode = self._teamIdToTricode(game["vTeam"]["teamId"])

        if game["isHomeTeam"]:
            home_tricode = ircutils.bold(home_tricode)
        else:
            away_tricode = ircutils.bold(away_tricode)

        return "{} | {} @ {}".format(date, away_tricode, home_tricode)

    def _pastGameToString(self, game):
        """Given a team's upcoming game, return a string with
        the opponent's tricode and the result.
        """
        date = self._ISODateToEasternDate(game["startTimeUTC"])

        home_tricode = self._teamIdToTricode(game["hTeam"]["teamId"])
        away_tricode = self._teamIdToTricode(game["vTeam"]["teamId"])

        home_score = int(game["hTeam"]["score"])
        away_score = int(game["vTeam"]["score"])

        if game["isHomeTeam"]:
            was_victory = home_score > away_score
        else:
            was_victory = away_score > home_score

        if home_score > away_score:
            home_tricode = ircutils.bold(home_tricode)
            home_score = ircutils.bold(home_score)
        else:
            away_tricode = ircutils.bold(away_tricode)
            away_score = ircutils.bold(away_score)

        result = (ircutils.mircColor("W", "green")
                  if was_victory else ircutils.mircColor("L", "red"))

        points = "{} {} {} {}".format(away_tricode, away_score, home_tricode,
                                      home_score)

        if game["seasonStageId"] == 1:
            points += " (Preseason)"

        return "{} {} | {}".format(date, result, points)

    ############################
    # Date-manipulation helpers
    ############################
    @classmethod
    def _getTodayDate(cls):
        """Get the current date formatted as "YYYYMMDD".
        Because the API separates games by day of start, we will
        consider and return the date in the Pacific timezone.
        The objective is to avoid reading future games anticipatedly
        when the day rolls over at midnight, which would cause us to
        ignore games in progress that may have started on the previous
        day.
        Taking the west coast time guarantees that the day will advance
        only when the whole continental US is already on that day.
        """
        today = cls._pacificTimeNow().date()
        today_iso = today.isoformat()
        return today_iso.replace("-", "")

    @staticmethod
    def _easternTimeNow():
        return pendulum.now("US/Eastern")

    @staticmethod
    def _pacificTimeNow():
        return pendulum.now("US/Pacific")

    @staticmethod
    def _ISODateToEasternDate(iso):
        """Convert the ISO date in UTC time that the API outputs into an
        Eastern-time date.
        (The default human-readable format for the listing of games).
        """
        date = pendulum.parse(iso)
        date_eastern = date.in_tz("US/Eastern")
        eastern_date = date_eastern.strftime("%a %m/%d")
        return "{}".format(eastern_date)

    @staticmethod
    def _ISODateToEasternTime(iso):
        """Convert the ISO date in UTC time that the API outputs into an
        Eastern time formatted with am/pm.
        (The default human-readable format for the listing of games).
        """
        date = pendulum.parse(iso)
        date_eastern = date.in_tz("US/Eastern")
        eastern_time = date_eastern.strftime("%-I:%M %p")
        return "{} ET".format(eastern_time)

    @staticmethod
    def _ISODateToEasternDatetime(iso):
        """Convert the ISO date in UTC time that the API outputs into a
        string with a date and Eastern time formatted with am/pm.
        """
        date = pendulum.parse(iso)
        date_eastern = date.in_tz("US/Eastern")
        eastern_datetime = date_eastern.strftime("%a %m/%d, %I:%M %p")
        return "{} ET".format(eastern_datetime)

    @staticmethod
    def _stripDateSeparators(date_string):
        return date_string.replace("-", "")

    @classmethod
    def _EnglishDateToDate(cls, date):
        """Convert a human-readable like 'yesterday' to a datetime
        object and return a 'YYYYMMDD' string.
        """
        if date == "yesterday":
            day_delta = -1
        elif date == "today" or date == "tonight":
            day_delta = 0
        elif date == "tomorrow":
            day_delta = 1
        # Calculate the day difference and return a string
        date_string = cls._pacificTimeNow().add(
            days=day_delta).strftime("%Y%m%d")
        return date_string

    @classmethod
    def _isValidTricode(cls, team):
        return team in cls._TEAM_TRICODES

    ############################
    # Input-parsing helpers
    ############################
    @classmethod
    def _isPotentialDate(cls, string):
        """Given a user-provided string, check whether it could be a
        date.
        """
        return string.lower() in cls._FUZZY_DAYS or string.replace(
            "-", "").isdigit()

    @classmethod
    def _parseTeamInput(cls, team):
        """Given a user-provided string, try to extract an upper-case
        team tricode from it. If not valid, throws a ValueError
        exception.
        """
        t = team.upper()
        if not cls._isValidTricode(t):
            raise ValueError("{} is not a valid team".format(team))
        return t

    @classmethod
    def _parseDateInput(cls, date):
        """Verify that the given string is a valid date formatted as
        YYYY-MM-DD. Also, the API seems to go back until 2014-10-04,
        so we will check that the input is not a date earlier than that.
        In case of failure, throws a ValueError exception.
        """
        date = date.lower()

        if date in cls._FUZZY_DAYS:
            date = cls._EnglishDateToDate(date)

        elif date.replace("-", "").isdigit():
            try:
                parsed_date = pendulum.from_format(date, "YYYY-MM-DD")
            except:
                raise ValueError("Incorrect date format, should be YYYY-MM-DD")

            # The current API goes back until 2014-10-04. Is it in range?
            if parsed_date < pendulum.datetime(2014, 10, 4):
                raise ValueError("I can only go back until 2014-10-04")
        else:
            raise ValueError("Date is not valid")

        return cls._stripDateSeparators(date)

    def _getRecapInfo(self, game):
        """Given a finished game, fetch its recap summary and a link
        to its video recap. It returns a string with the format
        '{summary} (link to video)'.

        The link is shortened by calling _shortenURL(str) -> str.
        """

        recap_base_url = ("https://www.nba.com/video/"
                          "{year}/{month}/{day}/"
                          "{game_id}-{away_team}-{home_team}-recap.xml")

        url = recap_base_url.format(
            year=game["starting_year"],
            month=game["starting_month"],
            day=game["starting_day"],
            game_id=game["game_id"],
            away_team=game["away_team"].lower(),
            home_team=game["home_team"].lower(),
        )

        xml = self._getURL(url)
        tree = ElementTree.fromstring(xml)

        res = []

        summary = tree.find("description")
        if summary is not None:
            res.append(summary.text)

        video_recap = tree.find("*file[@bitrate='1920x1080_5904']")
        if video_recap is not None:
            url = self._shortenURL(video_recap.text)
            res.append("({})".format(url))

        return " ".join(res)

    @staticmethod
    def _shortenURL(url):
        """ Run a link through an URL shortener and return the new url."""

        # Complete with the code that uses your desired
        # shortener service.
        return url
예제 #8
0
class EfnetQuotes(callbacks.Plugin):
    """A list of channel quotes that you can add, remove or call randomly."""
    threaded = True
    full_path = os.path.dirname(os.path.abspath(__file__))
    db_file = '{0}/data/efnetquotes.db'.format(full_path)

    def create_database(self, irc, table):
        conn = None

        try:
            print('Connecting to SQLite3 database...')
            conn = sqlite3.connect(self.db_file)
            cursor = conn.cursor()

            sql = """CREATE TABLE IF NOT EXISTS {0} (
                        id INTEGER PRIMARY KEY,
                        nick TEXT NOT NULL,
                        host TEXT NOT NULL,
                        quote TEXT NOT NULL,
                        channel TEXT NOT NULL,
                        timestamp INT DEFAULT NULL);""".format(table)

            cursor.execute(sql)
            conn.commit()
            cursor.close()
            print('Database created.')

        except Error as e:
            print(e)

        finally:
            if conn is not None:
                conn.close()
                print('Database connection closed.')

    def connect(self, irc, table):
        """create a database connection to a SQLite3 database"""
        conn = None

        try:
            """
            Doing a check to see if there is a file or not.
            If not, create a database.
            """
            with open(self.db_file) as f:
                pass

            print('Connecting to the SQLite3 database...')
            conn = sqlite3.connect(self.db_file)

            return conn

        except IOError as e:
            irc.reply('No database found. Creating new database...')
            print(e)
            self.create_database(irc, table)

        except Error as e:
            print(e)

    def addquote(self, irc, msg, args, text):
        """<quote>
        Use this command to add a quote to the bot.
        """
        conn = None

        try:
            msg = str(msg).split(' ')
            host = msg[0][1:]
            nick = host.split('!')[0]
            channel = msg[2]
            now = datetime.utcnow()
            timestamp = calendar.timegm(now.utctimetuple())
            table = '{0}quotes'.format(channel[1:])
            """
            Making sure the command is called in a channel and not private chat
            """
            if not channel.startswith('#'):
                print('You must be in a channel to add a quote.')
                return

            sql = """INSERT INTO {0} (nick,host,quote,channel,timestamp)
                        VALUES(?,?,?,?,?)""".format(table)

            conn = self.connect(irc, table)
            cursor = conn.cursor()
            cursor.execute(sql, (
                nick,
                host,
                text,
                channel,
                timestamp,
            ))
            conn.commit()
            cursor.close()
            print('Quote inserted into the database.')
            irc.reply('Quote added.')

        except Error as e:
            print(e)

            if str(e).startswith('no such table'):
                self.create_database(irc, table)
                irc.reply('Creating new database table...try again.')

        except AttributeError as e:
            irc.reply('Now try add the quote again!')
            print(e)

        finally:
            if conn is not None:
                conn.close()
                print('Closing database connection...')

    addquote = commands.wrap(addquote, ['text'])

    def quote(self, irc, msg, args, text):
        """- optional <argument>
        Use this command to randomly search for quotes.
        """
        conn = None
        try:
            msg = str(msg).split(' ')
            nick = msg[0][1:].split('!')[0]
            channel = msg[2]
            search = '%{0}%'.format(text)
            table = '{0}quotes'.format(channel[1:])

            conn = self.connect(irc, table)
            cursor = conn.cursor()
            """
            Checking to see if there is an argument or no argument sent with
            the command. If argument, is the user searching for a quote # or
            quote text. Making sure the command is called in a channel and not
            private chat.
            """
            if not channel.startswith('#'):
                irc.reply('You must be in the channel to use this command')
                return

            elif text is not None:

                if text.isdigit():
                    sql = """SELECT id,quote FROM {0} WHERE
                                channel=? AND id=?""".format(table)

                    cursor.execute(sql, (
                        channel,
                        text,
                    ))

                else:
                    sql = """SELECT id,quote FROM {0} WHERE channel=?
                                AND (id LIKE ? OR quote LIKE ?) ORDER BY random()
                                LIMIT 1;""".format(table)

                    cursor.execute(sql, (
                        channel,
                        text,
                        search,
                    ))

            else:
                sql = """SELECT id,quote FROM {0} WHERE channel=?
                            ORDER BY random() LIMIT 1;""".format(table)

                cursor.execute(sql, (channel, ))

            quote = cursor.fetchone()
            """
            Checking to see if one of the select statements returned
            back a query or no matches.
            """
            if quote is not None:
                irc.reply('#{0}: {1}'.format(quote[0], quote[1]))
            else:
                irc.reply('No matches/quotes.')

            cursor.close()

        except Error as e:
            print(e)

            if str(e).startswith('no such table'):
                irc.reply('No match/quotes.')

        except AttributeError as e:
            irc.reply('Now use the .addquote command to add a new quote.')
            print(e)

        finally:
            if conn is not None:
                conn.close()
                print('Closing database connection...')

    quote = commands.wrap(quote, [commands.optional('text')])

    def delquote(self, irc, msg, args, text):
        """<quote number>
        Use this command with to delete a quote with the given quote number.
        """
        conn = None

        try:
            msg = str(msg).split(' ')
            host = msg[0][1:]
            nick = host.split('!')[0]
            channel = msg[2]
            search = '%{0}%'.format(text)
            table = '{0}quotes'.format(channel[1:])
            """
            Making sure the command is called in a channel and not private chat
            """
            if not channel.startswith('#'):
                irc.reply('You must be in the channel to use this command')
                return

            conn = self.connect(irc, table)
            cursor = conn.cursor()

            user = """SELECT nick,host FROM {0} WHERE id=?;""".format(table)

            cursor.execute(user, (text, ))
            author = cursor.fetchone()
            """
            Checking to see if select statement returned the author
            of the quote
            """
            if author is not None:
                auth_host = author[1].split('!')[1]

            else:
                cursor.close()
                irc.reply('No match/quotes')
                return
            """
            Checks to see if a user is the author of the quote.
            If so, then delete the quote.
            """
            if nick == author[0] or host.endswith(auth_host):
                sql = """DELETE FROM {0} WHERE id=?;""".format(table)

                cursor.execute(sql, (text, ))
                quote = cursor.fetchone()
                conn.commit()
                cursor.close()
                irc.reply('Quote #{0} deleted.'.format(text))

            else:
                irc.reply('You must be the author to delete.')
                cursor.close()

        except Error as e:
            print(e)

        finally:
            if conn is not None:
                conn.close()
                print('Closing database connection...')

    delquote = commands.wrap(delquote, ['text'])
예제 #9
0
            page = response.read()
            response.close()
            opener.close()
    
            # Trim
            page = page[page.find(r'<div id=currency_converter_result>'):]
            page = page[:page.find(r'<input')-1]
    
            # if the tag is present but contains no data, its length will be 34
            if len(page) == 34:
                page = 'Invalid Currency.'
            # in the event of a conversion failure, '\nCould not convert.' appears
            elif page.find(r'Could not convert.') != -1:
                page = 'Could not convert.'
            else:
                # remove tags and use the data
                page = page.replace(r'<div id=currency_converter_result>', '', 1)
                page = page.replace(r'<span class=bld>', '', 1)
                page = page.replace(r'</span>', '', 1)

            irc.reply(page)
            del page, url, timeout

    ex = wrap(ex, ['somethingWithoutSpaces',
              optional('somethingWithoutSpaces'),
              optional('somethingWithoutSpaces')])

Class = Ex

# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
예제 #10
0
class PbinAdmin(supybot.callbacks.Plugin):
    '''Provide administrative control over wiki to authorized users.'''
    threaded = True

    def __init__(self, irc):
        self.__parent = super(PbinAdmin, self)
        self.__parent.__init__(irc)

    @wrap([optional('channel'), 'somethingWithoutSpaces'])
    def whitelist(self, irc, msg, args, channel, address):
        '''[<channel>] <address>

        Add a registered subnet to whitelist for specified IP address.'''
        return self._cmd_wrapper(irc, msg, args, channel, address, 'wl')

    @wrap([optional('channel'), 'somethingWithoutSpaces'])
    def greylist(self, irc, msg, args, channel, paste_id):
        '''[<channel>] <paste_id>

        Add address for specified paste to grey list.'''
        return self._cmd_wrapper(irc, msg, args, channel, paste_id, 'gl')

    @wrap([optional('channel'), 'somethingWithoutSpaces'])
    def blacklist(self, irc, msg, args, channel, paste_id):
        '''[<channel>] <paste_id>

        Add address for specified paste to black list.'''
        return self._cmd_wrapper(irc, msg, args, channel, paste_id, 'bl')

    @wrap([optional('channel'), 'somethingWithoutSpaces'])
    def delete(self, irc, msg, args, channel, paste_id):
        '''[<channel>] <paste_id>

        Delete a paste with specified ID.'''
        return self._cmd_wrapper(irc, msg, args, channel, paste_id, 'del')

    def _cmd_wrapper(self, irc, msg, args, channel, target, command):
        '''Send request to the API server after performing sanity checks.'''
        # Pre-flight checks
        if not self.registryValue('enabled', msg.args[0]):
            return None

        # Check capability
        if not supybot.world.testing:
            capability = supybot.ircdb.makeChannelCapability(
                channel, 'pbinadmin')
            if not supybot.ircdb.checkCapability(
                    msg.prefix, capability, ignoreDefaultAllow=True):
                irc.errorNoCapability(capability, Raise=True)

        # Send API request
        resp = requests.post(self.registryValue('api_host', msg.args[0]),
                             json={
                                 'token':
                                 self.registryValue('api_token', msg.args[0]),
                                 'command':
                                 command,
                                 'target':
                                 target
                             },
                             headers={'Content-type': 'application/json'})
        if not resp:
            irc.error(_('No response data from API request.'), Raise=True)
        if resp.status_code != 200:
            irc.error(_('Unexpected status code received: %s') %
                      (resp.status_code),
                      Raise=True)

        rdata = resp.json()
        if not rdata:
            irc.error(_('No data decoded.'), Raise=True)

        if rdata.get('status', '') == 'error':
            irc.error(rdata.get('message', _('Unexpected server response.')),
                      Raise=True)
        irc.reply(rdata.get('message', _('Missing server response')))
예제 #11
0
class WeatherBot(callbacks.Plugin):
    """A weather script that uses Darksky and Weatherstack.
    """

    threaded = True

    @wrap(["owner"])
    def createdb(self, irc: callbacks.NestedCommandsIrcProxy, msg: ircmsgs.IrcMsg, args: List[str]) -> None:
        """- takes no arguments.
        Creates a new user table.
        """
        try:
            result: str = User.create_tables()
            irc.reply(result, prefixNick=False)

        except DatabaseError as exc:
            log.error(str(exc))
            irc.reply("There was an error with the database. Check logs.", prefixNick=False)

    @wrap([getopts({"user": ""}), optional("text")])
    def weather(
        self,
        irc: callbacks.NestedCommandsIrcProxy,
        msg: ircmsgs.IrcMsg,
        args: List[str],
        optlist: List[Tuple[str, bool]],
        text: str,
    ) -> None:
        """- optional <location> OR [--user] <username>
        Calls the current weather given an optional arg .e.g. .weather 70119 -
        If you leave out the location, it will try to use the user's set location that is saved -
        You can find another user's weather by using the --user flag. e.g. .weather --user Chad
        """
        lookup_user: bool = False
        for opt, _ in optlist:
            if opt == "user":
                lookup_user = True

        try:
            optional_user = html.escape(text) if lookup_user and text else msg.nick
            if lookup_user and not text:
                irc.reply(f"Please specify the user name.", prefixNick=False)
                return

            user: Union[User, AnonymousUser] = get_user(optional_user)

            if lookup_user:
                if not isinstance(user, AnonymousUser):
                    weather: str = query_current_weather("", user)
                    irc.reply(weather, prefixNick=False)
                else:
                    irc.reply(f"No such user by the name of {text}.", prefixNick=False)

            elif not text and isinstance(user, AnonymousUser):
                irc.reply(f"No weather location set by {msg.nick}", prefixNick=False)

            elif not text:
                weather: str = query_current_weather(text, user)
                irc.reply(weather, prefixNick=False)

            else:
                deserialized_location: Dict[str, str] = UserSchema().load({"location": html.escape(text)}, partial=True)
                weather: str = query_current_weather(deserialized_location["location"], user)
                irc.reply(weather, prefixNick=False)

        except ValidationError as exc:
            if "location" in exc.messages:
                message = exc.messages["location"][0]
            irc.reply(message, prefixNick=False)
            log.error(str(exc), exc_info=True)

        except DatabaseError as exc:
            log.error(str(exc), exc_info=True)
            if "not created" in str(exc):
                irc.reply(str(exc), prefixNick=True)
            else:
                irc.reply("There is an error. Contact admin.", prefixNick=False)

        except (LocationNotFound, WeatherNotFound) as exc:
            irc.reply(str(exc), prefixNick=False)

        except RequestException as exc:
            log.error(str(exc), exc_info=True)
            if exc.response.status_code == 400:
                irc.reply("Unable to find this location.", prefixNick=False)
            else:
                irc.reply("There is an error. Contact admin.", prefixNick=False)

    @wrap([getopts({"metric": ""}), "text"])
    def setweather(
        self,
        irc: callbacks.NestedCommandsIrcProxy,
        msg: ircmsgs.IrcMsg,
        args: List[str],
        optlist: List[Tuple[str, bool]],
        text: str,
    ) -> None:
        """[--metric] <location>
        Sets the weather location for a user. Format is set to show imperial first by default.
        To show metric first, use the --metric option. e.g. setweather --metric 70118
        """
        try:
            format = 1
            for option, _ in optlist:
                if option == "metric":
                    format = 2

            deserialized_location: Dict[str, str] = UserSchema().load(
                {"location": html.escape(text), "format": format}, partial=True,
            )
            geo: Dict[str, str] = query_location(deserialized_location["location"])
            geo.update({"nick": msg.nick, "host": f"{msg.user}@{msg.host}", "format": format})
            if geo["location"] is None:
                raise LocationNotFound("Unable to find this location.")

            user_schema: Dict[str, str] = UserSchema().load(geo)
            user, created = User.get_or_create(
                nick=msg.nick,
                defaults={
                    "host": user_schema["host"],
                    "format": user_schema["format"],
                    "location": user_schema["location"],
                    "region": user_schema["region"],
                    "coordinates": user_schema["coordinates"],
                },
            )

            # If created is a boolean of 0, it means it found a user.
            # Updates the user fields and saves to db.
            if not created:
                user.host = user_schema["host"]
                user.format = user_schema["format"]
                user.location = user_schema["location"]
                user.region = user_schema["region"]
                user.coordinates = user_schema["coordinates"]
                user.save()

            units = "imperial" if format == 1 else "metric"
            log.info(f"{msg.nick} set their location to {text}")
            irc.reply(f"{msg.nick} set their weather to {units} first and {text}.", prefixNick=False)

        except ValidationError as exc:
            if "location" in exc.messages:
                message = exc.messages["location"][0]
                irc.reply(message, prefixNick=False)
            elif "format" in exc.messages:
                message = exc.messages["format"][0]
                irc.reply(message, prefixNick=False)
            log.error(str(exc), exc_info=True)

        except DatabaseError as exc:
            log.error(str(exc), exc_info=True)
            if "not created" in str(exc):
                irc.reply(str(exc), prefixNick=True)
            else:
                irc.reply("There is an error. Contact admin.", prefixNick=False)

        except LocationNotFound as exc:
            irc.reply(str(exc), prefixNick=False)

        except RequestException as exc:
            log.error(str(exc), exc_info=True)
            irc.reply("Unable to find this location.", prefixNick=False)
예제 #12
0
파일: plugin.py 프로젝트: santigl/NBA
class NBA(callbacks.Plugin):
    """Get scores from NBA.com."""

    _ENDPOINT_BASE_URL = 'https://data.nba.net'

    _SCOREBOARD_ENDPOINT = (_ENDPOINT_BASE_URL + '/10s/prod/v2/{}/' +
                            'scoreboard.json')

    _TODAY_ENDPOINT = (_ENDPOINT_BASE_URL + '/prod/v3/today.json')

    _FUZZY_DAYS = frozenset(('yesterday', 'tonight', 'today', 'tomorrow'))

    _TEAM_TRICODES = frozenset(
        ('CHA', 'ATL', 'IND', 'MEM', 'DET', 'UTA', 'CHI', 'TOR', 'CLE', 'OKC',
         'DAL', 'MIN', 'BOS', 'SAS', 'MIA', 'DEN', 'LAL', 'PHX', 'NOP', 'MIL',
         'HOU', 'NYK', 'ORL', 'SAC', 'PHI', 'BKN', 'POR', 'GSW', 'LAC', 'WAS'))

    def __init__(self, irc):
        self.__parent = super(NBA, self)
        self.__parent.__init__(irc)

        self._http = httplib2.Http('.cache')

    def nba(self, irc, msg, args, optional_team, optional_date):
        """[<TTT>] [<YYYY-MM-DD>]

        Get games for a given date. If none is specified, return
        games scheduled for today. Optionally add team abbreviation
        to filter for a specific team.
        """

        # Check to see if there's optional input and if there is check
        # if it's a date or a team, or both.
        try:
            team, date = self._parseOptionalArguments(optional_team,
                                                      optional_date)
        except ValueError as error:
            irc.error(str(error))
            return

        try:
            games = self._getTodayGames() if date is None \
                    else self._getGamesForDate(date)
        except ConnectionError as error:
            irc.error('Could not connect to nba.com')
            return
        except:
            irc.error('Something went wrong')
            return

        games = self._filterGamesWithTeam(team, games)

        games_string = self._resultAsString(games)

        # Single game query? We can show some extra info.
        if len(games) == 1:
            game = games[0]

            # If the game has ended, we fetch the recap info from NBA.com:
            if game['ended']:
                try:
                    recap = self._getRecapInfo(game)
                    games_string += ' | {} {}'.format(ircutils.bold('Recap:'),
                                                      recap)
                except:
                    pass

            else:
                # Otherwise, when querying a specific game in progress,
                # we show the broadcaster list.
                # Also, if it has a text nugget, and it's not
                # 'Watch live', we show it:
                broadcasters = game['tv_broadcasters']
                broadcasters_string = self._broadcastersToString(broadcasters)
                games_string += ' [{}]'.format(broadcasters_string)

                nugget = game['text_nugget']
                nugget_is_interesting = nugget and 'Watch live' not in nugget
                if nugget_is_interesting:
                    games_string += ' | {}'.format(nugget)

        irc.reply(games_string)

    nba = wrap(nba, [
        optional('somethingWithoutSpaces'),
        optional('somethingWithoutSpaces')
    ])

    def tv(self, irc, msg, args, team):
        """[<TTT>]

        Given a team, if there is a game scheduled for today,
        return where it is being broadcasted.
        """
        try:
            team = self._parseTeamInput(team)
        except ValueError as error:
            irc.error(str(error))
            return

        games = self._filterGamesWithTeam(team, self._getTodayGames())

        if not games:
            irc.reply('{} is not playing today.'.format(team))
            return

        game = games[0]
        game_string = self._gameToString(game)
        broadcasters_string = self._broadcastersToString(
            game['tv_broadcasters'])
        irc.reply('{} on: {}'.format(game_string, broadcasters_string))

    tv = wrap(tv, ['somethingWithoutSpaces'])

    def next(self, irc, msg, args, n, team, team2):
        """[<n>] <TTT> [<TTT>]

        Get the next <n> games (1 by default; max. 10) for a given team
        or, if two teams are provided, matchups between them.

        """
        MAX_GAMES_IN_RESULT = 10

        try:
            if team == team2:
                irc.error('Both teams should be different.')
                return

            team = self._parseTeamInput(team)
            if team2 is not None:
                team2 = self._parseTeamInput(team2)

            team_schedule = self._getTeamSchedule(team)
        except ValueError as error:
            irc.error(str(error))
            return

        last_played = team_schedule['lastStandardGamePlayedIndex']

        # Keeping only the games that haven't been played:
        future_games = team_schedule['standard'][last_played + 1:]

        if n is None:
            n = 1
        end = min(MAX_GAMES_IN_RESULT, n, len(future_games) - 1)

        if team2 is None:
            games = future_games
        else:
            # Filtering matchups between team and team2:
            team2_id = self._tricodeToTeamId(team2)
            games = [g for g in future_games \
                     if team2_id in [g['vTeam']['teamId'],
                                     g['hTeam']['teamId']]]

        if not games:
            irc.error('I could not find future games.')
            return

        for game in games[:end]:
            irc.reply(self._upcomingGameToString(game))

    next = wrap(next, [
        optional('positiveInt'), 'somethingWithoutSpaces',
        optional('somethingWithoutSpaces')
    ])

    def last(self, irc, msg, args, n, team, team2):
        """[<n>] <TTT> [<TTT>]

        Get the last <n> games (1 by default; max. 10) for a given team
        or, if two teams are provided, matchups between them.

        """
        MAX_GAMES_IN_RESULT = 10

        try:
            if team == team2:
                irc.error('Both teams should be different.')
                return

            team = self._parseTeamInput(team)
            if team2 is not None:
                team2 = self._parseTeamInput(team2)

            team_schedule = self._getTeamSchedule(team)
        except ValueError as error:
            irc.error(str(error))
            return

        last_played = team_schedule['lastStandardGamePlayedIndex']

        # Keeping only the games that have been played:
        team_past_games = team_schedule['standard'][:last_played + 1]

        # Making sure the number of games we will show is a valid one:
        if n is None:
            n = 1
        n = min(MAX_GAMES_IN_RESULT, n)

        if team2 is None:
            games = team_past_games
        else:
            # Filtering matchups between team and team2:
            team2_id = self._tricodeToTeamId(team2)
            games = [g for g in team_past_games \
                     if team2_id in [g['vTeam']['teamId'],
                                     g['hTeam']['teamId']]]

        if not games:
            irc.error('I could not find past games.')
            return

        for game in reversed(games[-n:]):  # Most-recent game first.
            irc.reply(self._pastGameToString(game))

    last = wrap(last, [
        optional('positiveInt'), 'somethingWithoutSpaces',
        optional('somethingWithoutSpaces')
    ])

    @classmethod
    def _parseOptionalArguments(cls, optional_team, optional_date):
        """Parse the optional arguments, which could be None, and return
        a (team, date) tuple. In case of finding an invalid argument, it
        throws a ValueError exception.
        """
        # No arguments:
        if optional_team is None:
            return (None, None)

        # Both arguments:
        if (optional_date is not None) and (optional_team is not None):
            team = cls._parseTeamInput(optional_team)
            date = cls._parseDateInput(optional_date)
            return (team, date)

        # Only one argument:
        if cls._isPotentialDate(optional_team):
            # Should be a date.
            team = None
            date = cls._parseDateInput(optional_team)
        else:
            # Should be a team.
            team = cls._parseTeamInput(optional_team)
            date = None

        return (team, date)

    def _getTodayGames(self):
        return self._getGames(self._getTodayDate())

    def _getGamesForDate(self, date):
        return self._getGames(date)

    @staticmethod
    def _filterGamesWithTeam(team, games):
        """Given a list of games, return those that involve a given
        team. If team is None, return the list with no modifications.
        """
        if team is None:
            return games

        return [
            g for g in games
            if team == g['home_team'] or team == g['away_team']
        ]

############################
# Content-getting helpers
############################

    def _getTodayJSON(self):
        today_url = self._ENDPOINT_BASE_URL + '/10s/prod/v3/today.json'
        return self._getJSON(today_url)

    def _getGames(self, date):
        """Given a date, populate the url with it and try to download
        its content. If successful, parse the JSON data and extract the
        relevant fields for each game. Returns a list of games.
        """
        url = self._getEndpointURL(date)

        # If asking for today's results, revalidate the cached data.
        # ('If-Mod.-Since' flag.). This allows to get real-time scores.
        revalidate_cache = (date == self._getTodayDate())
        response = self._getURL(url, revalidate_cache)

        json_data = self._extractJSON(response)

        return self._parseGames(json_data)

    @classmethod
    def _getEndpointURL(cls, date):
        return cls._SCOREBOARD_ENDPOINT.format(date)

    def _getTeamSchedule(self, tricode):
        """Fetch the json with the given team's schedule"""

        # First we fetch `today.json` to extract the path to teams'
        # schedules and `seasonScheduleYear`:
        today_json = self._getTodayJSON()
        schedule_path = today_json['links']['teamScheduleYear2']
        season_year = today_json['seasonScheduleYear']

        # We also need to convert the `tricode` to a `team_id`:
        team_id = self._tricodeToTeamId(tricode)

        # (The path looks like this:
        # '/prod/v1/{{seasonScheduleYear}}/teams/{{teamId}}/schedule.json')

        # Now we can fill-in the url:
        schedule_path = schedule_path.replace('{{teamId}}', team_id)
        schedule_path = schedule_path.replace('{{seasonScheduleYear}}',
                                              str(season_year))

        return self._getJSON(self._ENDPOINT_BASE_URL + schedule_path)['league']

    def _tricodeToTeamId(self, tricode):
        """Given a valid team tricode, get the `teamId` used in NBA.com"""

        teams_path = self._getJSON(self._TODAY_ENDPOINT)['links']['teams']
        teams_json = self._getJSON(self._ENDPOINT_BASE_URL + teams_path)

        for team in teams_json['league']['standard']:
            if team['tricode'] == tricode:
                return team['teamId']

        raise ValueError('{} is not a valid tricode'.format(tricode))

    def _teamIdToTricode(self, team_id):
        """Given a valid teamId, get the team's tricode"""

        teams_path = self._getJSON(self._TODAY_ENDPOINT)['links']['teams']
        teams_json = self._getJSON(self._ENDPOINT_BASE_URL + teams_path)

        for team in teams_json['league']['standard']:
            if team['teamId'] == team_id:
                return team['tricode']

        raise ValueError('{} is not a valid teamId'.format(team_id))

    def _getURL(self, url, force_revalidation=False):
        """Use httplib2 to download the URL's content.

        The `force_revalidation` parameter forces the data to be
        validated before being returned from the cache.
        In the worst case the data has not changed in the server,
        and we get a '304 - Not Modified' response.
        """
        user_agent = 'Mozilla/5.0 \
                      (X11; Ubuntu; Linux x86_64; rv:45.0) \
                      Gecko/20100101 Firefox/45.0'

        header = {'User-Agent': user_agent}

        if force_revalidation:
            header['Cache-Control'] = 'max-age=0'

        response, content = self._http.request(url, 'GET', headers=header)

        if response.fromcache:
            self.log.debug('%s - 304/Cache Hit', url)

        if response.status == 200:
            return content

        self.log.error('HTTP Error (%s): %s', url, error.code)
        raise ConnectionError('Could not access URL')

    @staticmethod
    def _extractJSON(body):
        return json.loads(body)

    def _getJSON(self, url):
        """Fetch `url` and return its contents decoded as json."""
        return self._extractJSON(self._getURL(url))

    @classmethod
    def _parseGames(cls, json_data):
        """Extract all relevant fields from NBA.com's scoreboard.json
        and return a list of games.
        """
        games = []
        for g in json_data['games']:
            # Starting times are in UTC. By default, we will show
            # Eastern times.
            # (In the future we could add a user option to select
            # timezones.)
            try:
                starting_time = cls._ISODateToEasternTime(g['startTimeUTC'])
            except:
                starting_time = 'TBD' if g['isStartTimeTBD'] else ''

            game_info = {
                'game_id': g['gameId'],
                'home_team': g['hTeam']['triCode'],
                'away_team': g['vTeam']['triCode'],
                'home_score': g['hTeam']['score'],
                'away_score': g['vTeam']['score'],
                'starting_year': g['startDateEastern'][0:4],
                'starting_month': g['startDateEastern'][4:6],
                'starting_day': g['startDateEastern'][6:8],
                'starting_time': starting_time,
                'starting_time_TBD': g['isStartTimeTBD'],
                'clock': g['clock'],
                'period': g['period'],
                'buzzer_beater': g['isBuzzerBeater'],
                'ended': (g['statusNum'] == 3),
                'text_nugget': g['nugget']['text'].strip(),
                'tv_broadcasters': cls._extractGameBroadcasters(g)
            }

            games.append(game_info)

        return games

    @staticmethod
    def _extractGameBroadcasters(game_json):
        """Extract the list of broadcasters from the API.
        Return a dictionary of broadcasts:
        (['vTeam', 'hTeam', 'national', 'canadian']) to
        the short name of the broadcaster.
        """
        json_data = game_json['watch']['broadcast']['broadcasters']
        game_broadcasters = dict()

        for category in json_data:
            broadcasters_list = json_data[category]
            if broadcasters_list and 'shortName' in broadcasters_list[0]:
                game_broadcasters[category] = broadcasters_list[0]['shortName']
        return game_broadcasters

############################
# Formatting helpers
############################

    @classmethod
    def _resultAsString(cls, games):
        if not games:
            return "No games found"

        # sort games list and put F(inal) games at end
        sorted_games = sorted(games, key=lambda k: k['ended'])
        return ' | '.join([cls._gameToString(g) for g in sorted_games])

    @classmethod
    def _gameToString(cls, game):
        """ Given a game, format the information into a string
        according to the context.

        For example:
        * "MEM @ CLE 07:00 PM ET" (a game that has not started yet),
        * "HOU 132 GSW 127 F OT2" (a game that ended and went to 2
        overtimes),
        * "POR 36 LAC 42 8:01 Q2" (a game in progress).
        """
        away_team = game['away_team']
        home_team = game['home_team']

        if game['period']['current'] == 0:  # The game hasn't started yet
            starting_time = game['starting_time'] \
                            if not game['starting_time_TBD'] \
                            else "TBD"
            return "{} @ {} {}".format(away_team, home_team, starting_time)

        # The game started => It has points:
        away_score = game['away_score']
        home_score = game['home_score']

        away_string = "{} {}".format(away_team, away_score)
        home_string = "{} {}".format(home_team, home_score)

        # Bold for the winning team:
        if int(away_score) > int(home_score):
            away_string = ircutils.bold(away_string)
        elif int(home_score) > int(away_score):
            home_string = ircutils.bold(home_string)

        game_string = "{} {} {}".format(
            away_string, home_string,
            cls._clockBoardToString(game['clock'], game['period'],
                                    game['ended']))
        # Highlighting 'buzzer-beaters':
        if game['buzzer_beater'] and not game['ended']:
            game_string = ircutils.mircColor(game_string,
                                             fg='yellow',
                                             bg='black')

        return game_string

    @classmethod
    def _clockBoardToString(cls, clock, period, game_ended):
        """Get a string with current period and, if the game is still
        in progress, the remaining time in it.
        """
        period_number = period['current']
        # Game hasn't started => There is no clock yet.
        if period_number == 0:
            return ''

        # Halftime
        if period['isHalftime']:
            return ircutils.mircColor('Halftime', 'orange')

        period_string = cls._periodToString(period_number)

        # Game finished:
        if game_ended:
            if period_number == 4:
                return ircutils.mircColor('F', 'red')

            return ircutils.mircColor("F {}".format(period_string), 'red')

        # Game in progress:
        if period['isEndOfPeriod']:
            return ircutils.mircColor("E{}".format(period_string), 'blue')

        # Period in progress, show clock:
        return "{} {}".format(clock,
                              ircutils.mircColor(period_string, 'green'))

    @staticmethod
    def _periodToString(period):
        """Get a string describing the current period in the game.

        Period is an integer counting periods from 1 (so 5 would be
        OT1).
        The output format is as follows: {Q1...Q4} (regulation);
        {OT, OT2, OT3...} (overtimes).
        """
        if period <= 4:
            return "Q{}".format(period)

        ot_number = period - 4
        if ot_number == 1:
            return "OT"
        return "OT{}".format(ot_number)

    @staticmethod
    def _broadcastersToString(broadcasters):
        """Given a broadcasters dictionary (category->name), where
        category is in ['vTeam', 'hTeam', 'national', 'canadian'],
        return a printable string representation of that list.
        """
        items = []
        for category in ['vTeam', 'hTeam', 'national', 'canadian']:
            if category in broadcasters:
                items.append(broadcasters[category])
        return ', '.join(items)

    def _upcomingGameToString(self, game):
        """Given a team's upcoming game, return a string with
        the opponent's tricode and the date of the game.
        """

        date = self._ISODateToEasternDatetime(game['startTimeUTC'])

        home_tricode = self._teamIdToTricode(game['hTeam']['teamId'])
        away_tricode = self._teamIdToTricode(game['vTeam']['teamId'])

        if game['isHomeTeam']:
            home_tricode = ircutils.bold(home_tricode)
        else:
            away_tricode = ircutils.bold(away_tricode)

        return '{} | {} @ {}'.format(date, away_tricode, home_tricode)

    def _pastGameToString(self, game):
        """Given a team's upcoming game, return a string with
        the opponent's tricode and the result.
        """
        date = self._ISODateToEasternDate(game['startTimeUTC'])

        home_tricode = self._teamIdToTricode(game['hTeam']['teamId'])
        away_tricode = self._teamIdToTricode(game['vTeam']['teamId'])

        home_score = int(game['hTeam']['score'])
        away_score = int(game['vTeam']['score'])

        if game['isHomeTeam']:
            was_victory = (home_score > away_score)
        else:
            was_victory = (away_score > home_score)

        if home_score > away_score:
            home_tricode = ircutils.bold(home_tricode)
            home_score = ircutils.bold(home_score)
        else:
            away_tricode = ircutils.bold(away_tricode)
            away_score = ircutils.bold(away_score)

        result = ircutils.mircColor('W', 'green') if was_victory \
                 else ircutils.mircColor('L', 'red')

        points = '{} {} {} {}'.format(away_tricode, away_score, home_tricode,
                                      home_score)

        if game['seasonStageId'] == 1:
            points += ' (Preseason)'

        return '{} {} | {}'.format(date, result, points)

############################
# Date-manipulation helpers
############################

    @classmethod
    def _getTodayDate(cls):
        """Get the current date formatted as "YYYYMMDD".
        Because the API separates games by day of start, we will
        consider and return the date in the Pacific timezone.
        The objective is to avoid reading future games anticipatedly
        when the day rolls over at midnight, which would cause us to
        ignore games in progress that may have started on the previous
        day.
        Taking the west coast time guarantees that the day will advance
        only when the whole continental US is already on that day.
        """
        today = cls._pacificTimeNow().date()
        today_iso = today.isoformat()
        return today_iso.replace('-', '')

    @staticmethod
    def _easternTimeNow():
        return datetime.datetime.now(pytz.timezone('US/Eastern'))

    @staticmethod
    def _pacificTimeNow():
        return datetime.datetime.now(pytz.timezone('US/Pacific'))

    @staticmethod
    def _ISODateToEasternDate(iso):
        """Convert the ISO date in UTC time that the API outputs into an
        Eastern-time date.
        (The default human-readable format for the listing of games).
        """
        date = dateutil.parser.parse(iso)
        date_eastern = date.astimezone(pytz.timezone('US/Eastern'))
        eastern_date = date_eastern.strftime('%a %m/%d')
        return "{}".format(eastern_date)

    @staticmethod
    def _ISODateToEasternTime(iso):
        """Convert the ISO date in UTC time that the API outputs into an
        Eastern time formatted with am/pm.
        (The default human-readable format for the listing of games).
        """
        date = dateutil.parser.parse(iso)
        date_eastern = date.astimezone(pytz.timezone('US/Eastern'))
        eastern_time = date_eastern.strftime('%-I:%M %p')
        return "{} ET".format(eastern_time)

    @staticmethod
    def _ISODateToEasternDatetime(iso):
        """Convert the ISO date in UTC time that the API outputs into a
        string with a date and Eastern time formatted with am/pm.
        """
        date = dateutil.parser.parse(iso)
        date_eastern = date.astimezone(pytz.timezone('US/Eastern'))
        eastern_datetime = date_eastern.strftime('%a %m/%d, %I:%M %p')
        return "{} ET".format(eastern_datetime)

    @staticmethod
    def _stripDateSeparators(date_string):
        return date_string.replace('-', '')

    @classmethod
    def _EnglishDateToDate(cls, date):
        """Convert a human-readable like 'yesterday' to a datetime
        object and return a 'YYYYMMDD' string.
        """
        if date == 'yesterday':
            day_delta = -1
        elif date == 'today' or date == 'tonight':
            day_delta = 0
        elif date == 'tomorrow':
            day_delta = 1
        # Calculate the day difference and return a string
        date_string = (cls._pacificTimeNow() +
                       datetime.timedelta(days=day_delta)).strftime('%Y%m%d')
        return date_string

    @classmethod
    def _isValidTricode(cls, team):
        return team in cls._TEAM_TRICODES


############################
# Input-parsing helpers
############################

    @classmethod
    def _isPotentialDate(cls, string):
        """Given a user-provided string, check whether it could be a
        date.
        """
        return (string.lower() in cls._FUZZY_DAYS
                or string.replace('-', '').isdigit())

    @classmethod
    def _parseTeamInput(cls, team):
        """Given a user-provided string, try to extract an upper-case
        team tricode from it. If not valid, throws a ValueError
        exception.
        """
        t = team.upper()
        if not cls._isValidTricode(t):
            raise ValueError('{} is not a valid team'.format(team))
        return t

    @classmethod
    def _parseDateInput(cls, date):
        """Verify that the given string is a valid date formatted as
        YYYY-MM-DD. Also, the API seems to go back until 2014-10-04,
        so we will check that the input is not a date earlier than that.
        In case of failure, throws a ValueError exception.
        """
        date = date.lower()

        if date in cls._FUZZY_DAYS:
            date = cls._EnglishDateToDate(date)

        elif date.replace('-', '').isdigit():
            try:
                parsed_date = datetime.datetime.strptime(date, '%Y-%m-%d')
            except:
                raise ValueError('Incorrect date format, should be YYYY-MM-DD')

            # The current API goes back until 2014-10-04. Is it in range?
            if parsed_date.date() < datetime.date(2014, 10, 4):
                raise ValueError('I can only go back until 2014-10-04')
        else:
            raise ValueError('Date is not valid')

        return cls._stripDateSeparators(date)

    def _getRecapInfo(self, game):
        """Given a finished game, fetch its recap summary and a link
        to its video recap. It returns a string with the format
        '{summary} (link to video)'.

        The link is shortened by calling _shortenURL(str) -> str.
        """

        recap_base_url = 'https://www.nba.com/video/'\
                         '{year}/{month}/{day}/'\
                         '{game_id}-{away_team}-{home_team}-recap.xml'

        url = recap_base_url.format(year=game['starting_year'],
                                    month=game['starting_month'],
                                    day=game['starting_day'],
                                    game_id=game['game_id'],
                                    away_team=game['away_team'].lower(),
                                    home_team=game['home_team'].lower())

        xml = self._getURL(url)
        tree = ElementTree.fromstring(xml)

        res = []

        summary = tree.find('description')
        if summary is not None:
            res.append(summary.text)

        video_recap = tree.find("*file[@bitrate='1920x1080_5904']")
        if video_recap is not None:
            url = self._shortenURL(video_recap.text)
            res.append('({})'.format(url))

        return ' '.join(res)

    @staticmethod
    def _shortenURL(url):
        """ Run a link through an URL shortener and return the new url."""

        # Complete with the code that uses your desired
        # shortener service.
        return url
예제 #13
0
파일: plugin.py 프로젝트: architek/yggscr
class YBot(callbacks.Plugin):
    """sup ygg bot"""
    threaded = True

    def __init__(self, irc):
        global _shout_err

        self.__parent = super(YBot, self)
        self.__parent.__init__(irc)
        self.yggb = YggBrowser(log=self.log)
        self._shout = YggShout(robs=self.yggb, log=self.log)
        _shout_err = 0
        self._col = dict()

    def yggv(self, irc, msg, args):
        """
        Prints the plugin version
        """
        irc.reply(yggscr.__version__)

    yggv = wrap(yggv)

    def yconn(self, irc, msg, args):
        """
        Print connection details
        """
        irc.reply("{}".format(self.yggb))

    yconn = wrap(yconn)

    def yprox(self, irc, msg, args, https_proxy):
        """[https proxy]
        Sets or removes proxy (http, socks, ..)
        """
        if https_proxy:
            self.yggb.proxify(https_proxy)
        else:
            self.yggb.proxify(None)
        irc.replySuccess()

    yprox = wrap(yprox, ['owner', optional('anything')])

    def ysearch(self, irc, msg, args, n, detail, p):  # noqa
        """[n(int)] [detail (True/False)] q:pattern [c:cat [s:subcat]] [opt:val]*
        Searches on ygg and return first page results -
        Will only return the first nmax results and waits 1s between each reply
        """
        q = {}
        try:
            for t in p.split():
                k, v = t.rsplit(':', 1)
                if k in q.keys():
                    if isinstance(q[k], list):
                        q[k].append(v)
                    else:
                        q[k] = [q[k], v]
                else:
                    q[k] = v
        except ValueError:
            irc.error("Wrong syntax")
            return

        q['name'] = q.pop('q')
        q['category'] = q.pop('c', "")
        q['sub_category'] = q.pop('s', "")

        if n is None:
            n = 3
        if detail is None:
            detail = False

        try:
            torrents = self.yggb.search_torrents(detail=detail,
                                                 q=q,
                                                 nmax=int(n))
        except (requests.exceptions.ProxyError,
                requests.exceptions.ConnectionError) as e:
            irc.error("Network Error: %s" % e)
            return
        except YggException as e:
            irc.error("Ygg Exception raised: %s" % e)
            return
        if torrents is None:
            irc.reply("No results")
            return
        for idx, torrent in enumerate(torrents[:n]):
            sleep(1)
            irc.reply(
                "%2d - %s [%s Size:%s C:%s S:%s L:%s Comm:%s Uploader:%s] : %s"
                % (1 + idx, torrent.title, torrent.publish_date, torrent.size,
                   torrent.completed, torrent.seed, torrent.leech,
                   torrent.comm, torrent.uploader, torrent.href))

    ysearch = wrap(ysearch, [optional('int'), optional('boolean'), 'text'])

    def ycat(self, irc, msg, args):
        """Will list available cat/subcat combinaisons
        """
        irc.reply("Available (cat, subcat) combinaisons:{}".format(
            list_cat_subcat()))

    ycat = wrap(ycat)

    def ylogin(self, irc, msg, args, yuser, ypass):
        """[user pass]
        Logins to ygg using given credentials or stored one
        """
        if not yuser and not ypass:
            yuser = self.registryValue('cred.user')
            ypass = self.registryValue('cred.pass')
            if not yuser or not ypass:
                irc.error("You need to set cred.user and cred.pass")
                return
        elif not ypass:
            irc.error("Wrong syntax")
            return
        try:
            self.yggb.login(yuser, ypass)
        except (requests.exceptions.ProxyError,
                requests.exceptions.ConnectionError) as e:
            irc.error("Network Error: %s" % e)
            return
        except YggException as e:
            irc.error("Ygg Exception raised: %s" % e)
            return
        except Exception as e:
            irc.error("Could not login to Ygg with credentials: %s" % e)
            return
        irc.replySuccess()
        self.log.info("Connected as {}".format(yuser))

    ylogin = wrap(
        ylogin, ['owner', optional('anything'),
                 optional('anything')])

    def ylogout(self, irc, msg, args):
        """
        Logout from ygg
        """
        self.yggb.logout()
        irc.replySuccess()

    ylogout = wrap(ylogout, ['owner'])

    def ystats(self, irc, msg, args):
        """
        Return ratio stats
        """
        if self.yggb.idstate == "Anonymous":
            irc.error("You need to be authenticated at ygg")
        else:
            try:
                r = self.yggb.get_stats()
            except (requests.exceptions.ProxyError,
                    requests.exceptions.ConnectionError) as e:
                irc.error("Network Error: %s" % e)
                return
            except YggException as e:
                irc.error("Ygg Exception raised: %s" % e)
                return
            except Exception as e:
                irc.error("Could not get stats: %s" % e)
                return
            irc.reply('↑ {:7.2f}GB ↓ {:7.2f}GB % {:6.4f}'.format(
                r['up'], r['down'], r['ratio']))
            irc.reply(
                '↑ Instant {}KBps Mean {}KBps ↓ Instant {}KBps Mean {}KBps'.
                format(r['i_up'], r['m_up'], r['i_down'], r['m_down']))

    ystats = wrap(ystats)

    def yresp(self, irc, msg, args):
        """
        Print http response on console
        """
        self.log.info("ygg request response:%s" % self.yggb.response())
        irc.replySuccess()

    yresp = wrap(yresp)

    def yping(self, irc, msg, args, n, quiet):
        """[n] [quiet: boolean(default False)]
        GET /
        """
        t = []
        statuses = defaultdict(int)
        mmin, mmax, mmean = float("inf"), float("-inf"), float("inf")

        if n is None:
            n = 1
        if n > 100:
            n = 100
        if n > 10 and quiet is False:
            n = 10

        for _ in range(n):
            try:
                t1 = time()
                sts = self.yggb.ping()
                t2 = time()
                dt = 1000 * (t2 - t1)
                mmax = max(mmax, dt)
                mmin = min(mmin, dt)
                t.append(dt)
                if not quiet:
                    irc.reply("{:>2} ping {} time={:>7.2f}ms http {}".format(
                        1 + _ if n > 1 else "", self.yggb.browser.url, dt,
                        sts),
                              prefixNick=False)
                statuses[sts] += 1
            except Exception as e:
                mmax = float("inf")
                irc.reply("{:>2} timeout! [{}]".format(1 + _, e),
                          prefixNick=False)
        if n == 1:
            return
        if t:
            mmean = sum(t) / len(t)
        str_statuses = ' | '.join('{}:{}'.format(key, value)
                                  for key, value in statuses.items())
        irc.reply(
            "{} packet{} transmitted, {} received, {:.2%} packet loss, http codes {}"
            .format(n, "s" if n > 1 else "", len(t), 1 - len(t) / n,
                    str_statuses),
            prefixNick=False)
        irc.reply("rtt min/avg/max = {:.2f}/{:.2f}/{:.2f} ms".format(
            mmin, mmean, mmax),
                  prefixNick=False)

    yping = wrap(yping, [optional('PositiveInt'), optional('boolean')])

    def colorize_user(self, user, group, w_colour):

        colours = ('blue', 'green', 'brown', 'purple', 'orange', 'yellow',
                   'light green', 'teal', 'light blue', 'pink', 'dark gray',
                   'light gray')

        # 1: unknown, 2: Membre, 3: supermod, 4: mod, 5: tp, 8: nouveau membre, 9: desactivé
        gcolours = {
            1: 'blue',
            3: 'orange',
            4: 'green',
            5: 'pink',
            8: 'purple',
            9: 'brown'
        }

        # Don't colorize members unless w_colour for color tracking
        if group == 2:
            if w_colour:
                mhash = sha256()
                mhash.update(user.encode())
                mhash = mhash.digest()[0]
                mhash = hash % len(colours)
                user = ircutils.mircColor(user, colours[mhash])
            else:
                pass
        elif group not in gcolours.keys():
            user = ircutils.mircColor(user, gcolours[1])
        else:
            user = ircutils.mircColor(user, gcolours[group])

        # High grade in bold
        if group in [1, 3, 4, 5]:
            user = ircutils.bold(user)
        return user

    def shoutify(self, shoutm, w_colour):
        user = "******".format(shoutm.user)
        user = self.colorize_user(user, shoutm.group, w_colour)
        fmt = self.registryValue('shout.fmt')
        msg = shoutm.message.replace('\n', ' ').replace('\n', ' ')
        return fmt.format(time=shoutm.mtime,
                          id=shoutm.id,
                          fuser=user,
                          user=shoutm.user,
                          group=shoutm.group,
                          message=msg)

    def yshout(self, irc, msg, args, n, w_colour=False, hfile=None):
        """[int n] [boolean user_colorized] [injected html file]
        Print last shout messages and detects gap. Time is UTC.
        User will be colorized if boolean is True.
        """
        global _shout_err
        rate_err = self.registryValue('shout.rate_err')
        if hfile:
            try:
                with open(hfile, "r") as fn:
                    html = fn.read()
            except Exception:
                irc.error("Can't read file %s" % hfile)
                return
            shoutm = ShoutMessage(shout=None,
                                  soup=BeautifulSoup(html, 'html.parser'))
            irc.reply(self.shoutify(shoutm, False), prefixNick=False)
            return
        try:
            self._shout.get_shouts()
            diff = self._shout.do_diff()
            _shout_err = 0
        except Exception as e:
            self.log.info("Could not dump shout, aborting. Error %s. Tid %s",
                          e, threading.get_ident())
            _shout_err += 1
            if _shout_err % rate_err == 0:
                irc.error(
                    "Shout ({} messages suppressed) (Exception {})".format(
                        rate_err, e))
                irc.error("Connection details: {}".format(self.yggb))
            return
        if n is None:
            n = len(diff)
        for removed, shoutm in diff[len(diff) - n:]:
            prefix = "REMOVED!!: " if removed else ""
            irc.reply(prefix + self.shoutify(shoutm, w_colour),
                      prefixNick=False)
            sleep(1)

    yshout = wrap(
        yshout,
        ['owner',
         optional('int'),
         optional('boolean'),
         optional('filename')])

    def ydebug(self, irc, msg, args, debug):
        """[debug: boolean to set debug]
        Get or set bot debug level
        """
        if debug is None:
            irc.reply(
                "Debug level for %s is %s" %
                (self.log.name, yggscr.ylogging.loggerlevel_as_text(self.log)))
        else:
            yggscr.ylogging.set_log_debug(debug)
            irc.replySuccess()

    ydebug = wrap(ydebug, [optional('boolean')])