From 60cf61d81aef15b777fed434d9344478bbe33a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikola=20Forr=C3=B3?= Date: Tue, 22 May 2018 15:29:02 +0200 Subject: [PATCH] Refactor everything and implement Discord client --- bot.py | 290 +++++--------------------- clients/discord.py | 56 +++++ clients/twitch.py | 170 +++++++++++++++ commands.py | 149 +++++++++++++ docker-compose.yaml.example | 2 +- requirements.txt | 1 + instagram.py => services/instagram.py | 25 ++- twitch.py => services/twitch.py | 17 +- youtube.py => services/youtube.py | 27 ++- settings.cfg.example | 19 +- 10 files changed, 480 insertions(+), 276 deletions(-) create mode 100644 clients/discord.py create mode 100644 clients/twitch.py create mode 100644 commands.py rename instagram.py => services/instagram.py (74%) rename twitch.py => services/twitch.py (85%) rename youtube.py => services/youtube.py (61%) diff --git a/bot.py b/bot.py index 7c19556..2c6f436 100644 --- a/bot.py +++ b/bot.py @@ -1,254 +1,60 @@ +import asyncio import configparser -import functools import logging import os -import random -import re -import string -import dateutil.parser -import irc.bot -import requests +from commands import Commands +from clients.discord import DiscordClient +from clients.twitch import TwitchClient -from instagram import Instagram -from twitch import Twitch -from youtube import Youtube - -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'^!(bella(gram|pics)|insta(gram|bella))$'), self.bellagram), - (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), - (re.compile(r'^!yt\s+(?P")?(?P.+)(?(q)")$'), self.query_youtube), - ] - self.server = config['IRC'].get('server', 'irc.chat.twitch.tv') - self.port = config['IRC'].getint('port', 6667) - self.nickname = config['IRC'].get('nickname') - 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') - self.bellagrams = self._get_bellagrams() - 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 _get_bellagrams(self): - username = config['Instagram'].get('username') - keywords = config['Instagram'].get('keywords').split(',') - media = Instagram(username, log).get_media() - if not media: - return None - return [m for m in media if [k for k in keywords if k.lower() in m['text'].lower()]] - - def _respond(self, connection, event, msg): - if event.target.startswith('#'): - connection.privmsg(event.target, msg) - else: - connection.privmsg('#jtv', '/w {0} {1}'.format(event.source.nick, msg)) - - def on_welcome(self, connection, event): - connection.cap('REQ', ':twitch.tv/membership') - connection.cap('REQ', ':twitch.tv/tags') - connection.cap('REQ', ':twitch.tv/commands') - for channel in config['IRC'].get('channels').split(','): - channel = '#{0}'.format(channel) - log.info('Joining %s', channel) - connection.join(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() - respond = functools.partial(self._respond, connection, event) - for pattern, action in self.patterns + self.commands: - m = pattern.match(message) - if m: - action(tags, respond, **m.groupdict()) - - def get(self, params): - r = requests.get('{0}/quotes'.format(self.api_url), params=params) - r.raise_for_status() - return r - - 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 - - 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 - - def bellagram(self, tags, respond, **kwargs): - if not self.bellagrams: - respond('Sorry @{0}, couldn\'t get any media from Instagram'.format(tags['display-name'])) - else: - respond(random.choice(self.bellagrams)['url']) - - def last_quote(self, tags, respond, **kwargs): - try: - quotes = self.get(dict( - sort_by='id', - sort_order='desc', - page_size=1)).json() - quote = quotes[0] - except (requests.exceptions.HTTPError, IndexError): - respond('Sorry @{0}, no quotes found'.format(tags['display-name'])) - else: - respond('!quote {0}'.format(quote['id'])) - - def find_quote(self, tags, respond, filter, **kwargs): - if len(filter) < 3: - respond('Sorry @{0}, the search phrase is too short'.format(tags['display-name'])) - return - try: - quotes = self.get(dict( - filter=filter, - sort_order='random', - page_size=1)).json() - quote = quotes[0] - except (requests.exceptions.HTTPError, IndexError): - respond('Sorry @{0}, no quotes found'.format(tags['display-name'])) - else: - respond('!quote {0}'.format(quote['id'])) - - def sync(self, tags, respond, since=None, **kwargs): - if int(tags['user-id']) != self.master_user_id: - respond('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) - return - if since is None: - try: - quotes = self.get(dict( - sort_by='id', - sort_order='desc', - page_size=1)).json() - 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(None, None, **m.groupdict()) - respond('@{0}: sync completed'.format(tags['display-name'])) - - def query_youtube(self, tags, respond, query, **kwargs): - api_key = config['Youtube'].get('api_key') - channel_id = config['Youtube'].get('channel_id') - yt = Youtube(api_key) - items = yt.search(channel_id, query, playlists=True, limit=1) - if not items: - items = yt.search(channel_id, query, playlists=False, limit=1) - if not items: - respond('Sorry @{0}, couldn\'t find anything on Youtube'.format(tags['display-name'])) - else: - respond('{0}: {1}'.format(items[0]['title'], items[0]['url'])) - - def add_quote(self, tags, respond, 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, tags, respond, 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, tags, respond, 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)) +TIMEOUT = 0.2 def main(): - bot = TwitchBot() - bot.start() + config = configparser.ConfigParser() + config.read('settings.cfg') + + level = logging.DEBUG if bool(int(os.getenv('DEBUG', 0))) else logging.INFO + + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('CMD: %(levelname)s: %(message)s')) + commands_logger = logging.getLogger('commands') + commands_logger.addHandler(handler) + commands_logger.setLevel(level) + + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('DISCORD: %(levelname)s: %(message)s')) + discord_logger = logging.getLogger('discord') + discord_logger.addHandler(handler) + discord_logger.setLevel(level) + + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter('TWITCH: %(levelname)s: %(message)s')) + twitch_logger = logging.getLogger('irc.client') + twitch_logger.addHandler(handler) + twitch_logger.setLevel(level) + + commands = Commands(config, commands_logger) + + discord_client = DiscordClient(config, discord_logger, commands) + + async def run_twitch_client(): + twitch_client = TwitchClient(config, twitch_logger, commands) + twitch_client.connect_() + while True: + twitch_client.process_data() + await asyncio.sleep(TIMEOUT) + + asyncio.ensure_future(run_twitch_client()) + + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(discord_client.start_()) + except: + loop.run_until_complete(discord_client.logout()) + finally: + loop.close() if __name__ == "__main__": diff --git a/clients/discord.py b/clients/discord.py new file mode 100644 index 0000000..a4e98bf --- /dev/null +++ b/clients/discord.py @@ -0,0 +1,56 @@ +import re + +import discord + +from commands import CommandError + + +class DiscordClient(discord.Client): + + def __init__(self, config, logger, commands): + self.config = config + self.logger = logger + self.commands = commands + self.supported_commands = [ + (re.compile(r'^!(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram), + (re.compile(r'^!yt\s+(?P")?(?P.+)(?(q)")$'), self._do_yt), + ] + super(DiscordClient, self).__init__() + + async def start_(self): + token = self.config['Discord'].get('token') + await self.start(token) + + async def on_ready(self): + self.logger.info('Logged in as {0}'.format(self.user.name)) + + async def on_message(self, message): + for pattern, action in self.supported_commands: + m = pattern.match(message.content) + if m: + await action(message, **m.groupdict()) + + async def _do_bellagram(self, message, **kwargs): + try: + bellagram = self.commands.bellagram() + except CommandError as e: + await self.send_message(message.channel, 'Sorry {0}, {1}'.format(message.author.mention, e)) + else: + embed = discord.Embed(title=bellagram['title'], url=bellagram['url'], color=0x8545bc) + embed.set_image(url=bellagram['display_url']) + embed.set_author(name=bellagram['owner'], url=bellagram['owner_url'], + icon_url=bellagram['owner_pic_url']) + await self.send_message(message.channel, embed=embed) + + async def _do_yt(self, message, query, **kwargs): + try: + result = self.commands.query_youtube(query) + except CommandError as e: + await self.send_message(message.channel, 'Sorry {0}, {1}'.format(message.author.mention, e)) + else: + embed = discord.Embed(title=result['title'], url=result['url'], + description=result['description'], color=0xff0000) + embed.set_thumbnail(url=result['thumbnail_url']) + embed.set_author(name=result['channel_title'], url=result['channel_url'], + icon_url=result['channel_thumbnail_url']) + await self.send_message(message.channel, embed=embed) diff --git a/clients/twitch.py b/clients/twitch.py new file mode 100644 index 0000000..16e60ed --- /dev/null +++ b/clients/twitch.py @@ -0,0 +1,170 @@ +import functools +import re +import string + +import irc.bot + +from commands import CommandError + + +# $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 TwitchClient(irc.bot.SingleServerIRCBot): + def __init__(self, config, logger, commands): + self.config = config + self.logger = logger + self.commands = commands + self.patterns = [ + (QUOTE_ADDED_PATTERN, self._add_quote), + (QUOTE_EDITED_PATTERN, self._edit_quote), + (QUOTE_REMOVED_PATTERN, self._remove_quote), + ] + self.supported_commands = [ + (re.compile(r'^!lastquote$'), self._do_lastquote), + (re.compile(r'^!findquote\s+(?P")?(?P.+)(?(q)")$'), self._do_findquote), + (re.compile(r'^!sync(\s+(?P.+))?$'), self._do_sync), + (re.compile(r'^!(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram), + (re.compile(r'^!yt\s+(?P")?(?P.+)(?(q)")$'), self._do_yt), + ] + server = self.config['IRC'].get('server') + port = self.config['IRC'].getint('port') + nickname = self.config['IRC'].get('nickname') + token = self.config['Twitch'].get('token') + self.logger.info('Connecting to %s:%d', server, port) + super(TwitchClient, self).__init__([(server, port, token)], nickname, nickname) + + def connect_(self): + self._connect() + + def process_data(self): + self.reactor.process_once() + + def on_welcome(self, connection, event): + connection.cap('REQ', ':twitch.tv/membership') + connection.cap('REQ', ':twitch.tv/tags') + connection.cap('REQ', ':twitch.tv/commands') + for channel in self.config['IRC'].get('channels').split(','): + channel = '#{0}'.format(channel) + self.logger.info('Joining %s', channel) + connection.join(channel) + + def on_join(self, connection, event): + self.logger.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 _send_response(self, connection, event, msg): + if event.target.startswith('#'): + connection.privmsg(event.target, msg) + else: + connection.privmsg('#jtv', '/w {0} {1}'.format(event.source.nick, msg)) + + 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() + send_response = functools.partial(self._send_response, connection, event) + for pattern, action in self.patterns + self.supported_commands: + m = pattern.match(message) + if m: + action(tags, send_response, **m.groupdict()) + + def _add_quote(self, tags, send_response, user, id, text, game, date, **kwargs): + if text[0] == text[-1] == '"': + text = text[1:-1] + self.logger.info('Adding quote %s: %s', id, text) + try: + self.commands.add_quote(id, text, game, date) + except CommandError as e: + self.logger.error('Failed to add quote: %s', e) + + def _edit_quote(self, tags, send_response, user, id, text, game, date, **kwargs): + if text[0] == text[-1] == '"': + text = text[1:-1] + self.logger.info('Editing quote %s: %s', id, text) + try: + self.commands.edit_quote(id, text, game, date) + except CommandError as e: + self.logger.error('Failed to add quote: %s', e) + + def _remove_quote(self, tags, send_response, user, id, **kwargs): + self.logger.info('Removing quote %s', id) + try: + self.commands.remove_quote(id) + except CommandError as e: + self.logger.error('Failed to remove quote: %s', e) + + def _do_lastquote(self, tags, send_response, **kwargs): + try: + quote = self.commands.last_quote() + except CommandError as e: + send_response('Sorry @{0}, {1}'.format(tags['display-name'], e)) + else: + send_response('!quote {0}'.format(quote['id'])) + + def _do_findquote(self, tags, send_response, filter, **kwargs): + try: + quote = self.commands.find_quote(filter) + except CommandError as e: + send_response('Sorry @{0}, {1}'.format(tags['display-name'], e)) + else: + send_response('!quote {0}'.format(quote['id'])) + + def _do_sync(self, tags, send_response, since=None, **kwargs): + master_user_id = self.config['Twitch'].getint('master_user_id') + if int(tags['user-id']) != self.master_user_id: + respond('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) + return + try: + messages = self.commands.get_twitch_messages(since) + except CommandError as e: + self.logger.error('Failed to get Twitch messages: %s', e) + else: + for message in messages: + for pattern, action in self.patterns: + m = pattern.match(message) + if m: + action(tags, send_response, **m.groupdict()) + + def _do_bellagram(self, tags, send_response, **kwargs): + try: + bellagram = self.commands.bellagram() + except CommandError as e: + send_response('Sorry @{0}, {1}'.format(tags['display-name'], e)) + else: + send_response(bellagram['url']) + + def _do_yt(self, tags, send_response, query, **kwargs): + try: + result = self.commands.query_youtube(query) + except CommandError as e: + send_response('Sorry @{0}, {1}'.format(tags['display-name'], e)) + else: + send_response('{0}: {1}'.format(result['title'], result['url'])) diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..63261be --- /dev/null +++ b/commands.py @@ -0,0 +1,149 @@ +import random + +import dateutil.parser +import requests + +from services.instagram import Instagram, InstagramError +from services.twitch import Twitch, TwitchError +from services.youtube import Youtube, YoutubeError + + +class CommandError(Exception): + pass + + +class Commands(object): + def __init__(self, config, logger): + self.config = config + self.logger = logger + self._bellagrams = self._collect_bellagrams() + + def add_quote(self, id, text, game, date): + try: + self._post_quotes(dict( + id=int(id), + date=dateutil.parser.parse(date, dayfirst=True).date().isoformat(), + game=game, + text=text)) + except requests.exceptions.HTTPError as e: + raise CommandError(e) + + def edit_quote(self, id, text, game, date): + try: + self._post_quotes(dict( + id=int(id), + date=dateutil.parser.parse(date, dayfirst=True).date().isoformat(), + game=game, + text=text)) + except requests.exceptions.HTTPError as e: + raise CommandError(e) + + def remove_quote(self, id): + try: + self._delete_quotes(int(id)) + except requests.exceptions.HTTPError as e: + raise CommandError(e) + + def get_twitch_messages(self, since): + if since is None: + try: + quotes = self._get_quotes(dict( + sort_by='id', + sort_order='desc', + page_size=1)).json() + quote = quotes.pop(0) + except requests.exceptions.HTTPError: + raise CommandError(e) + except IndexError: + raise CommandError(e) + else: + since = quote['date'] + api_url = self.config['Twitch'].get('api_url') + client_id = self.config['Twitch'].get('client_id') + user_id = self.config['Twitch'].getint('target_user_id') + since = dateutil.parser.parse(since).date() + twitch = Twitch(api_url, client_id) + try: + return twitch.get_messages(user_id, since) + except TwitchError as e: + raise CommandError(e) + + def last_quote(self): + try: + quotes = self._get_quotes(dict( + sort_by='id', + sort_order='desc', + page_size=1)).json() + quote = quotes.pop(0) + except (requests.exceptions.HTTPError, IndexError): + raise CommandError('no quotes found') + else: + return quote + + def find_quote(self, filter): + if len(filter) < 3: + raise CommandError('the search phrase is too short') + try: + quotes = self._get_quotes(dict( + filter=filter, + sort_order='random', + page_size=1)).json() + quote = quotes.pop(0) + except (requests.exceptions.HTTPError, IndexError): + raise CommandError('no quotes found') + else: + return quote + + def bellagram(self): + if not self._bellagrams: + raise CommandError('couldn\'t get any media from Instagram') + return random.choice(self._bellagrams) + + def query_youtube(self, query): + api_key = self.config['Youtube'].get('api_key') + channel_id = self.config['Youtube'].get('channel_id') + yt = Youtube(api_key) + try: + results = yt.search(channel_id, query, playlists=True, limit=1) + if not results: + results = yt.search(channel_id, query, playlists=False, limit=1) + result = results.pop(0) + except (YoutubeError, IndexError): + raise CommandError('couldn\'t find anything on Youtube') + else: + return result + + def _get_quotes(self, params): + api_url = self.config['Quotes'].get('api_url') + r = requests.get('{0}/quotes'.format(api_url), params=params) + r.raise_for_status() + return r + + def _post_quotes(self, data): + api_url = self.config['Quotes'].get('api_url') + api_key = self.config['Quotes'].get('api_key') + r = requests.post('{0}/quotes'.format(api_url), data=data, + headers={'X-Quotes-API-Key': api_key}) + r.raise_for_status() + return r + + def _delete_quotes(self, id): + api_url = self.config['Quotes'].get('api_url') + api_key = self.config['Quotes'].get('api_key') + r = requests.delete('{0}/quotes/{1}'.format(api_url, id), + headers={'X-Quotes-API-Key': api_key}) + r.raise_for_status() + return r + + def _collect_bellagrams(self): + username = self.config['Instagram'].get('username') + keywords = self.config['Instagram'].get('keywords').split(',') + instagram = Instagram(username) + try: + media = instagram.get_media() + media = [m for m in media if [k for k in keywords \ + if m['type'] == 'Image' and k.lower() in m['title'].lower()]] + except (InstagramError, IndexError): + return None + else: + return media diff --git a/docker-compose.yaml.example b/docker-compose.yaml.example index 3e467ae..e38e2e8 100644 --- a/docker-compose.yaml.example +++ b/docker-compose.yaml.example @@ -5,6 +5,6 @@ services: build: context: . volumes: - - /twitch-bot/settings.cfg:/bot/settings.cfg + - /bot/settings.cfg:/bot/settings.cfg environment: - DEBUG=0 diff --git a/requirements.txt b/requirements.txt index b7104f4..15d9aef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +discord.py google-api-python-client irc python-dateutil diff --git a/instagram.py b/services/instagram.py similarity index 74% rename from instagram.py rename to services/instagram.py index e9c2e7c..602bee7 100644 --- a/instagram.py +++ b/services/instagram.py @@ -12,15 +12,21 @@ QUERY_HASH = '42323d64886122307be10013ad2dcc44' SHARED_DATA = re.compile(r'window\._sharedData = (\{.*\});') -class Instagram(object): +class InstagramError(Exception): + pass + - def __init__(self, username, log=None): +class Instagram(object): + def __init__(self, username): self.username = username - self.log = log shared_data = self._get_shared_data() try: - self.user_id = shared_data['entry_data']['ProfilePage'][0]['graphql']['user']['id'] + graphql = shared_data['entry_data']['ProfilePage'][0]['graphql'] + self.user_id = graphql['user']['id'] self.rhx_gis = shared_data['rhx_gis'] + self.owner = dict( + name=graphql['user']['username'], + profile_pic_url=graphql['user']['profile_pic_url']) except (IndexError, KeyError, TypeError): self.user_id = None self.rhx_gis = None @@ -59,9 +65,12 @@ class Instagram(object): for edge in data['edges']: result.append(dict( type=edge['node']['__typename'].split('Graph')[1], - text=edge['node']['edge_media_to_caption']['edges'][0]['node']['text'], + title=edge['node']['edge_media_to_caption']['edges'][0]['node']['text'], url='{0}/p/{1}'.format(BASE_URL, edge['node']['shortcode']), - display_url=edge['node']['display_url'])) + display_url=edge['node']['display_url'], + owner=self.owner['name'], + owner_url='{0}/{1}'.format(BASE_URL, self.username), + owner_pic_url=self.owner['profile_pic_url'])) cursor = data['page_info']['end_cursor'] if not cursor: break @@ -76,8 +85,6 @@ class Instagram(object): try: result = self._get_media() except requests.exceptions.HTTPError as e: - if self.log: - self.log.error('Failed to retrieve media: %s', str(e)) - return None + raise InstagramError('Failed to retrieve media: {0}'.format(e)) else: return result diff --git a/twitch.py b/services/twitch.py similarity index 85% rename from twitch.py rename to services/twitch.py index 6a12ae6..85b8fdd 100644 --- a/twitch.py +++ b/services/twitch.py @@ -2,11 +2,14 @@ import dateutil.parser import requests +class TwitchError(Exception): + pass + + class Twitch(object): - def __init__(self, api_url, client_id, log=None): + def __init__(self, api_url, client_id): self.api_url = api_url self.client_id = client_id - self.log = log def _get_videos(self, user_id): def request(offset, limit): @@ -66,14 +69,10 @@ class Twitch(object): videos = self._get_videos(user_id) result = [] for video in [v for v in videos if v['date'] >= since]: - if self.log: - self.log.info('Processing VOD %d (%s)', video['id'], video['title']) comments = self._get_comments(video['id']) result.extend(comments) except requests.exceptions.HTTPError as e: - if self.log: - self.log.error('Failed to retrieve VOD messages: %s', str(e)) - return None + raise TwitchError('Failed to retrieve VOD messages: {0}'.format(e)) else: return result @@ -81,6 +80,4 @@ class Twitch(object): try: return self._get_stream_info(user_id) except requests.exceptions.HTTPError as e: - if self.log: - self.log.error('Failed to get stream info: %s', str(e)) - return None + raise TwitchError('Failed to get stream info: {0}'.format(e)) diff --git a/youtube.py b/services/youtube.py similarity index 61% rename from youtube.py rename to services/youtube.py index 4d6675d..32c981d 100644 --- a/youtube.py +++ b/services/youtube.py @@ -5,12 +5,24 @@ import googleapiclient.discovery BASE_URL = 'https://www.youtube.com' +class YoutubeError(Exception): + pass + + class Youtube(object): - def __init__(self, api_key, log=None): + def __init__(self, api_key): self.client = googleapiclient.discovery.build('youtube', 'v3', developerKey=api_key) - self.log = log def _search(self, channel_id, query, playlists, limit): + def get_thumbnail_url(thumbnails): + for key in ['high', 'medium', 'default']: + if key in thumbnails: + return thumbnails[key]['url'] + resp = self.client.channels().list( + id=channel_id, + maxResults=1, + part='snippet').execute() + channel = resp['items'][0] result = [] count = limit token = '' @@ -32,7 +44,12 @@ class Youtube(object): result.append(dict( kind=kind, url=url, - title=item['snippet']['title'])) + title=item['snippet']['title'], + description=item['snippet']['description'], + thumbnail_url=get_thumbnail_url(item['snippet']['thumbnails']), + channel_title=channel['snippet']['title'], + channel_url='{0}/c/{1}'.format(BASE_URL, channel['snippet']['customUrl']), + channel_thumbnail_url=get_thumbnail_url(channel['snippet']['thumbnails']))) count -= resp['pageInfo']['resultsPerPage'] if count <= 0: break @@ -45,6 +62,4 @@ class Youtube(object): try: return self._search(channel_id, query, playlists, limit) except googleapiclient.errors.HttpError as e: - if self.log: - self.log.error('Failed to query Youtube API: %s', str(e)) - return None + raise YoutubeError('Failed to query Youtube API: {}'.format(e)) diff --git a/settings.cfg.example b/settings.cfg.example index 9d2b0cc..d7e7614 100644 --- a/settings.cfg.example +++ b/settings.cfg.example @@ -4,14 +4,8 @@ port = 6667 nickname = spooky_lurker channels = lilialil -[Instagram] -username = lilialovesgames -keywords = bella,teeny,kitty,cat,😹,😻,🐱,🐈 - -[Youtube] -api_key = __GOOGLE_API_KEY__ -# ladylilia -channel_id = UC5970RJMoEcRNZl0MNp8tlQ +[Discord] +token = __DISCORD_TOKEN__ [Twitch] api_url = https://api.twitch.tv/v5 @@ -22,6 +16,15 @@ target_user_id = 92737529 # nikola_f master_user_id = 210957066 +[Instagram] +username = lilialovesgames +keywords = #bellameeze,bella,teeny,kitty,cat,😹,😻,🐱,🐈 + +[Youtube] +api_key = __GOOGLE_API_KEY__ +# ladylilia +channel_id = UC5970RJMoEcRNZl0MNp8tlQ + [Quotes] api_url = https://ladylilia.com/quotes/api api_key = __QUOTES_API_KEY__