Example #1
0
	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))
Example #2
0
	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")
Example #3
0
    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))
Example #4
0
    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)
Example #5
0
	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)
Example #6
0
    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)
Example #7
0
	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)
Example #8
0
    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")
Example #9
0
	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")