Example #1
0
    def update_chatters_stage2(self, chatters):
        points = 1 if self.bot.is_online else 0

        log.debug('Updating {0} chatters'.format(len(chatters)))

        self.bot.stream_manager.update_chatters(chatters, self.update_chatters_interval)

        with RedisManager.pipeline_context() as pipeline:
            with DBManager.create_session_scope() as db_session:
                user_models = UserManager.get().bulk_load_user_models(chatters, db_session)
                users = []
                for username in chatters:
                    user_model = user_models.get(username, None)
                    user = UserManager.get().get_user(username, db_session=db_session, user_model=user_model, redis=pipeline)
                    users.append(user)

                more_update_data = {}
                if self.bot.is_online:
                    more_update_data['minutes_in_chat_online'] = self.update_chatters_interval
                else:
                    more_update_data['minutes_in_chat_offline'] = self.update_chatters_interval

                points_to_give_out = {}
                dt_now = datetime.datetime.now().timestamp()
                for user in users:
                    user._set_last_seen(dt_now)

                    num_points = points
                    if user.subscriber:
                        num_points *= 5
                    # TODO: Load user tags during the pipeline redis data fetch
                    if self.bot.streamer == 'forsenlol' and 'trumpsc_sub' in user.get_tags():
                        num_points *= 0.5

                    num_points = int(num_points)

                    if num_points not in points_to_give_out:
                        points_to_give_out[num_points] = []

                    points_to_give_out[num_points].append(user.username)

                    user.save(save_to_db=False)

                for num_points, usernames in points_to_give_out.items():
                    payload = {
                            User.points: User.points + num_points,
                            }
                    if self.bot.is_online:
                        payload[User.minutes_in_chat_online] = User.minutes_in_chat_online + self.update_chatters_interval
                    else:
                        payload[User.minutes_in_chat_offline] = User.minutes_in_chat_offline + self.update_chatters_interval
                    db_session.query(User).filter(User.username.in_(usernames)).\
                            update(payload, synchronize_session=False)

                pipeline.execute()
Example #2
0
    def command_masspoints(self, **options):
        bot = options["bot"]
        source = options["source"]
        message = options["message"]

        if not message:
            return False

        pointsArgument = message.split(" ")[0]
        givePoints = 0

        try:
            givePoints = int(pointsArgument)
        except ValueError:
            bot.whisper(source.username_raw, "Error: You must give an integer")
            return False

        currentChatters = bot.twitch_tmi_api.get_chatters(bot.streamer)
        numUsers = len(currentChatters)
        if not currentChatters:
            bot.say("Error fetching chatters")
            return False

        with RedisManager.pipeline_context() as pipeline:
            with DBManager.create_session_scope() as db_session:
                userModels = UserManager.get().bulk_load_user_models(
                    currentChatters, db_session)
                for userName in currentChatters:
                    userModel = userModels.get(userName, None)
                    userInstance = UserManager.get().get_user(
                        userName,
                        db_session=db_session,
                        user_model=userModel,
                        redis=pipeline)

                    # Bot/idler check. Quicker than removing from list?
                    if userInstance.num_lines < 5:
                        numUsers -= 1
                        continue

                    if userInstance.subscriber:
                        userInstance.points += givePoints * self.settings[
                            "sub_points"]
                    else:
                        userInstance.points += givePoints

        bot.say("{} just gave {} viewers {} points each! Enjoy FeelsGoodMan".
                format(source.username_raw, numUsers, givePoints))
Example #3
0
    def stats():
        bot_commands_list = pajbot.web.utils.get_cached_commands()
        top_5_commands = sorted(bot_commands_list, key=lambda c: c['data']['num_uses'] if c['data'] is not None else -1, reverse=True)[:5]

        redis = RedisManager.get()

        # TODO: Make this hideable through some magic setting (NOT config.ini @_@)
        with DBManager.create_session_scope() as db_session:
            top_5_line_farmers = []
            for redis_user in redis.zrevrangebyscore(
                    '{streamer}:users:num_lines'.format(streamer=StreamHelper.get_streamer()),
                    '+inf',
                    '-inf',
                    start=0,
                    num=5,
                    withscores=True,
                    score_cast_func=int):
                user = UserManager.get_static(redis_user[0], db_session=db_session)
                user.save_to_redis = False
                user.num_lines = redis_user[1]
                top_5_line_farmers.append(user)

            data = {
                    'top_5_winners': db_session.query(UserDuelStats).order_by(UserDuelStats.duels_won.desc())[:5],
                    'top_5_points_won': db_session.query(UserDuelStats).order_by(UserDuelStats.profit.desc())[:5],
                    'top_5_points_lost': db_session.query(UserDuelStats).order_by(UserDuelStats.profit.asc())[:5],
                    'top_5_losers': db_session.query(UserDuelStats).order_by(UserDuelStats.duels_lost.desc())[:5],
                    'top_5_winrate': db_session.query(UserDuelStats).filter(UserDuelStats.duels_won >= 5).order_by(UserDuelStats.winrate.desc())[:5],
                    'bottom_5_winrate': db_session.query(UserDuelStats).filter(UserDuelStats.duels_lost >= 5).order_by(UserDuelStats.winrate.asc())[:5],
                    }

            return render_template('stats.html',
                    **data,
                    top_5_commands=top_5_commands,
                    top_5_line_farmers=top_5_line_farmers)
Example #4
0
    def stats():
        bot_commands_list = pajbot.web.utils.get_cached_commands()
        top_5_commands = sorted(bot_commands_list, key=lambda c: c['data']['num_uses'] if c['data'] is not None else -1, reverse=True)[:5]

        redis = RedisManager.get()

        # TODO: Make this hideable through some magic setting (NOT config.ini @_@)
        with DBManager.create_session_scope() as db_session:
            top_5_line_farmers = []
            for redis_user in redis.zrevrangebyscore(
                    '{streamer}:users:num_lines'.format(streamer=StreamHelper.get_streamer()),
                    '+inf',
                    '-inf',
                    start=0,
                    num=5,
                    withscores=True,
                    score_cast_func=int):
                user = UserManager.get_static(redis_user[0], db_session=db_session)
                user.save_to_redis = False
                user.num_lines = redis_user[1]
                top_5_line_farmers.append(user)

            return render_template('stats.html',
                    top_5_commands=top_5_commands,
                    top_5_line_farmers=top_5_line_farmers)
Example #5
0
    def stats():
        bot_commands_list = pajbot.web.utils.get_cached_commands()
        top_5_commands = sorted(bot_commands_list,
                                key=lambda c: c["data"]["num_uses"]
                                if c["data"] is not None else -1,
                                reverse=True)[:5]

        redis = RedisManager.get()

        # TODO: Make this hideable through some magic setting (NOT config.ini @_@)
        with DBManager.create_session_scope() as db_session:
            top_5_line_farmers = []
            for redis_user in redis.zrevrangebyscore(
                    "{streamer}:users:num_lines".format(
                        streamer=StreamHelper.get_streamer()),
                    "+inf",
                    "-inf",
                    start=0,
                    num=5,
                    withscores=True,
                    score_cast_func=int,
            ):
                user = UserManager.get_static(redis_user[0],
                                              db_session=db_session)
                user.save_to_redis = False
                top_5_line_farmers.append(user)

            return render_template("stats.html",
                                   top_5_commands=top_5_commands,
                                   top_5_line_farmers=top_5_line_farmers)
Example #6
0
 def on_message(self, source, message, emotes, whisper, urls, event):
     if whisper is False and source.username in self.valid_usernames:
         # Did twitchnotify tell us about a new sub?
         m = self.new_sub_regex.search(message)
         if m and 'subscribed to ' not in message:
             username = m.group(1)
             with UserManager.get().get_user_context(username) as user:
                 self.on_new_sub(user)
                 HandlerManager.trigger('on_user_sub', user)
Example #7
0
 def on_message(self, source, message, emotes, whisper, urls, event):
     if whisper is False and source.username in self.valid_usernames:
         # Did twitchnotify tell us about a new sub?
         m = self.new_sub_regex.search(message)
         if m:
             username = m.group(1)
             with UserManager.get().get_user_context(username) as user:
                 self.on_new_sub(user)
                 HandlerManager.trigger('on_user_sub', user)
Example #8
0
 def on_message(self, source, message, emotes, whisper, urls, event):
     if whisper is False and source.username in self.valid_usernames:
         # Did twitchnotify tell us about a new sub?
         m = self.new_sub_regex.search(message)
         if m:
             username = m.group(1)
             with UserManager.get().get_user_context(username) as user:
                 self.on_new_sub(user)
                 HandlerManager.trigger('on_user_sub', user)
         else:
             # Did twitchnotify tell us about a resub?
             m = self.resub_regex.search(message)
             if m:
                 username = m.group(1)
                 num_months = int(m.group(2))
                 with UserManager.get().get_user_context(username) as user:
                     self.on_resub(user, num_months)
                     HandlerManager.trigger('on_user_resub', user,
                                            num_months)
Example #9
0
    def get(username):
        user = UserManager.find_static(username)
        if not user:
            return {"error": "Not found"}, 404

        redis = RedisManager.get()
        key = "{streamer}:users:num_lines".format(
            streamer=StreamHelper.get_streamer())
        rank = redis.zrevrank(key, user.username)
        if rank is None:
            rank = redis.zcard(key)
        else:
            rank = rank + 1

        return user.jsonify()
