def _guess(self, message: IRCMessage) -> Union[IRCResponse, List[IRCResponse]]: channel = message.replyTo.lower() if channel not in self.gameStates: return IRCResponse( ResponseType.Say, '[Hangman] no game running, use {}hangman start to begin!'. format(self.bot.commandChar), message.replyTo) responses = [] gs = self.gameStates[channel] guess = message.parameters.lower() # single letter if len(guess) == 1: try: correct = gs.guessLetter(guess) except (AlreadyGuessedException, InvalidCharacterException) as e: return self._exceptionFormatter(e, message.replyTo) # whole phrase else: try: correct = gs.guessPhrase(guess) except (WrongPhraseLengthException, PhraseMismatchesGuessesException, PhraseUsesKnownBadLettersException) as e: return self._exceptionFormatter(e, message.replyTo) user = message.user.nick # split the username with a zero-width space # hopefully this kills client highlighting on nick mentions # user = user[:1] + '\u200b' + user[1:] # try a tiny arrow instead, some clients actually render zero-width spaces colUser = user[:1] + '\u034e' + user[1:] if correct: colUser = colour(A.normal[A.fg.green[colUser]]) else: colUser = colour(A.normal[A.fg.red[colUser]]) responses.append( IRCResponse(ResponseType.Say, '{} - {}'.format(gs.render(), colUser), message.replyTo)) if gs.finished: if correct: responses.append( IRCResponse(ResponseType.Say, '[Hangman] Congratulations {}!'.format(user), message.replyTo)) else: responses.append( IRCResponse( ResponseType.Say, '[Hangman] {} blew up the bomb! The {} was {}'.format( user, gs.wOrP(), gs.phrase), message.replyTo)) self._stop(message, suppressMessage=True) return responses
def _renderGuesses(self): colouredGuesses = [] for g in self.guesses: if g in self.phrase: colouredGuesses.append(colour(A.bold[A.fg.green[g]])) else: colouredGuesses.append(colour(A.fg.red[g])) reset = colour(A.normal['']) return '[{}{}]'.format(''.join(colouredGuesses), reset)
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search( r'twitter\.com/(?P<tweeter>[^/]+)/status(es)?/(?P<tweetID>[0-9]+)', url) if not match: return tweeter = match.group('tweeter') tweetID = match.group('tweetID') url = 'https://twitter.com/{}/status/{}'.format(tweeter, tweetID) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) soup = BeautifulSoup(response.content, 'lxml') tweet = soup.find(class_='permalink-tweet') displayName = tweet['data-name'] user = tweet.find(class_='username').text reply = tweet.find(class_='ReplyingToContextBelowAuthor') if reply: reply = 'r' + reply.text.strip()[1:] tweetText = tweet.find(class_='tweet-text') tweetTimeText = tweet.find(class_='client-and-actions').text.strip() try: tweetTimeText = time.strptime(tweetTimeText, '%I:%M %p - %d %b %Y') tweetTimeText = time.strftime('%Y/%m/%d %H:%M', tweetTimeText) except ValueError: pass tweetTimeText = re.sub(r'[\r\n\s]+', u' ', tweetTimeText) links = tweetText.find_all('a', {'data-expanded-url': True}) for link in links: link.string = ' ' + link['data-expanded-url'] embeddedLinks = tweetText.find_all('a', {'data-pre-embedded': 'true'}) for link in embeddedLinks: link.string = ' ' + link['href'] text = string.unescapeXHTML(tweetText.text) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) text = re.sub('[\r\n]+', graySplitter, text) formatString = str( colour(A.normal[A.fg.gray['[{time}]'], A.bold[ ' {name} ({user})', A.normal[A.fg.gray[' {reply}']] if reply else '', ':'], ' {text}'])) return formatString.format(time=tweetTimeText, name=displayName, user=user, reply=reply, text=text), url
def _guess(self, message: IRCMessage) -> Union[IRCResponse, List[IRCResponse]]: channel = message.replyTo.lower() if channel not in self.gameStates: return IRCResponse(ResponseType.Say, '[Hangman] no game running, use {}hangman start to begin!' .format(self.bot.commandChar), message.replyTo) responses = [] gs = self.gameStates[channel] guess = message.parameters.lower() # single letter if len(guess) == 1: try: correct = gs.guessLetter(guess) except (AlreadyGuessedException, InvalidCharacterException) as e: return self._exceptionFormatter(e, message.replyTo) # whole phrase else: try: correct = gs.guessPhrase(guess) except (WrongPhraseLengthException, PhraseMismatchesGuessesException, PhraseUsesKnownBadLettersException) as e: return self._exceptionFormatter(e, message.replyTo) user = message.user.nick # split the username with a zero-width space # hopefully this kills client highlighting on nick mentions # user = user[:1] + '\u200b' + user[1:] # try a tiny arrow instead, some clients actually render zero-width spaces colUser = user[:1] + '\u034e' + user[1:] if correct: colUser = colour(A.normal[A.fg.green[colUser]]) else: colUser = colour(A.normal[A.fg.red[colUser]]) responses.append(IRCResponse(ResponseType.Say, '{} - {}'.format(gs.render(), colUser), message.replyTo)) if gs.finished: if correct: responses.append(IRCResponse(ResponseType.Say, '[Hangman] Congratulations {}!'.format(user), message.replyTo)) else: responses.append(IRCResponse(ResponseType.Say, '[Hangman] {} blew up the bomb! The {} was {}' .format(user, gs.wOrP(), gs.phrase), message.replyTo)) self._stop(message, suppressMessage=True) return responses
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search(r'twitter\.com/(?P<tweeter>[^/]+)/status(es)?/(?P<tweetID>[0-9]+)', url) if not match: return tweeter = match.group('tweeter') tweetID = match.group('tweetID') url = 'https://twitter.com/{}/status/{}'.format(tweeter, tweetID) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) soup = BeautifulSoup(response.content, 'lxml') tweet = soup.find(class_='permalink-tweet') displayName = tweet['data-name'] user = tweet.find(class_='username').text reply = tweet.find(class_='ReplyingToContextBelowAuthor') if reply: reply = 'r' + reply.text.strip()[1:] tweetText = tweet.find(class_='tweet-text') tweetTimeText = tweet.find(class_='client-and-actions').text.strip() try: tweetTimeText = time.strptime(tweetTimeText, '%I:%M %p - %d %b %Y') tweetTimeText = time.strftime('%Y/%m/%d %H:%M', tweetTimeText) except ValueError: pass tweetTimeText = re.sub(r'[\r\n\s]+', u' ', tweetTimeText) links = tweetText.find_all('a', {'data-expanded-url': True}) for link in links: link.string = ' ' + link['data-expanded-url'] embeddedLinks = tweetText.find_all('a', {'data-pre-embedded': 'true'}) for link in embeddedLinks: link.string = ' ' + link['href'] text = string.unescapeXHTML(tweetText.text) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) text = re.sub('[\r\n]+', graySplitter, text) formatString = str(colour(A.normal[A.fg.gray['[{time}]'], A.bold[' {name} ({user})', A.normal[A.fg.gray[' {reply}']] if reply else '', ':'], ' {text}'])) return formatString.format(time=tweetTimeText, name=displayName, user=user, reply=reply, text=text), url
def follow(self, _: IRCMessage, url: str) -> [str, None]: # Heavily based on Didero's DideRobot code for the same # https://github.com/Didero/DideRobot/blob/06629fc3c8bddf8f729ce2d27742ff999dfdd1f6/commands/urlTitleFinder.py#L37 match = re.search(r'mixer\.com/(?P<mixerChannel>[^/]+)/?(\s|$)', url) if not match: return channel = match.group('mixerChannel') if self.mixerClientID is None: return '[Mixer Client ID not found]' chanData = {} channelOnline = False mixerHeaders = { 'Accept': 'application/json', 'Client-ID': self.mixerClientID } url = 'https://mixer.com/api/v1/channels/{}'.format(channel) response = self.bot.moduleHandler.runActionUntilValue( 'fetch-url', url, extraHeaders=mixerHeaders) streamData = response.json() if len(streamData) > 0 and 'online' in streamData: chanData = streamData channelOnline = streamData['online'] else: return output = [] if channelOnline: name = colour(A.normal[A.fg.green['{}'.format( chanData['user']['username'])]]) else: name = colour(A.normal[A.fg.red['{}'.format( chanData['user']['username'])]]) output.append(name) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) title = ' "{}"'.format( re.sub(r'[\r\n]+', graySplitter, chanData['name'].strip())) output.append(title) game = colour(A.normal[A.fg.gray[', playing '], '{}'.format(chanData['type']['name'])]) output.append(game) if chanData['audience'] == "18+": mature = colour(A.normal[A.fg.lightRed[' [Mature]']]) output.append(mature) if channelOnline: viewers = streamData['viewersCurrent'] status = colour(A.normal[A.fg.green[ ' (Live with {0:,d} viewers)'.format(viewers)]]) else: status = colour(A.normal[A.fg.red[' (Offline)']]) output.append(status) return ''.join(output), 'https://mixer.com/{}'.format(channel)
def follow(self, _: IRCMessage, url: str) -> [str, None]: # Heavily based on Didero's DideRobot code for the same # https://github.com/Didero/DideRobot/blob/06629fc3c8bddf8f729ce2d27742ff999dfdd1f6/commands/urlTitleFinder.py#L37 match = re.search(r'twitch\.tv/(?P<twitchChannel>[^/]+)/?(\s|$)', url) if not match: return channel = match.group('twitchChannel') if self.twitchClientID is None: return '[Twitch Client ID not found]' chanData = {} channelOnline = False twitchHeaders = {'Accept': 'application/vnd.twitchtv.v3+json', 'Client-ID': self.twitchClientID} url = 'https://api.twitch.tv/kraken/streams/{}'.format(channel) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url, extraHeaders=twitchHeaders) streamData = response.json() if 'stream' in streamData and streamData['stream'] is not None: chanData = streamData['stream']['channel'] channelOnline = True elif 'error' not in streamData: url = 'https://api.twitch.tv/kraken/channels/{}'.format(channel) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url, extraHeaders=twitchHeaders) chanData = response.json() if len(chanData) == 0: return output = [] if channelOnline: name = colour(A.normal[A.fg.green['{}'.format(chanData['display_name'])]]) else: name = colour(A.normal[A.fg.red['{}'.format(chanData['display_name'])]]) output.append(name) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) title = ' "{}"'.format(re.sub(r'[\r\n]+', graySplitter, chanData['status'].strip())) output.append(title) if chanData['game'] is not None: game = colour(A.normal[A.fg.gray[', playing '], '{}'.format(chanData['game'])]) output.append(game) if chanData['mature']: mature = colour(A.normal[A.fg.lightRed[' [Mature]']]) output.append(mature) if channelOnline: viewers = streamData['stream']['viewers'] status = colour(A.normal[A.fg.green[' (Live with {0:,d} viewers)'.format(viewers)]]) else: status = colour(A.normal[A.fg.red[' (Offline)']]) output.append(status) return ''.join(output), 'https://twitch.tv/{}'.format(channel)
def follow(self, _: IRCMessage, url: str) -> [str, None]: # Heavily based on Didero's DideRobot code for the same # https://github.com/Didero/DideRobot/blob/06629fc3c8bddf8f729ce2d27742ff999dfdd1f6/commands/urlTitleFinder.py#L37 match = re.search(r'mixer\.com/(?P<mixerChannel>[^/]+)/?(\s|$)', url) if not match: return channel = match.group('mixerChannel') if self.mixerClientID is None: return '[Mixer Client ID not found]' chanData = {} channelOnline = False mixerHeaders = {'Accept': 'application/json', 'Client-ID': self.mixerClientID} url = 'https://mixer.com/api/v1/channels/{}'.format(channel) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url, extraHeaders=mixerHeaders) streamData = response.json() if len(streamData) > 0 and 'online' in streamData: chanData = streamData channelOnline = streamData['online'] else: return output = [] if channelOnline: name = colour(A.normal[A.fg.green['{}'.format(chanData['user']['username'])]]) else: name = colour(A.normal[A.fg.red['{}'.format(chanData['user']['username'])]]) output.append(name) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) title = ' "{}"'.format(re.sub(r'[\r\n]+', graySplitter, chanData['name'].strip())) output.append(title) game = colour(A.normal[A.fg.gray[', playing '], '{}'.format(chanData['type']['name'])]) output.append(game) if chanData['audience'] == "18+": mature = colour(A.normal[A.fg.lightRed[' [Mature]']]) output.append(mature) if channelOnline: viewers = streamData['viewersCurrent'] status = colour(A.normal[A.fg.green[' (Live with {0:,d} viewers)'.format(viewers)]]) else: status = colour(A.normal[A.fg.red[' (Offline)']]) output.append(status) return ''.join(output), 'https://mixer.com/{}'.format(channel)
def execute(self, message: IRCMessage): if len(message.parameterList) < 3: return IRCResponse(ResponseType.Say, self.help(None), message.replyTo) try: amount = float(message.parameterList[0]) offset = 1 except ValueError: amount = 1.0 offset = 0 ccFrom = message.parameterList[offset].upper() ccTo = message.parameterList[offset + 2:] ccTo = ",".join(ccTo) ccTo = ccTo.upper() url = "https://api.exchangeratesapi.io/latest" params = { 'base': ccFrom, 'symbols': ccTo, } response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url, params=params) if response is None: return IRCResponse( ResponseType.Say, "Sorry, the currency API returned no data. Check your currencies!", message.replyTo) j = response.json() rates = j['rates'] if not rates: return IRCResponse( ResponseType.Say, "Some or all of those currencies weren't recognized!", message.replyTo) data = [] for curr, rate in rates.items(): data.append("{:.2f} {}".format(rate * amount, curr)) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return IRCResponse(ResponseType.Say, graySplitter.join(data), message.replyTo)
def execute(self, message: IRCMessage): if len(message.parameterList) < 3: return IRCResponse(ResponseType.Say, self.help(None), message.replyTo) try: amount = float(message.parameterList[0]) offset = 1 except ValueError: amount = 1.0 offset = 0 ccFrom = message.parameterList[offset].upper() ccTo = message.parameterList[offset + 2:] ccTo = ",".join(ccTo) ccTo = ccTo.upper() url = "https://api.exchangeratesapi.io/latest" params = { 'base': ccFrom, 'symbols': ccTo, } response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url, params=params) if response is None: return IRCResponse(ResponseType.Say, "Sorry, the currency API returned no data. Check your currencies!", message.replyTo) j = response.json() rates = j['rates'] if not rates: return IRCResponse(ResponseType.Say, "Some or all of those currencies weren't recognized!", message.replyTo) data = [] for curr, rate in rates.items(): data.append("{:.2f} {}".format(rate*amount, curr)) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return IRCResponse(ResponseType.Say, graySplitter.join(data), message.replyTo)
def execute(self, message: IRCMessage): if len(message.parameterList) == 0: return IRCResponse(ResponseType.Say, "You didn't give me any text to rainbow!", message.replyTo) if message.command == 'rainbow': fg = True else: fg = False startPos = 0 try: colList = [int(n) for n in message.parameterList[0].split(',')] startPos = len(message.parameterList[0]) + 1 except ValueError: if fg: colList = [4, 8, 9, 11, 12, 13] else: colList = [5, 7, 8, 3, 10, 2, 6] if not message.parameters[startPos:]: return IRCResponse(ResponseType.Say, "You didn't give me any text to rainbow after the colours!", message.replyTo) outputMessage = u'' if fg: for i, c in enumerate(message.parameters[startPos:]): outputMessage += self.colours[colList[i % len(colList)]] + c else: for i, c in enumerate(message.parameters[startPos:]): outputMessage += self.bgcolours[colList[i % len(colList)]] + c outputMessage += colour(A.normal['']) return IRCResponse(ResponseType.Say, outputMessage, message.replyTo)
class Rainbow(BotCommand): def triggers(self): return ['rainbow', 'rrainbow'] def help(self, query): return ('rainbow <text>' ' - outputs the specified text with rainbow colours;' ' rrainbow uses background colours') colours = [colour(A.fg.white['']), # 0 colour(A.fg.black['']), # 1 colour(A.fg.blue['']), # 2 colour(A.fg.green['']), # 3 colour(A.fg.lightRed['']), # 4 colour(A.fg.red['']), # 5 colour(A.fg.magenta['']), # 6 colour(A.fg.orange['']), # 7 colour(A.fg.yellow['']), # 8 colour(A.fg.lightGreen['']), # 9 colour(A.fg.cyan['']), # 10 colour(A.fg.lightCyan['']), # 11 colour(A.fg.lightBlue['']), # 12 colour(A.fg.lightMagenta['']), # 13 colour(A.fg.gray['']), # 14 colour(A.fg.lightGray['']), # 15 ] bgcolours = [colour(A.bg.white['']), # 0 colour(A.bg.black['']), # 1 colour(A.bg.blue['']), # 2 colour(A.bg.green['']), # 3 colour(A.bg.lightRed['']), # 4 colour(A.bg.red['']), # 5 colour(A.bg.magenta['']), # 6 colour(A.bg.orange['']), # 7 colour(A.bg.yellow['']), # 8 colour(A.bg.lightGreen['']), # 9 colour(A.bg.cyan['']), # 10 colour(A.bg.lightCyan['']), # 11 colour(A.bg.lightBlue['']), # 12 colour(A.bg.lightMagenta['']), # 13 colour(A.bg.gray['']), # 14 colour(A.bg.lightGray['']), # 15 ] def execute(self, message: IRCMessage): if len(message.parameterList) == 0: return IRCResponse(ResponseType.Say, "You didn't give me any text to rainbow!", message.replyTo) if message.command == 'rainbow': fg = True else: fg = False startPos = 0 try: colList = [int(n) for n in message.parameterList[0].split(',')] startPos = len(message.parameterList[0]) + 1 except ValueError: if fg: colList = [4, 8, 9, 11, 12, 13] else: colList = [5, 7, 8, 3, 10, 2, 6] if not message.parameters[startPos:]: return IRCResponse(ResponseType.Say, "You didn't give me any text to rainbow after the colours!", message.replyTo) outputMessage = u'' if fg: for i, c in enumerate(message.parameters[startPos:]): outputMessage += self.colours[colList[i % len(colList)]] + c else: for i, c in enumerate(message.parameters[startPos:]): outputMessage += self.bgcolours[colList[i % len(colList)]] + c outputMessage += colour(A.normal['']) return IRCResponse(ResponseType.Say, outputMessage, message.replyTo)
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search(r'itch\.io/', url) if not match: return response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) soup = BeautifulSoup(response.content, 'lxml') if not soup.find('body', {'data-page_name': 'view_game'}): return gameMetaInfo = soup.find(class_='game_info_panel_widget') if not gameMetaInfo: return # extract infobox information def extractInfo(namePattern: str): r = re.compile(namePattern) nameMatch = gameMetaInfo.find(text=r) if not nameMatch: return value = nameMatch.parent.parent.find_all('td')[-1] return value updated = extractInfo(r'^\s*Updated\s*$') updated = updated.abbr['title'] if updated else None published = extractInfo(r'^\s*Published\s*$') published = published.abbr['title'] if published else None status = extractInfo(r'^\s*Status\s*$') status = status.text.strip() if status else None platforms = extractInfo(r'^\s*Platforms\s*$') platforms = platforms.text.strip() if platforms else None rating = extractInfo(r'^\s*Rating\s*$') rating_stars = rating.find( class_='star_value')['content'] if rating else None rating_count = rating.find( class_='rating_count')['content'] if rating else None author = extractInfo(r'^\s*Author\s*$') author = author.text.strip() if author else None genre = extractInfo(r'^\s*Genre\s*$') genre = genre.text.strip() if genre else None # extract json information gameInfo = soup.find_all('script', {'type': 'application/ld+json'})[-1].text gameInfo = json.loads(gameInfo) title = gameInfo['name'] description = gameInfo['description'] if 'offers' in gameInfo: price = gameInfo['offers']['price'] currency = gameInfo['offers']['priceCurrency'] if gameInfo['offers']['priceValidUntil']: pass # fetch sale info (original price, percentage discount, end time) # build the output output = [] output.append(colour(A.normal[title, A.fg.gray[' by '], author])) if genre: output.append(colour(A.normal['Genre: ', genre])) outStatus = status if published: outStatus += ', published ' + published if updated: outStatus += ', last updated ' + updated output.append(colour(A.normal[outStatus])) if rating: output.append( colour(A.normal['Rating: ', rating_stars, '/5', A.fg.gray[' (', rating_count, ' ratings)']])) if price: output.append(colour(A.normal[price, ' ', currency])) # todo: sale stuff else: output.append('Free') if platforms: output.append(platforms) if description: output.append(description) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) response = graySplitter.join(output) return response, url
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search( r'twitter\.com/(?P<tweeter>[^/]+)/status(es)?/(?P<tweetID>[0-9]+)', url) if not match: return if not self.token: self.getToken() if not self.token: return # tweeter = match.group('tweeter') tweetID = match.group('tweetID') url = 'https://api.twitter.com/1.1/statuses/show.json' headers = {'Authorization': f'{self.tokenType} {self.token}'} params = {'id': tweetID, 'tweet_mode': 'extended'} response = self.mhRunActionUntilValue('fetch-url', url, params=params, extraHeaders=headers) j = response.json() # replace retweets with the original tweet if 'retweeted_status' in j: j = j['retweeted_status'] displayName = j['user']['name'] user = j['user']['screen_name'] if j['in_reply_to_screen_name'] and j[ 'in_reply_to_screen_name'] != user: reply = f"replying to @{j['in_reply_to_screen_name']}" else: reply = None tweetText = j['full_text'] # replace twitter shortened links with real urls for url in j['entities']['urls']: tweetText = tweetText.replace(url['url'], url['expanded_url']) # replace twitter shortened embedded media links with real urls if 'media' in j['entities']: mediaDict = {} for media in j['extended_entities']['media']: if media['url'] not in mediaDict: mediaDict[media['url']] = [media['media_url_https']] else: mediaDict[media['url']].append(media['media_url_https']) for media, mediaURLs in mediaDict.items(): splitter = ' · ' mediaString = splitter.join(mediaURLs) tweetText = tweetText.replace(media, mediaString) # unescape html entities to their unicode equivalents tweetText = html.unescape(tweetText) # Thu Jan 30 16:44:15 +0000 2020 tweetTimeText = j['created_at'] tweetTimeText = time.strptime(tweetTimeText, '%a %b %d %H:%M:%S %z %Y') tweetTimeText = time.strftime('%Y/%m/%d %H:%M UTC', tweetTimeText) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) text = re.sub('[\r\n]+', graySplitter, tweetText) formatString = str( colour(A.normal[A.fg.gray['[{time}]'], A.bold[ ' {name} (@{user})', A.normal[A.fg.gray[' {reply}']] if reply else '', ':'], ' {text}'])) return formatString.format(time=tweetTimeText, name=displayName, user=user, reply=reply, text=text), url
def execute(self, message: IRCMessage): subString = self._mangleEscapes(message.parameters) try: segments = list(self._parseSubcommandTree(subString)) except UnbalancedBracesException as e: red = colour(A.bold[A.fg.lightRed['']]) normal = colour(A.normal['']) error = (subString[:e.column] + red + subString[e.column] + normal + subString[e.column+1:]) error = self._unmangleEscapes(error, False) return [IRCResponse(ResponseType.Say, "Sub Error: {}".format(e.message), message.replyTo), IRCResponse(ResponseType.Say, error, message.replyTo)] prevLevel = -1 responseStack = [] extraVars = {} metadata = {} for segment in segments: (level, command, start, end) = segment # We've finished executing subcommands at the previous depth, # so replace subcommands with their output at the current depth if level < prevLevel: command = self._substituteResponses(command, responseStack, level, extraVars, start) # Replace any extraVars in the command for var, value in extraVars.items(): command = re.sub(r'\$\b{}\b'.format(re.escape(var)), '{}'.format(value), command) # Build a new message out of this segment inputMessage = IRCMessage(message.type, message.user, message.channel, self.bot.commandChar + command.lstrip(), self.bot, metadata=metadata) # Execute the constructed message if inputMessage.command.lower() in self.bot.moduleHandler.mappedTriggers: module = self.bot.moduleHandler.mappedTriggers[inputMessage.command.lower()] response = module.execute(inputMessage) """@type : IRCResponse""" else: return IRCResponse(ResponseType.Say, "'{}' is not a recognized command trigger" .format(inputMessage.command), message.replyTo) # Push the response onto the stack responseStack.append((level, response.response, start, end)) # Update the extraVars dict extraVars.update(response.ExtraVars) metadata = self._recursiveMerge(metadata, response.Metadata) prevLevel = level responseString = self._substituteResponses(subString, responseStack, -1, extraVars, -1) responseString = self._unmangleEscapes(responseString) return IRCResponse(ResponseType.Say, responseString, message.replyTo, extraVars=extraVars, metadata=metadata)
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search( r'(youtube\.com/watch.+v=|youtu\.be/)(?P<videoID>[^&#\?]{11})', url) if not match: return videoID = match.group('videoID') if self.youtubeKey is None: return '[YouTube API key not found]', None url = 'https://www.googleapis.com/youtube/v3/videos' fields = ('items(' 'id,' 'snippet(' 'title,' 'description,' 'channelTitle,' 'liveBroadcastContent' '),' 'contentDetails(duration),' 'statistics(viewCount),' 'liveStreamingDetails(scheduledStartTime)' ')') parts = 'snippet,contentDetails,statistics,liveStreamingDetails' params = { 'id': videoID, 'fields': fields, 'part': parts, 'key': self.youtubeKey, } response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url, params=params) j = response.json() if 'items' not in j: return None data = [] vid = j['items'][0] title = vid['snippet']['title'] data.append(title) channel = vid['snippet']['channelTitle'] data.append(channel) if vid['snippet']['liveBroadcastContent'] == 'none': length = parse_duration( vid['contentDetails']['duration']).total_seconds() m, s = divmod(int(length), 60) h, m = divmod(m, 60) if h > 0: length = '{0:02d}:{1:02d}:{2:02d}'.format(h, m, s) else: length = '{0:02d}:{1:02d}'.format(m, s) data.append(length) elif vid['snippet']['liveBroadcastContent'] == 'upcoming': startTime = vid['liveStreamingDetails']['scheduledStartTime'] startDateTime = dateutil.parser.parse(startTime) now = datetime.datetime.now(dateutil.tz.tzutc()) delta = startDateTime - now timespan = string.deltaTimeToString(delta, 'm') timeString = colour(A.normal['Live in ', A.fg.cyan[A.bold[timespan]]]) data.append(timeString) pass # time till stream starts, indicate it's upcoming elif vid['snippet']['liveBroadcastContent'] == 'live': status = str(colour(A.normal[A.fg.red[A.bold['{} Live']]])) status = status.format('●') data.append(status) else: pass # if we're here, wat views = int(vid['statistics']['viewCount']) data.append('{:,}'.format(views)) description = vid['snippet']['description'] if not description: description = '<no description available>' description = re.sub('(\n|\s)+', ' ', description) limit = 150 if len(description) > limit: description = '{} ...'.format(description[:limit].rsplit(' ', 1)[0]) data.append(description) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return graySplitter.join(data), 'http://youtu.be/{}'.format(videoID)
def follow(self, _: IRCMessage, url: str) -> [str, None]: ksMatch = re.search( r'kickstarter\.com/projects/(?P<ksID>[^/]+/[^/&#\?]+)', url) if not ksMatch: return ksID = ksMatch.group('ksID') url = 'https://www.kickstarter.com/projects/{}/description'.format( ksID) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) soup = BeautifulSoup(response.content, 'lxml') output = [] state = soup.find(id='main_content') pageStructureChanged = '[Kickstarter changed their page structure again :S ({})]' if not state: return pageStructureChanged.format('#main_content'), None if 'Campaign-state-canceled' in state['class']: state = 'cancelled' campaignState = colour(A.normal[A.fg.red['Cancelled']]) elif 'Campaign-state-suspended' in state['class']: state = 'suspended' campaignState = colour(A.normal[A.fg.blue['Suspended']]) elif 'Campaign-state-failed' in state['class']: state = 'failed' campaignState = colour(A.normal[A.fg.red['Failed']]) elif 'Campaign-state-successful' in state['class']: state = 'successful' campaignState = colour(A.normal[A.fg.green['Successful']]) elif 'Campaign-state-live' in state['class']: state = 'live' else: return '[Kickstarter state {!r} not recognised]'.format( state['class']), None if state in ['live', 'cancelled', 'suspended']: data = soup.find(attrs={'data-initial': True}) if not data: return pageStructureChanged.format( '{} data-initial'.format(state)), None data = json.loads(data['data-initial']) data = data['project'] shorturl = data['projectShortLink'] title = data['name'] if data['creator']: creator = data['creator']['name'] else: creator = None backerCount = int(data['backersCount']) pledged = float(data['pledged']['amount']) goal = float(data['goal']['amount']) currency = data['goal']['currency'] percentage = float(data['percentFunded']) if state == 'live': deadline = int(data['deadlineAt']) deadline = datetime.datetime.fromtimestamp( deadline, timezone.utc) now = datetime.datetime.now(timezone.utc) remaining = deadline - now remaining = remaining.total_seconds() remaining = remaining / 3600 days = math.floor(remaining / 24) hours = remaining % 24 campaignState = 'Duration: {0:.0f} days {1:.1f} hours to go'.format( days, hours) else: # Successful pattern = re.compile( r'\n\s*window\.current_project\s*=\s*"(?P<data>\{.*?\})";\n') script = soup.find("script", text=pattern) if not script: return pageStructureChanged.format( 'non-live script pattern'), None data = pattern.search(script.text).group('data') data = html.unescape(data) data = json.loads(data) shorturl = data['urls']['web']['project_short'] title = data['name'] creator = data['creator']['name'] backerCount = int(data['backers_count']) pledged = float(data['pledged']) goal = float(data['goal']) currency = data['currency'] percentage = (pledged / goal) * 100 if creator is not None: name = str(colour(A.normal['{}', A.fg.gray[' by '], '{}'])).format(title, creator) else: name = title output.append(name) if backerCount is not None: output.append('Backers: {:,d}'.format(backerCount)) if backerCount > 0: pledgePerBacker = pledged / backerCount else: pledgePerBacker = 0 if percentage >= 100: percentageString = A.fg.green['({2:,.0f}% funded)'] else: percentageString = A.fg.red['({2:,.0f}% funded)'] pledgePerBackerString = A.fg.gray['{3:,.0f}/backer'] pledgedString = colour(A.normal['Pledged: {0:,.0f}', A.fg.gray['/'], '{1:,.0f} {4} ', percentageString, ' ', pledgePerBackerString]) output.append( pledgedString.format(pledged, goal, percentage, pledgePerBacker, currency)) output.append(campaignState) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return graySplitter.join(output), shorturl
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search(r'(youtube\.com/watch.+v=|youtu\.be/)(?P<videoID>[^&#\?]{11})', url) if not match: return videoID = match.group('videoID') if self.youtubeKey is None: return '[YouTube API key not found]', None url = 'https://www.googleapis.com/youtube/v3/videos' fields = ('items(' 'id,' 'snippet(' 'title,' 'description,' 'channelTitle,' 'liveBroadcastContent' '),' 'contentDetails(duration),' 'statistics(viewCount),' 'liveStreamingDetails(scheduledStartTime)' ')') parts = 'snippet,contentDetails,statistics,liveStreamingDetails' params = { 'id': videoID, 'fields': fields, 'part': parts, 'key': self.youtubeKey, } response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url, params=params) j = response.json() if 'items' not in j: return None data = [] vid = j['items'][0] title = vid['snippet']['title'] data.append(title) channel = vid['snippet']['channelTitle'] data.append(channel) if vid['snippet']['liveBroadcastContent'] == 'none': length = parse_duration(vid['contentDetails']['duration']).total_seconds() m, s = divmod(int(length), 60) h, m = divmod(m, 60) if h > 0: length = '{0:02d}:{1:02d}:{2:02d}'.format(h, m, s) else: length = '{0:02d}:{1:02d}'.format(m, s) data.append(length) elif vid['snippet']['liveBroadcastContent'] == 'upcoming': startTime = vid['liveStreamingDetails']['scheduledStartTime'] startDateTime = dateutil.parser.parse(startTime) now = datetime.datetime.now(dateutil.tz.tzutc()) delta = startDateTime - now timespan = string.deltaTimeToString(delta, 'm') timeString = colour(A.normal['Live in ', A.fg.cyan[A.bold[timespan]]]) data.append(timeString) pass # time till stream starts, indicate it's upcoming elif vid['snippet']['liveBroadcastContent'] == 'live': status = str(colour(A.normal[A.fg.red[A.bold['{} Live']]])) status = status.format('●') data.append(status) else: pass # if we're here, wat views = int(vid['statistics']['viewCount']) data.append('{:,}'.format(views)) description = vid['snippet']['description'] if not description: description = '<no description available>' description = re.sub('(\n|\s)+', ' ', description) limit = 150 if len(description) > limit: description = '{} ...'.format(description[:limit].rsplit(' ', 1)[0]) data.append(description) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return graySplitter.join(data), 'http://youtu.be/{}'.format(videoID)
def execute(self, message: IRCMessage): if not message.parameterList: return IRCResponse(ResponseType.Say, "You didn't give a word! Usage: {}".format(self.help(None)), message.replyTo) mh = self.bot.moduleHandler searchURL = 'https://www.etymonline.com/search' query = message.parameterList[0] if len(message.parameterList) > 1: try: index = int(message.parameterList[1]) - 1 if index < 0: index = 0 except ValueError: return IRCResponse(ResponseType.Say, 'Index {!r} is not an integer! Usage: {}' .format(message.parameterList[1], self.help(None)), message.replyTo) else: index = 0 results = mh.runActionUntilValue('fetch-url', searchURL, params={'q': query}) soup = BeautifulSoup(results.content, 'lxml') words = soup.find_all(class_='word--C9UPa') if not words: return IRCResponse(ResponseType.Say, 'No results found for {!r}'.format(query), message.replyTo) totalResults = soup.find(class_='searchList__pageCount--2jQdB').text totalResults = int(re.sub(r'[^\d]', '', totalResults)) if index >= totalResults: index = totalResults - 1 displayIndex = '{}/{}'.format(index + 1, totalResults) if index >= len(words): results = mh.runActionUntilValue('fetch-url', searchURL, params={'q': query, 'page': index // len(words) + 1}) index %= len(words) soup = BeautifulSoup(results.content, 'lxml') words = soup.find_all(class_='word--C9UPa') if index >= len(words): index = len(words) - 1 word = words[index].find(class_='word__name--TTbAA') word = word.text defn = words[index].find(class_='word__defination--2q7ZH') defn = ' '.join(defn.text.splitlines()) limit = 500 if len(defn) > limit: defn = '{} ...'.format(defn[:limit].rsplit(' ', 1)[0]) wordURL = 'https://www.etymonline.com{}'.format(words[index]['href']) url = mh.runActionUntilValue('shorten-url', wordURL) response = colour(A.normal[A.bold['{}: '.format(word)], defn, A.fg.gray[' | {} | '.format(displayIndex)], url]) return IRCResponse(ResponseType.Say, response, message.replyTo)
def execute(self, message: IRCMessage): if len(message.parameterList) == 0: return IRCResponse(ResponseType.Say, "You didn't give a word! Usage: {0}".format(self.help), message.replyTo) search = quote(message.parameters) url = 'http://api.urbandictionary.com/v0/define?term={0}'.format(search) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) j = response.json() if len(j['list']) == 0: return IRCResponse(ResponseType.Say, "No entry found for '{0}'".format(message.parameters), message.replyTo) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) defn = j['list'][0] word = defn['word'] definition = defn['definition'] definition = graySplitter.join([s.strip() for s in definition.strip().splitlines() if s]) example = defn['example'] example = graySplitter.join([s.strip() for s in example.strip().splitlines() if s]) author = defn['author'] up = defn['thumbs_up'] down = defn['thumbs_down'] more = 'http://{}.urbanup.com/'.format(word.replace(' ', '-')) if word.lower() != message.parameters.lower(): word = "{0} (Contains '{1}')".format(word, message.parameters) defFormatString = str(colour(A.normal[A.bold["{0}:"], " {1}"])) exampleFormatString = str(colour(A.normal[A.bold["Example(s):"], " {0}"])) byFormatString = str(colour(A.normal["{0}", graySplitter, A.fg.lightGreen["+{1}"], A.fg.gray["/"], A.fg.lightRed["-{2}"], graySplitter, "More defs: {3}"])) responses = [IRCResponse(ResponseType.Say, defFormatString.format(word, definition), message.replyTo), IRCResponse(ResponseType.Say, exampleFormatString.format(example), message.replyTo), IRCResponse(ResponseType.Say, byFormatString.format(author, up, down, more), message.replyTo)] return responses
def execute(self, message: IRCMessage): if not message.parameterList: return IRCResponse( ResponseType.Say, "You didn't give a word! Usage: {}".format(self.help(None)), message.replyTo) mh = self.bot.moduleHandler searchURL = 'https://www.etymonline.com/search' query = message.parameterList[0] if len(message.parameterList) > 1: try: index = int(message.parameterList[1]) - 1 if index < 0: index = 0 except ValueError: return IRCResponse( ResponseType.Say, 'Index {!r} is not an integer! Usage: {}'.format( message.parameterList[1], self.help(None)), message.replyTo) else: index = 0 results = mh.runActionUntilValue('fetch-url', searchURL, params={'q': query}) soup = BeautifulSoup(results.content, 'lxml') words = soup.find_all(class_='word--C9UPa') if not words: return IRCResponse(ResponseType.Say, 'No results found for {!r}'.format(query), message.replyTo) totalResults = soup.find(class_='searchList__pageCount--2jQdB').text totalResults = int(re.sub(r'[^\d]', '', totalResults)) if index >= totalResults: index = totalResults - 1 displayIndex = '{}/{}'.format(index + 1, totalResults) if index >= len(words): results = mh.runActionUntilValue('fetch-url', searchURL, params={ 'q': query, 'page': index // len(words) + 1 }) index %= len(words) soup = BeautifulSoup(results.content, 'lxml') words = soup.find_all(class_='word--C9UPa') if index >= len(words): index = len(words) - 1 word = words[index].find(class_='word__name--TTbAA') word = word.text defn = words[index].find(class_='word__defination--2q7ZH') defn = ' '.join(defn.text.splitlines()) limit = 500 if len(defn) > limit: defn = '{} ...'.format(defn[:limit].rsplit(' ', 1)[0]) wordURL = 'https://www.etymonline.com{}'.format(words[index]['href']) url = mh.runActionUntilValue('shorten-url', wordURL) response = colour(A.normal[A.bold['{}: '.format(word)], defn, A.fg.gray[' | {} | '.format(displayIndex)], url]) return IRCResponse(ResponseType.Say, response, message.replyTo)
def execute(self, message: IRCMessage) -> Union[IRCResponse, List[IRCResponse]]: if not self.apiKey: return IRCResponse(ResponseType.Say, "No API key found.", message.replyTo) if len(message.parameterList) == 0: return IRCResponse(ResponseType.Say, "You didn't give me a search query.", message.replyTo) params = { 'input': message.parameters, 'output': 'json', 'appid': self.apiKey, 'podindex': '5,4,3,2,1' } result = self.bot.moduleHandler.runActionUntilValue("fetch-url", self.waBaseURL, params) if not result or 'queryresult' not in result.json(): output = 'No Wolfram Alpha data could be found at this moment. Try again later.' else: j = result.json()['queryresult'] if 'error' in j and j['error'] != False: if 'msg' in j['error']: output = f"Wolfram Alpha returned an error: {j['error']['msg']}" else: output = 'Wolfram Alpha returned an unknown error' elif 'success' not in j or j['success'] == False: output = 'No results found.' didyoumeans = [] if 'didyoumeans' in j: tmpList = [] if isinstance(j['didyoumeans'], dict): tmpList.append(j['didyoumeans']) else: tmpList = j['didyoumeans'] for didyoumean in tmpList: if didyoumean['level'] != 'low': didyoumeans.append(didyoumean['val']) if len(didyoumeans) > 0: output = f"{output} Did you mean {' or '.join(didyoumeans)}?" else: result = None for pod in j['pods'][1:]: if 'input' in [pod['id'].lower(), pod['title'].lower()]: continue for subpod in pod['subpods']: if 'plaintext' not in subpod or subpod['plaintext'].startswith('\n'): continue plaintext = subpod['plaintext'].replace('\n', ' | ').strip() if not plaintext: continue # Probably an image result = plaintext break if result: break output = result if result else 'No relevant information was found' url = f'http://www.wolframalpha.com/input/?i={quote_plus(message.parameters)}' shortenedUrl = self.bot.moduleHandler.runActionUntilValue('shorten-url', url) if not shortenedUrl: shortenedUrl = url output = f'{output} | {shortenedUrl}' graySplitter = colour(A.normal['', A.fg.gray['|'], '']) output = re.sub('(\| )+', '| ', re.sub(' +', ' ', output)).replace('|', graySplitter) return IRCResponse(ResponseType.Say, output, message.replyTo)
def execute(self, message: IRCMessage) -> Union[IRCResponse, List[IRCResponse]]: if not self.apiKey: return IRCResponse(ResponseType.Say, "No API key found.", message.replyTo) if len(message.parameterList) == 0: return IRCResponse(ResponseType.Say, "You didn't give me a search query.", message.replyTo) params = { 'input': message.parameters, 'output': 'json', 'appid': self.apiKey, 'podindex': '5,4,3,2,1' } result = self.bot.moduleHandler.runActionUntilValue( "fetch-url", self.waBaseURL, params) if not result or 'queryresult' not in result.json(): output = 'No Wolfram Alpha data could be found at this moment. Try again later.' else: j = result.json()['queryresult'] if 'error' in j and j['error'] != False: if 'msg' in j['error']: output = f"Wolfram Alpha returned an error: {j['error']['msg']}" else: output = 'Wolfram Alpha returned an unknown error' elif 'success' not in j or j['success'] == False: output = 'No results found.' didyoumeans = [] if 'didyoumeans' in j: tmpList = [] if isinstance(j['didyoumeans'], dict): tmpList.append(j['didyoumeans']) else: tmpList = j['didyoumeans'] for didyoumean in tmpList: if didyoumean['level'] != 'low': didyoumeans.append(didyoumean['val']) if len(didyoumeans) > 0: output = f"{output} Did you mean {' or '.join(didyoumeans)}?" else: result = None for pod in j['pods'][1:]: if 'input' in [pod['id'].lower(), pod['title'].lower()]: continue for subpod in pod['subpods']: if 'plaintext' not in subpod or subpod[ 'plaintext'].startswith('\n'): continue plaintext = subpod['plaintext'].replace('\n', ' | ').strip() if not plaintext: continue # Probably an image result = plaintext break if result: break output = result if result else 'No relevant information was found' url = f'http://www.wolframalpha.com/input/?i={quote_plus(message.parameters)}' shortenedUrl = self.bot.moduleHandler.runActionUntilValue( 'shorten-url', url) if not shortenedUrl: shortenedUrl = url output = f'{output} | {shortenedUrl}' graySplitter = colour(A.normal['', A.fg.gray['|'], '']) output = re.sub('(\| )+', '| ', re.sub(' +', ' ', output)).replace('|', graySplitter) return IRCResponse(ResponseType.Say, output, message.replyTo)
def execute(self, message: IRCMessage): subString = self._mangleEscapes(message.parameters) try: segments = list(self._parseSubcommandTree(subString)) except UnbalancedBracesException as e: red = colour(A.bold[A.fg.lightRed['']]) normal = colour(A.normal['']) error = (subString[:e.column] + red + subString[e.column] + normal + subString[e.column + 1:]) error = self._unmangleEscapes(error, False) return [ IRCResponse(ResponseType.Say, "Sub Error: {}".format(e.message), message.replyTo), IRCResponse(ResponseType.Say, error, message.replyTo) ] prevLevel = -1 responseStack = [] extraVars = {} metadata = {} for segment in segments: (level, command, start, end) = segment # We've finished executing subcommands at the previous depth, # so replace subcommands with their output at the current depth if level < prevLevel: command = self._substituteResponses(command, responseStack, level, extraVars, start) # Replace any extraVars in the command for var, value in extraVars.items(): command = re.sub(r'\$\b{}\b'.format(re.escape(var)), '{}'.format(value), command) # Build a new message out of this segment inputMessage = IRCMessage(message.type, message.user, message.channel, self.bot.commandChar + command.lstrip(), self.bot, metadata=metadata) # Execute the constructed message if inputMessage.command.lower( ) in self.bot.moduleHandler.mappedTriggers: module = self.bot.moduleHandler.mappedTriggers[ inputMessage.command.lower()] response = module.execute(inputMessage) """@type : IRCResponse""" else: return IRCResponse( ResponseType.Say, "'{}' is not a recognized command trigger".format( inputMessage.command), message.replyTo) # Push the response onto the stack responseStack.append((level, response.response, start, end)) # Update the extraVars dict extraVars.update(response.ExtraVars) metadata = self._recursiveMerge(metadata, response.Metadata) prevLevel = level responseString = self._substituteResponses(subString, responseStack, -1, extraVars, -1) responseString = self._unmangleEscapes(responseString) return IRCResponse(ResponseType.Say, responseString, message.replyTo, extraVars=extraVars, metadata=metadata)
def follow(self, _: IRCMessage, url: str) -> [str, None]: ksMatch = re.search(r'kickstarter\.com/projects/(?P<ksID>[^/]+/[^/&#\?]+)', url) if not ksMatch: return ksID = ksMatch.group('ksID') url = 'https://www.kickstarter.com/projects/{}/description'.format(ksID) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) soup = BeautifulSoup(response.content, 'lxml') output = [] state = soup.find(id='main_content') pageStructureChanged = '[Kickstarter changed their page structure again :S ({})]' if not state: return pageStructureChanged.format('#main_content'), None if 'Campaign-state-canceled' in state['class']: state = 'cancelled' campaignState = colour(A.normal[A.fg.red['Cancelled']]) elif 'Campaign-state-suspended' in state['class']: state = 'suspended' campaignState = colour(A.normal[A.fg.blue['Suspended']]) elif 'Campaign-state-failed' in state['class']: state = 'failed' campaignState = colour(A.normal[A.fg.red['Failed']]) elif 'Campaign-state-successful' in state['class']: state = 'successful' campaignState = colour(A.normal[A.fg.green['Successful']]) elif 'Campaign-state-live' in state['class']: state = 'live' else: return '[Kickstarter state {!r} not recognised]'.format(state['class']), None if state in ['live', 'cancelled', 'suspended']: data = soup.find(attrs={'data-initial': True}) if not data: return pageStructureChanged.format('{} data-initial'.format(state)), None data = json.loads(data['data-initial']) data = data['project'] shorturl = data['projectShortLink'] title = data['name'] if data['creator']: creator = data['creator']['name'] else: creator = None backerCount = int(data['backersCount']) pledged = float(data['pledged']['amount']) goal = float(data['goal']['amount']) currency = data['goal']['currency'] percentage = float(data['percentFunded']) if state == 'live': deadline = int(data['deadlineAt']) deadline = datetime.datetime.fromtimestamp(deadline, timezone.utc) now = datetime.datetime.now(timezone.utc) remaining = deadline - now remaining = remaining.total_seconds() remaining = remaining / 3600 days = math.floor(remaining/24) hours = remaining % 24 campaignState = 'Duration: {0:.0f} days {1:.1f} hours to go'.format(days, hours) else: # Successful pattern = re.compile(r'\n\s*window\.current_project\s*=\s*"(?P<data>\{.*?\})";\n') script = soup.find("script", text=pattern) if not script: return pageStructureChanged.format('non-live script pattern'), None data = pattern.search(script.text).group('data') data = html.unescape(data) data = json.loads(data) shorturl = data['urls']['web']['project_short'] title = data['name'] creator = data['creator']['name'] backerCount = int(data['backers_count']) pledged = float(data['pledged']) goal = float(data['goal']) currency = data['currency'] percentage = (pledged / goal) * 100 if creator is not None: name = str(colour(A.normal['{}', A.fg.gray[' by '], '{}'])).format(title, creator) else: name = title output.append(name) if backerCount is not None: output.append('Backers: {:,d}'.format(backerCount)) if backerCount > 0: pledgePerBacker = pledged / backerCount else: pledgePerBacker = 0 if percentage >= 100: percentageString = A.fg.green['({2:,.0f}% funded)'] else: percentageString = A.fg.red['({2:,.0f}% funded)'] pledgePerBackerString = A.fg.gray['{3:,.0f}/backer'] pledgedString = colour(A.normal['Pledged: {0:,.0f}', A.fg.gray['/'], '{1:,.0f} {4} ', percentageString, ' ', pledgePerBackerString]) output.append(pledgedString.format(pledged, goal, percentage, pledgePerBacker, currency)) output.append(campaignState) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return graySplitter.join(output), shorturl
def execute(self, message: IRCMessage): if len(message.parameterList) == 0: return IRCResponse( ResponseType.Say, "You didn't give a word! Usage: {0}".format(self.help), message.replyTo) search = quote(message.parameters) url = 'http://api.urbandictionary.com/v0/define?term={0}'.format( search) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) j = response.json() if len(j['list']) == 0: return IRCResponse( ResponseType.Say, "No entry found for '{0}'".format(message.parameters), message.replyTo) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) defn = j['list'][0] word = defn['word'] definition = defn['definition'] definition = graySplitter.join( [s.strip() for s in definition.strip().splitlines() if s]) example = defn['example'] example = graySplitter.join( [s.strip() for s in example.strip().splitlines() if s]) author = defn['author'] up = defn['thumbs_up'] down = defn['thumbs_down'] more = 'http://{}.urbanup.com/'.format(word.replace(' ', '-')) if word.lower() != message.parameters.lower(): word = "{0} (Contains '{1}')".format(word, message.parameters) defFormatString = str(colour(A.normal[A.bold["{0}:"], " {1}"])) exampleFormatString = str( colour(A.normal[A.bold["Example(s):"], " {0}"])) byFormatString = str( colour(A.normal["{0}", graySplitter, A.fg.lightGreen["+{1}"], A.fg.gray["/"], A.fg.lightRed["-{2}"], graySplitter, "More defs: {3}"])) responses = [ IRCResponse(ResponseType.Say, defFormatString.format(word, definition), message.replyTo), IRCResponse(ResponseType.Say, exampleFormatString.format(example), message.replyTo), IRCResponse(ResponseType.Say, byFormatString.format(author, up, down, more), message.replyTo) ] return responses
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search(r'itch\.io/', url) if not match: return response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) soup = BeautifulSoup(response.content, 'lxml') if not soup.find('body', {'data-page_name': 'view_game'}): return gameMetaInfo = soup.find(class_='game_info_panel_widget') if not gameMetaInfo: return # extract infobox information def extractInfo(namePattern: str): r = re.compile(namePattern) nameMatch = gameMetaInfo.find(text=r) if not nameMatch: return value = nameMatch.parent.parent.find_all('td')[-1] return value updated = extractInfo(r'^\s*Updated\s*$') updated = updated.abbr['title'] if updated else None published = extractInfo(r'^\s*Published\s*$') published = published.abbr['title'] if published else None status = extractInfo(r'^\s*Status\s*$') status = status.text.strip() if status else None platforms = extractInfo(r'^\s*Platforms\s*$') platforms = platforms.text.strip() if platforms else None rating = extractInfo(r'^\s*Rating\s*$') rating_stars = rating.find(class_='star_value')['content'] if rating else None rating_count = rating.find(class_='rating_count')['content'] if rating else None author = extractInfo(r'^\s*Author\s*$') author = author.text.strip() if author else None genre = extractInfo(r'^\s*Genre\s*$') genre = genre.text.strip() if genre else None # extract json information gameInfo = soup.find_all('script', {'type': 'application/ld+json'})[-1].text gameInfo = json.loads(gameInfo) title = gameInfo['name'] description = gameInfo['description'] if 'offers' in gameInfo: price = gameInfo['offers']['price'] currency = gameInfo['offers']['priceCurrency'] if gameInfo['offers']['priceValidUntil']: pass # fetch sale info (original price, percentage discount, end time) # build the output output = [] output.append(colour(A.normal[title, A.fg.gray[' by '], author])) if genre: output.append(colour(A.normal['Genre: ', genre])) outStatus = status if published: outStatus += ', published ' + published if updated: outStatus += ', last updated ' + updated output.append(colour(A.normal[outStatus])) if rating: output.append(colour(A.normal['Rating: ', rating_stars, '/5', A.fg.gray[' (', rating_count, ' ratings)']])) if price: output.append(colour(A.normal[price, ' ', currency])) # todo: sale stuff else: output.append('Free') if platforms: output.append(platforms) if description: output.append(description) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) response = graySplitter.join(output) return response, url
def follow(self, _: IRCMessage, origUrl: str) -> [str, None]: match = re.search(r'(i\.)?imgur\.com/(?P<imgurID>[^\.]+)', origUrl) if not match: return origImgurID = match.group('imgurID') if self.imgurClientID is None: return '[imgur Client ID not found]', None albumLink = False if origImgurID.startswith('a/'): imgurID = origImgurID.replace('a/', '') url = 'https://api.imgur.com/3/album/{}'.format(imgurID) albumLink = True elif origImgurID.startswith('gallery/'): imgurID = origImgurID.replace('gallery/', '') url = 'https://api.imgur.com/3/gallery/{}'.format(imgurID) else: imgurID = origImgurID url = 'https://api.imgur.com/3/image/{}'.format(origImgurID) headers = {'Authorization': 'Client-ID {}'.format(self.imgurClientID)} mh = self.bot.moduleHandler response = mh.runActionUntilValue('fetch-url', url, extraHeaders=headers) if not response: return j = response.json() imageData = j['data'] if not imageData['title']: if imageData['section']: # subreddit galleries have a different endpoint with better data. # we don't know if it's a subreddit gallery image until we fetch it, # so we're stuck with this double-lookup. oh well. url = ('https://api.imgur.com/3/gallery/r/{}/{}' .format(imageData['section'], imgurID)) response = mh.runActionUntilValue('fetch-url', url, extraHeaders=headers) if not response: return j = response.json() imageData = j['data'] else: # fallback to the html page title if no other title was found. # this should always result in <No Title> now as all the endpoints are covered, # but they may add new ones so we're leaving this here just in case. titleUrl = 'https://imgur.com/{}'.format(origImgurID) response = mh.runActionUntilValue('fetch-url', titleUrl) title = mh.runActionUntilValue('get-html-title', response.content) imageData['title'] = title.replace(' - Imgur', '') if imageData['title'] in ['imgur: the simple image sharer', 'Imgur: The magic of the Internet']: imageData['title'] = None data = [] if imageData['title']: data.append(imageData['title']) else: data.append('<No Title>') if imageData['nsfw']: data.append('\x034\x02NSFW!\x0F') if albumLink: data.append('Album: {} Images'.format(imageData['images_count'])) else: if 'is_album' in imageData and imageData['is_album']: data.append('Album: {:,d} Images'.format(len(imageData['images']))) else: if imageData['animated']: data.append('\x032\x02Animated!\x0F') data.append('{:,d}x{:,d}'.format(imageData['width'], imageData['height'])) data.append('Size: {:,d}kb'.format(int(imageData['size']/1024))) data.append('Views: {:,d}'.format(imageData['views'])) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return graySplitter.join(data), '[no imgur url]'
def followURL(self, _: IRCMessage, url: str, showContents: bool = False) -> [str, None]: # check this is actually a Mastodon instance we're looking at hostname = urlparse(url).hostname endpoint = 'https://{domain}/api/v1/instance'.format(domain=hostname) endpointResponse = self.bot.moduleHandler.runActionUntilValue( 'fetch-url', endpoint) if not endpointResponse: return try: endpointJSON = endpointResponse.json() except json.decoder.JSONDecodeError: return if 'uri' not in endpointJSON: return response = self.bot.moduleHandler.runActionUntilValue( 'fetch-url', '{}/embed'.format(url)) if not response: return soup = BeautifulSoup(response.content, 'lxml') toot = soup.find(class_='entry') if not toot: # presumably not a toot, ignore return date = toot.find(class_='dt-published')['value'] date = dateutil.parser.parse(date) date = date.astimezone(dateutil.tz.UTC) date = date.strftime('%Y/%m/%d %H:%M') name = toot.find(class_='p-name') name = self.translateEmojo(name).text.strip() user = toot.find(class_='display-name__account').text.strip() user = '******'.format(name, user) content = toot.find(class_='status__content') summary = content.find(class_='p-summary') if summary: summary = self.translateEmojo(summary).text.strip() text = content.find(class_='e-content') text = self.translateEmojo(text) # if there's no p tag, add one wrapping everything if not text.find_all('p'): text_children = list(text.children) wrapper_p = soup.new_tag('p') text.clear() text.append(wrapper_p) for child in text_children: wrapper_p.append(child) # replace <br /> tags with a newline for br in text.find_all("br"): br.replace_with('\n') # then replace consecutive <p> tags with a double newline lines = [line.text for line in text.find_all('p')] text = '\n\n'.join(lines) # strip empty lines, strip leading/ending whitespace, # and replace newlines with gray pipes graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) lines = [l.strip() for l in text.splitlines() if l.strip()] text = graySplitter.join(lines) media = toot.find('div', {'data-component': 'MediaGallery'}) if media: media = json.loads(media['data-props']) media = media['media'] numMedia = len(media) if numMedia == 1: medType = media[0]['type'] #size = media[0]['meta']['original']['size'] description = media[0]['description'] description = ': {}'.format(description) if description else '' media = '(attached {medType}{description})'.format( medType=medType, description=description) else: media = '({} media attached)'.format(numMedia) formatString = str( colour(A.normal[A.fg.gray['[{date}]'], A.bold[' {user}:'], A.fg.red[' [{summary}]'] if summary else '', ' {text}' if not summary or showContents else '', A.fg.gray[' {media}'] if media else ''])) return formatString.format(date=date, user=user, summary=summary, text=text, media=media), ''
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search( r'store\.steampowered\.com/(?P<steamType>(app|sub))/(?P<steamID>[0-9]+)', url) if not match: return steamType = match.group('steamType') steamId = match.group('steamID') steamType = {'app': 'app', 'sub': 'package'}[steamType] params = '{0}details/?{0}ids={1}&cc=US&l=english&v=1'.format( steamType, steamId) url = 'http://store.steampowered.com/api/{}'.format(params) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) j = response.json() if not j[steamId]['success']: return # failure appData = j[steamId]['data'] data = [] # name if 'developers' in appData: developers = ', '.join(appData['developers']) name = colour(A.normal[appData['name'], A.fg.gray[' by '], developers]) else: name = appData['name'] data.append(name) # package contents (might need to trim this...) if 'apps' in appData: appNames = [app['name'] for app in appData['apps']] apps = 'Package containing: {}'.format(', '.join(appNames)) data.append(apps) # genres if 'genres' in appData: genres = ', '.join( [genre['description'] for genre in appData['genres']]) data.append('Genres: ' + genres) # release date releaseDate = appData['release_date'] if not releaseDate['coming_soon']: if releaseDate['date']: data.append('Released: ' + releaseDate['date']) else: upcomingDate = A.fg.cyan[A.bold[str(releaseDate['date'])]] data.append(colour(A.normal['To Be Released: ', upcomingDate])) # metacritic # http://www.metacritic.com/faq#item32 # (Why is the breakdown of green, yellow, and red scores different for games?) if 'metacritic' in appData: metaScore = appData['metacritic']['score'] if metaScore < 50: metacritic = colour(A.fg.red[str(metaScore)]) elif metaScore < 75: metacritic = colour(A.fg.orange[str(metaScore)]) else: metacritic = colour(A.fg.green[str(metaScore)]) data.append('Metacritic: {}'.format(metacritic)) # dlc count if 'dlc' in appData: dlc = 'DLC: {}'.format(len(appData['dlc'])) data.append(dlc) # prices if 'is_free' in appData: if appData['is_free']: free = colour(A.fg.cyan['Free']) data.append(free) priceField = {'app': 'price_overview', 'package': 'price'}[steamType] if priceField in appData: prices = { 'USD': appData[priceField], 'GBP': self.getSteamPrice(steamType, steamId, 'GB'), 'EUR': self.getSteamPrice(steamType, steamId, 'FR'), 'AUD': self.getSteamPrice(steamType, steamId, 'AU') } currencies = { 'USD': '$', 'GBP': '\u00A3', 'EUR': '\u20AC', 'AUD': 'AU$' } # filter out AUD if same as USD (most are) if not prices['AUD'] or prices['AUD']['final'] == prices['USD'][ 'final']: del prices['AUD'] # filter out any missing prices prices = {key: val for key, val in prices.items() if val} priceList = [ '{}{:,.2f}'.format(currencies[val['currency']], val['final'] / 100.0) for val in prices.values() ] priceString = '/'.join(priceList) if prices['USD']['discount_percent'] > 0: discount = ' ({}% sale!)'.format( prices['USD']['discount_percent']) priceString += colour(A.fg.green[A.bold[discount]]) data.append(priceString) # platforms if 'platforms' in appData: platforms = appData['platforms'] platformArray = [] if platforms['windows']: platformArray.append('Win') else: platformArray.append('---') if platforms['mac']: platformArray.append('Mac') else: platformArray.append('---') if platforms['linux']: platformArray.append('Lin') else: platformArray.append('---') data.append('/'.join(platformArray)) # description if 'short_description' in appData and appData[ 'short_description'] is not None: limit = 100 description = appData['short_description'] if len(description) > limit: description = '{} ...'.format(description[:limit].rsplit( ' ', 1)[0]) data.append(description) url = ('http://store.steampowered.com/{}/{}'.format({ 'app': 'app', 'package': 'sub' }[steamType], steamId)) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return graySplitter.join(data), url
def follow(self, _: IRCMessage, url: str) -> [str, None]: match = re.search(r'store\.steampowered\.com/(?P<steamType>(app|sub))/(?P<steamID>[0-9]+)', url) if not match: return steamType = match.group('steamType') steamId = match.group('steamID') steamType = {'app': 'app', 'sub': 'package'}[steamType] params = '{0}details/?{0}ids={1}&cc=US&l=english&v=1'.format(steamType, steamId) url = 'http://store.steampowered.com/api/{}'.format(params) response = self.bot.moduleHandler.runActionUntilValue('fetch-url', url) j = response.json() if not j[steamId]['success']: return # failure appData = j[steamId]['data'] data = [] # name if 'developers' in appData: developers = ', '.join(appData['developers']) name = colour(A.normal[appData['name'], A.fg.gray[' by '], developers]) else: name = appData['name'] data.append(name) # package contents (might need to trim this...) if 'apps' in appData: appNames = [app['name'] for app in appData['apps']] apps = 'Package containing: {}'.format(', '.join(appNames)) data.append(apps) # genres if 'genres' in appData: genres = ', '.join([genre['description'] for genre in appData['genres']]) data.append('Genres: ' + genres) # release date releaseDate = appData['release_date'] if not releaseDate['coming_soon']: if releaseDate['date']: data.append('Released: ' + releaseDate['date']) else: upcomingDate = A.fg.cyan[A.bold[str(releaseDate['date'])]] data.append(colour(A.normal['To Be Released: ', upcomingDate])) # metacritic # http://www.metacritic.com/faq#item32 # (Why is the breakdown of green, yellow, and red scores different for games?) if 'metacritic' in appData: metaScore = appData['metacritic']['score'] if metaScore < 50: metacritic = colour(A.fg.red[str(metaScore)]) elif metaScore < 75: metacritic = colour(A.fg.orange[str(metaScore)]) else: metacritic = colour(A.fg.green[str(metaScore)]) data.append('Metacritic: {}'.format(metacritic)) # dlc count if 'dlc' in appData: dlc = 'DLC: {}'.format(len(appData['dlc'])) data.append(dlc) # prices if 'is_free' in appData: if appData['is_free']: free = colour(A.fg.cyan['Free']) data.append(free) priceField = {'app': 'price_overview', 'package': 'price'}[steamType] if priceField in appData: prices = {'USD': appData[priceField], 'GBP': self.getSteamPrice(steamType, steamId, 'GB'), 'EUR': self.getSteamPrice(steamType, steamId, 'FR'), 'AUD': self.getSteamPrice(steamType, steamId, 'AU')} currencies = {'USD': '$', 'GBP': '\u00A3', 'EUR': '\u20AC', 'AUD': 'AU$'} # filter out AUD if same as USD (most are) if not prices['AUD'] or prices['AUD']['final'] == prices['USD']['final']: del prices['AUD'] # filter out any missing prices prices = {key: val for key, val in prices.items() if val} priceList = ['{}{:,.2f}'.format(currencies[val['currency']], val['final'] / 100.0) for val in prices.values()] priceString = '/'.join(priceList) if prices['USD']['discount_percent'] > 0: discount = ' ({}% sale!)'.format(prices['USD']['discount_percent']) priceString += colour(A.fg.green[A.bold[discount]]) data.append(priceString) # platforms if 'platforms' in appData: platforms = appData['platforms'] platformArray = [] if platforms['windows']: platformArray.append('Win') else: platformArray.append('---') if platforms['mac']: platformArray.append('Mac') else: platformArray.append('---') if platforms['linux']: platformArray.append('Lin') else: platformArray.append('---') data.append('/'.join(platformArray)) # description if 'short_description' in appData and appData['short_description'] is not None: limit = 100 description = appData['short_description'] if len(description) > limit: description = '{} ...'.format(description[:limit].rsplit(' ', 1)[0]) data.append(description) url = ('http://store.steampowered.com/{}/{}' .format({'app': 'app', 'package': 'sub'}[steamType], steamId)) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) return graySplitter.join(data), url
from base64 import b64decode, b64encode from collections import OrderedDict from html.entities import name2codepoint from datetime import timedelta from dateutil.parser import parse from enum import Enum import re from twisted.words.protocols.irc import assembleFormattedText as colour, attributes as A graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) def isNumber(s: str) -> bool: """returns True if string s can be cast to a number, False otherwise""" try: float(s) return True except ValueError: return False # From this SO answer: http://stackoverflow.com/a/6043797/331047 def splitUTF8(s: str, n: int) -> str: """Split UTF-8 s into chunks of maximum byte length n""" while len(s) > n: k = n while (ord(s[k]) & 0xc0) == 0x80: k -= 1 yield s[:k] s = s[k:]
def follow(self, _: IRCMessage, url: str) -> [str, None]: # Heavily based on Didero's DideRobot code for the same # https://github.com/Didero/DideRobot/blob/06629fc3c8bddf8f729ce2d27742ff999dfdd1f6/commands/urlTitleFinder.py#L37 match = re.search(r'twitch\.tv/(?P<twitchChannel>[^/]+)/?(\s|$)', url) if not match: return channel = match.group('twitchChannel') if self.twitchClientID is None: return '[Twitch Client ID not found]' chanData = {} channelOnline = False twitchHeaders = { 'Accept': 'application/vnd.twitchtv.v3+json', 'Client-ID': self.twitchClientID } url = 'https://api.twitch.tv/kraken/streams/{}'.format(channel) response = self.bot.moduleHandler.runActionUntilValue( 'fetch-url', url, extraHeaders=twitchHeaders) streamData = response.json() if 'stream' in streamData and streamData['stream'] is not None: chanData = streamData['stream']['channel'] channelOnline = True elif 'error' not in streamData: url = 'https://api.twitch.tv/kraken/channels/{}'.format(channel) response = self.bot.moduleHandler.runActionUntilValue( 'fetch-url', url, extraHeaders=twitchHeaders) chanData = response.json() if len(chanData) == 0: return output = [] if channelOnline: name = colour(A.normal[A.fg.green['{}'.format( chanData['display_name'])]]) else: name = colour(A.normal[A.fg.red['{}'.format( chanData['display_name'])]]) output.append(name) graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) title = ' "{}"'.format( re.sub(r'[\r\n]+', graySplitter, chanData['status'].strip())) output.append(title) if chanData['game'] is not None: game = colour(A.normal[A.fg.gray[', playing '], '{}'.format(chanData['game'])]) output.append(game) if chanData['mature']: mature = colour(A.normal[A.fg.lightRed[' [Mature]']]) output.append(mature) if channelOnline: viewers = streamData['stream']['viewers'] status = colour(A.normal[A.fg.green[ ' (Live with {0:,d} viewers)'.format(viewers)]]) else: status = colour(A.normal[A.fg.red[' (Offline)']]) output.append(status) return ''.join(output), 'https://twitch.tv/{}'.format(channel)
from base64 import b64decode, b64encode from collections import OrderedDict from html.entities import name2codepoint from datetime import timedelta from dateutil.parser import parse import re from twisted.words.protocols.irc import assembleFormattedText as colour, attributes as A graySplitter = colour(A.normal[' ', A.fg.gray['|'], ' ']) def isNumber(s: str) -> bool: """returns True if string s can be cast to a number, False otherwise""" try: float(s) return True except ValueError: return False # From this SO answer: http://stackoverflow.com/a/6043797/331047 def splitUTF8(s: str, n: int) -> str: """Split UTF-8 s into chunks of maximum byte length n""" while len(s) > n: k = n while (ord(s[k]) & 0xc0) == 0x80: k -= 1 yield s[:k] s = s[k:]