Implement !clip command

master
Nikola Forró 6 years ago
parent 15a659cfd1
commit d30e01ba9f

@ -8,6 +8,8 @@ using [HTTP API](https://gitea.brno.mraveniste.cc/turbotraktor/ladylilia.com/src
* `!bellagram`, `!bellapics`, `!instabella`, `!instagram` - posts a link to a random Instagram picture of Bella * `!bellagram`, `!bellapics`, `!instabella`, `!instagram` - posts a link to a random Instagram picture of Bella
* `!yt QUERY` - queries Lady Lilia's Youtube channel and posts a link to the most relevant result * `!yt QUERY` - queries Lady Lilia's Youtube channel and posts a link to the most relevant result
- `QUERY` can contain `|` (logical **or**) and `-` (logical **not**) operators, for example: `!yt oblivion -nehrim` - `QUERY` can contain `|` (logical **or**) and `-` (logical **not**) operators, for example: `!yt oblivion -nehrim`
* `!clip PATTERN` - searches for Twitch clips matching `PATTERN` and in case multiple matches are found, returns one of them randomly
- `PATTERN` has to be at least 3 characters long and it can be enclosed in double quotes in case it contains spaces
### Twitch-only commands ### Twitch-only commands

@ -37,6 +37,7 @@ class DiscordClient(discord.Client):
self.supported_commands = [ self.supported_commands = [
(re.compile(r'^(?P<prefix>!|\?)(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram), (re.compile(r'^(?P<prefix>!|\?)(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram),
(re.compile(r'^(?P<prefix>!|\?)yt\s+(?P<q>")?(?P<query>.+)(?(q)")$'), self._do_yt), (re.compile(r'^(?P<prefix>!|\?)yt\s+(?P<q>")?(?P<query>.+)(?(q)")$'), self._do_yt),
(re.compile(r'^(?P<prefix>!|\?)clip\s+(?P<q>")?(?P<filter>.+)(?(q)")$'), self._do_clip),
] ]
super(DiscordClient, self).__init__() super(DiscordClient, self).__init__()
@ -90,3 +91,18 @@ class DiscordClient(discord.Client):
await self.send_message(message.channel, embed=embed) await self.send_message(message.channel, embed=embed)
elif prefix == '?': elif prefix == '?':
await self.send_message(message.channel, result['url']) await self.send_message(message.channel, result['url'])
@cooldown(retries=3, timeout=5*60, failure=_cooldown_failure)
async def _do_clip(self, user, message, filter, prefix, **kwargs):
try:
result = self.commands.find_clip(filter)
except CommandError as e:
await self.send_message(message.channel, 'Sorry {0}, {1}'.format(message.author.mention, e))
else:
if prefix == '!':
embed = discord.Embed(title=result['title'], url=result['url'], color=0xff5733)
embed.set_thumbnail(url=result['thumbnail_medium'])
embed.set_author(name=result['curator_display_name'], icon_url=result['curator_logo'])
await self.send_message(message.channel, embed=embed)
elif prefix == '?':
await self.send_message(message.channel, result['url'])

@ -48,6 +48,7 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
(re.compile(r'^!syncquotes$'), self._do_syncquotes), (re.compile(r'^!syncquotes$'), self._do_syncquotes),
(re.compile(r'^!(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram), (re.compile(r'^!(bella(gram|pics)|insta(gram|bella))$'), self._do_bellagram),
(re.compile(r'^!yt\s+(?P<q>")?(?P<query>.+)(?(q)")$'), self._do_yt), (re.compile(r'^!yt\s+(?P<q>")?(?P<query>.+)(?(q)")$'), self._do_yt),
(re.compile(r'^!clip\s+(?P<q>")?(?P<filter>.+)(?(q)")$'), self._do_clip),
] ]
server = self.config['IRC'].get('server') server = self.config['IRC'].get('server')
port = self.config['IRC'].getint('port') port = self.config['IRC'].getint('port')
@ -143,9 +144,9 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
respond('Sorry @{0}, you are not allowed to do this'.format(tags['display-name'])) respond('Sorry @{0}, you are not allowed to do this'.format(tags['display-name']))
return return
try: try:
messages = self.commands.get_twitch_messages() messages = self.commands.get_quote_messages()
except CommandError as e: except CommandError as e:
self.logger.error('Failed to get Twitch messages: %s', e) self.logger.error('Failed to get quote messages: %s', e)
else: else:
for message in messages: for message in messages:
for pattern, action in self.patterns: for pattern, action in self.patterns:
@ -168,3 +169,11 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
send_response('Sorry @{0}, {1}'.format(tags['display-name'], e)) send_response('Sorry @{0}, {1}'.format(tags['display-name'], e))
else: else:
send_response('{0}: {1}'.format(result['title'], result['url'])) send_response('{0}: {1}'.format(result['title'], result['url']))
def _do_clip(self, tags, send_response, filter, **kwargs):
try:
result = self.commands.find_clip(filter)
except CommandError as e:
send_response('Sorry @{0}, {1}'.format(tags['display-name'], e))
else:
send_response('{0}: {1}'.format(result['title'], result['url']))

@ -4,10 +4,12 @@ import dateutil.parser
import requests import requests
from services.instagram import Instagram, InstagramError from services.instagram import Instagram, InstagramError
from services.twitch import Twitch, TwitchError
from services.youtube import Youtube, YoutubeError from services.youtube import Youtube, YoutubeError
CLIPS_BASE_URL = 'https://clips.twitch.tv'
class CommandError(Exception): class CommandError(Exception):
pass pass
@ -44,15 +46,15 @@ class Commands(object):
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
raise CommandError(e) raise CommandError(e)
def get_twitch_messages(self): def get_quote_messages(self):
api_url = self.config['Twitch Logs'].get('api_url')
params = dict(commenter='bellateeny', term='quote')
try: try:
r = requests.get('{0}/search'.format(api_url), params=params) messages = self._get_messages(dict(
r.raise_for_status() commenter='bellateeny',
except requests.exceptions.HTTPError: term='quote'))
except requests.exceptions.HTTPError as e:
raise CommandError(e) raise CommandError(e)
return [m.get('message_body', '') for m in r.json()] else:
return [m.get('message_body', '') for m in messages]
def last_quote(self): def last_quote(self):
try: try:
@ -96,6 +98,21 @@ class Commands(object):
else: else:
return result return result
def find_clip(self, filter):
if len(filter) < 3:
raise CommandError('the search phrase is too short')
try:
clips = self._get_clips(dict(
filter=filter,
sort_order='random',
page_size=1)).json()
clip = clips.pop(0)
clip['url'] = '{0}/{1}'.format(CLIPS_BASE_URL, clip['slug'])
except (requests.exceptions.HTTPError, IndexError):
raise CommandError('no clips found')
else:
return clip
def _get_quotes(self, params): def _get_quotes(self, params):
api_url = self.config['Quotes'].get('api_url') api_url = self.config['Quotes'].get('api_url')
r = requests.get('{0}/quotes'.format(api_url), params=params) r = requests.get('{0}/quotes'.format(api_url), params=params)
@ -118,6 +135,18 @@ class Commands(object):
r.raise_for_status() r.raise_for_status()
return r return r
def _get_messages(self, params):
api_url = self.config['Twitch'].get('cache_api_url')
r = requests.get('{0}/search'.format(api_url), params=params)
r.raise_for_status()
return r
def _get_clips(self, params):
api_url = self.config['Twitch'].get('cache_api_url')
r = requests.get('{0}/clips'.format(api_url), params=params)
r.raise_for_status()
return r
def _collect_bellagrams(self): def _collect_bellagrams(self):
username = self.config['Instagram'].get('username') username = self.config['Instagram'].get('username')
keywords = self.config['Instagram'].get('keywords').split(',') keywords = self.config['Instagram'].get('keywords').split(',')

@ -1,83 +0,0 @@
import dateutil.parser
import requests
class TwitchError(Exception):
pass
class Twitch(object):
def __init__(self, api_url, client_id):
self.api_url = api_url
self.client_id = client_id
def _get_videos(self, channel_id):
def request(offset, limit):
url = '{0}/channels/{1}/videos'.format(self.api_url, channel_id)
params = dict(client_id=self.client_id, offset=offset, limit=limit)
r = requests.get(url, params=params)
r.raise_for_status()
return r.json()
result = []
data = request(0, 1)
total = data.get('_total', 0)
limit = 100
for offset in range(0, total, limit):
data = request(offset, limit)
for vid in data.get('videos', []):
result.append(dict(
id=int(vid['_id'].lstrip('v')),
title=vid['title'],
date=dateutil.parser.parse(vid['recorded_at']).date()))
return result
def _get_comments(self, video_id):
def request(cursor):
url = '{0}/videos/{1}/comments'.format(self.api_url, video_id)
params = dict(client_id=self.client_id, cursor=cursor)
r = requests.get(url, params=params)
r.raise_for_status()
return r.json()
result = []
cursor = ''
while True:
data = request(cursor)
for comment in data.get('comments', []):
result.append(comment['message']['body'])
cursor = data.get('_next')
if not cursor:
break
return result
def _get_stream_info(self, channel_id):
def request():
url = '{0}/streams/{1}'.format(self.api_url, channel_id)
params = dict(client_id=self.client_id)
r = requests.get(url, params=params)
r.raise_for_status()
return r.json()
data = request()
if data['stream'] is None:
return None
return dict(
id=int(data['stream']['_id']),
game=data['stream']['game'],
viewers=int(data['stream']['viewers']))
def get_messages(self, channel_id, since):
try:
videos = self._get_videos(channel_id)
result = []
for video in [v for v in videos if v['date'] >= since]:
comments = self._get_comments(video['id'])
result.extend(comments)
except requests.exceptions.HTTPError as e:
raise TwitchError('Failed to retrieve VOD messages: {0}'.format(e))
else:
return result
def get_stream_info(self, channel_id):
try:
return self._get_stream_info(channel_id)
except requests.exceptions.HTTPError as e:
raise TwitchError('Failed to get stream info: {0}'.format(e))

@ -64,7 +64,7 @@ class Youtube(object):
try: try:
return self._search(channel_id, query, playlists, limit) return self._search(channel_id, query, playlists, limit)
except googleapiclient.errors.HttpError as e: except googleapiclient.errors.HttpError as e:
raise YoutubeError('Failed to query Youtube API: {}'.format(e)) raise YoutubeError('Failed to query Youtube API: {0}'.format(e))
def find_best_match(self, channel_ids, query): def find_best_match(self, channel_ids, query):
results = [] results = []
@ -73,7 +73,7 @@ class Youtube(object):
results.extend(self._search(channel_id, query, playlists=True, limit=1)) results.extend(self._search(channel_id, query, playlists=True, limit=1))
results.extend(self._search(channel_id, query, playlists=False, limit=1)) results.extend(self._search(channel_id, query, playlists=False, limit=1))
except googleapiclient.errors.HttpError as e: except googleapiclient.errors.HttpError as e:
raise YoutubeError('Failed to query Youtube API: {}'.format(e)) raise YoutubeError('Failed to query Youtube API: {0}'.format(e))
if not results: if not results:
return None return None
tokens = [t for t in query.split('|') if not t.strip().startswith('-')] or [''] tokens = [t for t in query.split('|') if not t.strip().startswith('-')] or ['']

@ -8,11 +8,8 @@ channels = lilialil
token = __DISCORD_TOKEN__ token = __DISCORD_TOKEN__
[Twitch] [Twitch]
api_url = https://api.twitch.tv/v5 cache_api_url = https://ladylilia.com/twitch-cache/api
client_id = __TWITCH_CLIENT_ID__
token = oauth:__TWITCH_OAUTH_TOKEN__ token = oauth:__TWITCH_OAUTH_TOKEN__
# lilialil
channel_id = 92737529
# nikola_f # nikola_f
master_user_id = 210957066 master_user_id = 210957066
@ -28,6 +25,3 @@ channel_ids = UC5970RJMoEcRNZl0MNp8tlQ,UCHNjavmkFUf2n0uzWSgj16w
[Quotes] [Quotes]
api_url = https://ladylilia.com/quotes/api api_url = https://ladylilia.com/quotes/api
api_key = __QUOTES_API_KEY__ api_key = __QUOTES_API_KEY__
[Twitch Logs]
api_url = https://ladylilia.com/twitch-logs/api

Loading…
Cancel
Save