Example #10
0
    def user_profile(username):
        with DBManager.create_session_scope() as db_session:
            user = UserManager.find_static(username, db_session=db_session)
            if not user:
                return render_template('no_user.html'), 404

            try:
                roulette_stats, roulettes = get_roulette(app, user)
            except TypeError:
                roulette_stats = None
                roulettes = {}
            try:
                dotabet_stats, recent_bets = get_bets(app, user)
            except TypeError:
                dotabet_stats = None
                recent_bets = {}

            return render_template('user.html',
                                   user=user,
                                   roulette_stats=roulette_stats,
                                   dotabet_stats=dotabet_stats,
                                   roulettes=roulettes,
                                   bets=recent_bets)
Example #11
0
    def get(username):
        user = UserManager.find_static(username)
        if not user:
            return {"error": "Not found"}, 404

        return user.jsonify()
Example #12
0
class Bot:
    """
    Main class for the twitch bot
    """

    version = '1.32'
    date_fmt = '%H:%M'
    admin = None
    url_regex_str = r'\(?(?:(http|https):\/\/)?(?:((?:[^\W\s]|\.|-|[:]{1})+)@{1})?((?:www.)?(?:[^\W\s]|\.|-)+[\.][^\W\s]{2,4}|localhost(?=\/)|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::(\d*))?([\/]?[^\s\?]*[\/]{1})*(?:\/?([^\s\n\?\[\]\{\}\#]*(?:(?=\.)){1}|[^\s\n\?\[\]\{\}\.\#]*)?([\.]{1}[^\s\?\#]*)?)?(?:\?{1}([^\s\n\#\[\]]*))?([\#][^\s\n]*)?\)?'

    last_ping = datetime.datetime.now()
    last_pong = datetime.datetime.now()

    def parse_args():
        parser = argparse.ArgumentParser()
        parser.add_argument('--config', '-c',
                            default='config.ini',
                            help='Specify which config file to use '
                                    '(default: config.ini)')
        parser.add_argument('--silent',
                            action='count',
                            help='Decides whether the bot should be '
                            'silent or not')
        # TODO: Add a log level argument.

        return parser.parse_args()

    def load_config(self, config):
        self.config = config

        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.domain = config['web'].get('domain', 'localhost')

        self.nickname = config['main'].get('nickname', 'pajbot')
        self.password = config['main'].get('password', 'abcdef')

        self.timezone = config['main'].get('timezone', 'UTC')
        os.environ['TZ'] = self.timezone

        if config['main'].getboolean('verified', False):
            TMI.promote_to_verified()

        self.trusted_mods = config.getboolean('main', 'trusted_mods')

        TimeManager.init_timezone(self.timezone)

        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.silent = False
        self.dev = False

        if 'flags' in config:
            self.silent = True if 'silent' in config['flags'] and config['flags']['silent'] == '1' else self.silent
            self.dev = True if 'dev' in config['flags'] and config['flags']['dev'] == '1' else self.dev

        DBManager.init(self.config['main']['db'])

        redis_options = {}
        if 'redis' in config:
            redis_options = config._sections['redis']

        RedisManager.init(**redis_options)

    def __init__(self, config, args=None):
        # Load various configuration variables from the given config object
        # The config object that should be passed through should
        # come from pajbot.utils.load_config
        self.load_config(config)

        # Update the database scheme if necessary using alembic
        # In case of errors, i.e. if the database is out of sync or the alembic
        # binary can't be called, we will shut down the bot.
        pajbot.utils.alembic_upgrade()

        # 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 = datetime.datetime.now()
        ActionParser.bot = self

        HandlerManager.init_handlers()

        self.socket_manager = SocketManager(self)
        self.stream_manager = StreamManager(self)

        StreamHelper.init_bot(self, self.stream_manager)
        ScheduleManager.init()

        self.users = UserManager()
        self.decks = DeckManager()
        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.filters = FilterManager().reload()
        self.banphrase_manager = BanphraseManager(self).load()
        self.timer_manager = TimerManager(self).load()
        self.kvi = KVIManager()
        self.emotes = EmoteManager(self)
        self.twitter_manager = TwitterManager(self)

        HandlerManager.trigger('on_managers_loaded')

        # Reloadable managers
        self.reloadable = {
                'filters': self.filters,
                }

        # Commitable managers
        self.commitable = {
                'commands': self.commands,
                'filters': self.filters,
                'banphrases': self.banphrase_manager,
                }

        self.execute_every(10 * 60, self.commit_all)
        self.execute_every(1, self.do_tick)

        try:
            self.admin = self.config['main']['admin']
        except KeyError:
            log.warning('No admin user specified. See the [main] section in config.example.ini for its usage.')
        if self.admin:
            with self.users.get_user_context(self.admin) as user: pass
                # user.level = 2000

        self.parse_version()

        relay_host = self.config['main'].get('relay_host', None)
        relay_password = self.config['main'].get('relay_password', None)
        if relay_host is None or relay_password is None:
            self.irc = MultiIRCManager(self)
        else:
            self.irc = SingleIRCManager(self)

        self.reactor.add_global_handler('all_events', self.irc._dispatcher, -10)

        twitch_client_id = None
        twitch_oauth = None
        if 'twitchapi' in self.config:
            twitch_client_id = self.config['twitchapi'].get('client_id', None)
            twitch_oauth = self.config['twitchapi'].get('oauth', None)

        # A client ID is required for the bot to work properly now, give an error for now
        if twitch_client_id is None:
            log.error('MISSING CLIENT ID, SET "client_id" VALUE UNDER [twitchapi] SECTION IN CONFIG FILE')

        self.twitchapi = TwitchAPI(twitch_client_id, twitch_oauth)

        self.data = {}
        self.data_cb = {}
        self.url_regex = re.compile(self.url_regex_str, re.IGNORECASE)

        self.data['broadcaster'] = self.streamer
        self.data['version'] = self.version
        self.data['version_brief'] = self.version_brief
        self.data['bot_name'] = self.nickname
        self.data_cb['status_length'] = self.c_status_length
        self.data_cb['stream_status'] = self.c_stream_status
        self.data_cb['bot_uptime'] = self.c_uptime
        self.data_cb['current_time'] = self.c_current_time

        self.silent = True if args.silent else self.silent

        if self.silent:
            log.info('Silent mode enabled')

        """
        For actions that need to access the main thread,
        we can use the mainthread_queue.
        """
        self.mainthread_queue = ActionQueue()
        self.execute_every(1, self.mainthread_queue.parse_action)

        self.websocket_manager = WebSocketManager(self)

        try:
            if self.config['twitchapi']['update_subscribers'] == '1':
                self.execute_every(30 * 60,
                                   self.action_queue.add,
                                   (self.update_subscribers_stage1, ))
        except:
            pass

        # XXX: TEMPORARY UGLY CODE
        HandlerManager.add_handler('on_user_gain_tokens', self.on_user_gain_tokens)
        HandlerManager.add_handler('send_whisper', self.whisper)

    def on_connect(self, sock):
        return self.irc.on_connect(sock)

    def on_user_gain_tokens(self, user, tokens_gained):
        self.whisper(user.username, 'You finished todays quest! You have been awarded with {} tokens.'.format(tokens_gained))

    def update_subscribers_stage1(self):
        limit = 25
        offset = 0
        subscribers = []
        log.info('Starting stage1 subscribers update')

        try:
            retry_same = 0
            while True:
                log.debug('Beginning sub request {0} {1}'.format(limit, offset))
                subs, retry_same, error = self.twitchapi.get_subscribers(self.streamer, 0, offset, 0 if retry_same is False else retry_same)
                log.debug('got em!')

                if error is True:
                    log.error('Too many attempts, aborting')
                    return False

                if retry_same is False:
                    offset += limit
                    if len(subs) == 0:
                        # We don't need to retry, and the last query finished propery
                        # Break out of the loop and start fiddling with the subs!
                        log.debug('Done!')
                        break
                    else:
                        log.debug('Fetched {0} subs'.format(len(subs)))
                        subscribers.extend(subs)

                if retry_same is not False:
                    # In case the next attempt is a retry, wait for 3 seconds
                    log.debug('waiting for 3 seconds...')
                    time.sleep(3)
                    log.debug('waited enough!')
            log.debug('Finished with the while True loop!')
        except:
            log.exception('Caught an exception while trying to get subscribers')
            return

        log.info('Ended stage1 subscribers update')
        if len(subscribers) > 0:
            log.info('Got some subscribers, so we are pushing them to stage 2!')
            self.mainthread_queue.add(self.update_subscribers_stage2,
                                      args=[subscribers])
            log.info('Pushed them now.')

    def update_subscribers_stage2(self, subscribers):
        self.kvi['active_subs'].set(len(subscribers) - 1)

        self.users.reset_subs()

        self.users.update_subs(subscribers)

    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_tm(self, key, extra={}):
        val = self.emotes.get_emote_epm(key)
        if not val:
            return None
        return '{0:,d}'.format(val)

    def get_emote_count(self, key, extra={}):
        val = self.emotes.get_emote_count(key)
        if not val:
            return None
        return '{0:,d}'.format(val)

    def get_emote_tm_record(self, key, extra={}):
        val = self.emotes.get_emote_epmrecord(key)
        if not val:
            return None
        return '{0:,d}'.format(val)

    def get_source_value(self, 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

    def get_command_value(self, 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)
            else:
                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(self.date_fmt)
        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 len(ret) == 0:
            return None
        return ret

    def get_args_value(self, key, extra={}):
        range = 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:
                    range = (int(range_str[0]), int(range_str[1]))

            if range is None:
                range = (int(key), len(msg_parts))
        except (TypeError, ValueError):
            range = (0, len(msg_parts))

        try:
            return ' '.join(msg_parts[range[0]:range[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]
        elif key in self.data:
            return self.data[key]
        elif key in self.data_cb:
            return self.data_cb[key]()

        log.warning('Unknown key passed to get_value: {0}'.format(key))
        return None

    def privmsg_arr(self, arr):
        for msg in arr:
            self.privmsg(msg)

    def privmsg_from_file(self, url, per_chunk=35, chunk_delay=30):
        try:
            r = requests.get(url)
            lines = r.text.split('\n')
            i = 0
            while len(lines) > 0:
                if i == 0:
                    self.privmsg_arr(lines[:per_chunk])
                else:
                    self.execute_delayed(chunk_delay * i, self.privmsg_arr, (lines[:per_chunk],))
                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.
    def eval_from_file(self, event, url):
        try:
            r = requests.get(url)
            lines = r.text.splitlines()
            import copy
            for msg in lines:
                cloned_event = copy.deepcopy(event)
                cloned_event.arguments = [msg]
                # omit the source connectino as None (since its not used)
                self.on_pubmsg(None, cloned_event)
        except:
            log.exception('BabyRage')
            self.say('Exception')
        self.say('All lines done')

    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 time_since(datetime.datetime.now().timestamp(), self.start_time.timestamp())

    def c_current_time(self):
        return datetime.datetime.now()

    @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 time_since(time.time(), self.stream_manager.current_stream.stream_start.timestamp())
        else:
            if self.stream_manager.last_stream is not None:
                return time_since(time.time(), self.stream_manager.last_stream.stream_end.timestamp())
            else:
                return 'No recorded stream FeelsBadMan '

    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=''):
        log.debug('Banning {}'.format(username))
        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=''):
        log.debug('Timing out {} for {} seconds'.format(username, duration))
        self._timeout(username, duration, reason)
        # self.execute_delayed(1, 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)
        self.execute_delayed(1, self._timeout, (user.username, duration, reason))

    def _timeout_user(self, user, duration, reason=''):
        self._timeout(user.username, duration, reason)

    def whisper(self, username, *messages, separator='. '):
        """
        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, separator=separator)
        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: {}'.format(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 = clean_up_message(message)
            if not message:
                return False

            # log.info('Sending message: {0}'.format(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):
        if not self.silent:
            self.say('.me ' + message[:500], channel=channel)

    def parse_version(self):
        self.version = self.version
        self.version_brief = self.version

        if self.dev:
            try:
                current_branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).decode('utf8').strip()
                latest_commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf8').strip()[:8]
                commit_number = subprocess.check_output(['git', 'rev-list', 'HEAD', '--count']).decode('utf8').strip()
                self.version = '{0} DEV ({1}, {2}, commit {3})'.format(self.version, current_branch, latest_commit, commit_number)
            except:
                pass

    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, msg_raw, source, event, tags={}, whisper=False):
        msg_lower = msg_raw.lower()

        emote_tag = None

        for tag in tags:
            if tag['key'] == 'subscriber' and event.target == self.channel:
                source.subscriber = tag['value'] == '1'
            elif tag['key'] == 'emotes' and tag['value']:
                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

        # 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
        message_emotes = self.emotes.parse_message_twitch_emotes(source, msg_raw, emote_tag, whisper)

        urls = self.find_unique_urls(msg_raw)

        if whisper:
            self.whisper('datguy1', '{} said: {}'.format(source.username, msg_raw))
        # log.debug('{2}{0}: {1}'.format(source.username, msg_raw, '<w>' if whisper else ''))

        res = HandlerManager.trigger('on_message',
                source, msg_raw, message_emotes, whisper, urls, event,
                stop_on_false=True)
        if res is False:
            return False

        source.last_seen = datetime.datetime.now()
        source.last_active = datetime.datetime.now()

        if source.ignored:
            return False

        if msg_lower[:1] == '!':
            msg_lower_parts = msg_lower.split(' ')
            trigger = msg_lower_parts[0][1:]
            msg_raw_parts = msg_raw.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 = {
                        'emotes': message_emotes,
                        'trigger': trigger,
                        }
                command.run(self, source, remaining_message, event=event, args=extra_args, whisper=whisper)

    @time_method
    def redis_test(self, username):
        try:
            pipeline = RedisManager.get().pipeline()
            pipeline.hget('pajlada:users:points', username)
            pipeline.hget('pajlada:users:ignored', username)
            pipeline.hget('pajlada:users:banned', username)
            pipeline.hget('pajlada:users:last_active', username)
        except:
            pipeline.reset()
        finally:
            b = pipeline.execute()
            log.info(b)

    @time_method
    def redis_test2(self, username):
        redis = RedisManager.get()
        values = [
                redis.hget('pajlada:users:points', username),
                redis.hget('pajlada:users:ignored', username),
                redis.hget('pajlada:users:banned', username),
                redis.hget('pajlada:users:last_active', username),
                ]
        return values

    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.say('Received a ping. Last ping received {} ago'.format(time_since(datetime.datetime.now().timestamp(), self.last_ping.timestamp())))
        # log.info('Received a ping. Last ping received {} ago'.format(time_since(datetime.datetime.now().timestamp(), self.last_ping.timestamp())))
        self.last_ping = datetime.datetime.now()

    def on_pong(self, chatconn, event):
        # self.say('Received a pong. Last pong received {} ago'.format(time_since(datetime.datetime.now().timestamp(), self.last_pong.timestamp())))
        # log.info('Received a pong. Last pong received {} ago'.format(time_since(datetime.datetime.now().timestamp(), self.last_pong.timestamp())))
        self.last_pong = datetime.datetime.now()

    def on_pubnotice(self, chatconn, event):
        return
        type = 'whisper' if chatconn in self.whisper_manager else 'normal'
        log.debug('NOTICE {}@{}: {}'.format(type, event.target, event.arguments))

    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 len(event.arguments) > 0:
                msg = event.arguments[0]
            HandlerManager.trigger('on_usernotice',
                    source, msg, 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, event.arguments[0],
                    stop_on_false=True)
            if res is False:
                return False
            self.parse_message(event.arguments[0], source, event, tags=event.tags)

    @time_method
    def reload_all(self):
        log.info('Reloading all...')
        for key, manager in self.reloadable.items():
            log.debug('Reloading {0}'.format(key))
            manager.reload()
            log.debug('Done with {0}'.format(key))
        log.info('ok!')

    @time_method
    def commit_all(self):
        log.info('Commiting all...')
        for key, manager in self.commitable.items():
            log.info('Commiting {0}'.format(key))
            manager.commit()
            log.info('Done with {0}'.format(key))
        log.info('ok!')

        HandlerManager.trigger('on_commit', stop_on_false=False)

    def do_tick(self):
        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('{} is restarting in {} seconds.'.format(self.nickname, quit_delay))

        self.execute_delayed(quit_delay, self.quit_bot)

    def quit_bot(self, **options):
        self.commit_all()
        quit = '{nickname} {version} shutting down...'
        phrase_data = {
                'nickname': self.nickname,
                'version': self.version,
                }

        try:
            ScheduleManager.base_scheduler.print_jobs()
            ScheduleManager.base_scheduler.shutdown(wait=False)
        except:
            log.exception('Error while shutting down the apscheduler')

        try:
            self.say('I have to leave PepeHands')
        except Exception:
            log.exception('Exception caught while trying to say quit phrase')

        self.twitter_manager.quit()
        self.socket_manager.quit()
        self.irc.quit()

        sys.exit(0)

    def apply_filter(self, resp, filter):
        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 time_since(var * 60, 0, format='long'),
                'time_since': lambda var, args: 'no time' if var == 0 else time_since(var, 0, format='long'),
                'time_since_dt': _filter_time_since_dt,
                'urlencode': _filter_urlencode,
                'join': _filter_join,
                'number_format': _filter_number_format,
                'add': _filter_add,
                }
        if filter.name in available_filters:
            return available_filters[filter.name](resp, filter.arguments)
        return resp

    def find_unique_urls(self, message):
        from pajbot.modules.linkchecker import find_unique_urls
        return find_unique_urls(self.url_regex, message)
Example #13
0
    def __init__(self, config, args=None):
        # Load various configuration variables from the given config object
        # The config object that should be passed through should
        # come from pajbot.utils.load_config
        self.load_config(config)

        # Update the database scheme if necessary using alembic
        # In case of errors, i.e. if the database is out of sync or the alembic
        # binary can't be called, we will shut down the bot.
        pajbot.utils.alembic_upgrade()

        # 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 = datetime.datetime.now()
        ActionParser.bot = self

        HandlerManager.init_handlers()

        self.socket_manager = SocketManager(self)
        self.stream_manager = StreamManager(self)

        StreamHelper.init_bot(self, self.stream_manager)
        ScheduleManager.init()

        self.users = UserManager()
        self.decks = DeckManager()
        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.filters = FilterManager().reload()
        self.banphrase_manager = BanphraseManager(self).load()
        self.timer_manager = TimerManager(self).load()
        self.kvi = KVIManager()
        self.emotes = EmoteManager(self)
        self.twitter_manager = TwitterManager(self)

        HandlerManager.trigger('on_managers_loaded')

        # Reloadable managers
        self.reloadable = {
                'filters': self.filters,
                }

        # Commitable managers
        self.commitable = {
                'commands': self.commands,
                'filters': self.filters,
                'banphrases': self.banphrase_manager,
                }

        self.execute_every(10 * 60, self.commit_all)
        self.execute_every(1, self.do_tick)

        try:
            self.admin = self.config['main']['admin']
        except KeyError:
            log.warning('No admin user specified. See the [main] section in config.example.ini for its usage.')
        if self.admin:
            with self.users.get_user_context(self.admin) as user: pass
                # user.level = 2000

        self.parse_version()

        relay_host = self.config['main'].get('relay_host', None)
        relay_password = self.config['main'].get('relay_password', None)
        if relay_host is None or relay_password is None:
            self.irc = MultiIRCManager(self)
        else:
            self.irc = SingleIRCManager(self)

        self.reactor.add_global_handler('all_events', self.irc._dispatcher, -10)

        twitch_client_id = None
        twitch_oauth = None
        if 'twitchapi' in self.config:
            twitch_client_id = self.config['twitchapi'].get('client_id', None)
            twitch_oauth = self.config['twitchapi'].get('oauth', None)

        # A client ID is required for the bot to work properly now, give an error for now
        if twitch_client_id is None:
            log.error('MISSING CLIENT ID, SET "client_id" VALUE UNDER [twitchapi] SECTION IN CONFIG FILE')

        self.twitchapi = TwitchAPI(twitch_client_id, twitch_oauth)

        self.data = {}
        self.data_cb = {}
        self.url_regex = re.compile(self.url_regex_str, re.IGNORECASE)

        self.data['broadcaster'] = self.streamer
        self.data['version'] = self.version
        self.data['version_brief'] = self.version_brief
        self.data['bot_name'] = self.nickname
        self.data_cb['status_length'] = self.c_status_length
        self.data_cb['stream_status'] = self.c_stream_status
        self.data_cb['bot_uptime'] = self.c_uptime
        self.data_cb['current_time'] = self.c_current_time

        self.silent = True if args.silent else self.silent

        if self.silent:
            log.info('Silent mode enabled')

        """
        For actions that need to access the main thread,
        we can use the mainthread_queue.
        """
        self.mainthread_queue = ActionQueue()
        self.execute_every(1, self.mainthread_queue.parse_action)

        self.websocket_manager = WebSocketManager(self)

        try:
            if self.config['twitchapi']['update_subscribers'] == '1':
                self.execute_every(30 * 60,
                                   self.action_queue.add,
                                   (self.update_subscribers_stage1, ))
        except:
            pass

        # XXX: TEMPORARY UGLY CODE
        HandlerManager.add_handler('on_user_gain_tokens', self.on_user_gain_tokens)
        HandlerManager.add_handler('send_whisper', self.whisper)
Example #14
0
File: bot.py Project: jardg/pajbot
class Bot:
    """
    Main class for the twitch bot
    """

    version = '1.30'
    date_fmt = '%H:%M'
    admin = None
    url_regex_str = r'\(?(?:(http|https):\/\/)?(?:((?:[^\W\s]|\.|-|[:]{1})+)@{1})?((?:www.)?(?:[^\W\s]|\.|-)+[\.][^\W\s]{2,4}|localhost(?=\/)|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::(\d*))?([\/]?[^\s\?]*[\/]{1})*(?:\/?([^\s\n\?\[\]\{\}\#]*(?:(?=\.)){1}|[^\s\n\?\[\]\{\}\.\#]*)?([\.]{1}[^\s\?\#]*)?)?(?:\?{1}([^\s\n\#\[\]]*))?([\#][^\s\n]*)?\)?'

    last_ping = datetime.datetime.now()
    last_pong = datetime.datetime.now()

    def parse_args():
        parser = argparse.ArgumentParser()
        parser.add_argument('--config', '-c',
                            default='config.ini',
                            help='Specify which config file to use '
                                    '(default: config.ini)')
        parser.add_argument('--silent',
                            action='count',
                            help='Decides whether the bot should be '
                            'silent or not')
        # TODO: Add a log level argument.

        return parser.parse_args()

    def load_config(self, config):
        self.config = config

        self.domain = config['web'].get('domain', 'localhost')

        self.nickname = config['main'].get('nickname', 'pajbot')
        self.password = config['main'].get('password', 'abcdef')

        self.timezone = config['main'].get('timezone', 'UTC')

        self.trusted_mods = config.getboolean('main', 'trusted_mods')

        TimeManager.init_timezone(self.timezone)

        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.wolfram = None
        if 'wolfram' in config['main']:
            try:
                import wolframalpha
                self.wolfram = wolframalpha.Client(config['main']['wolfram'])
            except ImportError:
                pass

        self.silent = False
        self.dev = False

        if 'flags' in config:
            self.silent = True if 'silent' in config['flags'] and config['flags']['silent'] == '1' else self.silent
            self.dev = True if 'dev' in config['flags'] and config['flags']['dev'] == '1' else self.dev

        DBManager.init(self.config['main']['db'])

        redis_options = {}
        if 'redis' in config:
            redis_options = config._sections['redis']

        RedisManager.init(**redis_options)

    def __init__(self, config, args=None):
        # Load various configuration variables from the given config object
        # The config object that should be passed through should
        # come from pajbot.utils.load_config
        self.load_config(config)

        # Update the database scheme if necessary using alembic
        # In case of errors, i.e. if the database is out of sync or the alembic
        # binary can't be called, we will shut down the bot.
        pajbot.utils.alembic_upgrade()

        # 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 = datetime.datetime.now()
        ActionParser.bot = self

        HandlerManager.init_handlers()

        self.socket_manager = SocketManager(self)
        self.stream_manager = StreamManager(self)

        StreamHelper.init_bot(self, self.stream_manager)
        ScheduleManager.init()

        self.users = UserManager()
        self.decks = DeckManager()
        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.filters = FilterManager().reload()
        self.banphrase_manager = BanphraseManager(self).load()
        self.timer_manager = TimerManager(self).load()
        self.kvi = KVIManager()
        self.emotes = EmoteManager(self)
        self.twitter_manager = TwitterManager(self)

        HandlerManager.trigger('on_managers_loaded')

        # Reloadable managers
        self.reloadable = {
                'filters': self.filters,
                }

        # Commitable managers
        self.commitable = {
                'commands': self.commands,
                'filters': self.filters,
                'banphrases': self.banphrase_manager,
                }

        self.execute_every(10 * 60, self.commit_all)
        self.execute_every(1, self.do_tick)

        try:
            self.admin = self.config['main']['admin']
        except KeyError:
            log.warning('No admin user specified. See the [main] section in config.example.ini for its usage.')
        if self.admin:
            with self.users.get_user_context(self.admin) as user:
                user.level = 2000

        self.parse_version()

        relay_host = self.config['main'].get('relay_host', None)
        relay_password = self.config['main'].get('relay_password', None)
        if relay_host is None or relay_password is None:
            self.irc = MultiIRCManager(self)
        else:
            self.irc = SingleIRCManager(self)

        self.reactor.add_global_handler('all_events', self.irc._dispatcher, -10)

        twitch_client_id = None
        twitch_oauth = None
        if 'twitchapi' in self.config:
            twitch_client_id = self.config['twitchapi'].get('client_id', None)
            twitch_oauth = self.config['twitchapi'].get('oauth', None)

        # A client ID is required for the bot to work properly now, give an error for now
        if twitch_client_id is None:
            log.error('MISSING CLIENT ID, SET "client_id" VALUE UNDER [twitchapi] SECTION IN CONFIG FILE')

        self.twitchapi = TwitchAPI(twitch_client_id, twitch_oauth)

        self.data = {}
        self.data_cb = {}
        self.url_regex = re.compile(self.url_regex_str, re.IGNORECASE)

        self.data['broadcaster'] = self.streamer
        self.data['version'] = self.version
        self.data['version_brief'] = self.version_brief
        self.data['bot_name'] = self.nickname
        self.data_cb['status_length'] = self.c_status_length
        self.data_cb['stream_status'] = self.c_stream_status
        self.data_cb['bot_uptime'] = self.c_uptime
        self.data_cb['current_time'] = self.c_current_time

        self.silent = True if args.silent else self.silent

        if self.silent:
            log.info('Silent mode enabled')

        """
        For actions that need to access the main thread,
        we can use the mainthread_queue.
        """
        self.mainthread_queue = ActionQueue()
        self.execute_every(1, self.mainthread_queue.parse_action)

        self.websocket_manager = WebSocketManager(self)

        try:
            if self.config['twitchapi']['update_subscribers'] == '1':
                self.execute_every(30 * 60,
                                   self.action_queue.add,
                                   (self.update_subscribers_stage1, ))
        except:
            pass

        # XXX: TEMPORARY UGLY CODE
        HandlerManager.add_handler('on_user_gain_tokens', self.on_user_gain_tokens)
        HandlerManager.add_handler('send_whisper', self.whisper)

    def on_connect(self, sock):
        return self.irc.on_connect(sock)

    def on_user_gain_tokens(self, user, tokens_gained):
        self.whisper(user.username, 'You finished todays quest! You have been awarded with {} tokens.'.format(tokens_gained))

    def update_subscribers_stage1(self):
        limit = 100
        offset = 0
        subscribers = []
        log.info('Starting stage1 subscribers update')

        try:
            retry_same = 0
            while True:
                log.debug('Beginning sub request {0} {1}'.format(limit, offset))
                subs, retry_same, error = self.twitchapi.get_subscribers(self.streamer, limit, offset, 0 if retry_same is False else retry_same)
                log.debug('got em!')

                if error is True:
                    log.error('Too many attempts, aborting')
                    return False

                if retry_same is False:
                    offset += limit
                    if len(subs) == 0:
                        # We don't need to retry, and the last query finished propery
                        # Break out of the loop and start fiddling with the subs!
                        log.debug('Done!')
                        break
                    else:
                        log.debug('Fetched {0} subs'.format(len(subs)))
                        subscribers.extend(subs)

                if retry_same is not False:
                    # In case the next attempt is a retry, wait for 3 seconds
                    log.debug('waiting for 3 seconds...')
                    time.sleep(3)
                    log.debug('waited enough!')
            log.debug('Finished with the while True loop!')
        except:
            log.exception('Caught an exception while trying to get subscribers')
            return

        log.info('Ended stage1 subscribers update')
        if len(subscribers) > 0:
            log.info('Got some subscribers, so we are pushing them to stage 2!')
            self.mainthread_queue.add(self.update_subscribers_stage2,
                                      args=[subscribers])
            log.info('Pushed them now.')

    def update_subscribers_stage2(self, subscribers):
        self.kvi['active_subs'].set(len(subscribers) - 1)

        self.users.reset_subs()

        self.users.update_subs(subscribers)

    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_tm(self, key, extra={}):
        val = self.emotes.get_emote_epm(key)
        if not val:
            return None
        return '{0:,d}'.format(val)

    def get_emote_count(self, key, extra={}):
        val = self.emotes.get_emote_count(key)
        if not val:
            return None
        return '{0:,d}'.format(val)

    def get_emote_tm_record(self, key, extra={}):
        val = self.emotes.get_emote_epmrecord(key)
        if not val:
            return None
        return '{0:,d}'.format(val)

    def get_source_value(self, 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

    def get_command_value(self, 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)
            else:
                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(self.date_fmt)
        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 len(ret) == 0:
            return None
        return ret

    def get_args_value(self, key, extra={}):
        range = 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:
                    range = (int(range_str[0]), int(range_str[1]))

            if range is None:
                range = (int(key), len(msg_parts))
        except (TypeError, ValueError):
            range = (0, len(msg_parts))

        try:
            return ' '.join(msg_parts[range[0]:range[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]
        elif key in self.data:
            return self.data[key]
        elif key in self.data_cb:
            return self.data_cb[key]()

        log.warning('Unknown key passed to get_value: {0}'.format(key))
        return None

    def privmsg_arr(self, arr):
        for msg in arr:
            self.privmsg(msg)

    def privmsg_from_file(self, url, per_chunk=35, chunk_delay=30):
        try:
            r = requests.get(url)
            lines = r.text.split('\n')
            i = 0
            while len(lines) > 0:
                if i == 0:
                    self.privmsg_arr(lines[:per_chunk])
                else:
                    self.execute_delayed(chunk_delay * i, self.privmsg_arr, (lines[:per_chunk],))

                del lines[:per_chunk]

                i = i + 1
        except:
            log.exception('error in privmsg_from_file')

    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 time_since(datetime.datetime.now().timestamp(), self.start_time.timestamp())

    def c_current_time(self):
        return datetime.datetime.now()

    @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 time_since(time.time(), self.stream_manager.current_stream.stream_start.timestamp())
        else:
            if self.stream_manager.last_stream is not None:
                return time_since(time.time(), self.stream_manager.last_stream.stream_end.timestamp())
            else:
                return 'No recorded stream FeelsBadMan '

    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=''):
        log.debug('Banning {}'.format(username))
        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=''):
        log.debug('Timing out {} for {} seconds'.format(username, duration))
        self._timeout(username, duration, reason)
        self.execute_delayed(1, 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)
        self.execute_delayed(1, self._timeout, (user.username, duration, reason))

    def whisper(self, username, *messages, separator='. '):
        """
        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, 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)
        else:
            log.warning('Unknown send_message method: {}'.format(method))

    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()

            if len(message) >= 1:
                if (message[0] == '.' or message[0] == '/') and not message[1:3] == 'me':
                    log.warning('Message we attempted to send started with . or /, skipping.')
                    return

                log.info('Sending message: {0}'.format(message))

                self.privmsg(message[:510], channel)

    def me(self, message, channel=None):
        if not self.silent:
            message = message.strip()

            if len(message) >= 1:
                if message[0] == '.' or message[0] == '/':
                    log.warning('Message we attempted to send started with . or /, skipping.')
                    return

                log.info('Sending message: {0}'.format(message))

                self.privmsg('.me ' + message[:500], channel)

    def parse_version(self):
        self.version = self.version
        self.version_brief = self.version

        if self.dev:
            try:
                current_branch = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD']).decode('utf8').strip()
                latest_commit = subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('utf8').strip()[:8]
                commit_number = subprocess.check_output(['git', 'rev-list', 'HEAD', '--count']).decode('utf8').strip()
                self.version = '{0} DEV ({1}, {2}, commit {3})'.format(self.version, current_branch, latest_commit, commit_number)
            except:
                pass

    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, msg_raw, source, event, tags={}, whisper=False):
        msg_lower = msg_raw.lower()

        emote_tag = None

        for tag in tags:
            if tag['key'] == 'subscriber' and event.target == self.channel:
                source.subscriber = tag['value'] == '1'
            elif tag['key'] == 'emotes' and tag['value']:
                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

        # 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
        message_emotes = self.emotes.parse_message_twitch_emotes(source, msg_raw, emote_tag, whisper)

        urls = self.find_unique_urls(msg_raw)

        log.debug('{2}{0}: {1}'.format(source.username, msg_raw, '<w>' if whisper else ''))

        res = HandlerManager.trigger('on_message',
                source, msg_raw, message_emotes, whisper, urls, event,
                stop_on_false=True)
        if res is False:
            return False

        source.last_seen = datetime.datetime.now()
        source.last_active = datetime.datetime.now()

        """
        if self.streamer == 'forsenlol' and whisper is False:
            follow_time = self.twitchapi.get_follow_relationship2(source.username, self.streamer)

            if follow_time is False:
                self._timeout(source.username, 600, '2 years follow mode (or api is down?)')
                return

            follow_age = datetime.datetime.now() - follow_time
            log.debug(follow_age)

            if follow_age.days < 730:
                log.debug('followed less than 730 days LUL')
                self._timeout(source.username, 600, '2 years follow mode')
                """

        if source.ignored:
            return False

        if msg_lower[:1] == '!':
            msg_lower_parts = msg_lower.split(' ')
            trigger = msg_lower_parts[0][1:]
            msg_raw_parts = msg_raw.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 = {
                        'emotes': message_emotes,
                        'trigger': trigger,
                        }
                command.run(self, source, remaining_message, event=event, args=extra_args, whisper=whisper)

    @time_method
    def redis_test(self, username):
        try:
            pipeline = RedisManager.get().pipeline()
            pipeline.hget('pajlada:users:points', username)
            pipeline.hget('pajlada:users:ignored', username)
            pipeline.hget('pajlada:users:banned', username)
            pipeline.hget('pajlada:users:last_active', username)
        except:
            pipeline.reset()
        finally:
            b = pipeline.execute()
            log.info(b)

    @time_method
    def redis_test2(self, username):
        redis = RedisManager.get()
        values = [
                redis.hget('pajlada:users:points', username),
                redis.hget('pajlada:users:ignored', username),
                redis.hget('pajlada:users:banned', username),
                redis.hget('pajlada:users:last_active', username),
                ]
        return values

    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.say('Received a ping. Last ping received {} ago'.format(time_since(datetime.datetime.now().timestamp(), self.last_ping.timestamp())))
        log.info('Received a ping. Last ping received {} ago'.format(time_since(datetime.datetime.now().timestamp(), self.last_ping.timestamp())))
        self.last_ping = datetime.datetime.now()

    def on_pong(self, chatconn, event):
        # self.say('Received a pong. Last pong received {} ago'.format(time_since(datetime.datetime.now().timestamp(), self.last_pong.timestamp())))
        log.info('Received a pong. Last pong received {} ago'.format(time_since(datetime.datetime.now().timestamp(), self.last_pong.timestamp())))
        self.last_pong = datetime.datetime.now()

    def on_pubnotice(self, chatconn, event):
        return
        type = 'whisper' if chatconn in self.whisper_manager else 'normal'
        log.debug('NOTICE {}@{}: {}'.format(type, event.target, event.arguments))

    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 len(event.arguments) > 0:
                msg = event.arguments[0]
            HandlerManager.trigger('on_usernotice',
                    source, msg, 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, event.arguments[0],
                    stop_on_false=True)
            if res is False:
                return False

            self.parse_message(event.arguments[0], source, event, tags=event.tags)

    @time_method
    def reload_all(self):
        log.info('Reloading all...')
        for key, manager in self.reloadable.items():
            log.debug('Reloading {0}'.format(key))
            manager.reload()
            log.debug('Done with {0}'.format(key))
        log.info('ok!')

    @time_method
    def commit_all(self):
        log.info('Commiting all...')
        for key, manager in self.commitable.items():
            log.info('Commiting {0}'.format(key))
            manager.commit()
            log.info('Done with {0}'.format(key))
        log.info('ok!')

        HandlerManager.trigger('on_commit', stop_on_false=False)

    def do_tick(self):
        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('{} is restarting in {} seconds.'.format(self.nickname, quit_delay))

        self.execute_delayed(quit_delay, self.quit_bot)

    def quit_bot(self, **options):
        self.commit_all()
        quit = '{nickname} {version} shutting down...'
        phrase_data = {
                'nickname': self.nickname,
                'version': self.version,
                }

        try:
            ScheduleManager.base_scheduler.print_jobs()
            ScheduleManager.base_scheduler.shutdown(wait=False)
        except:
            log.exception('Error while shutting down the apscheduler')

        try:
            self.say(quit.format(**phrase_data))
        except Exception:
            log.exception('Exception caught while trying to say quit phrase')

        self.twitter_manager.quit()
        self.socket_manager.quit()
        self.irc.quit()

        sys.exit(0)

    def apply_filter(self, resp, filter):
        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 time_since(var * 60, 0, format='long'),
                'time_since': lambda var, args: 'no time' if var == 0 else time_since(var, 0, format='long'),
                'time_since_dt': _filter_time_since_dt,
                'urlencode': _filter_urlencode,
                'join': _filter_join,
                'number_format': _filter_number_format,
                }
        if filter.name in available_filters:
            return available_filters[filter.name](resp, filter.arguments)
        return resp

    def find_unique_urls(self, message):
        from pajbot.modules.linkchecker import find_unique_urls
        return find_unique_urls(self.url_regex, message)
