async def __updatePubSubSubscriptions(self): if self.__isManagingPubSub: self.__timber( 'PubSubUtils', f'Unable to update PubSub subscriptions because it is currently working!' ) return self.__isManagingPubSub = True pubSubTopicsToAdd: List[Topic] = list() pubSubTopicsToRemove: List[Topic] = list() newPubSubEntries = await self.__getSubscribeReadyPubSubEntries() if utils.hasItems(newPubSubEntries): for newPubSubEntry in newPubSubEntries: pubSubTopicsToAdd.append(newPubSubEntry.getTopic()) self.__pubSubEntries[newPubSubEntry.getUserName()].put( newPubSubEntry.getTopic()) if utils.hasItems(self.__pubSubEntries): for topicQueue in self.__pubSubEntries.values(): if topicQueue.qsize() > self.__maxConnectionsPerTwitchChannel: pubSubTopicsToRemove.append( topicQueue.get(block=True, timeout=3)) if utils.hasItems(pubSubTopicsToAdd): self.__timber.log( 'PubSubUtils', f'Subscribing to {len(newPubSubEntries)} PubSub user(s)...') await self.__pubSubPool.subscribe_topics(pubSubTopicsToAdd) self.__timber.log( 'PubSubUtils', f'Finished subscribing to {len(newPubSubEntries)} PubSub user(s)' ) if utils.hasItems(pubSubTopicsToRemove): self.__timber.log( 'PubSubUtils', f'Unsubscribing from {len(pubSubTopicsToRemove)} PubSub user(s)...' ) await self.__pubSubPool.unsubscribe_topics(pubSubTopicsToRemove) self.__timber.log( 'PubSubUtils', f'Finished unsubscribing from {len(pubSubTopicsToRemove)} PubSub user(s)' ) self.__isManagingPubSub = False
def getTimeZones(self, timeZones: List[str]): if not utils.hasItems(timeZones): return None newTimeZones = list() for timeZone in timeZones: newTimeZone = self.getTimeZone(timeZone) if newTimeZone is not None: newTimeZones.append(newTimeZone) if utils.hasItems(newTimeZones): return newTimeZones else: return None
async def event_raw_usernotice(self, channel: Channel, tags: Dict): if not utils.hasItems(tags): return msgId = tags.get('msg-id') if not utils.isValidStr(msgId): return twitchUser = await self.__usersRepository.getUserAsync(channel.name) generalSettings = await self.__generalSettingsRepository.getAllAsync() if msgId == 'raid': await self.__raidLogEvent.handleEvent( twitchChannel = channel, twitchUser = twitchUser, tags = tags ) await self.__raidThankEvent.handleEvent( twitchChannel = channel, twitchUser = twitchUser, tags = tags ) elif msgId == 'subgift' or msgId == 'anonsubgift': await self.__subGiftThankingEvent.handleEvent( twitchChannel = channel, twitchUser = twitchUser, tags = tags ) elif generalSettings.isDebugLoggingEnabled(): self.__timber.log('CynanBot', f'event_raw_usernotice(): {tags}')
def __createUsers(self, jsonContents: Dict[str, Any]) -> List[User]: if not utils.hasItems(jsonContents): raise ValueError( f'jsonContents argument is malformed: \"{jsonContents}\"') users: List[User] = list() for key in jsonContents: user = self.__createUser(key, jsonContents[key]) users.append(user) if not utils.hasItems(users): raise RuntimeError( f'Unable to read in any users from users repository file: \"{self.__usersFile}\"' ) users.sort(key=lambda user: user.getHandle().lower()) return users
def __init__(self, definitions: List[str], word: str): if not utils.hasItems(definitions): raise ValueError( f'definitions argument is malformed: \"{definitions}\"') elif not utils.isValidStr(word): raise ValueError(f'word argument is malformed: \"{word}\"') self.__definitions = definitions self.__word = word
async def handlePointRedemption(self, twitchChannel: Channel, twitchUser: User, redemptionMessage: str, rewardId: str, userIdThatRedeemed: str, userNameThatRedeemed: str) -> bool: if twitchChannel is None: raise ValueError( f'twitchChannel argument is malformed: \"{twitchChannel}\"') elif twitchUser is None: raise ValueError( f'twitchUser argument is malformed: \"{twitchUser}\"') elif not utils.isValidStr(rewardId): raise ValueError(f'rewardId argument is malformed: \"{rewardId}\"') elif not utils.isValidStr(userIdThatRedeemed): raise ValueError( f'userIdThatRedeemed argument is malformed: \"{userIdThatRedeemed}\"' ) elif not utils.isValidStr(userNameThatRedeemed): raise ValueError( f'userNameThatRedeemed argument is malformed: \"{userNameThatRedeemed}\"' ) if not twitchUser.isPkmnEnabled(): return False splits = utils.getCleanedSplits(redemptionMessage) if not utils.hasItems(splits): await twitchUtils.safeSend( twitchChannel, f'⚠ Sorry @{userNameThatRedeemed}, you must specify the exact user name of the person you want to fight' ) return False opponentUserName = utils.removePreceedingAt(splits[0]) generalSettings = await self.__generalSettingsRepository.getAllAsync() actionCompleted = False if generalSettings.isFuntoonApiEnabled(): if await self.__funtoonRepository.pkmnBattle( userThatRedeemed=userNameThatRedeemed, userToBattle=opponentUserName, twitchChannel=twitchUser.getHandle()): actionCompleted = True if not actionCompleted and generalSettings.isFuntoonTwitchChatFallbackEnabled( ): await twitchUtils.safeSend( twitchChannel, f'!battle {userNameThatRedeemed} {opponentUserName}') actionCompleted = True self.__timber.log( 'PkmnBattleRedemption', f'Redeemed pkmn battle for {userNameThatRedeemed}:{userIdThatRedeemed} in {twitchUser.getHandle()}' ) return actionCompleted
def __init__(self, jsonContents: Dict[str, Any], generalSettingsFile: str): if not utils.hasItems(jsonContents): raise ValueError( f'jsonContents argument is malformed: \"{jsonContents}\"') elif not utils.isValidStr(generalSettingsFile): raise ValueError( f'generalSettingsFile argument is malformed: \"{generalSettingsFile}\"' ) self.__jsonContents: Dict[str, Any] = jsonContents self.__generalSettingsFile: str = generalSettingsFile
def __init__(self, jsonContents: Dict[str, Any], authRepositoryFile: str): if not utils.hasItems(jsonContents): raise ValueError( f'jsonContents argument is malformed: \"{jsonContents}\"') elif not utils.isValidStr(authRepositoryFile): raise ValueError( f'authRepositoryFile argument is malformed: \"{authRepositoryFile}\"' ) self.__jsonContents: Dict[str, Any] = jsonContents self.__authRepositoryFile: str = authRepositoryFile
def __init__( self, commandNames: List[str], apiName: str ): if not utils.hasItems(commandNames): raise ValueError(f'commandNames argument is malformed: \"{commandNames}\"') elif not utils.isValidStr(apiName): raise ValueError(f'apiName argument is malformed: \"{apiName}\"') self.__commandNames = commandNames self.__apiName = apiName
def getLocalLeaderboard(self, entries: List[CutenessEntry], delimiter: str) -> str: if not utils.hasItems(entries): raise ValueError(f'entries argument is malformed: \"{entries}\"') elif delimiter is None: raise ValueError( f'delimiter argument is malformed: \"{delimiter}\"') entryStrings: List[str] = list() for entry in entries: entryStrings.append(self.getLocalLeaderboardPlacement(entry)) return delimiter.join(entryStrings)
def __getEnName(jsonResponse: Dict) -> str: if jsonResponse is None: raise ValueError(f'jsonResponse argument is malformed: \"{jsonResponse}\"') names = jsonResponse['names'] if not utils.hasItems(names): raise ValueError(f'\"names\" field in JSON response is empty: {jsonResponse}') for name in names: if name['language']['name'] == 'en': return name['name'].capitalize() raise RuntimeError(f'can\'t find \"en\" language name in \"names\" field: {names}')
def __init__(self, definitions: List[str], furigana: str, url: str, word: str): if not utils.hasItems(definitions): raise ValueError( f'definitions argument is malformed: \"{definitions}\"') elif not utils.isValidUrl(url): raise ValueError(f'url argument is malformed: \"{url}\"') elif not utils.isValidStr(word): raise ValueError(f'word argument is malformed: \"{word}\"') self.__definitions = definitions self.__furigana = furigana self.__url = url self.__word = word
def getGlobalSuperTriviaGameControllers(self) -> List[str]: gameControllersJson: List[str] = self.__jsonContents.get( 'superTriviaGameControllers') if not utils.hasItems(gameControllersJson): return None gameControllers: List[str] = list() for gameController in gameControllersJson: if utils.isValidStr(gameController): gameControllers.append(gameController) gameControllers.sort(key=lambda gameController: gameController.lower()) return gameControllers
def __init__( self, pokedexId: int, pokepediaTypes: Dict[PokepediaGeneration, PokepediaType], name: str ): if not utils.isValidNum(pokedexId): raise ValueError(f'pokedexId argument is malformed: \"{pokedexId}\"') if not utils.hasItems(pokepediaTypes): raise ValueError(f'pokepediaTypes argument is malformed: \"{pokepediaTypes}\"') elif not utils.isValidStr(name): raise ValueError(f'name argument is malformed: \"{name}\"') self.__pokedexId = pokedexId self.__pokepediaTypes = pokepediaTypes self.__name = name
def __fetchWotd(self, languageEntry: LanguageEntry) -> Wotd: if languageEntry is None: raise ValueError(f'languageEntry argument is malformed: \"{languageEntry}\"') cacheValue = self.__cache[languageEntry] if cacheValue is not None: return cacheValue print(f'Refreshing Word Of The Day for \"{languageEntry.getApiName()}\"... ({utils.getNowTimeText()})') ############################################################################## # retrieve word of the day from https://www.transparent.com/word-of-the-day/ # ############################################################################## rawResponse = None try: rawResponse = requests.get( url = f'https://wotd.transparent.com/rss/{languageEntry.getApiName()}-widget.xml?t=0', timeout = utils.getDefaultTimeout() ) except (ConnectionError, HTTPError, MaxRetryError, NewConnectionError, Timeout) as e: print(f'Exception occurred when attempting to fetch Word Of The Day for \"{languageEntry.getApiName()}\": {e}') raise RuntimeError(f'Exception occurred when attempting to fetch Word Of The Day for \"{languageEntry.getApiName()}\": {e}') xmlTree = xmltodict.parse(rawResponse.content) if not utils.hasItems(xmlTree): print(f'xmlTree for \"{languageEntry.getApiName()}\" is malformed: {xmlTree}') raise RuntimeError(f'xmlTree for \"{languageEntry.getApiName()}\" is malformed: {xmlTree}') wordsTree = xmlTree['xml']['words'] word = wordsTree['word'] definition = wordsTree['translation'] englishExample = wordsTree.get('enphrase') foreignExample = wordsTree.get('fnphrase') languageName = wordsTree['langname'] transliteration = wordsTree.get('wotd:transliteratedWord') return Wotd( languageEntry = languageEntry, word = word, definition = definition, englishExample = englishExample, languageName = languageName, foreignExample = foreignExample, transliteration = transliteration )
def __parseCutenessBoosterPacksFromJson( self, jsonList: List[Dict]) -> List[CutenessBoosterPack]: if not utils.hasItems(jsonList): return None cutenessBoosterPacks: List[CutenessBoosterPack] = list() for cutenessBoosterPackJson in jsonList: cutenessBoosterPacks.append( CutenessBoosterPack( amount=utils.getIntFromDict(cutenessBoosterPackJson, 'amount'), rewardId=utils.getStrFromDict(cutenessBoosterPackJson, 'rewardId'))) cutenessBoosterPacks.sort(key=lambda pack: pack.getAmount()) return cutenessBoosterPacks
def __findAndCreateUser(self, handle: str, jsonContents: Dict[str, Any]) -> User: if not utils.isValidStr(handle): raise ValueError(f'handle argument is malformed: \"{handle}\"') elif not utils.hasItems(jsonContents): raise ValueError( f'jsonContents argument is malformed: \"{jsonContents}\"') handle = handle.lower() for key in jsonContents: if key.lower() == handle: return self.__createUser(handle, jsonContents[key]) raise RuntimeError( f'Unable to find user with handle \"{handle}\" in users repository file: \"{self.__usersFile}\"' )
def toStr(self, includePrices: bool = False, inStockProductsOnly: bool = True, delimiter: str = ', ') -> str: if includePrices is None: raise ValueError( f'includePrices argument is malformed: \"{includePrices}\"') elif inStockProductsOnly is None: raise ValueError( f'inStockProductsOnly argument is malformed: \"{inStockProductsOnly}\"' ) elif delimiter is None: raise ValueError( f'delimiter argument is malformed: \"{delimiter}\"') if not self.hasProducts(): return '🍃 Analogue store is empty' productStrings = list() for product in self.__products: if inStockProductsOnly: if product.inStock(): productStrings.append( product.toStr(includePrice=includePrices, includeStockInfo=False)) else: productStrings.append( product.toStr(includePrice=includePrices, includeStockInfo=True)) if inStockProductsOnly and not utils.hasItems(productStrings): return '🍃 Analogue store has nothing in stock' productsString = delimiter.join(productStrings) if inStockProductsOnly: return f'Analogue products in stock: {productsString}' else: return f'Analogue products: {productsString}'
def __parsePkmnCatchBoosterPacksFromJson( self, jsonList: List[Dict]) -> List[PkmnCatchBoosterPack]: if not utils.hasItems(jsonList): return None pkmnCatchBoosterPacks: List[PkmnCatchBoosterPack] = list() for pkmnCatchBoosterPackJson in jsonList: pkmnCatchTypeStr = utils.getStrFromDict(d=pkmnCatchBoosterPackJson, key='catchType', fallback='') pkmnCatchType: PkmnCatchType = None if utils.isValidStr(pkmnCatchTypeStr): pkmnCatchType = PkmnCatchType.fromStr(pkmnCatchTypeStr) pkmnCatchBoosterPacks.append( PkmnCatchBoosterPack(pkmnCatchType=pkmnCatchType, rewardId=utils.getStrFromDict( pkmnCatchBoosterPackJson, 'rewardId'))) return pkmnCatchBoosterPacks
def hasAlerts(self) -> bool: return utils.hasItems(self.__alerts)
def __refreshStoreStock(self) -> AnalogueStoreStock: print(f'Refreshing Analogue store stock... ({utils.getNowTimeText()})') rawResponse = None try: rawResponse = requests.get(url=self.__storeUrl, timeout=utils.getDefaultTimeout()) except (ConnectionError, HTTPError, MaxRetryError, NewConnectionError, Timeout) as e: print( f'Exception occurred when attempting to fetch Analogue store stock: {e}' ) raise RuntimeError( f'Exception occurred when attempting to fetch Analogue store stock: {e}' ) htmlTree = html.fromstring(rawResponse.content) if htmlTree is None: print(f'Analogue store\'s htmlTree is malformed: \"{htmlTree}\"') raise ValueError( f'Analogue store\'s htmlTree is malformed: \"{htmlTree}\"') productTrees = htmlTree.find_class('store_product-header__1rLY-') if not utils.hasItems(productTrees): print( f'Analogue store\'s productTrees list is malformed: \"{productTrees}\"' ) raise ValueError( f'Analogue store\'s productTrees list is malformed: \"{productTrees}\"' ) products = list() for productTree in productTrees: productTrees = productTree.find_class('store_title__3eCzb') if productTrees is None or len(productTrees) != 1: continue nameAndPrice = utils.cleanStr(productTrees[0].text_content()) if len(nameAndPrice) == 0: continue elif '8BitDo'.lower() in nameAndPrice.lower(): # don't show 8BitDo products in the final stock listing continue name = None price = None indexOfDollar = nameAndPrice.find('$') if indexOfDollar == -1: name = utils.cleanStr(nameAndPrice) else: name = utils.cleanStr(nameAndPrice[0:indexOfDollar]) price = utils.cleanStr( nameAndPrice[indexOfDollar:len(nameAndPrice)]) if name[len(name) - 1] == '1': name = name[0:len(name) - 1] productType = AnalogueProductType.fromStr(name) inStock = True outOfStockElement = productTree.find_class( 'button_Disabled__2CEbR') if utils.hasItems(outOfStockElement): inStock = False products.append( AnalogueStoreEntry(productType=productType, inStock=inStock, name=name, price=price)) return AnalogueStoreStock(products=products)
def hasProducts(self) -> bool: return utils.hasItems(self.__products)
def search(self, query: str) -> JishoResult: if not utils.isValidStr(query): raise ValueError(f'query argument is malformed: \"{query}\"') query = query.strip() print(f'Looking up \"{query}\"... ({utils.getNowTimeText()})') encodedQuery = urllib.parse.quote(query) requestUrl = f'https://jisho.org/search/{encodedQuery}' rawResponse = None try: rawResponse = requests.get(url=requestUrl, timeout=utils.getDefaultTimeout()) except (ConnectionError, HTTPError, MaxRetryError, NewConnectionError, Timeout) as e: print( f'Exception occurred when attempting to search Jisho for \"{query}\": {e}' ) raise RuntimeError( f'Exception occurred when attempting to search Jisho for \"{query}\": {e}' ) htmlTree = html.fromstring(rawResponse.content) if htmlTree is None: print( f'Exception occurred when attempting to decode Jisho\'s response for \"{query}\" into HTML tree' ) raise RuntimeError( f'Exception occurred when attempting to decode Jisho\'s response for \"{query}\" into HTML tree' ) parentElements = htmlTree.find_class('concept_light-representation') if not utils.hasItems(parentElements): print( f'Exception occurred when attempting to find parent elements in Jisho\'s HTML tree in query for \"{query}\"' ) raise ValueError( f'Exception occurred when attempting to find parent elements in Jisho\'s HTML tree in query for \"{query}\"' ) textElements = parentElements[0].find_class('text') if textElements is None or len(textElements) != 1: print( f'Exception occurred when attempting to find text elements in Jisho\'s HTML tree in query for \"{query}\"' ) raise ValueError( f'Exception occurred when attempting to find text elements in Jisho\'s HTML tree in query for \"{query}\"' ) word = utils.cleanStr(textElements[0].text_content()) if not utils.isValidStr(word): print( f'Exception occurred when checking that Jisho\'s word is valid in query for \"{query}\"' ) raise ValueError( f'Exception occurred when checking that Jisho\'s word is valid in query for \"{query}\"' ) definitionElements = htmlTree.find_class('meaning-meaning') if not utils.hasItems(definitionElements): print( f'Exception occurred when attempting to find definition elements in Jisho\'s HTML tree in query for \"{query}\"' ) raise ValueError( f'Exception occurred when attempting to find definition elements in Jisho\'s HTML tree in query for \"{query}\"' ) definitions = list() for definitionElement in definitionElements: breakUnitElements = definitionElement.find_class('break-unit') if breakUnitElements is None or len(breakUnitElements) != 0: continue definition = utils.cleanStr(definitionElement.text_content()) if not utils.isValidStr(definition): continue number = locale.format_string("%d", len(definitions) + 1, grouping=True) definitions.append(f'#{number} {definition}') if len(definitions) >= self.__definitionsMaxSize: # keep from adding tons of definitions break if not utils.hasItems(definitions): print(f'Unable to find any viable definitions for \"{query}\"') raise ValueError( f'Unable to find any viable definitions for \"{query}\"') furigana = None furiganaElements = htmlTree.find_class('furigana') if utils.hasItems(furiganaElements): furigana = utils.cleanStr(furiganaElements[0].text_content()) return JishoResult(definitions=definitions, furigana=furigana, url=requestUrl, word=word)
def __init__(self, entries: List[LanguageEntry]): if not utils.hasItems(entries): raise ValueError(f'entries argument is malformed: \"{entries}\"') self.__entries = entries
def search(self, query: str) -> EnEsDictionaryResult: if not utils.isValidStr(query): raise ValueError(f'query argument is malformed: \"{query}\"') query = query.strip() print(f'Looking up \"{query}\"... ({utils.getNowTimeText()})') encodedQuery = urllib.parse.quote(query) requestUrl = 'https://www.dictionaryapi.com/api/v3/references/spanish/json/{}?key={}'.format( encodedQuery, self.__merriamWebsterApiKey) rawResponse = None try: rawResponse = requests.get(url=requestUrl, timeout=utils.getDefaultTimeout()) except (ConnectionError, HTTPError, MaxRetryError, NewConnectionError, Timeout) as e: print( f'Exception occurred when attempting to search Merriam Webster for \"{query}\": {e}' ) raise RuntimeError( f'Exception occurred when attempting to search Merriam Webster for \"{query}\": {e}' ) jsonResponse = None try: jsonResponse = rawResponse.json() except JSONDecodeError as e: print( f'Exception occurred when attempting to decode Merriam Webster\'s response into JSON for \"{query}\": {e}' ) raise RuntimeError( f'Exception occurred when attempting to decode Merriam Webster\'s response into JSON for \"{query}\": {e}' ) if not utils.hasItems(jsonResponse): print( f'jsonResponse for \"{query}\" has no definitions: {jsonResponse}' ) raise ValueError( f'jsonResponse \"{query}\" has no definitions: {jsonResponse}') definitions = list() for entry in jsonResponse: definition = None if isinstance(entry, str): definition = entry elif not entry['meta'].get('offensive', True) and utils.hasItems( entry['shortdef']): definition = entry['shortdef'][0] definition = utils.cleanStr(definition) if not utils.isValidStr(definition): continue index = 0 add = True while (index < len(definitions) and add): if definitions[index].lower() == definition.lower(): add = False index = index + 1 if add: number = locale.format_string("%d", len(definitions) + 1, grouping=True) definitions.append(f'#{number} {definition}') if len(definitions) >= self.__definitionsMaxSize: # keep from adding tons of definitions break if not utils.hasItems(definitions): print(f'Unable to find any viable definitions for \"{query}\"') raise ValueError( f'Unable to find any viable definitions for \"{query}\"') return EnEsDictionaryResult(definitions=definitions, word=query)
def hasConditions(self) -> bool: return utils.hasItems(self.__conditions)
def hasTomorrowsConditions(self) -> bool: return utils.hasItems(self.__tomorrowsConditions)
async def __getSubscribeReadyPubSubEntries(self) -> List[PubSubEntry]: twitchHandles = await self.__twitchTokensRepository.getExpiringTwitchHandles( ) authSnapshot = await self.__authRepository.getAllAsync() users: List[User] = None if twitchHandles is None: # if twitchHandles is None, then we must do a full validate and refresh users = await self.__usersRepository.getUsersAsync() elif len(twitchHandles) == 0: # if twitchHandles is empty, then there is no need to do a validate or refresh of anyone return None else: # if twitchHandles has entries, then we will validate and refresh those specific users users = list() for twitchHandle in twitchHandles: users.append(await self.__usersRepository.getUserAsync(twitchHandle)) usersAndTwitchTokens: Dict[User, str] = dict() for user in users: twitchAccessToken = await self.__twitchTokensRepository.getAccessToken( user.getHandle()) if utils.isValidStr(twitchAccessToken): usersAndTwitchTokens[user] = twitchAccessToken if not utils.hasItems(usersAndTwitchTokens): return None usersToRemove: List[User] = list() for user in usersAndTwitchTokens: try: await self.__twitchTokensRepository.validateAndRefreshAccessToken( twitchClientId=authSnapshot.requireTwitchClientId(), twitchClientSecret=authSnapshot.requireTwitchClientSecret( ), twitchHandle=user.getHandle()) usersAndTwitchTokens[ user] = await self.__twitchTokensRepository.getAccessToken( user.getHandle()) except (TwitchAccessTokenMissingException, TwitchExpiresInMissingException, TwitchJsonException, TwitchNetworkException, TwitchRefreshTokenMissingException) as e: # if we run into this error, that most likely means that this user changed # their password usersToRemove.append(user) self.__timber.log( 'CynanBot', f'Failed to validate and refresh access Twitch token for {user.getHandle()}: {e}' ) if utils.hasItems(usersToRemove): for user in usersToRemove: del usersAndTwitchTokens[user] pubSubEntries: List[PubSubEntry] = list() for user in usersAndTwitchTokens: twitchAccessToken = usersAndTwitchTokens[user] userId = await self.__userIdsRepository.fetchUserIdAsInt( userName=user.getHandle(), twitchAccessToken=twitchAccessToken, twitchClientId=authSnapshot.requireTwitchClientId()) pubSubEntries.append( PubSubEntry( userId=userId, userName=user.getHandle().lower(), topic=pubsub.channel_points(twitchAccessToken)[userId])) return pubSubEntries