def getCurrentlyLiveStreamers(self, serverChannelString): """ Get a string with all the currently live streamers that the provided channel follows. If there's only a few streamers live, more expansive info is shown per streamer :param serverChannelString: The server name followed by the channel name, separated by a space :return: A tuple, first entry is a success boolean, second one is the result string with either the error message or the currently live streamers """ streamerIdsToCheck = {} for streamername, streamerdata in self.watchedStreamersData.iteritems(): if serverChannelString in streamerdata['followChannels'] or serverChannelString in streamerdata['reportChannels']: streamerIdsToCheck[streamerdata['clientId']] = streamername isSuccess, result = self.retrieveStreamDataForIds(streamerIdsToCheck) if not isSuccess: self.logError(u"[TwitchWatch] An error occurred during a manual live check. " + result) return (False, "I'm sorry, I wasn't able to retrieve data from Twitch. It's probably entirely their fault, not mine though. Try again in a little while") if len(result) == 0: return (True, "Nobody's live, it seems. Time for videogames and/or random streams, I guess!") #One or more streamers are live, show info on each of them reportStrings = [] shouldUseShortReportString = len(result) >= 4 # Use shorter report strings if there's 4 or more people live for streamerId, streamerdata in result.iteritems(): streamername = streamerIdsToCheck[streamerId] displayname = streamername if self.doesStreamerHaveNickname(streamername, serverChannelString): displayname = self.watchedStreamersData[streamername]['nicknames'][serverChannelString] url = u"https://twitch.tv/{}".format(streamername) if shouldUseShortReportString: reportStrings.append(u"{} ({})".format(displayname, url)) else: reportStrings.append(StringUtil.removeNewlines(u"{}: {} [{}] ({})".format(IrcFormattingUtil.makeTextBold(displayname), streamerdata['title'], streamerdata['game_name'], url))) return (True, StringUtil.joinWithSeparator(reportStrings))
def formatNewTweetText(self, username, tweetData, tweetAge=None, addTweetAge=False): if addTweetAge: if not tweetAge: tweetAge = self.getTweetAge(tweetData['created_at']) tweetAge = DateTimeUtil.durationSecondsToText(tweetAge.total_seconds()) tweetAge = ' ({} ago)'.format(tweetAge) else: tweetAge = '' tweetUrl = "http://twitter.com/_/status/{}".format(tweetData['id_str']) #Use _ instead of username to save some characters #Remove newlines formattedTweetText = tweetData['full_text'].replace('\n\n', '\n').replace('\n', Constants.GREY_SEPARATOR) #Fix special characters (convert '&' to '&' for instance) formattedTweetText = HTMLParser.HTMLParser().unescape(formattedTweetText) #Remove the link to the photo at the end, but mention that there is one if 'media' in tweetData['entities']: for mediaItem in tweetData['entities']['media']: formattedTweetText = formattedTweetText.replace(mediaItem['url'], u'') formattedTweetText += u"(has {})".format(mediaItem['type']) #Add in all the text around the tweet now, so we get a better sense of message length formattedTweetText = u"{name}: {text}{age}{sep}{url}".format(name=IrcFormattingUtil.makeTextBold(self.getDisplayName(username)), text=formattedTweetText, age=tweetAge, sep=Constants.GREY_SEPARATOR, url=tweetUrl) #Expand URLs (if it'd fit) if 'urls' in tweetData['entities']: for urldata in tweetData['entities']['urls']: if len(formattedTweetText) - len(urldata['url']) + len(urldata['expanded_url']) < 325: formattedTweetText = formattedTweetText.replace(urldata['url'], urldata['expanded_url']) return formattedTweetText
def getCurrentlyLiveStreamers(self, serverChannelString): """ Get a string with all the currently live streamers that the provided channel follows. If there's only a few streamers live, more expansive info is shown per streamer :param serverChannelString: The server name followed by the channel name, separated by a space :return: A tuple, first entry is a success boolean, second one is the result string with either the error message or the currently live streamers """ streamerIdsToCheck = {} for streamername, streamerdata in self.watchedStreamersData.iteritems( ): if serverChannelString in streamerdata[ 'followChannels'] or serverChannelString in streamerdata[ 'reportChannels']: streamerIdsToCheck[streamerdata['clientId']] = streamername isSuccess, result = self.retrieveStreamDataForIds(streamerIdsToCheck) if not isSuccess: self.logError( u"[TwitchWatch] An error occurred during a manual live check. " + result) return ( False, "I'm sorry, I wasn't able to retrieve data from Twitch. It's probably entirely their fault, not mine though. Try again in a little while" ) if len(result) == 0: return ( True, "Nobody's live, it seems. Time for videogames and/or random streams, I guess!" ) #One or more streamers are live, show info on each of them reportStrings = [] shouldUseShortReportString = len( result ) >= 4 # Use shorter report strings if there's 4 or more people live for streamerId, streamerdata in result.iteritems(): streamername = streamerIdsToCheck[streamerId] displayname = streamername if self.doesStreamerHaveNickname(streamername, serverChannelString): displayname = self.watchedStreamersData[streamername][ 'nicknames'][serverChannelString] url = u"https://twitch.tv/{}".format(streamername) if shouldUseShortReportString: reportStrings.append(u"{} ({})".format(displayname, url)) else: reportStrings.append( StringUtil.removeNewlines(u"{}: {} [{}] ({})".format( IrcFormattingUtil.makeTextBold(displayname), streamerdata['title'], streamerdata['game_name'], url))) return (True, StringUtil.joinWithSeparator(reportStrings))
def formatNewTweetText(self, username, tweetData, tweetAge=None, addTweetAge=False): if addTweetAge: if not tweetAge: tweetAge = self.getTweetAge(tweetData['created_at']) tweetAge = DateTimeUtil.durationSecondsToText( tweetAge.total_seconds()) tweetAge = ' ({} ago)'.format(tweetAge) else: tweetAge = '' tweetUrl = "http://twitter.com/_/status/{}".format( tweetData['id_str'] ) #Use _ instead of username to save some characters #Remove newlines formattedTweetText = tweetData['full_text'].replace( '\n\n', '\n').replace('\n', Constants.GREY_SEPARATOR) #Fix special characters (convert '&' to '&' for instance) formattedTweetText = HTMLParser.HTMLParser().unescape( formattedTweetText) #Remove the link to the photo at the end, but mention that there is one if 'media' in tweetData['entities']: for mediaItem in tweetData['entities']['media']: formattedTweetText = formattedTweetText.replace( mediaItem['url'], u'') formattedTweetText += u"(has {})".format(mediaItem['type']) #Add in all the text around the tweet now, so we get a better sense of message length formattedTweetText = u"{name}: {text}{age}{sep}{url}".format( name=IrcFormattingUtil.makeTextBold(self.getDisplayName(username)), text=formattedTweetText, age=tweetAge, sep=Constants.GREY_SEPARATOR, url=tweetUrl) #Expand URLs (if it'd fit) if 'urls' in tweetData['entities']: for urldata in tweetData['entities']['urls']: if len(formattedTweetText) - len(urldata['url']) + len( urldata['expanded_url']) < 325: formattedTweetText = formattedTweetText.replace( urldata['url'], urldata['expanded_url']) return formattedTweetText
def execute(self, message): """ :type message: IrcMessage """ replytext = u"" if 'openweathermap' not in GlobalStore.commandhandler.apikeys: replytext = u"No API key for OpenWeatherMap found, please tell my owner so they can fix this" elif message.messagePartsLength == 0: replytext = u"Please enter the name of a city" else: params = { "APPID": GlobalStore.commandhandler.apikeys['openweathermap'], "q": message.message, "units": "metric" } requestType = 'weather' if message.trigger == 'forecast': requestType = 'forecast/daily' params['cnt'] = 4 #Number of days to get forecast for try: req = requests.get("http://api.openweathermap.org/data/2.5/" + requestType, params=params, timeout=5.0) data = json.loads(req.text) except requests.exceptions.Timeout: replytext = u"Sorry, the weather API took too long to respond. Please try again in a little while" except ValueError: replytext = u"Sorry, I couldn't retrieve that data. Try again in a little while, maybe it'll work then" self.logError("[weather] JSON load error. Data received:") self.logError(req.text) else: if data['cod'] != 200 and data['cod'] != "200": if data['cod'] == 404 or data['cod'] == '404': replytext = u"I'm sorry, I don't know where that is" else: replytext = u"An error occurred, please tell my owner to look at the debug output, or try again in a little while ({}: {})".format( data['cod'], data['message']) self.logError("[weather] ERROR in API lookup:") self.logError(data) else: #We've got data! Parse it def getWindDirection(angle): #The highest wind angle where the direction applies windDirectionTranslation = { 11.25: 'N', 33.75: 'NNE', 56.25: 'NE', 78.75: 'ENE', 101.25: 'E', 123.75: 'ESE', 146.25: 'SE', 168.75: 'SSE', 191.25: 'S', 213.75: 'SSW', 236.25: 'SW', 258.75: 'WSW', 281.25: 'W', 303.75: 'WNW', 326.25: 'NW', 348.75: 'NNW', 360.0: 'N' } windDirection = 'N' for maxDegrees in sorted( windDirectionTranslation.keys()): if angle < maxDegrees: break else: windDirection = windDirectionTranslation[ maxDegrees] return windDirection def celsiusToFahrenheit(celsius): return (celsius * 9 / 5) + 32 if message.trigger == 'weather': dataAge = round((time.time() - data['dt']) / 60) dataAgeDisplay = u"" if dataAge <= 0: dataAgeDisplay = u"brand new" else: dataAgeDisplay = u"{dataAge:.0f} minute" if dataAge > 1: dataAgeDisplay += u"s" dataAgeDisplay += u" old" dataAgeDisplay = dataAgeDisplay.format( dataAge=dataAge) windString = u"{:.1f} m/s".format( data['wind']['speed']) #Only add a wind direction if we know it if 'deg' in data['wind']: windString += u", " + getWindDirection( data['wind']['deg']) #Not all replies include a placename or a countryname placename = data['name'] if 'name' in data and len( data['name']) > 0 else None countryname = data['sys'][ 'country'] if 'sys' in data and 'country' in data[ 'sys'] and len( data['sys']['country']) > 0 else None if placename and countryname: replytext = u"{} ({})".format( placename, countryname) elif placename: replytext = u"{}".format(placename) elif countryname: replytext = u"Somewhere in {}".format(countryname) else: replytext = u"Somewhere unknown" #Add the actual weather info replytext += u": {tempC:.1f}°C / {tempF:.1f}°F, {weatherType}. Wind: {windString}. Humidity of {humidity}% (Data is {dataAge})" replytext = replytext.format( tempC=data['main']['temp'], tempF=celsiusToFahrenheit(data['main']['temp']), weatherType=data['weather'][0]['description'], windString=windString, humidity=data['main']['humidity'], dataAge=dataAgeDisplay) else: #Forecast placename = data['city'][ 'name'] if 'city' in data and 'name' in data[ 'city'] and len( data['city']['name']) > 0 else None countryname = data['city'][ 'country'] if 'city' in data and 'country' in data[ 'city'] and len( data['city']['country']) > 0 else None replytext = u"Forecast for " if placename and countryname: replytext += u"{} ({})".format( placename, countryname) elif placename: replytext += placename elif countryname: replytext += countryname else: replytext += u"somewhere unknown" replytext += u": " forecasts = [] for day in data['list']: dayname = datetime.datetime.utcfromtimestamp( day['dt']).strftime(u"%a").upper() forecast = u"{dayname}: {minTempC:.0f}-{maxTempC:.0f}°C / {minTempF:.0f}-{maxTempF:.0f}°F, {weatherType}, {humidity}% hum., {windSpeed:.0f}m/s {windDir} wind" forecast = forecast.format( dayname=IrcFormattingUtil.makeTextBold( dayname), minTempC=day['temp']['min'], maxTempC=day['temp']['max'], minTempF=celsiusToFahrenheit( day['temp']['min']), maxTempF=celsiusToFahrenheit( day['temp']['max']), humidity=day['humidity'], windSpeed=day['speed'], windDir=getWindDirection(day['deg']), weatherType=day['weather'][0]['description']) forecasts.append(forecast) replytext += StringUtil.joinWithSeparator(forecasts) message.bot.sendMessage(message.source, replytext)
def execute(self, message): """ :type message: IrcMessage """ replytext = u"" if 'openweathermap' not in GlobalStore.commandhandler.apikeys: replytext = u"No API key for OpenWeatherMap found, please tell my owner so they can fix this" elif message.messagePartsLength == 0: replytext = u"Please enter the name of a city" else: params = {"APPID": GlobalStore.commandhandler.apikeys['openweathermap'], "q": message.message, "units": "metric"} requestType = 'weather' if message.trigger == 'forecast': requestType = 'forecast/daily' params['cnt'] = 4 #Number of days to get forecast for try: req = requests.get("http://api.openweathermap.org/data/2.5/" + requestType, params=params, timeout=5.0) data = json.loads(req.text) except requests.exceptions.Timeout: replytext = u"Sorry, the weather API took too long to respond. Please try again in a little while" except ValueError: replytext = u"Sorry, I couldn't retrieve that data. Try again in a little while, maybe it'll work then" self.logError("[weather] JSON load error. Data received:") self.logError(req.text) else: if data['cod'] != 200 and data['cod'] != "200": if data['cod'] == 404 or data['cod'] == '404': replytext = u"I'm sorry, I don't know where that is" else: replytext = u"An error occurred, please tell my owner to look at the debug output, or try again in a little while ({}: {})".format(data['cod'], data['message']) self.logError("[weather] ERROR in API lookup:") self.logError(data) else: #We've got data! Parse it def getWindDirection(angle): #The highest wind angle where the direction applies windDirectionTranslation = {11.25: 'N', 33.75: 'NNE', 56.25: 'NE', 78.75: 'ENE', 101.25: 'E', 123.75: 'ESE', 146.25: 'SE', 168.75: 'SSE', 191.25: 'S', 213.75: 'SSW', 236.25: 'SW', 258.75: 'WSW', 281.25: 'W', 303.75: 'WNW', 326.25: 'NW', 348.75: 'NNW', 360.0: 'N'} windDirection = 'N' for maxDegrees in sorted(windDirectionTranslation.keys()): if angle < maxDegrees: break else: windDirection = windDirectionTranslation[maxDegrees] return windDirection def celsiusToFahrenheit(celsius): return (celsius * 9 / 5) + 32 if message.trigger == 'weather': dataAge = round((time.time() - data['dt']) / 60) dataAgeDisplay = u"" if dataAge <= 0: dataAgeDisplay = u"brand new" else: dataAgeDisplay = u"{dataAge:.0f} minute" if dataAge > 1: dataAgeDisplay += u"s" dataAgeDisplay += u" old" dataAgeDisplay = dataAgeDisplay.format(dataAge=dataAge) windString = u"{:.1f} m/s".format(data['wind']['speed']) #Only add a wind direction if we know it if 'deg' in data['wind']: windString += u", " + getWindDirection(data['wind']['deg']) #Not all replies include a placename or a countryname placename = data['name'] if 'name' in data and len(data['name']) > 0 else None countryname = data['sys']['country'] if 'sys' in data and 'country' in data['sys'] and len(data['sys']['country']) > 0 else None if placename and countryname: replytext = u"{} ({})".format(placename, countryname) elif placename: replytext = u"{}".format(placename) elif countryname: replytext = u"Somewhere in {}".format(countryname) else: replytext = u"Somewhere unknown" #Add the actual weather info replytext += u": {tempC:.1f}°C / {tempF:.1f}°F, {weatherType}. Wind: {windString}. Humidity of {humidity}% (Data is {dataAge})" replytext = replytext.format(tempC=data['main']['temp'], tempF=celsiusToFahrenheit(data['main']['temp']), weatherType=data['weather'][0]['description'], windString=windString, humidity=data['main']['humidity'], dataAge=dataAgeDisplay) else: #Forecast placename = data['city']['name'] if 'city' in data and 'name' in data['city'] and len(data['city']['name']) > 0 else None countryname = data['city']['country'] if 'city' in data and 'country' in data['city'] and len(data['city']['country']) > 0 else None replytext = u"Forecast for " if placename and countryname: replytext += u"{} ({})".format(placename, countryname) elif placename: replytext += placename elif countryname: replytext += countryname else: replytext += u"somewhere unknown" replytext += u": " forecasts = [] for day in data['list']: dayname = datetime.datetime.utcfromtimestamp(day['dt']).strftime(u"%a").upper() forecast = u"{dayname}: {minTempC:.0f}-{maxTempC:.0f}°C / {minTempF:.0f}-{maxTempF:.0f}°F, {weatherType}, {humidity}% hum., {windSpeed:.0f}m/s {windDir} wind" forecast = forecast.format(dayname=IrcFormattingUtil.makeTextBold(dayname), minTempC=day['temp']['min'], maxTempC=day['temp']['max'], minTempF=celsiusToFahrenheit(day['temp']['min']), maxTempF=celsiusToFahrenheit(day['temp']['max']), humidity=day['humidity'], windSpeed=day['speed'], windDir=getWindDirection(day['deg']), weatherType=day['weather'][0]['description']) forecasts.append(forecast) replytext += StringUtil.joinWithSeparator(forecasts) message.bot.sendMessage(message.source, replytext)
def execute(self, message): """ :type message: IrcMessage """ if message.messagePartsLength == 0: message.reply( "There's far too many boardgames to just pick a random one! Please provide a search query", "say") return #Since the API's search is a bit crap and doesn't sort properly, scrape the web search page try: request = requests.get("https://boardgamegeek.com/geeksearch.php", params={ "action": "search", "objecttype": "boardgame", "q": message.message }, timeout=10.0) except requests.exceptions.Timeout: message.reply( "Either your search query was too extensive for BoardGameGeek, or they're distracted by a boardgame. Either way, they took too long to respond, sorry" ) return if request.status_code != 200: message.reply( "Something seems to have gone wrong. At BoardGameGeek, I mean, because I never make mistaks. Try again in a little while", "say") return page = BeautifulSoup(request.content, "html.parser") #Get the first result row row = page.find(class_="collection_objectname") if row is None: message.reply( "BoardGameGeek doesn't think a game called '{}' exists. Maybe you made a typo?" .format(message.message), "say") return #Then get the link to the board game page from that, to get the game ID from the URL # Format of the url is '/boardgame/[ID]/[NAME] gameId = row.find('a')['href'].split('/', 3)[2] #Now query the API to get info on this game try: request = requests.get( "https://www.boardgamegeek.com/xmlapi2/thing", params={'id': gameId}, timeout=10.0) except requests.exceptions.Timeout: message.reply( "I know you need some patience for boardgames, but not for info about boardgames. BoardGameGeek took too long to respond, sorry" ) return try: xml = ElementTree.fromstring(request.content) except ElementTree.ParseError: message.reply( "I don't know how to read the data returned by BoardGameGeek, which is weird because I'm coded very well. Try again in a little while, see if it works then?", "say") return item = xml.find('item') if item is None: #Specific check otherwise Python prints a warning message.reply( "I'm sorry, I didn't find any games called '{}'. Did you make a typo? Or did you just invent a new game?!" .format(message.message), "say") print request.content return replytext = u"{} ({} players, {} min, {}): ".format( IrcFormattingUtil.makeTextBold(item.find('name').attrib['value']), self.getValueRangeDescription(item, 'minplayers', 'maxplayers'), self.getValueRangeDescription(item, 'minplaytime', 'maxplaytime'), item.find('yearpublished').attrib['value']) url = u"{}http://boardgamegeek.com/boardgame/{})".format( Constants.GREY_SEPARATOR, gameId) #Fit in as much of the description as we can lengthLeft = Constants.MAX_MESSAGE_LENGTH - len(replytext) - len(url) description = HTMLParser.HTMLParser().unescape( item.find('description').text) #Some descriptions start with a disclaimer that it's from the publisher, remove that to save space if description.startswith(u"Game description from the publisher" ) or description.startswith( u"From the manufacturer's website"): description = description.split('\n', 1)[1].lstrip() #Remove newlines description = description.replace('\n', ' ') #Slice it so it fits in the available space, cut at the last word separator description = description[:lengthLeft] description = description[:description.rfind(' ')] + u'[...]' #Show the result replytext += description + url message.reply(replytext, "say")
def execute(self, message): """ :type message: IrcMessage.IrcMessage """ appId = GlobalStore.commandhandler.apikeys.get('oxforddictionaries', {}).get('app_id', None) appKey = GlobalStore.commandhandler.apikeys.get( 'oxforddictionaries', {}).get('app_key', None) if not appId or not appKey: return message.reply( "Since I don't know a lot of words myself, I need access to the Oxford Dictionaries to help you out here, and I don't seem to have API keys required, sorry! " "Poke my owner(s), they can probably add them", "say") if message.messagePartsLength == 0: return message.reply( "Since I don't have a wordlist handy, I can't just pick a random word to define, so you'll have to enter something to look up. Thanks!", "say") try: apiresult = requests.get( "https://od-api.oxforddictionaries.com:443/api/v1/entries/en/" + message.message, headers={ "app_id": appId, "app_key": appKey }, timeout=10.0) except requests.exceptions.Timeout: return message.reply( "Hmm, it took the Oxford site a bit too long to respond. They're probably busy trying to keep up with internet slang or something. Try again in a bit!", "say") if apiresult.status_code == 404: return message.reply( "Apparently that's not a word Oxford Dictionaries knows about. So either it's one of those words only teenagers use, or it doesn't exist. Or you made a typo, which happens to the best of us", "say") elif apiresult.status_code != 200: return message.reply( "Seems like Oxford Dictionary isn't feeling well, since they did not send a happy reply. Give them some time to recover, and try again in a bit", "say") #There's always going to be at least one entry from here on, since otherwise we would've gotten a status code 404 reply #The result is in a 'results' field. It can list multiple entries, but since they can be different word types, it could make the output confusing, so just use the first entry for now data = apiresult.json()['results'][0] #In case the found word is different from the entered word, retrieve it from the dataset replytext = IrcFormattingUtil.makeTextBold(data['word']) #Get the word type of the first entry, since that's what we're going to get the definition(s) from. Word type is 'Noun', 'Verb', etc wordType = data['lexicalEntries'][0]['lexicalCategory'].lower() if wordType != 'other': replytext += " ({})".format(wordType) replytext += ": " #The actual definitions are inside the 'lexicalEntries' field, which is a list of dictionaries # Each dictionary contains an 'entries' field, which is another list of dictionaries # Each of those dicts has a 'senses' dictionary list, which contains a 'definitions' list #'Eating' the entires list means the definitions will be added in reverse order, but we will be eating that list too, so it'll be reversed again definitions = [] while len(data['lexicalEntries']) > 0: lexicalEntry = data['lexicalEntries'].pop() entry = lexicalEntry['entries'].pop() if 'senses' in entry: while entry['senses']: sense = entry['senses'].pop() if 'definitions' in sense: definitions.extend(sense['definitions']) elif 'short_definitions' in sense: definitions.extend(sense['short_definitions']) #Not all words have a definition. Something like 'swum' has a 'crossReferenceMarkers' list that mentions which word it's related to elif 'crossReferenceMarkers' in sense: definitions.extend(sense['crossReferenceMarkers']) else: definitions.append("[definition not found]") #Some words without definition reference the word they're derived from, list that so users can look that word up elif 'derivativeOf' in lexicalEntry: derivative = lexicalEntry['derivativeOf'][0]['text'] definitions.append( "word is derived from '{}'".format(derivative)) if not definitions: #Apparently we didn't find any good definitions for some reason replytext += "No definitions found, for some reason, even though the word definitely exists. Language is difficult, even for a dictionary" else: #Keep adding definitions to the output text until we run out of message space separatorLength = len(Constants.GREY_SEPARATOR) while definitions and len(replytext) + separatorLength + len( definitions[0]) < Constants.MAX_MESSAGE_LENGTH: replytext += definitions.pop() + Constants.GREY_SEPARATOR #Remove the last trailing separator replytext = replytext[:-separatorLength] #Add how much defitions we skipped, if necessary if len(definitions) > 0: replytext += " ({:,} more)".format(len(definitions)) #Done! Show our result message.reply(replytext, "say")
def execute(self, message): """ :type message: IrcMessage """ url = "http://www.humblebundle.com/" #Allow for any special bundle search if message.messagePartsLength > 0: #Some bundles have a name with spaces in it. The URL replaces these with dashes, so we should too urlSuffix = message.message.replace(' ', '-').lower() if urlSuffix == 'store': message.reply( "I'm sorry, I can't retrieve store data (yet (maybe))") return #Correct a possible typo, since the weekly bundle is called 'weekly' and not 'week' elif urlSuffix == 'week': url += 'weekly' else: url += urlSuffix #Only add all the games if the full trigger is used addGameList = message.trigger == 'humblebundle' try: pageDownload = requests.get(url, timeout=10.0) except requests.ConnectionError: message.reply( "Sorry, I couldn't connect to the Humble Bundle site. Try again in a little while!" ) return except requests.exceptions.Timeout: message.reply( "Sorry, the Humble Bundle site took too long to respond. Try again in a bit!" ) return if pageDownload.status_code != 200: self.logWarning( "[Humble] Page '{}' returned code {} instead of 200 (OK)". format(url, pageDownload.status_code)) message.reply( "Sorry, I can't retrieve that bundle page. Either the site is down, or that bundle doesn't exist" ) return page = BeautifulSoup(pageDownload.content, 'html.parser') #Get the part of the title up to the first opening parenthesis, since that's where the 'Pay what you wan't message starts title = page.title.string[:page.title.string.find('(') - 1] #First (try to) get a list of all the games with price requirements #Only if we actually need to show all the games if addGameList: lockedGames = {'BTA': [], 'Fixed': []} for lockImageElement in page.find_all('i', class_='hb-lock'): lockType = None if 'green' in lockImageElement.attrs['class']: lockType = 'BTA' elif 'blue' in lockImageElement.attrs['class']: lockType = 'Fixed' else: continue #The game name is a sibling of the lock node, so parse the lock's parent text lockedGameElement = lockImageElement.parent #If the game name consists of a single line (and it's not empty) store that if lockedGameElement.string and len( lockedGameElement.string) > 0: lockedGames[lockType].append( lockedGameElement.string.strip().lower()) #Multiple lines. Add both the first line, and a combination of all the lines else: lines = list(lockedGameElement.stripped_strings) if len(lines) > 0: lockedGames[lockType].append(lines[0].strip().lower()) #If there's multiple lines, join them and add the full title too if len(lines) > 1: lockedGames[lockType].append( " ".join(lines).lower()) #The names of the games (or books) are listed in italics in the description section, get them from there #Also do this if we don't need to list the games, since we do need a game count gamePriceCategories = {"PWYW": [], "BTA": [], "Fixed": []} gamecount = 0 descriptionElement = page.find(class_='bundle-info-text') gameFound = False if not descriptionElement: self.logError("[Humble] No description element found!") else: descriptionGameList = [] for paragraph in descriptionElement.find_all('p'): #If there is a bolded element, and it's at the start of the paragraph, AND we've already found names, we're done, # because all the games are listed in the first paragraph boldedElement = paragraph.find('b') if boldedElement and paragraph.text.startswith( boldedElement.text) and gameFound: break #Otherwise, add all the titles listed to the collection for titleElement in paragraph.find_all(['i', 'em']): gameFound = True gamename = titleElement.text.strip( " ,.;" ) #Sometimes punctuation marks are included in the tag, remove those #If the site lists two games after each other, they don't start a new HTML tag, so the game names # get mushed together. Split that up if "," in gamename: gamenames = gamename.split(",") for splitGamename in gamenames: splitGamename = splitGamename.strip(" ,.;") if len(splitGamename) > 0: descriptionGameList.append(splitGamename) #If there's no comma, it's just a single game name else: descriptionGameList.append(gamename) gamecount = len(descriptionGameList) #Now check to see which category the games we found belong to if addGameList: for gamename in descriptionGameList: #See if this title is in the locked-games lists we found earlier if gamename.lower() in lockedGames['BTA']: gamePriceCategories['BTA'].append(gamename) elif gamename.lower() in lockedGames['Fixed']: gamePriceCategories['Fixed'].append(gamename) else: gamePriceCategories['PWYW'].append(gamename) #Totals aren't shown on the site immediately, but are edited into the page with Javascript. Get info from there totalMoney = -1.0 contributors = -1 avgPrice = -1.0 timeLeft = u"" for scriptElement in page.find_all('script'): script = scriptElement.text if script.count("'initial_stats_data':") > 0: #This script element contains data like the average price and the time left match = re.search("'initial_stats_data':(.+),", script) if not match: self.logWarning( "[Humble] Expected to find initial values, but failed:" ) self.logWarning(script) else: data = json.loads(match.group(1)) if 'rawtotal' in data: totalMoney = data['rawtotal'] else: self.logWarning( "[Humble] Sales data found, but total amount is missing!" ) self.logWarning(json.dumps(data)) if 'numberofcontributions' in data and 'total' in data[ 'numberofcontributions']: contributors = int( data['numberofcontributions']['total']) else: self.logWarning("[Humble] Contributor data not found!") self.logWarning(json.dumps(data)) if totalMoney > -1.0 and contributors > -1: avgPrice = totalMoney / contributors else: self.logWarning( "[Humble] Money raised and/or number of contributors not found!" ) self.logWarning(json.dumps(data)) #The time variable is in a different script than the other data, search for it separately timeLeftMatch = re.search( 'var timing = \{"start": \d+, "end": (\d+)\};', script) if timeLeftMatch: timeLeft = DateTimeUtil.durationSecondsToText( int(timeLeftMatch.group(1)) - time.time(), 'm') #If we found all the data we need, we can stop if avgPrice > -1.0 and timeLeft != u"": break if totalMoney == -1.0 or contributors == -1 or avgPrice == -1.0: replytext = u"Sorry, the data could not be retrieved. This is either because the site is down, or because of some weird bug. Please try again in a little while" else: replytext = u"{} has an average price of ${:.2f} and raised ${:,} from {:,} people.".format( title, round(avgPrice, 2), round(totalMoney, 2), contributors) if timeLeft != u"": replytext += u" It will end in {}.".format(timeLeft) replytext += u" It contains {:,} titles".format(gamecount) if addGameList: replytext += u":" #Add a list of all the games found for priceType in ('PWYW', 'BTA', 'Fixed'): if len(gamePriceCategories[priceType]) > 0: replytext += u" {}: {}".format( IrcFormattingUtil.makeTextBold(priceType), StringUtil.joinWithSeparator( gamePriceCategories[priceType])) if not message.isPrivateMessage and len( replytext) > Constants.MAX_MESSAGE_LENGTH: replytext = replytext[:Constants.MAX_MESSAGE_LENGTH - 5] + u"[...]" replytext += u" (itemlist may be wrong)" #Add the url too, so people can go see the bundle easily replytext += u" ({})".format(url) message.reply(replytext)
def execute(self, message): """ :type message: IrcMessage """ url = "http://www.humblebundle.com/" #Allow for any special bundle search if message.messagePartsLength > 0: #Some bundles have a name with spaces in it. The URL replaces these with dashes, so we should too urlSuffix = message.message.replace(' ', '-').lower() if urlSuffix == 'store': message.reply("I'm sorry, I can't retrieve store data (yet (maybe))") return #Correct a possible typo, since the weekly bundle is called 'weekly' and not 'week' elif urlSuffix == 'week': url += 'weekly' else: url += urlSuffix #Only add all the games if the full trigger is used addGameList = message.trigger == 'humblebundle' try: pageDownload = requests.get(url, timeout=10.0) except requests.ConnectionError: message.reply("Sorry, I couldn't connect to the Humble Bundle site. Try again in a little while!") return except requests.exceptions.Timeout: message.reply("Sorry, the Humble Bundle site took too long to respond. Try again in a bit!") return if pageDownload.status_code != 200: self.logWarning("[Humble] Page '{}' returned code {} instead of 200 (OK)".format(url, pageDownload.status_code)) message.reply("Sorry, I can't retrieve that bundle page. Either the site is down, or that bundle doesn't exist") return page = BeautifulSoup(pageDownload.content, 'html.parser') #Get the part of the title up to the first opening parenthesis, since that's where the 'Pay what you wan't message starts title = page.title.string[:page.title.string.find('(') - 1] #First (try to) get a list of all the games with price requirements #Only if we actually need to show all the games if addGameList: lockedGames = {'BTA': [], 'Fixed': []} for lockImageElement in page.find_all('i', class_='hb-lock'): lockType = None if 'green' in lockImageElement.attrs['class']: lockType = 'BTA' elif 'blue' in lockImageElement.attrs['class']: lockType = 'Fixed' else: continue #The game name is a sibling of the lock node, so parse the lock's parent text lockedGameElement = lockImageElement.parent #If the game name consists of a single line (and it's not empty) store that if lockedGameElement.string and len(lockedGameElement.string) > 0: lockedGames[lockType].append(lockedGameElement.string.strip().lower()) #Multiple lines. Add both the first line, and a combination of all the lines else: lines = list(lockedGameElement.stripped_strings) if len(lines) > 0: lockedGames[lockType].append(lines[0].strip().lower()) #If there's multiple lines, join them and add the full title too if len(lines) > 1: lockedGames[lockType].append(" ".join(lines).lower()) #The names of the games (or books) are listed in italics in the description section, get them from there #Also do this if we don't need to list the games, since we do need a game count gamePriceCategories = {"PWYW": [], "BTA": [], "Fixed": []} gamecount = 0 descriptionElement = page.find(class_='bundle-info-text') gameFound = False if not descriptionElement: self.logError("[Humble] No description element found!") else: descriptionGameList = [] for paragraph in descriptionElement.find_all('p'): #If there is a bolded element, and it's at the start of the paragraph, AND we've already found names, we're done, # because all the games are listed in the first paragraph boldedElement = paragraph.find('b') if boldedElement and paragraph.text.startswith(boldedElement.text) and gameFound: break #Otherwise, add all the titles listed to the collection for titleElement in paragraph.find_all(['i', 'em']): gameFound = True gamename = titleElement.text.strip(" ,.;") #Sometimes punctuation marks are included in the tag, remove those #If the site lists two games after each other, they don't start a new HTML tag, so the game names # get mushed together. Split that up if "," in gamename: gamenames = gamename.split(",") for splitGamename in gamenames: splitGamename = splitGamename.strip(" ,.;") if len(splitGamename) > 0: descriptionGameList.append(splitGamename) #If there's no comma, it's just a single game name else: descriptionGameList.append(gamename) gamecount = len(descriptionGameList) #Now check to see which category the games we found belong to if addGameList: for gamename in descriptionGameList: #See if this title is in the locked-games lists we found earlier if gamename.lower() in lockedGames['BTA']: gamePriceCategories['BTA'].append(gamename) elif gamename.lower() in lockedGames['Fixed']: gamePriceCategories['Fixed'].append(gamename) else: gamePriceCategories['PWYW'].append(gamename) #Totals aren't shown on the site immediately, but are edited into the page with Javascript. Get info from there totalMoney = -1.0 contributors = -1 avgPrice = -1.0 timeLeft = u"" for scriptElement in page.find_all('script'): script = scriptElement.text if script.count("'initial_stats_data':") > 0: #This script element contains data like the average price and the time left match = re.search("'initial_stats_data':(.+),", script) if not match: self.logWarning("[Humble] Expected to find initial values, but failed:") self.logWarning(script) else: data = json.loads(match.group(1)) if 'rawtotal' in data: totalMoney = data['rawtotal'] else: self.logWarning("[Humble] Sales data found, but total amount is missing!") self.logWarning(json.dumps(data)) if 'numberofcontributions' in data and 'total' in data['numberofcontributions']: contributors = int(data['numberofcontributions']['total']) else: self.logWarning("[Humble] Contributor data not found!") self.logWarning(json.dumps(data)) if totalMoney > -1.0 and contributors > -1: avgPrice = totalMoney / contributors else: self.logWarning("[Humble] Money raised and/or number of contributors not found!") self.logWarning(json.dumps(data)) #The time variable is in a different script than the other data, search for it separately timeLeftMatch = re.search('var timing = \{"start": \d+, "end": (\d+)\};', script) if timeLeftMatch: timeLeft = DateTimeUtil.durationSecondsToText(int(timeLeftMatch.group(1)) - time.time(), 'm') #If we found all the data we need, we can stop if avgPrice > -1.0 and timeLeft != u"": break if totalMoney == -1.0 or contributors == -1 or avgPrice == -1.0: replytext = u"Sorry, the data could not be retrieved. This is either because the site is down, or because of some weird bug. Please try again in a little while" else: replytext = u"{} has an average price of ${:.2f} and raised ${:,} from {:,} people.".format(title, round(avgPrice, 2), round(totalMoney, 2), contributors) if timeLeft != u"": replytext += u" It will end in {}.".format(timeLeft) replytext += u" It contains {:,} titles".format(gamecount) if addGameList: replytext += u":" #Add a list of all the games found for priceType in ('PWYW', 'BTA', 'Fixed'): if len(gamePriceCategories[priceType]) > 0: replytext += u" {}: {}".format(IrcFormattingUtil.makeTextBold(priceType), StringUtil.joinWithSeparator(gamePriceCategories[priceType])) if not message.isPrivateMessage and len(replytext) > Constants.MAX_MESSAGE_LENGTH: replytext = replytext[:Constants.MAX_MESSAGE_LENGTH - 5] + u"[...]" replytext += u" (itemlist may be wrong)" #Add the url too, so people can go see the bundle easily replytext += u" ({})".format(url) message.reply(replytext)
def executeScheduledFunction(self): #Go through all our stored streamers, and see if we need to report online status somewhere # If we do, check if they're actually online streamerIdsToCheck = { } #Store as a clientId-to-streamername dict to facilitate reverse lookup in self.streamerdata later for streamername, data in self.watchedStreamersData.iteritems(): if len(data['reportChannels']) > 0: #Ok, store that we need to check whether this stream is online or not # Because doing the check one time for all streamers at once is far more efficient streamerIdsToCheck[data['clientId']] = streamername if len(streamerIdsToCheck) == 0: #Nothing to do! Let's stop now return isSuccess, liveStreamDataById = self.retrieveStreamDataForIds( streamerIdsToCheck.keys()) if not isSuccess: self.logError( u"[TwitchWatch] An error occurred during the scheduled live check. " + liveStreamDataById) #Still update the last checked time, so we do get results when the connection works again self.lastLiveCheckTime = time.time() return #If the last time we checked for updates was (far) longer ago than the time between update checks, we've probably been offline for a while # Any data we retrieve could be old, so don't report it, but just log who's streaming and who isn't if self.lastLiveCheckTime: shouldReport = time.time( ) - self.lastLiveCheckTime <= self.scheduledFunctionTime * 6 else: shouldReport = True if not shouldReport: self.logDebug( "[TwitchWatcher] Skipping reporting on live streams, since our last check was {} seconds ago, which is too long" .format(time.time() - self.lastLiveCheckTime)) self.lastLiveCheckTime = time.time() channelMessages = { } #key is string with server-channel, separated by a space. Value is a list of tuples with data on streams that are live #Go through all the required IDs and check if the API returned info info on that stream. If so, store that data for display later for streamerId, streamername in streamerIdsToCheck.iteritems(): #Check if the requested ID exists in the API reply. If it didn't, the stream is offline if streamerId not in liveStreamDataById: self.watchedStreamersData[streamername][ 'hasBeenReportedLive'] = False #If we have already reported the stream is live, skip over it now. Otherwise report that it has gone live elif not self.watchedStreamersData[streamername][ 'hasBeenReportedLive']: self.watchedStreamersData[streamername][ 'hasBeenReportedLive'] = True if shouldReport: #Stream is live, store some info to display later for serverChannelString in self.watchedStreamersData[ streamername]['reportChannels']: #Add this stream's data to the channel's reporting output if serverChannelString not in channelMessages: channelMessages[serverChannelString] = [] channelMessages[serverChannelString].append({ 'streamername': streamername, 'gameName': liveStreamDataById[streamerId]['game_name'], 'title': liveStreamDataById[streamerId]['title'] }) #Save live status of all the streams self.saveWatchedStreamerData() if shouldReport: #And now report each online stream to each channel that wants it for serverChannelString, streamdatalist in channelMessages.iteritems( ): server, channel = serverChannelString.rsplit(" ", 1) #First check if we're even in the server and channel we need to report to if server not in GlobalStore.bothandler.bots or channel not in GlobalStore.bothandler.bots[ server].channelsUserList: continue reportStrings = [] #If we have a lot of live streamers to report, keep it short. Otherwise, we can be a bit more verbose useShortReportString = len(streamdatalist) >= 4 for streamdata in streamdatalist: displayname = self.getStreamerNickname( streamdata['streamername'], serverChannelString) url = "https://twitch.tv/" + streamdata['streamername'] #A lot of live streamers to report, keep it short. Just the streamer name and the URL if useShortReportString: reportStrings.append(u"{} ({})".format( displayname, url)) # Only a few streamers live, we can be a bit more verbose else: reportStrings.append( StringUtil.removeNewlines( u"{}: {} [{}] ({})".format( IrcFormattingUtil.makeTextBold( displayname), streamdata['title'], streamdata['gameName'], url))) #Now make the bot say it GlobalStore.bothandler.bots[server].sendMessage( channel.encode("utf8"), u"Streamer{} went live: ".format( u's' if len(reportStrings) > 1 else u'') + StringUtil.joinWithSeparator(reportStrings), "say")
def executeScheduledFunction(self): #Go through all our stored streamers, and see if we need to report online status somewhere # If we do, check if they're actually online streamerIdsToCheck = {} #Store as a clientId-to-streamername dict to facilitate reverse lookup in self.streamerdata later for streamername, data in self.watchedStreamersData.iteritems(): if len(data['reportChannels']) > 0: #Ok, store that we need to check whether this stream is online or not # Because doing the check one time for all streamers at once is far more efficient streamerIdsToCheck[data['clientId']] = streamername if len(streamerIdsToCheck) == 0: #Nothing to do! Let's stop now return isSuccess, liveStreamDataById = self.retrieveStreamDataForIds(streamerIdsToCheck.keys()) if not isSuccess: self.logError(u"[TwitchWatch] An error occurred during the scheduled live check. " + liveStreamDataById) #Still update the last checked time, so we do get results when the connection works again self.lastLiveCheckTime = time.time() return #If the last time we checked for updates was (far) longer ago than the time between update checks, we've probably been offline for a while # Any data we retrieve could be old, so don't report it, but just log who's streaming and who isn't if self.lastLiveCheckTime: shouldReport = time.time() - self.lastLiveCheckTime <= self.scheduledFunctionTime * 6 else: shouldReport = True if not shouldReport: self.logDebug("[TwitchWatcher] Skipping reporting on live streams, since our last check was {} seconds ago, which is too long".format(time.time() - self.lastLiveCheckTime)) self.lastLiveCheckTime = time.time() channelMessages = {} #key is string with server-channel, separated by a space. Value is a list of tuples with data on streams that are live #Go through all the required IDs and check if the API returned info info on that stream. If so, store that data for display later for streamerId, streamername in streamerIdsToCheck.iteritems(): #Check if the requested ID exists in the API reply. If it didn't, the stream is offline if streamerId not in liveStreamDataById: self.watchedStreamersData[streamername]['hasBeenReportedLive'] = False #If we have already reported the stream is live, skip over it now. Otherwise report that it has gone live elif not self.watchedStreamersData[streamername]['hasBeenReportedLive']: self.watchedStreamersData[streamername]['hasBeenReportedLive'] = True if shouldReport: #Stream is live, store some info to display later for serverChannelString in self.watchedStreamersData[streamername]['reportChannels']: #Add this stream's data to the channel's reporting output if serverChannelString not in channelMessages: channelMessages[serverChannelString] = [] channelMessages[serverChannelString].append({'streamername': streamername, 'gameName': liveStreamDataById[streamerId]['game_name'], 'title': liveStreamDataById[streamerId]['title']}) #Save live status of all the streams self.saveWatchedStreamerData() if shouldReport: #And now report each online stream to each channel that wants it for serverChannelString, streamdatalist in channelMessages.iteritems(): server, channel = serverChannelString.rsplit(" ", 1) #First check if we're even in the server and channel we need to report to if server not in GlobalStore.bothandler.bots or channel not in GlobalStore.bothandler.bots[server].channelsUserList: continue reportStrings = [] #If we have a lot of live streamers to report, keep it short. Otherwise, we can be a bit more verbose useShortReportString = len(streamdatalist) >= 4 for streamdata in streamdatalist: displayname = self.getStreamerNickname(streamdata['streamername'], serverChannelString) url = "https://twitch.tv/" + streamdata['streamername'] #A lot of live streamers to report, keep it short. Just the streamer name and the URL if useShortReportString: reportStrings.append(u"{} ({})".format(displayname, url)) # Only a few streamers live, we can be a bit more verbose else: reportStrings.append(StringUtil.removeNewlines(u"{}: {} [{}] ({})".format(IrcFormattingUtil.makeTextBold(displayname), streamdata['title'], streamdata['gameName'], url))) #Now make the bot say it GlobalStore.bothandler.bots[server].sendMessage(channel.encode("utf8"), u"Streamer{} went live: ".format(u's' if len(reportStrings) > 1 else u'') + StringUtil.joinWithSeparator(reportStrings), "say")
def execute(self, message): """ :type message: IrcMessage.IrcMessage """ appId = GlobalStore.commandhandler.apikeys.get('oxforddictionaries', {}).get('app_id', None) appKey = GlobalStore.commandhandler.apikeys.get('oxforddictionaries', {}).get('app_key', None) if not appId or not appKey: return message.reply("Since I don't know a lot of words myself, I need access to the Oxford Dictionaries to help you out here, and I don't seem to have API keys required, sorry! " "Poke my owner(s), they can probably add them", "say") if message.messagePartsLength == 0: return message.reply("Since I don't have a wordlist handy, I can't just pick a random word to define, so you'll have to enter something to look up. Thanks!", "say") try: apiresult = requests.get("https://od-api.oxforddictionaries.com:443/api/v1/entries/en/" + message.message, headers={"app_id": appId, "app_key": appKey}, timeout=10.0) except requests.exceptions.Timeout: return message.reply("Hmm, it took the Oxford site a bit too long to respond. They're probably busy trying to keep up with internet slang or something. Try again in a bit!", "say") if apiresult.status_code == 404: return message.reply("Apparently that's not a word Oxford Dictionaries knows about. So either it's one of those words only teenagers use, or it doesn't exist. Or you made a typo, which happens to the best of us", "say") elif apiresult.status_code != 200: return message.reply("Seems like Oxford Dictionary isn't feeling well, since they did not send a happy reply. Give them some time to recover, and try again in a bit", "say") #There's always going to be at least one entry from here on, since otherwise we would've gotten a status code 404 reply #The result is in a 'results' field. It can list multiple entries, but since they can be different word types, it could make the output confusing, so just use the first entry for now data = apiresult.json()['results'][0] #In case the found word is different from the entered word, retrieve it from the dataset replytext = IrcFormattingUtil.makeTextBold(data['word']) #Get the word type of the first entry, since that's what we're going to get the definition(s) from. Word type is 'Noun', 'Verb', etc wordType = data['lexicalEntries'][0]['lexicalCategory'].lower() if wordType != 'other': replytext += " ({})".format(wordType) replytext += ": " #The actual definitions are inside the 'lexicalEntries' field, which is a list of dictionaries # Each dictionary contains an 'entries' field, which is another list of dictionaries # Each of those dicts has a 'senses' dictionary list, which contains a 'definitions' list #'Eating' the entires list means the definitions will be added in reverse order, but we will be eating that list too, so it'll be reversed again definitions = [] while len(data['lexicalEntries']) > 0: lexicalEntry = data['lexicalEntries'].pop() entry = lexicalEntry['entries'].pop() if 'senses' in entry: while entry['senses']: sense = entry['senses'].pop() if 'definitions' in sense: definitions.extend(sense['definitions']) elif 'short_definitions' in sense: definitions.extend(sense['short_definitions']) #Not all words have a definition. Something like 'swum' has a 'crossReferenceMarkers' list that mentions which word it's related to elif 'crossReferenceMarkers' in sense: definitions.extend(sense['crossReferenceMarkers']) else: definitions.append("[definition not found]") #Some words without definition reference the word they're derived from, list that so users can look that word up elif 'derivativeOf' in lexicalEntry: derivative = lexicalEntry['derivativeOf'][0]['text'] definitions.append("word is derived from '{}'".format(derivative)) if not definitions: #Apparently we didn't find any good definitions for some reason replytext += "No definitions found, for some reason, even though the word definitely exists. Language is difficult, even for a dictionary" else: #Keep adding definitions to the output text until we run out of message space separatorLength = len(Constants.GREY_SEPARATOR) while definitions and len(replytext) + separatorLength + len(definitions[0]) < Constants.MAX_MESSAGE_LENGTH: replytext += definitions.pop() + Constants.GREY_SEPARATOR #Remove the last trailing separator replytext = replytext[:-separatorLength] #Add how much defitions we skipped, if necessary if len(definitions) > 0: replytext += " ({:,} more)".format(len(definitions)) #Done! Show our result message.reply(replytext, "say")
def execute(self, message): """ :type message: IrcMessage """ if message.messagePartsLength == 0: message.reply("There's far too many boardgames to just pick a random one! Please provide a search query", "say") return #Since the API's search is a bit crap and doesn't sort properly, scrape the web search page try: request = requests.get("https://boardgamegeek.com/geeksearch.php", params={"action": "search", "objecttype": "boardgame", "q": message.message}, timeout=10.0) except requests.exceptions.Timeout: message.reply("Either your search query was too extensive for BoardGameGeek, or they're distracted by a boardgame. Either way, they took too long to respond, sorry") return if request.status_code != 200: message.reply("Something seems to have gone wrong. At BoardGameGeek, I mean, because I never make mistaks. Try again in a little while", "say") return page = BeautifulSoup(request.content, "html.parser") #Get the first result row row = page.find(class_="collection_objectname") if row is None: message.reply("BoardGameGeek doesn't think a game called '{}' exists. Maybe you made a typo?".format(message.message), "say") return #Then get the link to the board game page from that, to get the game ID from the URL # Format of the url is '/boardgame/[ID]/[NAME] gameId = row.find('a')['href'].split('/', 3)[2] #Now query the API to get info on this game try: request = requests.get("https://www.boardgamegeek.com/xmlapi2/thing", params={'id': gameId}, timeout=10.0) except requests.exceptions.Timeout: message.reply("I know you need some patience for boardgames, but not for info about boardgames. BoardGameGeek took too long to respond, sorry") return try: xml = ElementTree.fromstring(request.content) except ElementTree.ParseError: message.reply("I don't know how to read the data returned by BoardGameGeek, which is weird because I'm coded very well. Try again in a little while, see if it works then?", "say") return item = xml.find('item') if item is None: #Specific check otherwise Python prints a warning message.reply("I'm sorry, I didn't find any games called '{}'. Did you make a typo? Or did you just invent a new game?!".format(message.message), "say") print request.content return replytext = u"{} ({} players, {} min, {}): ".format(IrcFormattingUtil.makeTextBold(item.find('name').attrib['value']), self.getValueRangeDescription(item, 'minplayers', 'maxplayers'), self.getValueRangeDescription(item, 'minplaytime', 'maxplaytime'), item.find('yearpublished').attrib['value']) url = u"{}http://boardgamegeek.com/boardgame/{})".format(Constants.GREY_SEPARATOR, gameId) #Fit in as much of the description as we can lengthLeft = Constants.MAX_MESSAGE_LENGTH - len(replytext) - len(url) description = HTMLParser.HTMLParser().unescape(item.find('description').text) #Some descriptions start with a disclaimer that it's from the publisher, remove that to save space if description.startswith(u"Game description from the publisher") or description.startswith(u"From the manufacturer's website"): description = description.split('\n', 1)[1].lstrip() #Remove newlines description = description.replace('\n', ' ') #Slice it so it fits in the available space, cut at the last word separator description = description[:lengthLeft] description = description[:description.rfind(' ')] + u'[...]' #Show the result replytext += description + url message.reply(replytext, "say")