Example #15
0
File: bot.py Project: jardg/pajbot
    def __init__(self, config, args=None):
        # Load various configuration variables from the given config object
        # The config object that should be passed through should
        # come from pajbot.utils.load_config
        self.load_config(config)

        # Update the database scheme if necessary using alembic
        # In case of errors, i.e. if the database is out of sync or the alembic
        # binary can't be called, we will shut down the bot.
        pajbot.utils.alembic_upgrade()

        # 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 = datetime.datetime.now()
        ActionParser.bot = self

        HandlerManager.init_handlers()

        self.socket_manager = SocketManager(self)
        self.stream_manager = StreamManager(self)

        StreamHelper.init_bot(self, self.stream_manager)
        ScheduleManager.init()

        self.users = UserManager()
        self.decks = DeckManager()
        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.filters = FilterManager().reload()
        self.banphrase_manager = BanphraseManager(self).load()
        self.timer_manager = TimerManager(self).load()
        self.kvi = KVIManager()
        self.emotes = EmoteManager(self)
        self.twitter_manager = TwitterManager(self)

        HandlerManager.trigger('on_managers_loaded')

        # Reloadable managers
        self.reloadable = {
                'filters': self.filters,
                }

        # Commitable managers
        self.commitable = {
                'commands': self.commands,
                'filters': self.filters,
                'banphrases': self.banphrase_manager,
                }

        self.execute_every(10 * 60, self.commit_all)
        self.execute_every(1, self.do_tick)

        try:
            self.admin = self.config['main']['admin']
        except KeyError:
            log.warning('No admin user specified. See the [main] section in config.example.ini for its usage.')
        if self.admin:
            with self.users.get_user_context(self.admin) as user:
                user.level = 2000

        self.parse_version()

        relay_host = self.config['main'].get('relay_host', None)
        relay_password = self.config['main'].get('relay_password', None)
        if relay_host is None or relay_password is None:
            self.irc = MultiIRCManager(self)
        else:
            self.irc = SingleIRCManager(self)

        self.reactor.add_global_handler('all_events', self.irc._dispatcher, -10)

        twitch_client_id = None
        twitch_oauth = None
        if 'twitchapi' in self.config:
            twitch_client_id = self.config['twitchapi'].get('client_id', None)
            twitch_oauth = self.config['twitchapi'].get('oauth', None)

        # A client ID is required for the bot to work properly now, give an error for now
        if twitch_client_id is None:
            log.error('MISSING CLIENT ID, SET "client_id" VALUE UNDER [twitchapi] SECTION IN CONFIG FILE')

        self.twitchapi = TwitchAPI(twitch_client_id, twitch_oauth)

        self.data = {}
        self.data_cb = {}
        self.url_regex = re.compile(self.url_regex_str, re.IGNORECASE)

        self.data['broadcaster'] = self.streamer
        self.data['version'] = self.version
        self.data['version_brief'] = self.version_brief
        self.data['bot_name'] = self.nickname
        self.data_cb['status_length'] = self.c_status_length
        self.data_cb['stream_status'] = self.c_stream_status
        self.data_cb['bot_uptime'] = self.c_uptime
        self.data_cb['current_time'] = self.c_current_time

        self.silent = True if args.silent else self.silent

        if self.silent:
            log.info('Silent mode enabled')

        """
        For actions that need to access the main thread,
        we can use the mainthread_queue.
        """
        self.mainthread_queue = ActionQueue()
        self.execute_every(1, self.mainthread_queue.parse_action)

        self.websocket_manager = WebSocketManager(self)

        try:
            if self.config['twitchapi']['update_subscribers'] == '1':
                self.execute_every(30 * 60,
                                   self.action_queue.add,
                                   (self.update_subscribers_stage1, ))
        except:
            pass

        # XXX: TEMPORARY UGLY CODE
        HandlerManager.add_handler('on_user_gain_tokens', self.on_user_gain_tokens)
        HandlerManager.add_handler('send_whisper', self.whisper)
