def cmd_link(self, bot: Bot, source: User, message: str, event: Any, **rest: Any) -> None: bot.send_message_to_user( source, self.settings["phrase_room_link"].format(room_name=self.settings["room_name"]), event, method="reply", )
def selftimeout(self, bot: Bot, source: User, event: Any, **rest) -> bool: if self.settings["subscribers_only"] and not source.subscriber: return True if self.settings["vip_only"] and not source.vip: return True if source.moderator is True: return True random_value = random.randint(self.settings["low_value"], self.settings["high_value"]) standard_response = f"You got a {random_value}" if random_value == 0 and self.settings["zero_response"] != "": bot.send_message_to_user( source, f"{standard_response}. {self.settings['zero_response']}", event, method="reply") else: timeout_length = self.seconds_conversion(random_value) # Check if timeout value is over Twitch's maximum timeout_length = min(timeout_length, 1209600) bot.timeout(source, timeout_length, f"{standard_response}!", once=True) return True
def status_duel(self, bot: Bot, source: User, **rest: Any) -> None: """ 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: List[str] = [] if source.id in self.duel_requests: duelling = User.find_by_id(db_session, self.duel_requests[source.id]) if duelling: 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]) if challenger: 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!" )
def show_emote(self, bot: Bot, source: User, args: Dict[str, Any], **rest: Any) -> bool: 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 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!") return True
def get_duel_stats(bot: Bot, source: User, **rest: Any) -> None: """ 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 setUp(self): from pajbot.bot import Bot from pajbot.tbutil import load_config import datetime config = load_config('config.ini') args = Bot.parse_args() self.pajbot = Bot(config, args) self.source = self.pajbot.users['omgthisuserdoesnotexist123'] self.source.username_raw = 'OmgthisuserdoesnotexiSt123' self.source.points = 142 self.source.last_seen = datetime.datetime.strptime('17:01:42', '%H:%M:%S')
def add_link_whitelist(self, bot: Bot, source, message, **rest) -> bool: 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") return True
def setUp(self): from pajbot.bot import Bot from pajbot.models.user import User, UserManager from pajbot.tbutil import load_config import datetime config = load_config('config.ini') args = Bot.parse_args() self.pajbot = Bot(config, args) self.source = self.pajbot.users['testuser123Kappa'] self.source.username_raw = 'PajladA' self.source.points = 142 self.source.last_seen = datetime.datetime.strptime( '17:01:42', '%H:%M:%S')
def run(args): from pajbot.tbutil import load_config config = load_config(args.config) if 'main' not in config: log.error('Missing section [main] in config') sys.exit(1) if 'sql' in config: log.error( 'The [sql] section in config is no longer used. See config.example.ini for the new format under [main].' ) sys.exit(1) if 'db' not in config['main']: log.error('Missing required db config in the [main] section.') sys.exit(1) pajbot = Bot(config, args) pajbot.connect() def on_sigterm(signal, frame): pajbot.quit() sys.exit(0) signal.signal(signal.SIGTERM, on_sigterm) try: pajbot.start() except KeyboardInterrupt: pajbot.quit() pass
def decline_duel(self, bot: Bot, source: User, **options: Any) -> None: """ 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 with DBManager.create_session_scope() as db_session: requestor = User.find_by_id(db_session, self.duel_targets[source.id]) if not requestor: bot.whisper( source, "Your challenge never existed, don't ask me what happened!" ) return 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 __init__(self, bot: Bot) -> None: super().__init__(bot) self.twitter_stream: Optional[MyStreamListener] = None if "twitter" not in bot.config: return try: if self.use_twitter_stream: self.check_twitter_connection() bot.execute_every(60 * 5, self.check_twitter_connection) except: log.exception("Twitter authentication failed.")
def run(args): from pajbot.utils import load_config config = load_config(args.config) if "main" not in config: log.error("Missing section [main] in config") sys.exit(1) if "sql" in config: log.error( "The [sql] section in config is no longer used. See the example config for the new format under [main]." ) sys.exit(1) if "db" not in config["main"]: log.error("Missing required db config in the [main] section.") sys.exit(1) pajbot = Bot(config, args) pajbot.connect() def on_sigterm(signal, frame): pajbot.quit_bot() sys.exit(0) signal.signal(signal.SIGTERM, on_sigterm) try: pajbot.start() except KeyboardInterrupt: pajbot.quit_bot()
def update_game(self, bot: Bot, source, message, **rest) -> Any: if not message: bot.say("You must specify a game to update to!") return # Resolve game name to game ID game: Optional[ TwitchGame] = self.bot.twitch_helix_api.get_game_by_game_name( message) if not game: bot.say(f"Unable to find a game with the name '{message}'") return return self.generic_update(bot, source, message, "game", {"game_id": game.id})
def apply_substitutions(text, substitutions: Dict[Any, Substitution], bot: Bot, extra): for needle, sub in substitutions.items(): if sub.key and sub.argument: param = sub.key extra["argument"] = MessageAction.get_argument_value( extra["message"], sub.argument - 1) elif sub.key: param = sub.key elif sub.argument: param = MessageAction.get_argument_value(extra["message"], sub.argument - 1) else: log.error("Unknown param for response.") continue value: Any = sub.cb(param, extra) if value is None: return None try: for f in sub.filters: value = bot.apply_filter(value, f) except: log.exception("Exception caught in filter application") if value is None: return None text = text.replace(needle, str(value)) return text
def apply_substitutions(text, substitutions: Dict[Any, Substitution], bot: Bot, extra): for needle, sub in substitutions.items(): if sub.key and sub.argument: param = sub.key extra["argument"] = get_argument_value(extra["message"], sub.argument) elif sub.key: param = sub.key elif sub.argument: param = get_argument_value(extra["message"], sub.argument) else: log.error("Unknown param for response.") continue # The dictionary of substitutions here will always come from get_substitutions, which means it will always have a callback to call assert sub.cb is not None value: Any = sub.cb(param, extra) try: for f in sub.filters: value = bot.apply_filter(value, f) except: log.exception("Exception caught in filter application") if value is None: return None text = text.replace(needle, str(value)) return text
def run(args): from pajbot.utils import load_config config = load_config(args.config) if 'main' not in config: log.error('Missing section [main] in config') sys.exit(1) if 'sql' in config: log.error('The [sql] section in config is no longer used. See config.example.ini for the new format under [main].') sys.exit(1) if 'db' not in config['main']: log.error('Missing required db config in the [main] section.') sys.exit(1) pajbot = Bot(config, args) pajbot.connect() def on_sigterm(signal, frame): pajbot.quit_bot() sys.exit(0) signal.signal(signal.SIGTERM, on_sigterm) try: pajbot.start() except KeyboardInterrupt: pajbot.quit_bot() pass
def setUp(self): from pajbot.bot import Bot from pajbot.models.user import User, UserManager from pajbot.tbutil import load_config import datetime config = load_config('config.ini') args = Bot.parse_args() self.pajbot = Bot(config, args) self.source = self.pajbot.users['testuser123Kappa'] self.source.username_raw = 'PajladA' self.source.points = 142 self.source.last_seen = datetime.datetime.strptime('17:01:42', '%H:%M:%S')
def generic_update(self, bot: Bot, source, message: str, field: str, extra_args: Dict[str, str]) -> None: if not message: bot.say(f"You must specify a {field} to update to!") return if ("user:edit:broadcast" not in bot.streamer_access_token_manager.token.scope or not self.bot.twitch_helix_api.modify_channel_information( self.bot.streamer_user_id, authorization=bot.streamer_access_token_manager, **extra_args, )): bot.say( "Error: The streamer grants permission to update the game. The streamer needs to be re-authenticated to fix this problem." ) return log_msg = f'{source} updated the {field} to "{message}"' bot.say(log_msg) AdminLogManager.add_entry(f"{field.capitalize()} set", source, log_msg)
def cancel_duel(self, bot: Bot, source: User, **rest: Any) -> None: """ 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]) if not challenged: bot.whisper(source, "Could not find the user you challenged??") return 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 add_link_blacklist(self, bot: Bot, source, message, **rest) -> bool: options, new_links = self.parse_link_blacklist_arguments(message) if options is False: return False 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 update_title(self, bot: Bot, source, message, **rest) -> Any: auth_error = "Error: The streamer must grant permissions to update the title. The streamer needs to be re-authenticated to fix this problem." if ("user:edit:broadcast" not in bot.streamer_access_token_manager.token.scope and "channel:manage:broadcast" not in bot.streamer_access_token_manager.token.scope): bot.say(auth_error) return title = message if not title: bot.say("You must specify a title to update to!") return try: bot.twitch_helix_api.modify_channel_information( bot.streamer.id, {"title": title}, authorization=bot.streamer_access_token_manager, ) except HTTPError as e: if e.response.status_code == 401: log.error(f"Failed to update title to '{title}' - auth error") bot.say(auth_error) bot.streamer_access_token_manager.invalidate_token() elif e.response.status_code == 400: log.error(f"Title '{title}' contains banned words") bot.say( f"{source}, Title contained banned words. Please remove the banned words and try again." ) elif e.response.status_code == 500: log.error( f"Failed to update title to '{title}' - internal server error" ) bot.say( f"{source}, Failed to update the title! Please try again.") else: log.exception(f"Unhandled HTTPError when updating to {title}") return log_msg = f'{source} updated the title to "{title}"' bot.say(log_msg) AdminLogManager.add_entry("Title set", source, log_msg)
def update_game(self, bot: Bot, source, message, **rest) -> Any: auth_error = "Error: The streamer must grant permissions to update the game. The streamer needs to be re-authenticated to fix this problem." if ("user:edit:broadcast" not in bot.streamer_access_token_manager.token.scope and "channel:manage:broadcast" not in bot.streamer_access_token_manager.token.scope): bot.say(auth_error) return game_name = message if not game_name: bot.say("You must specify a game to update to!") return # Resolve game name to game ID game = bot.twitch_helix_api.get_game_by_game_name(game_name) if not game: bot.say(f"Unable to find a game with the name '{game_name}'") return try: bot.twitch_helix_api.modify_channel_information( bot.streamer.id, {"game_id": game.id}, authorization=bot.streamer_access_token_manager, ) except HTTPError as e: if e.response.status_code == 401: log.error( f"Failed to update game to '{game_name}' - auth error") bot.say(auth_error) bot.streamer_access_token_manager.invalidate_token() elif e.response.status_code == 500: log.error( f"Failed to update game to '{game_name}' - internal server error" ) bot.say(f"{source}, Failed to update game! Please try again.") else: log.exception( f"Unhandled HTTPError when updating to {game_name}") return log_msg = f'{source} updated the game to "{game_name}"' bot.say(log_msg) AdminLogManager.add_entry("Game set", source, log_msg)
def initiate_duel(self, bot: Bot, source: User, message: str, **rest: Any) -> bool: """ 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 max_pot = self.settings["max_pot"] 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 = 0 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.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 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") return True
def accept_duel(self, bot: Bot, source: User, **rest: Any) -> None: """ 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]) if not requestor: bot.whisper( source, "The user who challenged you is gone, I don't know where they went!" ) return 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 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 bingo_start(self, bot: Bot, source, message: str, event, args) -> bool: if self.bingo_running: bot.send_message_to_user(source, "A bingo is already running FailFish", event, method="reply") return False emote_instances = args["emote_instances"] known_sets = self.make_known_sets_dict(bot) selected_sets: Set[Tuple[str, Tuple[Emote, ...], bool]] = set() points_reward: Optional[int] = None unparsed_options = [] words_in_message = [s for s in message.split(" ") if len(s) > 0] if len(words_in_message) <= 0: bot.send_message_to_user( source, "You must at least give me some emote sets or emotes to choose from! FailFish", event, method="reply", ) 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 parsed_int: Optional[int] = None # Is the current word a number? try: parsed_int = int(word) except ValueError: pass 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.send_message_to_user( source, f"You can't start a bingo with that many points. FailFish {max_points} are allowed at most.", event, method="reply", ) return False allow_negative_bingo = self.settings["allow_negative_bingo"] if points_reward < 0 and not allow_negative_bingo: bot.send_message_to_user( source, "You can't start a bingo with negative points. FailFish", event, method="reply") return False min_points = -self.settings["max_negative_points"] if points_reward < min_points: bot.send_message_to_user( source, "You can't start a bingo with that many negative points. FailFish {min_points} are allowed at most.", event, method="reply", ) return False if len(selected_sets) <= 0: bot.send_message_to_user( source, "You must at least give me some emotes or emote sets to choose from! FailFish", event, method="reply", ) return False selected_set_names = [] selected_discrete_emote_codes = [] selected_emotes: Set[Emote] = 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) return True
pajbot = Bot(config, args) pajbot.connect() def on_sigterm(signal, frame): pajbot.quit_bot() sys.exit(0) signal.signal(signal.SIGTERM, on_sigterm) try: pajbot.start() except KeyboardInterrupt: pajbot.quit_bot() pass def handle_exceptions(exctype, value, tb): log.error('Logging an uncaught exception', exc_info=(exctype, value, tb)) if __name__ == '__main__': from pajbot.utils import init_logging sys.excepthook = handle_exceptions args = Bot.parse_args() init_logging('pajbot') run(args)
pajbot = Bot(config, args) pajbot.connect() def on_sigterm(signal, frame): pajbot.quit() sys.exit(0) signal.signal(signal.SIGTERM, on_sigterm) try: pajbot.start() except KeyboardInterrupt: pajbot.quit() pass def handle_exceptions(exctype, value, tb): log.error('Logging an uncaught exception', exc_info=(exctype, value, tb)) if __name__ == "__main__": from pajbot.tbutil import init_logging sys.excepthook = handle_exceptions args = Bot.parse_args() init_logging('pajbot') run(args)