class LeagueRankModule(BaseModule): ID = __name__.split('.')[-1] NAME = 'LeagueRank module' DESCRIPTION = 'Enable this to check the rank of others in League of Legends in the chat.' SETTINGS = [ ModuleSetting( key='riot_api_key', label='Riot developer api key', type='text', required=True, placeholder='i.e. 1e3415de-1234-5432-f331-67abb0454d12', default=''), ModuleSetting( key='default_summoner', label='Default summoner name', type='text', required=True, placeholder='i.e. moregainfreeman (remove space)', default=''), ModuleSetting( key='default_region', label='Default region', type='text', required=True, placeholder='i.e. euw/eune/na/br/oce', default=''), ] def load_commands(self, **options): # TODO: Aliases should be set in settings? # This way, it can be run alongside other modules self.commands['rank'] = Command.raw_command(self.league_rank, delay_all=15, delay_user=30, description='Check streamer\'s or other players League of Legends rank in chat.', examples=[ CommandExample(None, 'Check streamer\'s rank', chat='user:!rank\n' 'bot: The Summoner Moregain Freeman on region EUW is currently in PLATINUM IV with 62 LP 4Head', description='Bot says broadcaster\'s region, League-tier, division and LP').parse(), CommandExample(None, 'Check other player\'s rank on default region', chat='user:!rank forsen\n' 'bot: The Summoner forsen on region EUW is currently in SILVER IV with 36 LP 4Head', description='Bot says player\'s region, League-tier, division and LP').parse(), CommandExample(None, 'Check other player\'s rank on another region', chat='user:!rank imaqtpie na\n' 'bot: The Summoner Imaqtpie on region NA is currently in CHALLENGER I with 441 LP 4Head', description='Bot says player\'s region, League-tier, division and LP. Other regions to use as arguments: euw, eune, na, oce, br, kr, las, lan, ru, tr').parse(), ], ) self.commands['lolrank'] = self.commands['rank'] self.commands['ranklol'] = self.commands['rank'] self.commands['leaguerank'] = self.commands['rank'] def league_rank(self, **options): try: from riotwatcher import RiotWatcher, LoLException except ImportError: log.error('Missing required module for League Rank module: riotwatcher') return False source = options['source'] message = options['message'] bot = options['bot'] riot_api_key = self.settings['riot_api_key'] summoner_name = self.settings['default_summoner'] def_region = self.settings['default_region'] region_list = ['br', 'eune', 'euw', 'kr', 'lan', 'las', 'na', 'oce', 'ru', 'tr'] if message: summoner_name = message.split()[0] try: region = message.split()[1].lower() except IndexError: region = def_region.lower() if region not in region_list: bot.whisper(source.username, 'Region is not valid. Please enter a valid region, region is optional and the default region is {}'.format(def_region.upper())) return False else: pass else: region = def_region.lower() error_404 = "Game data not found" error_429 = "Too many requests" try: rw = RiotWatcher(riot_api_key, default_region=region) summoner = rw.get_summoner(name=summoner_name) summoner_id = str(summoner['id']) summoner_name = summoner['name'] except LoLException as e: if e == error_429: bot.say('Too many requests. Try again in {} seconds'.format(e.headers['Retry-After'])) return False elif e == error_404: bot.say('The summoner not found. Use a valid summoner name (remove spaces) and region FailFish') return False try: summoner_league = rw.get_league_entry(summoner_ids=(summoner_id, )) tier = summoner_league[summoner_id][0]['tier'] division = summoner_league[summoner_id][0]['entries'][0]['division'] league_points = summoner_league[summoner_id][0]['entries'][0]['leaguePoints'] bot.say('The Summoner {} on region {} is currently in {} {} with {} LP 4Head'.format(summoner_name, region.upper(), tier, division, league_points)) except LoLException as e: if e == error_429: bot.say('Too many requests. Try again in {} seconds'.format(e.headers['Retry-After'])) return False elif e == error_404: bot.say('The Summoner {} on region {} is currently UNRANKED.. FeelsBadMan'.format(summoner_name, region.upper())) return False else: bot.say('Trouble fetching summoner rank.. Kappa Try again later!') return False
class LeagueRankModule(BaseModule): ID = __name__.split(".")[-1] NAME = "LeagueRank module" DESCRIPTION = "Enable this to check the rank of others in League of Legends in the chat." CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="riot_api_key", label="Riot developer api key", type="text", required=True, placeholder="i.e. 1e3415de-1234-5432-f331-67abb0454d12", default="", ), ModuleSetting( key="default_summoner", label="Default summoner name", type="text", required=True, placeholder="i.e. moregainfreeman (remove space)", default="", ), ModuleSetting( key="default_region", label="Default region", type="text", required=True, placeholder="i.e. euw/eune/na/br/oce", default="", ), ModuleSetting( key="online_global_cd", label="Global cooldown (seconds)", type="number", required=True, placeholder="", default=15, constraints={ "min_value": 0, "max_value": 120 }, ), ModuleSetting( key="online_user_cd", label="Per-user cooldown (seconds)", type="number", required=True, placeholder="", default=30, constraints={ "min_value": 0, "max_value": 240 }, ), ] def load_commands(self, **options): self.commands["lolrank"] = Command.raw_command( self.league_rank, delay_all=self.settings["online_global_cd"], delay_user=self.settings["online_user_cd"], description= "Check streamer's or other players League of Legends rank in chat.", examples=[ CommandExample( None, "Check streamer's rank", chat="user:!rank\n" "bot: The Summoner Moregain Freeman on region EUW is currently in PLATINUM IV with 62 LP 4Head", description= "Bot says broadcaster's region, League-tier, division and LP", ).parse(), CommandExample( None, "Check other player's rank on default region", chat="user:!rank forsen\n" "bot: The Summoner forsen on region EUW is currently in SILVER IV with 36 LP 4Head", description= "Bot says player's region, League-tier, division and LP", ).parse(), CommandExample( None, "Check other player's rank on another region", chat="user:!rank imaqtpie na\n" "bot: The Summoner Imaqtpie on region NA is currently in CHALLENGER I with 441 LP 4Head", description= "Bot says player's region, League-tier, division and LP. Other regions to use as arguments: euw, eune, na, oce, br, kr, las, lan, ru, tr", ).parse(), ], ) self.commands["ranklol"] = self.commands["lolrank"] self.commands["leaguerank"] = self.commands["lolrank"] def league_rank(self, bot, source, message, **rest): try: from riotwatcher import RiotWatcher, LoLException except ImportError: log.error( "Missing required module for League Rank module: riotwatcher") return False riot_api_key = self.settings["riot_api_key"] summoner_name = self.settings["default_summoner"] def_region = self.settings["default_region"] if len(riot_api_key) == 0: log.error("Missing riot API key in settings.") return False region_list = [ "br", "eune", "euw", "kr", "lan", "las", "na", "oce", "ru", "tr" ] if message: summoner_name = message.split()[0] try: region = message.split()[1].lower() except IndexError: region = def_region.lower() if region not in region_list: bot.whisper( source, f"Region is not valid. Please enter a valid region, region is optional and the default region is {def_region.upper()}", ) return False else: pass else: region = def_region.lower() if len(summoner_name) == 0 or len(region) == 0: return False error_404 = "Game data not found" error_429 = "Too many requests" try: rw = RiotWatcher(riot_api_key, default_region=region) summoner = rw.get_summoner(name=summoner_name) summoner_id = str(summoner["id"]) summoner_name = summoner["name"] except LoLException as e: if e == error_429: bot.say( f"Too many requests. Try again in {e.headers['Retry-After']} seconds" ) return False elif e == error_404: bot.say( "The summoner not found. Use a valid summoner name (remove spaces) and region FailFish" ) return False else: log.info(f"Something unknown went wrong: {e}") return False try: summoner_league = rw.get_league_entry(summoner_ids=(summoner_id, )) tier = summoner_league[summoner_id][0]["tier"] division = summoner_league[summoner_id][0]["entries"][0][ "division"] league_points = summoner_league[summoner_id][0]["entries"][0][ "leaguePoints"] bot.say( f"The Summoner {summoner_name} on region {region.upper()} is currently in {tier} {division} with {league_points} LP 4Head" ) except LoLException as e: if e == error_429: bot.say( f"Too many requests. Try again in {e.headers['Retry-After']} seconds" ) return False elif e == error_404: bot.say( f"The Summoner {summoner_name} on region {region.upper()} is currently UNRANKED.. FeelsBadMan" ) return False else: bot.say( "Trouble fetching summoner rank.. Kappa Try again later!") return False
class DuelModule(BaseModule): ID = __name__.split('.')[-1] NAME = 'Duel (mini game)' DESCRIPTION = 'Let players duel to win or lose points.' CATEGORY = 'Game' SETTINGS = [ ModuleSetting(key='max_pot', label='How many points you can duel for at most', type='number', required=True, placeholder='', default=420, constraints={ 'min_value': 0, 'max_value': 69000, }), ModuleSetting( key='message_won', label='Winner message | Available arguments: {winner}, {loser}', type='text', required=True, placeholder='{winner} won the duel vs {loser} PogChamp', default='{winner} won the duel vs {loser} PogChamp', constraints={ 'min_str_len': 10, 'max_str_len': 400, }), ModuleSetting( key='message_won_points', label= 'Points message | Available arguments: {winner}, {loser}, {total_pot}, {extra_points}', type='text', required=True, placeholder= '{winner} won the duel vs {loser} PogChamp . The pot was {total_pot}, the winner gets their bet back + {extra_points} points', default= '{winner} won the duel vs {loser} PogChamp . The pot was {total_pot}, the winner gets their bet back + {extra_points} points', constraints={ 'min_str_len': 10, 'max_str_len': 400, }), ModuleSetting(key='online_global_cd', label='Global cooldown (seconds)', type='number', required=True, placeholder='', default=0, constraints={ 'min_value': 0, 'max_value': 120, }), ModuleSetting(key='online_user_cd', label='Per-user cooldown (seconds)', type='number', required=True, placeholder='', default=5, constraints={ 'min_value': 0, 'max_value': 240, }), ModuleSetting(key='show_on_clr', label='Show duels on the clr overlay', type='boolean', required=True, default=True), ] def load_commands(self, **options): self.commands['duel'] = pajbot.models.command.Command.raw_command( self.initiate_duel, delay_all=self.settings['online_global_cd'], delay_user=self.settings['online_user_cd'], description='Initiate a duel with a user', examples=[ pajbot.models.command.CommandExample( None, '0-point duel', chat='user:!duel Karl_Kons\n' 'bot>user:You have challenged Karl_Kons for 0 points', description='Duel Karl_Kons for 0 points').parse(), pajbot.models.command.CommandExample( None, '69-point duel', chat='user:!duel Karl_Kons 69\n' 'bot>user:You have challenged Karl_Kons for 69 points', description='Duel Karl_Kons for 69 points').parse(), ], ) self.commands[ 'cancelduel'] = pajbot.models.command.Command.raw_command( self.cancel_duel, delay_all=0, delay_user=10, description='Cancel your duel request') self.commands['accept'] = pajbot.models.command.Command.raw_command( self.accept_duel, delay_all=0, delay_user=0, description='Accept a duel request') self.commands['decline'] = pajbot.models.command.Command.raw_command( self.decline_duel, delay_all=0, delay_user=0, description='Decline a duel request') self.commands['deny'] = self.commands['decline'] self.commands[ 'duelstatus'] = pajbot.models.command.Command.raw_command( self.status_duel, delay_all=0, delay_user=5, description='Current duel request info') self.commands['duelstats'] = pajbot.models.command.Command.raw_command( self.get_duel_stats, delay_all=0, delay_user=120, description='Get your duel statistics') def __init__(self): super().__init__() self.duel_requests = {} self.duel_request_price = {} self.duel_targets = {} def initiate_duel(self, **options): """ Initiate a duel with a user. You can also bet points on the winner. By default, the maximum amount of points you can spend is 420. How to add: !add funccommand duel initiate_duel --cd 0 --usercd 5 How to use: !duel USERNAME POINTS_TO_BET """ bot = options['bot'] source = options['source'] message = options['message'] if message is None: return False max_pot = self.settings['max_pot'] msg_split = message.split() username = msg_split[0] user = bot.users.find(username) duel_price = 0 if user is None: # No user was found with this username return False if len(msg_split) > 1: try: duel_price = int(msg_split[1]) if duel_price < 0: return False if duel_price > max_pot: duel_price = max_pot except ValueError: pass if source.username in self.duel_requests: bot.whisper( source.username, 'You already have a duel request active with {}. Type !cancelduel to cancel your duel request.' .format(self.duel_requests[source.username])) return False if user == source: # You cannot duel yourself return False if user.last_active is None or ( datetime.datetime.now() - user._last_active).total_seconds() > 5 * 60: bot.whisper( source.username, 'This user has not been active in chat within the last 5 minutes. Get them to type in chat before sending another challenge' ) return False if not user.can_afford(duel_price) or not source.can_afford( duel_price): bot.whisper( source.username, 'You or your target do not have more than {} points, therefore you cannot duel for that amount.' .format(duel_price)) return False if user.username in self.duel_targets: bot.whisper( source.username, 'This person is already being challenged by {}. Ask them to answer the offer by typing !deny or !accept' .format(self.duel_targets[user.username])) return False self.duel_targets[user.username] = source.username self.duel_requests[source.username] = user.username self.duel_request_price[source.username] = duel_price bot.whisper( user.username, 'You have been challenged to a duel by {} for {} points. You can either !accept or !deny this challenge.' .format(source.username_raw, duel_price)) bot.whisper( source.username, 'You have challenged {} for {} points'.format( user.username_raw, duel_price)) def cancel_duel(self, **options): """ Cancel any duel requests you've sent. How to add: !add funccomand cancelduel|duelcancel cancel_duel --cd 0 --usercd 10 How to use: !cancelduel """ bot = options['bot'] source = options['source'] if source.username not in self.duel_requests: bot.whisper(source.username, 'You have not sent any duel requests') return bot.whisper( source.username, 'You have cancelled the duel vs {}'.format( self.duel_requests[source.username])) del self.duel_targets[self.duel_requests[source.username]] del self.duel_requests[source.username] def accept_duel(self, **options): """ Accepts any active duel requests you've received. How to add: !add funccommand accept accept_duel --cd 0 --usercd 0 How to use: !accept """ bot = options['bot'] source = options['source'] duel_tax = 0.3 # 30% tax if source.username not in self.duel_targets: bot.whisper(source.username, 'You are not being challenged to a duel by anyone.') return requestor = bot.users[self.duel_targets[source.username]] duel_price = self.duel_request_price[self.duel_targets[ source.username]] if not source.can_afford(duel_price) or not requestor.can_afford( duel_price): bot.whisper( source.username, 'Your duel request with {} was cancelled due to one of you not having enough points.' .format(requestor.username_raw)) bot.whisper( requestor.username, 'Your duel request with {} was cancelled due to one of you not having enough points.' .format(source.username_raw)) del self.duel_requests[self.duel_targets[source.username]] del self.duel_targets[source.username] return False source.points -= duel_price requestor.points -= duel_price winning_pot = int(duel_price * (1.0 - duel_tax)) participants = [source, requestor] winner = random.choice(participants) participants.remove(winner) loser = participants.pop() winner.points += duel_price winner.points += winning_pot winner.save() loser.save() DuelManager.user_won(winner, winning_pot) DuelManager.user_lost(loser, duel_price) arguments = { 'winner': winner.username, 'loser': loser.username, 'total_pot': duel_price, 'extra_points': winning_pot, } if duel_price > 0: message = self.get_phrase('message_won_points', **arguments) if duel_price >= 500 and self.settings['show_on_clr']: bot.websocket_manager.emit( 'notification', { 'message': '{} won the duel vs {}'.format(winner.username_raw, loser.username_raw) }) else: message = self.get_phrase('message_won', **arguments) bot.say(message) del self.duel_requests[self.duel_targets[source.username]] del self.duel_targets[source.username] HandlerManager.trigger('on_duel_complete', winner, loser, winning_pot, duel_price) def decline_duel(self, **options): """ Declines any active duel requests you've received. How to add: !add funccommand deny|decline decline_duel --cd 0 --usercd 0 How to use: !decline """ bot = options['bot'] source = options['source'] if source.username not in self.duel_targets: bot.whisper(source.username, 'You are not being challenged to a duel') return False requestor_username = self.duel_targets[source.username] bot.whisper( source.username, 'You have declined the duel vs {}'.format(requestor_username)) bot.whisper( requestor_username, '{} declined the duel challenge with you.'.format( source.username_raw)) del self.duel_targets[source.username] del self.duel_requests[requestor_username] def status_duel(self, **options): """ Whispers you the current status of your active duel requests/duel targets How to add: !add funccommand duelstatus|statusduel status_duel --cd 0 --usercd 5 How to use: !duelstatus """ bot = options['bot'] source = options['source'] msg = [] if source.username in self.duel_requests: msg.append('You have a duel request for {} points by {}'.format( self.duel_request_price[source.username], self.duel_requests[source.username])) if source.username in self.duel_targets: msg.append( 'You have a pending duel request from {} for {} points'.format( self.duel_targets[source.username], self.duel_request_price[self.duel_targets[ source.username]])) if len(msg) > 0: bot.whisper(source.username, '. '.join(msg)) else: bot.whisper( source.username, 'You have no duel request or duel target. Type !duel USERNAME POT to duel someone!' ) def get_duel_stats(self, **options): """ Whispers the users duel winratio to the user """ bot = options['bot'] source = options['source'] with DBManager.create_session_scope( expire_on_commit=False) as db_session: db_session.add(source.user_model) if source.duel_stats is None: bot.whisper(source.username, 'You have no recorded duels.') return True bot.whisper( source.username, 'duels: {ds.duels_total} winrate: {ds.winrate:.2f}% streak: {ds.current_streak} profit: {ds.profit}' .format(ds=source.duel_stats))
class EmoteTimeoutModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Emote timeout" DESCRIPTION = "Times out users who post emotes from Twitch, BTTV or FFZ" CATEGORY = "Filter" SETTINGS = [ ModuleSetting(key="timeout_twitch", label="Timeout any twitch emotes", type="boolean", required=True, default=False), ModuleSetting(key="timeout_ffz", label="Timeout any FFZ emotes", type="boolean", required=True, default=False), ModuleSetting(key="timeout_bttv", label="Timeout any BTTV emotes", type="boolean", required=True, default=False), ModuleSetting( key="bypass_level", label="Level to bypass module", type="number", required=True, placeholder="", default=420, constraints={ "min_value": 100, "max_value": 1000 }, ), ModuleSetting( key="moderation_action", label="Moderation action to apply", type="options", required=True, default="Delete", options=["Delete", "Timeout"], ), ModuleSetting( key="timeout_duration", label="Timeout duration (if moderation action is timeout)", type="number", required=True, placeholder="", default=5, constraints={ "min_value": 3, "max_value": 120 }, ), ModuleSetting(key="online_chat_only", label="Only enabled in online chat", type="boolean", required=True, default=True), ] def delete_or_timeout(self, user, msg_id, reason): if self.settings["moderation_action"] == "Delete": self.bot.delete_message(msg_id) elif self.settings["moderation_action"] == "Timeout": self.bot.timeout_user_once(user, self.settings["timeout_duration"], reason) def on_message(self, source, emote_instances, msg_id, **rest): if source.level >= self.settings[ "bypass_level"] or source.moderator is True: return True if self.settings["online_chat_only"] and not self.bot.is_online: return True if self.settings["timeout_twitch"] and any(e.emote.provider == "twitch" for e in emote_instances): self.delete_or_timeout(source, msg_id, "No Twitch emotes allowed") return False if self.settings["timeout_ffz"] and any(e.emote.provider == "ffz" for e in emote_instances): self.delete_or_timeout(source, msg_id, "No FFZ emotes allowed") return False if self.settings["timeout_bttv"] and any(e.emote.provider == "bttv" for e in emote_instances): self.delete_or_timeout(source, msg_id, "No BTTV emotes allowed") return False return True def enable(self, bot): HandlerManager.add_handler("on_message", self.on_message) def disable(self, bot): HandlerManager.remove_handler("on_message", self.on_message)
class PlaySoundTokenCommandModule(BaseModule): ID = 'tokencommand-' + __name__.split('.')[-1] NAME = '!playsound' DESCRIPTION = 'Play a sound on stream' PARENT_MODULE = QuestModule SETTINGS = [ ModuleSetting( key='point_cost', label='Point cost', type='number', required=True, placeholder='Point cost', default=0, constraints={ 'min_value': 0, 'max_value': 999999, }), ModuleSetting( key='token_cost', label='Token cost', type='number', required=True, placeholder='Token cost', default=3, constraints={ 'min_value': 0, 'max_value': 15, }), ModuleSetting( key='sample_cd', label='Cooldown for the same sample (seconds)', type='number', required=True, placeholder='', default=20, constraints={ 'min_value': 5, 'max_value': 120, }), ModuleSetting( key='sub_only', label='Subscribers only', type='boolean', required=True, default=True), ] def __init__(self): super().__init__() self.valid_samples = Samples.valid_samples self.sample_cache = [] def play_sound(self, **options): bot = options['bot'] message = options['message'] source = options['source'] if message: sample = message.split(' ')[0].lower() if sample in self.sample_cache: bot.whisper(source.username, 'The sample {0} was played too recently. Please wait before trying to use it again'.format(sample)) return False if sample == 'random': sample = random.choice(self.valid_samples.keys()) if sample in self.valid_samples: log.debug('Played sound: {0}'.format(sample)) payload = {'sample': sample} bot.websocket_manager.emit('play_sound', payload) if not (source.username == 'pajlada') or True: self.sample_cache.append(sample) bot.execute_delayed(self.settings['sample_cd'], self.sample_cache.remove, ('{0}'.format(sample), )) return True bot.whisper(source.username, 'Your sample is not valid. Check out all the valid samples here: https://pajbot.com/playsounds') return False def load_commands(self, **options): self.commands['#playsound'] = pajbot.models.command.Command.raw_command( self.play_sound, tokens_cost=self.settings['token_cost'], cost=self.settings['point_cost'], sub_only=self.settings['sub_only'], description='Play a sound on stream! Costs {} tokens, sub only for now.'.format(self.settings['token_cost']), can_execute_with_whisper=True, examples=[ pajbot.models.command.CommandExample(None, 'Play the "cumming" sample', chat='user:!#playsound cumming\n' 'bot>user:Successfully played your sample cumming').parse(), pajbot.models.command.CommandExample(None, 'Play the "fuckyou" sample', chat='user:!#playsound fuckyou\n' 'bot>user:Successfully played your sample fuckyou').parse(), ], ) self.commands['#playsound'].long_description = 'Playsounds can be tried out <a href="https://pajbot.com/playsounds">here</a>'
class DotaBetModule(BaseModule): AUTHOR = "DatGuy1" ID = __name__.split(".")[-1] NAME = "DotA Betting" DESCRIPTION = "Enables betting on DotA 2 games with !dotabet" CATEGORY = "Game" SETTINGS = [ ModuleSetting( # Not required key="max_return", label="Maximum return odds", type="number", placeholder="", default="20"), ModuleSetting( # Not required key="min_return", label="Minimum return odds", type="text", placeholder="", default="1.10"), ModuleSetting( key="steam3_id", label="Steam 3 ID of streamer (number only)", type="number", required=True, placeholder="", default=""), ModuleSetting( key="api_key", label="Steam API Key", type="text", required=True, placeholder="", default=""), ] def __init__(self, bot): super().__init__(bot) self.action_queue = ActionQueue() self.action_queue.start() self.bets = {} self.betting_open = False self.message_closed = True self.isRadiant = False self.matchID = 0 self.oldID = 0 self.winPoints = 0 self.lossPoints = 0 self.winBetters = 0 self.lossBetters = 0 self.gettingTeam = False self.secondAttempt = False self.calibrating = True self.calibratingSecond = True self.jobPaused = False self.spectating = False self.job = ScheduleManager.execute_every(25, self.poll_webapi) self.job.pause() self.reminder_job = ScheduleManager.execute_every( 200, self.reminder_bet) self.reminder_job.pause() # self.finish_job = ScheduleManager.execute_every(60, self.get_game) # self.finish_job.pause() # self.close_job = ScheduleManager.execute_every(1200, self.bot.websocket_manager.emit, ("dotabet_close_game", )) # self.close_job.pause() def reminder_bet(self): if self.betting_open: self.bot.me("monkaS 👉 🕒 place your bets people") self.bot.websocket_manager.emit( "notification", { "message": "monkaS 👉 🕒 place your bets people"}) else: if not self.message_closed: winRatio, lossRatio = self.get_odds_ratio(self.winPoints, self.lossPoints) self.bot.me("The betting for the current game has been closed! Winners can expect a {:0.2f} (win betters) or {:0.2f} (loss betters) return " "ratio".format(winRatio, lossRatio)) self.bot.websocket_manager.emit( "notification", { "message": "The betting for the current game has been closed!"}) if not self.spectating: self.bot.execute_delayed( 15, self.bot.websocket_manager.emit, ("dotabet_close_game", )) self.message_closed = True def reinit_params(self): self.winBetters = 0 self.winPoints = 0 self.lossBetters = 0 self.lossPoints = 0 def get_odds_ratio(self, winPoints, lossPoints): solveFormula = lambda x,y: 1.+ (float(x) / (float(y))) winRatio = solveFormula(lossPoints, winPoints) lossRatio = solveFormula(winPoints, lossPoints) ratioList = [winRatio, lossRatio] for c, curRatio in enumerate(ratioList): if self.maxReturn and curRatio > self.maxReturn: ratioList[c] = self.maxReturn if self.minReturn and curRatio < self.minReturn: ratioList[c] = self.minReturn return tuple(ratioList) def spread_points(self, gameResult): winners = 0 losers = 0 total_winnings = 0 total_losings = 0 if gameResult == "win": solveFormula = self.get_odds_ratio(self.winPoints, self.lossPoints)[0] else: solveFormula = self.get_odds_ratio(self.winPoints, self.lossPoints)[1] with DBManager.create_session_scope() as db_session: db_bets = {} for username in self.bets: bet_for_win, betPoints = self.bets[username] points = int(betPoints * solveFormula) user = self.bot.users.find(username, db_session=db_session) if user is None: continue correct_bet = ( gameResult == "win" and bet_for_win is True) or ( gameResult == "loss" and bet_for_win is False) db_bets[username] = DotaBetBet( user.id, "win" if bet_for_win else "loss", betPoints, 0) if correct_bet: winners += 1 total_winnings += points - betPoints db_bets[username].profit = points user.points += points self.bot.whisper( user.username, "You bet {} points on the correct outcome and gained an extra {} points, " "you now have {} points PogChamp".format( betPoints, points - betPoints, user.points)) else: losers += 1 total_losings += betPoints db_bets[username].profit = -betPoints self.bot.whisper( user.username, "You bet {} points on the wrong outcome, so you lost it all. :(".format(betPoints)) startString = "The game ended as a {}. {} users won an extra {} points, while {}" \ " lost {} points. Winners can expect a {:0.2f} return ratio.".format(gameResult, winners, total_winnings, losers, total_losings, solveFormula) if self.spectating: resultString = startString[:20] + "radiant " + startString[20:] else: resultString = startString # for username in db_bets: # bet = db_bets[username] # db_session.add(bet) # db_session.commit() # self.bets = {} self.betting_open = False self.message_closed = True self.reinit_params() bet_game = DotaBetGame( gameResult, total_winnings - total_losings, winners, losers) db_session.add(bet_game) self.bot.websocket_manager.emit( "notification", { "message": resultString, "length": 8}) self.bot.me(resultString) def get_game(self): gameResult = "loss" # log.debug(self.isRadiant) odURL = "https://api.opendota.com/api/players/{}/recentMatches".format( self.settings["steam3_id"]) gameHistory = requests.get(odURL).json()[0] if gameHistory["match_id"] != self.matchID: self.matchID = gameHistory["match_id"] if self.calibrating: self.calibrating = False return if self.isRadiant and gameHistory["radiant_win"]: gameResult = "win" else: if not self.isRadiant and not gameHistory["radiant_win"]: gameResult = "win" else: gameResult = "loss" # log.error(gameResult) self.spread_points(gameResult) def poll_webapi(self): serverID = "" with open("/srv/admiralbullbot/configs/currentID.txt", "r") as f: serverID = f.read() try: serverID = int(serverID) except ValueError: return False if self.calibratingSecond and serverID != 0: self.calibratingSecond = False return False if serverID == 0: self.bot.execute_delayed(100, self.close_shit) return False if self.oldID == serverID: return False self.oldID = serverID self.bot.execute_delayed(12, self.get_team, (serverID, )) def startGame(self): if not self.betting_open: self.bets = {} self.betting_open = True self.message_closed = False self.bot.websocket_manager.emit("dotabet_new_game") bulldogTeam = "radiant" if self.isRadiant else "dire" openString = "A new game has begun! Bulldog is on {}. Vote with !dotabet win/lose POINTS".format( bulldogTeam) self.bot.websocket_manager.emit( "notification", {"message": openString}) # self.bot.websocket_manager.emit("dotabet_new_game") self.bot.me(openString) def get_team(self, serverID): attempts = 0 if not serverID: return webURL = "https://api.steampowered.com/IDOTA2MatchStats_570/GetRealtimeStats/v1?" \ "server_steam_id={}&key={}".format(serverID, self.settings["api_key"]) jsonText = requests.get(webURL).json() try: while not jsonText: # Could bug and not return anything if attempts > 60: if not self.secondAttempt: self.bot.execute_delayed( 20, self.get_team, (serverID, )) self.secondAttempt = True else: self.bot.say( "Couldn\"t find which team Bulldog is on for this game. Mods - handle this round manually :)") self.job.pause() self.jobPaused = True self.secondAttempt = False attempts = 0 return attempts += 1 jsonText = requests.get(webURL).json() log.debug(jsonText) try: self.gettingTeam = True for i in range(2): for player in jsonText["teams"][i]["players"]: log.debug(player["name"]) if player["accountid"] == self.settings["steam3_id"]: if i == 0: self.isRadiant = True else: self.isRadiant = False self.bot.me( "Is bulldog on radiant? {}. If he isn\"t then tag a mod with BabyRage fix " "bet".format( self.isRadiant)) raise ExitLoop except KeyError: jsonText = "" except ExitLoop: pass self.gettingTeam = False self.betting_open = True self.secondAttempt = False self.startGame() def command_open(self, **options): openString = "Betting has been opened" bot = options["bot"] message = options["message"] self.calibrating = True if message: if "dire" in message: self.isRadiant = False elif "radi" in message: self.isRadiant = True elif "spectat" in message: self.isRadiant = True self.spectating = True openString += ". Reminder to bet with radiant/dire instead of win/loss" self.calibrating = False if not self.betting_open: bot.websocket_manager.emit("notification", {"message": openString}) if not self.spectating: bot.websocket_manager.emit("dotabet_new_game") bot.me(openString) self.betting_open = True self.message_closed = False self.job.pause() self.jobPaused = True def command_stats(self, **options): bot = options["bot"] source = options["source"] bot.say( "{}/{} betters on {}/{} points".format(self.winBetters, self.lossBetters, self.winPoints, self.lossPoints)) def close_shit(self): if self.jobPaused: return False self.betting_open = False self.reminder_bet() def command_close(self, **options): bot = options["bot"] source = options["source"] message = options["message"] if self.betting_open: count_down = 15 if message and message.isdigit(): count_down = int(message) if count_down > 0: bot.me("Betting will be locked in {} seconds! Place your bets people monkaS".format(count_down)) bot.execute_delayed(count_down, self.lock_bets, (bot,)) elif message: if "l" in message.lower() or "dire" in message.lower(): self.spread_points("loss") elif "w" in message.lower() or "radi" in message.lower(): self.spread_points("win") else: bot.whisper(source.username, "Are you pretending?") return False self.calibrating = True self.spectating = False def lock_bets(self, bot): self.betting_open = False self.reminder_bet() self.job.resume() self.jobPaused = False def command_restart(self, **options): bot = options["bot"] message = options["message"] source = options["source"] reason = "" if not message: reason = "No reason given EleGiggle" else: reason = message with DBManager.create_session_scope() as db_session: for username in self.bets: bet_for_win, betPoints = self.bets[username] user = self.bot.users.find(username, db_session=db_session) if not user: continue user.points += betPoints bot.whisper( user.username, "Your {} points bet has been refunded. The reason given is: " "{}".format( betPoints, reason)) self.bets = {} self.betting_open = False self.message_closed = True self.winBetters = 0 self.lossBetters = 0 bot.me("All your bets have been refunded and betting has been restarted.") def command_resetbet(self, **options): self.bets = {} options["bot"].me("The bets have been reset :)") self.winBetters = 0 self.lossBetters = 0 def command_betstatus(self, **options): bot = options["bot"] if self.betting_open: bot.say("Betting is open") elif self.winBetters > 0 or self.lossBetters > 0: bot.say("There is currently a bet with points not given yet") else: bot.say("There is no bet running") def command_bet(self, **options): bot = options["bot"] source = options["source"] message = options["message"] if message is None: return False if not self.betting_open: bot.whisper( source.username, "Betting is not currently open. Wait until the next game :\\") return False msg_parts = message.split(" ") if len(msg_parts) < 2: bot.whisper( source.username, "Invalid bet. You must do !dotabet radiant/dire POINTS (if spectating a game) " "or !dotabet win/loss POINTS (if playing)") return False if source.username in self.bets: bot.whisper( source.username, "You have already bet on this game. Wait until the next game starts!") return False points = 0 try: points = utils.parse_points_amount(source, msg_parts[1]) if points > 1500: points = 1500 except InvalidPointAmount as e: bot.whisper( source.username, "Invalid bet. You must do !dotabet radiant/dire POINTS (if spectating a game) " "or !dotabet win/loss POINTS (if playing) {}".format(e)) return False if points < 1: bot.whisper(source.username, "You can't bet less than 1 point you goddamn pleb Bruh") return False if not source.can_afford(points): bot.whisper( source.username, "You don't have {} points to bet".format(points)) return False outcome = msg_parts[0].lower() bet_for_win = False if "w" in outcome or "radi" in outcome: bet_for_win = True elif "l" in outcome or "dire" in outcome: bet_for_win = False else: bot.whisper( source.username, "Invalid bet. You must do !dotabet radiant/dire POINTS (if spectating a game) " "or !dotabet win/loss POINTS (if playing)") return False if bet_for_win: self.winBetters += 1 self.winPoints += points else: self.lossBetters += 1 self.lossPoints += points source.points -= points self.bets[source.username] = (bet_for_win, points) payload = { "win_betters": self.winBetters, "loss_betters": self.lossBetters, "win_points": self.winPoints, "loss_points": self.lossPoints } if not self.spectating: bot.websocket_manager.emit("dotabet_update_data", data=payload) finishString = "You have bet {} points on this game resulting in a ".format( points) if self.spectating: finishString = finishString + "radiant " bot.whisper( source.username, "{}{}".format( finishString, "win" if bet_for_win else "loss")) def load_commands(self, **options): self.commands["dotabet"] = Command.raw_command( self.command_bet, delay_all=0, delay_user=0, can_execute_with_whisper=True, description="Bet points", examples=[ CommandExample( None, "Bet 69 points on a win", chat="user:!dotabet win 69\n" "bot>user: You have bet 69 points on this game resulting in a win.", description="Bet that the streamer will win for 69 points").parse(), ], ) self.commands["bet"] = self.commands["dotabet"] self.commands["openbet"] = Command.raw_command( self.command_open, level=420, delay_all=0, delay_user=0, can_execute_with_whisper=True, description="Open bets", ) self.commands["restartbet"] = Command.raw_command( self.command_restart, level=420, delay_all=0, delay_user=0, can_execute_with_whisper=True, description="Restart bets", ) self.commands["closebet"] = Command.raw_command( self.command_close, level=420, delay_all=0, delay_user=0, can_execute_with_whisper=True, description="Close bets", ) self.commands["resetbet"] = Command.raw_command( self.command_resetbet, level=500, can_execute_with_whisper=True, description="Reset bets", ) self.commands["betstatus"] = Command.raw_command( self.command_betstatus, level=420, can_execute_with_whisper=True, description="Status of bets", ) self.commands["currentbets"] = Command.raw_command( self.command_stats, level=100, delay_all=0, delay_user=10, can_execute_with_whisper=True, ) # self.commands["betstats"] def enable(self, bot): if bot: self.job.resume() self.reminder_job.resume() # self.finish_job.resume() # self.close_job.resume() self.bot = bot # Move this to somewhere better self.maxReturn = self.settings["max_return"] if "max_return" in self.settings else None self.minReturn = float(self.settings["min_return"]) if "min_return" in self.settings else None def disable(self, bot): if bot: self.job.pause() self.reminder_job.pause()
class BingoModule(BaseModule): ID = __name__.split('.')[-1] NAME = 'Bingo Games' DESCRIPTION = 'Chat Bingo Game for Twitch and BTTV Emotes' ENABLED_DEFAULT = False CATEGORY = 'Game' SETTINGS = [ ModuleSetting( key='max_points', label='Max points for a bingo', type='number', required=True, placeholder='', default=3000, constraints={ 'min_value': 0, 'max_value': 35000, }), ModuleSetting( key='allow_negative_bingo', label='Allow negative bingo', type='boolean', required=True, default=True), ModuleSetting( key='max_negative_points', label='Max negative points for a bingo', type='number', required=True, placeholder='', default=1500, constraints={ 'min_value': 1, 'max_value': 35000, }) ] def __init__(self): super().__init__() self.bot = None self.bingo_running = False self.bingo_bttv_twitch_running = False def load_commands(self, **options): self.commands['bingo'] = Command.multiaction_command( level=500, default=None, command='bingo', commands={ 'emotes': Command.raw_command(self.bingo_emotes, level=500, delay_all=15, delay_user=15, description='Start an emote bingo with BTTV and TWITCH global emotes', examples=[ CommandExample(None, 'Emote bingo for 100 points', chat='user:!bingo emotes\n' 'bot: A bingo has started! Guess the right target to win 100 points! Only one target per message! ', description='').parse(), CommandExample(None, 'Emote bingo for 222 points', chat='user:!bingo emotes 222\n' 'bot: A bingo has started! Guess the right target to win 222 points! Only one target per message! ', description='').parse(), ]), 'bttv': Command.raw_command(self.bingo_bttv, level=500, delay_all=15, delay_user=15, description='Start an emote bingo with BTTV global emotes', examples=[ CommandExample(None, 'Emote bingo for 100 points', chat='user:!bingo bttv\n' 'bot: A bingo has started! Guess the right target to win 100 points! Only one target per message! Use BTTV global emotes. ', description='').parse(), CommandExample(None, 'Emote bingo for 222 points', chat='user:!bingo bttv 222\n' 'bot: A bingo has started! Guess the right target to win 222 points! Only one target per message! Use BTTV global emotes. ', description='').parse(), ]), 'twitch': Command.raw_command(self.bingo_twitch, level=500, delay_all=15, delay_user=15, description='Start an emote bingo with TWITCH global emotes', examples=[ CommandExample(None, 'Emote bingo for 100 points', chat='user:!bingo twitch\n' 'bot: A bingo has started! Guess the right target to win 100 points! Only one target per message! Use TWITCH global emotes. ', description='').parse(), CommandExample(None, 'Emote bingo for 222 points', chat='user:!bingo twitch 222\n' 'bot: A bingo has started! Guess the right target to win 222 points! Only one target per message! Use TWITCH global emotes. ', description='').parse(), ]), 'cancel': Command.raw_command(self.bingo_cancel, level=500, delay_all=15, delay_user=15, description='Cancel a running bingo', examples=[ CommandExample(None, 'Cancel a bingo', chat='user:!bingo cancel\n' 'bot: Bingo cancelled by pajlada FeelsBadMan', description='').parse(), ]), 'help': Command.raw_command(self.bingo_help_random, level=500, delay_all=15, delay_user=15, description='The bot will help the chat with a random letter from the bingo target', examples=[ CommandExample(None, 'Get a random letter from the bingo target', chat='user:!bingo help\n' 'bot: A bingo for 100 points is still running. You should maybe use a a a a a for the target', description='').parse(), ]), 'cheat': Command.raw_command(self.bingo_help_first, level=500, delay_all=15, delay_user=15, description='The bot will help the chat with the first letter from the bingo target', examples=[ CommandExample(None, 'Get the first letter from the bingo target', chat='user:!bingo cheat\n' 'bot: A bingo for 100 points is still running. You should use W W W W W as the first letter for the target', description='').parse(), ]), }) def set_bingo_target(self, target, bingo_points_win): self.bingo_target = target self.bingo_running = True self.bingo_points = bingo_points_win log.debug('Bingo target set: {0} for {1} points'.format(target, bingo_points_win)) def set_bingo_target_bttv(self, target_bttv, bingo_points_win): self.bingo_target = target_bttv self.bingo_running = True self.bingo_points = bingo_points_win log.debug('Bingo Bttv target set: {0} for {1} points'.format(target_bttv, bingo_points_win)) def bingo_emotes(self, bot, source, message, event, args): """ Twitch and BTTV emotes """ if hasattr(self, 'bingo_running') and self.bingo_running is True: bot.me('{0}, a bingo is already running OMGScoots'.format(source.username_raw)) return False self.bingo_bttv_twitch_running = True start_random_emote_bingo = random.choice(['1', '2']) if start_random_emote_bingo == '1': return self.bingo_twitch(bot, source, message, event, args) elif start_random_emote_bingo == '2': return self.bingo_bttv(bot, source, message, event, args) def bingo_bttv(self, bot, source, message, event, args): """ BTTV emotes """ if hasattr(self, 'bingo_running') and self.bingo_running is True: bot.me('{0}, a bingo is already running OMGScoots'.format(source.username_raw)) return False bingo_points_win = 100 try: if message is not None and self.settings['allow_negative_bingo'] is True: bingo_points_win = int(message.split()[0]) if message is not None and self.settings['allow_negative_bingo'] is False: if int(message.split()[0]) >= 0: bingo_points_win = int(message.split()[0]) except (IndexError, TypeError, ValueError): pass if bingo_points_win >= 0: bingo_points_win = min(bingo_points_win, self.settings['max_points']) if bingo_points_win <= -1: bingo_points_win = max(bingo_points_win, -self.settings['max_negative_points']) self.emotes_bttv = bot.emotes.get_global_bttv_emotes() target_bttv = random.choice(self.emotes_bttv) self.set_bingo_target_bttv(target_bttv, bingo_points_win) if hasattr(self, 'bingo_bttv_twitch_running') and self.bingo_bttv_twitch_running is True: bot.me('A bingo has started! Guess the right target to win {0} points! Only one target per message! Use BTTV and TWITCH global emotes.'.format(bingo_points_win)) bot.websocket_manager.emit('notification', {'message': 'A bingo has started!'}) bot.execute_delayed(0.75, bot.websocket_manager.emit, ('notification', {'message': 'Guess the target, win the prize!'})) return True else: bot.me('A bingo has started! Guess the right target to win {0} points! Only one target per message! Use BTTV global emotes.'.format(bingo_points_win)) bot.websocket_manager.emit('notification', {'message': 'A bingo has started!'}) bot.execute_delayed(0.75, bot.websocket_manager.emit, ('notification', {'message': 'Guess the target, win the prize!'})) return False def bingo_twitch(self, bot, source, message, event, args): """ Twitch emotes """ if hasattr(self, 'bingo_running') and self.bingo_running is True: bot.me('{0}, a bingo is already running OMGScoots'.format(source.username_raw)) return False bingo_points_win = 100 try: if message is not None and self.settings['allow_negative_bingo'] is True: bingo_points_win = int(message.split()[0]) if message is not None and self.settings['allow_negative_bingo'] is False: if int(message.split()[0]) >= 0: bingo_points_win = int(message.split()[0]) except (IndexError, TypeError, ValueError): pass if bingo_points_win >= 0: bingo_points_win = min(bingo_points_win, self.settings['max_points']) if bingo_points_win <= -1: bingo_points_win = max(bingo_points_win, -self.settings['max_negative_points']) self.emotes = bot.emotes.get_global_emotes() target = random.choice(self.emotes) self.set_bingo_target(target, bingo_points_win) if hasattr(self, 'bingo_bttv_twitch_running') and self.bingo_bttv_twitch_running is True: bot.me('A bingo has started! Guess the right target to win {0} points! Only one target per message! Use BTTV and TWITCH global emotes.'.format(bingo_points_win)) bot.websocket_manager.emit('notification', {'message': 'A bingo has started!'}) bot.execute_delayed(0.75, bot.websocket_manager.emit, ('notification', {'message': 'Guess the target, win the prize!'})) return True else: bot.me('A bingo has started! Guess the right target to win {0} points! Only one target per message! Use TWITCH global emotes.'.format(bingo_points_win)) bot.websocket_manager.emit('notification', {'message': 'A bingo has started!'}) bot.execute_delayed(0.75, bot.websocket_manager.emit, ('notification', {'message': 'Guess the target, win the prize!'})) return False def bingo_cancel(self, bot, source, message, event, args): """ cancel a bingo """ if hasattr(self, 'bingo_running') and self.bingo_running is True: bot.me('Bingo cancelled by {0} FeelsBadMan'.format(source.username_raw)) log.debug('Bingo cancelled by {0}'.format(source.username_raw)) self.bingo_running = False self.bingo_bttv_twitch_running = False return True else: bot.me('{0}, no bingo is currently running FailFish'.format(source.username_raw)) return False def bingo_help_random(self, bot, source, message, event, args): """ Random letter of the target """ if hasattr(self, 'bingo_running') and self.bingo_running is True: target_split_random = random.choice(list(self.bingo_target.lower())) bot.me('A bingo for {0} points is still running. You should maybe use the letter {1} {1} {1} {1} {1} for the target'.format(self.bingo_points, target_split_random)) log.debug('Bingo help: {0}'.format(target_split_random)) return True else: bot.me('{0}, no bingo is currently running FailFish'.format(source.username_raw)) return False def bingo_help_first(self, bot, source, message, event, args): """ First letter of the target """ if hasattr(self, 'bingo_running') and self.bingo_running is True: target_first_letter = ' '.join(list(self.bingo_target)[:1]) bot.me('A bingo for {0} points is still running. You should use {1} {1} {1} {1} {1} as the first letter for the target'.format(self.bingo_points, target_first_letter)) log.debug('Bingo help: {0}'.format(target_first_letter)) return True else: bot.me('{0}, no bingo is currently running FailFish'.format(source.username_raw)) return False def on_message(self, source, msg_raw, message_emotes, whisper, urls, event): if len(message_emotes) > 0: if hasattr(self, 'bingo_running') and self.bingo_running is True: if len(message_emotes) == 1 and len(msg_raw.split(' ')) == 1: if message_emotes[0]['code'] == self.bingo_target: HandlerManager.trigger('on_bingo_win', source, self.bingo_points, self.bingo_target) self.bingo_running = False self.bingo_bttv_twitch_running = False self.bot.me('{0} won the bingo! {1} was the target. Congrats, {2} points to you PogChamp'.format(source.username_raw, self.bingo_target, self.bingo_points)) source.points += self.bingo_points log.info('{0} won the bingo for {1} points!'.format(source.username_raw, self.bingo_points)) def enable(self, bot): HandlerManager.add_handler('on_message', self.on_message) self.bot = bot def disable(self, bot): HandlerManager.remove_handler('on_message', self.on_message)
class BingoModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Bingo" DESCRIPTION = "Chat Bingo Game for Twitch, FFZ, BTTV and 7TV Emotes" ENABLED_DEFAULT = False CATEGORY = "Game" SETTINGS = [ ModuleSetting( key="default_points", label="Defaults points reward for a bingo", type="number", required=True, placeholder="", default=100, constraints={"min_value": 0, "max_value": 35000}, ), ModuleSetting( key="max_points", label="Max points for a bingo", type="number", required=True, placeholder="", default=3000, constraints={"min_value": 0, "max_value": 35000}, ), ModuleSetting( key="allow_negative_bingo", label="Allow negative bingo", type="boolean", required=True, default=True ), ModuleSetting( key="max_negative_points", label="Max negative points for a bingo", type="number", required=True, placeholder="", default=1500, constraints={"min_value": 1, "max_value": 35000}, ), ] def __init__(self, bot): super().__init__(bot) self.active_game = None @property def bingo_running(self): return self.active_game is not None @staticmethod def make_twitch_sets(manager): tier_one_emotes = ("Tier 1 sub emotes", manager.tier_one_emotes) tier_two_emotes = ("Tier 2 sub emotes", manager.tier_two_emotes) tier_three_emotes = ("Tier 3 sub emotes", manager.tier_three_emotes) global_emotes = ("Global Twitch emotes", manager.global_emotes) all_emotes = ("Global + Twitch tier 1 sub emotes", manager.tier_one_emotes + manager.global_emotes) return { "twitch": tier_one_emotes, "sub": tier_one_emotes, "tier1": tier_one_emotes, "tier2": tier_two_emotes, "tier3": tier_three_emotes, **two_word_variations("twitch", "sub", tier_one_emotes), **two_word_variations("twitch", "tier1", tier_one_emotes), **two_word_variations("twitch", "tier2", tier_two_emotes), **two_word_variations("twitch", "tier3", tier_three_emotes), **two_word_variations("twitch", "global", global_emotes), **two_word_variations("twitch", "channel", tier_one_emotes), **two_word_variations("twitch", "all", all_emotes), } @staticmethod def make_bttv_ffz_7tv_sets(manager): friendly_name = manager.friendly_name channel_emotes = (f"Channel {friendly_name} emotes", manager.channel_emotes) global_emotes = (f"Global {friendly_name} emotes", manager.global_emotes) all_emotes = (f"Global + Channel {friendly_name} emotes", manager.channel_emotes + manager.global_emotes) key = friendly_name.lower() return { key: channel_emotes, **two_word_variations(key, "global", global_emotes), **two_word_variations(key, "channel", channel_emotes), **two_word_variations(key, "all", all_emotes), } def make_known_sets_dict(self): # we first make a dict containing lists as the list of emotes (because it's less to type...) list_dict = { **self.make_twitch_sets(self.bot.emote_manager.twitch_emote_manager), **self.make_bttv_ffz_7tv_sets(self.bot.emote_manager.ffz_emote_manager), **self.make_bttv_ffz_7tv_sets(self.bot.emote_manager.bttv_emote_manager), **self.make_bttv_ffz_7tv_sets(self.bot.emote_manager.seventv_emote_manager), "all": ( "FFZ, BTTV and 7TV Channel emotes + Tier 1 subemotes", self.bot.emote_manager.ffz_emote_manager.channel_emotes + self.bot.emote_manager.bttv_emote_manager.channel_emotes + self.bot.emote_manager.seventv_emote_manager.channel_emotes + self.bot.emote_manager.twitch_emote_manager.tier_one_emotes, ), } # then convert all the lists to tuples so they are hashable # and can be stored in a set of "selected sets" later return {key: (set_name, tuple(set_emotes), False) for key, (set_name, set_emotes) in list_dict.items()} def bingo_start(self, bot, source, message, event, args): if self.bingo_running: bot.say(f"{source}, a bingo is already running FailFish") return False emote_instances = args["emote_instances"] known_sets = self.make_known_sets_dict() selected_sets = set() points_reward = None unparsed_options = [] words_in_message = [s for s in message.split(" ") if len(s) > 0] if len(words_in_message) <= 0: bot.say(f"{source}, You must at least give me some emote sets or emotes to choose from! FailFish") return False emote_index_offset = len("!bingo start ") # we can't iterate using words_in_message here because that would mess up the accompanying index for index, word in iterate_split_with_index(message.split(" ")): if len(word) <= 0: continue # Is the current word an emote? potential_emote_instance = next((e for e in emote_instances if e.start == index + emote_index_offset), None) if potential_emote_instance is not None: # single-emote set with the name of the emote new_set = (potential_emote_instance.emote.code, (potential_emote_instance.emote,), True) selected_sets.add(new_set) continue # Is the current word a number? try: parsed_int = int(word) except ValueError: parsed_int = None if parsed_int is not None: # if points_reward is already set this is the second number in the message if points_reward is not None: unparsed_options.append(word) continue points_reward = parsed_int continue # Is the current word a known set? cleaned_key = remove_emotes_suffix(word).lower() if cleaned_key in known_sets: selected_sets.add(known_sets[cleaned_key]) continue unparsed_options.append(word) if len(unparsed_options) > 0: bot.say( "{}, I don't know what to do with the argument{} {} BabyRage".format( source, "" if len(unparsed_options) == 1 else "s", # pluralization join_to_sentence(['"' + s + '"' for s in unparsed_options]), ) ) return False default_points = self.settings["default_points"] if points_reward is None: points_reward = default_points max_points = self.settings["max_points"] if points_reward > max_points: bot.say( f"{source}, You can't start a bingo with that many points. FailFish {max_points} are allowed at most." ) return False allow_negative_bingo = self.settings["allow_negative_bingo"] if points_reward < 0 and not allow_negative_bingo: bot.say(f"{source}, You can't start a bingo with negative points. FailFish") return False min_points = -self.settings["max_negative_points"] if points_reward < min_points: bot.say( f"{source}, You can't start a bingo with that many negative points. FailFish {min_points} are allowed at most." ) return False if len(selected_sets) <= 0: bot.say(f"{source}, You must at least give me some emotes or emote sets to choose from! FailFish") return False selected_set_names = [] selected_discrete_emote_codes = [] selected_emotes = set() for set_name, set_emotes, is_discrete_emote in selected_sets: if is_discrete_emote: selected_discrete_emote_codes.append(set_name) else: selected_set_names.append(set_name) selected_emotes.update(set_emotes) correct_emote = random.choice(list(selected_emotes)) user_messages = [] if len(selected_set_names) > 0: user_messages.append(join_to_sentence(selected_set_names)) if len(selected_discrete_emote_codes) > 0: # the space at the end is so the ! from the below message doesn't stop the last emote from showing up in chat user_messages.append(f"these emotes: {' '.join(selected_discrete_emote_codes)} ") bot.me( f"A bingo has started! ThunBeast Guess the right emote to win {points_reward} points! B) Only one emote per message! Select from {' and '.join(user_messages)}!" ) log.info(f"A Bingo game has begun for {points_reward} points, correct emote is {correct_emote}") self.active_game = BingoGame(correct_emote, points_reward) def bingo_cancel(self, bot, source, message, event, args): if not self.bingo_running: bot.say(f"{source}, no bingo is running FailFish") return False self.active_game = None bot.me(f"Bingo cancelled by {source} FeelsBadMan") def bingo_help_random(self, bot, source, message, event, args): if not self.bingo_running: bot.say(f"{source}, no bingo is running FailFish") return False correct_emote_code = self.active_game.correct_emote.code random_letter = random.choice(correct_emote_code) bot.me( f"A bingo for {self.active_game.points_reward} points is still running. You should maybe use {random_letter} {random_letter} {random_letter} {random_letter} {random_letter} for the target" ) def bingo_help_first(self, bot, source, message, event, args): if not self.bingo_running: bot.say(f"{source}, no bingo is running FailFish") return False correct_emote_code = self.active_game.correct_emote.code first_letter = correct_emote_code[0] bot.me( f"A bingo for {self.active_game.points_reward} points is still running. You should maybe use {first_letter} {first_letter} {first_letter} {first_letter} {first_letter} for the target" ) def on_message(self, source, message, emote_instances, **rest): if not self.bingo_running: return if len(emote_instances) != 1: return correct_emote = self.active_game.correct_emote correct_emote_code = correct_emote.code typed_emote = emote_instances[0].emote typed_emote_code = typed_emote.code # we check for BOTH exact match (which works by comparing provider and ID, see __eq__ and __hash__ in # the Emote class) and for code-only match because we want to allow equal-named sub and ffz/bttv/7tv emotes # to be treated equally (e.g. sub-emote pajaL vs bttv emote pajaL) # The reason exact match can differ from code match is in case of regex twitch emotes, such as :) and :-) # If the "correct_emote" was chosen from the list of global twitch emotes, then its code will be the regex # for the emote (If the bingo was started by specifying :) as an explicit emote, then the code will be # :)). To make sure we don't trip on this we only compare by provider and provider ID. exact_match = correct_emote == typed_emote only_code_match = correct_emote_code == typed_emote_code if not (exact_match or only_code_match): return # user guessed the emote HandlerManager.trigger("on_bingo_win", source, self.active_game) points_reward = self.active_game.points_reward source.points += points_reward self.active_game = None self.bot.me( f"{source} won the bingo! {correct_emote_code} was the target. Congrats, {points_reward} points to you PogChamp" ) def load_commands(self, **options): self.commands["bingo"] = Command.multiaction_command( level=500, default=None, command="bingo", commands={ "start": Command.raw_command( self.bingo_start, level=500, delay_all=15, delay_user=15, description="Start an emote bingo with specified emote sets", examples=[ CommandExample( None, "Emote bingo for default points", chat="user:!bingo start bttv\n" "bot: A bingo has started! Guess the right target to win 100 points! " "Only one target per message! Select from Channel BTTV Emotes!", description="", ).parse(), CommandExample( None, "Emote bingo for 222 points", chat="user:!bingo start bttv 222\n" "bot: A bingo has started! Guess the right target to win 222 points! " "Only one target per message! Select from Channel BTTV Emotes!", description="", ).parse(), ], ), "cancel": Command.raw_command( self.bingo_cancel, level=500, delay_all=15, delay_user=15, description="Cancel a running bingo", examples=[ CommandExample( None, "Cancel a bingo", chat="user:!bingo cancel\n" "bot: Bingo cancelled by pajlada FeelsBadMan", description="", ).parse() ], ), "help": Command.raw_command( self.bingo_help_random, level=500, delay_all=15, delay_user=15, description="The bot will help the chat with a random letter from the bingo target", examples=[ CommandExample( None, "Get a random letter from the bingo target", chat="user:!bingo help\n" "bot: A bingo for 100 points is still running. You should maybe use a a a a a for the target", description="", ).parse() ], ), "cheat": Command.raw_command( self.bingo_help_first, level=500, delay_all=15, delay_user=15, description="The bot will help the chat with the first letter from the bingo target", examples=[ CommandExample( None, "Get the first letter from the bingo target", chat="user:!bingo cheat\n" "bot: A bingo for 100 points is still running. You should use W W W W W as the first letter for the target", description="", ).parse() ], ), }, ) def enable(self, bot): HandlerManager.add_handler("on_message", self.on_message) def disable(self, bot): HandlerManager.remove_handler("on_message", self.on_message)
class MassPingProtectionModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Mass Ping Protection" DESCRIPTION = "Times out users who post messages that mention too many users at once." CATEGORY = "Filter" SETTINGS = [ ModuleSetting( key="max_ping_count", label="Maximum number of pings allowed in each message", type="number", required=True, placeholder="", default=5, constraints={ "min_value": 3, "max_value": 100 }, ), ModuleSetting( key="timeout_length_base", label="Base Timeout length (seconds)", type="number", required=True, placeholder="", default=120, constraints={ "min_value": 30, "max_value": 3600 }, ), ModuleSetting( key="extra_timeout_length_per_ping", label= "Timeout length per extra (disallowed extra) ping in the message (seconds)", type="number", required=True, placeholder="", default=30, constraints={ "min_value": 0, "max_value": 600 }, ), ModuleSetting( key="whisper_offenders", label="Send offenders a whisper explaining the timeout", type="boolean", required=True, default=True, ), ModuleSetting( key="bypass_level", label="Level to bypass module", type="number", required=True, placeholder="", default=420, constraints={ "min_value": 100, "max_value": 2000 }, ), ] def __init__(self, bot): super().__init__(bot) @staticmethod def is_known_user(username): streamer = StreamHelper.get_streamer() return RedisManager.get().hexists( "{streamer}:users:last_seen".format(streamer=streamer), username) @staticmethod def count_pings(message, source, emote_instances): pings = set() for match in username_in_message_pattern.finditer(message): matched_part = match.group() start_idx = match.start() end_idx = match.end() potential_emote = next( (e for e in emote_instances if e.start == start_idx and e.end == end_idx), None) # this "username" is an emote. skip if potential_emote is not None: continue matched_part = matched_part.lower() # this is the sending user. We allow people to "ping" themselves if matched_part == source.username or matched_part == source.username_raw.lower( ): continue # check that this word is a known user (we have seen this username before) if not MassPingProtectionModule.is_known_user(matched_part): continue pings.add(matched_part) return len(pings) def determine_timeout_length(self, message, source, emote_instances): ping_count = MassPingProtectionModule.count_pings( message, source, emote_instances) pings_too_many = ping_count - self.settings["max_ping_count"] if pings_too_many <= 0: return 0 return self.settings["timeout_length_base"] + self.settings[ "extra_timeout_length_per_ping"] * pings_too_many def check_message(self, message, source): emote_instances, _ = self.bot.emote_manager.parse_all_emotes(message) # returns False if message is good, # True if message is bad. return self.determine_timeout_length(message, source, emote_instances) > 0 def on_pubmsg(self, source, message, emote_instances, **rest): if source.level >= self.settings[ "bypass_level"] or source.moderator is True: return timeout_duration = self.determine_timeout_length( message, source, emote_instances) if timeout_duration <= 0: return self.bot.timeout_user(source, timeout_duration, reason="Too many users pinged in message") if self.settings["whisper_offenders"]: self.bot.whisper( source.username, ("You have been timed out for {} seconds because your message mentioned too many users at once." ).format(timeout_duration), ) return False def enable(self, bot): HandlerManager.add_handler("on_message", self.on_pubmsg, priority=150) def disable(self, bot): HandlerManager.remove_handler("on_message", self.on_pubmsg)
class ClipCommandModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Clip" DESCRIPTION = "Enables the usage of the !clip command" CATEGORY = "Feature" PARENT_MODULE = BasicCommandsModule SETTINGS = [ # TODO: Add discord support ModuleSetting( key="subscribers_only", label="Only allow subscribers to use the !clip command.", type="boolean", required=True, default=False, ), ModuleSetting( key="delay_clip", label= "Add a delay before the clip is captured (to account for the brief delay between the broadcaster's stream and the viewer's experience).", type="boolean", required=True, default=True, ), ModuleSetting( key="thumbnail_check", label= "Delay the bot response by 5 seconds to ensure the clip thumbnail has been generated for webchat users.", type="boolean", required=True, default=False, ), ModuleSetting( key="online_response", label= "Message response while the streamer is online | Available arguments: {streamer}, {clip}", type="text", required=True, placeholder="", default="New clip PogChamp 👉 {clip}", constraints={ "min_str_len": 1, "max_str_len": 400 }, ), ModuleSetting( key="offline_response", label= "Message response if the streamer is offline. Remove text to disable message | Available arguments: {streamer}", type="text", required=False, placeholder="", default="Cannot clip while {streamer} is offline! BibleThump", constraints={"max_str_len": 400}, ), ModuleSetting( key="response_method", label="Method of response to command usage", type="options", required=True, default="say", options=["say", "whisper", "reply"], ), ModuleSetting( key="global_cd", label="Global cooldown (seconds)", type="number", required=True, placeholder="", default=30, constraints={ "min_value": 0, "max_value": 120 }, ), ModuleSetting( key="user_cd", label="Per-user cooldown (seconds)", type="number", required=True, placeholder="", default=60, constraints={ "min_value": 0, "max_value": 240 }, ), ModuleSetting( key="level", label="Level required to use the command", type="number", required=True, placeholder="", default=100, constraints={ "min_value": 100, "max_value": 2000 }, ), ] def load_commands(self, **options): self.commands["clip"] = Command.raw_command( self.clip, sub_only=self.settings["subscribers_only"], delay_all=self.settings["global_cd"], delay_user=self.settings["user_cd"], level=self.settings["level"], can_execute_with_whisper=bool( self.settings["response_method"] == "reply"), command="clip", examples=[ CommandExample( None, "Make a new clip while the stream is online", chat="user:!clip\n" "bot: " + self.settings["online_response"].format( source="pajlada", streamer=StreamHelper.get_streamer(), clip= "https://clips.twitch.tv/ExpensiveWonderfulClamArsonNoSexy", ), description="", ).parse() ], ) def clip(self, bot, event, source, **rest): if self.settings["subscribers_only"] and source.subscriber is False: return True if not self.bot.is_online: if self.settings["offline_response"] != "": bot.send_message_to_user( source, self.settings["offline_response"].format( source="{source}", streamer=bot.streamer_display), event, method=self.settings["response_method"], ) return True try: if self.settings["delay_clip"] or ( source.name == StreamHelper.get_streamer()) is True: clip_id = self.bot.twitch_helix_api.create_clip( StreamHelper.get_streamer_id(), self.bot.bot_token_manager, has_delay=True) else: clip_id = self.bot.twitch_helix_api.create_clip( StreamHelper.get_streamer_id(), self.bot.bot_token_manager) except HTTPError as e: if e.response.status_code == 503: bot.send_message_to_user( source, "Failed to create clip! Does the streamer have clips disabled?", event, method=self.settings["response_method"], ) elif e.response.status_code != 401: bot.send_message_to_user( source, "Failed to create clip! Please try again.", event, method=self.settings["response_method"], ) else: bot.send_message_to_user( source, "Error: The bot token does not grant permission to create clips. The bot needs to be re-authenticated to fix this problem.", event, method=self.settings["response_method"], ) return True clip_url = f"https://clips.twitch.tv/{clip_id}" if self.settings["thumbnail_check"] is True: self.bot.execute_delayed( 5, bot.send_message_to_user, source, self.settings["online_response"].format( source="{source}", streamer=bot.streamer_display, clip=clip_url), event, method=self.settings["response_method"], ) else: bot.send_message_to_user( source, self.settings["online_response"].format( source="{source}", streamer=bot.streamer_display, clip=clip_url), event, )
class PlaySoundTokenCommandModule(BaseModule): ID = 'tokencommand-' + __name__.split('.')[-1] NAME = '!playsound' DESCRIPTION = 'Play a sound on stream' PARENT_MODULE = QuestModule SETTINGS = [ ModuleSetting(key='point_cost', label='Point cost', type='number', required=True, placeholder='Point cost', default=0, constraints={ 'min_value': 0, 'max_value': 999999, }), ModuleSetting(key='token_cost', label='Token cost', type='number', required=True, placeholder='Token cost', default=3, constraints={ 'min_value': 0, 'max_value': 15, }), ModuleSetting(key='sample_cd', label='Cooldown for the same sample (seconds)', type='number', required=True, placeholder='', default=20, constraints={ 'min_value': 5, 'max_value': 120, }), ] def __init__(self): super().__init__() Samples.all_samples.sort() self.valid_samples = [sample.command for sample in Samples.all_samples] self.sample_cache = [] def play_sound(self, **options): bot = options['bot'] message = options['message'] source = options['source'] if message: sample = message.split(' ')[0].lower() if sample in self.sample_cache: return False if sample == 'random': sample = random.choice(self.valid_samples) if sample in self.valid_samples: log.debug('Played sound: {0}'.format(sample)) payload = {'sample': sample} bot.websocket_manager.emit('play_sound', payload) bot.whisper( source.username, 'Successfully played your sample {0}'.format(sample)) self.sample_cache.append(sample) bot.execute_delayed(self.settings['sample_cd'], self.sample_cache.remove, ('{0}'.format(sample), )) return True bot.whisper( source.username, 'Your sample is not valid. Check out all the valid samples here: {0}/commands/playsound' .format(bot.domain)) return False def load_commands(self, **options): self.commands[ '#playsound'] = pajbot.models.command.Command.raw_command( self.play_sound, tokens_cost=self.settings['token_cost'], cost=self.settings['point_cost'], sub_only=True, description= 'Play a sound on stream! Costs {} tokens, sub only for now.'. format(self.settings['token_cost']), can_execute_with_whisper=True, examples=[ pajbot.models.command.CommandExample( None, 'Play the "cumming" sample', chat='user:!#playsound cumming\n' 'bot>user:Successfully played your sample cumming'). parse(), pajbot.models.command.CommandExample( None, 'Play the "fuckyou" sample', chat='user:!#playsound fuckyou\n' 'bot>user:Successfully played your sample fuckyou'). parse(), ], ) global_script = """<script> function playOrStopSound(elem, audio) { if(elem.innerHTML=="Play") { elem.innerHTML="Stop"; audio.play(); } else { elem.innerHTML="Play"; audio.pause(); audio.currentTime=0; } } </script>""" local_script = """<script> var elem{0.command}=document.getElementById('btnTogglePlay{0.command}'); var snd{0.command} = new Audio("{0.href}"); snd{0.command}.onended=function(){{elem{0.command}.innerHTML='Play';}}; elem{0.command}.addEventListener("click", function(){{ playOrStopSound(elem{0.command}, snd{0.command}); }}); </script>""" html_valid_samples = global_script for sample in Samples.all_samples: parsed_sample = local_script.format(sample) html_valid_samples += ''.join([ '<tr><td class="command-sample{1}">!#playsound {0.command}</td><td><button id="btnTogglePlay{0.command}">Play</button>{2}</td></tr>' .format(sample, ' new' if sample.new else '', parsed_sample) ]) self.commands[ '#playsound'].long_description = '<h5 style="margin-top: 20px;">Valid samples</h5><table>{}</table>'.format( html_valid_samples)
class ChattersRefreshModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Chatters refresh" DESCRIPTION = "Fetches a list of chatters and updates points/time accordingly - this is required to be turned on in order for the bot to record user time and/or points" ENABLED_DEFAULT = True CATEGORY = "Internal" SETTINGS = [ ModuleSetting( key="base_points_pleb", label= "Award this points amount every 10 minutes to non-subscribers", type="number", required=True, placeholder="", default=2, constraints={ "min_value": 0, "max_value": 500000 }, ), ModuleSetting( key="base_points_sub", label="Award this points amount every 10 minutes to subscribers", type="number", required=True, placeholder="", default=10, constraints={ "min_value": 0, "max_value": 500000 }, ), ModuleSetting( key="offline_chat_multiplier", label= "Apply this multiplier to the awarded points if the stream is currently offline (in percent, 100 = same as online chat, 0 = nothing)", type="number", required=True, placeholder="", default=0, constraints={ "min_value": 0, "max_value": 1000 }, ), ] UPDATE_INTERVAL = 10 # minutes def __init__(self, bot): super().__init__(bot) self.scheduled_job = None def update_chatters_cmd(self, bot, source, **rest): # TODO if you wanted to improve this: Provide the user with feedback # whether the update succeeded, and if yes, how many users were updated bot.whisper(source, "Reloading list of chatters...") bot.action_queue.submit(self._update_chatters, only_last_seen=True) @time_method def _update_chatters(self, only_last_seen=False): chatter_logins = self.bot.twitch_tmi_api.get_chatter_logins_by_login( self.bot.streamer) chatter_basics = self.bot.twitch_helix_api.bulk_get_user_basics_by_login( chatter_logins) # filter out invalid/deleted/etc. users chatter_basics = [e for e in chatter_basics if e is not None] is_stream_online = self.bot.stream_manager.online if is_stream_online: add_time_in_chat_online = timedelta(minutes=self.UPDATE_INTERVAL) add_time_in_chat_offline = timedelta(minutes=0) else: add_time_in_chat_online = timedelta(minutes=0) add_time_in_chat_offline = timedelta(minutes=self.UPDATE_INTERVAL) add_points_pleb = self.settings["base_points_pleb"] add_points_sub = self.settings["base_points_sub"] if not is_stream_online: offline_chat_multiplier = self.settings[ "offline_chat_multiplier"] / 100 add_points_pleb = int( round(add_points_pleb * offline_chat_multiplier)) add_points_sub = int( round(add_points_sub * offline_chat_multiplier)) if only_last_seen: add_time_in_chat_online = timedelta(minutes=0) add_time_in_chat_offline = timedelta(minutes=0) add_points_pleb = 0 add_points_sub = 0 update_values = [{ **basics.jsonify(), "add_points_pleb": add_points_pleb, "add_points_sub": add_points_sub, "add_time_in_chat_online": add_time_in_chat_online, "add_time_in_chat_offline": add_time_in_chat_offline, } for basics in chatter_basics] with DBManager.create_session_scope() as db_session: db_session.execute( text(""" INSERT INTO "user"(id, login, name, points, time_in_chat_online, time_in_chat_offline, last_seen) VALUES (:id, :login, :name, :add_points_pleb, :add_time_in_chat_online, :add_time_in_chat_offline, now()) ON CONFLICT (id) DO UPDATE SET points = "user".points + CASE WHEN "user".subscriber THEN :add_points_sub ELSE :add_points_pleb END, time_in_chat_online = "user".time_in_chat_online + :add_time_in_chat_online, time_in_chat_offline = "user".time_in_chat_offline + :add_time_in_chat_offline, last_seen = now() """), update_values, ) log.info(f"Successfully updated {len(chatter_basics)} chatters") def load_commands(self, **options): self.commands["reload"] = Command.multiaction_command( command="reload", commands={ "chatters": Command.raw_command( self.update_chatters_cmd, delay_all=120, delay_user=120, level=1000, examples=[ CommandExample( None, f"Reload who is currently chatting", chat= f"user:!reload chatters\nbot>user: Reloading list of chatters...", description= "Note: Updates only last_seen values, does not award points for watching the stream.", ).parse() ], ) }, ) def enable(self, bot): # Web interface, nothing to do if not bot: return # every 10 minutes, add the chatters update to the action queue self.scheduled_job = ScheduleManager.execute_every( self.UPDATE_INTERVAL * 60, lambda: self.bot.action_queue.submit(self._update_chatters)) def disable(self, bot): # Web interface, nothing to do if not bot: return self.scheduled_job.remove()
class DuelModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Duel" DESCRIPTION = "Let users duel to win or lose points." CATEGORY = "Game" SETTINGS = [ ModuleSetting( key="message_won", label="Winner message | Available arguments: {winner}, {loser}", type="text", required=True, placeholder="{winner} won the duel vs {loser} PogChamp", default="{winner} won the duel vs {loser} PogChamp", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="message_won_points", label= "Points message | Available arguments: {winner}, {loser}, {total_pot}, {extra_points}", type="text", required=True, placeholder= "{winner} won the duel vs {loser} PogChamp . The pot was {total_pot}, the winner gets their bet back + {extra_points} points", default= "{winner} won the duel vs {loser} PogChamp . The pot was {total_pot}, the winner gets their bet back + {extra_points} points", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="duel_tax", label="Duel tax (deduct this percent value from the win)", type="number", required=True, placeholder="", default=30, constraints={ "min_value": 0, "max_value": 100 }, ), ModuleSetting( key="online_global_cd", label="Global cooldown (seconds)", type="number", required=True, placeholder="", default=0, constraints={ "min_value": 0, "max_value": 120 }, ), ModuleSetting( key="online_user_cd", label="Per-user cooldown (seconds)", type="number", required=True, placeholder="", default=5, constraints={ "min_value": 0, "max_value": 240 }, ), ModuleSetting(key="show_on_clr", label="Show duels on the clr overlay", type="boolean", required=True, default=True), ModuleSetting( key="max_duel_age", label="Auto-cancel duels after this many minutes", type="number", required=True, placeholder="", default=5, constraints={ "min_value": 1, "max_value": 60 }, ), ] def load_commands(self, **options): self.commands["duel"] = Command.raw_command( self.initiate_duel, delay_all=self.settings["online_global_cd"], delay_user=self.settings["online_user_cd"], description="Initiate a duel with a user", examples=[ CommandExample( None, "0-point duel", chat="user:!duel Karl_Kons\n" "bot>user:You have challenged Karl_Kons for 0 points", description="Duel Karl_Kons for 0 points", ).parse(), CommandExample( None, "69-point duel", chat="user:!duel Karl_Kons 69\n" "bot>user:You have challenged Karl_Kons for 69 points", description="Duel Karl_Kons for 69 points", ).parse(), ], ) self.commands["cancelduel"] = Command.raw_command( self.cancel_duel, delay_all=0, delay_user=10, description="Cancel your duel request") self.commands["accept"] = Command.raw_command( self.accept_duel, delay_all=0, delay_user=0, description="Accept a duel request") self.commands["decline"] = Command.raw_command( self.decline_duel, delay_all=0, delay_user=0, description="Decline a duel request") self.commands["deny"] = self.commands["decline"] self.commands["duelstatus"] = Command.raw_command( self.status_duel, delay_all=0, delay_user=5, description="Current duel request info") self.commands["duelstats"] = Command.raw_command( self.get_duel_stats, delay_all=0, delay_user=120, description="Get your duel statistics") def __init__(self, bot): super().__init__(bot) self.duel_requests = {} self.duel_request_price = {} self.duel_targets = {} self.blUsers = ["admiralbulldog", "infinitegachi"] self.duel_begin_time = {} self.gc_job = None def initiate_duel(self, bot, source, message, **rest): """ Initiate a duel with a user. You can also bet points on the winner. By default, the maximum amount of points you can spend is 420. How to use: !duel USERNAME POINTS_TO_BET """ if message is None: return False msg_split = message.split() input = msg_split[0] with DBManager.create_session_scope() as db_session: user = User.find_by_user_input(db_session, input) if user is None: # No user was found with this username return False duel_price = 1 if len(msg_split) > 1: try: duel_price = utils.parse_points_amount( source, msg_split[1]) if duel_price < 1: # bot.whisper(source, f"Really? {duel_price} points?") # bot.whisper( # user, # f"{source} tried to duel you for {duel_price} points. What a cheapskate EleGiggle", # ) return False except InvalidPointAmount as e: bot.whisper(source, f"{e}. Usage: !duel USERNAME POINTS") return False if source.id in self.duel_requests: currently_duelling = User.find_by_id( db_session, self.duel_requests[source.id]) if currently_duelling is None: del self.duel_requests[source.id] return False bot.whisper( source, f"You already have a duel request active with {currently_duelling}. Type !cancelduel to cancel your duel request.", ) return False if user == source: # You cannot duel yourself return False if user.last_active is None or ( utils.now() - user.last_active) > timedelta(minutes=5): bot.whisper( source, "This user has not been active in chat within the last 5 minutes. Get them to type in chat before sending another challenge", ) return False if user.login in self.blUsers: return True if not user.can_afford(duel_price) or not source.can_afford( duel_price): bot.whisper( source, f"You or your target do not have more than {duel_price} points, therefore you cannot duel for that amount.", ) return False if user.id in self.duel_targets: challenged_by = User.find_by_id(db_session, self.duel_requests[user.id]) bot.whisper( source, f"This person is already being challenged by {challenged_by}. Ask them to answer the offer by typing !deny or !accept", ) return False self.duel_targets[user.id] = source.id self.duel_requests[source.id] = user.id self.duel_request_price[source.id] = duel_price self.duel_begin_time[source.id] = utils.now() bot.whisper( user, f"You have been challenged to a duel by {source} for {duel_price} points. You can either !accept or !deny this challenge.", ) bot.whisper(source, f"You have challenged {user} for {duel_price} points") def cancel_duel(self, bot, source, **rest): """ Cancel any duel requests you've sent. How to use: !cancelduel """ if source.id not in self.duel_requests: bot.whisper(source, "You have not sent any duel requests") return with DBManager.create_session_scope() as db_session: challenged = User.find_by_id(db_session, self.duel_requests[source.id]) bot.whisper(source, f"You have cancelled the duel vs {challenged}") del self.duel_targets[challenged.id] del self.duel_request_price[source.id] del self.duel_begin_time[source.id] del self.duel_requests[source.id] def accept_duel(self, bot, source, **rest): """ Accepts any active duel requests you've received. How to use: !accept """ if source.id not in self.duel_targets: bot.whisper(source, "You are not being challenged to a duel by anyone.") return with DBManager.create_session_scope() as db_session: requestor = User.find_by_id(db_session, self.duel_targets[source.id]) duel_price = self.duel_request_price[self.duel_targets[source.id]] if not source.can_afford(duel_price) or not requestor.can_afford( duel_price): bot.whisper( source, f"Your duel request with {requestor} was cancelled due to one of you not having enough points.", ) bot.whisper( requestor, f"Your duel request with {source} was cancelled due to one of you not having enough points.", ) del self.duel_requests[requestor.id] del self.duel_request_price[requestor.id] del self.duel_begin_time[requestor.id] del self.duel_targets[source.id] return False source.points -= duel_price requestor.points -= duel_price winning_pot = int(duel_price * (1.0 - self.settings["duel_tax"] / 100)) participants = [source, requestor] winner = random.choice(participants) participants.remove(winner) loser = participants.pop() winner.points += duel_price winner.points += winning_pot # Persist duel statistics winner.duel_stats.won(winning_pot) loser.duel_stats.lost(duel_price) arguments = { "winner": winner.name, "loser": loser.name, "total_pot": duel_price, "extra_points": winning_pot, } if duel_price > 0: message = self.get_phrase("message_won_points", **arguments) if duel_price >= 500 and self.settings["show_on_clr"]: bot.websocket_manager.emit( "notification", {"message": f"{winner} won the duel vs {loser}"}) else: message = self.get_phrase("message_won", **arguments) bot.say(message) del self.duel_requests[requestor.id] del self.duel_request_price[requestor.id] del self.duel_begin_time[requestor.id] del self.duel_targets[source.id] HandlerManager.trigger("on_duel_complete", winner=winner, loser=loser, points_won=winning_pot, points_bet=duel_price) def decline_duel(self, bot, source, **options): """ Declines any active duel requests you've received. How to use: !decline """ if source.id not in self.duel_targets: bot.whisper(source, "You are not being challenged to a duel") return False with DBManager.create_session_scope() as db_session: requestor = User.find_by_id(db_session, self.duel_targets[source.id]) bot.whisper(source, f"You have declined the duel vs {requestor}") bot.whisper(requestor, f"{source} declined the duel challenge with you.") del self.duel_targets[source.id] del self.duel_requests[requestor.id] del self.duel_request_price[requestor.id] del self.duel_begin_time[requestor.id] def status_duel(self, bot, source, **rest): """ Whispers you the current status of your active duel requests/duel targets How to use: !duelstatus """ with DBManager.create_session_scope() as db_session: msg = [] if source.id in self.duel_requests: duelling = User.find_by_id(db_session, self.duel_requests[source.id]) msg.append( f"You have a duel request for {self.duel_request_price[source.id]} points by {duelling}" ) if source.id in self.duel_targets: challenger = User.find_by_id(db_session, self.duel_targets[source.id]) msg.append( f"You have a pending duel request from {challenger} for {self.duel_request_price[self.duel_targets[source.id]]} points" ) if len(msg) > 0: bot.whisper(source, ". ".join(msg)) else: bot.whisper( source, "You have no duel request or duel target. Type !duel USERNAME POT to duel someone!" ) @staticmethod def get_duel_stats(bot, source, **rest): """ Whispers the users duel winratio to the user """ if source.duel_stats is None: bot.whisper(source, "You have no recorded duels.") return True bot.whisper( source, f"duels: {source.duel_stats.duels_total} winrate: {source.duel_stats.winrate:.2f}% streak: {source.duel_stats.current_streak} profit: {source.duel_stats.profit}", ) def _cancel_expired_duels(self): now = utils.now() for source_id, started_at in self.duel_begin_time.items(): duel_age = now - started_at if duel_age <= timedelta(minutes=self.settings["max_duel_age"]): # Duel is not too old continue with DBManager.create_session_scope() as db_session: source = User.find_by_id(db_session, source_id) challenged = User.find_by_id(db_session, self.duel_requests[source.id]) if source is not None and challenged is not None: self.bot.whisper( source, f"{challenged} didn't accept your duel request in time, so the duel has been cancelled. Ditched pepeLaugh", ) del self.duel_targets[self.duel_requests[source.id]] del self.duel_requests[source.id] del self.duel_request_price[source.id] del self.duel_begin_time[source.id] def enable(self, bot): if not bot: return # We can't use bot.execute_every directly since we can't later cancel jobs created through bot.execute_every self.gc_job = ScheduleManager.execute_every( 30, lambda: self.bot.execute_now(self._cancel_expired_duels)) def disable(self, bot): if not bot: return self.gc_job.remove() self.gc_job = None
class ActionCheckerModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Action Command Moderation" DESCRIPTION = "Dis/allows messages who use the /me command." CATEGORY = "Moderation" SETTINGS = [ ModuleSetting( key="only_allow_action_messages", label="Only allow /me messages", type="boolean", required=True, default=False, ), ModuleSetting( key="allow_timeout_reason", label="Timeout Reason", type="text", required=False, placeholder="", default="Lack of /me usage", constraints={}, ), ModuleSetting( key="disallow_action_messages", label="Disallow /me messages", type="boolean", required=True, default=True, ), ModuleSetting( key="disallow_timeout_reason", label="Timeout Reason", type="text", required=False, placeholder="", default="No /me usage allowed!", constraints={}, ), ModuleSetting( key="enabled_by_stream_status", label="Enable moderation of the /me command when the stream is:", type="options", required=True, default="Offline and Online", options=["Online Only", "Offline Only", "Offline and Online"], ), ModuleSetting( key="moderation_action", label="Moderation action to apply", type="options", required=True, default="Timeout", options=["Delete", "Timeout"], ), ModuleSetting( key="timeout_length", label="Timeout length", type="number", required=True, placeholder="Timeout length in seconds", default=30, constraints={"min_value": 30, "max_value": 3600}, ), ModuleSetting( key="bypass_level", label="Level to bypass module", type="number", required=True, placeholder="", default=500, constraints={"min_value": 100, "max_value": 1000}, ), ] def delete_or_timeout(self, user, msg_id, reason): if self.settings["moderation_action"] == "Delete": self.bot.delete_message(msg_id) elif self.settings["moderation_action"] == "Timeout": self.bot.timeout(user, self.settings["timeout_length"], reason, once=True) def on_message(self, source, message, event, msg_id, **rest): if self.settings["enabled_by_stream_status"] == "Online Only" and not self.bot.is_online: return if self.settings["enabled_by_stream_status"] == "Offline Only" and self.bot.is_online: return if source.level >= self.settings["bypass_level"] or source.moderator is True: return if event.type == "action" and self.settings["disallow_action_messages"] is True: self.delete_or_timeout(source, msg_id, self.settings["disallow_timeout_reason"]) return False elif event.type != "action" and self.settings["only_allow_action_messages"] is True: self.delete_or_timeout(source, msg_id, self.settings["allow_timeout_reason"]) return False def enable(self, bot): HandlerManager.add_handler("on_message", self.on_message) def disable(self, bot): HandlerManager.remove_handler("on_message", self.on_message)
class PlaySoundTokenCommandModule(BaseModule): ID = 'tokencommand-' + __name__.split('.')[-1] NAME = '!playsound' DESCRIPTION = 'Play a sound on stream' SETTINGS = [ ModuleSetting(key='point_cost', label='Point cost', type='number', required=True, placeholder='Point cost', default=0, constraints={ 'min_value': 0, 'max_value': 999999, }), ModuleSetting(key='token_cost', label='Token cost', type='number', required=True, placeholder='Token cost', default=3, constraints={ 'min_value': 0, 'max_value': 15, }), ModuleSetting(key='sample_cd', label='Cooldown for the same sample (seconds)', type='number', required=True, placeholder='', default=20, constraints={ 'min_value': 5, 'max_value': 120, }), ModuleSetting(key='sub_only', label='Subscribers only', type='boolean', required=True, default=True), ModuleSetting(key='global_cd', label='Global playsound cooldown (seconds)', type='number', required=True, placeholder='', default=2, constraints={ 'min_value': 0, 'max_value': 600, }), ] def __init__(self): super().__init__() self.valid_samples = [] self.sample_cache = [] possibleCategories = [ 'bulldog', 'gachi', 'others', 'personalities', 'weeb' ] for category in possibleCategories: for sampleName, sampleURL in RedisManager.get().hgetall( 'playsounds:{}'.format(category)).items(): self.valid_samples.append(sampleName) def refresh_sounds(self, **options): self.valid_samples = [] possibleCategories = [ 'bulldog', 'gachi', 'others', 'personalities', 'weeb' ] for category in possibleCategories: for sampleName, sampleURL in RedisManager.get().hgetall( 'playsounds:{}'.format(category)).items(): self.valid_samples.append(sampleName) def play_sound(self, **options): bot = options['bot'] message = options['message'] source = options['source'] if message: sample = message.split(' ')[0].lower() if sample in self.sample_cache: bot.whisper( source.username, 'The sample {0} was played too recently. Please wait before trying to use it again' .format(sample)) return False if sample == 'random': sample = random.choice(self.valid_samples) if sample in self.valid_samples: log.debug('Played sound: {0}'.format(sample)) bot.whisper(source.username, 'Played sound: {}'.format(sample)) payload = {'sample': sample} bot.websocket_manager.emit('play_sound', payload) if not (source.username.lower() == 'datguy1' or source.username.lower() == 'admiralbulldog') or True: self.sample_cache.append(sample) bot.execute_delayed(self.settings['sample_cd'], self.sample_cache.remove, ('{0}'.format(sample), )) return True bot.whisper( source.username, 'Your sample is not valid. Check out all the valid samples here: http://chatbot.admiralbulldog.live/playsound' ) return False def load_commands(self, **options): self.commands['playsound'] = pajbot.models.command.Command.raw_command( self.play_sound, cost=self.settings['point_cost'], sub_only=self.settings['sub_only'], delay_all=self.settings['global_cd'], description='Play a sound on stream! Costs {} points.'.format( self.settings['point_cost']), can_execute_with_whisper=True, examples=[ pajbot.models.command.CommandExample( None, 'Play the "cumming" sample', chat='user:!playsound cumming\n' 'bot>user:Successfully played your sample cumming').parse( ), pajbot.models.command.CommandExample( None, 'Play the "fuckyou" sample', chat='user:!playsound fuckyou\n' 'bot>user:Successfully played your sample fuckyou').parse( ), ], ) self.commands[ 'playsound'].long_description = 'Playsounds can be tried out <a href="http://chatbot.admiralbulldog.live/playsound">here</a>' self.commands[ 'refreshsound'] = pajbot.models.command.Command.raw_command( self.refresh_sounds, level=500, can_execute_with_whisper=True)
class RaffleModule(BaseModule): ID = __name__.split('.')[-1] NAME = 'Raffle' DESCRIPTION = 'Users can participate in a raffle to win points.' CATEGORY = 'Game' SETTINGS = [ ModuleSetting( key='single_max_points', label='Max points for a single raffle', type='number', required=True, placeholder='', default=3000, constraints={ 'min_value': 0, 'max_value': 35000, }), ModuleSetting( key='max_length', label='Max length for a single raffle in seconds', type='number', required=True, placeholder='', default=120, constraints={ 'min_value': 0, 'max_value': 1200, }), ModuleSetting( key='allow_negative_raffles', label='Allow negative raffles', type='boolean', required=True, default=True), ModuleSetting( key='max_negative_points', label='Max negative points for a single raffle', type='number', required=True, placeholder='', default=3000, constraints={ 'min_value': 1, 'max_value': 35000, }), ModuleSetting( key='multi_enabled', label='Enable multi-raffles (!multiraffle/!mraffle)', type='boolean', required=True, default=True), ModuleSetting( key='multi_max_points', label='Max points for a multi raffle', type='number', required=True, placeholder='', default=100000, constraints={ 'min_value': 0, 'max_value': 1000000, }), ModuleSetting( key='multi_max_length', label='Max length for a multi raffle in seconds', type='number', required=True, placeholder='', default=600, constraints={ 'min_value': 0, 'max_value': 1200, }), ModuleSetting( key='multi_allow_negative_raffles', label='Allow negative multi raffles', type='boolean', required=True, default=True), ModuleSetting( key='multi_max_negative_points', label='Max negative points for a multi raffle', type='number', required=True, placeholder='', default=10000, constraints={ 'min_value': 1, 'max_value': 100000, }), ModuleSetting( key='multi_raffle_on_sub', label='Start a multi raffle when someone subscribes', type='boolean', required=True, default=False), ModuleSetting( key='default_raffle_type', label='Default raffle (What raffle type !raffle should invoke)', type='options', required=True, default='Single Raffle', options=[ 'Single Raffle', 'Multi Raffle', ]), ] def __init__(self): super().__init__() self.raffle_running = False self.raffle_users = [] self.raffle_points = 0 self.raffle_length = 0 def load_commands(self, **options): self.commands['singleraffle'] = Command.raw_command(self.raffle, delay_all=0, delay_user=0, level=500, description='Start a raffle for points', command='raffle', examples=[ CommandExample(None, 'Start a raffle for 69 points', chat='user:!raffle 69\n' 'bot:A raffle has begun for 69 points. Type !join to join the raffle! The raffle will end in 60 seconds.', description='Start a 60-second raffle for 69 points').parse(), CommandExample(None, 'Start a raffle with a different length', chat='user:!raffle 69 30\n' 'bot:A raffle has begun for 69 points. Type !join to join the raffle! The raffle will end in 30 seconds.', description='Start a 30-second raffle for 69 points').parse(), ], ) self.commands['sraffle'] = self.commands['singleraffle'] self.commands['join'] = Command.raw_command(self.join, delay_all=0, delay_user=5, description='Join a running raffle', examples=[ CommandExample(None, 'Join a running raffle', chat='user:!join', description='You don\'t get confirmation whether you joined the raffle or not.').parse(), ], ) if self.settings['multi_enabled']: self.commands['multiraffle'] = Command.raw_command(self.multi_raffle, delay_all=0, delay_user=0, level=500, description='Start a multi-raffle for points', command='multiraffle', examples=[ CommandExample(None, 'Start a multi-raffle for 69 points', chat='user:!multiraffle 69\n' 'bot:A multi-raffle has begun for 69 points. Type !join to join the raffle! The raffle will end in 60 seconds.', description='Start a 60-second raffle for 69 points').parse(), CommandExample(None, 'Start a multi-raffle with a different length', chat='user:!multiraffle 69 30\n' 'bot:A multi-raffle has begun for 69 points. Type !join to join the raffle! The raffle will end in 30 seconds.', description='Start a 30-second multi-raffle for 69 points').parse(), ], ) self.commands['mraffle'] = self.commands['multiraffle'] if self.settings['default_raffle_type'] == 'Multi Raffle' and self.settings['multi_enabled']: self.commands['raffle'] = self.commands['multiraffle'] else: self.commands['raffle'] = self.commands['singleraffle'] def raffle(self, **options): bot = options['bot'] source = options['source'] message = options['message'] if self.raffle_running is True: bot.say('{0}, a raffle is already running OMGScoots'.format(source.username_raw)) return False self.raffle_users = [] self.raffle_running = True self.raffle_points = 100 self.raffle_length = 60 try: if message is not None and self.settings['allow_negative_raffles'] is True: self.raffle_points = int(message.split()[0]) if message is not None and self.settings['allow_negative_raffles'] is False: if int(message.split()[0]) >= 0: self.raffle_points = int(message.split()[0]) except (IndexError, ValueError, TypeError): pass try: if message is not None: if int(message.split()[1]) >= 5: self.raffle_length = int(message.split()[1]) except (IndexError, ValueError, TypeError): pass if self.raffle_points >= 0: self.raffle_points = min(self.raffle_points, self.settings['single_max_points']) if self.raffle_points <= -1: self.raffle_points = max(self.raffle_points, -self.settings['max_negative_points']) self.raffle_length = min(self.raffle_length, self.settings['max_length']) bot.websocket_manager.emit('notification', {'message': 'A raffle has been started!'}) bot.execute_delayed(0.75, bot.websocket_manager.emit, ('notification', {'message': 'Type !join to enter!'})) bot.me('A raffle has begun for {} points. type !join to join the raffle! The raffle will end in {} seconds'.format(self.raffle_points, self.raffle_length)) bot.execute_delayed(self.raffle_length * 0.25, bot.me, ('The raffle for {} points ends in {} seconds! Type !join to join the raffle!'.format(self.raffle_points, round(self.raffle_length * 0.75)), )) bot.execute_delayed(self.raffle_length * 0.50, bot.me, ('The raffle for {} points ends in {} seconds! Type !join to join the raffle!'.format(self.raffle_points, round(self.raffle_length * 0.50)), )) bot.execute_delayed(self.raffle_length * 0.75, bot.me, ('The raffle for {} points ends in {} seconds! Type !join to join the raffle!'.format(self.raffle_points, round(self.raffle_length * 0.25)), )) bot.execute_delayed(self.raffle_length, self.end_raffle) def join(self, **options): source = options['source'] if not self.raffle_running: return False for user in self.raffle_users: if user == source: return False # Added user to the raffle self.raffle_users.append(source) def end_raffle(self): if not self.raffle_running: return False self.raffle_running = False if len(self.raffle_users) == 0: self.bot.me('Wow, no one joined the raffle DansGame') return False winner = random.choice(self.raffle_users) self.raffle_users = [] self.bot.websocket_manager.emit('notification', {'message': '{} won {} points in the raffle!'.format(winner.username_raw, self.raffle_points)}) self.bot.me('The raffle has finished! {0} won {1} points! PogChamp'.format(winner.username_raw, self.raffle_points)) winner.points += self.raffle_points HandlerManager.trigger('on_raffle_win', winner, self.raffle_points) def multi_start_raffle(self, points, length): if self.raffle_running: return False self.raffle_users = [] self.raffle_running = True self.raffle_points = points self.raffle_length = length if self.raffle_points >= 0: self.raffle_points = min(self.raffle_points, self.settings['multi_max_points']) if self.raffle_points <= -1: self.raffle_points = max(self.raffle_points, -self.settings['multi_max_negative_points']) self.raffle_length = min(self.raffle_length, self.settings['multi_max_length']) self.bot.websocket_manager.emit('notification', {'message': 'A raffle has been started!'}) self.bot.execute_delayed(0.75, self.bot.websocket_manager.emit, ('notification', {'message': 'Type !join to enter!'})) self.bot.me('A multi-raffle has begun, {} points will be split among the winners. type !join to join the raffle! The raffle will end in {} seconds'.format(self.raffle_points, self.raffle_length)) self.bot.execute_delayed(self.raffle_length * 0.25, self.bot.me, ('The multi-raffle for {} points ends in {} seconds! Type !join to join the raffle!'.format(self.raffle_points, round(self.raffle_length * 0.75)), )) self.bot.execute_delayed(self.raffle_length * 0.50, self.bot.me, ('The multi-raffle for {} points ends in {} seconds! Type !join to join the raffle!'.format(self.raffle_points, round(self.raffle_length * 0.50)), )) self.bot.execute_delayed(self.raffle_length * 0.75, self.bot.me, ('The multi-raffle for {} points ends in {} seconds! Type !join to join the raffle!'.format(self.raffle_points, round(self.raffle_length * 0.25)), )) self.bot.execute_delayed(self.raffle_length, self.multi_end_raffle) def multi_raffle(self, **options): bot = options['bot'] source = options['source'] message = options['message'] if self.raffle_running is True: bot.say('{0}, a raffle is already running OMGScoots'.format(source.username_raw)) return False points = 100 try: if message is not None and self.settings['multi_allow_negative_raffles'] is True: points = int(message.split()[0]) if message is not None and self.settings['multi_allow_negative_raffles'] is False: if int(message.split()[0]) >= 0: points = int(message.split()[0]) except (IndexError, ValueError, TypeError): pass length = 60 try: if message is not None: if int(message.split()[1]) >= 5: length = int(message.split()[1]) except (IndexError, ValueError, TypeError): pass self.multi_start_raffle(points, length) def multi_end_raffle(self): if not self.raffle_running: return False self.raffle_running = False if len(self.raffle_users) == 0: self.bot.me('Wow, no one joined the raffle DansGame') return False # Shuffle the list of participants random.shuffle(self.raffle_users) num_participants = len(self.raffle_users) abs_points = abs(self.raffle_points) max_winners = min(num_participants, 200) min_point_award = 100 negative = self.raffle_points < 0 # Decide how we should pick the winners log.info('Num participants: {}'.format(num_participants)) for winner_percentage in [x * 0.01 for x in range(1, 26)]: log.info('Winner percentage: {}'.format(winner_percentage)) num_winners = math.ceil(num_participants * winner_percentage) points_per_user = math.ceil(abs_points / num_winners) log.info('nw: {}, ppu: {}'.format(num_winners, points_per_user)) if num_winners > max_winners: num_winners = max_winners points_per_user = math.ceil(abs_points / num_winners) break elif points_per_user < min_point_award: num_winners = max(1, min(math.floor(abs_points / min_point_award), num_participants)) points_per_user = math.ceil(abs_points / num_winners) break log.info('k done. got {} winners'.format(num_winners)) winners = self.raffle_users[:num_winners] self.raffle_users = [] if negative: points_per_user *= -1 self.bot.me('The multi-raffle has finished! {0} users won {1} points each! PogChamp'.format(len(winners), points_per_user)) winners_arr = [] for winner in winners: winner.points += points_per_user winners_arr.append(winner) winners_str = generate_winner_list(winners_arr) if len(winners_str) > 300: self.bot.me('{} won {} points each!'.format(winners_str, points_per_user)) winners_arr = [] if len(winners_arr) > 0: winners_str = generate_winner_list(winners_arr) self.bot.me('{} won {} points each!'.format(winners_str, points_per_user)) HandlerManager.trigger('on_multiraffle_win', winners, points_per_user) def on_user_sub(self, user): if self.settings['multi_raffle_on_sub'] is False: return MAX_REWARD = 10000 points = StreamHelper.get_viewers() * 5 if points == 0: points = 100 length = 30 points = min(points, MAX_REWARD) self.multi_start_raffle(points, length) def on_user_resub(self, user, num_months): if self.settings['multi_raffle_on_sub'] is False: return MAX_REWARD = 10000 points = StreamHelper.get_viewers() * 5 if points == 0: points = 100 length = 30 points = min(points, MAX_REWARD) points += (num_months - 1) * 500 self.multi_start_raffle(points, length) def enable(self, bot): self.bot = bot HandlerManager.add_handler('on_user_sub', self.on_user_sub) HandlerManager.add_handler('on_user_resub', self.on_user_resub) def disable(self, bot): HandlerManager.remove_handler('on_user_sub', self.on_user_sub) HandlerManager.remove_handler('on_user_resub', self.on_user_resub)
class LinkCheckerModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Link Checker" DESCRIPTION = "Checks links if they're bad" ENABLED_DEFAULT = True CATEGORY = "Filter" SETTINGS = [ ModuleSetting( key="ban_pleb_links", label="Disallow links from non-subscribers", type="boolean", required=True, default=False, ), ModuleSetting(key="ban_sub_links", label="Disallow links from subscribers", type="boolean", required=True, default=False), ModuleSetting( key="timeout_length", label="Timeout length", type="number", required=True, placeholder="Timeout length in seconds", default=60, constraints={ "min_value": 1, "max_value": 3600 }, ), ModuleSetting( key="bypass_level", label="Level to bypass module", type="number", required=True, placeholder="", default=500, constraints={ "min_value": 100, "max_value": 1000 }, ), ] def __init__(self, bot): super().__init__(bot) self.db_session = None self.links = {} self.blacklisted_links = [] self.whitelisted_links = [] self.cache = LinkCheckerCache( ) # cache[url] = True means url is safe, False means the link is bad if bot and "safebrowsingapi" in bot.config["main"]: # XXX: This should be loaded as a setting instead. # There needs to be a setting for settings to have them as "passwords" # so they're not displayed openly self.safe_browsing_api = SafeBrowsingAPI( bot.config["main"]["safebrowsingapi"]) else: self.safe_browsing_api = None def enable(self, bot): if not bot: return HandlerManager.add_handler("on_message", self.on_message, priority=100) HandlerManager.add_handler("on_commit", self.on_commit) if self.db_session is not None: self.db_session.commit() self.db_session.close() self.db_session = None self.db_session = DBManager.create_session() self.blacklisted_links = [] for link in self.db_session.query(BlacklistedLink): self.blacklisted_links.append(link) self.whitelisted_links = [] for link in self.db_session.query(WhitelistedLink): self.whitelisted_links.append(link) def disable(self, bot): if not bot: return pajbot.managers.handler.HandlerManager.remove_handler( "on_message", self.on_message) pajbot.managers.handler.HandlerManager.remove_handler( "on_commit", self.on_commit) if self.db_session is not None: self.db_session.commit() self.db_session.close() self.db_session = None self.blacklisted_links = [] self.whitelisted_links = [] def reload(self): log.info( f"Loaded {len(self.blacklisted_links)} bad links and {len(self.whitelisted_links)} good links" ) return self super_whitelist = ["pajlada.se", "pajlada.com", "forsen.tv", "pajbot.com"] def on_message(self, source, whisper, urls, **rest): if whisper: return if source.level >= self.settings[ "bypass_level"] or source.moderator is True: return if len(urls) > 0: do_timeout = False ban_reason = "You are not allowed to post links in chat" whisper_reason = "??? KKona" if self.settings[ "ban_pleb_links"] is True and source.subscriber is False: do_timeout = True whisper_reason = "You cannot post non-verified links in chat if you're a pleb" elif self.settings[ "ban_sub_links"] is True and source.subscriber is True: do_timeout = True whisper_reason = "You cannot post non-verified links in chat if you're a subscriber" if do_timeout is True: # Check if the links are in our super-whitelist. i.e. on the pajlada.se domain o forsen.tv for url in urls: parsed_url = Url(url) if len(parsed_url.parsed.netloc.split(".")) < 2: continue whitelisted = False for whitelist in self.super_whitelist: if is_subdomain(parsed_url.parsed.netloc, whitelist): whitelisted = True break if whitelisted is False and self.is_whitelisted(url): whitelisted = True if whitelisted is False: self.bot.timeout(source, self.settings["timeout_length"], reason=ban_reason) if source.time_in_chat_online >= timedelta(hours=1): self.bot.whisper(source, whisper_reason) return False for url in urls: # Action which will be taken when a bad link is found def action(): self.bot.timeout(source, self.settings["timeout_length"], reason="Banned link") # First we perform a basic check if self.simple_check(url, action) == self.RET_FURTHER_ANALYSIS: # If the basic check returns no relevant data, we queue up a proper check on the URL self.bot.action_queue.submit(self.check_url, url, action) def on_commit(self, **rest): if self.db_session is not None: self.db_session.commit() def delete_from_cache(self, url): if url in self.cache: del self.cache[url] def cache_url(self, url, safe): if url in self.cache and self.cache[url] == safe: return self.cache[url] = safe self.bot.execute_delayed(20, self.delete_from_cache, url) def counteract_bad_url(self, url, action=None, want_to_cache=True, want_to_blacklist=False): log.debug(f"LinkChecker: BAD URL FOUND {url.url}") if action: action() if want_to_cache: self.cache_url(url.url, False) if want_to_blacklist: self.blacklist_url(url.url, url.parsed) return True def blacklist_url(self, url, parsed_url=None, level=0): if not (url.lower().startswith("http://") or url.lower().startswith("https://")): url = "http://" + url if parsed_url is None: parsed_url = urllib.parse.urlparse(url) if self.is_blacklisted(url, parsed_url): return False domain = parsed_url.netloc.lower() path = parsed_url.path.lower() if domain.startswith("www."): domain = domain[4:] if path.endswith("/"): path = path[:-1] if path == "": path = "/" link = BlacklistedLink(domain, path, level) self.db_session.add(link) self.blacklisted_links.append(link) self.db_session.commit() def whitelist_url(self, url, parsed_url=None): if not (url.lower().startswith("http://") or url.lower().startswith("https://")): url = "http://" + url if parsed_url is None: parsed_url = urllib.parse.urlparse(url) if self.is_whitelisted(url, parsed_url): return domain = parsed_url.netloc.lower() path = parsed_url.path.lower() if domain.startswith("www."): domain = domain[4:] if path.endswith("/"): path = path[:-1] if path == "": path = "/" link = WhitelistedLink(domain, path) self.db_session.add(link) self.whitelisted_links.append(link) self.db_session.commit() def is_blacklisted(self, url, parsed_url=None, sublink=False): if parsed_url is None: parsed_url = urllib.parse.urlparse(url) domain = parsed_url.netloc.lower() path = parsed_url.path.lower() if path == "": path = "/" domain_split = domain.split(".") if len(domain_split) < 2: return False for link in self.blacklisted_links: if link.is_subdomain(domain): if link.is_subpath(path): if not sublink: return True elif ( link.level >= 1 ): # if it's a sublink, but the blacklisting level is 0, we don't consider it blacklisted return True return False def is_whitelisted(self, url, parsed_url=None): if parsed_url is None: parsed_url = urllib.parse.urlparse(url) domain = parsed_url.netloc.lower() path = parsed_url.path.lower() if path == "": path = "/" domain_split = domain.split(".") if len(domain_split) < 2: return False for link in self.whitelisted_links: if link.is_subdomain(domain): if link.is_subpath(path): return True return False RET_BAD_LINK = -1 RET_FURTHER_ANALYSIS = 0 RET_GOOD_LINK = 1 def basic_check(self, url, action, sublink=False): """ Check if the url is in the cache, or if it's Return values: 1 = Link is OK -1 = Link is bad 0 = Link needs further analysis """ if url.url in self.cache: if not self.cache[url.url]: # link is bad self.counteract_bad_url(url, action, False, False) return self.RET_BAD_LINK return self.RET_GOOD_LINK if self.is_blacklisted(url.url, url.parsed, sublink): self.counteract_bad_url(url, action, want_to_blacklist=False) return self.RET_BAD_LINK if self.is_whitelisted(url.url, url.parsed): self.cache_url(url.url, True) return self.RET_GOOD_LINK return self.RET_FURTHER_ANALYSIS def simple_check(self, url, action): url = Url(url) if len(url.parsed.netloc.split(".")) < 2: # The URL is broken, ignore it return self.RET_FURTHER_ANALYSIS return self.basic_check(url, action) def check_url(self, url, action): url = Url(url) if len(url.parsed.netloc.split(".")) < 2: # The URL is broken, ignore it return try: self._check_url(url, action) except: log.exception("LinkChecker unhandled exception while _check_url") def _check_url(self, url, action): # XXX: The basic check is currently performed twice on links found in messages. Solve res = self.basic_check(url, action) if res == self.RET_GOOD_LINK: return elif res == self.RET_BAD_LINK: return connection_timeout = 2 read_timeout = 1 try: r = requests.head(url.url, allow_redirects=True, timeout=connection_timeout, headers={"User-Agent": self.bot.user_agent}) except: self.cache_url(url.url, True) return checkcontenttype = "content-type" in r.headers and r.headers[ "content-type"] == "application/octet-stream" checkdispotype = "disposition-type" in r.headers and r.headers[ "disposition-type"] == "attachment" if checkcontenttype or checkdispotype: # triggering a download not allowed self.counteract_bad_url(url, action) return redirected_url = Url(r.url) if is_same_url(url, redirected_url) is False: res = self.basic_check(redirected_url, action) if res == self.RET_GOOD_LINK: return elif res == self.RET_BAD_LINK: return if self.safe_browsing_api and self.safe_browsing_api.is_url_bad( redirected_url.url): # harmful url detected log.debug("Google Safe Browsing API lists URL") self.counteract_bad_url(url, action, want_to_blacklist=False) self.counteract_bad_url(redirected_url, want_to_blacklist=False) return if "content-type" not in r.headers or not r.headers[ "content-type"].startswith("text/html"): return # can't analyze non-html content maximum_size = 1024 * 1024 * 10 # 10 MB receive_timeout = 3 html = "" try: response = requests.get( url=url.url, stream=True, timeout=(connection_timeout, read_timeout), headers={"User-Agent": self.bot.user_agent}, ) content_length = response.headers.get("Content-Length") if content_length and int( response.headers.get("Content-Length")) > maximum_size: log.error("This file is too big!") return size = 0 start = pajbot.utils.now().timestamp() for chunk in response.iter_content(1024): if pajbot.utils.now().timestamp() - start > receive_timeout: log.error("The site took too long to load") return size += len(chunk) if size > maximum_size: log.error("This file is too big! (fake header)") return html += str(chunk) except requests.exceptions.ConnectTimeout: log.warning(f"Connection timed out while checking {url.url}") self.cache_url(url.url, True) return except requests.exceptions.ReadTimeout: log.warning(f"Reading timed out while checking {url.url}") self.cache_url(url.url, True) return except: log.exception("Unhandled exception") return try: soup = BeautifulSoup(html, "html.parser") except: return original_url = url original_redirected_url = redirected_url urls = [] for link in soup.find_all( "a"): # get a list of links to external sites url = link.get("href") if url is None: continue if url.startswith("//"): urls.append("http:" + url) elif url.startswith("http://") or url.startswith("https://"): urls.append(url) for url in urls: # check if the site links to anything dangerous url = Url(url) if is_subdomain(url.parsed.netloc, original_url.parsed.netloc): # log.debug('Skipping because internal link') continue res = self.basic_check(url, action, sublink=True) if res == self.RET_BAD_LINK: self.counteract_bad_url(url) self.counteract_bad_url(original_url, want_to_blacklist=False) self.counteract_bad_url(original_redirected_url, want_to_blacklist=False) return elif res == self.RET_GOOD_LINK: continue try: r = requests.head( url.url, allow_redirects=True, timeout=connection_timeout, headers={"User-Agent": self.bot.user_agent}, ) except: continue redirected_url = Url(r.url) if not is_same_url(url, redirected_url): res = self.basic_check(redirected_url, action, sublink=True) if res == self.RET_BAD_LINK: self.counteract_bad_url(url) self.counteract_bad_url(original_url, want_to_blacklist=False) self.counteract_bad_url(original_redirected_url, want_to_blacklist=False) return elif res == self.RET_GOOD_LINK: continue if self.safe_browsing_api and self.safe_browsing_api.is_url_bad( redirected_url.url): # harmful url detected log.debug(f"Evil sublink {url} by google API") self.counteract_bad_url(original_url, action) self.counteract_bad_url(original_redirected_url) self.counteract_bad_url(url) self.counteract_bad_url(redirected_url) return # if we got here, the site is clean for our standards self.cache_url(original_url.url, True) self.cache_url(original_redirected_url.url, True) return def load_commands(self, **options): self.commands["add"] = Command.multiaction_command( level=100, delay_all=0, delay_user=0, default=None, command="add", commands={ "link": Command.multiaction_command( level=500, delay_all=0, delay_user=0, default=None, commands={ "blacklist": Command.raw_command( self.add_link_blacklist, level=500, delay_all=0, delay_user=0, description="Blacklist a link", examples=[ CommandExample( None, "Add a link to the blacklist for a shallow search", chat= "user:!add link blacklist --shallow scamlink.lonk/\n" "bot>user:Successfully added your links", description= "Added the link scamlink.lonk/ to the blacklist for a shallow search", ).parse(), CommandExample( None, "Add a link to the blacklist for a deep search", chat= "user:!add link blacklist --deep scamlink.lonk/\n" "bot>user:Successfully added your links", description= "Added the link scamlink.lonk/ to the blacklist for a deep search", ).parse(), ], ), "whitelist": Command.raw_command( self.add_link_whitelist, level=500, delay_all=0, delay_user=0, description="Whitelist a link", examples=[ CommandExample( None, "Add a link to the whitelist", chat= "user:!add link whitelink safelink.lonk/\n" "bot>user:Successfully added your links", description= "Added the link safelink.lonk/ to the whitelist", ).parse() ], ), }, ) }, ) self.commands["remove"] = Command.multiaction_command( level=100, delay_all=0, delay_user=0, default=None, command="remove", commands={ "link": Command.multiaction_command( level=500, delay_all=0, delay_user=0, default=None, commands={ "blacklist": Command.raw_command( self.remove_link_blacklist, level=500, delay_all=0, delay_user=0, description="Remove a link from the blacklist.", examples=[ CommandExample( None, "Remove a link from the blacklist.", chat="user:!remove link blacklist 20\n" "bot>user:Successfully removed blacklisted link with id 20", description= "Remove a link from the blacklist with an ID", ).parse() ], ), "whitelist": Command.raw_command( self.remove_link_whitelist, level=500, delay_all=0, delay_user=0, description="Remove a link from the whitelist.", examples=[ CommandExample( None, "Remove a link from the whitelist.", chat="user:!remove link whitelist 12\n" "bot>user:Successfully removed blacklisted link with id 12", description= "Remove a link from the whitelist with an ID", ).parse() ], ), }, ) }, ) def add_link_blacklist(self, bot, source, message, **rest): options, new_links = self.parse_link_blacklist_arguments(message) if new_links: parts = new_links.split(" ") try: for link in parts: if len(link) > 1: self.blacklist_url(link, **options) AdminLogManager.post("Blacklist link added", source, link) bot.whisper(source, "Successfully added your links") return True except: log.exception("Unhandled exception in add_link_blacklist") bot.whisper(source, "Some error occurred while adding your links") return False else: bot.whisper(source, "Usage: !add link blacklist LINK") return False def add_link_whitelist(self, bot, source, message, **rest): parts = message.split(" ") try: for link in parts: self.whitelist_url(link) AdminLogManager.post("Whitelist link added", source, link) except: log.exception("Unhandled exception in add_link") bot.whisper(source, "Some error occurred white adding your links") return False bot.whisper(source, "Successfully added your links") def remove_link_blacklist(self, bot, source, message, **rest): if not message: bot.whisper(source, "Usage: !remove link blacklist ID") return False id = None try: id = int(message) except ValueError: pass link = self.db_session.query(BlacklistedLink).filter_by( id=id).one_or_none() if link: self.blacklisted_links.remove(link) self.db_session.delete(link) self.db_session.commit() else: bot.whisper(source, "No link with the given id found") return False AdminLogManager.post("Blacklist link removed", source, link.domain) bot.whisper( source, f"Successfully removed blacklisted link with id {link.id}") def remove_link_whitelist(self, bot, source, message, **rest): if not message: bot.whisper(source, "Usage: !remove link whitelist ID") return False id = None try: id = int(message) except ValueError: pass link = self.db_session.query(WhitelistedLink).filter_by( id=id).one_or_none() if link: self.whitelisted_links.remove(link) self.db_session.delete(link) self.db_session.commit() else: bot.whisper(source, "No link with the given id found") return False AdminLogManager.post("Whitelist link removed", source, link.domain) bot.whisper( source, f"Successfully removed whitelisted link with id {link.id}") @staticmethod def parse_link_blacklist_arguments(message): parser = argparse.ArgumentParser() parser.add_argument("--deep", dest="level", action="store_true") parser.add_argument("--shallow", dest="level", action="store_false") parser.set_defaults(level=False) try: args, unknown = parser.parse_known_args(message.split()) except SystemExit: return False, False except: log.exception("Unhandled exception in add_link_blacklist") return False, False # Strip options of any values that are set as None options = {k: v for k, v in vars(args).items() if v is not None} response = " ".join(unknown) if "level" in options: options["level"] = int(options["level"]) return options, response
class SlotMachineModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Slot Machine" DESCRIPTION = "Lets players play slot machines for points" CATEGORY = "Game" SETTINGS = [ ModuleSetting( key="message_won", label= "Won message | Available arguments: {bet}, {points}, {user}, {emotes}, {result}", type="text", required=True, placeholder="{user} | {emotes} | won {result} points PogChamp", default="{user} | {emotes} | won {result} points PogChamp", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="message_lost", label= "Lost message | Available arguments: {bet}, {points}, {user}, {emotes}", type="text", required=True, placeholder="{user} | {emotes} | lost {bet} points LUL", default="{user} | {emotes} | lost {bet} points LUL", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="low_tier_emotes", label= "Low tier emotes, space-separated. Low-tier emote are 3 times as likely to appear as high tier emotes (they get 3 slots compared to high emotes 1 slot per roll)", type="text", required=True, placeholder="KKona 4Head NaM", default="KKona 4Head NaM", constraints={ "min_str_len": 0, "max_str_len": 400 }, ), ModuleSetting( key="high_tier_emotes", label="High tier emotes, space-separated", type="text", required=True, placeholder="OpieOP EleGiggle", default="OpieOP EleGiggle", constraints={ "min_str_len": 0, "max_str_len": 400 }, ), ModuleSetting( key="ltsw", label="Low tier small win (Percentage) 22.6% with 2 low 2 high", type="number", required=True, placeholder="", default=125, constraints={ "min_value": 0, "max_value": 1000000 }, ), ModuleSetting( key="ltbw", label="Low tier big win (Percentage) 0.98% with 2 low 2 high", type="number", required=True, placeholder="", default=175, constraints={ "min_value": 0, "max_value": 1000000 }, ), ModuleSetting( key="htsw", label="High tier small win (Percentage) 0.14% with 2 low 2 high", type="number", required=True, placeholder="", default=225, constraints={ "min_value": 0, "max_value": 1000000 }, ), ModuleSetting( key="htbw", label="High tier big win (Percentage) 0.07% with 2 low 2 high", type="number", required=True, placeholder="", default=400, constraints={ "min_value": 0, "max_value": 1000000 }, ), ModuleSetting( key="online_global_cd", label="Global cooldown (seconds)", type="number", required=True, placeholder="", default=0, constraints={ "min_value": 0, "max_value": 120 }, ), ModuleSetting( key="online_user_cd", label="Per-user cooldown (seconds)", type="number", required=True, placeholder="", default=60, constraints={ "min_value": 0, "max_value": 240 }, ), ModuleSetting( key="min_bet", label="Minimum bet", type="number", required=True, placeholder="", default=1, constraints={ "min_value": 1, "max_value": 1000000 }, ), ModuleSetting( key="can_execute_with_whisper", label="Allow users to use the module from whispers", type="boolean", required=True, default=False, ), ModuleSetting( key="options_output", label="Result output options", type="options", required=True, default="1. Show results in chat", options=[ "1. Show results in chat", "2. Show results in whispers", "3. Show results in chat if it's over X points else it will be whispered.", "4. Combine output in chat", ], ), ModuleSetting( key="min_show_points", label="Min points you need to win or lose (if options 3)", type="number", required=True, placeholder="", default=100, constraints={ "min_value": 1, "max_value": 1000000 }, ), ModuleSetting( key="only_slots_after_sub", label="Only allow slots after sub", type="boolean", required=True, default=False, ), ModuleSetting( key="after_sub_slots_time", label= "How long after a sub people can use the slot machine (seconds)", type="number", required=True, placeholder="", default=30, constraints={ "min_value": 5, "max_value": 3600 }, ), ModuleSetting( key="alert_message_after_sub", label= "Message to announce the allowance of slotmachine usage after re/sub, leave empty to disable the message. | Available arguments: {seconds}", type="text", required=True, default= "Slot machine is now allowed for {seconds} seconds! PogChamp", constraints={ "min_str_len": 0, "max_str_len": 300 }, ), ] def __init__(self, bot): super().__init__(bot) self.last_sub = None self.output_buffer = "" self.output_buffer_args = [] self.last_add = None def load_commands(self, **options): self.commands["slotmachine"] = Command.raw_command( self.pull, delay_all=self.settings["online_global_cd"], delay_user=self.settings["online_user_cd"], description="play slot machine for points", can_execute_with_whisper=self.settings["can_execute_with_whisper"], examples=[ CommandExample( None, "SlotMachine for 69 points", chat="user:!slotmachine 69\n" "bot:pajlada won 69 points in slotmachine xd! FeelsGoodMan", description="Do a slot machine pull for 69 points", ).parse() ], ) self.commands["smp"] = self.commands["slotmachine"] self.commands["slots"] = self.commands["slotmachine"] def pull(self, bot, source, message, **rest): if self.settings["only_slots_after_sub"]: if self.last_sub is None: return False if utils.now() - self.last_sub > datetime.timedelta( seconds=self.settings["after_sub_slots_time"]): return False if message is None: bot.whisper( source, "I didn't recognize your bet! Usage: !slotmachine 150 to bet 150 points" ) return False low_tier_emotes = self.settings["low_tier_emotes"].split() high_tier_emotes = self.settings["high_tier_emotes"].split() if len(low_tier_emotes) == 0 or len(high_tier_emotes) == 0: return False msg_split = message.split(" ") try: bet = pajbot.utils.parse_points_amount(source, msg_split[0]) except pajbot.exc.InvalidPointAmount as e: bot.whisper(source, str(e)) return False if not source.can_afford(bet): bot.whisper( source, f"You don't have enough points to do a slot machine pull for {bet} points :(" ) return False if bet < self.settings["min_bet"]: bot.whisper( source, f"You have to bet at least {self.settings['min_bet']} point! :(" ) return False # how much of the users point they're expected to get back (basically how much the house yoinks) expected_return = 1.0 ltsw = self.settings["ltsw"] / 100.0 htsw = self.settings["htsw"] / 100.0 ltbw = self.settings["ltbw"] / 100.0 htbw = self.settings["htbw"] / 100.0 bet_return, randomized_emotes = pull_lol(low_tier_emotes, high_tier_emotes, bet, expected_return, ltsw, htsw, ltbw, htbw) # Calculating the result if bet_return <= 0.0: points = -bet else: points = bet * bet_return source.points += points arguments = { "bet": bet, "result": points, "user": source.name, "points": source.points, "win": points > 0, "emotes": " ".join(randomized_emotes), } if points > 0: out_message = self.get_phrase("message_won", **arguments) else: out_message = self.get_phrase("message_lost", **arguments) if self.settings["options_output"] == "4. Combine output in chat": if bot.is_online: self.add_message(bot, arguments) else: bot.me(out_message) if self.settings["options_output"] == "1. Show results in chat": bot.me(out_message) if self.settings["options_output"] == "2. Show results in whispers": bot.whisper(source, out_message) if (self.settings["options_output"] == "3. Show results in chat if it's over X points else it will be whispered." ): if abs(points) >= self.settings["min_show_points"]: bot.me(out_message) else: bot.whisper(source, out_message) HandlerManager.trigger("on_slot_machine_finish", user=source, points=points) def on_tick(self, **rest): if self.output_buffer == "": return if self.last_add is None: return diff = utils.now() - self.last_add if diff.seconds > 3: self.flush_output_buffer() def flush_output_buffer(self): msg = self.output_buffer self.bot.me(msg) self.output_buffer = "" self.output_buffer_args = [] def add_message(self, bot, arguments): parts = [] new_buffer = "SlotMachine: " win_emote = "forsenPls" lose_emote = "forsenSWA" for arg in self.output_buffer_args: parts.append( f"{win_emote if arg['win'] else lose_emote} {arg['user']} {'+' if arg['win'] else '-'}{arg['bet']}" ) parts.append( f"{win_emote if arguments['win'] else lose_emote} {arguments['user']} {'+' if arguments['win'] else '-'}{arguments['bet']}" ) log.debug(parts) new_buffer += ", ".join(parts) if len(new_buffer) > 480: self.flush_output_buffer() else: self.output_buffer = new_buffer log.info("Set output buffer to " + new_buffer) self.output_buffer_args.append(arguments) self.last_add = utils.now() def on_user_sub_or_resub(self, **rest): now = utils.now() # True if we already announced the alert_message_after_sub within the last 5 seconds. Prevents # spam after bulk sub gifts. skip_message = self.last_sub is not None and now - self.last_sub < datetime.timedelta( seconds=5) self.last_sub = now if (self.settings["only_slots_after_sub"] and self.settings["alert_message_after_sub"] != "" and not skip_message): self.bot.say(self.settings["alert_message_after_sub"].format( seconds=self.settings["after_sub_slots_time"])) def enable(self, bot): HandlerManager.add_handler("on_user_sub", self.on_user_sub_or_resub) HandlerManager.add_handler("on_user_resub", self.on_user_sub_or_resub) HandlerManager.add_handler("on_tick", self.on_tick) def disable(self, bot): HandlerManager.remove_handler("on_user_sub", self.on_user_sub_or_resub) HandlerManager.remove_handler("on_user_resub", self.on_user_sub_or_resub) HandlerManager.remove_handler("on_tick", self.on_tick)
class AbCommandModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Add Between" DESCRIPTION = "Inject an emote inbetween each letter/word in message via the !ab command" CATEGORY = "Feature" PARENT_MODULE = BasicCommandsModule SETTINGS = [ ModuleSetting( key="level", label="minimum level (make sure people don't abuse this command)", type="number", required=True, placeholder="", default=250, constraints={ "min_value": 100, "max_value": 2000 }, ), ModuleSetting( key="global_cd", label="Global cooldown (seconds)", type="number", required=True, placeholder="", default=15, constraints={ "min_value": 0, "max_value": 240 }, ), ModuleSetting( key="user_cd", label="Per-user cooldown (seconds)", type="number", required=True, placeholder="", default=30, constraints={ "min_value": 0, "max_value": 240 }, ), ] @staticmethod def ab(bot, source, message, **rest): if not message: return False # check if there is a link in the message check_message = find_unique_urls(URL_REGEX, message) if len(check_message) > 0: return False msg_parts = message.split(" ") if len(msg_parts) >= 2: outer_str = msg_parts[0] inner_str = f" {outer_str} ".join( msg_parts[1:] if len(msg_parts) >= 3 else msg_parts[1]) bot.say(f"{source}, {outer_str} {inner_str} {outer_str}") def load_commands(self, **options): self.commands["ab"] = Command.raw_command( self.ab, delay_all=self.settings["global_cd"], delay_user=self.settings["user_cd"], level=self.settings["level"], description="Inject emote inbetween each letter/word in message", command="ab", examples=[ CommandExample( None, "Inject emote inbetween each letter in message", chat="user:!ab Keepo KEEPO\n" "bot:pajlada, Keepo K Keepo E Keepo E Keepo P Keepo O Keepo", description="", ).parse(), CommandExample( None, "Inject emote inbetween each word in message", chat="user:!ab Kreygasm NOW THATS WHAT I CALL MUSIC\n" "bot:pajlada, Kreygasm NOW Kreygasm THATS Kreygasm WHAT Kreygasm I Kreygasm CALL Kreygasm MUSIC Kreygasm", description="", ).parse(), ], ) self.commands["abc"] = self.commands["ab"]
class DubtrackModule(BaseModule): AUTHOR = 'TalVivian @ github.com/TalVivian' ID = __name__.split('.')[-1] NAME = 'Dubtrack module' DESCRIPTION = 'Gets currently playing song from dubtrack' CATEGORY = 'Feature' SETTINGS = [ ModuleSetting(key='room_name', label='Dubtrack room', type='text', required=True, placeholder='Dubtrack room (i.e. pajlada)', default='pajlada', constraints={ 'min_str_len': 1, 'max_str_len': 70, }), ModuleSetting( key='phrase_room_link', label='Room link message | Available arguments: {room_name}', type='text', required=True, placeholder= 'Request your songs at https://dubtrack.fm/join/{room_name}', default= 'Request your songs at https://dubtrack.fm/join/{room_name}', constraints={ 'min_str_len': 1, 'max_str_len': 400, }), ModuleSetting( key='phrase_current_song', label= 'Current song message | Available arguments: {song_name}, {song_link}', type='text', required=True, placeholder='Current song: {song_name}, link: {song_link}', default='Current song: {song_name}, link: {song_link}', constraints={ 'min_str_len': 1, 'max_str_len': 400, }), ModuleSetting( key='phrase_current_song_no_link', label= 'Current song message if no song link is available | Available arguments: {song_name}', type='text', required=True, placeholder='Current song: {song_name}', default='Current song: {song_name}', constraints={ 'min_str_len': 1, 'max_str_len': 400, }), ModuleSetting( key='phrase_no_current_song', label='Current song message when there\'s nothing playing', type='text', required=True, placeholder='There\'s no song playing right now FeelsBadMan', default='There\'s no song playing right now FeelsBadMan', constraints={ 'min_str_len': 1, 'max_str_len': 400, }), ModuleSetting(key='global_cd', label='Global cooldown (seconds)', type='number', required=True, placeholder='', default=5, constraints={ 'min_value': 0, 'max_value': 120, }), ModuleSetting(key='user_cd', label='Per-user cooldown (seconds)', type='number', required=True, placeholder='', default=15, constraints={ 'min_value': 0, 'max_value': 240, }), ModuleSetting( key='if_dt_alias', label='Allow !dt as !dubtrack', type='boolean', required=True, default=True, ), ModuleSetting( key='if_short_alias', label='Allow !dubtrack [s, l, u] as !dubtrack [song, link, update]', type='boolean', required=True, default=True, ), ModuleSetting( key='if_song_alias', label='Allow !song as !dubtrack song', type='boolean', required=True, default=True, ), ] def __init__(self, **options): super().__init__() self.clear() def link(self, **options): bot = options['bot'] arguments = {'room_name': self.settings['room_name']} bot.say(self.get_phrase('phrase_room_link', **arguments)) def clear(self): self.song_name = None self.song_id = None self.song_link = None def update_song(self, force=False): if force: self.clear() url = 'https://api.dubtrack.fm/room/' + self.settings['room_name'] r = requests.get(url) if r.status_code != 200: log.warning('Dubtrack api not responding') self.clear() return text = json.loads(r.text) if text['code'] != 200: log.warning('Dubtrack api invalid response') self.clear() return data = text['data']['currentSong'] if data is None: # No song playing self.clear() return if self.song_id == data['songid']: # No need to update song return raw_song_name = data['name'] self.song_name = html.unescape(raw_song_name) self.song_id = data['songid'] if data['type'] == 'youtube': self.song_link = 'https://youtu.be/' + data['fkid'] elif data['type'] == 'soundcloud': url = 'https://api.dubtrack.fm/song/' + data['songid'] + '/redirect' self.song_link = None r = requests.get(url, allow_redirects=False) if r.status_code != 301: log.warning('Couldn\'t resolve soundcloud link') return new_song_link = r.headers['Location'] self.song_link = re.sub('^http', 'https', new_song_link) else: log.warning('Unknown link type') self.song_link = None def say_song(self, bot): if self.song_name is None: bot.say(self.get_phrase('phrase_no_current_song')) return arguments = {'song_name': self.song_name} if self.song_link: arguments['song_link'] = self.song_link bot.say(self.get_phrase('phrase_current_song', **arguments)) else: bot.say(self.get_phrase('phrase_current_song_no_link', **arguments)) def song(self, **options): self.update_song() self.say_song(options['bot']) def update(self, **options): self.update_song(force=True) self.say_song(options['bot']) def load_commands(self, **options): commands = { 'link': Command.raw_command( self.link, level=100, delay_all=self.settings['global_cd'], delay_user=self.settings['user_cd'], description='Get link to your dubtrack', examples=[ CommandExample( None, 'Ask bot for dubtrack link', chat='user:!dubtrack link\n' 'bot:Request your songs at https://dubtrack.fm/join/pajlada' ).parse(), ], ), 'song': Command.raw_command( self.song, level=100, delay_all=self.settings['global_cd'], delay_user=self.settings['user_cd'], description='Get current song', run_in_thread=True, examples=[ CommandExample( None, 'Ask bot for current song (youtube)', chat='user:!dubtrack song\n' 'bot:Current song: NOMA - Brain Power, link: https://youtu.be/9R8aSKwTEMg' ).parse(), CommandExample( None, 'Ask bot for current song (soundcloud)', chat='user:!dubtrack song\n' 'bot:Current song: This is Bondage, link: https://soundcloud.com/razq35/nightlife' ).parse(), CommandExample( None, 'Ask bot for current song (nothing playing)', chat='user:!dubtrack song\n' 'bot:There\'s no song playing right now FeelsBadMan'). parse(), ], ), 'update': Command.raw_command( self.update, level=500, delay_all=self.settings['global_cd'], delay_user=self.settings['user_cd'], description='Force reloading the song and get current song', run_in_thread=True, ), } if self.settings['if_short_alias']: commands['l'] = commands['link'] commands['s'] = commands['song'] commands['u'] = commands['update'] self.commands['dubtrack'] = Command.multiaction_command( level=100, default='link', # If the user does not input any argument fallback='link', # If the user inputs an invalid argument command='dubtrack', commands=commands, ) if self.settings['if_dt_alias']: self.commands['dt'] = self.commands['dubtrack'] if self.settings['if_song_alias']: self.commands['song'] = commands['song']
class ShowEmoteModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Show Emote" DESCRIPTION = "Show a single emote on screen for a few seconds using !#showemote" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="point_cost", label="Point cost", type="number", required=True, placeholder="Point cost", default=100, constraints={ "min_value": 0, "max_value": 999999 }, ), ModuleSetting( key="token_cost", label="Token cost", type="number", required=True, placeholder="Token cost", default=0, constraints={ "min_value": 0, "max_value": 15 }, ), ModuleSetting(key="sub_only", label="Subscribers only", type="boolean", required=True, default=False), ModuleSetting(key="can_whisper", label="Command can be whispered", type="boolean", required=True, default=True), ModuleSetting( key="global_cd", label="Global cooldown (seconds)", type="number", required=True, placeholder="", default=5, constraints={ "min_value": 0, "max_value": 600 }, ), ModuleSetting( key="user_cd", label="Per-user cooldown (seconds)", type="number", required=True, placeholder="", default=15, constraints={ "min_value": 0, "max_value": 1200 }, ), ModuleSetting( key="command_name", label="Command name (i.e. #showemote)", type="text", required=True, placeholder="Command name (no !)", default="#showemote", constraints={ "min_str_len": 1, "max_str_len": 20 }, ), ModuleSetting( key="emote_whitelist", label= "Whitelisted emotes (separate by spaces). Leave empty to use the blacklist.", type="text", required=True, placeholder="i.e. Kappa Keepo PogChamp KKona", default="", ), ModuleSetting( key="emote_blacklist", label= "Blacklisted emotes (separate by spaces). Leave empty to allow all emotes.", type="text", required=True, placeholder="i.e. Kappa Keepo PogChamp KKona", default="", ), ModuleSetting( key="emote_opacity", label="Emote opacity (in percent)", type="number", required=True, placeholder="", default=100, constraints={ "min_value": 0, "max_value": 100 }, ), ModuleSetting( key="emote_persistence_time", label="Time in milliseconds until emotes disappear on screen", type="number", required=True, placeholder="", default=5000, constraints={ "min_value": 500, "max_value": 60000 }, ), ModuleSetting( key="emote_onscreen_scale", label="Scale emotes onscreen by this factor (100 = normal size)", type="number", required=True, placeholder="", default=100, constraints={ "min_value": 0, "max_value": 100000 }, ), ModuleSetting( key="success_whisper", label="Send a whisper when emote was successfully sent", type="boolean", required=True, default=True, ), ] def is_emote_allowed(self, emote_code): if len(self.settings["emote_whitelist"].strip()) > 0: return emote_code in self.settings["emote_whitelist"] return emote_code not in self.settings["emote_blacklist"] def show_emote(self, bot, source, args, **rest): emote_instances = args["emote_instances"] if len(emote_instances) <= 0: # No emotes in the given message bot.whisper(source, "No valid emotes were found in your message.") return False first_emote = emote_instances[0].emote # request to show emote is ignored but return False ensures user is refunded tokens/points if not self.is_emote_allowed(first_emote.code): return False self.bot.websocket_manager.emit( "new_emotes", { "emotes": [first_emote.jsonify()], "opacity": self.settings["emote_opacity"], "persistence_time": self.settings["emote_persistence_time"], "scale": self.settings["emote_onscreen_scale"], }, ) if self.settings["success_whisper"]: bot.whisper( source, f"Successfully sent the emote {first_emote.code} to the stream!" ) def load_commands(self, **options): self.commands[self.settings["command_name"]] = Command.raw_command( self.show_emote, delay_all=self.settings["global_cd"], delay_user=self.settings["user_cd"], tokens_cost=self.settings["token_cost"], cost=self.settings["point_cost"], description="Show an emote on stream!", sub_only=self.settings["sub_only"], can_execute_with_whisper=self.settings["can_whisper"], examples=[ CommandExample( None, "Show an emote on stream.", chat=f"user:!{self.settings['command_name']} Keepo\n" "bot>user: Successfully sent the emote Keepo to the stream!", description="", ).parse() ], )
class GivePointsModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Give Points" DESCRIPTION = "Allows users to donate points to others" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="command_name", label="Command name (i.e. givepoints)", type="text", required=True, placeholder="Command name (no !)", default="givepoints", constraints={"min_str_len": 2, "max_str_len": 25}, ), ModuleSetting( key="source_requires_sub", label="Users need to be subbed to give away points", type="boolean", required=True, default=True, ), ModuleSetting( key="target_requires_sub", label="Target needs to be subbed to receive points", type="boolean", required=True, default=False, ), ] def give_points(self, bot, source, message, **rest): if message is None or len(message) == 0: # The user did not supply any arguments return False msg_split = message.split(" ") if len(msg_split) < 2: # The user did not supply enough arguments bot.whisper(source, f"Usage: !{self.command_name} USERNAME POINTS") return False input = msg_split[0] try: num_points = utils.parse_points_amount(source, msg_split[1]) except InvalidPointAmount as e: bot.whisper(source, f"{e}. Usage: !{self.command_name} USERNAME POINTS") return False if num_points <= 0: # The user tried to specify a negative amount of points bot.whisper(source, "You cannot give away negative points admiralCute") return True # elif num_points < 250: # bot.whisper(source, "You must give 250 points or more :) Be charitable :)") # return True if not source.can_afford(num_points): # The user tried giving away more points than he owns bot.whisper(source, f"You cannot give away more points than you have. You have {source.points} points.") return False with DBManager.create_session_scope() as db_session: target = User.find_by_user_input(db_session, input) if target is None: # The user tried donating points to someone who doesn't exist in our database bot.whisper(source, "This user does not exist FailFish") return False if target == "admiralbulldog": bot.whisper(source, "But why?") return False if target == source: # The user tried giving points to themselves bot.whisper(source, "You can't give points to yourself Bruh") return True if self.settings["target_requires_sub"] is True and target.subscriber is False: # Settings indicate that the target must be a subscriber, which he isn't bot.whisper(source, "Your target must be a subscriber.") return False source.points -= num_points target.points += num_points bot.whisper(source, f"Successfully gave away {num_points} points to {target}") bot.whisper(target, f"{source} just gave you {num_points} points! You should probably thank them ;-)") def load_commands(self, **options): self.command_name = self.settings["command_name"].lower().replace("!", "").replace(" ", "") self.commands[self.command_name] = Command.raw_command( self.give_points, sub_only=self.settings["source_requires_sub"], delay_all=0, delay_user=60, can_execute_with_whisper=True, examples=[ CommandExample( None, "Give points to a user.", chat=f"user:!{self.command_name} pajapaja 4444\n" "bot>user: Successfully gave away 4444 points to pajapaja", description="", ).parse() ], )
class RouletteModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Roulette" DESCRIPTION = "Lets players roulette with themselves for points" CATEGORY = "Game" SETTINGS = [ ModuleSetting( key="command_name", label="Command name (e.g. roulette)", type="text", required=True, placeholder="Command name (no !)", default="roulette", constraints={ "min_str_len": 2, "max_str_len": 15 }, ), ModuleSetting( key="message_won", label="Won message | Available arguments: {bet}, {points}, {user}", type="text", required=True, placeholder= "{user} won {bet} points in roulette and now has {points} points! FeelsGoodMan", default= "{user} won {bet} points in roulette and now has {points} points! FeelsGoodMan", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="message_lost", label="Lost message | Available arguments: {bet}, {points}, {user}", type="text", required=True, placeholder= "{user} lost {bet} points in roulette and now has {points} points! FeelsBadMan", default= "{user} lost {bet} points in roulette and now has {points} points! FeelsBadMan", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="rigged_percentage", label= "Rigged %, lower = more chance of winning. 50 = 50% of winning. 25 = 75% of winning", type="number", required=True, placeholder="", default=50, constraints={ "min_value": 1, "max_value": 100 }, ), ModuleSetting( key="online_global_cd", label="Global cooldown (seconds)", type="number", required=True, placeholder="", default=0, constraints={ "min_value": 0, "max_value": 120 }, ), ModuleSetting( key="online_user_cd", label="Per-user cooldown (seconds)", type="number", required=True, placeholder="", default=60, constraints={ "min_value": 0, "max_value": 240 }, ), ModuleSetting( key="min_roulette_amount", label="Minimum roulette amount", type="number", required=True, placeholder="", default=1, constraints={ "min_value": 1, "max_value": 3000 }, ), ModuleSetting( key="can_execute_with_whisper", label="Allow users to roulette in whispers", type="boolean", required=True, default=False, ), ModuleSetting( key="options_output", label="Result output options", type="options", required=True, default="1. Show results in chat", options=[ "1. Show results in chat", "2. Show results in whispers", "3. Show results in chat if it's over X points else it will be whispered.", "4. Combine output in chat", ], ), ModuleSetting( key="min_show_points", label="Min points you need to win or lose (if options 3)", type="number", required=True, placeholder="", default=100, constraints={ "min_value": 1, "max_value": 150000 }, ), ModuleSetting( key="only_roulette_after_sub", label="Only allow roulettes after sub", type="boolean", required=True, default=False, ), ModuleSetting( key="after_sub_roulette_time", label="How long after a sub people can roulette (seconds)", type="number", required=True, placeholder="", default=30, constraints={ "min_value": 5, "max_value": 3600 }, ), ModuleSetting( key="alert_message_after_sub", label= "Message to announce rouletting has been enabled after a sub or resub, leave empty to disable message. | Available arguments: {seconds}", type="text", required=True, default="Rouletting is now allowed for {seconds} seconds! PogChamp", constraints={ "min_str_len": 0, "max_str_len": 300 }, ), ] def __init__(self, bot): super().__init__(bot) self.last_sub = None self.output_buffer = "" self.output_buffer_args = [] self.last_add = None def load_commands(self, **options): self.commands[self.settings["command_name"].lower( ).replace("!", "").replace(" ", "")] = Command.raw_command( self.roulette, delay_all=self.settings["online_global_cd"], delay_user=self.settings["online_user_cd"], description="Roulette for points", can_execute_with_whisper=self.settings["can_execute_with_whisper"], examples=[ CommandExample( None, "Roulette for 69 points", chat="user:!" + self.settings["command_name"] + " 69\n" "bot:pajlada won 69 points in roulette! FeelsGoodMan", description="Do a roulette for 69 points", ).parse() ], ) def rigged_random_result(self): return random.randint(1, 100) > self.settings["rigged_percentage"] def roulette(self, bot, source, message, **rest): if self.settings["only_roulette_after_sub"]: if self.last_sub is None: return False if utils.now() - self.last_sub > datetime.timedelta( seconds=self.settings["after_sub_roulette_time"]): return False if message is None: bot.whisper( source, "I didn't recognize your bet! Usage: !" + self.settings["command_name"] + " 150 to bet 150 points", ) return False msg_split = message.split(" ") try: bet = utils.parse_points_amount(source, msg_split[0]) except pajbot.exc.InvalidPointAmount as e: bot.whisper(source, str(e)) return False if not source.can_afford(bet): bot.whisper( source, f"You don't have enough points to do a roulette for {bet} points :(" ) return False if bet < self.settings["min_roulette_amount"]: bot.whisper( source, f"You have to bet at least {self.settings['min_roulette_amount']} point! :(" ) return False # Calculating the result result = self.rigged_random_result() points = bet if result else -bet source.points += points with DBManager.create_session_scope() as db_session: r = Roulette(source.id, points) db_session.add(r) arguments = { "bet": bet, "user": source.name, "points": source.points, "win": points > 0 } if points > 0: out_message = self.get_phrase("message_won", **arguments) else: out_message = self.get_phrase("message_lost", **arguments) if self.settings["options_output"] == "4. Combine output in chat": if bot.is_online: self.add_message(bot, arguments) else: bot.me(out_message) if self.settings["options_output"] == "1. Show results in chat": bot.me(out_message) if self.settings["options_output"] == "2. Show results in whispers": bot.whisper(source, out_message) if (self.settings["options_output"] == "3. Show results in chat if it's over X points else it will be whispered." ): if abs(points) >= self.settings["min_show_points"]: bot.me(out_message) else: bot.whisper(source, out_message) HandlerManager.trigger("on_roulette_finish", user=source, points=points) def on_tick(self, **rest): if self.output_buffer == "": return if self.last_add is None: return diff = utils.now() - self.last_add if diff.seconds > 3: self.flush_output_buffer() def flush_output_buffer(self): msg = self.output_buffer self.bot.me(msg) self.output_buffer = "" self.output_buffer_args = [] def add_message(self, bot, arguments): parts = [] new_buffer = "Roulette: " win_emote = "forsenPls" lose_emote = "forsenSWA" for arg in self.output_buffer_args: parts.append( f"{win_emote if arg['win'] else lose_emote} {arg['user']} {'+' if arg['win'] else '-'}{arg['bet']}" ) parts.append( f"{win_emote if arguments['win'] else lose_emote} {arguments['user']} {'+' if arguments['win'] else '-'}{arguments['bet']}" ) log.debug(parts) new_buffer += ", ".join(parts) if len(new_buffer) > 480: self.flush_output_buffer() else: self.output_buffer = new_buffer log.info("Set output buffer to " + new_buffer) self.output_buffer_args.append(arguments) self.last_add = utils.now() def on_user_sub_or_resub(self, **rest): now = utils.now() # True if we already announced the alert_message_after_sub within the last 5 seconds. Prevents # spam after bulk sub gifts. skip_message = self.last_sub is not None and now - self.last_sub < datetime.timedelta( seconds=5) self.last_sub = now if (self.settings["only_roulette_after_sub"] and self.settings["alert_message_after_sub"] != "" and not skip_message): self.bot.say(self.settings["alert_message_after_sub"].format( seconds=self.settings["after_sub_roulette_time"])) def enable(self, bot): HandlerManager.add_handler("on_user_sub", self.on_user_sub_or_resub) HandlerManager.add_handler("on_user_resub", self.on_user_sub_or_resub) HandlerManager.add_handler("on_tick", self.on_tick) def disable(self, bot): HandlerManager.remove_handler("on_user_sub", self.on_user_sub_or_resub) HandlerManager.remove_handler("on_user_resub", self.on_user_sub_or_resub) HandlerManager.remove_handler("on_tick", self.on_tick)
class SongrequestModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Songrequest" DESCRIPTION = "Request Songs" CATEGORY = "Feature" SETTINGS = [ ModuleSetting(key="youtube_key", label="Youtube developer key", type="text", required=True, default=""), ModuleSetting( key="max_song_length", label="Max song length (in seconds)", type="number", required=True, placeholder="Max song length (in seconds)", default=360, constraints={ "min_value": 1, "max_value": 3600 }, ), ModuleSetting( key="point_cost", label="Point costs for requesting a song", type="number", required=True, default=500, constraints={ "min_value": 0, "max_value": 250000 }, ), ModuleSetting( key="backup_playlist_id", label= "Songs to play when no song is being requested backup playlist id", type="text", required=True, default="", ), ModuleSetting( key="volume", label="Default volume for song requests", type="number", required=True, default=100, constraints={ "min_value": 0, "max_value": 100 }, ), ModuleSetting( key="volume_multiplier", label="Volume multiplier", type="number", required=True, default="100", constraints={ "min_value": 0, "max_value": 100 }, ), ModuleSetting( key="use_spotify", label="Checks Spotify for current song if no song is playing", type="boolean", required=True, default=True, ), ModuleSetting( key="send_message_in_chat", label="Send a message in chat upon a song request", type="boolean", required=True, default=True, ), ModuleSetting( key="message_in_chat", label= "Message sent in chat after someone requests a song {username} is the requestor, {title} is the song title, {current_pos} is the current queue position, {playing_in} is how long until the song is played", type="text", required=True, default= '{username} just requested the song "{title}" to be played KKona', ), ModuleSetting( key="message_in_chat_no_songs_playing", label="Message sent when no songs are playing", type="text", required=True, default="No songs are currently playing", ), ModuleSetting( key="message_in_chat_when_song_is_playing", label= "Message sent when a song is playing, {title} is the title of the song, {requestor} is the person who requested, {time_left} is the time left for playing", type="text", required=True, default="The current song is {title} requested by {requestor}", ), ModuleSetting( key="message_in_chat_when_song_is_playing_spotify", label= "Message sent when a song is playing, {title} is the title of the song, {artists} is the list of artists", type="text", required=True, default="The current song is {title} by {artists}", ), ModuleSetting( key="message_in_chat_when_next_song", label= "Message sent when a next song is requested, {title} is the title of the song, {requestor} is the person who requested, {playing_in} is when the song will play", type="text", required=True, default="The next song is {title} requested by {requestor}", ), ModuleSetting( key="message_in_chat_when_next_song_none", label= "Message sent when a next song is requested but there isn't one", type="text", required=True, default="There are no songs currently queued", ), ModuleSetting( key="send_message_on_open", label="Send message when song request is opened", type="boolean", required=True, default=True, ), ModuleSetting( key="message_sent_on_open", label="Message sent when song request is opened", type="text", required=True, default="Song Request has been opened!", ), ModuleSetting( key="send_message_on_close", label="Send message when song request is closed", type="boolean", required=True, default=True, ), ModuleSetting( key="message_sent_on_close", label="Message sent when song request is closed", type="text", required=True, default="Song Request has been closed!", ), ] def getBackUpListSongs(self, next_page=None): songs = [] urlin = ( f"https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&maxResults=50&playlistId={self.settings['backup_playlist_id']}&key={self.settings['youtube_key']}" + (f"&pageToken={next_page}" if next_page else "")) with urllib.request.urlopen(urlin) as url: data = json.loads(url.read().decode()) for song in data["items"]: songs.append(song["snippet"]["resourceId"]["videoId"]) try: next_page = data["nextPageToken"] return songs + self.getBackUpListSongs(next_page) except: return songs def create_song_request_queue(self, video_id, bot, source): with DBManager.create_session_scope() as db_session: song_info = SongRequestSongInfo._create_or_get( db_session, video_id, self.youtube) if not song_info: log.error("There was an error!") return False if song_info.banned: bot.whisper(source, "That song is banned! FeelsWeirdMan") return False skip_after = (self.settings["max_song_length"] if song_info.duration > self.settings["max_song_length"] else None) songrequest_queue = SongrequestQueue._create( db_session, video_id, skip_after, source.id) db_session.commit() m, s = divmod(int(songrequest_queue.playing_in(db_session)), 60) m = int(m) s = int(s) playing_in = f"{m:02d}:{s:02d}" if self.settings["send_message_in_chat"]: bot.say(self.settings["message_in_chat"].format( username=source.username_raw, title=song_info.title, current_pos=songrequest_queue.queue + (1 if SongrequestQueue._get_current_song(db_session) else 0), playing_in=playing_in, )) self.bot.songrequest_manager._playlist() return True def add_song(self, bot, source, message, **rest): if not message: self.bot.whisper( source, "Could not find a valid youtube ID in your argument.") return False # 1. Find youtube ID in message msg_split = message.split(" ") youtube_id = find_youtube_id_in_string(msg_split[0]) if youtube_id is False: youtube_id = find_youtube_video_by_search(message) if youtube_id is None: self.bot.whisper( source, "Could not find a valid youtube ID in your argument.") return False # 2. Make sure the stream is live stream_id = StreamHelper.get_current_stream_id() if stream_id is None or stream_id is False: self.bot.whisper( source, "You cannot request songs while the stream is offline.") return False return self.create_song_request_queue(youtube_id, bot, source) def get_current_song(self, bot, source, message, **rest): with DBManager.create_session_scope() as db_session: current_song = SongrequestQueue._get_current_song(db_session) if current_song: m, s = divmod(current_song.playing_in(db_session), 60) m = int(m) s = int(s) time_left = f"{m:02d}:{s:02d}" if current_song.requested_by: bot.say( self.settings["message_in_chat_when_song_is_playing"]. format( title=current_song.song_info.title, requestor=current_song.requested_by.username_raw, time_left=time_left, )) return True bot.say(self.settings["message_in_chat_when_song_is_playing"]. format(title=current_song.song_info.title, requestor="Backup Playlist", time_left=time_left)) return True if self.settings["use_spotify"]: is_playing, title, artistsArr = bot.spotify_api.state( bot.spotify_token_manager) if is_playing: bot.say(self.settings[ "message_in_chat_when_song_is_playing_spotify"].format( title=title, artists=", ".join( [str(artist) for artist in artistsArr]))) return True bot.say(self.settings["message_in_chat_no_songs_playing"]) return True def get_next_song(self, bot, source, message, **rest): with DBManager.create_session_scope() as db_session: next_song = SongrequestQueue._get_next_song(db_session) if next_song: m, s = divmod(next_song.playing_in(db_session), 60) m = int(m) s = int(s) playing_in = f"{m:02d}:{s:02d}" if next_song.requestor: bot.say( self.settings["message_in_chat_when_next_song"].format( title=next_song.song_info.title, requestor=next_song.requestor.username_raw, playing_in=playing_in, )) return True bot.say(self.settings["message_in_chat_when_next_song"].format( title=next_song.song_info.title, requestor="Backup Playlist", playing_in=playing_in)) return True bot.say(self.settings["message_in_chat_when_next_song_none"]) return True def open_module(self, bot, source, message, **rest): if self.bot.songrequest_manager.open_module_function(): if self.settings["send_message_on_open"]: bot.whisper(source, self.settings["message_sent_on_open"]) bot.say(self.settings["message_sent_on_open"]) return bot.whisper(source, "Song request is already open!") def close_module(self, bot, source, message, **rest): if self.bot.songrequest_manager.close_module_function(): if self.settings["send_message_on_open"]: bot.whisper(source, self.settings["message_sent_on_close"]) bot.say(self.settings["message_sent_on_close"]) return bot.whisper(source, "Song request is already closed!") def skip(self, bot, source, message, **rest): if self.bot.songrequest_manager.skip_function(source.login): bot.whisper(source, "Song has been skipped!") return bot.whisper(source, "No song is playing!") def pause(self, bot, source, message, **rest): if self.bot.songrequest_manager.pause_function(): bot.whisper(source, "Song has been paused") return bot.whisper(source, "Song is already paused!") def resume(self, bot, source, message, **rest): if self.bot.songrequest_manager.resume_function(): bot.whisper(source, "Song has been resumed") return bot.whisper(source, "Song is already playing!") def volume(self, bot, source, message, **rest): if not message: bot.say( f"The current volume is {self.bot.songrequest_manager.volume_val()}%" ) return True try: val = int(message) if val < 0 or val > 100: bot.whisper( source, "Invalid volume setting enter a volume between 0-100") return False except: bot.whisper(source, "Invalid volume setting enter a volume between 0-100") return False self.bot.songrequest_manager.volume_function(val) bot.whisper(source, "Volume has been changed to " + message + "%") return True def show_video(self, bot, source, message, **rest): if self.bot.songrequest_manager.show_function(): bot.whisper(source, "The video has been shown!") return True bot.whisper(source, "The video is already showing!") return True def hide_video(self, bot, source, message, **rest): if self.bot.songrequest_manager.hide_function(): bot.whisper(source, "The video has been hidden!") return True bot.whisper(source, "The video is already hidden!") return True def load_commands(self, **options): self.commands["sr"] = self.commands[ "songrequest"] = Command.raw_command( self.add_song, delay_all=0, delay_user=3, notify_on_error=True, cost=self.settings["point_cost"]) self.commands["song"] = Command.raw_command(self.get_current_song, delay_all=0, delay_user=3, notify_on_error=True) self.commands["next"] = Command.raw_command(self.get_next_song, delay_all=0, delay_user=3, notify_on_error=True) self.commands["opensr"] = Command.raw_command(self.open_module, delay_all=0, delay_user=3, level=500, notify_on_error=True) self.commands["closesr"] = Command.raw_command(self.close_module, delay_all=0, delay_user=3, level=500, notify_on_error=True) self.commands["skip"] = Command.raw_command(self.skip, delay_all=0, delay_user=3, level=500, notify_on_error=True) self.commands["pause"] = Command.raw_command(self.pause, delay_all=0, delay_user=3, level=500, notify_on_error=True) self.commands["resume"] = Command.raw_command(self.resume, delay_all=0, delay_user=3, level=500, notify_on_error=True) self.commands["volume"] = Command.raw_command(self.volume, delay_all=0, delay_user=3, level=500, notify_on_error=True) self.commands["showvideo"] = Command.raw_command(self.show_video, delay_all=0, delay_user=3, level=500, notify_on_error=True) self.commands["hidevideo"] = Command.raw_command(self.hide_video, delay_all=0, delay_user=3, level=500, notify_on_error=True) def enable(self, bot): if not self.bot: return import apiclient from apiclient.discovery import build def build_request(_, *args, **kwargs): import httplib2 new_http = httplib2.Http() return apiclient.http.HttpRequest(new_http, *args, **kwargs) self.youtube = build("youtube", "v3", developerKey=self.settings["youtube_key"], requestBuilder=build_request) with DBManager.create_session_scope() as db_session: SongrequestQueue._clear_backup_songs(db_session) if self.settings["backup_playlist_id"] and self.settings[ "backup_playlist_id"] != "": backup_songs = self.getBackUpListSongs() random.shuffle(backup_songs) SongrequestQueue._load_backup_songs(db_session, backup_songs, self.youtube, self.settings) db_session.commit() SongrequestQueue._update_queue() self.bot.songrequest_manager.enable(self.settings, self.youtube) HandlerManager.add_handler( "on_stream_stop", self.bot.songrequest_manager.close_module_function) def disable(self, bot): if not self.bot: return self.bot.songrequest_manager.disable() self.bot.songrequest_manager.disable() HandlerManager.remove_handler( "on_stream_stop", self.bot.songrequest_manager.close_module_function)
class EmoteTimeoutModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Emote Timeout" DESCRIPTION = "Times out users who post emoji or Twitch, BTTV, FFZ or 7TV emotes" CATEGORY = "Moderation" SETTINGS = [ ModuleSetting( key="timeout_twitch", label="Timeout any Twitch emotes", type="boolean", required=True, default=False ), ModuleSetting(key="timeout_ffz", label="Timeout any FFZ emotes", type="boolean", required=True, default=False), ModuleSetting( key="timeout_bttv", label="Timeout any BTTV emotes", type="boolean", required=True, default=False ), ModuleSetting(key="timeout_7tv", label="Timeout any 7TV emotes", type="boolean", required=True, default=False), ModuleSetting( key="timeout_emoji", label="Timeout any unicode emoji", type="boolean", required=True, default=False ), ModuleSetting( key="bypass_level", label="Level to bypass module", type="number", required=True, placeholder="", default=420, constraints={"min_value": 100, "max_value": 1000}, ), ModuleSetting( key="moderation_action", label="Moderation action to apply", type="options", required=True, default="Delete", options=["Delete", "Timeout"], ), ModuleSetting( key="timeout_duration", label="Timeout duration (if moderation action is timeout)", type="number", required=True, placeholder="", default=5, constraints={"min_value": 1, "max_value": 1209600}, ), ModuleSetting( key="enable_in_online_chat", label="Enabled in online chat", type="boolean", required=True, default=True ), ModuleSetting( key="enable_in_offline_chat", label="Enabled in offline chat", type="boolean", required=True, default=True ), ModuleSetting( key="twitch_timeout_reason", label="Twitch Emote Timeout Reason", type="text", required=False, placeholder="", default="No Twitch emotes allowed", constraints={}, ), ModuleSetting( key="ffz_timeout_reason", label="FFZ Emote Timeout Reason", type="text", required=False, placeholder="", default="No FFZ emotes allowed", constraints={}, ), ModuleSetting( key="bttv_timeout_reason", label="BTTV Emote Timeout Reason", type="text", required=False, placeholder="", default="No BTTV emotes allowed", constraints={}, ), ModuleSetting( key="7tv_timeout_reason", label="7TV Emote Timeout Reason", type="text", required=False, placeholder="", default="No 7TV emotes allowed", constraints={}, ), ModuleSetting( key="emoji_timeout_reason", label="Emoji Timeout Reason", type="text", required=False, placeholder="", default="No emoji allowed", constraints={}, ), ModuleSetting( key="disable_warnings", label="Disable warning timeouts", type="boolean", required=True, default=False, ), ] def on_message(self, source, message, emote_instances, msg_id, **rest): if source.level >= self.settings["bypass_level"] or source.moderator is True: return True if self.bot.is_online and not self.settings["enable_in_online_chat"]: return True if not self.bot.is_online and not self.settings["enable_in_offline_chat"]: return True if self.settings["timeout_twitch"] and any(e.emote.provider == "twitch" for e in emote_instances): self.bot.delete_or_timeout( source, self.settings["moderation_action"], msg_id, self.settings["timeout_duration"], self.settings["twitch_timeout_reason"], disable_warnings=self.settings["disable_warnings"], once=True, ) return False if self.settings["timeout_ffz"] and any(e.emote.provider == "ffz" for e in emote_instances): self.bot.delete_or_timeout( source, self.settings["moderation_action"], msg_id, self.settings["timeout_duration"], self.settings["ffz_timeout_reason"], disable_warnings=self.settings["disable_warnings"], once=True, ) return False if self.settings["timeout_bttv"] and any(e.emote.provider == "bttv" for e in emote_instances): self.bot.delete_or_timeout( source, self.settings["moderation_action"], msg_id, self.settings["timeout_duration"], self.settings["bttv_timeout_reason"], disable_warnings=self.settings["disable_warnings"], once=True, ) return False if self.settings["timeout_7tv"] and any(e.emote.provider == "7tv" for e in emote_instances): self.bot.delete_or_timeout( source, self.settings["moderation_action"], msg_id, self.settings["timeout_duration"], self.settings["7tv_timeout_reason"], disable_warnings=self.settings["disable_warnings"], once=True, ) return False if self.settings["timeout_emoji"] and any(emoji in message for emoji in ALL_EMOJI): self.bot.delete_or_timeout( source, self.settings["moderation_action"], msg_id, self.settings["timeout_duration"], self.settings["emoji_timeout_reason"], disable_warnings=self.settings["disable_warnings"], once=True, ) return False return True def enable(self, bot): HandlerManager.add_handler("on_message", self.on_message, priority=150, run_if_propagation_stopped=True) def disable(self, bot): HandlerManager.remove_handler("on_message", self.on_message)
class EightBallModule(BaseModule): ID = __name__.split('.')[-1] NAME = '8-ball' DESCRIPTION = 'Gives users access to the !8ball command!' CATEGORY = 'Game' SETTINGS = [ ModuleSetting( key='online_global_cd', label='Global cooldown (seconds)', type='number', required=True, placeholder='', default=4, constraints={ 'min_value': 0, 'max_value': 120, }), ModuleSetting( key='online_user_cd', label='Per-user cooldown (seconds)', type='number', required=True, placeholder='', default=10, constraints={ 'min_value': 0, 'max_value': 240, }), ] def __init__(self): super().__init__() self.phrases = [ 'sure', 'are you kidding?!', 'yeah', 'no', 'i think so', 'don\'t bet on it', 'ja', 'doubtful', 'for sure', 'forget about it', 'nein', 'maybe', 'Kappa Keepo PogChamp', 'sure', 'i dont think so', 'it is so', 'leaning towards no', 'look deep in your heart and you will see the answer', 'most definitely', 'most likely', 'my sources say yes', 'never', 'nah m8', 'might actually be yes', 'no.', 'outlook good', 'outlook not so good', 'perhaps', 'mayhaps', 'that\'s a tough one', 'idk kev', 'don\'t ask that', 'the answer to that isn\'t pretty', 'the heavens point to yes', 'who knows?', 'without a doubt', 'yesterday it would\'ve been a yes, but today it\'s a yep', 'you will have to wait' ] self.emotes = [ 'Kappa', 'Keepo', 'xD', 'KKona', '4Head', 'EleGiggle', 'DansGame', 'KappaCool', 'BrokeBack', 'OpieOP', 'KappaRoss', 'KappaPride', 'FeelsBadMan', 'FeelsGoodMan', 'PogChamp', 'VisLaud', 'OhMyDog', 'FrankerZ', 'DatSheffy', 'BabyRage', 'VoHiYo', 'haHAA', 'FeelsBirthdayMan', 'LUL' ] def eightball_command(self, **options): source = options['source'] bot = options['bot'] message = options['message'] if message and len(message) > 0: phrase = random.choice(self.phrases) emote = random.choice(self.emotes) bot.me('{source.username_raw}, the 8-ball says... {phrase} {emote}'.format(source=source, phrase=phrase, emote=emote)) else: return False def load_commands(self, **options): self.commands['8ball'] = pajbot.models.command.Command.raw_command(self.eightball_command, delay_all=self.settings['online_global_cd'], delay_user=self.settings['online_user_cd'], description='Need help with a decision? Use the !8ball command!', examples=[ pajbot.models.command.CommandExample(None, '!8ball', chat='user:!8ball Should I listen to gachimuchi?\n' 'bot:pajlada, the 8-ball says... Of course you should!', description='Ask the 8ball an important question').parse(), ], )
class CaseCheckerModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Case Checker" DESCRIPTION = "Times out users who post messages that contain lowercase/uppercase letters." CATEGORY = "Moderation" SETTINGS = [ ModuleSetting( key="timeout_uppercase", label="Timeout any uppercase in messages", type="boolean", required=True, default=False, ), ModuleSetting( key="timeout_lowercase", label="Timeout any lowercase in messages", type="boolean", required=True, default=False, ), ModuleSetting( key="bypass_level", label="Level to bypass module", type="number", required=True, placeholder="", default=500, constraints={ "min_value": 100, "max_value": 1000 }, ), ModuleSetting( key="timeout_duration", label="Timeout duration", type="number", required=True, placeholder="", default=3, constraints={ "min_value": 3, "max_value": 120 }, ), ModuleSetting(key="online_chat_only", label="Only enabled in online chat", type="boolean", required=True, default=True), ModuleSetting( key="uppercase_timeout_reason", label="Uppercase Timeout Reason", type="text", required=False, placeholder="", default="no uppercase characters allowed", constraints={}, ), ModuleSetting( key="lowercase_timeout_reason", label="Lowercase Timeout Reason", type="text", required=False, placeholder="", default="NO LOWERCASE CHARACTERS ALLOWED", constraints={}, ), ] def on_message(self, source, message, **rest): if source.level >= self.settings[ "bypass_level"] or source.moderator is True: return True if self.settings["online_chat_only"] and not self.bot.is_online: return True if self.settings["timeout_uppercase"] and any(c.isupper() for c in message): self.bot.timeout(source, self.settings["timeout_duration"], reason=self.settings["uppercase_timeout_reason"], once=True) return False if self.settings["timeout_lowercase"] and any(c.islower() for c in message): self.bot.timeout(source, self.settings["timeout_duration"], reason=self.settings["lowercase_timeout_reason"], once=True) return False return True def enable(self, bot): HandlerManager.add_handler("on_message", self.on_message) def disable(self, bot): HandlerManager.remove_handler("on_message", self.on_message)
class SubAlertModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Subscription Alert - Chat" DESCRIPTION = "Prints a message in chat/whispers when a user re/subscribes" CATEGORY = "Feature" ENABLED_DEFAULT = True SETTINGS = [ ModuleSetting( key="chat_message", label="Enable a chat message for someone who subscribed", type="boolean", required=True, default=True, ), ModuleSetting( key="new_sub", label="New sub chat message | Available arguments: {username}", type="text", required=True, placeholder="Sub hype! {username} just subscribed PogChamp", default="Sub hype! {username} just subscribed PogChamp", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="new_prime_sub", label= "New prime sub chat message | Available arguments: {username}", type="text", required=True, placeholder= "Thank you for smashing that prime button! {username} PogChamp", default= "Thank you for smashing that prime button! {username} PogChamp", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="new_gift_sub", label= "New gift sub chat message | Available arguments: {username}, {gifted_by}", type="text", required=True, placeholder= "{gifted_by} gifted a fresh sub to {username}! PogChamp", default="{gifted_by} gifted a fresh sub to {username}! PogChamp", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="resub", label= "Resub chat message | Available arguments: {username}, {num_months}, {substreak_string}", type="text", required=True, placeholder= "Resub hype! {username} just subscribed, {num_months} months in a row PogChamp <3", default= "Resub hype! {username} just subscribed, {num_months} months in a row PogChamp <3", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="resub_prime", label= "Resub chat message (Prime sub) | Available arguments: {username}, {num_months}, {substreak_string}", type="text", required=True, placeholder= "Thank you for smashing it {num_months} in a row {username}", default= "Thank you for smashing it {num_months} in a row {username}", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="resub_gift", label= "Resub chat message (Gift sub) | Available arguments: {username}, {num_months}, {gifted_by}, {substreak_string}", type="text", required=True, placeholder= "{username} got gifted a resub by {gifted_by}, that's {num_months} months in a row PogChamp", default= "{username} got gifted a resub by {gifted_by}, that's {num_months} months in a row PogChamp", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="substreak_string", label= "Sub streak string. Empty if streak was not shared | Available arguments: {username}, {num_months}", type="text", required=True, placeholder="{num_months} in a row PogChamp", default="{num_months} in a row PogChamp", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="whisper_message", label="Enable a whisper message for someone who subscribed", type="boolean", required=True, default=False, ), ModuleSetting( key="whisper_after", label="Whisper the message after X seconds", type="number", required=True, placeholder="", default=5, constraints={ "min_value": 1, "max_value": 120 }, ), ModuleSetting( key="new_sub_whisper", label= "Whisper message for new subs | Available arguments: {username}", type="text", required=True, placeholder="Thank you for subscribing {username} <3", default="Thank you for subscribing {username} <3", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="resub_whisper", label= "Whisper message for resubs | Available arguments: {username}, {num_months}", type="text", required=True, placeholder= "Thank you for subscribing for {num_months} months in a row {username} <3", default= "Thank you for subscribing for {num_months} months in a row {username} <3", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="grant_points_on_sub", label= "Give points to user when they subscribe/resubscribe. 0 = off", type="number", required=True, placeholder="", default=0, constraints={ "min_value": 0, "max_value": 50000 }, ), ModuleSetting( key="message_on_give_points", label= "Enable posting a message to chat upon user getting points due to sub", type="boolean", required=True, default=True, ), ModuleSetting( key="message_on_give_points_value", label= "Message to user upon getting points due to sub | Available arguments: {username}, {points_given} ", type="text", required=True, placeholder="", default= "{username} was given {points_given} points for subscribing! FeelsAmazingMan", constraints={ "min_str_len": 0, "max_str_len": 400 }, ), ModuleSetting( key="grant_points_on_bits", label= "Give points to user when they cheer with bits (per bit). 0 = OFF ", type="number", required=True, placeholder="", default=1, constraints={ "min_value": 0, "max_value": 50000 }, ), ModuleSetting( key="grant_points_on_donate", label="Give points to user when they donate (per USD). 0 = OFF ", type="number", required=True, placeholder="", default=100, constraints={ "min_value": 0, "max_value": 50000 }, ), ] def __init__(self, bot): super().__init__(bot) def on_sub_shared(self, user): if self.settings["grant_points_on_sub"] <= 0: return user.points += self.settings["grant_points_on_sub"] if self.settings["message_on_give_points"]: points_given = self.settings["grant_points_on_sub"] username = user self.bot.say(self.settings["message_on_give_points_value"].format( points_given=points_given, username=username)) def on_new_sub(self, user, sub_type, gifted_by=None): """ A new user just subscribed. Send the event to the websocket manager, and send a customized message in chat. Also increase the number of active subscribers in the database by one. """ if gifted_by: self.on_sub_shared(gifted_by) gifted_name = gifted_by.name else: self.on_sub_shared(user) gifted_name = "" self.bot.kvi["active_subs"].inc() payload = {"username": user.name, "gifted_by": gifted_name} if self.settings["chat_message"] is True: if sub_type == "Prime": self.bot.say(self.get_phrase("new_prime_sub", **payload)) else: if gifted_by: self.bot.say(self.get_phrase("new_gift_sub", **payload)) else: self.bot.say(self.get_phrase("new_sub", **payload)) if self.settings["whisper_message"] is True: self.bot.execute_delayed( self.settings["whisper_after"], self.bot.whisper, user, self.get_phrase("new_sub_whisper", **payload)) def on_resub(self, user, num_months, sub_type, gifted_by=None, substreak_count=0): """ A user just re-subscribed. Send the event to the websocket manager, and send a customized message in chat. """ if gifted_by: self.on_sub_shared(gifted_by) gifted_name = gifted_by.name else: self.on_sub_shared(user) gifted_name = "" payload = { "username": user.name, "num_months": num_months, "gifted_by": gifted_name } if substreak_count and substreak_count > 0: payload["substreak_string"] = self.get_phrase( "substreak_string", username=user.name, num_months=substreak_count, gifted_by=gifted_by) else: payload["substreak_string"] = "" if self.settings["chat_message"] is True: if sub_type == "Prime": self.bot.say(self.get_phrase("resub_prime", **payload)) else: if gifted_by: self.bot.say(self.get_phrase("resub_gift", **payload)) else: self.bot.say(self.get_phrase("resub", **payload)) if self.settings["whisper_message"] is True: self.bot.execute_delayed( self.settings["whisper_after"], self.bot.whisper, user, self.get_phrase("resub_whisper", **payload)) def on_usernotice(self, source, tags, **rest): if "msg-id" not in tags: return if tags["msg-id"] == "resub": num_months = -1 substreak_count = 0 if "msg-param-months" in tags: num_months = int(tags["msg-param-months"]) if "msg-param-cumulative-months" in tags: num_months = int(tags["msg-param-cumulative-months"]) if "msg-param-streak-months" in tags: substreak_count = int(tags["msg-param-streak-months"]) if "msg-param-should-share-streak" in tags: should_share = bool(tags["msg-param-should-share-streak"]) if not should_share: substreak_count = 0 if "msg-param-sub-plan" not in tags: log.debug( f"subalert msg-id is resub, but missing msg-param-sub-plan: {tags}" ) return # log.debug('msg-id resub tags: {}'.format(tags)) # TODO: Should we check room id with streamer ID here? Maybe that's for pajbot2 instead self.on_resub(source, num_months, tags["msg-param-sub-plan"], None, substreak_count) HandlerManager.trigger("on_user_resub", user=source, num_months=num_months) elif tags["msg-id"] == "subgift": num_months = 0 substreak_count = 0 if "msg-param-months" in tags: num_months = int(tags["msg-param-months"]) if "msg-param-cumulative-months" in tags: num_months = int(tags["msg-param-cumulative-months"]) if "msg-param-streak-months" in tags: substreak_count = int(tags["msg-param-streak-months"]) if "msg-param-should-share-streak" in tags: should_share = bool(tags["msg-param-should-share-streak"]) if not should_share: substreak_count = 0 if "display-name" not in tags: log.debug( f"subalert msg-id is subgift, but missing display-name: {tags}" ) return with DBManager.create_session_scope() as db_session: receiver_id = tags["msg-param-recipient-id"] receiver_login = tags["msg-param-recipient-user-name"] receiver_name = tags["msg-param-recipient-display-name"] receiver = User.from_basics( db_session, UserBasics(receiver_id, receiver_login, receiver_name)) if num_months > 1: # Resub self.on_resub(receiver, num_months, tags["msg-param-sub-plan"], source, substreak_count) HandlerManager.trigger("on_user_resub", user=receiver, num_months=num_months) else: # New sub self.on_new_sub(receiver, tags["msg-param-sub-plan"], source) HandlerManager.trigger("on_user_sub", user=receiver) elif tags["msg-id"] == "sub": if "msg-param-sub-plan" not in tags: log.debug( f"subalert msg-id is sub, but missing msg-param-sub-plan: {tags}" ) return self.on_new_sub(source, tags["msg-param-sub-plan"]) HandlerManager.trigger("on_user_sub", user=source) else: log.debug(f"Unhandled msg-id: {tags['msg-id']} - tags: {tags}") def on_cheer(self, user, bits_cheered, **rest): if self.settings["grant_points_on_bits"] <= 0: return points_to_give = int( bits_cheered) * self.settings["grant_points_on_bits"] user.points += points_to_give self.bot.whisper( user, "You have been given " + str(points_to_give) + " points for cheering " + str(bits_cheered) + " bits") def on_donate(self, user, amount, **rest): if self.settings["grant_points_on_donate"] <= 0: return points_to_give = int(amount * self.settings["grant_points_on_donate"]) user.points += points_to_give self.bot.whisper( user, f"You have been given {points_to_give} points for donating ${amount:.2f}" ) def enable(self, bot): HandlerManager.add_handler("on_usernotice", self.on_usernotice) HandlerManager.add_handler("on_donate", self.on_donate) HandlerManager.add_handler("on_cheer", self.on_cheer) def disable(self, bot): HandlerManager.remove_handler("on_usernotice", self.on_usernotice) HandlerManager.remove_handler("on_donate", self.on_donate) HandlerManager.remove_handler("on_cheer", self.on_cheer)
class DuelModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Duel (mini game)" DESCRIPTION = "Let players duel to win or lose points." CATEGORY = "Game" SETTINGS = [ ModuleSetting( key="max_pot", label="How many points you can duel for at most", type="number", required=True, placeholder="", default=420, constraints={ "min_value": 0, "max_value": 69000 }, ), ModuleSetting( key="message_won", label="Winner message | Available arguments: {winner}, {loser}", type="text", required=True, placeholder="{winner} won the duel vs {loser} PogChamp", default="{winner} won the duel vs {loser} PogChamp", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="message_won_points", label= "Points message | Available arguments: {winner}, {loser}, {total_pot}, {extra_points}", type="text", required=True, placeholder= "{winner} won the duel vs {loser} PogChamp . The pot was {total_pot}, the winner gets their bet back + {extra_points} points", default= "{winner} won the duel vs {loser} PogChamp . The pot was {total_pot}, the winner gets their bet back + {extra_points} points", constraints={ "min_str_len": 10, "max_str_len": 400 }, ), ModuleSetting( key="online_global_cd", label="Global cooldown (seconds)", type="number", required=True, placeholder="", default=0, constraints={ "min_value": 0, "max_value": 120 }, ), ModuleSetting( key="online_user_cd", label="Per-user cooldown (seconds)", type="number", required=True, placeholder="", default=5, constraints={ "min_value": 0, "max_value": 240 }, ), ModuleSetting(key="show_on_clr", label="Show duels on the clr overlay", type="boolean", required=True, default=True), ] def load_commands(self, **options): self.commands["duel"] = Command.raw_command( self.initiate_duel, delay_all=self.settings["online_global_cd"], delay_user=self.settings["online_user_cd"], description="Initiate a duel with a user", examples=[ CommandExample( None, "0-point duel", chat="user:!duel Karl_Kons\n" "bot>user:You have challenged Karl_Kons for 0 points", description="Duel Karl_Kons for 0 points", ).parse(), CommandExample( None, "69-point duel", chat="user:!duel Karl_Kons 69\n" "bot>user:You have challenged Karl_Kons for 69 points", description="Duel Karl_Kons for 69 points", ).parse(), ], ) self.commands["cancelduel"] = Command.raw_command( self.cancel_duel, delay_all=0, delay_user=10, description="Cancel your duel request") self.commands["accept"] = Command.raw_command( self.accept_duel, delay_all=0, delay_user=0, description="Accept a duel request") self.commands["decline"] = Command.raw_command( self.decline_duel, delay_all=0, delay_user=0, description="Decline a duel request") self.commands["deny"] = self.commands["decline"] self.commands["duelstatus"] = Command.raw_command( self.status_duel, delay_all=0, delay_user=5, description="Current duel request info") self.commands["duelstats"] = Command.raw_command( self.get_duel_stats, delay_all=0, delay_user=120, description="Get your duel statistics") def __init__(self, bot): super().__init__(bot) self.duel_requests = {} self.duel_request_price = {} self.duel_targets = {} self.blUsers = ['admiralbulldog', 'infinitegachi'] def initiate_duel(self, **options): """ Initiate a duel with a user. You can also bet points on the winner. By default, the maximum amount of points you can spend is 420. How to add: !add funccommand duel initiate_duel --cd 0 --usercd 5 How to use: !duel USERNAME POINTS_TO_BET """ bot = options["bot"] source = options["source"] message = options["message"] if message is None: return False max_pot = self.settings["max_pot"] msg_split = message.split() username = msg_split[0] user = bot.users.find(username) duel_price = 300 if user is None: # No user was found with this username return False if len(msg_split) > 1: try: duel_price = pajbot.utils.parse_points_amount( source, msg_split[1]) if duel_price < 300: bot.whisper(source.username, 'You may only duel for 300+ points, no pussy') bot.whisper(user.username, '{} tried to duel you for less than 300 points. What a ' \ 'cheapskate EleGiggle'.format(source.username_raw)) return False except pajbot.exc.InvalidPointAmount as e: bot.whisper(user.username, str(e)) return False if source.username in self.duel_requests: bot.whisper( source.username, "You already have a duel request active with {}. Type !cancelduel to cancel your duel request." .format(self.duel_requests[source.username]), ) return False if user == source: # You cannot duel yourself return False if user.username in self.blUsers: return True if user.last_active is None or ( utils.now() - user._last_active).total_seconds() > 5 * 60: bot.whisper( source.username, "This user has not been active in chat within the last 5 minutes. Get them to type in chat before sending another challenge", ) return False if not user.can_afford(duel_price) or not source.can_afford( duel_price): bot.whisper( source.username, "You or your target do not have more than {} points, therefore you cannot duel for that amount." .format(duel_price), ) return False if user.username in self.duel_targets: bot.whisper( source.username, "This person is already being challenged by {}. Ask them to answer the offer by typing !deny or !accept" .format(self.duel_targets[user.username]), ) return False self.duel_targets[user.username] = source.username self.duel_requests[source.username] = user.username self.duel_request_price[source.username] = duel_price bot.whisper( user.username, "You have been challenged to a duel by {} for {} points. You can either !accept or !deny this challenge." .format(source.username_raw, duel_price), ) bot.whisper( source.username, "You have challenged {} for {} points".format( user.username_raw, duel_price)) bot.execute_delayed(60, self.time_expired, (source.username, user.username, bot)) def time_expired(self, initiator, target, bot): if target in self.duel_targets and initiator in self.duel_requests: del self.duel_targets[self.duel_requests[initiator]] del self.duel_requests[initiator] bot.whisper( initiator, 'Your duel request against {} has expired. Ditched OMEGALUL'. format(target)) bot.whisper( target, 'Chu ignoring {} for, his duel request against you expired cmonBruh' .format(initiator)) def cancel_duel(self, **options): """ Cancel any duel requests you've sent. How to add: !add funccomand cancelduel|duelcancel cancel_duel --cd 0 --usercd 10 How to use: !cancelduel """ bot = options["bot"] source = options["source"] if source.username not in self.duel_requests: bot.whisper(source.username, "You have not sent any duel requests") return bot.whisper( source.username, "You have cancelled the duel vs {}".format( self.duel_requests[source.username])) del self.duel_targets[self.duel_requests[source.username]] del self.duel_requests[source.username] def accept_duel(self, **options): """ Accepts any active duel requests you've received. How to add: !add funccommand accept accept_duel --cd 0 --usercd 0 How to use: !accept """ bot = options["bot"] source = options["source"] if source.username not in self.duel_targets: bot.whisper(source.username, "You are not being challenged to a duel by anyone.") return requestor = bot.users[self.duel_targets[source.username]] duel_price = self.duel_request_price[self.duel_targets[ source.username]] if not source.can_afford(duel_price) or not requestor.can_afford( duel_price): bot.whisper( source.username, "Your duel request with {} was cancelled due to one of you not having enough points." .format(requestor.username_raw), ) bot.whisper( requestor.username, "Your duel request with {} was cancelled due to one of you not having enough points." .format(source.username_raw), ) del self.duel_requests[self.duel_targets[source.username]] del self.duel_targets[source.username] return False source.points -= duel_price requestor.points -= duel_price participants = [source, requestor] winner = random.choice(participants) participants.remove(winner) loser = participants.pop() winner.points += duel_price * 2 winner.save() loser.save() DuelManager.user_won(winner, duel_price * 2) DuelManager.user_lost(loser, duel_price) arguments = { "winner": winner.username, "loser": loser.username, "total_pot": duel_price } message = self.get_phrase("message_won_points", **arguments) bot.say(message) del self.duel_requests[self.duel_targets[source.username]] del self.duel_targets[source.username] # HandlerManager.trigger("on_duel_complete", winner, loser, winning_pot, duel_price) def decline_duel(self, **options): """ Declines any active duel requests you've received. How to add: !add funccommand deny|decline decline_duel --cd 0 --usercd 0 How to use: !decline """ bot = options["bot"] source = options["source"] if source.username not in self.duel_targets: bot.whisper(source.username, "You are not being challenged to a duel") return False requestor_username = self.duel_targets[source.username] bot.whisper( source.username, "You have declined the duel vs {}".format(requestor_username)) bot.whisper( requestor_username, "{} declined the duel challenge with you.".format( source.username_raw)) del self.duel_targets[source.username] del self.duel_requests[requestor_username] def status_duel(self, **options): """ Whispers you the current status of your active duel requests/duel targets How to add: !add funccommand duelstatus|statusduel status_duel --cd 0 --usercd 5 How to use: !duelstatus """ bot = options["bot"] source = options["source"] msg = [] if source.username in self.duel_requests: msg.append("You have a duel request for {} points by {}".format( self.duel_request_price[source.username], self.duel_requests[source.username])) if source.username in self.duel_targets: msg.append( "You have a pending duel request from {} for {} points".format( self.duel_targets[source.username], self.duel_request_price[self.duel_targets[ source.username]])) if len(msg) > 0: bot.whisper(source.username, ". ".join(msg)) else: bot.whisper( source.username, "You have no duel request or duel target. Type !duel USERNAME POT to duel someone!" ) @staticmethod def get_duel_stats(**options): """ Whispers the users duel winratio to the user """ bot = options["bot"] source = options["source"] with DBManager.create_session_scope( expire_on_commit=False) as db_session: db_session.add(source.user_model) if source.duel_stats is None: bot.whisper(source.username, "You have no recorded duels.") return True bot.whisper( source.username, "duels: {ds.duels_total} winrate: {ds.winrate:.2f}% streak: {ds.current_streak} profit: {ds.profit}" .format(ds=source.duel_stats), )
class TriviaModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Trivia" DESCRIPTION = "Trivia!" CATEGORY = "Game" SETTINGS = [ ModuleSetting( key="hint_count", label= "How many hints the user should get before the question is ruined.", type="number", required=True, default=2, constraints={ "min_value": 0, "max_value": 4 }, ), ModuleSetting( key="step_delay", label= "Time between each step (step_delay*(hint_count+1) = length of each question)", type="number", required=True, placeholder="", default=10, constraints={ "min_value": 5, "max_value": 45 }, ), ModuleSetting( key="default_point_bounty", label="Default point bounty per right answer", type="number", required=True, placeholder="", default=0, constraints={ "min_value": 0, "max_value": 50 }, ), ModuleSetting( key="question_delay", label="Delay between questions in seconds.", type="number", required=True, placeholder="", default=0, constraints={ "min_value": 0, "max_value": 600 }, ), ] def __init__(self, bot): super().__init__(bot) self.job = None self.trivia_running = False self.last_question = None self.question = None self.step = 0 self.last_step = None self.point_bounty = 0 def poll_trivia(self): if self.question is None and (self.last_question is None or utils.now() - self.last_question >= datetime.timedelta(seconds=12)): url = "http://jservice.io/api/random" r = requests.get(url, headers={"User-Agent": self.bot.user_agent}) self.question = r.json()[0] self.question["answer"] = (self.question["answer"].replace( "<i>", "").replace("</i>", "").replace("\\", "").replace( "(", "").replace(")", "").strip('"').strip(".")) if (len(self.question["answer"]) == 0 or len(self.question["question"]) <= 1 or "href=" in self.question["answer"]): self.question = None return self.step = 0 self.last_step = None # Is it time for the next step? condition = self.last_question is None or utils.now( ) - self.last_question >= datetime.timedelta( seconds=self.settings["question_delay"]) if (self.last_step is None and condition) or ( self.last_step is not None and utils.now() - self.last_step >= datetime.timedelta(seconds=self.settings["step_delay"])): self.last_step = utils.now() self.step += 1 if self.step == 1: self.step_announce() elif self.step < self.settings["hint_count"] + 2: self.step_hint() else: self.step_end() def step_announce(self): try: self.bot.safe_me( f'KKona A new question has begun! In the category "{self.question["category"]["title"]}", the question/hint/clue is "{self.question["question"]}" KKona' ) except: self.step = 0 self.question = None pass def step_hint(self): # find out what % of the answer should be revealed full_hint_reveal = int(math.floor(len(self.question["answer"]) / 2)) current_hint_reveal = int( math.floor(((self.step) / self.settings["hint_count"]) * full_hint_reveal)) hint_arr = [] index = 0 for c in self.question["answer"]: if c == " ": hint_arr.append(" ") else: if index < current_hint_reveal: hint_arr.append(self.question["answer"][index]) else: hint_arr.append("_") index += 1 hint_str = "".join(hint_arr) self.bot.safe_me(f'OpieOP Here\'s a hint, "{hint_str}" OpieOP') def step_end(self): if self.question is not None: self.bot.safe_me( f'MingLee No one could answer the trivia! The answer was "{self.question["answer"]}" MingLee' ) self.question = None self.step = 0 self.last_question = utils.now() def command_start(self, bot, source, message, **rest): if self.trivia_running: bot.safe_me(f"{source}, a trivia is already running") return self.trivia_running = True self.job = ScheduleManager.execute_every(1, self.poll_trivia) try: self.point_bounty = int(message) if self.point_bounty < 0: self.point_bounty = 0 elif self.point_bounty > 50: self.point_bounty = 50 except: self.point_bounty = self.settings["default_point_bounty"] if self.point_bounty > 0: bot.safe_me( f"The trivia has started! {self.point_bounty} points for each right answer!" ) else: bot.safe_me("The trivia has started!") HandlerManager.add_handler("on_message", self.on_message) def command_stop(self, bot, source, **rest): if not self.trivia_running: bot.safe_me(f"{source}, no trivia is active right now") return self.job.remove() self.job = None self.trivia_running = False self.step_end() bot.safe_me("The trivia has been stopped.") HandlerManager.remove_handler("on_message", self.on_message) def on_message(self, source, message, whisper, **rest): if not message or whisper: return if self.question: right_answer = self.question["answer"].lower() user_answer = message.lower() if len(right_answer) <= 5: correct = right_answer == user_answer else: ratio = Levenshtein.ratio(right_answer, user_answer) correct = ratio >= 0.94 if correct: if self.point_bounty > 0: self.bot.safe_me( f"{source} got the answer right! The answer was {self.question['answer']} FeelsGoodMan They get {self.point_bounty} points! PogChamp" ) source.points += self.point_bounty else: self.bot.safe_me( f"{source} got the answer right! The answer was {self.question['answer']} FeelsGoodMan" ) self.question = None self.step = 0 self.last_question = utils.now() def load_commands(self, **options): self.commands["trivia"] = Command.multiaction_command( level=100, delay_all=0, delay_user=0, can_execute_with_whisper=True, commands={ "start": Command.raw_command(self.command_start, level=500, delay_all=0, delay_user=10, can_execute_with_whisper=True), "stop": Command.raw_command(self.command_stop, level=500, delay_all=0, delay_user=0, can_execute_with_whisper=True), }, )