Example #16
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,
        }
Example #17
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)
Example #18
0
    def update_chatters_stage2(self, chatters):
        points = 1 if self.bot.is_online else 0

        log.debug('Updating {0} chatters'.format(len(chatters)))

        self.bot.stream_manager.update_chatters(chatters,
                                                self.update_chatters_interval)

        with RedisManager.pipeline_context() as pipeline:
            with DBManager.create_session_scope() as db_session:
                user_models = UserManager.get().bulk_load_user_models(
                    chatters, db_session)
                users = []
                for username in chatters:
                    user_model = user_models.get(username, None)
                    user = UserManager.get().get_user(username,
                                                      db_session=db_session,
                                                      user_model=user_model,
                                                      redis=pipeline)
                    users.append(user)

                more_update_data = {}
                if self.bot.is_online:
                    more_update_data[
                        'minutes_in_chat_online'] = self.update_chatters_interval
                else:
                    more_update_data[
                        'minutes_in_chat_offline'] = self.update_chatters_interval

                points_to_give_out = {}
                dt_now = datetime.datetime.now().timestamp()
                for user in users:
                    user._set_last_seen(dt_now)

                    num_points = points
                    if user.subscriber:
                        num_points *= 5
                    # TODO: Load user tags during the pipeline redis data fetch
                    if self.bot.streamer == 'forsenlol' and 'trumpsc_sub' in user.get_tags(
                    ):
                        num_points *= 0.5

                    num_points = int(num_points)

                    if num_points not in points_to_give_out:
                        points_to_give_out[num_points] = []

                    points_to_give_out[num_points].append(user.username)

                    user.save(save_to_db=False)

                for num_points, usernames in points_to_give_out.items():
                    payload = {
                        User.points: User.points + num_points,
                    }
                    if self.bot.is_online:
                        payload[
                            User.
                            minutes_in_chat_online] = User.minutes_in_chat_online + self.update_chatters_interval
                    else:
                        payload[
                            User.
                            minutes_in_chat_offline] = User.minutes_in_chat_offline + self.update_chatters_interval
                    db_session.query(User).filter(User.username.in_(usernames)).\
                            update(payload, synchronize_session=False)

                pipeline.execute()
