def load_bot(config: Config, twitch_helix_api: TwitchHelixAPI) -> UserBasics: bot_id, bot_login = load_bot_id_or_login(config) if bot_id is not None: return twitch_helix_api.require_user_basics_by_id(bot_id) if bot_login is not None: return twitch_helix_api.require_user_basics_by_login(bot_login) raise ValueError("Bad config, missing bot id or login")
def load_streamer(config: Config, twitch_helix_api: TwitchHelixAPI) -> UserBasics: streamer_id, streamer_login = load_streamer_id_or_login(config) if streamer_id is not None: return twitch_helix_api.require_user_basics_by_id(streamer_id) if streamer_login is not None: return twitch_helix_api.require_user_basics_by_login(streamer_login) raise ValueError("Bad config, missing streamer id or login")
def find_or_create_from_login(db_session, twitch_helix_api: TwitchHelixAPI, login: str) -> Optional[User]: user_from_db = (db_session.query(User).filter_by(login=login).order_by( User.login_last_updated.desc()).one_or_none()) if user_from_db is not None: return user_from_db # no user in DB! Query Helix API for user basics, then create user/update existing user and return. basics = twitch_helix_api.get_user_basics_by_login(login) if basics is None: return None return User.from_basics(db_session, basics)
def download_logo(twitch_helix_api: TwitchHelixAPI, streamer: UserBasics) -> None: logo_url = twitch_helix_api.get_profile_image_url(streamer.id) if logo_url is None: log.warn( f"Failed to query Twitch API for the profile image url of streamer {streamer.login}" ) return logo_raw_path = f"static/images/logo_{streamer.login}.png" # returns bytes logo_image_bytes = BaseAPI(None).get_binary(logo_url) # write full-size image... with open(logo_raw_path, "wb") as logo_raw_file: logo_raw_file.write(logo_image_bytes)
def __init__(self, config, args): self.config = config self.args = args ScheduleManager.init() DBManager.init(self.config["main"]["db"]) # redis redis_options = {} if "redis" in config: redis_options = dict(config.items("redis")) RedisManager.init(**redis_options) utils.wait_for_redis_data_loaded(RedisManager.get()) self.nickname = config["main"].get("nickname", "pajbot") if config["main"].getboolean("verified", False): self.tmi_rate_limits = TMIRateLimits.VERIFIED elif config["main"].getboolean("known", False): self.tmi_rate_limits = TMIRateLimits.KNOWN else: self.tmi_rate_limits = TMIRateLimits.BASE self.whisper_output_mode = WhisperOutputMode.from_config_value( config["main"].get("whisper_output_mode", "normal") ) # phrases self.phrases = { "welcome": ["{nickname} {version} running! HeyGuys"], "quit": ["{nickname} {version} shutting down... BibleThump"], } if "phrases" in config: phrases = config["phrases"] if "welcome" in phrases: self.phrases["welcome"] = phrases["welcome"].splitlines() if "quit" in phrases: self.phrases["quit"] = phrases["quit"].splitlines() # Remembers whether the "welcome" phrases have already been said. We don't want to send the # welcome messages to chat again on a reconnect. self.welcome_messages_sent = False # streamer if "streamer" in config["main"]: self.streamer = config["main"]["streamer"] self.channel = "#" + self.streamer elif "target" in config["main"]: self.channel = config["main"]["target"] self.streamer = self.channel[1:] self.bot_domain = self.config["web"]["domain"] self.streamer_display = self.config["web"]["streamer_name"] log.debug("Loaded config") # do this earlier since schema upgrade can depend on the helix api self.api_client_credentials = ClientCredentials( self.config["twitchapi"]["client_id"], self.config["twitchapi"]["client_secret"], self.config["twitchapi"]["redirect_uri"], ) self.twitch_id_api = TwitchIDAPI(self.api_client_credentials) self.twitch_tmi_api = TwitchTMIAPI() self.app_token_manager = AppAccessTokenManager(self.twitch_id_api, RedisManager.get()) self.twitch_helix_api = TwitchHelixAPI(RedisManager.get(), self.app_token_manager) self.twitch_v5_api = TwitchKrakenV5API(self.api_client_credentials, RedisManager.get()) self.bot_user_id = self.twitch_helix_api.get_user_id(self.nickname) if self.bot_user_id is None: raise ValueError("The bot login name you entered under [main] does not exist on twitch.") self.streamer_user_id = self.twitch_helix_api.get_user_id(self.streamer) if self.streamer_user_id is None: raise ValueError("The streamer login name you entered under [main] does not exist on twitch.") self.streamer_access_token_manager = UserAccessTokenManager( api=self.twitch_id_api, redis=RedisManager.get(), username=self.streamer, user_id=self.streamer_user_id ) StreamHelper.init_streamer(self.streamer, self.streamer_user_id, self.streamer_display) # SQL migrations with DBManager.create_dbapi_connection_scope() as sql_conn: sql_migratable = DatabaseMigratable(sql_conn) sql_migration = Migration(sql_migratable, pajbot.migration_revisions.db, self) sql_migration.run() # Redis migrations redis_migratable = RedisMigratable(redis_options=redis_options, namespace=self.streamer) redis_migration = Migration(redis_migratable, pajbot.migration_revisions.redis, self) redis_migration.run() # Thread pool executor for async actions self.action_queue = ActionQueue() # refresh points_rank and num_lines_rank regularly UserRanksRefreshManager.start(self.action_queue) self.reactor = irc.client.Reactor() # SafeDefaultScheduler makes the bot not exit on exception in the main thread # e.g. on actions via bot.execute_now, etc. self.reactor.scheduler_class = SafeDefaultScheduler self.reactor.scheduler = SafeDefaultScheduler() self.start_time = utils.now() ActionParser.bot = self HandlerManager.init_handlers() self.socket_manager = SocketManager(self.streamer, self.execute_now) self.stream_manager = StreamManager(self) StreamHelper.init_stream_manager(self.stream_manager) self.decks = DeckManager() self.banphrase_manager = BanphraseManager(self).load() self.timer_manager = TimerManager(self).load() self.kvi = KVIManager() # bot access token if "password" in self.config["main"]: log.warning( "DEPRECATED - Using bot password/oauth token from file. " "You should authenticate in web gui using route /bot_login " "and remove password from config file" ) access_token = self.config["main"]["password"] if access_token.startswith("oauth:"): access_token = access_token[6:] self.bot_token_manager = UserAccessTokenManager( api=None, redis=None, username=self.nickname, user_id=self.bot_user_id, token=UserAccessToken.from_implicit_auth_flow_token(access_token), ) else: self.bot_token_manager = UserAccessTokenManager( api=self.twitch_id_api, redis=RedisManager.get(), username=self.nickname, user_id=self.bot_user_id ) self.emote_manager = EmoteManager(self.twitch_v5_api, self.action_queue) self.epm_manager = EpmManager() self.ecount_manager = EcountManager() if "twitter" in self.config and self.config["twitter"].get("streaming_type", "twitter") == "tweet-provider": self.twitter_manager = PBTwitterManager(self) else: self.twitter_manager = TwitterManager(self) self.module_manager = ModuleManager(self.socket_manager, bot=self).load() self.commands = CommandManager( socket_manager=self.socket_manager, module_manager=self.module_manager, bot=self ).load() self.websocket_manager = WebSocketManager(self) HandlerManager.trigger("on_managers_loaded") # Commitable managers self.commitable = {"commands": self.commands, "banphrases": self.banphrase_manager} self.execute_every(60, self.commit_all) self.execute_every(1, self.do_tick) # promote the admin to level 2000 self.admin = self.config["main"].get("admin", None) if self.admin is None: log.warning("No admin user specified. See the [main] section in the example config for its usage.") else: with DBManager.create_session_scope() as db_session: admin_user = User.find_or_create_from_login(db_session, self.twitch_helix_api, self.admin) if admin_user is None: log.warning( "The login name you entered for the admin user does not exist on twitch. " "No admin user has been created." ) else: admin_user.level = 2000 # silent mode self.silent = ( "flags" in config and "silent" in config["flags"] and config["flags"]["silent"] == "1" ) or args.silent if self.silent: log.info("Silent mode enabled") # dev mode self.dev = "flags" in config and "dev" in config["flags"] and config["flags"]["dev"] == "1" if self.dev: self.version_long = utils.extend_version_if_possible(VERSION) else: self.version_long = VERSION self.irc = IRCManager(self) relay_host = self.config["main"].get("relay_host", None) relay_password = self.config["main"].get("relay_password", None) if relay_host is not None or relay_password is not None: log.warning( "DEPRECATED - Relaybroker support is no longer implemented. relay_host and relay_password are ignored" ) self.data = { "broadcaster": self.streamer, "version": self.version_long, "version_brief": VERSION, "bot_name": self.nickname, "bot_domain": self.bot_domain, "streamer_display": self.streamer_display, } self.data_cb = { "status_length": self.c_status_length, "stream_status": self.c_stream_status, "bot_uptime": self.c_uptime, "current_time": self.c_current_time, "molly_age_in_years": self.c_molly_age_in_years, } self.user_agent = f"pajbot1/{VERSION} ({self.nickname})"
class Bot: """ Main class for the twitch bot """ def __init__(self, config, args): self.config = config self.args = args ScheduleManager.init() DBManager.init(self.config["main"]["db"]) # redis redis_options = {} if "redis" in config: redis_options = dict(config.items("redis")) RedisManager.init(**redis_options) utils.wait_for_redis_data_loaded(RedisManager.get()) self.nickname = config["main"].get("nickname", "pajbot") if config["main"].getboolean("verified", False): self.tmi_rate_limits = TMIRateLimits.VERIFIED elif config["main"].getboolean("known", False): self.tmi_rate_limits = TMIRateLimits.KNOWN else: self.tmi_rate_limits = TMIRateLimits.BASE self.whisper_output_mode = WhisperOutputMode.from_config_value( config["main"].get("whisper_output_mode", "normal") ) # phrases self.phrases = { "welcome": ["{nickname} {version} running! HeyGuys"], "quit": ["{nickname} {version} shutting down... BibleThump"], } if "phrases" in config: phrases = config["phrases"] if "welcome" in phrases: self.phrases["welcome"] = phrases["welcome"].splitlines() if "quit" in phrases: self.phrases["quit"] = phrases["quit"].splitlines() # Remembers whether the "welcome" phrases have already been said. We don't want to send the # welcome messages to chat again on a reconnect. self.welcome_messages_sent = False # streamer if "streamer" in config["main"]: self.streamer = config["main"]["streamer"] self.channel = "#" + self.streamer elif "target" in config["main"]: self.channel = config["main"]["target"] self.streamer = self.channel[1:] self.bot_domain = self.config["web"]["domain"] self.streamer_display = self.config["web"]["streamer_name"] log.debug("Loaded config") # do this earlier since schema upgrade can depend on the helix api self.api_client_credentials = ClientCredentials( self.config["twitchapi"]["client_id"], self.config["twitchapi"]["client_secret"], self.config["twitchapi"]["redirect_uri"], ) self.twitch_id_api = TwitchIDAPI(self.api_client_credentials) self.twitch_tmi_api = TwitchTMIAPI() self.app_token_manager = AppAccessTokenManager(self.twitch_id_api, RedisManager.get()) self.twitch_helix_api = TwitchHelixAPI(RedisManager.get(), self.app_token_manager) self.twitch_v5_api = TwitchKrakenV5API(self.api_client_credentials, RedisManager.get()) self.bot_user_id = self.twitch_helix_api.get_user_id(self.nickname) if self.bot_user_id is None: raise ValueError("The bot login name you entered under [main] does not exist on twitch.") self.streamer_user_id = self.twitch_helix_api.get_user_id(self.streamer) if self.streamer_user_id is None: raise ValueError("The streamer login name you entered under [main] does not exist on twitch.") self.streamer_access_token_manager = UserAccessTokenManager( api=self.twitch_id_api, redis=RedisManager.get(), username=self.streamer, user_id=self.streamer_user_id ) StreamHelper.init_streamer(self.streamer, self.streamer_user_id, self.streamer_display) # SQL migrations with DBManager.create_dbapi_connection_scope() as sql_conn: sql_migratable = DatabaseMigratable(sql_conn) sql_migration = Migration(sql_migratable, pajbot.migration_revisions.db, self) sql_migration.run() # Redis migrations redis_migratable = RedisMigratable(redis_options=redis_options, namespace=self.streamer) redis_migration = Migration(redis_migratable, pajbot.migration_revisions.redis, self) redis_migration.run() # Thread pool executor for async actions self.action_queue = ActionQueue() # refresh points_rank and num_lines_rank regularly UserRanksRefreshManager.start(self.action_queue) self.reactor = irc.client.Reactor() # SafeDefaultScheduler makes the bot not exit on exception in the main thread # e.g. on actions via bot.execute_now, etc. self.reactor.scheduler_class = SafeDefaultScheduler self.reactor.scheduler = SafeDefaultScheduler() self.start_time = utils.now() ActionParser.bot = self HandlerManager.init_handlers() self.socket_manager = SocketManager(self.streamer, self.execute_now) self.stream_manager = StreamManager(self) StreamHelper.init_stream_manager(self.stream_manager) self.decks = DeckManager() self.banphrase_manager = BanphraseManager(self).load() self.timer_manager = TimerManager(self).load() self.kvi = KVIManager() # bot access token if "password" in self.config["main"]: log.warning( "DEPRECATED - Using bot password/oauth token from file. " "You should authenticate in web gui using route /bot_login " "and remove password from config file" ) access_token = self.config["main"]["password"] if access_token.startswith("oauth:"): access_token = access_token[6:] self.bot_token_manager = UserAccessTokenManager( api=None, redis=None, username=self.nickname, user_id=self.bot_user_id, token=UserAccessToken.from_implicit_auth_flow_token(access_token), ) else: self.bot_token_manager = UserAccessTokenManager( api=self.twitch_id_api, redis=RedisManager.get(), username=self.nickname, user_id=self.bot_user_id ) self.emote_manager = EmoteManager(self.twitch_v5_api, self.action_queue) self.epm_manager = EpmManager() self.ecount_manager = EcountManager() if "twitter" in self.config and self.config["twitter"].get("streaming_type", "twitter") == "tweet-provider": self.twitter_manager = PBTwitterManager(self) else: self.twitter_manager = TwitterManager(self) self.module_manager = ModuleManager(self.socket_manager, bot=self).load() self.commands = CommandManager( socket_manager=self.socket_manager, module_manager=self.module_manager, bot=self ).load() self.websocket_manager = WebSocketManager(self) HandlerManager.trigger("on_managers_loaded") # Commitable managers self.commitable = {"commands": self.commands, "banphrases": self.banphrase_manager} self.execute_every(60, self.commit_all) self.execute_every(1, self.do_tick) # promote the admin to level 2000 self.admin = self.config["main"].get("admin", None) if self.admin is None: log.warning("No admin user specified. See the [main] section in the example config for its usage.") else: with DBManager.create_session_scope() as db_session: admin_user = User.find_or_create_from_login(db_session, self.twitch_helix_api, self.admin) if admin_user is None: log.warning( "The login name you entered for the admin user does not exist on twitch. " "No admin user has been created." ) else: admin_user.level = 2000 # silent mode self.silent = ( "flags" in config and "silent" in config["flags"] and config["flags"]["silent"] == "1" ) or args.silent if self.silent: log.info("Silent mode enabled") # dev mode self.dev = "flags" in config and "dev" in config["flags"] and config["flags"]["dev"] == "1" if self.dev: self.version_long = utils.extend_version_if_possible(VERSION) else: self.version_long = VERSION self.irc = IRCManager(self) relay_host = self.config["main"].get("relay_host", None) relay_password = self.config["main"].get("relay_password", None) if relay_host is not None or relay_password is not None: log.warning( "DEPRECATED - Relaybroker support is no longer implemented. relay_host and relay_password are ignored" ) self.data = { "broadcaster": self.streamer, "version": self.version_long, "version_brief": VERSION, "bot_name": self.nickname, "bot_domain": self.bot_domain, "streamer_display": self.streamer_display, } self.data_cb = { "status_length": self.c_status_length, "stream_status": self.c_stream_status, "bot_uptime": self.c_uptime, "current_time": self.c_current_time, "molly_age_in_years": self.c_molly_age_in_years, } self.user_agent = f"pajbot1/{VERSION} ({self.nickname})" @property def password(self): return f"oauth:{self.bot_token_manager.token.access_token}" def start(self): """Start the IRC client.""" self.reactor.process_forever() def get_kvi_value(self, key, extra={}): return self.kvi[key].get() def get_last_tweet(self, key, extra={}): return self.twitter_manager.get_last_tweet(key) def get_emote_epm(self, key, extra={}): epm = self.epm_manager.get_emote_epm(key) # maybe we simply haven't seen this emote yet (during the bot runtime) but it's a valid emote? if epm is None and self.emote_manager.match_word_to_emote(key) is not None: epm = 0 if epm is None: return None # formats the number with grouping (e.g. 112,556) and zero decimal places return f"{epm:,.0f}" def get_emote_epm_record(self, key, extra={}): val = self.epm_manager.get_emote_epm_record(key) if val is None: return None # formats the number with grouping (e.g. 112,556) and zero decimal places return f"{val:,.0f}" def get_emote_count(self, key, extra={}): val = self.ecount_manager.get_emote_count(key) if val is None: return None # formats the number with grouping (e.g. 112,556) and zero decimal places return f"{val:,.0f}" @staticmethod def get_source_value(key, extra={}): try: return getattr(extra["source"], key) except: log.exception("Caught exception in get_source_value") return None def get_user_value(self, key, extra={}): try: with DBManager.create_session_scope() as db_session: user = User.find_by_user_input(db_session, extra["argument"]) if user is not None: return getattr(user, key) except: log.exception("Caught exception in get_source_value") return None @staticmethod def get_command_value(key, extra={}): try: return getattr(extra["command"].data, key) except: log.exception("Caught exception in get_source_value") return None def get_usersource_value(self, key, extra={}): try: with DBManager.create_session_scope() as db_session: user = User.find_by_user_input(db_session, extra["argument"]) if user is not None: return getattr(user, key) return getattr(extra["source"], key) except: log.exception("Caught exception in get_source_value") return None def get_time_value(self, key, extra={}): try: tz = timezone(key) return datetime.datetime.now(tz).strftime("%H:%M") except: log.exception("Unhandled exception in get_time_value") return None def get_current_song_value(self, key, extra={}): if self.stream_manager.online: current_song = PleblistManager.get_current_song(self.stream_manager.current_stream.id) inner_keys = key.split(".") val = current_song for inner_key in inner_keys: val = getattr(val, inner_key, None) if val is None: return None if val is not None: return val return None def get_strictargs_value(self, key, extra={}): ret = self.get_args_value(key, extra) if not ret: return None return ret @staticmethod def get_args_value(key, extra={}): r = None try: msg_parts = extra["message"].split(" ") except (KeyError, AttributeError): msg_parts = [""] try: if "-" in key: range_str = key.split("-") if len(range_str) == 2: r = (int(range_str[0]), int(range_str[1])) if r is None: r = (int(key), len(msg_parts)) except (TypeError, ValueError): r = (0, len(msg_parts)) try: return " ".join(msg_parts[r[0] : r[1]]) except AttributeError: return "" except: log.exception("UNHANDLED ERROR IN get_args_value") return "" def get_value(self, key, extra={}): if key in extra: return extra[key] if key in self.data: return self.data[key] if key in self.data_cb: return self.data_cb[key]() log.warning("Unknown key passed to get_value: %s", key) return None def privmsg_arr(self, arr, target=None): for msg in arr: self.privmsg(msg, target) def privmsg_arr_chunked(self, arr, per_chunk=35, chunk_delay=30, target=None): i = 0 while arr: if i == 0: self.privmsg_arr(arr[:per_chunk], target) else: self.execute_delayed(chunk_delay * i, self.privmsg_arr, arr[:per_chunk], target) del arr[:per_chunk] i = i + 1 def privmsg_from_file(self, url, per_chunk=35, chunk_delay=30, target=None): try: r = requests.get(url, headers={"User-Agent": self.user_agent}) r.raise_for_status() content_type = r.headers["Content-Type"] if content_type is not None and cgi.parse_header(content_type)[0] != "text/plain": log.error("privmsg_from_file should be fed with a text/plain URL. Refusing to send.") return lines = r.text.splitlines() self.privmsg_arr_chunked(lines, per_chunk=per_chunk, chunk_delay=chunk_delay, target=target) except: log.exception("error in privmsg_from_file") # event is an event to clone and change the text from. # Usage: !eval bot.eval_from_file(event, 'https://pastebin.com/raw/LhCt8FLh') def eval_from_file(self, event, url): try: r = requests.get(url, headers={"User-Agent": self.user_agent}) r.raise_for_status() content_type = r.headers["Content-Type"] if content_type is not None and cgi.parse_header(content_type)[0] != "text/plain": log.error("eval_from_file should be fed with a text/plain URL. Refusing to send.") return lines = r.text.splitlines() import copy for msg in lines: cloned_event = copy.deepcopy(event) cloned_event.arguments = [msg] # omit the source connection as None (since its not used) self.on_pubmsg(None, cloned_event) self.whisper_login(event.source.user.lower(), f"Successfully evaluated {len(lines)} lines") except: log.exception("BabyRage") self.whisper_login(event.source.user.lower(), "Exception BabyRage") def privmsg(self, message, channel=None): if channel is None: channel = self.channel self.irc.privmsg(channel, message, is_whisper=False) def c_uptime(self): return utils.time_ago(self.start_time) @staticmethod def c_current_time(): return utils.now() @staticmethod def c_molly_age_in_years(): molly_birth = datetime.datetime(2018, 10, 29, tzinfo=datetime.timezone.utc) now = utils.now() diff = now - molly_birth return diff.total_seconds() / 3600 / 24 / 365 @property def is_online(self): return self.stream_manager.online def c_stream_status(self): return "online" if self.stream_manager.online else "offline" def c_status_length(self): if self.stream_manager.online: return utils.time_ago(self.stream_manager.current_stream.stream_start) if self.stream_manager.last_stream is not None: return utils.time_ago(self.stream_manager.last_stream.stream_end) return "No recorded stream FeelsBadMan " def execute_now(self, function, *args, **kwargs): self.execute_delayed(0, function, *args, **kwargs) def execute_at(self, at, function, *args, **kwargs): self.reactor.scheduler.execute_at(at, lambda: function(*args, **kwargs)) def execute_delayed(self, delay, function, *args, **kwargs): self.reactor.scheduler.execute_after(delay, lambda: function(*args, **kwargs)) def execute_every(self, period, function, *args, **kwargs): self.reactor.scheduler.execute_every(period, lambda: function(*args, **kwargs)) def _ban(self, login, reason=None): message = f"/ban {login}" if reason is not None: message += f" {reason}" self.privmsg(message) def ban(self, user, reason=None): self.timeout(user, 30, reason, once=True) self.execute_delayed(1, self._ban, user.login, reason) def unban(self, user): self.privmsg(f"/unban {user.login}") def untimeout(self, user): self.privmsg(f"/untimeout {user.login}") def _timeout(self, login, duration, reason=None): message = f"/timeout {login} {duration}" if reason is not None: message += f" {reason}" self.privmsg(message) def timeout(self, user, duration, reason=None, once=False): self._timeout(user.login, duration, reason) if not once: self.execute_delayed(1, self._timeout, user.login, duration, reason) def timeout_login(self, login, duration, reason=None, once=False): self._timeout(login, duration, reason) if not once: self.execute_delayed(1, self._timeout, login, duration, reason) def timeout_warn(self, user, duration, reason=None): duration, punishment = user.timeout(duration, warning_module=self.module_manager["warning"]) self.timeout(user, duration, reason) return (duration, punishment) def delete_message(self, msg_id): self.privmsg(f"/delete {msg_id}") def whisper(self, user, message): if self.whisper_output_mode == WhisperOutputMode.NORMAL: self.irc.whisper(user.login, message) if self.whisper_output_mode == WhisperOutputMode.CHAT: self.privmsg(f"{user}, {message}") elif self.whisper_output_mode == WhisperOutputMode.DISABLED: log.debug(f'Whisper "{message}" to user "{user}" was not sent (due to config setting)') def whisper_login(self, login, message): if self.whisper_output_mode == WhisperOutputMode.NORMAL: self.irc.whisper(login, message) if self.whisper_output_mode == WhisperOutputMode.CHAT: self.privmsg(f"{login}, {message}") elif self.whisper_output_mode == WhisperOutputMode.DISABLED: log.debug(f'Whisper "{message}" to user "{login}" was not sent (due to config setting)') def send_message_to_user(self, user, message, event, method="say"): if method == "say": self.say(user.name + ", " + lowercase_first_letter(message)) elif method == "whisper": self.whisper(user, message) elif method == "me": self.me(message) elif method == "reply": if event.type in ["action", "pubmsg"]: self.say(message) elif event.type == "whisper": self.whisper(user, message) else: log.warning("Unknown send_message method: %s", method) def safe_privmsg(self, message, channel=None): # Check for banphrases res = self.banphrase_manager.check_message(message, None) if res is not False: self.privmsg(f"filtered message ({res.id})", channel) return self.privmsg(message, channel) def say(self, message, channel=None): if message is None: log.warning("message=None passed to Bot::say()") return if self.silent: return message = utils.clean_up_message(message) self.privmsg(message[:510], channel) def is_bad_message(self, message): return self.banphrase_manager.check_message(message, None) is not False def safe_me(self, message, channel=None): if not self.is_bad_message(message): self.me(message, channel) def me(self, message, channel=None): self.say("/me " + message[:500], channel=channel) def connect(self): self.irc.start() def parse_message(self, message, source, event, tags={}, whisper=False): msg_lower = message.lower() emote_tag = tags["emotes"] msg_id = tags.get("id", None) # None on whispers! badges_string = tags.get("badges", "") badges = dict((badge.split("/") for badge in badges_string.split(",") if badge != "")) if not whisper and event.target == self.channel: # Moderator or broadcaster, both count source.moderator = tags["mod"] == "1" or source.id == self.streamer_user_id # Having the founder badge means that the subscriber tag is set to 0. Therefore it's more stable to just check badges source.subscriber = "founder" in badges or "subscriber" in badges # once they are a founder they are always be a founder, regardless if they are a sub or not. if not source.founder: source.founder = "founder" in badges source.vip = "vip" in badges if not whisper and source.banned: self.ban( source, reason=f"User is on the {self.nickname} banlist. Contact a moderator level 1000 or higher for unban.", ) return False # Parse emotes in the message emote_instances, emote_counts = self.emote_manager.parse_all_emotes(message, emote_tag) now = utils.now() source.last_seen = now source.last_active = now if not whisper: # increment epm and ecount self.epm_manager.handle_emotes(emote_counts) self.ecount_manager.handle_emotes(emote_counts) urls = self.find_unique_urls(message) res = HandlerManager.trigger( "on_message", source=source, message=message, emote_instances=emote_instances, emote_counts=emote_counts, whisper=whisper, urls=urls, msg_id=msg_id, event=event, ) if res is False: return False if source.ignored: return False if msg_lower[:1] == "!": msg_lower_parts = msg_lower.split(" ") trigger = msg_lower_parts[0][1:] msg_raw_parts = message.split(" ") remaining_message = " ".join(msg_raw_parts[1:]) if len(msg_raw_parts) > 1 else None if trigger in self.commands: command = self.commands[trigger] extra_args = { "emote_instances": emote_instances, "emote_counts": emote_counts, "trigger": trigger, "msg_id": msg_id, } command.run(self, source, remaining_message, event=event, args=extra_args, whisper=whisper) def on_whisper(self, chatconn, event): tags = {tag["key"]: tag["value"] if tag["value"] is not None else "" for tag in event.tags} id = tags["user-id"] login = event.source.user name = tags["display-name"] with DBManager.create_session_scope(expire_on_commit=False) as db_session: source = User.from_basics(db_session, UserBasics(id, login, name)) self.parse_message(event.arguments[0], source, event, tags, whisper=True) def on_usernotice(self, chatconn, event): tags = {tag["key"]: tag["value"] if tag["value"] is not None else "" for tag in event.tags} id = tags["user-id"] login = tags["login"] name = tags["display-name"] with DBManager.create_session_scope(expire_on_commit=False) as db_session: source = User.from_basics(db_session, UserBasics(id, login, name)) if event.arguments and len(event.arguments) > 0: msg = event.arguments[0] else: msg = None # e.g. user didn't type an extra message to share with the streamer HandlerManager.trigger("on_usernotice", source=source, message=msg, tags=tags) if msg is not None: self.parse_message(msg, source, event, tags) def on_action(self, chatconn, event): self.on_pubmsg(chatconn, event) def on_pubmsg(self, chatconn, event): tags = {tag["key"]: tag["value"] if tag["value"] is not None else "" for tag in event.tags} id = tags["user-id"] login = event.source.user name = tags["display-name"] if event.source.user == self.nickname: return False if self.streamer == "forsen": if "zonothene" in login: self._ban(login) return True raw_m = event.arguments[0].lower() if raw_m.startswith("!lastseen forsen"): if len(raw_m) > len("!lastseen forsen2"): if raw_m[16] == " ": return True else: return True if raw_m.startswith("!lastseen @forsen"): if len(raw_m) > len("!lastseen @forsen2"): if raw_m[17] == " ": return True else: return True if self.streamer == "nymn": if "hades_k" in login: self.timeout_login(login, 3600, reason="Bad username") return True if "hades_b" in login: self.timeout_login(login, 3600, reason="Bad username") return True with DBManager.create_session_scope(expire_on_commit=False) as db_session: source = User.from_basics(db_session, UserBasics(id, login, name)) res = HandlerManager.trigger("on_pubmsg", source=source, message=event.arguments[0]) if res is False: return False self.parse_message(event.arguments[0], source, event, tags=tags) def on_pubnotice(self, chatconn, event): tags = {tag["key"]: tag["value"] if tag["value"] is not None else "" for tag in event.tags} HandlerManager.trigger( "on_pubnotice", stop_on_false=False, channel=event.target[1:], msg_id=tags["msg-id"], message=event.arguments[0], ) def on_clearchat(self, chatconn, event): tags = {tag["key"]: tag["value"] if tag["value"] is not None else "" for tag in event.tags} # Ignore "Chat has been cleared by a moderator" messages if "target-user-id" not in tags: return target_user_id = tags["target-user-id"] with DBManager.create_session_scope() as db_session: user = User.find_by_id(db_session, target_user_id) if user is None: # User is not otherwise known, we won't store their timeout (they need to type first) # We could theoretically also do an API call here to figure out everything about that user, # but that could easily overwhelm the bot when lots of unknown users are banned quickly (e.g. bots). return if "ban-duration" in tags: # timeout ban_duration = int(tags["ban-duration"]) user.timeout_end = utils.now() + datetime.timedelta(seconds=ban_duration) else: # permaban # this sets timeout_end to None user.timed_out = False def on_welcome(self, _conn, _event): """Gets triggered on IRC welcome, i.e. when the login is successful.""" if self.welcome_messages_sent: return for p in self.phrases["welcome"]: self.privmsg(p.format(nickname=self.nickname, version=self.version_long)) self.welcome_messages_sent = True def commit_all(self): for key, manager in self.commitable.items(): manager.commit() HandlerManager.trigger("on_commit", stop_on_false=False) @staticmethod def do_tick(): HandlerManager.trigger("on_tick") def quit(self, message, event, **options): quit_chub = self.config["main"].get("control_hub", None) quit_delay = 0 if quit_chub is not None and event.target == f"#{quit_chub}": quit_delay_random = 300 try: if message is not None and int(message.split()[0]) >= 1: quit_delay_random = int(message.split()[0]) except (IndexError, ValueError, TypeError): pass quit_delay = random.randint(0, quit_delay_random) log.info("%s is restarting in %d seconds.", self.nickname, quit_delay) self.execute_delayed(quit_delay, self.quit_bot) def quit_bot(self, **options): self.commit_all() HandlerManager.trigger("on_quit") phrase_data = {"nickname": self.nickname, "version": self.version_long} try: ScheduleManager.base_scheduler.print_jobs() ScheduleManager.base_scheduler.shutdown(wait=False) except: log.exception("Error while shutting down the apscheduler") try: for p in self.phrases["quit"]: self.privmsg(p.format(**phrase_data)) except Exception: log.exception("Exception caught while trying to say quit phrase") self.twitter_manager.quit() self.socket_manager.quit() sys.exit(0) def apply_filter(self, resp, f): available_filters = { "strftime": _filter_strftime, "lower": lambda var, args: var.lower(), "upper": lambda var, args: var.upper(), "title": lambda var, args: var.title(), "capitalize": lambda var, args: var.capitalize(), "swapcase": lambda var, args: var.swapcase(), "time_since_minutes": lambda var, args: "no time" if var == 0 else utils.time_since(var * 60, 0, time_format="long"), "time_since": lambda var, args: "no time" if var == 0 else utils.time_since(var, 0, time_format="long"), "time_since_dt": _filter_time_since_dt, "urlencode": _filter_urlencode, "join": _filter_join, "number_format": _filter_number_format, "add": _filter_add, "or_else": _filter_or_else, "or_broadcaster": self._filter_or_broadcaster, "or_streamer": self._filter_or_broadcaster, "slice": _filter_slice, } if f.name in available_filters: return available_filters[f.name](resp, f.arguments) return resp def _filter_or_broadcaster(self, var, args): return _filter_or_else(var, self.streamer) def find_unique_urls(self, message): from pajbot.modules.linkchecker import find_unique_urls return find_unique_urls(URL_REGEX, message)
def init(args): import subprocess import sys from flask import request from flask import session import pajbot.utils import pajbot.web.common import pajbot.web.routes from pajbot.managers.db import DBManager from pajbot.managers.redis import RedisManager from pajbot.models.module import ModuleManager from pajbot.models.sock import SocketClientManager from pajbot.streamhelper import StreamHelper from pajbot.utils import load_config from pajbot.web.models import errors from pajbot.web.utils import download_logo from pajbot.web.utils import download_sub_badge config = load_config(args.config) api_client_credentials = ClientCredentials( config["twitchapi"]["client_id"], config["twitchapi"]["client_secret"], config["twitchapi"].get( "redirect_uri", f"https://{config['web']['domain']}/login/authorized"), ) redis_options = {} if "redis" in config: redis_options = dict(config["redis"]) RedisManager.init(redis_options) twitch_id_api = TwitchIDAPI(api_client_credentials) app_token_manager = AppAccessTokenManager(twitch_id_api, RedisManager.get()) twitch_helix_api = TwitchHelixAPI(RedisManager.get(), app_token_manager) twitch_badges_api = TwitchBadgesAPI(RedisManager.get()) app.api_client_credentials = api_client_credentials app.twitch_id_api = twitch_id_api app.twitch_helix_api = twitch_helix_api if "web" not in config: log.error("Missing [web] section in config.ini") sys.exit(1) app.streamer = cfg.load_streamer(config, twitch_helix_api) app.streamer_display = app.streamer.name if "streamer_name" in config["web"]: app.streamer_display = config["web"]["streamer_name"] app.bot_user = cfg.load_bot(config, twitch_helix_api) StreamHelper.init_streamer(app.streamer.login, app.streamer.id, app.streamer.name) try: download_logo(twitch_helix_api, app.streamer) except: log.exception("Error downloading the streamers profile picture") subscriber_badge_version = config["web"].get("subscriber_badge_version", "0") # Specifying a value of -1 in the config will disable sub badge downloading. Useful if you want to keep a custom version of a sub badge for a streamer if subscriber_badge_version != "-1": try: download_sub_badge(twitch_badges_api, app.streamer, subscriber_badge_version) except: log.exception("Error downloading the streamers subscriber badge") SocketClientManager.init(app.streamer.login) app.bot_modules = config["web"].get("modules", "").split() app.bot_commands_list = [] app.bot_config = config # https://flask.palletsprojects.com/en/1.1.x/quickstart/#sessions # https://flask.palletsprojects.com/en/1.1.x/api/#sessions # https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.secret_key app.secret_key = _load_secret_key(app.bot_user.id, app.streamer.id) app.bot_dev = "flags" in config and "dev" in config["flags"] and config[ "flags"]["dev"] == "1" DBManager.init(config["main"]["db"]) app.module_manager = ModuleManager(None).load() pajbot.web.routes.admin.init(app) pajbot.web.routes.api.init(app) pajbot.web.routes.base.init(app) # Make a CSRF exemption for the /api/v1/banphrases/test endpoint csrf.exempt("pajbot.web.routes.api.banphrases.apibanphrasetest") pajbot.web.common.filters.init(app) pajbot.web.common.assets.init(app) pajbot.web.common.menu.init(app) app.register_blueprint(pajbot.web.routes.clr.page) errors.init(app, config) pajbot.web.routes.clr.config = config version = VERSION last_commit = None if app.bot_dev: version = extend_version_if_possible(VERSION) try: last_commit = subprocess.check_output( ["git", "log", "-1", "--format=%cd"]).decode("utf8").strip() except: log.exception( "Failed to get last_commit, will not show last commit") default_variables = { "version": version, "last_commit": last_commit, "bot": { "name": app.bot_user.login }, "site": { "domain": config["web"]["domain"], "deck_tab_images": cfg.get_boolean(config["web"], "deck_tab_images", False), "websocket": { "host": config["websocket"].get( "host", f"wss://{config['web']['domain']}/clrsocket") }, }, "streamer": { "name": app.streamer_display, "full_name": app.streamer.login, "id": app.streamer.id }, "modules": app.bot_modules, "request": request, "session": session, "google_analytics": config["web"].get("google_analytics", None), } @app.context_processor def current_time(): current_time = {} current_time["current_time"] = pajbot.utils.now() return current_time @app.context_processor def inject_default_variables(): return default_variables
def __init__(self, config, args): self.config = config self.args = args self.last_ping = utils.now() self.last_pong = utils.now() DBManager.init(self.config["main"]["db"]) # redis redis_options = {} if "redis" in config: redis_options = dict(config.items("redis")) RedisManager.init(**redis_options) wait_for_redis_data_loaded(RedisManager.get()) # Pepega SE points sync pajbot.models.user.Config.se_sync_token = config["main"].get( "se_sync_token", None) pajbot.models.user.Config.se_channel = config["main"].get( "se_channel", None) self.nickname = config["main"].get("nickname", "pajbot") self.timezone = config["main"].get("timezone", "UTC") if config["main"].getboolean("verified", False): TMI.promote_to_verified() # phrases self.phrases = { "welcome": ["{nickname} {version} running!"], "quit": ["{nickname} {version} shutting down..."] } if "phrases" in config: phrases = config["phrases"] if "welcome" in phrases: self.phrases["welcome"] = phrases["welcome"].splitlines() if "quit" in phrases: self.phrases["quit"] = phrases["quit"].splitlines() TimeManager.init_timezone(self.timezone) # streamer if "streamer" in config["main"]: self.streamer = config["main"]["streamer"] self.channel = "#" + self.streamer elif "target" in config["main"]: self.channel = config["main"]["target"] self.streamer = self.channel[1:] StreamHelper.init_streamer(self.streamer) log.debug("Loaded config") # do this earlier since schema upgrade can depend on the helix api self.api_client_credentials = ClientCredentials( self.config["twitchapi"]["client_id"], self.config["twitchapi"]["client_secret"], self.config["twitchapi"]["redirect_uri"], ) self.twitch_id_api = TwitchIDAPI(self.api_client_credentials) self.app_token_manager = AppAccessTokenManager(self.twitch_id_api, RedisManager.get()) self.twitch_helix_api = TwitchHelixAPI(RedisManager.get(), self.app_token_manager) self.twitch_v5_api = TwitchKrakenV5API(self.api_client_credentials, RedisManager.get()) self.twitch_legacy_api = TwitchLegacyAPI(self.api_client_credentials, RedisManager.get()) self.twitch_tmi_api = TwitchTMIAPI() self.bot_user_id = self.twitch_helix_api.get_user_id(self.nickname) if self.bot_user_id is None: raise ValueError( "The bot login name you entered under [main] does not exist on twitch." ) self.streamer_user_id = self.twitch_helix_api.get_user_id( self.streamer) if self.streamer_user_id is None: raise ValueError( "The streamer login name you entered under [main] does not exist on twitch." ) # SQL migrations sql_conn = DBManager.engine.connect().connection sql_migratable = DatabaseMigratable(sql_conn) sql_migration = Migration(sql_migratable, pajbot.migration_revisions.db, self) sql_migration.run() # Redis migrations redis_migratable = RedisMigratable(redis_options=redis_options, namespace=self.streamer) redis_migration = Migration(redis_migratable, pajbot.migration_revisions.redis, self) redis_migration.run() # Actions in this queue are run in a separate thread. # This means actions should NOT access any database-related stuff. self.action_queue = ActionQueue() self.action_queue.start() self.reactor = irc.client.Reactor(self.on_connect) self.start_time = utils.now() ActionParser.bot = self HandlerManager.init_handlers() self.socket_manager = SocketManager(self.streamer, self.execute_now) self.stream_manager = StreamManager(self) StreamHelper.init_bot(self, self.stream_manager) ScheduleManager.init() self.users = UserManager() self.decks = DeckManager() self.banphrase_manager = BanphraseManager(self).load() self.timer_manager = TimerManager(self).load() self.kvi = KVIManager() # bot access token if "password" in self.config["main"]: log.warning( "DEPRECATED - Using bot password/oauth token from file. " "You should authenticate in web gui using route /bot_login " "and remove password from config file") access_token = self.config["main"]["password"] if access_token.startswith("oauth:"): access_token = access_token[6:] self.bot_token_manager = UserAccessTokenManager( api=None, redis=None, username=self.nickname, user_id=self.bot_user_id, token=UserAccessToken.from_implicit_auth_flow_token( access_token), ) else: self.bot_token_manager = UserAccessTokenManager( api=self.twitch_id_api, redis=RedisManager.get(), username=self.nickname, user_id=self.bot_user_id) self.emote_manager = EmoteManager(self.twitch_v5_api, self.twitch_legacy_api, self.action_queue) self.epm_manager = EpmManager() self.ecount_manager = EcountManager() self.twitter_manager = TwitterManager(self) self.module_manager = ModuleManager(self.socket_manager, bot=self).load() self.commands = CommandManager(socket_manager=self.socket_manager, module_manager=self.module_manager, bot=self).load() self.websocket_manager = WebSocketManager(self) HandlerManager.trigger("on_managers_loaded") # Commitable managers self.commitable = { "commands": self.commands, "banphrases": self.banphrase_manager } self.execute_every(10 * 60, self.commit_all) self.execute_every(1, self.do_tick) # promote the admin to level 2000 admin = None try: admin = self.config["main"]["admin"] except KeyError: log.warning( "No admin user specified. See the [main] section in the example config for its usage." ) if admin is not None: with self.users.get_user_context(admin) as user: user.level = 2000 # silent mode self.silent = ("flags" in config and "silent" in config["flags"] and config["flags"]["silent"] == "1") or args.silent if self.silent: log.info("Silent mode enabled") # dev mode self.dev = "flags" in config and "dev" in config["flags"] and config[ "flags"]["dev"] == "1" if self.dev: self.version_long = extend_version_if_possible(VERSION) else: self.version_long = VERSION self.irc = IRCManager(self) relay_host = self.config["main"].get("relay_host", None) relay_password = self.config["main"].get("relay_password", None) if relay_host is not None or relay_password is not None: log.warning( "DEPRECATED - Relaybroker support is no longer implemented. relay_host and relay_password are ignored" ) self.reactor.add_global_handler("all_events", self.irc._dispatcher, -10) self.data = { "broadcaster": self.streamer, "version": self.version_long, "version_brief": VERSION, "bot_name": self.nickname, } self.data_cb = { "status_length": self.c_status_length, "stream_status": self.c_stream_status, "bot_uptime": self.c_uptime, "current_time": self.c_current_time, "molly_age_in_years": self.c_molly_age_in_years, }
class Bot: """ Main class for the twitch bot """ def __init__(self, config, args): self.config = config self.args = args self.last_ping = utils.now() self.last_pong = utils.now() DBManager.init(self.config["main"]["db"]) # redis redis_options = {} if "redis" in config: redis_options = dict(config.items("redis")) RedisManager.init(**redis_options) wait_for_redis_data_loaded(RedisManager.get()) # Pepega SE points sync pajbot.models.user.Config.se_sync_token = config["main"].get( "se_sync_token", None) pajbot.models.user.Config.se_channel = config["main"].get( "se_channel", None) self.nickname = config["main"].get("nickname", "pajbot") self.timezone = config["main"].get("timezone", "UTC") if config["main"].getboolean("verified", False): TMI.promote_to_verified() # phrases self.phrases = { "welcome": ["{nickname} {version} running!"], "quit": ["{nickname} {version} shutting down..."] } if "phrases" in config: phrases = config["phrases"] if "welcome" in phrases: self.phrases["welcome"] = phrases["welcome"].splitlines() if "quit" in phrases: self.phrases["quit"] = phrases["quit"].splitlines() TimeManager.init_timezone(self.timezone) # streamer if "streamer" in config["main"]: self.streamer = config["main"]["streamer"] self.channel = "#" + self.streamer elif "target" in config["main"]: self.channel = config["main"]["target"] self.streamer = self.channel[1:] StreamHelper.init_streamer(self.streamer) log.debug("Loaded config") # do this earlier since schema upgrade can depend on the helix api self.api_client_credentials = ClientCredentials( self.config["twitchapi"]["client_id"], self.config["twitchapi"]["client_secret"], self.config["twitchapi"]["redirect_uri"], ) self.twitch_id_api = TwitchIDAPI(self.api_client_credentials) self.app_token_manager = AppAccessTokenManager(self.twitch_id_api, RedisManager.get()) self.twitch_helix_api = TwitchHelixAPI(RedisManager.get(), self.app_token_manager) self.twitch_v5_api = TwitchKrakenV5API(self.api_client_credentials, RedisManager.get()) self.twitch_legacy_api = TwitchLegacyAPI(self.api_client_credentials, RedisManager.get()) self.twitch_tmi_api = TwitchTMIAPI() self.bot_user_id = self.twitch_helix_api.get_user_id(self.nickname) if self.bot_user_id is None: raise ValueError( "The bot login name you entered under [main] does not exist on twitch." ) self.streamer_user_id = self.twitch_helix_api.get_user_id( self.streamer) if self.streamer_user_id is None: raise ValueError( "The streamer login name you entered under [main] does not exist on twitch." ) # SQL migrations sql_conn = DBManager.engine.connect().connection sql_migratable = DatabaseMigratable(sql_conn) sql_migration = Migration(sql_migratable, pajbot.migration_revisions.db, self) sql_migration.run() # Redis migrations redis_migratable = RedisMigratable(redis_options=redis_options, namespace=self.streamer) redis_migration = Migration(redis_migratable, pajbot.migration_revisions.redis, self) redis_migration.run() # Actions in this queue are run in a separate thread. # This means actions should NOT access any database-related stuff. self.action_queue = ActionQueue() self.action_queue.start() self.reactor = irc.client.Reactor(self.on_connect) self.start_time = utils.now() ActionParser.bot = self HandlerManager.init_handlers() self.socket_manager = SocketManager(self.streamer, self.execute_now) self.stream_manager = StreamManager(self) StreamHelper.init_bot(self, self.stream_manager) ScheduleManager.init() self.users = UserManager() self.decks = DeckManager() self.banphrase_manager = BanphraseManager(self).load() self.timer_manager = TimerManager(self).load() self.kvi = KVIManager() # bot access token if "password" in self.config["main"]: log.warning( "DEPRECATED - Using bot password/oauth token from file. " "You should authenticate in web gui using route /bot_login " "and remove password from config file") access_token = self.config["main"]["password"] if access_token.startswith("oauth:"): access_token = access_token[6:] self.bot_token_manager = UserAccessTokenManager( api=None, redis=None, username=self.nickname, user_id=self.bot_user_id, token=UserAccessToken.from_implicit_auth_flow_token( access_token), ) else: self.bot_token_manager = UserAccessTokenManager( api=self.twitch_id_api, redis=RedisManager.get(), username=self.nickname, user_id=self.bot_user_id) self.emote_manager = EmoteManager(self.twitch_v5_api, self.twitch_legacy_api, self.action_queue) self.epm_manager = EpmManager() self.ecount_manager = EcountManager() self.twitter_manager = TwitterManager(self) self.module_manager = ModuleManager(self.socket_manager, bot=self).load() self.commands = CommandManager(socket_manager=self.socket_manager, module_manager=self.module_manager, bot=self).load() self.websocket_manager = WebSocketManager(self) HandlerManager.trigger("on_managers_loaded") # Commitable managers self.commitable = { "commands": self.commands, "banphrases": self.banphrase_manager } self.execute_every(10 * 60, self.commit_all) self.execute_every(1, self.do_tick) # promote the admin to level 2000 admin = None try: admin = self.config["main"]["admin"] except KeyError: log.warning( "No admin user specified. See the [main] section in the example config for its usage." ) if admin is not None: with self.users.get_user_context(admin) as user: user.level = 2000 # silent mode self.silent = ("flags" in config and "silent" in config["flags"] and config["flags"]["silent"] == "1") or args.silent if self.silent: log.info("Silent mode enabled") # dev mode self.dev = "flags" in config and "dev" in config["flags"] and config[ "flags"]["dev"] == "1" if self.dev: self.version_long = extend_version_if_possible(VERSION) else: self.version_long = VERSION self.irc = IRCManager(self) relay_host = self.config["main"].get("relay_host", None) relay_password = self.config["main"].get("relay_password", None) if relay_host is not None or relay_password is not None: log.warning( "DEPRECATED - Relaybroker support is no longer implemented. relay_host and relay_password are ignored" ) self.reactor.add_global_handler("all_events", self.irc._dispatcher, -10) self.data = { "broadcaster": self.streamer, "version": self.version_long, "version_brief": VERSION, "bot_name": self.nickname, } self.data_cb = { "status_length": self.c_status_length, "stream_status": self.c_stream_status, "bot_uptime": self.c_uptime, "current_time": self.c_current_time, "molly_age_in_years": self.c_molly_age_in_years, } @property def password(self): return "oauth:{}".format(self.bot_token_manager.token.access_token) def on_connect(self, sock): return self.irc.on_connect(sock) def start(self): """Start the IRC client.""" self.reactor.process_forever() def get_kvi_value(self, key, extra={}): return self.kvi[key].get() def get_last_tweet(self, key, extra={}): return self.twitter_manager.get_last_tweet(key) def get_emote_epm(self, key, extra={}): epm = self.epm_manager.get_emote_epm(key) # maybe we simply haven't seen this emote yet (during the bot runtime) but it's a valid emote? if epm is None and self.emote_manager.match_word_to_emote( key) is not None: epm = 0 if epm is None: return None # formats the number with grouping (e.g. 112,556) and zero decimal places return "{0:,.0f}".format(epm) def get_emote_epm_record(self, key, extra={}): val = self.epm_manager.get_emote_epm_record(key) if val is None: return None # formats the number with grouping (e.g. 112,556) and zero decimal places return "{0:,.0f}".format(val) def get_emote_count(self, key, extra={}): val = self.ecount_manager.get_emote_count(key) if val is None: return None # formats the number with grouping (e.g. 112,556) and zero decimal places return "{0:,.0f}".format(val) @staticmethod def get_source_value(key, extra={}): try: return getattr(extra["source"], key) except: log.exception("Caught exception in get_source_value") return None def get_user_value(self, key, extra={}): try: user = self.users.find(extra["argument"]) if user: return getattr(user, key) except: log.exception("Caught exception in get_source_value") return None @staticmethod def get_command_value(key, extra={}): try: return getattr(extra["command"].data, key) except: log.exception("Caught exception in get_source_value") return None def get_usersource_value(self, key, extra={}): try: user = self.users.find(extra["argument"]) if user: return getattr(user, key) return getattr(extra["source"], key) except: log.exception("Caught exception in get_source_value") return None def get_time_value(self, key, extra={}): try: tz = timezone(key) return datetime.datetime.now(tz).strftime("%H:%M") except: log.exception("Unhandled exception in get_time_value") return None def get_current_song_value(self, key, extra={}): if self.stream_manager.online: current_song = PleblistManager.get_current_song( self.stream_manager.current_stream.id) inner_keys = key.split(".") val = current_song for inner_key in inner_keys: val = getattr(val, inner_key, None) if val is None: return None if val is not None: return val return None def get_strictargs_value(self, key, extra={}): ret = self.get_args_value(key, extra) if not ret: return None return ret @staticmethod def get_args_value(key, extra={}): r = None try: msg_parts = extra["message"].split(" ") except (KeyError, AttributeError): msg_parts = [""] try: if "-" in key: range_str = key.split("-") if len(range_str) == 2: r = (int(range_str[0]), int(range_str[1])) if r is None: r = (int(key), len(msg_parts)) except (TypeError, ValueError): r = (0, len(msg_parts)) try: return " ".join(msg_parts[r[0]:r[1]]) except AttributeError: return "" except: log.exception("UNHANDLED ERROR IN get_args_value") return "" def get_notify_value(self, key, extra={}): payload = { "message": extra["message"] or "", "trigger": extra["trigger"], "user": extra["source"].username_raw } self.websocket_manager.emit("notify", payload) return "" def get_value(self, key, extra={}): if key in extra: return extra[key] if key in self.data: return self.data[key] if key in self.data_cb: return self.data_cb[key]() log.warning("Unknown key passed to get_value: %s", key) return None def privmsg_arr(self, arr, target=None): for msg in arr: self.privmsg(msg, target) def privmsg_from_file(self, url, per_chunk=35, chunk_delay=30, target=None): try: r = requests.get(url) r.raise_for_status() content_type = r.headers["Content-Type"] if content_type is not None and cgi.parse_header( content_type)[0] != "text/plain": log.error( "privmsg_from_file should be fed with a text/plain URL. Refusing to send." ) return lines = r.text.splitlines() i = 0 while lines: if i == 0: self.privmsg_arr(lines[:per_chunk], target) else: self.execute_delayed(chunk_delay * i, self.privmsg_arr, (lines[:per_chunk], target)) del lines[:per_chunk] i = i + 1 except: log.exception("error in privmsg_from_file") # event is an event to clone and change the text from. # Usage: !eval bot.eval_from_file(event, 'https://pastebin.com/raw/LhCt8FLh') def eval_from_file(self, event, url): try: r = requests.get(url) r.raise_for_status() content_type = r.headers["Content-Type"] if content_type is not None and cgi.parse_header( content_type)[0] != "text/plain": log.error( "eval_from_file should be fed with a text/plain URL. Refusing to send." ) return lines = r.text.splitlines() import copy for msg in lines: cloned_event = copy.deepcopy(event) cloned_event.arguments = [msg] # omit the source connection as None (since its not used) self.on_pubmsg(None, cloned_event) self.whisper(event.source.user.lower(), "Successfully evaluated {0} lines".format(len(lines))) except: log.exception("BabyRage") self.whisper(event.source.user.lower(), "Exception BabyRage") def privmsg(self, message, channel=None, increase_message=True): if channel is None: channel = self.channel return self.irc.privmsg(message, channel, increase_message=increase_message) def c_uptime(self): return utils.time_ago(self.start_time) @staticmethod def c_current_time(): return utils.now() @staticmethod def c_molly_age_in_years(): molly_birth = datetime.datetime(2018, 10, 29, tzinfo=datetime.timezone.utc) now = utils.now() diff = now - molly_birth return diff.total_seconds() / 3600 / 24 / 365 @property def is_online(self): return self.stream_manager.online def c_stream_status(self): return "online" if self.stream_manager.online else "offline" def c_status_length(self): if self.stream_manager.online: return utils.time_ago( self.stream_manager.current_stream.stream_start) if self.stream_manager.last_stream is not None: return utils.time_ago(self.stream_manager.last_stream.stream_end) return "No recorded stream FeelsBadMan " def execute_now(self, function, arguments=()): self.execute_delayed(0, function, arguments) def execute_at(self, at, function, arguments=()): self.reactor.scheduler.execute_at(at, lambda: function(*arguments)) def execute_delayed(self, delay, function, arguments=()): self.reactor.scheduler.execute_after(delay, lambda: function(*arguments)) def execute_every(self, period, function, arguments=()): self.reactor.scheduler.execute_every(period, lambda: function(*arguments)) def _ban(self, username, reason=""): self.privmsg(".ban {0} {1}".format(username, reason), increase_message=False) def ban(self, username, reason=""): self._timeout(username, 30, reason) self.execute_delayed(1, self._ban, (username, reason)) def ban_user(self, user, reason=""): self._timeout(user.username, 30, reason) self.execute_delayed(1, self._ban, (user.username, reason)) def unban(self, username): self.privmsg(".unban {0}".format(username), increase_message=False) def _timeout(self, username, duration, reason=""): self.privmsg(".timeout {0} {1} {2}".format(username, duration, reason), increase_message=False) def timeout(self, username, duration, reason=""): self._timeout(username, duration, reason) def timeout_warn(self, user, duration, reason=""): duration, punishment = user.timeout( duration, warning_module=self.module_manager["warning"]) self.timeout(user.username, duration, reason) return (duration, punishment) def timeout_user(self, user, duration, reason=""): self._timeout(user.username, duration, reason) def timeout_user_once(self, user, duration, reason): self._timeout(user.username, duration, reason) def _timeout_user(self, user, duration, reason=""): self._timeout(user.username, duration, reason) def delete_message(self, msg_id): self.privmsg(".delete {0}".format(msg_id)) def whisper(self, username, *messages, separator=". ", **rest): """ Takes a sequence of strings and concatenates them with separator. Then sends that string as a whisper to username """ if len(messages) < 0: return False message = separator.join(messages) return self.irc.whisper(username, message) def send_message_to_user(self, user, message, event, separator=". ", method="say"): if method == "say": self.say(user.username + ", " + lowercase_first_letter(message), separator=separator) elif method == "whisper": self.whisper(user.username, message, separator=separator) elif method == "me": self.me(message) elif method == "reply": if event.type in ["action", "pubmsg"]: self.say(message, separator=separator) elif event.type == "whisper": self.whisper(user.username, message, separator=separator) else: log.warning("Unknown send_message method: %s", method) def safe_privmsg(self, message, channel=None, increase_message=True): # Check for banphrases res = self.banphrase_manager.check_message(message, None) if res is not False: self.privmsg("filtered message ({})".format(res.id), channel, increase_message) return self.privmsg(message, channel, increase_message) def say(self, *messages, channel=None, separator=". "): """ Takes a sequence of strings and concatenates them with separator. Then sends that string to the given channel. """ if len(messages) < 0: return False if not self.silent: message = separator.join(messages).strip() message = utils.clean_up_message(message) if not message: return False self.privmsg(message[:510], channel) def is_bad_message(self, message): return self.banphrase_manager.check_message(message, None) is not False def safe_me(self, message, channel=None): if not self.is_bad_message(message): self.me(message, channel) def me(self, message, channel=None): self.say(".me " + message[:500], channel=channel) def on_welcome(self, chatconn, event): return self.irc.on_welcome(chatconn, event) def connect(self): return self.irc.start() def on_disconnect(self, chatconn, event): self.irc.on_disconnect(chatconn, event) def parse_message(self, message, source, event, tags={}, whisper=False): msg_lower = message.lower() emote_tag = None msg_id = None for tag in tags: if tag["key"] == "subscriber" and event.target == self.channel: source.subscriber = tag["value"] == "1" elif tag["key"] == "emotes": emote_tag = tag["value"] elif tag["key"] == "display-name" and tag["value"]: source.username_raw = tag["value"] elif tag["key"] == "user-type": source.moderator = tag[ "value"] == "mod" or source.username == self.streamer elif tag["key"] == "id": msg_id = tag["value"] # source.num_lines += 1 if source is None: log.error("No valid user passed to parse_message") return False if source.banned: self.ban(source.username) return False # If a user types when timed out, we assume he's been unbanned for a good reason and remove his flag. if source.timed_out is True: source.timed_out = False # Parse emotes in the message emote_instances, emote_counts = self.emote_manager.parse_all_emotes( message, emote_tag) if not whisper: # increment epm and ecount self.epm_manager.handle_emotes(emote_counts) self.ecount_manager.handle_emotes(emote_counts) urls = self.find_unique_urls(message) res = HandlerManager.trigger( "on_message", source=source, message=message, emote_instances=emote_instances, emote_counts=emote_counts, whisper=whisper, urls=urls, msg_id=msg_id, event=event, ) if res is False: return False source.last_seen = utils.now() source.last_active = utils.now() if source.ignored: return False if whisper: self.whisper("datguy1", "{} said: {}".format(source.username_raw, message)) if msg_lower[:1] == "!": msg_lower_parts = msg_lower.split(" ") trigger = msg_lower_parts[0][1:] msg_raw_parts = message.split(" ") remaining_message = " ".join( msg_raw_parts[1:]) if len(msg_raw_parts) > 1 else None if trigger in self.commands: command = self.commands[trigger] extra_args = { "emote_instances": emote_instances, "emote_counts": emote_counts, "trigger": trigger, "msg_id": msg_id, } command.run(self, source, remaining_message, event=event, args=extra_args, whisper=whisper) def on_whisper(self, chatconn, event): # We use .lower() in case twitch ever starts sending non-lowercased usernames username = event.source.user.lower() with self.users.get_user_context(username) as source: self.parse_message(event.arguments[0], source, event, whisper=True, tags=event.tags) def on_ping(self, chatconn, event): self.last_ping = utils.now() def on_pong(self, chatconn, event): self.last_pong = utils.now() def on_usernotice(self, chatconn, event): # We use .lower() in case twitch ever starts sending non-lowercased usernames tags = {} for d in event.tags: tags[d["key"]] = d["value"] if "login" not in tags: return username = tags["login"] with self.users.get_user_context(username) as source: msg = "" if event.arguments: msg = event.arguments[0] HandlerManager.trigger("on_usernotice", source=source, message=msg, tags=tags) def on_action(self, chatconn, event): self.on_pubmsg(chatconn, event) def on_pubmsg(self, chatconn, event): if event.source.user == self.nickname: return False username = event.source.user.lower() # We use .lower() in case twitch ever starts sending non-lowercased usernames with self.users.get_user_context(username) as source: res = HandlerManager.trigger("on_pubmsg", source=source, message=event.arguments[0]) if res is False: return False self.parse_message(event.arguments[0], source, event, tags=event.tags) @time_method def commit_all(self): for key, manager in self.commitable.items(): manager.commit() HandlerManager.trigger("on_commit", stop_on_false=False) @staticmethod def do_tick(): HandlerManager.trigger("on_tick") def quit(self, message, event, **options): quit_chub = self.config["main"].get("control_hub", None) quit_delay = 0 if quit_chub is not None and event.target == ("#{}".format(quit_chub)): quit_delay_random = 300 try: if message is not None and int(message.split()[0]) >= 1: quit_delay_random = int(message.split()[0]) except (IndexError, ValueError, TypeError): pass quit_delay = random.randint(0, quit_delay_random) log.info("%s is restarting in %d seconds.", self.nickname, quit_delay) self.execute_delayed(quit_delay, self.quit_bot) def quit_bot(self, **options): self.commit_all() HandlerManager.trigger("on_quit") phrase_data = {"nickname": self.nickname, "version": self.version_long} try: ScheduleManager.base_scheduler.print_jobs() ScheduleManager.base_scheduler.shutdown(wait=False) except: log.exception("Error while shutting down the apscheduler") try: for p in self.phrases["quit"]: self.privmsg(p.format(**phrase_data)) except Exception: log.exception("Exception caught while trying to say quit phrase") self.twitter_manager.quit() self.socket_manager.quit() sys.exit(0) def apply_filter(self, resp, f): available_filters = { "strftime": _filter_strftime, "lower": lambda var, args: var.lower(), "upper": lambda var, args: var.upper(), "time_since_minutes": lambda var, args: "no time" if var == 0 else utils.time_since(var * 60, 0, time_format="long"), "time_since": lambda var, args: "no time" if var == 0 else utils.time_since(var, 0, time_format="long"), "time_since_dt": _filter_time_since_dt, "urlencode": _filter_urlencode, "join": _filter_join, "number_format": _filter_number_format, "add": _filter_add, "or_else": _filter_or_else, "or_broadcaster": self._filter_or_broadcaster, "or_streamer": self._filter_or_broadcaster, } if f.name in available_filters: return available_filters[f.name](resp, f.arguments) return resp def _filter_or_broadcaster(self, var, args): return _filter_or_else(var, self.streamer) def find_unique_urls(self, message): from pajbot.modules.linkchecker import find_unique_urls return find_unique_urls(URL_REGEX, message)
def init(args): import subprocess import sys from flask import request from flask import session from flask import g from flask_scrypt import generate_random_salt import pajbot.utils import pajbot.web.common import pajbot.web.routes from pajbot.managers.db import DBManager from pajbot.managers.redis import RedisManager from pajbot.managers.schedule import ScheduleManager from pajbot.managers.songrequest_queue_manager import SongRequestQueueManager from pajbot.models.module import ModuleManager from pajbot.models.sock import SocketClientManager from pajbot.streamhelper import StreamHelper from pajbot.utils import load_config from pajbot.web.models import errors from pajbot.web.utils import download_logo from pajbot.web.utils import download_sub_badge ScheduleManager.init() config = load_config(args.config) # ScheduleManager.init() api_client_credentials = ClientCredentials( config["twitchapi"]["client_id"], config["twitchapi"]["client_secret"], config["twitchapi"]["redirect_uri"] ) redis_options = {} if "redis" in config: redis_options = dict(config["redis"]) RedisManager.init(**redis_options) id_api = TwitchIDAPI(api_client_credentials) app_token_manager = AppAccessTokenManager(id_api, RedisManager.get()) twitch_helix_api = TwitchHelixAPI(RedisManager.get(), app_token_manager) twitch_badges_api = TwitchBadgesAPI(RedisManager.get()) if "web" not in config: log.error("Missing [web] section in config.ini") sys.exit(1) if "secret_key" not in config["web"]: salt = generate_random_salt() config.set("web", "secret_key", salt.decode("utf-8")) with open(args.config, "w") as configfile: config.write(configfile) streamer = config["main"]["streamer"] SongRequestQueueManager.init(streamer) streamer_user_id = twitch_helix_api.get_user_id(streamer) if streamer_user_id is None: raise ValueError("The streamer login name you entered under [main] does not exist on twitch.") StreamHelper.init_streamer(streamer, streamer_user_id) try: download_logo(twitch_helix_api, streamer, streamer_user_id) except: log.exception("Error downloading the streamers profile picture") subscriber_badge_version = config["web"].get("subscriber_badge_version", "0") # Specifying a value of -1 in the config will disable sub badge downloading. Useful if you want to keep a custom version of a sub badge for a streamer if subscriber_badge_version != "-1": try: download_sub_badge(twitch_badges_api, streamer, streamer_user_id, subscriber_badge_version) except: log.exception("Error downloading the streamers subscriber badge") SocketClientManager.init(streamer) app.bot_modules = config["web"].get("modules", "").split() app.bot_commands_list = [] app.bot_config = config app.secret_key = config["web"]["secret_key"] app.bot_dev = "flags" in config and "dev" in config["flags"] and config["flags"]["dev"] == "1" DBManager.init(config["main"]["db"]) app.module_manager = ModuleManager(None).load() pajbot.web.routes.admin.init(app) pajbot.web.routes.api.init(app) pajbot.web.routes.base.init(app) pajbot.web.common.filters.init(app) pajbot.web.common.assets.init(app) pajbot.web.common.menu.init(app) app.register_blueprint(pajbot.web.routes.clr.page) errors.init(app, config) pajbot.web.routes.clr.config = config version = VERSION last_commit = None if app.bot_dev: version = extend_version_if_possible(VERSION) try: last_commit = subprocess.check_output(["git", "log", "-1", "--format=%cd"]).decode("utf8").strip() except: log.exception("Failed to get last_commit, will not show last commit") default_variables = { "version": version, "last_commit": last_commit, "bot": {"name": config["main"]["nickname"]}, "site": { "domain": config["web"]["domain"], "deck_tab_images": config.getboolean("web", "deck_tab_images"), "websocket": {"host": config["websocket"].get("host", f"wss://{config['web']['domain']}/clrsocket")}, "songrequestWS": { "host": config["songrequest-websocket"].get( "host", f"wss://{config['web']['domain']}/songrequest_websocket" ) }, }, "streamer": {"name": config["web"]["streamer_name"], "full_name": config["main"]["streamer"]}, "modules": app.bot_modules, "request": request, "session": session, "google_analytics": config["web"].get("google_analytics", None), } @app.context_processor def current_time(): current_time = {} current_time["current_time"] = pajbot.utils.now() return current_time @app.context_processor def inject_default_variables(): return default_variables
def init(args): import configparser import logging import subprocess import sys from flask import request from flask import session from flask_scrypt import generate_random_salt import pajbot.utils import pajbot.web.common import pajbot.web.routes from pajbot import constants from pajbot.managers.db import DBManager from pajbot.managers.redis import RedisManager from pajbot.managers.time import TimeManager from pajbot.models.module import ModuleManager from pajbot.models.sock import SocketClientManager from pajbot.streamhelper import StreamHelper from pajbot.utils import load_config from pajbot.web.models import errors from pajbot.web.utils import download_logo log = logging.getLogger(__name__) config = configparser.ConfigParser() config = load_config(args.config) config.read("webconfig.ini") api_client_credentials = ClientCredentials( config["twitchapi"]["client_id"], config["twitchapi"]["client_secret"], config["twitchapi"]["redirect_uri"] ) redis_options = {} if "redis" in config: redis_options = dict(config["redis"]) RedisManager.init(**redis_options) id_api = TwitchIDAPI(api_client_credentials) app_token_manager = AppAccessTokenManager(id_api, RedisManager.get()) twitch_helix_api = TwitchHelixAPI(RedisManager.get(), app_token_manager) if "web" not in config: log.error("Missing [web] section in config.ini") sys.exit(1) if "pleblist_password_salt" not in config["web"]: salt = generate_random_salt() config.set("web", "pleblist_password_salt", salt.decode("utf-8")) if "pleblist_password" not in config["web"]: salt = generate_random_salt() config.set("web", "pleblist_password", salt.decode("utf-8")) if "secret_key" not in config["web"]: salt = generate_random_salt() config.set("web", "secret_key", salt.decode("utf-8")) if "logo" not in config["web"]: try: download_logo(twitch_helix_api, config["main"]["streamer"]) config.set("web", "logo", "set") except: log.exception("Error downloading logo") StreamHelper.init_web(config["main"]["streamer"]) SocketClientManager.init(config["main"]["streamer"]) with open(args.config, "w") as configfile: config.write(configfile) app.bot_modules = config["web"].get("modules", "").split() app.bot_commands_list = [] app.bot_config = config app.secret_key = config["web"]["secret_key"] DBManager.init(config["main"]["db"]) TimeManager.init_timezone(config["main"].get("timezone", "UTC")) app.module_manager = ModuleManager(None).load() pajbot.web.routes.admin.init(app) pajbot.web.routes.api.init(app) pajbot.web.routes.base.init(app) pajbot.web.common.filters.init(app) pajbot.web.common.assets.init(app) pajbot.web.common.menu.init(app) app.register_blueprint(pajbot.web.routes.clr.page) errors.init(app, config) pajbot.web.routes.clr.config = config version = extend_version_if_possible(VERSION) try: last_commit = subprocess.check_output(["git", "log", "-1", "--format=%cd"]).decode("utf8").strip() except: log.exception("Failed to get last_commit, will not show last commit") last_commit = None default_variables = { "version": version, "last_commit": last_commit, "bot": {"name": config["main"]["nickname"]}, "site": { "domain": config["web"]["domain"], "deck_tab_images": config.getboolean("web", "deck_tab_images"), "websocket": { "host": config["websocket"].get("host", "wss://{}/clrsocket".format(config["web"]["domain"])) }, }, "streamer": {"name": config["web"]["streamer_name"], "full_name": config["main"]["streamer"]}, "modules": app.bot_modules, "request": request, "session": session, "google_analytics": config["web"].get("google_analytics", None), } @app.context_processor def current_time(): current_time = {} current_time["current_time"] = pajbot.utils.now() return current_time @app.context_processor def inject_default_variables(): return default_variables