예제 #1
0
class TyggBot:
    """
    Main class for the twitch bot
    """

    """ Singleton instance of TyggBot, one instance of the script
    should never have two active classes. """
    instance = None

    version = '1.4.1b'
    date_fmt = '%H:%M'
    update_chatters_interval = 5

    default_settings = {
            'broadcaster': 'test_broadcaster',
            'ban_ascii': True,
            'ban_msg_length': True,
            'motd_interval_offline': 60,
            'motd_interval_online': 5,
            'max_msg_length': 350,
            'lines_offline': True,
            'parse_pyramids': False,
            }

    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 init_twitter(self):
        try:
            self.twitter_auth = tweepy.OAuthHandler(self.config['twitter']['consumer_key'], self.config['twitter']['consumer_secret'])
            self.twitter_auth.set_access_token(self.config['twitter']['access_token'], self.config['twitter']['access_token_secret'])

            self.twitter = tweepy.API(self.twitter_auth)

            if self.use_twitter_stream:
                self.connect_to_twitter_stream()
                self.execute_every(60 * 5, self.check_twitter_connection)
        except:
            log.exception('Twitter authentication failed.')
            self.twitter = False

    def check_twitter_connection(self):
        try:
            if self.twitter_stream.running is False:
                self.connect_to_twitter_stream()
        except:
            log.exception('Caught exception while checking twitter connection')

    def connect_to_twitter_stream(self):
        try:
            class MyStreamListener(tweepy.StreamListener):
                relevant_users = [
                    'tyggbar', 'forsensc2', 'pajtest', 'rubarthasdf', 'nymn_hs'
                    ]

                def on_status(self, tweet):
                    if tweet.user.screen_name.lower() in self.relevant_users:
                        if not tweet.text.startswith('RT ') and tweet.in_reply_to_screen_name is None:
                            tw = tweet_prettify_urls(tweet)
                            TyggBot.instance.say('Volcania New tweet from {0}: {1}'.format(tweet.user.screen_name, tw.replace("\n", " ")))

                def on_error(self, status):
                    log.warning('Unhandled in twitter stream: {0}'.format(status))

            if not self.twitter_stream:
                listener = MyStreamListener()
                self.twitter_stream = tweepy.Stream(self.twitter_auth, listener, retry_420=3 * 60, daemonize_thread=True)

            self.twitter_stream.userstream(_with='followings', replies='all', async=True)
        except:
            log.exception('Exception caught while trying to connect to the twitter stream')

    def load_default_phrases(self):
        default_phrases = {
                'welcome': False,
                'quit': False,
                'nl': '{username} has typed {num_lines} messages in this channel!',
                'nl_0': '{username} has not typed any messages in this channel BibleThump',
                'nl_pos': '{username} is rank {nl_pos} line-farmer in this channel!',
                'new_sub': 'Sub hype! {username} just subscribed PogChamp',
                'resub': 'Resub hype! {username} just subscribed, {num_months} months in a row PogChamp <3 PogChamp',
                }
        if 'phrases' in self.config:
            self.phrases = {}

            for phrase_key, phrase_value in self.config['phrases'].items():
                log.debug('Including from config {0}: {1}'.format(phrase_key, phrase_value))
                if len(phrase_value.strip()) <= 0:
                    self.phrases[phrase_key] = False
                else:
                    self.phrases[phrase_key] = phrase_value

            for phrase_key, phrase_value in default_phrases.items():
                if phrase_key not in self.phrases:
                    log.debug('Overriding from default {0}: {1}'.format(phrase_key, phrase_value))
                    self.phrases[phrase_key] = phrase_value
        else:
            self.phrases = default_phrases

    def __init__(self, config, args):
        self.config = config
        self.nickname = config['main']['nickname']
        self.password = config['main']['password']
        self.default_settings['broadcaster'] = config['main']['streamer']

        try:
            self.sqlconn = pymysql.connect(unix_socket=config['sql']['unix_socket'], user=config['sql']['user'], passwd=config['sql']['passwd'], charset='utf8mb4')
            self.sqlconn.select_db(config['sql']['db'])
        except pymysql.err.OperationalError as e:
            error_code, error_message = e.args
            if error_code == 1045:
                log.error('Access denied to database with user \'{0}\'. Review your config file.'.format(config['sql']['user']))
            elif error_code == 1044:
                log.error('Access denied to database \'{0}\' with user \'{1}\'. Make sure the database \'{0}\' exists and user \'{1}\' has full access to it.'.format(config['sql']['db'], config['sql']['user']))
            else:
                log.error(e)
            sys.exit(1)

        self.sqlconn.autocommit(True)

        update_database(self.sqlconn)

        self.load_default_phrases()

        self.reactor = irc.client.Reactor()
        self.connection_manager = ConnectionManager(self.reactor, self, TMI.message_limit)

        client_id = None
        oauth = None
        if 'twitchapi' in self.config:
            if 'client_id' in self.config['twitchapi']:
                client_id = self.config['twitchapi']['client_id']
            if 'oauth' in self.config['twitchapi']:
                oauth = self.config['twitchapi']['oauth']

        self.twitchapi = TwitchAPI(client_id, oauth)

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

        if 'wolfram' in config['main']:
            Dispatch.wolfram = wolframalpha.Client(config['main']['wolfram'])
        else:
            Dispatch.wolfram = None

        self.whisper_manager = None

        TyggBot.instance = self

        self.is_online = False
        self.ascii_timeout_duration = 120
        self.msg_length_timeout_duration = 120

        self.base_path = os.path.dirname(os.path.realpath(__file__))
        self.data = {}
        self.data_cb = {}

        self.data_cb['status_length'] = self.c_status_length
        self.data_cb['stream_status'] = self.c_stream_status
        self.data_cb['time_norway'] = self.c_time_norway
        self.data_cb['bot_uptime'] = self.c_uptime
        self.data_cb['time_since_latest_deck'] = self.c_time_since_latest_deck
        self.ignores = []

        self.start_time = datetime.now()

        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.kvi = KVIData(self.sqlconn)
        self.tbm = TBMath()
        self.last_sync = time.time()

        self.users = UserManager(self.sqlconn)
        self.emotes = EmoteManager(self.sqlconn)
        self.emotes.load()

        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

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

        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)
                log.info(current_branch)
            except:
                log.exception('what')

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

        self.reconnection_interval = 5

        # Initialize MOTD-printing
        self.motd_iterator = 0
        self.motd_minute = 0
        self.motd_messages = []
        self.execute_every(60, self.motd_tick)

        self.load_all()

        self.whisper_manager = WhisperConnectionManager(self.reactor, self, self.streamer, TMI.whispers_message_limit, TMI.whispers_limit_interval)
        self.whisper_manager.start(accounts=[{'username': self.nickname, 'oauth': self.password}])

        self.num_offlines = 0
        self.execute_every(20, self.refresh_stream_status)

        self.twitter_stream = False
        if 'twitter' in config:
            self.use_twitter_stream = 'streaming' in config['twitter'] and config['twitter']['streaming'] == '1'
            self.init_twitter()
        else:
            self.twitter = None

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

        """
        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.link_checker = LinkChecker(self, self.execute_delayed)
        self.link_tracker = LinkTracker(self.sqlconn)
        self.pyramid_parser = PyramidParser(self)
        self.websocket_manager = WebSocketManager(self)

        """
        Update chatters every `update_chatters_interval' minutes.
        By default, this is set to run every 5 minutes.
        """
        self.execute_every(self.update_chatters_interval * 60,
                           self.action_queue.add,
                           (self.update_chatters_stage1, ))

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

    def update_subscribers_stage1(self):
        limit = 100
        offset = 0
        subscribers = []

        try:
            subs = self.twitchapi.get_subscribers(self.streamer, limit, offset)
            while len(subs) > 0:
                for sub in subs:
                    subscribers.append(sub['user']['name'])

                offset += limit
                subs = self.twitchapi.get_subscribers(self.streamer, limit, offset)
        except:
            log.exception('Caught an exception while trying to get subscribers')
            return

        if len(subscribers) > 0:
            self.mainthread_queue.add(self.update_subscribers_stage2,
                                      args=[subscribers])

    def update_subscribers_stage2(self, subscribers):
        self.kvi.insert('active_subs', len(subscribers) - 1)

        for username, user in self.users.items():
            if user.subscriber:
                user.subscriber = False
                user.needs_sync = True

        for subscriber in subscribers:
            user = self.users[subscriber]
            user.subscriber = True
            user.needs_sync = True

    def update_chatters_stage1(self):
        chatters = self.twitchapi.get_chatters(self.streamer)
        if len(chatters) > 0:
            self.mainthread_queue.add(self.update_chatters_stage2, args=[chatters])

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

        for chatter in chatters:
            user = self.users[chatter]
            if self.is_online:
                user.minutes_in_chat_online += self.update_chatters_interval
            else:
                user.minutes_in_chat_offline += self.update_chatters_interval
            user.touch(points * (5 if user.subscriber else 1))

    def motd_tick(self):
        if len(self.motd_messages) == 0:
            return

        self.motd_minute += 1
        interval = self.settings['motd_interval_online'] if self.is_online else self.settings['motd_interval_offline']
        if self.motd_minute >= interval:
            self.motd_cycle()

    def motd_cycle(self):
        if len(self.motd_messages) == 0:
            return

        log.debug('Sending MOTD message in the cycle.')
        self.say(self.motd_messages[self.motd_iterator % len(self.motd_messages)])
        self.motd_minute = 0
        self.motd_iterator += 1

    def refresh_stream_status(self):
        try:
            status = self.twitchapi.get_status(self.streamer)
            if status['error'] is True:
                log.error('An error occured while fetching stream status')
                return

            if status['online']:
                self.is_online = True
                self.kvi.set('stream_status', 1)
                self.kvi.set('last_online', int(time.time()))
                self.num_offlines = 0
                self.ascii_timeout_duration = 120
                self.msg_length_timeout_duration = 120
            else:
                self.ascii_timeout_duration = 10
                self.msg_length_timeout_duration = 10
                stream_status = self.kvi.get('stream_status')
                if (stream_status == 1 and self.num_offlines >= 10) or stream_status == 0:
                    self.is_online = False
                    self.kvi.set('stream_status', 0)
                    self.kvi.set('last_offline', int(time.time()))
                else:
                    self.kvi.set('last_online', int(time.time()))
                self.num_offlines += 1
        except:
            log.exception('Uncaught exception while refreshing stream status')

    def _dispatcher(self, connection, event):
        if connection == self.connection_manager.get_main_conn() or connection in self.whisper_manager:
            do_nothing = lambda c, e: None
            method = getattr(self, "on_" + event.type, do_nothing)
            method(connection, event)

    def start(self):
        """Start the IRC client."""
        self.reactor.process_forever()

    def get_kvi_value(self, key, extra={}):
        return self.kvi.get(key)

    def get_last_tweet(self, key, extra={}):
        if self.twitter:
            try:
                public_tweets = self.twitter.user_timeline(key)
                for tweet in public_tweets:
                    if not tweet.text.startswith('RT ') and tweet.in_reply_to_screen_name is None:
                        tw = tweet_prettify_urls(tweet)
                        return '{0} ({1} ago)'.format(tw.replace("\n", " "), time_since(datetime.now().timestamp(), tweet.created_at.timestamp(), format='short'))
            except Exception:
                log.exception('Exception caught while getting last tweet')
                return 'FeelsBadMan'
        else:
            return 'Twitter not set up FeelsBadMan'

        return 'FeelsBadMan'

    def get_emote_tm(self, key, extra={}):
        emote = self.emotes.find(key)
        if emote:
            return emote.tm
        return None

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

    def get_emote_tm_record(self, key, extra={}):
        emote = self.emotes.find(key)
        if emote:
            return '{0:,d}'.format(emote.tm_record)
        return None

    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_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]()
        elif key in self.settings:
            return self.settings[key]

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

    def get_cursor(self):
        self.sqlconn.ping()
        return self.sqlconn.cursor()

    def get_dictcursor(self):
        self.sqlconn.ping()
        return self.sqlconn.cursor(pymysql.cursors.DictCursor)

    def reload(self):
        self.sync_to()
        self.load_all()

    def privmsg(self, message, channel=None):
        try:
            if channel is None:
                channel = self.channel

            self.connection_manager.privmsg(channel, message)
        except Exception:
            log.exception('Exception caught while sending privmsg')

    def c_time_norway(self):
        return datetime.now(timezone('Europe/Oslo')).strftime(TyggBot.date_fmt)

    def c_uptime(self):
        return time_since(datetime.now().timestamp(), self.start_time.timestamp())

    def is_online(self):
        return self.kvi.get('stream_status') == 1

    def c_stream_status(self):
        if self.kvi.get('stream_status') == 1:
            return 'online'
        else:
            return 'offline'

    def c_status_length(self):
        stream_status = self.kvi.get('stream_status')

        if stream_status == 1:
            return time_since(time.time(), self.kvi.get('last_offline'))
        else:
            return time_since(time.time(), self.kvi.get('last_online'))

    def c_time_since_latest_deck(self):
        return time_since(time.time(), self.kvi.get('latest_deck_time'))

    def _ban(self, username):
        self.privmsg('.ban {0}'.format(username))

    def execute_at(self, at, function, arguments=()):
        self.reactor.execute_at(at, function, arguments)

    def execute_delayed(self, delay, function, arguments=()):
        self.reactor.execute_delayed(delay, function, arguments)

    def execute_every(self, period, function, arguments=()):
        self.reactor.execute_every(period, function, arguments)

    def ban(self, username):
        self._timeout(username, 30)
        self.execute_delayed(1, self._ban, (username, ))

    def ban_user(self, user):
        if not user.ban_immune:
            self._timeout(user.username, 30)
            self.execute_delayed(1, self._ban, (user.username, ))

    def unban(self, username):
        self.privmsg('.unban {0}'.format(username))

    def _timeout(self, username, duration):
        self.privmsg('.timeout {0} {1}'.format(username, duration))

    def timeout(self, username, duration):
        self._timeout(username, duration)
        self.execute_delayed(1, self._timeout, (username, duration))

    def timeout_user(self, user, duration):
        if not user.ban_immune:
            self._timeout(user.username, duration)
            self.execute_delayed(1, self._timeout, (user.username, duration))

    def whisper(self, username, message):
        if self.whisper_manager:
            self.whisper_manager.whisper(username, message)
        else:
            log.debug('No whisper conn set up.')

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

            if len(message) >= 1:
                if (message[0] == '.' or message[0] == '/') and not message[: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 sync_to(self):
        self.sqlconn.ping()
        self.sqlconn.autocommit(False)
        cursor = self.sqlconn.cursor()

        log.debug('Syncing data from TyggBot to the database...')

        self.users.sync()

        for trigger, command in self.commands.items():
            if not command.synced:
                command.sync(cursor)
                command.synced = True

        for filter in self.filters:
            if not filter.synced:
                filter.sync(cursor)
                filter.synced = True

        self.emotes.sync()

        self.link_tracker.sync()

        self.sqlconn.commit()
        self.sqlconn.autocommit(True)
        cursor.close()

    def load_all(self):
        self._load_commands()
        self._load_filters()
        self._load_settings()
        self._load_ignores()
        self._load_motd()

    def _load_commands(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        from command import Command

        cursor.execute('SELECT * FROM `tb_commands`')

        self.commands = {}

        self.commands['reload'] = Command.admin_command(self.reload)
        self.commands['quit'] = Command.admin_command(self.quit)
        self.commands['ignore'] = Command.admin_command(Dispatch.ignore, type='func')
        self.commands['unignore'] = Command.admin_command(Dispatch.unignore, type='func')
        self.commands['add'] = Command()
        self.commands['add'].load_from_db({
            'id': -1,
            'level': 500,
            'action': '{ "type":"multi", "default":"nothing", "args": [ { "level":500, "command":"banphrase", "action": { "type":"func", "cb":"add_banphrase" } }, { "level":500, "command":"win", "action": { "type":"func", "cb":"add_win" } }, { "level":500, "command":"command", "action": { "type":"func", "cb":"add_command" } }, { "level":2000, "command":"funccommand", "action": { "type":"func", "cb":"add_funccommand" } }, { "level":500, "command":"nothing", "action": { "type":"say", "message":"" } }, { "level":500, "command":"alias", "action": { "type":"func", "cb":"add_alias" } }, { "level":500, "command":"link", "action": { "type":"func", "cb":"add_link" } } ] }',
            'do_sync': False,
            'delay_all': 0,
            'delay_user': 1,
            'extra_args': None,
            })
        self.commands['remove'] = Command()
        self.commands['remove'].load_from_db({
            'id': -1,
            'level': 500,
            'action': '{ "type":"multi", "default":"nothing", "args": [ { "level":500, "command":"banphrase", "action": { "type":"func", "cb":"remove_banphrase" } }, { "level":500, "command":"win", "action": { "type":"func", "cb":"remove_win" } }, { "level":500, "command":"command", "action": { "type":"func", "cb":"remove_command" } }, { "level":500, "command":"nothing", "action": { "type":"say", "message":"" } }, { "level":500, "command":"alias", "action": { "type":"func", "cb":"remove_alias" } }, { "level":500, "command":"link", "action": { "type":"func", "cb":"remove_link" } } ] }',
            'do_sync': False,
            'delay_all': 0,
            'delay_user': 1,
            'extra_args': None,
            })
        self.commands['rem'] = self.commands['remove']
        self.commands['del'] = self.commands['remove']
        self.commands['delete'] = self.commands['remove']
        self.commands['debug'] = Command()
        self.commands['debug'].load_from_db({
            'id': -1,
            'level': 250,
            'action': '{ "type":"multi", "default":"nothing", "args": [ { "level":250, "command":"command", "action": { "type":"func", "cb":"debug_command" } }, { "level":250, "command":"user", "action": { "type":"func", "cb":"debug_user" } }, { "level":250, "command":"nothing", "action": { "type":"say", "message":"" } } ] }',
            'do_sync': False,
            'delay_all': 0,
            'delay_user': 1,
            'extra_args': None,
            })
        self.commands['level'] = Command.admin_command(Dispatch.level, type='func')
        self.commands['eval'] = Command.admin_command(Dispatch.eval, type='func', level=2000)

        num_commands = 0
        num_aliases = 0

        for row in cursor:
            try:
                cmd = Command()
                cmd.load_from_db(row)

                if cmd.is_enabled():
                    for alias in row['command'].split('|'):
                        if alias not in self.commands:
                            self.commands[alias] = cmd
                            num_aliases += 1
                        else:
                            log.error('Command !{0} is already in use'.format(alias))

                num_commands += 1
            except Exception:
                log.exception('Exception caught when loading command')
                continue

        log.debug('Loaded {0} commands ({1} aliases)'.format(num_commands, num_aliases))
        cursor.close()

    def _load_filters(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        cursor.execute('SELECT * FROM `tb_filters`')

        self.filters = []

        num_filters = 0

        for row in cursor:
            try:
                filter = Filter(row)

                if filter.is_enabled():
                    self.filters.append(filter)
                    num_filters += 1
            except Exception:
                log.exception('Exception caught when loading filter')
                continue

        log.debug('Loaded {0} filters'.format(num_filters))
        cursor.close()

    def _load_settings(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        cursor.execute('SELECT * FROM `tb_settings`')

        self.settings = {}

        for row in cursor:
            self.settings[row['setting']] = Setting.parse(row['type'], row['value'])
            if self.settings[row['setting']] is None:
                log.error('ERROR LOADING SETTING {0}'.format(row['setting']))

        for setting in self.default_settings:
            if setting not in self.settings:
                self.settings[setting] = self.default_settings[setting]

        cursor.close()

    def _load_ignores(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        cursor.execute('SELECT * FROM `tb_ignores`')

        self.ignores = []

        for row in cursor:
            self.ignores.append(row['username'])

        cursor.close()

    def _load_motd(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        cursor.execute('SELECT * FROM `tb_motd` WHERE `enabled`=1')

        self.motd_messages = []

        for row in cursor:
            self.motd_messages.append(row['message'])

        cursor.close()

    def on_welcome(self, chatconn, event):
        if chatconn in self.whisper_manager:
            log.debug('Connected to Whisper server.')
        else:
            log.debug('Connected to IRC server.')

    def connect(self):
        return self.connection_manager.start()

    def on_disconnect(self, chatconn, event):
        if chatconn in self.whisper_manager:
            log.debug('Disconnecting from Whisper server')
            self.whisper_manager.on_disconnect(chatconn)

        else:
            log.debug('Disconnected from IRC server')
            self.connection_manager.on_disconnect(chatconn)

    def check_msg_content(self, source, msg_raw, event):
        msg_lower = msg_raw.lower()

        for f in self.filters:
            if f.type == 'regex':
                m = f.search(source, msg_lower)
                if m:
                    log.debug('Matched regex filter \'{0}\''.format(f.name))
                    f.run(self, source, msg_raw, event, {'match': m})
                    return True
            elif f.type == 'banphrase':
                if f.filter in msg_lower:
                    log.debug('Matched banphrase filter \'{0}\''.format(f.name))
                    f.run(self, source, msg_raw, event)
                    return True
        return False  # message was ok

    def parse_message(self, msg_raw, source, event, tags={}, whisper=False):
        msg_lower = msg_raw.lower()

        if source is None:
            log.error('No valid user passed to parse_message')
            return False

        for tag in tags:
            if tag['key'] == 'subscriber':
                if source.subscriber and tag['value'] == '0':
                    source.subscriber = False
                    source.needs_sync = True
                elif not source.subscriber and tag['value'] == '1':
                    source.subscriber = True
                    source.needs_sync = True
            elif tag['key'] == 'emotes' and tag['value']:
                emote_data = tag['value'].split('/')
                for emote in emote_data:
                    try:
                        emote_id, emote_occurrence = emote.split(':')
                        emote_indices = emote_occurrence.split(',')
                        emote_count = len(emote_indices)
                        emote = self.emotes[int(emote_id)]
                        emote.add(emote_count, self.reactor)
                        if emote.id == -1 and emote.code is None:
                            # The emote we just detected is new, set its code.
                            first_index, last_index = emote_indices[0].split('-')
                            emote.code = msg_raw[int(first_index):int(last_index) + 1]
                            if emote.code not in self.emotes:
                                self.emotes[emote.code] = emote
                    except:
                        log.exception('Exception caught while splitting emote data')
            elif tag['key'] == 'display-name' and tag['value']:
                try:
                    source.update_username(tag['value'])
                except:
                    log.exception('Exception caught while updating a users username')

        if self.settings['parse_pyramids']:
            self.pyramid_parser.parse_line(msg_raw, source)

        for emote in self.emotes.custom_data:
            num = len(emote.regex.findall(msg_raw))
            if num > 0:
                emote.add(num, self.reactor)

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

        if not whisper:
            if source.level < 500:
                if self.check_msg_content(source, msg_raw, event):
                    # If we've matched a filter, we should not have to run a command.
                    return

            urls = self.link_checker.find_urls_in_message(msg_raw)
            for url in urls:
                self.link_tracker.add(url)

                if source.level < 500:
                    # Action which will be taken when a bad link is found
                    action = Action(self.timeout, args=[source.username, 20])
                    # Queue up a check on the URL
                    self.action_queue.add(self.link_checker.check_url, args=[url, action])

            # TODO: Change to if source.ignored
            if source.username in self.ignores:
                return

        if msg_lower[:1] == '!':
            msg_lower_parts = msg_lower.split(' ')
            command = msg_lower_parts[0][1:]
            msg_raw_parts = msg_raw.split(' ')
            extra_msg = ' '.join(msg_raw_parts[1:]) if len(msg_raw_parts) > 1 else None
            if command in self.commands:
                if source.level >= self.commands[command].level:
                    command = self.commands[command]
                    if (whisper and (command.can_execute_with_whisper or source.level >= 420)) or not whisper:
                        command.run(self, source, extra_msg, event)
                    return

        source.wrote_message(not whisper and (self.is_online or self.settings['lines_offline']))

    def on_whisper(self, chatconn, event):
        # We use .lower() in case twitch ever starts sending non-lowercased usernames
        source = self.users[event.source.user.lower()]
        self.parse_message(event.arguments[0], source, event, whisper=True)

    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

        # We use .lower() in case twitch ever starts sending non-lowercased usernames
        source = self.users[event.source.user.lower()]

        cur_time = time.time()

        msg = event.arguments[0]
        msg_len = len(msg)

        if msg_len > 70 and source.level < 500:
            non_alnum = sum(not c.isalnum() for c in msg)
            ratio = non_alnum / msg_len

            # TODO: Implement a better anti-ascii feature.
            if self.settings['ban_ascii']:
                if (msg_len > 240 and ratio > 0.8) or ratio > 0.93:
                    log.debug('Timeouting {0} because of a high ascii ratio ({1}). Message length: {2}'.format(source.username, ratio, msg_len))
                    self.timeout_user(source, self.ascii_timeout_duration)
                    self.whisper(source.username, 'You have been timed out for {0} seconds because your message contained too many ascii characters.'.format(self.ascii_timeout_duration))
                    return

            if self.settings['ban_msg_length']:
                max_msg_length = self.settings['max_msg_length']
                if msg_len > max_msg_length:
                    log.debug('Timeouting {0} because of a message length: {1}'.format(source.username, msg_len))
                    self.timeout_user(source, self.msg_length_timeout_duration)
                    self.whisper(source.username, 'You have been timed out for {0} seconds because your message was too long.'.format(self.msg_length_timeout_duration))
                    return

        if cur_time - self.last_sync >= 10 * 60:
            self.sync_to()
            self.last_sync = cur_time

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

    def quit(self):
        self.sync_to()
        if self.phrases['quit']:
            phrase_data = {
                    'nickname': self.nickname,
                    'version': self.version,
                    }

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

        if self.twitter_stream:
            self.twitter_stream.disconnect()

        if self.whisper_manager:
            self.whisper_manager.quit()

        sys.exit(0)
예제 #2
0
    def __init__(self, config, args):
        self.config = config
        self.nickname = config['main']['nickname']
        self.password = config['main']['password']
        self.default_settings['broadcaster'] = config['main']['streamer']

        try:
            self.sqlconn = pymysql.connect(unix_socket=config['sql']['unix_socket'], user=config['sql']['user'], passwd=config['sql']['passwd'], charset='utf8mb4')
            self.sqlconn.select_db(config['sql']['db'])
        except pymysql.err.OperationalError as e:
            error_code, error_message = e.args
            if error_code == 1045:
                log.error('Access denied to database with user \'{0}\'. Review your config file.'.format(config['sql']['user']))
            elif error_code == 1044:
                log.error('Access denied to database \'{0}\' with user \'{1}\'. Make sure the database \'{0}\' exists and user \'{1}\' has full access to it.'.format(config['sql']['db'], config['sql']['user']))
            else:
                log.error(e)
            sys.exit(1)

        self.sqlconn.autocommit(True)

        update_database(self.sqlconn)

        self.load_default_phrases()

        self.reactor = irc.client.Reactor()
        self.connection_manager = ConnectionManager(self.reactor, self, TMI.message_limit)

        client_id = None
        oauth = None
        if 'twitchapi' in self.config:
            if 'client_id' in self.config['twitchapi']:
                client_id = self.config['twitchapi']['client_id']
            if 'oauth' in self.config['twitchapi']:
                oauth = self.config['twitchapi']['oauth']

        self.twitchapi = TwitchAPI(client_id, oauth)

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

        if 'wolfram' in config['main']:
            Dispatch.wolfram = wolframalpha.Client(config['main']['wolfram'])
        else:
            Dispatch.wolfram = None

        self.whisper_manager = None

        TyggBot.instance = self

        self.is_online = False
        self.ascii_timeout_duration = 120
        self.msg_length_timeout_duration = 120

        self.base_path = os.path.dirname(os.path.realpath(__file__))
        self.data = {}
        self.data_cb = {}

        self.data_cb['status_length'] = self.c_status_length
        self.data_cb['stream_status'] = self.c_stream_status
        self.data_cb['time_norway'] = self.c_time_norway
        self.data_cb['bot_uptime'] = self.c_uptime
        self.data_cb['time_since_latest_deck'] = self.c_time_since_latest_deck
        self.ignores = []

        self.start_time = datetime.now()

        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.kvi = KVIData(self.sqlconn)
        self.tbm = TBMath()
        self.last_sync = time.time()

        self.users = UserManager(self.sqlconn)
        self.emotes = EmoteManager(self.sqlconn)
        self.emotes.load()

        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

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

        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)
                log.info(current_branch)
            except:
                log.exception('what')

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

        self.reconnection_interval = 5

        # Initialize MOTD-printing
        self.motd_iterator = 0
        self.motd_minute = 0
        self.motd_messages = []
        self.execute_every(60, self.motd_tick)

        self.load_all()

        self.whisper_manager = WhisperConnectionManager(self.reactor, self, self.streamer, TMI.whispers_message_limit, TMI.whispers_limit_interval)
        self.whisper_manager.start(accounts=[{'username': self.nickname, 'oauth': self.password}])

        self.num_offlines = 0
        self.execute_every(20, self.refresh_stream_status)

        self.twitter_stream = False
        if 'twitter' in config:
            self.use_twitter_stream = 'streaming' in config['twitter'] and config['twitter']['streaming'] == '1'
            self.init_twitter()
        else:
            self.twitter = None

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

        """
        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.link_checker = LinkChecker(self, self.execute_delayed)
        self.link_tracker = LinkTracker(self.sqlconn)
        self.pyramid_parser = PyramidParser(self)
        self.websocket_manager = WebSocketManager(self)

        """
        Update chatters every `update_chatters_interval' minutes.
        By default, this is set to run every 5 minutes.
        """
        self.execute_every(self.update_chatters_interval * 60,
                           self.action_queue.add,
                           (self.update_chatters_stage1, ))

        try:
            if self.config['twitchapi']['update_subscribers'] == '1':
                self.execute_every(30 * 60,
                                   self.action_queue.add,
                                   (self.update_subscribers_stage1, ))
        except:
            pass
예제 #3
0
#!/usr/bin/env python3

import sys, os
import configparser
import pymysql

sys.path.insert(0, os.path.dirname(os.path.realpath(__file__ + '/../')))
os.chdir(os.path.dirname(os.path.realpath(__file__ + '/../')))

from models.user import User, UserManager
from kvidata import KVIData

config = configparser.ConfigParser()

config.read('config.ini')

sqlconn = pymysql.connect(unix_socket=config['sql']['unix_socket'], user=config['sql']['user'], passwd=config['sql']['passwd'], db=config['sql']['db'], charset='utf8')
kvi = KVIData(sqlconn)

users = UserManager(sqlconn)

os.chdir(os.path.dirname(os.path.realpath(__file__)))

for nl in kvi.fetch_all('nl'):
    user = users[nl['key']]
    user.num_lines = nl['value']
    user.needs_sync = True

users.sync()
예제 #4
0
class TyggBot:
    """
    Main class for the twitch bot
    """

    version = '0.9.5.2'
    date_fmt = '%H:%M'
    #date_fmt = '%A %B '
    commands = {}
    filters = []
    settings = {}
    emotes = {}
    twitchapi = False

    silent = False
    dev = False

    """ Singleton instance of TyggBot, one instance of the script
    should never have two active classes."""
    instance = None

    default_settings = {
            'broadcaster': 'test_broadcaster',
            'ban_ascii': True,
            'ban_msg_length': True,
        }

    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 init_twitter(self):
        try:
            self.twitter_auth = tweepy.OAuthHandler(self.config['twitter']['consumer_key'], self.config['twitter']['consumer_secret'])
            self.twitter_auth.set_access_token(self.config['twitter']['access_token'], self.config['twitter']['access_token_secret'])

            self.twitter = tweepy.API(self.twitter_auth)

            if self.use_twitter_stream:
                self.connect_to_twitter_stream()
        except:
            log.exception('Twitter authentication failed.')
            self.twitter = False

    def connect_to_twitter_stream(self):
        try:
            class MyStreamListener(tweepy.StreamListener):
                relevant_users = [
                        'tyggbar', 'forsensc2', 'pajtest', 'rubarthasdf'
                        ]

                def on_status(self, tweet):
                    if tweet.user.screen_name.lower() in self.relevant_users:
                        if not tweet.text.startswith('RT ') and tweet.in_reply_to_screen_name is None:
                            TyggBot.instance.say('Volcania New tweet from {0}: {1}'.format(tweet.user.screen_name, tweet.text.replace("\n", " ")))

                def on_error(self, status):
                    log.warning('Unhandled in twitter stream: {0}'.format(status))

            if not self.twitter_stream:
                listener = MyStreamListener()
                self.twitter_stream = tweepy.Stream(self.twitter_auth, listener, retry_420=3*60, daemonize_thread=True)

            self.twitter_stream.userstream(_with='followings', replies='all', async=True)
        except:
            log.exception('Exception caught while trying to connect to the twitter stream')

    def load_default_phrases(self):
        default_phrases = {
                'welcome': False,
                'quit': False,
                'nl': '{username} has typed {num_lines} messages in this channel!',
                'nl_0': '{username} has not typed any messages in this channel BibleThump',
                'new_sub': 'Sub hype! {username} just subscribed PogChamp',
                'resub': 'Resub hype! {username} just subscribed, {num_months} months in a row PogChamp <3 PogChamp',
                }
        if 'phrases' in self.config:
            self.phrases = {}

            for phrase_key, phrase_value in self.config['phrases'].items():
                log.debug('Including from config {0}: {1}'.format(phrase_key, phrase_value))
                if len(phrase_value.strip()) <= 0:
                    self.phrases[phrase_key] = False
                else:
                    self.phrases[phrase_key] = phrase_value

            for phrase_key, phrase_value in default_phrases.items():
                if phrase_key not in self.phrases:
                    log.debug('Overriding from default {0}: {1}'.format(phrase_key, phrase_value))
                    self.phrases[phrase_key] = phrase_value
        else:
            self.phrases = default_phrases

    def __init__(self, config, args):
        self.config = config

        self.sqlconn = pymysql.connect(unix_socket=config['sql']['unix_socket'], user=config['sql']['user'], passwd=config['sql']['passwd'], db=config['sql']['db'], charset='utf8')
        self.sqlconn.autocommit(True)

        update_database(self.sqlconn)

        self.load_default_phrases()

        self.reactor = irc.client.Reactor()
        self.connection = self.reactor.server()

        self.twitchapi = TwitchAPI(type='api')

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

        if 'wolfram' in config['main']:
            Dispatch.wolfram = wolframalpha.Client(config['main']['wolfram'])
        else:
            wolfram = None

        self.whisper_conn = None

        TyggBot.instance = self

        self.base_path = os.path.dirname(os.path.realpath(__file__))
        self.data = {}
        self.data_cb = {}

        self.data_cb['status_length'] = self.c_status_length
        self.data_cb['stream_status'] = self.c_stream_status
        self.data_cb['time_norway'] = self.c_time_norway
        self.data_cb['bot_uptime'] = self.c_uptime
        self.data_cb['time_since_latest_deck'] = self.c_time_since_latest_deck
        self.ignores = []

        self.start_time = datetime.now()

        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.kvi = KVIData(self.sqlconn)
        self.tbm = TBMath()
        self.last_sync = time.time()

        self.users = UserManager(self.sqlconn)

        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

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

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

        self.sync_from()

        self.nickname = config['main']['nickname']
        self.password = config['main']['password']
        self.reconnection_interval = 5

        self.load_all()

        self.whisper_conn = WhisperConn(self.streamer, self.nickname, self.password, self.reactor)
        self.whisper_conn.connect()

        self.num_commands_sent = 0
        self.connection.execute_every(30, self.reset_command_throttle)

        self.twitter_stream = False
        if 'twitter' in config:
            self.use_twitter_stream = 'streaming' in config['twitter'] and config['twitter']['streaming'] == '1'
            self.init_twitter()
        else:
            self.twitter = None

        self.connection.execute_every(1, self.shift_emotes)

        self.ws_clients = []
        if 'websocket' in config and config['websocket']['enabled'] == '1':
            self.init_websocket_server()
            self.execute_every(1, self.refresh_emote_data)

        self.urls_to_check = queue.Queue()
        self.connection.execute_every(1, self.check_urls)

    def refresh_emote_data(self):
        if len(self.ws_clients) > 0:
            emote_data = {}
            for emote in self.emotes:
                emote_data[emote.code] = {
                        'code': emote.code,
                        'pm': emote.pm,
                        'tm': emote.tm,
                        'count': emote.count,
                        }

            payload = json.dumps(emote_data, separators=(',',':')).encode('utf8')
            for client in self.ws_clients:
                client.sendMessage(payload, False)

    def init_websocket_server(self):
        import twisted
        from twisted.internet import reactor

        twisted.python.log.startLogging(sys.stdout)

        from autobahn.twisted.websocket import WebSocketServerFactory, \
                WebSocketServerProtocol

        class MyServerProtocol(WebSocketServerProtocol):
            def onConnect(self, request):
                log.info('Client connecting: {0}'.format(request.peer))

            def onOpen(self):
                log.info('WebSocket connection open. {0}'.format(self))
                TyggBot.instance.ws_clients.append(self)

            def onMessage(self, payload, isBinary):
                if isBinary:
                    log.info('Binary message received: {0} bytes'.format(len(payload)))
                else:
                    TyggBot.instance.me('Recieved message: {0}'.format(payload.decode('utf8')))
                    log.info('Text message received: {0}'.format(payload.decode('utf8')))

            def onClose(self, wasClean, code, reason):
                log.info('WebSocket connection closed: {0}'.format(reason))
                TyggBot.instance.ws_clients.remove(self)

        factory = WebSocketServerFactory()
        factory.protocol = MyServerProtocol

        def reactor_run(reactor, factory, port):
            log.info(reactor)
            log.info(factory)
            log.info(port)
            reactor.listenTCP(port, factory)
            reactor.run(installSignalHandlers=0)

        reactor_thread = threading.Thread(target=reactor_run, args=(reactor, factory, int(self.config['websocket']['port'])))
        reactor_thread.daemon = True
        reactor_thread.start()

        self.ws_factory = factory

    def shift_emotes(self):
        for emote in self.emotes:
            emote.shift()

    def reset_command_throttle(self):
        self.num_commands_sent = 0

    def _dispatcher(self, connection, event):
        do_nothing = lambda c, e: None
        method = getattr(self, "on_" + event.type, do_nothing)
        method(connection, event)

    def start(self):
        """Start the IRC client."""
        self.reactor.process_forever()

    def get_kvi_value(self, key, extra={}):
        return self.kvi.get(key)

    def get_last_tweet(self, key, extra={}):
        if self.twitter:
            try:
                public_tweets = self.twitter.user_timeline(key)
                for tweet in public_tweets:
                    if not tweet.text.startswith('RT ') and tweet.in_reply_to_screen_name is None:
                        return '{0} ({1} ago)'.format(tweet.text.replace("\n", " "), time_since(datetime.now().timestamp(), tweet.created_at.timestamp(), format='short'))
            except Exception as e:
                log.error('Exception caught {0}'.format(e))
                return 'FeelsBadMan'
        else:
            return 'Twitter not set up FeelsBadMan'

        return 'FeelsBadMan'

    def get_emote_pm(self, key, extra={}):
        for emote in self.emotes:
            if key == emote.code:
                return emote.pm
        return 0

    def get_emote_tm(self, key, extra={}):
        for emote in self.emotes:
            if key == emote.code:
                return emote.tm
        return 0

    def get_emote_count(self, key, extra={}):
        for emote in self.emotes:
            if key == emote.code:
                return emote.count
        return 0

    def get_emote_pm_record(self, key, extra={}):
        for emote in self.emotes:
            if key == emote.code:
                return emote.pm_record
        return 0

    def get_emote_tm_record(self, key, extra={}):
        for emote in self.emotes:
            if key == emote.code:
                return emote.tm_record
        return 0

    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]()
        elif key in self.settings:
            return self.settings[key]

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

    def get_cursor(self):
        self.sqlconn.ping()
        return self.sqlconn.cursor()

    def get_dictcursor(self):
        self.sqlconn.ping()
        return self.sqlconn.cursor(pymysql.cursors.DictCursor)

    def reload(self):
        self.sync_to()
        self.load_all()

    def privmsg(self, message, priority=False, channel=None):
        # Non-prioritized messages are allowed 50% of the message limit
        if (not priority and self.num_commands_sent > TMI.message_limit/2) or (priority and self.num_commands_sent > TMI.message_limit):
            log.error('Skipping this say, because we are sending too many messages.')
            return False

        try:
            if channel is None:
                channel = self.channel

            self.connection.privmsg(channel, message)
            self.num_commands_sent += 1
        except Exception as e:
            log.error('Exception caught while sending privmsg: {0}'.format(e))

    def c_time_norway(self):
        return datetime.now(timezone('Europe/Oslo')).strftime(TyggBot.date_fmt)

    def c_uptime(self):
        return time_since(datetime.now().timestamp(), self.start_time.timestamp())

    def c_stream_status(self):
        if self.kvi.get('stream_status') == 1:
            return 'online'
        else:
            return 'offline'

    def c_status_length(self):
        stream_status = self.kvi.get('stream_status')

        if stream_status == 1:
            return time_since(time.time(), self.kvi.get('last_offline'))
        else:
            return time_since(time.time(), self.kvi.get('last_online'))

    def c_time_since_latest_deck(self):
        return time_since(time.time(), self.kvi.get('latest_deck_time'))

    def _ban(self, username):
        self.privmsg('.ban {0}'.format(username), True)

    def execute_at(self, at, function, arguments=()):
        self.reactor.execute_at(at, function, arguments)

    def execute_delayed(self, delay, function, arguments=()):
        self.reactor.execute_delayed(delay, function, arguments)

    def execute_every(self, period, function, arguments=()):
        self.reactor.execute_every(period, function, arguments)

    def ban(self, username):
        self._timeout(username, 30)
        self.execute_delayed(1, self._ban, (username, ))

    def unban(self, username):
        self.privmsg('.unban {0}'.format(username), True)

    def _timeout(self, username, duration):
        self.privmsg('.timeout {0} {1}'.format(username, duration), True)

    def timeout(self, username, duration):
        self._timeout(username, duration)
        self.execute_delayed(1, self._timeout, (username, duration))

    def whisper(self, username, message):
        if self.whisper_conn:
            log.debug('Sending whisper {0} to {1}'.format(message, username))
            self.whisper_conn.whisper(username, message)
        else:
            log.debug('No whisper conn set up.')

    def say(self, message, force=False):
        if force or not self.silent:
            message = message.strip()

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

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

                self.privmsg(message[:400])
            else:
                log.warning('Message too short, skipping...')

    def me(self, message, force=False):
        if force or 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[:400])
            else:
                log.warning('Message too short, skipping...')

    def sync_to(self):
        self.sqlconn.ping()
        cursor = self.sqlconn.cursor()

        log.debug('Syncing data from TyggBot to the database...')

        self.users.sync()

        for trigger, command in self.commands.items():
            if not command.synced:
                command.sync(cursor)
                command.synced = True

        for filter in self.filters:
            if not filter.synced:
                filter.sync(cursor)
                filter.synced = True

        for emote in self.emotes:
            emote.sync(cursor)

        cursor.close()

    def sync_from(self):
        pass

    def load_all(self):
        self._load_commands()
        self._load_filters()
        self._load_settings()
        self._load_ignores()
        self._load_emotes()

    def _load_commands(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        from command import Command

        cursor.execute('SELECT * FROM `tb_commands`')

        self.commands = {}

        self.commands['reload'] = Command.admin_command(self.reload)
        self.commands['quit'] = Command.admin_command(self.quit)
        self.commands['ignore'] = Command.admin_command(Dispatch.ignore, type='func')
        self.commands['unignore'] = Command.admin_command(Dispatch.unignore, type='func')
        self.commands['add'] = Command()
        self.commands['add'].load_from_db({
            'id': -1,
            'level': 500,
            'action': '{ "type":"multi", "default":"nothing", "args": [ { "level":500, "command":"banphrase", "action": { "type":"func", "cb":"add_banphrase" } }, { "level":500, "command":"win", "action": { "type":"func", "cb":"add_win" } }, { "level":500, "command":"command", "action": { "type":"func", "cb":"add_command" } }, { "level":2000, "command":"funccommand", "action": { "type":"func", "cb":"add_funccommand" } }, { "level":500, "command":"nothing", "action": { "type":"say", "message":"" } } ] }',
            'do_sync': False,
            'delay_all': 5,
            'delay_user': 15,
            'enabled': True,
            'num_uses': 0,
            'extra_args': None,
            })
        self.commands['remove'] = Command()
        self.commands['remove'].load_from_db({
            'id': -1,
            'level': 500,
            'action': '{ "type":"multi", "default":"nothing", "args": [ { "level":500, "command":"banphrase", "action": { "type":"func", "cb":"remove_banphrase" } }, { "level":500, "command":"win", "action": { "type":"func", "cb":"remove_win" } }, { "level":500, "command":"command", "action": { "type":"func", "cb":"remove_command" } }, { "level":500, "command":"nothing", "action": { "type":"say", "message":"" } } ] }',
            'do_sync': False,
            'delay_all': 5,
            'delay_user': 15,
            'enabled': True,
            'num_uses': 0,
            'extra_args': None,
            })
        self.commands['rem'] = self.commands['remove']
        self.commands['del'] = self.commands['remove']
        self.commands['delete'] = self.commands['remove']
        self.commands['debug'] = Command()
        self.commands['debug'].load_from_db({
            'id': -1,
            'level': 250,
            'action': '{ "type":"multi", "default":"nothing", "args": [ { "level":250, "command":"command", "action": { "type":"func", "cb":"debug_command" } }, { "level":250, "command":"user", "action": { "type":"func", "cb":"debug_user" } }, { "level":250, "command":"nothing", "action": { "type":"say", "message":"" } } ] }',
            'do_sync': False,
            'delay_all': 5,
            'delay_user': 15,
            'enabled': True,
            'num_uses': 0,
            'extra_args': None,
            })
        self.commands['level'] = Command.admin_command(Dispatch.level, type='func')
        self.commands['eval'] = Command.admin_command(Dispatch.eval, type='func', level=2000)

        num_commands = 0
        num_aliases = 0

        for row in cursor:
            try:
                cmd = Command()
                cmd.load_from_db(row)

                if cmd.is_enabled():
                    for alias in row['command'].split('|'):
                        if alias not in self.commands:
                            self.commands[alias] = cmd
                            num_aliases += 1
                        else:
                            log.error('Command !{0} is already in use'.format(alias))

                num_commands += 1
            except Exception as e:
                log.error('Exception caught when loading command: {0}'.format(e))
                continue

        log.debug('Loaded {0} commands ({1} aliases)'.format(num_commands, num_aliases))
        cursor.close()

    def _load_filters(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        cursor.execute('SELECT * FROM `tb_filters`')

        self.filters = []

        num_filters = 0

        for row in cursor:
            try:
                filter = Filter(row)

                if filter.is_enabled():
                    self.filters.append(filter)
                    num_filters += 1
            except Exception as e:
                log.error('Exception caught when loading filter: {0}'.format(e))
                continue

        log.debug('Loaded {0} filters'.format(num_filters))
        cursor.close()

    def _load_settings(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        cursor.execute('SELECT * FROM `tb_settings`')

        self.settings = {}

        for row in cursor:
            self.settings[row['setting']] = Setting.parse(row['type'], row['value'])
            if self.settings[row['setting']] is None:
                log.error('ERROR LOADING SETTING {0}'.format(row['setting']))

        for setting in self.default_settings:
            if setting not in self.settings:
                self.settings[setting] = self.default_settings[setting]

        cursor.close()

    def _load_ignores(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        cursor.execute('SELECT * FROM `tb_ignores`')

        self.ignores = []

        for row in cursor:
            self.ignores.append(row['username'])

        cursor.close()

    def _load_emotes(self):
        cursor = self.sqlconn.cursor(pymysql.cursors.DictCursor)

        cursor.execute('SELECT * FROM `tb_emote`')

        self.emotes = []

        for row in cursor:
            self.emotes.append(Emote.load_from_row(row))

        cursor.close()

    def on_welcome(self, chatconn, event):
        if chatconn == self.connection:
            log.debug('Connected to IRC server.')
            if irc.client.is_channel(self.channel):
                chatconn.join(self.channel)

                if self.phrases['welcome']:
                    phrase_data = {
                            'nickname': self.nickname,
                            'version': self.version,
                            }

                    try:
                        self.say(self.phrases['welcome'].format(**phrase_data))
                    except Exception as e:
                        log.error(e)
        elif chatconn == self.whisper_conn:
            log.debug('Connected to Whisper server.')

    def _connected_checker(self):
        if not self.connection.is_connected():
            self.connection.execute_delayed(self.reconnection_interval,
                                            self._connected_checker)

            self.connect()

    def connect(self):
        log.debug('Fetching random IRC server...')
        data = self.twitchapi.get(['channels', self.streamer, 'chat_properties'])
        if data and len(data['chat_servers']) > 0:
            server = random.choice(data['chat_servers'])
            ip, port = server.split(':')
            port = int(port)

            log.debug('Fetched {0}:{1}'.format(ip, port))

            try:
                irc.client.SimpleIRCClient.connect(self, ip, port, self.nickname, self.password, self.nickname)
                self.connection.cap('REQ', 'twitch.tv/membership')
                self.connection.cap('REQ', 'twitch.tv/commands')
                self.connection.cap('REQ', 'twitch.tv/tags')
                return True
            except irc.client.ServerConnectionError:
                pass
        log.debug('Connecting to IRC server...')

        self.connection.execute_delayed(self.reconnection_interval,
                                        self._connected_checker)

        return False

    def on_disconnect(self, chatconn, event):
        if chatconn == self.connection:
            log.debug('Disconnected from IRC server')
            self.sync_to()
            self.connection.execute_delayed(self.reconnection_interval,
                                            self._connected_checker)
        elif chatconn == self.whisper_conn:
            log.debug('Disconnecting from Whisper server')
            self.whisper_conn.execute_delayed(self.whisper_conn.reconnection_interval,
                                              self.whisper_conn._connected_checker)

    def check_msg_content(self, source, msg_raw, event):
        for f in self.filters:
            if f.type == 'regex':
                m = f.search(source, msg_lower)
                if m:
                    log.debug('Matched regex filter \'{0}\''.format(f.name))
                    f.run(self, source, msg_raw, event, {'match':m})
                    return True
                elif f.type == 'banphrase':
                    if f.filter in msg_lower:
                        log.debug('Matched banphrase filter \'{0}\''.format(f.name))
                        f.run(self, source, msg_raw, event)
                        return True
        return False # message was ok

    def check_link_content(self, source, content, event):
        return self.check_msg_content(source, content, event) # same check as for normal chat messages, probably should be changed

    def check_url(self, _url):
        try: r = requests.get(_url['url'])
        except: return
        self.check_url_content(_url['source'], r.text, _url['event'])
        return

    def _check_urls(self):
        while True:
            try: _url = self.urls_to_check.get(False)
            except queue.Empty: return
            self.check_url(_url)

    def check_urls(self):
        t = threading.Thread(self._check_urls)
        t.start()

    def parse_message(self, msg_raw, source=None, event=None, pretend=False, force=False, tags={}):
        msg_lower = msg_raw.lower()

        for tag in tags:
            if tag['key'] == 'subscriber':
                if source.subscriber and tag['value'] == '0':
                    source.subscriber = False
                    source.needs_sync = True
                elif not source.subscriber and tag['value'] == '1':
                    source.subscriber = True
                    source.needs_sync = True

        for emote in self.emotes:
            num = len(emote.regex.findall(msg_raw))
            if num > 0:
                emote.add(num)

        if source is None and not event:
            log.error('No nick or event passed to parse_message')
            return False

        log.debug('{0}: {1}'.format(source.username, msg_raw))

        if not force:
            if source.level < 500:
                if self.check_msg_content(source, msg_raw, event): return # If we've matched a filter, we should not have to run a command.
                regex = r'((http:\/\/)|\b)(\w|\.)*\.(((aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-zA-Z]{2})\/\S*)|(aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|[a-zA-Z]{2}))'

                 #probably shit regex, but kind of works
                urls = re.finditer(regex, msg_raw)
                for i in urls:
                    url = i.group(0)
                    if not (url.startswith('http://') or url.startswith('https://')): url = 'http://' + url
                    _url['url'] = url
                    _url['source'] = source
                    _url['event'] = event                    
                    self.urls_to_check.put(_url)
            # TODO: Change to if source.ignored
            if source.username in self.ignores:
                return

        if msg_lower[:1] == '!':
            msg_lower_parts = msg_lower.split(' ')
            command = msg_lower_parts[0][1:]
            msg_raw_parts = msg_raw.split(' ')
            extra_msg = ' '.join(msg_raw_parts[1:]) if len(msg_raw_parts) > 1 else None
            if command in self.commands:
                if source.level >= self.commands[command].level:
                    self.commands[command].run(self, source, extra_msg, event)
                    return

        source.num_lines += 1
        source.needs_sync = True

    def on_whisper(self, chatconn, event):
        # We use .lower() in case twitch ever starts sending non-lowercased usernames
        source = self.users[event.source.user.lower()]

        if source.level >= 500:
            # Only moderators and above can send commands through whispers
            self.parse_message(event.arguments[0], source, event)

    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

        # We use .lower() in case twitch ever starts sending non-lowercased usernames
        source = self.users[event.source.user.lower()]

        cur_time = time.time()

        msg = event.arguments[0]
        msg_len = len(msg)

        if msg_len > 70:
            non_alnum = sum(not c.isalnum() for c in msg)
            ratio = non_alnum/msg_len

            log.debug('Ascii ratio: {0}'.format(ratio))
            if self.settings['ban_ascii']:
                if (msg_len > 140 and ratio > 0.8) or ratio > 0.91:
                    log.debug('Timeouting {0} because of a high ascii ratio ({1}). Message length: {2}'.format(source.username, ratio, msg_len))
                    self.timeout(source.username, 120)
                    self.whisper(source.username, 'You have been timed out for 120 seconds because your message contained too many ascii characters.')
                    return

            if self.settings['ban_msg_length']:
                if msg_len > 450:
                    log.debug('Timeouting {0} because of a message length: {1}'.format(source.username, msg_len))
                    self.timeout(source.username, 20)
                    self.whisper(source.username, 'You have been timed out for 20 seconds because your message was too long.')
                    return

        if cur_time - self.last_sync >= 60:
            self.sync_to()
            self.last_sync = cur_time

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

    def quit(self):
        self.sync_to()
        if self.phrases['quit']:
            phrase_data = {
                    'nickname': self.nickname,
                    'version': self.version,
                    }

            try:
                self.say(self.phrases['quit'].format(**phrase_data))
            except Exception as e:
                log.error(e)

        if self.twitter_stream:
            self.twitter_stream.disconnect()

        self.connection.quit('bye')
        if self.whisper_conn:
            self.whisper_conn.connection.quit('bye')

        sys.exit(0)
예제 #5
0
    def __init__(self, config, args):
        self.config = config

        self.sqlconn = pymysql.connect(unix_socket=config['sql']['unix_socket'], user=config['sql']['user'], passwd=config['sql']['passwd'], db=config['sql']['db'], charset='utf8')
        self.sqlconn.autocommit(True)

        update_database(self.sqlconn)

        self.load_default_phrases()

        self.reactor = irc.client.Reactor()
        self.connection = self.reactor.server()

        self.twitchapi = TwitchAPI(type='api')

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

        if 'wolfram' in config['main']:
            Dispatch.wolfram = wolframalpha.Client(config['main']['wolfram'])
        else:
            wolfram = None

        self.whisper_conn = None

        TyggBot.instance = self

        self.base_path = os.path.dirname(os.path.realpath(__file__))
        self.data = {}
        self.data_cb = {}

        self.data_cb['status_length'] = self.c_status_length
        self.data_cb['stream_status'] = self.c_stream_status
        self.data_cb['time_norway'] = self.c_time_norway
        self.data_cb['bot_uptime'] = self.c_uptime
        self.data_cb['time_since_latest_deck'] = self.c_time_since_latest_deck
        self.ignores = []

        self.start_time = datetime.now()

        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.kvi = KVIData(self.sqlconn)
        self.tbm = TBMath()
        self.last_sync = time.time()

        self.users = UserManager(self.sqlconn)

        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

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

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

        self.sync_from()

        self.nickname = config['main']['nickname']
        self.password = config['main']['password']
        self.reconnection_interval = 5

        self.load_all()

        self.whisper_conn = WhisperConn(self.streamer, self.nickname, self.password, self.reactor)
        self.whisper_conn.connect()

        self.num_commands_sent = 0
        self.connection.execute_every(30, self.reset_command_throttle)

        self.twitter_stream = False
        if 'twitter' in config:
            self.use_twitter_stream = 'streaming' in config['twitter'] and config['twitter']['streaming'] == '1'
            self.init_twitter()
        else:
            self.twitter = None

        self.connection.execute_every(1, self.shift_emotes)

        self.ws_clients = []
        if 'websocket' in config and config['websocket']['enabled'] == '1':
            self.init_websocket_server()
            self.execute_every(1, self.refresh_emote_data)

        self.urls_to_check = queue.Queue()
        self.connection.execute_every(1, self.check_urls)
예제 #6
0
from kvidata import KVIData
from models.user import User
from models.user import UserManager

sys.path.insert(0, os.path.dirname(os.path.realpath(__file__ + "/../")))
os.chdir(os.path.dirname(os.path.realpath(__file__ + "/../")))

config = configparser.ConfigParser()

config.read("config.ini")

sqlconn = pymysql.connect(
    unix_socket=config["sql"]["unix_socket"],
    user=config["sql"]["user"],
    passwd=config["sql"]["passwd"],
    db=config["sql"]["db"],
    charset="utf8",
)
kvi = KVIData(sqlconn)

users = UserManager(sqlconn)

os.chdir(os.path.dirname(os.path.realpath(__file__)))

for nl in kvi.fetch_all("nl"):
    user = users[nl["key"]]
    user.num_lines = nl["value"]
    user.needs_sync = True

users.sync()