Example #19
0
    def user_profile(username):
        with DBManager.create_session_scope() as db_session:
            user = UserManager.find_static(username, db_session=db_session)
            if not user:
                return render_template('no_user.html'), 404

            roulettes = db_session.query(Roulette).filter_by(user_id=user.id).order_by(Roulette.created_at.desc()).all()

            roulette_stats = None
            if len(roulettes) > 0:
                profit = 0
                total_points = 0
                biggest_loss = 0
                biggest_win = 0
                biggest_winstreak = 0
                biggest_losestreak = 0
                num_wins = 0
                num_losses = 0
                winrate = 0
                num_roulettes = len(roulettes)
                cw = 0
                for roulette in roulettes:
                    profit += roulette.points
                    total_points += abs(roulette.points)

                    if roulette.points > 0:
                        # a win!
                        num_wins += 1
                        if cw < 0:
                            if abs(cw) > biggest_losestreak:
                                biggest_losestreak = abs(cw)
                            cw = 0
                        cw += 1
                    else:
                        # a loss
                        num_losses += 1
                        if cw > 0:
                            if cw > biggest_winstreak:
                                biggest_winstreak = cw
                            cw = 0
                        cw -= 1

                    if roulette.points < biggest_loss:
                        biggest_loss = roulette.points
                    elif roulette.points > biggest_win:
                        biggest_win = roulette.points

                # Calculate winrate
                if num_losses == 0:
                    winrate = 1
                elif num_wins == 0:
                    winrate = 0
                else:
                    winrate = num_wins / num_roulettes

                # Finalize win/lose streaks in case we're currently
                # on the biggest win/lose streak
                if cw < 0:
                    if abs(cw) > biggest_losestreak:
                        biggest_losestreak = abs(cw)
                elif cw > 0:
                    if cw > biggest_winstreak:
                        biggest_winstreak = cw

                if 'roulette' in app.module_manager:
                    roulette_base_winrate = 1.0 - app.module_manager['roulette'].settings['rigged_percentage'] / 100
                else:
                    roulette_base_winrate = 0.45

                roulette_stats = {
                        'profit': profit,
                        'total_points': total_points,
                        'biggest_win': biggest_win,
                        'biggest_loss': biggest_loss,
                        'num_roulettes': num_roulettes,
                        'biggest_winstreak': biggest_winstreak,
                        'biggest_losestreak': biggest_losestreak,
                        'winrate': winrate,
                        'winrate_str': '{:.2f}%'.format(winrate * 100),
                        'roulette_base_winrate': roulette_base_winrate,
                        }

            return render_template('user.html',
                    user=user,
                    roulette_stats=roulette_stats,
                    roulettes=roulettes)
