Beispiel #1
0
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")
Beispiel #2
0
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")
Beispiel #3
0
    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)
Beispiel #4
0
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)
Beispiel #5
0
    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})"
Beispiel #6
0
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)
Beispiel #7
0
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
Beispiel #8
0
    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,
        }
Beispiel #9
0
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)
Beispiel #10
0
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
Beispiel #11
0
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