import configparser import logging import os import re import string import dateutil.parser import irc.bot import requests from twitch import Twitch config = configparser.ConfigParser() config.read('settings.cfg') log = logging.getLogger('irc.client') log.addHandler(logging.StreamHandler()) log.setLevel(logging.DEBUG if bool(int(os.getenv('DEBUG', 0))) else logging.INFO) # $username --> Sweet! Thanks for the quote! #$id: $response QUOTE_ADDED_PATTERN = re.compile(r'''^ (?P.+)\s+-->\s+ Sweet!\s+Thanks\s+for\s+the\s+quote!\s+ \#(?P\d+):\s+ (?P.+)\s+ \[(?P.+)\]\s+ \[(?P.+)\]$''', re.VERBOSE) # $username --> Successfully edited Quote #$id: $response QUOTE_EDITED_PATTERN = re.compile(r'''^ (?P.+)\s+-->\s+ Successfully\s+edited\s+Quote\s+ \#(?P\d+):\s+ (?P.+)\s+ \[(?P.+)\]\s+ \[(?P.+)\]$''', re.VERBOSE) # $username --> Successfully deleted Quote #$id. QUOTE_REMOVED_PATTERN = re.compile(r'''^ (?P.+)\s+-->\s+ Successfully\s+deleted\s+Quote\s+ \#(?P\d+)\.$''', re.VERBOSE) class TwitchBot(irc.bot.SingleServerIRCBot): def __init__(self): self.patterns = [ (QUOTE_ADDED_PATTERN, self.add_quote), (QUOTE_EDITED_PATTERN, self.edit_quote), (QUOTE_REMOVED_PATTERN, self.remove_quote), ] self.commands = [ (re.compile(r'^!lastquote$'), self.last_quote), (re.compile(r'^!findquote\s+(?P")?(?P.+)(?(q)")$'), self.find_quote), (re.compile(r'^!sync(\s+(?P.+))?$'), self.sync), ] self.server = config['IRC'].get('server', 'irc.chat.twitch.tv') self.port = config['IRC'].getint('port', 6667) self.nickname = config['IRC'].get('nickname') self.channel = '#{0}'.format(config['IRC'].get('channel')) self.token = config['Twitch'].get('token') self.master_user_id = config['Twitch'].getint('master_user_id') self.api_url = config['Quotes'].get('api_url') self.api_key = config['Quotes'].get('api_key') log.info('Connecting to %s:%d', self.server, self.port) super(TwitchBot, self).__init__([(self.server, self.port, self.token)], self.nickname, self.nickname) def on_welcome(self, connection, event): connection.cap('REQ', ':twitch.tv/membership') connection.cap('REQ', ':twitch.tv/tags') connection.cap('REQ', ':twitch.tv/commands') log.info('Joining %s', self.channel) connection.join(self.channel) def on_join(self, connection, event): log.info('Joined %s', event.target) def on_pubmsg(self, connection, event): self.process_message(connection, event) def on_whisper(self, connection, event): self.process_message(connection, event) def process_message(self, connection, event): tags = {t['key']: t['value'] for t in event.tags} message = ''.join([c for c in event.arguments[0] if c in string.printable]) message = message.rstrip() for pattern, action in self.patterns + self.commands: m = pattern.match(message) if m: action(connection, tags, **m.groupdict()) def get(self, params): r = requests.get('{0}/quotes'.format(self.api_url), params=params) r.raise_for_status() return r.json() def post(self, data): r = requests.post('{0}/quotes'.format(self.api_url), data=data, headers={'X-Quotes-API-Key': self.api_key}) r.raise_for_status() return r.json() def delete(self, id): r = requests.delete('{0}/quotes/{1}'.format(self.api_url, id), headers={'X-Quotes-API-Key': self.api_key}) r.raise_for_status() return r.json() def last_quote(self, connection, tags, **kwargs): try: quotes = self.get(dict( sort_by='id', sort_order='desc', page_size=1)) quote = quotes[0] except (requests.exceptions.HTTPError, IndexError): msg = 'Sorry @{0}, no quotes found'.format(tags['display-name']) else: msg = '!quote {0}'.format(quote['id']) connection.privmsg(self.channel, msg) def find_quote(self, connection, tags, filter, **kwargs): if len(filter) < 3: msg = 'Sorry @{0}, the search phrase is too short'.format(tags['display-name']) connection.privmsg(self.channel, msg) return try: quotes = self.get(dict( filter=filter, sort_by='id', sort_order='desc', page_size=1)) quote = quotes[0] except (requests.exceptions.HTTPError, IndexError): msg = 'Sorry @{0}, no quotes found'.format(tags['display-name']) else: msg = '!quote {0}'.format(quote['id']) connection.privmsg(self.channel, msg) def sync(self, connection, tags, since=None, **kwargs): if int(tags['user-id']) != self.master_user_id: msg = 'Sorry @{0}, you are not allowed to do this'.format(tags['display-name']) connection.privmsg(self.channel, msg) return if since is None: try: quotes = self.get(dict( sort_by='id', sort_order='desc', page_size=1)) quote = quotes[0] except requests.exceptions.HTTPError as e: log.error('Failed to get quotes: %s', str(e)) return except IndexError: log.error('No quotes available') return else: since = quote['date'] api_url = config['Twitch'].get('api_url') client_id = config['Twitch'].get('client_id') user_id = config['Twitch'].getint('target_user_id') since = dateutil.parser.parse(since).date() messages = Twitch(api_url, client_id, log).get_messages(user_id, since) if not messages: return for message in messages: for pattern, action in self.patterns: m = pattern.match(message) if m: action(connection, None, **m.groupdict()) msg = '@{0}, sync completed'.format(tags['display-name']) connection.privmsg(self.channel, msg) def add_quote(self, connection, tags, user, id, text, game, date, **kwargs): if text[0] == text[-1] == '"': text = text[1:-1] log.info('Adding quote %s: %s', id, text) try: self.post(dict( id=int(id), date=dateutil.parser.parse(date, dayfirst=True).date().isoformat(), game=game, text=text)) except requests.exceptions.HTTPError as e: log.error('Failed to add quote: %s', str(e)) def edit_quote(self, connection, tags, user, id, text, game, date, **kwargs): if text[0] == text[-1] == '"': text = text[1:-1] log.info('Editing quote %s: %s', id, text) try: self.post(dict( id=int(id), date=dateutil.parser.parse(date, dayfirst=True).date().isoformat(), game=game, text=text)) except requests.exceptions.HTTPError as e: log.error('Failed to edit quote: %s', str(e)) def remove_quote(self, connection, tags, user, id, **kwargs): log.info('Removing quote %s', id) try: self.delete(int(id)) except requests.exceptions.HTTPError as e: log.error('Failed to remove quote: %s', str(e)) def main(): bot = TwitchBot() bot.start() if __name__ == "__main__": main()