Example #20
0
    def user_profile(username):
        with DBManager.create_session_scope() as db_session:
            user = UserManager.find_static(username, db_session=db_session)
            if not user:
                return render_template('no_user.html'), 404

            roulettes = db_session.query(Roulette).filter_by(
                user_id=user.id).order_by(Roulette.created_at.desc()).all()

            roulette_stats = None
            if len(roulettes) > 0:
                profit = 0
                total_points = 0
                biggest_loss = 0
                biggest_win = 0
                biggest_winstreak = 0
                biggest_losestreak = 0
                num_wins = 0
                num_losses = 0
                winrate = 0
                num_roulettes = len(roulettes)
                cw = 0
                for roulette in roulettes:
                    profit += roulette.points
                    total_points += abs(roulette.points)

                    if roulette.points > 0:
                        # a win!
                        num_wins += 1
                        if cw < 0:
                            if abs(cw) > biggest_losestreak:
                                biggest_losestreak = abs(cw)
                            cw = 0
                        cw += 1
                    else:
                        # a loss
                        num_losses += 1
                        if cw > 0:
                            if cw > biggest_winstreak:
                                biggest_winstreak = cw
                            cw = 0
                        cw -= 1

                    if roulette.points < biggest_loss:
                        biggest_loss = roulette.points
                    elif roulette.points > biggest_win:
                        biggest_win = roulette.points

                # Calculate winrate
                if num_losses == 0:
                    winrate = 1
                elif num_wins == 0:
                    winrate = 0
                else:
                    winrate = num_wins / num_roulettes

                # Finalize win/lose streaks in case we're currently
                # on the biggest win/lose streak
                if cw < 0:
                    if abs(cw) > biggest_losestreak:
                        biggest_losestreak = abs(cw)
                elif cw > 0:
                    if cw > biggest_winstreak:
                        biggest_winstreak = cw

                if 'roulette' in app.module_manager:
                    roulette_base_winrate = 1.0 - app.module_manager[
                        'roulette'].settings['rigged_percentage'] / 100
                else:
                    roulette_base_winrate = 0.45

                roulette_stats = {
                    'profit': profit,
                    'total_points': total_points,
                    'biggest_win': biggest_win,
                    'biggest_loss': biggest_loss,
                    'num_roulettes': num_roulettes,
                    'biggest_winstreak': biggest_winstreak,
                    'biggest_losestreak': biggest_losestreak,
                    'winrate': winrate,
                    'winrate_str': '{:.2f}%'.format(winrate * 100),
                    'roulette_base_winrate': roulette_base_winrate,
                }

            return render_template('user.html',
                                   user=user,
                                   roulette_stats=roulette_stats,
                                   roulettes=roulettes)
