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 execute(self, message): """ :type message: IrcMessage """ #First just get anything new, if there is any subprocess.check_output(['git', 'pull']) #Check if any new updates were pulled in outputLines = subprocess.check_output(['git', 'log', '--format=oneline']).splitlines() commitMessages = [] linecount = 0 for line in outputLines: lineparts = line.split(" ", 1) #If we've reached a commit we've already mentioned, stop the whole thing if lineparts[0] == self.lastCommitHash: break linecount += 1 #Only show the last few commit messages, but keep counting lines regardless if len(commitMessages) < self.MAX_UPDATES_TO_DISPLAY : commitMessages.append(lineparts[1]) if linecount == 0: replytext = u"No updates found, seems I'm up-to-date. I feel so hip!" elif linecount == 1: replytext = u"One new commit: {}".format(commitMessages[0]) else: commitMessages.reverse() #Otherwise the messages are ordered new to old replytext = u"{:,} new commits: {}".format(linecount, StringUtil.joinWithSeparator(commitMessages)) if linecount > self.MAX_UPDATES_TO_DISPLAY: replytext += u"; {:,} older ones".format(linecount - self.MAX_UPDATES_TO_DISPLAY) #Set the last mentioned hash to the newest one self.lastCommitHash = outputLines[0].split(" ", 1)[0] message.reply(replytext, "say")
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 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 """ 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")