class WarningModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Warnings" DESCRIPTION = "Gives people warnings before timing them out for the full duration for banphrase and stuff" CATEGORY = "Moderation" ENABLED_DEFAULT = True SETTINGS = [ ModuleSetting( key="total_chances", label= "How many warnings a user can receive before full timeout length", type="number", required=True, placeholder="", default=2, constraints={ "min_value": 1, "max_value": 10 }, ), ModuleSetting( key="length", label="How long warnings last before they expire", type="number", required=True, placeholder="Warning expiration length in seconds", default=600, constraints={ "min_value": 60, "max_value": 3600 }, ), ModuleSetting( key="base_timeout", label="Base timeout for warnings", type="number", required=True, placeholder="Base timeout length for warnings in seconds", default=10, constraints={ "min_value": 5, "max_value": 30 }, ), ModuleSetting( key="redis_prefix", label= "Prefix in the redis database. Only touch if you know what you're doing.", type="text", required=True, placeholder="Can be left blank, don't worry!", default="", ), ]
class WinDuelsQuestModule(BaseQuest): ID = "quest-" + __name__.split(".")[-1] NAME = "Win duels" DESCRIPTION = "Win X duels and make profit in every duel." PARENT_MODULE = QuestModule CATEGORY = "Quest" SETTINGS = [ ModuleSetting( key="quest_limit", label="How many duels must a user win.", type="number", required=True, placeholder="", default=10, constraints={ "min_value": 1, "max_value": 200 }, ) ] def get_limit(self): return self.settings["quest_limit"] def on_duel_complete(self, winner, points_won, **rest): if points_won < 1: return user_progress = self.get_user_progress(winner.username, default=0) if user_progress >= self.get_limit(): return user_progress += 1 redis = RedisManager.get() if user_progress == self.get_limit(): self.finish_quest(redis, winner) self.set_user_progress(winner.username, user_progress, redis=redis) def start_quest(self): HandlerManager.add_handler("on_duel_complete", self.on_duel_complete) redis = RedisManager.get() self.load_progress(redis=redis) def stop_quest(self): HandlerManager.remove_handler("on_duel_complete", self.on_duel_complete) redis = RedisManager.get() self.reset_progress(redis=redis) def get_objective(self): return "Win {} duels and make profit in every duel.".format( self.get_limit())
class WinDuelPointsQuestModule(BaseQuest): ID = "quest-" + __name__.split(".")[-1] NAME = "Win points in duels" DESCRIPTION = "You need to win X amount of points in a duel to complete this quest." PARENT_MODULE = QuestModule SETTINGS = [ ModuleSetting( key="min_value", label="Minimum amount of points the user needs to win", type="number", required=True, placeholder="", default=250, constraints={"min_value": 50, "max_value": 2000}, ), ModuleSetting( key="max_value", label="Maximum amount of points the user needs to win", type="number", required=True, placeholder="", default=750, constraints={"min_value": 100, "max_value": 4000}, ), ] LIMIT = 1 def __init__(self, bot): super().__init__(bot) self.points_required_key = "{streamer}:current_quest_points_required".format( streamer=StreamHelper.get_streamer() ) # The points_required variable is randomized at the start of the quest. # It will be a value between settings['min_value'] and settings['max_value'] self.points_required = None self.progress = {} def on_duel_complete(self, winner, points_won, **rest): if points_won < 1: # This duel did not award any points. # That means it's entirely irrelevant to us return total_points_won = self.get_user_progress(winner.username, default=0) if total_points_won >= self.points_required: # The user has already won enough points, and been rewarded already. return # If we get here, this means the user has not completed the quest yet. # And the user won some points in this duel total_points_won += points_won redis = RedisManager.get() if total_points_won >= self.points_required: # Reward the user with some tokens self.finish_quest(redis, winner) # Save the users "points won" progress self.set_user_progress(winner.username, total_points_won, redis=redis) def start_quest(self): HandlerManager.add_handler("on_duel_complete", self.on_duel_complete) redis = RedisManager.get() self.load_progress(redis=redis) self.load_data(redis=redis) self.LIMIT = self.points_required def load_data(self, redis=None): if redis is None: redis = RedisManager.get() self.points_required = redis.get(self.points_required_key) try: self.points_required = int(self.points_required) except (TypeError, ValueError): pass if self.points_required is None: try: self.points_required = random.randint(self.settings["min_value"], self.settings["max_value"] + 1) except ValueError: # someone f****d up self.points_required = 500 redis.set(self.points_required_key, self.points_required) def stop_quest(self): HandlerManager.remove_handler("on_duel_complete", self.on_duel_complete) redis = RedisManager.get() self.reset_progress(redis=redis) redis.delete(self.points_required_key) def get_objective(self): return "Make a profit of {} or more points in one or multiple duels.".format(self.points_required)
class PredictModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Prediction module" DESCRIPTION = "Handles predictions of arena wins" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="challenge_name", label="The name of the challenge", type="text", required=True, placeholder="The name of the challenge", default="100in10 arena", ), ModuleSetting( key="max_wins", label="The maximum amount of wins the user can predict", type="number", required=True, placeholder="The maximum amount of wins the user can bet", default=120, constraints={"min_value": 0}, ), ModuleSetting(key="sub_only", label="Sub only", type="boolean", required=True, default=True), ModuleSetting( key="mini_command", label="Mini predict command (Leave empty to disable)", type="text", required=True, default="", ), ModuleSetting( key="mini_max_wins", label= "The maximum amount of wins the user can predict in the mini prediction", type="number", required=True, placeholder="The maximum amount of wins the user can bet", default=12, constraints={"min_value": 0}, ), ] def load_commands(self, **options): self.commands["predict"] = Command.multiaction_command( level=100, default="vote", fallback="vote", delay_all=0, delay_user=0, commands={ "vote": Command.raw_command( self.predict, delay_all=0, delay_user=10, sub_only=self.settings["sub_only"], can_execute_with_whisper=True, description="Predict how many wins will occur in the " + self.settings["challenge_name"] + " challenge", ), "new": Command.raw_command( self.new_predict, delay_all=10, delay_user=10, description="Starts a new " + self.settings["challenge_name"] + " run", level=750, ), "end": Command.raw_command( self.end_predict, delay_all=10, delay_user=10, description="Ends a " + self.settings["challenge_name"] + " run", level=750, ), "close": Command.raw_command( self.close_predict, delay_all=10, delay_user=10, description="Close submissions to the latest " + self.settings["challenge_name"] + " run", level=750, ), }, ) # XXX: DEPRECATED, WILL BE REMOVED self.commands["newpredict"] = Command.raw_command( self.new_predict_depr, delay_all=10, delay_user=10, description="Starts a new " + self.settings["challenge_name"] + " run", level=750, ) self.commands["endpredict"] = Command.raw_command( self.end_predict_depr, delay_all=10, delay_user=10, description="Ends a " + self.settings["challenge_name"] + " run", level=750, ) self.commands["closepredict"] = Command.raw_command( self.close_predict_depr, delay_all=10, delay_user=10, description="Close submissions to the latest " + self.settings["challenge_name"] + " run", level=750, ) mini_command = self.settings["mini_command"].lower().replace( "!", "").replace(" ", "") if len(mini_command) > 0: self.commands[mini_command] = Command.multiaction_command( level=100, default="vote", fallback="vote", delay_all=0, delay_user=0, commands={ "vote": Command.raw_command( self.mini_predict, delay_all=0, delay_user=10, sub_only=self.settings["sub_only"], can_execute_with_whisper=True, description="Predict how many wins will occur in the " + self.settings["challenge_name"] + " challenge", ), "new": Command.raw_command( self.mini_new_predict, delay_all=10, delay_user=10, description="Starts a new " + self.settings["challenge_name"] + " run", level=750, ), "end": Command.raw_command( self.mini_end_predict, delay_all=10, delay_user=10, description="Ends a " + self.settings["challenge_name"] + " run", level=750, ), "close": Command.raw_command( self.mini_close_predict, delay_all=10, delay_user=10, description="Close submissions to the latest " + self.settings["challenge_name"] + " run", level=750, ), }, ) def new_predict_depr(self, **options): bot = options["bot"] source = options["source"] bot.whisper( source.username, 'This command is deprecated, please use "!predict new" in the future.' ) self.new_predict(**options) def end_predict_depr(self, **options): bot = options["bot"] source = options["source"] bot.whisper( source.username, 'This command is deprecated, please use "!predict end" in the future.' ) self.end_predict(**options) def close_predict_depr(self, **options): bot = options["bot"] source = options["source"] bot.whisper( source.username, 'This command is deprecated, please use "!predict close" in the future.' ) self.close_predict(**options) def shared_predict(self, bot, source, message, type): if type == 0: max_wins = self.settings["max_wins"] else: max_wins = self.settings["mini_max_wins"] example_wins = round(max_wins / 2) bad_command_message = "{username}, Missing or invalid argument to command. Valid argument could be {example_wins} where {example_wins} is a number between 0 and {max_wins} (inclusive).".format( username=source.username_raw, example_wins=example_wins, max_wins=max_wins) if source.id is None: log.warning( "Source ID is NONE, attempting to salvage by commiting users to the database." ) log.info("New ID is: {}".format(source.id)) bot.whisper(source.username, "uuh, please try the command again :D") return False prediction_number = None if message is None or len(message) < 0: bot.say(bad_command_message) return True try: prediction_number = int(message.split(" ")[0]) except (KeyError, ValueError): bot.say(bad_command_message) return True if prediction_number < 0 or prediction_number > max_wins: bot.say(bad_command_message) return True with DBManager.create_session_scope() as db_session: # Get the current open prediction current_prediction_run = ( db_session.query(PredictionRun).filter_by( ended=None, open=True, type=type).one_or_none()) if current_prediction_run is None: bot.say( "{}, There is no {} run active that accepts predictions right now." .format(source.username_raw, self.settings["challenge_name"])) return True user_entry = (db_session.query(PredictionRunEntry).filter_by( prediction_run_id=current_prediction_run.id, user_id=source.id).one_or_none()) if user_entry is not None: old_prediction_num = user_entry.prediction user_entry.prediction = prediction_number bot.say("{}, Updated your prediction for run {} from {} to {}". format(source.username_raw, current_prediction_run.id, old_prediction_num, prediction_number)) else: user_entry = PredictionRunEntry(current_prediction_run.id, source.id, prediction_number) db_session.add(user_entry) bot.say( "{}, Your prediction for {} wins in run {} has been submitted." .format(source.username_raw, prediction_number, current_prediction_run.id)) @staticmethod def shared_new_predict(bot, source, type): with DBManager.create_session_scope() as db_session: # Check if there is already an open prediction current_prediction_run = ( db_session.query(PredictionRun).filter_by( ended=None, open=True, type=type).one_or_none()) if current_prediction_run is not None: bot.say( "{}, There is already a prediction run accepting submissions, close it before you can start a new run." .format(source.username_raw)) return True new_prediction_run = PredictionRun(type) db_session.add(new_prediction_run) db_session.commit() bot.say( "A new prediction run has been started, and is now accepting submissions. Prediction run ID: {}" .format(new_prediction_run.id)) @staticmethod def shared_end_predict(bot, source, type): with DBManager.create_session_scope() as db_session: # Check if there is a non-ended, but closed prediction run we can end predictions = db_session.query(PredictionRun).filter_by( ended=None, open=False, type=type).all() if len(predictions) == 0: bot.say( "{}, There is no closed prediction runs we can end right now." .format(source.username_raw)) return True for prediction in predictions: prediction.ended = utils.now() bot.say("Closed predictions with IDs {}".format(", ".join( [str(p.id) for p in predictions]))) @staticmethod def shared_close_predict(bot, source, type): with DBManager.create_session_scope() as db_session: # Check if there is a non-ended, but closed prediction run we can end current_prediction_run = ( db_session.query(PredictionRun).filter_by( ended=None, open=True, type=type).one_or_none()) if current_prediction_run is None: bot.say( "{}, There is no open prediction runs we can close right now." .format(source.username_raw)) return True current_prediction_run.open = False bot.say( "{}, Predictions are no longer accepted for prediction run {}". format(source.username_raw, current_prediction_run.id)) def predict(self, **options): bot = options["bot"] message = options["message"] source = options["source"] self.shared_predict(bot, source, message, 0) def mini_predict(self, **options): bot = options["bot"] message = options["message"] source = options["source"] self.shared_predict(bot, source, message, 1) def new_predict(self, **options): bot = options["bot"] source = options["source"] self.shared_new_predict(bot, source, 0) def mini_new_predict(self, **options): bot = options["bot"] source = options["source"] self.shared_new_predict(bot, source, 1) def end_predict(self, **options): bot = options["bot"] source = options["source"] self.shared_end_predict(bot, source, 0) def mini_end_predict(self, **options): bot = options["bot"] source = options["source"] self.shared_end_predict(bot, source, 1) def close_predict(self, **options): bot = options["bot"] source = options["source"] self.shared_close_predict(bot, source, 0) def mini_close_predict(self, **options): bot = options["bot"] source = options["source"] self.shared_close_predict(bot, source, 1)
class WinHsBetPointsQuestModule(BaseQuest): ID = "quest-" + __name__.split(".")[-1] NAME = "HsBet Points" DESCRIPTION = "Win X points with Hearthstone bets." PARENT_MODULE = QuestModule CATEGORY = "Quest" SETTINGS = [ ModuleSetting( key="min_value", label="Minimum amount of points the user needs to win", type="number", required=True, placeholder="", default=200, constraints={ "min_value": 25, "max_value": 2000 }, ), ModuleSetting( key="max_value", label="Maximum amount of points the user needs to win", type="number", required=True, placeholder="", default=650, constraints={ "min_value": 100, "max_value": 4000 }, ), ] LIMIT = 1 def __init__(self, bot): super().__init__(bot) self.hsbet_points_key = f"{StreamHelper.get_streamer()}:current_quest_hsbet_points" self.hsbet_points_required = None self.progress = {} def on_user_win_hs_bet(self, user, points_won, **rest): if points_won < 1: return user_progress = self.get_user_progress(user, default=0) if user_progress >= self.hsbet_points_required: return user_progress += points_won redis = RedisManager.get() if user_progress >= self.hsbet_points_required: self.finish_quest(redis, user) self.set_user_progress(user, user_progress, redis=redis) def start_quest(self): HandlerManager.add_handler("on_user_win_hs_bet", self.on_user_win_hs_bet) redis = RedisManager.get() self.load_progress(redis=redis) self.load_data(redis=redis) self.LIMIT = self.hsbet_points_required def load_data(self, redis=None): if redis is None: redis = RedisManager.get() self.hsbet_points_required = redis.get(self.hsbet_points_key) try: self.hsbet_points_required = int(self.hsbet_points_required) except (TypeError, ValueError): pass if self.hsbet_points_required is None: try: self.hsbet_points_required = random.randint( self.settings["min_value"], self.settings["max_value"]) except ValueError: self.hsbet_points_required = 500 redis.set(self.hsbet_points_key, self.hsbet_points_required) def stop_quest(self): HandlerManager.remove_handler("on_user_win_hs_bet", self.on_user_win_hs_bet) redis = RedisManager.get() self.reset_progress(redis=redis) redis.delete(self.hsbet_points_key) def get_objective(self): return f"Make a profit of {self.hsbet_points_required} or more points in one or multiple hearthstone bets."
class CLROverlayModule(BaseModule): ID = "clroverlay-group" NAME = "CLR Overlay" DESCRIPTION = "A collection of overlays that can be used in the streaming software of choice" CATEGORY = "Feature" ENABLED_DEFAULT = True MODULE_TYPE = ModuleType.TYPE_ALWAYS_ENABLED BASE_ALLOWLIST_LABEL: str = "Allowlisted emotes (separate by spaces). Leave empty to use the blocklist." BASE_BLOCKLIST_LABEL: str = "Blocklisted emotes (separate by spaces). Leave empty to allow all emotes." ALLOWLIST_LABEL: str = f"{BASE_ALLOWLIST_LABEL} If this and the blocklist are empty, the parent module's allow and blocklist will be used." BLOCKLIST_LABEL: str = f"{BASE_BLOCKLIST_LABEL} If this and the allowlist are empty, the parent module's allow and blocklist will be used." EMOTELIST_PLACEHOLDER_TEXT: str = "e.g. Kappa Keepo PogChamp KKona" SETTINGS = [ ModuleSetting( key="emote_allowlist", label=BASE_ALLOWLIST_LABEL, type="text", required=True, placeholder=EMOTELIST_PLACEHOLDER_TEXT, default="", ), ModuleSetting( key="emote_blocklist", label=BASE_BLOCKLIST_LABEL, type="text", required=True, placeholder=EMOTELIST_PLACEHOLDER_TEXT, default="", ), ] def __init__(self, bot: Optional[Bot]) -> None: super().__init__(bot) self.allowlisted_emotes: Set[str] = set() self.blocklisted_emotes: Set[str] = set() def on_loaded(self) -> None: self.allowlisted_emotes = set( self.settings["emote_allowlist"].strip().split(" ") if self.settings["emote_allowlist"] else [] ) self.blocklisted_emotes = set( self.settings["emote_blocklist"].strip().split(" ") if self.settings["emote_blocklist"] else [] ) def is_emote_allowed(self, emote_code: str) -> bool: if len(self.allowlisted_emotes) > 0: return emote_code in self.allowlisted_emotes return emote_code not in self.blocklisted_emotes @classmethod def convert(cls, obj: Optional[BaseModule]) -> None: if obj is None: return if obj is not CLROverlayModule: raise RuntimeError("Paraneter sent to CLROverlay.convert must be a CLROverlayModule") obj.__class__ = CLROverlayModule
class AsciiProtectionModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Ascii Protection" DESCRIPTION = "Times out users who post messages that contain too many ASCII characters." CATEGORY = "Filter" SETTINGS = [ ModuleSetting( key="min_msg_length", label="Minimum message length to be considered bad", type="number", required=True, placeholder="", default=70, constraints={ "min_value": 20, "max_value": 1000 }, ), ModuleSetting( key="timeout_length", label="Timeout length", type="number", required=True, placeholder="Timeout length in seconds", default=120, 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 }, ), ModuleSetting( key="whisper_offenders", label="Send offenders a whisper explaining the timeout", type="boolean", required=True, default=False, ), ] @staticmethod def check_message(message): non_alnum = sum(not c.isalnum() for c in message) ratio = non_alnum / len(message) log.debug(message) log.debug(ratio) if (len(message) > 240 and ratio > 0.8) or ratio > 0.93: return True return False def on_pubmsg(self, source, message, **rest): if source.level >= self.settings[ "bypass_level"] or source.moderator is True: return if len(message) <= self.settings["min_msg_length"]: return if AsciiProtectionModule.check_message(message) is False: return duration, punishment = self.bot.timeout_warn( source, self.settings["timeout_length"], reason="Too many ASCII characters") """ We only send a notification to the user if he has spent more than one hour watching the stream. """ if self.settings[ "whisper_offenders"] and duration > 0 and source.minutes_in_chat_online > 60: self.bot.whisper( source.username, "You have been {punishment} because your message contained too many ascii characters." .format(punishment=punishment), ) return False def enable(self, bot): HandlerManager.add_handler("on_pubmsg", self.on_pubmsg) def disable(self, bot): HandlerManager.remove_handler("on_pubmsg", self.on_pubmsg)
class QuestModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Quest system" DESCRIPTION = "Give users a single quest at the start of each stream" CATEGORY = "Game" SETTINGS = [ ModuleSetting( key="action_currentquest", label="MessageAction for !currentquest", type="options", required=True, default="say", options=["say", "whisper", "me", "reply"], ), ModuleSetting( key="action_tokens", label="MessageAction for !tokens", type="options", required=True, default="whisper", options=["say", "whisper", "me", "reply"], ), ModuleSetting( key="reward_type", label="Reward type", type="options", required=True, default="tokens", options=["tokens", "points"], ), ModuleSetting(key="reward_amount", label="Reward amount", type="number", required=True, default=5), ModuleSetting( key="max_tokens", label="Max tokens", type="number", required=True, default=15, constraints={ "min_value": 1, "max_value": 5000 }, ), ] def __init__(self, bot): super().__init__(bot) self.current_quest = None self.current_quest_key = None def my_progress(self, bot, source, **rest): if self.current_quest is not None: quest_progress = self.current_quest.get_user_progress(source) quest_limit = self.current_quest.get_limit() if quest_limit is not None and quest_progress >= quest_limit: bot.whisper(source, "You have completed todays quest!") elif quest_progress is not False: bot.whisper( source, f"Your current quest progress is {quest_progress}") else: bot.whisper(source, "You have no progress on the current quest.") else: bot.say(f"{source}, There is no quest active right now.") def get_current_quest(self, bot, event, source, **rest): if self.current_quest: message_quest = f"the current quest active is {self.current_quest.get_objective()}." else: message_quest = "there is no quest active right now." bot.send_message_to_user(source, message_quest, event, method=self.settings["action_currentquest"]) def get_user_tokens(self, bot, event, source, **rest): message_tokens = f"{source}, you have {source.tokens} tokens." # todo use bot.send_message_to_user or similar if self.settings["action_tokens"] == "say": bot.say(message_tokens) elif self.settings["action_tokens"] == "whisper": bot.whisper(source, message_tokens) elif self.settings["action_tokens"] == "me": bot.me(message_tokens) elif self.settings["action_tokens"] == "reply": if event.type in ["action", "pubmsg"]: bot.say(message_tokens) elif event.type == "whisper": bot.whisper(source, message_tokens) def load_commands(self, **options): self.commands["myprogress"] = Command.raw_command( self.my_progress, can_execute_with_whisper=True, delay_all=0, delay_user=10) self.commands["currentquest"] = Command.raw_command( self.get_current_quest, can_execute_with_whisper=True, delay_all=2, delay_user=10) self.commands["tokens"] = Command.raw_command( self.get_user_tokens, can_execute_with_whisper=True, delay_all=0, delay_user=10) self.commands["quest"] = self.commands["currentquest"] def on_stream_start(self, **rest): if not self.current_quest_key: log.error( "Current quest key not set when on_stream_start event fired, something is wrong" ) return False available_quests = list( filter(lambda m: m.ID.startswith("quest-"), self.submodules)) if not available_quests: log.error("No quests enabled.") return False self.current_quest = random.choice(available_quests) self.current_quest.quest_module = self self.current_quest.start_quest() redis = RedisManager.get() redis.set(self.current_quest_key, self.current_quest.ID) self.bot.say("Stream started, new quest has been chosen!") self.bot.say( f"Current quest objective: {self.current_quest.get_objective()}") return True def on_stream_stop(self, **rest): if self.current_quest is None: log.info("No quest active on stream stop.") return False if not self.current_quest_key: log.error( "Current quest key not set when on_stream_stop event fired, something is wrong" ) return False self.current_quest.stop_quest() self.current_quest = None self.bot.say("Stream ended, quest has been reset.") redis = RedisManager.get() # Remove any mentions of the current quest redis.delete(self.current_quest_key) last_stream_id = StreamHelper.get_last_stream_id() if last_stream_id is False: log.error("No last stream ID found.") # No last stream ID found. why? return False return True def on_managers_loaded(self, **rest): # This function is used to resume a quest in case the bot starts when the stream is already live if not self.current_quest_key: log.error( "Current quest key not set when on_managers_loaded event fired, something is wrong" ) return if self.current_quest: # There's already a quest chosen for today return redis = RedisManager.get() current_quest_id = redis.get(self.current_quest_key) log.info(f"Try to load submodule with ID {current_quest_id}") if not current_quest_id: # No "current quest" was chosen by an above manager return current_quest_id = current_quest_id quest = find(lambda m: m.ID == current_quest_id, self.submodules) if not quest: log.info("No quest with id %s found in submodules (%s)", current_quest_id, self.submodules) return self.current_quest = quest self.current_quest.quest_module = self self.current_quest.start_quest() log.info(f"Resumed quest {quest.get_objective()}") def enable(self, bot): if self.bot: self.current_quest_key = f"{self.bot.streamer}:current_quest" HandlerManager.add_handler("on_stream_start", self.on_stream_start) HandlerManager.add_handler("on_stream_stop", self.on_stream_stop) HandlerManager.add_handler("on_managers_loaded", self.on_managers_loaded) def disable(self, bot): HandlerManager.remove_handler("on_stream_start", self.on_stream_start) HandlerManager.remove_handler("on_stream_stop", self.on_stream_stop) HandlerManager.remove_handler("on_managers_loaded", self.on_managers_loaded)
class WinHsBetWinsQuestModule(BaseQuest): ID = "quest-" + __name__.split(".")[-1] NAME = "HsBet Wins" DESCRIPTION = "Bet the right outcome on Hearthstone games X times." PARENT_MODULE = QuestModule CATEGORY = "Quest" SETTINGS = [ ModuleSetting( key="quest_limit", label="How many right outcomes must have a user.", type="number", required=True, placeholder="", default=4, constraints={ "min_value": 1, "max_value": 20 }, ) ] def get_limit(self): return self.settings["quest_limit"] def on_user_win_hs_bet(self, user, **rest): # User needs to make 1 point profit at least # if points_reward < 1: # return user_progress = self.get_user_progress(user, default=0) if user_progress >= self.get_limit(): return user_progress += 1 redis = RedisManager.get() if user_progress == self.get_limit(): self.finish_quest(redis, user) self.set_user_progress(user, user_progress, redis=redis) def start_quest(self): HandlerManager.add_handler("on_user_win_hs_bet", self.on_user_win_hs_bet) redis = RedisManager.get() self.load_progress(redis=redis) def stop_quest(self): HandlerManager.remove_handler("on_user_win_hs_bet", self.on_user_win_hs_bet) redis = RedisManager.get() self.reset_progress(redis=redis) def get_objective(self): return f"Bet the right outcome on {self.get_limit()} Hearthstone games."
class TypeEmoteQuestModule(BaseQuest): ID = "quest-" + __name__.split(".")[-1] NAME = "Type X emote Y times" DESCRIPTION = "A user needs to type a specific emote Y times to complete this quest." PARENT_MODULE = QuestModule SETTINGS = [ ModuleSetting( key="quest_limit", label="How many emotes you need to type", type="number", required=True, placeholder="How many emotes you need to type (default 100)", default=100, constraints={ "min_value": 10, "max_value": 200 }, ) ] def __init__(self, bot): super().__init__(bot) self.current_emote_key = f"{StreamHelper.get_streamer()}:current_quest_emote" self.current_emote = None self.progress = {} def get_limit(self): return self.settings["quest_limit"] def on_message(self, source, emote_instances, **rest): typed_emotes = { emote_instance.emote for emote_instance in emote_instances } if self.current_emote not in typed_emotes: return user_progress = self.get_user_progress(source, default=0) + 1 if user_progress > self.get_limit(): log.debug( f"{source} has already completed the quest. Moving along.") # no need to do more return redis = RedisManager.get() if user_progress == self.get_limit(): self.finish_quest(redis, source) self.set_user_progress(source, user_progress, redis=redis) def start_quest(self): HandlerManager.add_handler("on_message", self.on_message) redis = RedisManager.get() self.load_progress(redis=redis) self.load_data(redis=redis) def load_data(self, redis=None): if redis is None: redis = RedisManager.get() redis_json = redis.get(self.current_emote_key) if redis_json is None: # randomize an emote # TODO possibly a setting to allow the user to configure the twitch_global=True, etc # parameters to random_emote? self.current_emote = self.bot.emote_manager.random_emote( twitch_global=True) # If EmoteManager has no global emotes, current_emote will be None if self.current_emote is not None: redis.set(self.current_emote_key, json.dumps(self.current_emote.jsonify())) else: self.current_emote = Emote(**json.loads(redis_json)) def stop_quest(self): HandlerManager.remove_handler("on_message", self.on_message) redis = RedisManager.get() self.reset_progress(redis=redis) redis.delete(self.current_emote_key) def get_objective(self): return f"Use the {self.current_emote.code} emote {self.get_limit()} times"
class EmoteComboModule(BaseModule): ID = __name__.split(".")[-1] NAME = "Emote Combo (web interface)" DESCRIPTION = "Shows emote combos in the web interface CLR thing" CATEGORY = "Feature" SETTINGS = [ ModuleSetting( key="min_emote_combo", label="Minimum number of emotes required to trigger the combo", type="number", required=True, placeholder="", default=5, constraints={"min_value": 2, "max_value": 100}, ) ] def __init__(self, bot): super().__init__(bot) self.emote_count = 0 self.current_emote = None def inc_emote_count(self): self.emote_count += 1 if self.emote_count >= self.settings["min_emote_combo"]: self.bot.websocket_manager.emit( "emote_combo", {"emote": self.current_emote.jsonify(), "count": self.emote_count} ) def reset(self): self.emote_count = 0 self.current_emote = None def on_message(self, emote_instances, emote_counts, whisper, **rest): if whisper: return True # Check if the message contains exactly one unique emote num_unique_emotes = len(emote_counts) if num_unique_emotes != 1: self.reset() return True new_emote = emote_instances[0].emote new_emote_code = new_emote.code # if there is currently a combo... if self.current_emote is not None: # and this emote is not equal to the combo emote... if self.current_emote.code != new_emote_code: # The emote of this message is not the one we were previously counting, reset. # We do not stop. # We start counting this emote instead. self.reset() if self.current_emote is None: self.current_emote = new_emote self.inc_emote_count() 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 AsciiProtectionModule(BaseModule): ID = __name__.split(".")[-1] NAME = "ASCII Protection" DESCRIPTION = "Times out users who post messages that contain too many ASCII characters." CATEGORY = "Moderation" SETTINGS = [ ModuleSetting( key="enabled_by_stream_status", label="Enable moderation of ASCII characters when the stream is:", type="options", required=True, default="Offline and Online", options=["Online Only", "Offline Only", "Offline and Online"], ), ModuleSetting( key="min_msg_length", label="Minimum message length to be considered bad", type="number", required=True, placeholder="", default=70, constraints={"min_value": 20, "max_value": 500}, ), 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=120, constraints={"min_value": 1, "max_value": 1209600}, ), 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_reason", label="Timeout Reason", type="text", required=False, placeholder="", default="Too many ASCII characters", constraints={}, ), ModuleSetting( key="disable_warnings", label="Disable warning timeouts", type="boolean", required=True, default=False, ), ] @staticmethod def check_message(message): if len(message) <= 0: return False non_alnum = sum(not c.isalnum() for c in message) ratio = non_alnum / len(message) if (len(message) > 240 and ratio > 0.8) or ratio > 0.93: return True return False def on_pubmsg(self, source, message, tags, **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 len(message) <= self.settings["min_msg_length"]: return if AsciiProtectionModule.check_message(message) is False: return if self.settings["moderation_action"] == "Delete": self.bot.delete_message(tags["id"]) elif self.settings["disable_warnings"] is True and self.settings["moderation_action"] == "Timeout": self.bot.timeout(source, self.settings["timeout_length"], reason=self.settings["timeout_reason"]) else: self.bot.timeout_warn(source, self.settings["timeout_length"], reason=self.settings["timeout_reason"]) return False def enable(self, bot): HandlerManager.add_handler("on_pubmsg", self.on_pubmsg, priority=150, run_if_propagation_stopped=True) def disable(self, bot): HandlerManager.remove_handler("on_pubmsg", self.on_pubmsg)
class TypeMeMessageQuestModule(BaseQuest): ID = "quest-" + __name__.split(".")[-1] NAME = "Colorful chat /me" DESCRIPTION = "Type X /me messages with X message length." PARENT_MODULE = QuestModule CATEGORY = "Quest" SETTINGS = [ ModuleSetting( key="quest_limit", label="How many messages does the user needs to type?", type="number", required=True, placeholder="", default=100, constraints={ "min_value": 1, "max_value": 200 }, ), ModuleSetting( key="quest_message_length", label="How many letters minimum should be in the message?", type="number", required=True, placeholder="", default=15, constraints={ "min_value": 1, "max_value": 500 }, ), ] def get_limit(self): return self.settings["quest_limit"] def get_quest_message_length(self): return self.settings["quest_message_length"] def on_message(self, source, message, event, **rest): if len(message) < self.get_quest_message_length( ) or event.type != "action": return user_progress = self.get_user_progress(source.username, default=0) if user_progress >= self.get_limit(): return user_progress += 1 redis = RedisManager.get() if user_progress == self.get_limit(): self.finish_quest(redis, source) self.set_user_progress(source.username, user_progress, redis=redis) def start_quest(self): HandlerManager.add_handler("on_message", self.on_message) redis = RedisManager.get() self.load_progress(redis=redis) def stop_quest(self): HandlerManager.remove_handler("on_message", self.on_message) redis = RedisManager.get() self.reset_progress(redis=redis) def get_objective(self): return "Type {0} /me messages with a length of minimum {1} letters KappaPride ".format( self.get_limit(), self.get_quest_message_length())
class AsciiProtectionModule(BaseModule): ID = __name__.split(".")[-1] NAME = "ASCII Protection" DESCRIPTION = "Times out users who post messages that contain too many ASCII characters." CATEGORY = "Moderation" SETTINGS = [ ModuleSetting( key="enabled_by_stream_status", label="Enable moderation of ASCII characters when the stream is:", type="options", required=True, default="Offline and Online", options=["Online Only", "Offline Only", "Offline and Online"], ), ModuleSetting( key="min_msg_length", label="Minimum message length to be considered bad", type="number", required=True, placeholder="", default=70, constraints={ "min_value": 20, "max_value": 1000 }, ), 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=120, 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 }, ), ModuleSetting( key="timeout_reason", label="Timeout Reason", type="text", required=False, placeholder="", default="Too many ASCII characters", constraints={}, ), ModuleSetting( key="whisper_offenders", label="Send offenders a whisper explaining the timeout", type="boolean", required=True, default=False, ), ModuleSetting( key="whisper_timeout_reason", label="Whisper Timeout Reason | Available arguments: {punishment}", type="text", required=False, placeholder="", default= "You have been {punishment} because your message contained too many ascii characters.", constraints={}, ), ] @staticmethod def check_message(message): if len(message) <= 0: return False non_alnum = sum(not c.isalnum() for c in message) ratio = non_alnum / len(message) if (len(message) > 240 and ratio > 0.8) or ratio > 0.93: return True return False def on_pubmsg(self, source, message, tags, **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 len(message) <= self.settings["min_msg_length"]: return if AsciiProtectionModule.check_message(message) is False: return if self.settings["moderation_action"] == "Delete": self.bot.delete_message(tags["id"]) else: duration, punishment = self.bot.timeout_warn( source, self.settings["timeout_length"], reason=self.settings["timeout_reason"]) """ We only send a notification to the user if he has spent more than one hour watching the stream. """ if self.settings[ "whisper_offenders"] and duration > 0 and source.time_in_chat_online >= timedelta( hours=1): self.bot.whisper( source, self.settings["whisper_timeout_reason"].format( punishment=punishment)) return False def enable(self, bot): HandlerManager.add_handler("on_pubmsg", self.on_pubmsg) def disable(self, bot): HandlerManager.remove_handler("on_pubmsg", self.on_pubmsg)
class EmoteComboModule(BaseModule): ID = __name__.rsplit(".", maxsplit=1)[-1] NAME = "Emote Combos" DESCRIPTION = "Shows emote combos on the CLR pajbot overlay" CATEGORY = "Feature" PARENT_MODULE = CLROverlayModule SETTINGS = [ ModuleSetting( key="min_emote_combo", label="Minimum number of emotes required to trigger the combo", type="number", required=True, placeholder="", default=5, constraints={"min_value": 2, "max_value": 100}, ), ModuleSetting( key="emote_allowlist", label=CLROverlayModule.ALLOWLIST_LABEL, type="text", required=True, placeholder=CLROverlayModule.EMOTELIST_PLACEHOLDER_TEXT, default="", ), ModuleSetting( key="emote_blocklist", label=CLROverlayModule.BLOCKLIST_LABEL, type="text", required=True, placeholder=CLROverlayModule.EMOTELIST_PLACEHOLDER_TEXT, default="", ), ] def __init__(self, bot: Optional[Bot]) -> None: super().__init__(bot) self.allowlisted_emotes: Set[str] = set() self.blocklisted_emotes: Set[str] = set() self.parent_module: Optional[CLROverlayModule] = ( CLROverlayModule.convert(self.bot.module_manager["clroverlay-group"]) if self.bot else None ) self.emote_count: int = 0 self.current_emote: Optional[Emote] = None def on_loaded(self) -> None: self.allowlisted_emotes = set( self.settings["emote_allowlist"].strip().split(" ") if self.settings["emote_allowlist"] else [] ) self.blocklisted_emotes = set( self.settings["emote_blocklist"].strip().split(" ") if self.settings["emote_blocklist"] else [] ) def is_emote_allowed(self, emote_code: str) -> bool: if len(self.allowlisted_emotes) > 0: return emote_code in self.allowlisted_emotes if len(self.blocklisted_emotes) > 0: return emote_code not in self.blocklisted_emotes if not self.parent_module: return True return self.parent_module.is_emote_allowed(emote_code) def inc_emote_count(self) -> None: if self.bot is None: log.warning("EmoteCombo inc_emote_count called when bot is none") return assert self.current_emote is not None self.emote_count += 1 if self.emote_count >= self.settings["min_emote_combo"]: self.bot.websocket_manager.emit( "emote_combo", {"emote": self.current_emote.jsonify(), "count": self.emote_count} ) def reset(self) -> None: self.emote_count = 0 self.current_emote = None def on_message( self, emote_instances: List[EmoteInstance], emote_counts: EmoteInstanceCountMap, whisper: bool, **rest: Any ) -> bool: if whisper: return True # Check if the message contains exactly one unique emote num_unique_emotes = len(emote_counts) if num_unique_emotes != 1: self.reset() return True new_emote = emote_instances[0].emote new_emote_code = new_emote.code if self.is_emote_allowed(new_emote_code) is False: self.reset() return True # if there is currently a combo... if self.current_emote is not None: # and this emote is not equal to the combo emote... if self.current_emote.code != new_emote_code: # The emote of this message is not the one we were previously counting, reset. # We do not stop. # We start counting this emote instead. self.reset() if self.current_emote is None: self.current_emote = new_emote self.inc_emote_count() return True def enable(self, bot: Optional[Bot]) -> None: HandlerManager.add_handler("on_message", self.on_message) def disable(self, bot: Optional[Bot]) -> None: HandlerManager.remove_handler("on_message", self.on_message)