Example #21
0
class Bot:
    """
    Main class for the twitch bot
    """

    version = "1.35"
    date_fmt = "%H:%M"
    admin = None
    url_regex_str = r"\(?(?:(http|https):\/\/)?(?:((?:[^\W\s]|\.|-|[:]{1})+)@{1})?((?:www.)?(?:[^\W\s]|\.|-)+[\.][^\W\s]{2,4}|localhost(?=\/)|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::(\d*))?([\/]?[^\s\?]*[\/]{1})*(?:\/?([^\s\n\?\[\]\{\}\#]*(?:(?=\.)){1}|[^\s\n\?\[\]\{\}\.\#]*)?([\.]{1}[^\s\?\#]*)?)?(?:\?{1}([^\s\n\#\[\]]*))?([\#][^\s\n]*)?\)?"

    last_ping = pajbot.utils.now()
    last_pong = pajbot.utils.now()

    @staticmethod
    def parse_args():
        parser = argparse.ArgumentParser()
        parser.add_argument(
            "--config",
            "-c",
            default="config.ini",
            help="Specify which config file to use (default: config.ini)")
        parser.add_argument(
            "--silent",
            action="count",
            help="Decides whether the bot should be silent or not")
        # TODO: Add a log level argument.

        return parser.parse_args()

    bot_token = None

    @property
    def password(self):
        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")
            return self.config["main"]["password"]

        if self.bot_token is None:
            self.bot_token = BotToken(self.config)

        t = self.bot_token.access_token()
        return "oauth:{}".format(t)

    def load_config(self, config):
        self.config = config

        DBManager.init(self.config["main"]["db"])

        redis_options = {}
        if "redis" in config:
            redis_options = dict(config.items("redis"))

        RedisManager.init(**redis_options)

        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.domain = config["web"].get("domain", "localhost")

        self.nickname = config["main"].get("nickname", "pajbot")

        self.timezone = config["main"].get("timezone", "UTC")

        if config["main"].getboolean("verified", False):
            TMI.promote_to_verified()

        self.trusted_mods = config.getboolean("main", "trusted_mods")

        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)

        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)

        self.silent = False
        self.dev = False

        if "flags" in config:
            self.silent = True if "silent" in config["flags"] and config[
                "flags"]["silent"] == "1" else self.silent
            self.dev = True if "dev" in config["flags"] and config["flags"][
                "dev"] == "1" else self.dev

    def __init__(self, config, args=None):
        # Load various configuration variables from the given config object
        # The config object that should be passed through should
        # come from pajbot.utils.load_config
        self.load_config(config)
        log.debug("Loaded config")

        # streamer is additionally initialized here so streamer can be accessed by the DB migrations
        # before StreamHelper.init_bot() is called later (which depends on an upgraded DB because
        # StreamManager accesses the DB)
        StreamHelper.init_streamer(self.streamer)

        # Update the database (and partially redis) scheme if necessary using alembic
        # In case of errors, i.e. if the database is out of sync or the alembic
        # binary can't be called, we will shut down the bot.
        pajbot.utils.alembic_upgrade()
        log.debug("ran db upgrade")

        # 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 = pajbot.utils.now()
        ActionParser.bot = self

        HandlerManager.init_handlers()

        self.socket_manager = SocketManager(self.streamer)
        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()

        twitch_client_id = None
        twitch_oauth = None
        if "twitchapi" in self.config:
            twitch_client_id = self.config["twitchapi"].get("client_id", None)
            twitch_oauth = self.config["twitchapi"].get("oauth", None)

        # A client ID is required for the bot to work properly now, give an error for now
        if twitch_client_id is None:
            log.error(
                'MISSING CLIENT ID, SET "client_id" VALUE UNDER [twitchapi] SECTION IN CONFIG FILE'
            )

        self.twitchapi = TwitchAPI(twitch_client_id, twitch_oauth)
        self.emote_manager = EmoteManager(twitch_client_id)
        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()

        HandlerManager.trigger("on_managers_loaded")

        # Reloadable managers
        self.reloadable = {}

        # 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)

        try:
            self.admin = self.config["main"]["admin"]
        except KeyError:
            log.warning(
                "No admin user specified. See the [main] section in config.example.ini for its usage."
            )
        if self.admin:
            with self.users.get_user_context(self.admin) as user:
                user.level = 2000

        self.parse_version()

        relay_host = self.config["main"].get("relay_host", None)
        relay_password = self.config["main"].get("relay_password", None)
        if relay_host is None or relay_password is None:
            self.irc = MultiIRCManager(self)
        else:
            self.irc = SingleIRCManager(self)

        self.reactor.add_global_handler("all_events", self.irc._dispatcher,
                                        -10)

        self.data = {}
        self.data_cb = {}
        self.url_regex = re.compile(self.url_regex_str, re.IGNORECASE)

        self.data["broadcaster"] = self.streamer
        self.data["version"] = self.version
        self.data["version_brief"] = self.version_brief
        self.data["bot_name"] = self.nickname
        self.data_cb["status_length"] = self.c_status_length
        self.data_cb["stream_status"] = self.c_stream_status
        self.data_cb["bot_uptime"] = self.c_uptime
        self.data_cb["current_time"] = self.c_current_time

        self.silent = True if args.silent else self.silent

        if self.silent:
            log.info("Silent mode enabled")
        """
        For actions that need to access the main thread,
        we can use the mainthread_queue.
        """
        self.mainthread_queue = ActionQueue()
        self.execute_every(1, self.mainthread_queue.parse_action)

        self.websocket_manager = WebSocketManager(self)

        try:
            if self.config["twitchapi"]["update_subscribers"] == "1":
                self.execute_every(30 * 60, self.action_queue.add,
                                   (self.update_subscribers_stage1, ))
        except:
            pass

    def on_connect(self, sock):
        return self.irc.on_connect(sock)

    def update_subscribers_stage1(self):
        limit = 25
        offset = 0
        subscribers = []
        log.info("Starting stage1 subscribers update")

        try:
            retry_same = 0
            while True:
                log.debug("Beginning sub request limit=%s offset=%s", limit,
                          offset)
                subs, retry_same, error = self.twitchapi.get_subscribers(
                    self.streamer, 0, offset,
                    0 if retry_same is False else retry_same)
                log.debug("got em!")

                if error is True:
                    log.error("Too many attempts, aborting")
                    return False

                if retry_same is False:
                    offset += limit
                    if not subs:
                        # We don't need to retry, and the last query finished propery
                        # Break out of the loop and start fiddling with the subs!
                        log.debug("Done!")
                        break

                    log.debug("Fetched %d subs", len(subs))
                    subscribers.extend(subs)

                if retry_same is not False:
                    # In case the next attempt is a retry, wait for 3 seconds
                    log.debug("waiting for 3 seconds...")
                    time.sleep(3)
                    log.debug("waited enough!")
            log.debug("Finished with the while True loop!")
        except:
            log.exception(
                "Caught an exception while trying to get subscribers")
            return None

        log.info("Ended stage1 subscribers update")
        if subscribers:
            log.info(
                "Got some subscribers, so we are pushing them to stage 2!")
            self.mainthread_queue.add(self.update_subscribers_stage2,
                                      args=[subscribers])
            log.info("Pushed them now.")

    def update_subscribers_stage2(self, subscribers):
        self.kvi["active_subs"].set(len(subscribers) - 1)

        self.users.reset_subs()

        self.users.update_subs(subscribers)

    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={}):
        val = self.epm_manager.get_emote_epm(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_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(self.date_fmt)
        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(
                    "privmsg_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 time_ago(self.start_time)

    @staticmethod
    def c_current_time():
        return pajbot.utils.now()

    @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 time_ago(self.stream_manager.current_stream.stream_start)

        if self.stream_manager.last_stream is not None:
            return time_ago(self.stream_manager.last_stream.stream_end)

        return "No recorded stream FeelsBadMan "

    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=""):
        log.debug("Banning %s", username)
        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=""):
        # log.debug("Timing out %s for %d seconds", username, duration)
        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(self, user, duration, reason=""):
        self._timeout(user.username, duration, reason)

    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 = clean_up_message(message)
            if not message:
                return False

            # log.info("Sending message: %s", 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 parse_version(self):
        self.version = self.version
        self.version_brief = self.version

        if self.dev:
            try:
                current_branch = (subprocess.check_output(
                    ["git", "rev-parse", "--abbrev-ref",
                     "HEAD"]).decode("utf8").strip())
                latest_commit = subprocess.check_output(
                    ["git", "rev-parse", "HEAD"]).decode("utf8").strip()[:8]
                commit_number = subprocess.check_output(
                    ["git", "rev-list", "HEAD",
                     "--count"]).decode("utf8").strip()
                self.version = "{0} DEV ({1}, {2}, commit {3})".format(
                    self.version, current_branch, latest_commit, commit_number)
            except:
                log.exception("hmm")

    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

        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

        # 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)

        self.epm_manager.handle_emotes(emote_counts)
        self.ecount_manager.handle_emotes(emote_counts)

        urls = self.find_unique_urls(message)

        # log.debug("{2}{0}: {1}".format(source.username, message, "<w>" if whisper else ""))

        res = HandlerManager.trigger(
            "on_message",
            source=source,
            message=message,
            emote_instances=emote_instances,
            emote_counts=emote_counts,
            whisper=whisper,
            urls=urls,
            event=event,
        )
        if res is False:
            return False

        source.last_seen = pajbot.utils.now()
        source.last_active = pajbot.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
                }
                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.say('Received a ping. Last ping received {} ago'.format(time_since(pajbot.utils.now().timestamp(), self.last_ping.timestamp())))
        log.info("Received a ping. Last ping received %s ago",
                 time_ago(self.last_ping))
        self.last_ping = pajbot.utils.now()

    def on_pong(self, chatconn, event):
        # self.say('Received a pong. Last pong received {} ago'.format(time_since(pajbot.utils.now().timestamp(), self.last_pong.timestamp())))
        log.info("Received a pong. Last pong received %s ago",
                 time_ago(self.last_pong))
        self.last_pong = pajbot.utils.now()

    def on_pubnotice(self, chatconn, event):
        return
        type = "whisper" if chatconn in self.whisper_manager else "normal"
        log.debug("NOTICE {}@{}: {}".format(type, event.target,
                                            event.arguments))

    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 reload_all(self):
        log.info("Reloading all...")
        for key, manager in self.reloadable.items():
            log.debug("Reloading %s", key)
            manager.reload()
            log.debug("Done with %s", key)
        log.info("ok!")

    @time_method
    def commit_all(self):
        # log.info("Commiting all...")
        for key, manager in self.commitable.items():
            # log.info("Commiting %s", key)
            manager.commit()
            # log.info("Done with %s", key)
        # log.info("ok!")

        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):
        HandlerManager.trigger("on_quit")
        self.commit_all()
        phrase_data = {"nickname": self.nickname, "version": self.version}

        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"]:
                if not self.silent:
                    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()
        self.irc.quit()

        sys.exit(0)

    @staticmethod
    def apply_filter(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 time_since(var * 60, 0, time_format="long"),
            "time_since":
            lambda var, args: "no time"
            if var == 0 else 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,
        }
        if f.name in available_filters:
            return available_filters[f.name](resp, f.arguments)
        return resp

    def find_unique_urls(self, message):
        from pajbot.modules.linkchecker import find_unique_urls

        return find_unique_urls(self.url_regex, message)
