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"])
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())
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)
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()
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'])
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)])
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
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'])
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:
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')))
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)
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
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')])