Example #22
0
    def __init__(self, config, args=None):
        # Load various configuration variables from the given config object
        # The config object that should be passed through should
        # come from pajbot.utils.load_config
        self.load_config(config)
        log.debug("Loaded config")

        # streamer is additionally initialized here so streamer can be accessed by the DB migrations
        # before StreamHelper.init_bot() is called later (which depends on an upgraded DB because
        # StreamManager accesses the DB)
        StreamHelper.init_streamer(self.streamer)

        # Update the database (and partially redis) scheme if necessary using alembic
        # In case of errors, i.e. if the database is out of sync or the alembic
        # binary can't be called, we will shut down the bot.
        pajbot.utils.alembic_upgrade()
        log.debug("ran db upgrade")

        # 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 = pajbot.utils.now()
        ActionParser.bot = self

        HandlerManager.init_handlers()

        self.socket_manager = SocketManager(self.streamer)
        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()

        twitch_client_id = None
        twitch_oauth = None
        if "twitchapi" in self.config:
            twitch_client_id = self.config["twitchapi"].get("client_id", None)
            twitch_oauth = self.config["twitchapi"].get("oauth", None)

        # A client ID is required for the bot to work properly now, give an error for now
        if twitch_client_id is None:
            log.error(
                'MISSING CLIENT ID, SET "client_id" VALUE UNDER [twitchapi] SECTION IN CONFIG FILE'
            )

        self.twitchapi = TwitchAPI(twitch_client_id, twitch_oauth)
        self.emote_manager = EmoteManager(twitch_client_id)
        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()

        HandlerManager.trigger("on_managers_loaded")

        # Reloadable managers
        self.reloadable = {}

        # 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)

        try:
            self.admin = self.config["main"]["admin"]
        except KeyError:
            log.warning(
                "No admin user specified. See the [main] section in config.example.ini for its usage."
            )
        if self.admin:
            with self.users.get_user_context(self.admin) as user:
                user.level = 2000

        self.parse_version()

        relay_host = self.config["main"].get("relay_host", None)
        relay_password = self.config["main"].get("relay_password", None)
        if relay_host is None or relay_password is None:
            self.irc = MultiIRCManager(self)
        else:
            self.irc = SingleIRCManager(self)

        self.reactor.add_global_handler("all_events", self.irc._dispatcher,
                                        -10)

        self.data = {}
        self.data_cb = {}
        self.url_regex = re.compile(self.url_regex_str, re.IGNORECASE)

        self.data["broadcaster"] = self.streamer
        self.data["version"] = self.version
        self.data["version_brief"] = self.version_brief
        self.data["bot_name"] = self.nickname
        self.data_cb["status_length"] = self.c_status_length
        self.data_cb["stream_status"] = self.c_stream_status
        self.data_cb["bot_uptime"] = self.c_uptime
        self.data_cb["current_time"] = self.c_current_time

        self.silent = True if args.silent else self.silent

        if self.silent:
            log.info("Silent mode enabled")
        """
        For actions that need to access the main thread,
        we can use the mainthread_queue.
        """
        self.mainthread_queue = ActionQueue()
        self.execute_every(1, self.mainthread_queue.parse_action)

        self.websocket_manager = WebSocketManager(self)

        try:
            if self.config["twitchapi"]["update_subscribers"] == "1":
                self.execute_every(30 * 60, self.action_queue.add,
                                   (self.update_subscribers_stage1, ))
        except:
            pass