async def quote_delete(bot, channel_id, cmd, args, **kwargs): if len(args) != 1: raise Send_error(f'Syntax error, use !{cmd} <number>') r = await bot.db.fetchone( 'SELECT enabled FROM twitch_quotes WHERE channel_id=%s and number=%s', ( channel_id, args[0], )) if not r: raise Send_error('Unknown quote') if r['enabled'] == 0: raise Send_error('This quote is deleted') await bot.db.execute( 'UPDATE twitch_quotes SET ' 'enabled=0 ' 'WHERE channel_id=%s AND number=%s', ( channel_id, args[0], )) raise Send_error('Quote deleted')
async def set_game(bot, channel_id, args, var_args, **kwargs): game = ' '.join(args) or ' '.join(var_args['set_game']) if not game: raise Send_error('You must specify a game or a category') game_id='' if game != '<unset>': data = await utils.twitch_channel_token_request( bot, channel_id, 'https://api.twitch.tv/helix/search/categories', params={'query': game}, ) if 'data' in data and data['data']: game_id = data['data'][0]['id'] game = data['data'][0]['name'] else: raise Send_error(f'No game or category found for {game}') try: await utils.twitch_channel_token_request( bot, channel_id, f'https://api.twitch.tv/helix/channels?broadcaster_id={channel_id}', method='PATCH', json={ 'game_id': game_id, }, ) except utils.Twitch_request_error as e: raise Send_error(f'Unable to change game/category. Error: {str(e)}') raise Send_error(f'Game/category changed to: {game}')
async def time(bot, var_args, **kwargs): if 'time' not in var_args or not var_args['time']: raise Send_error('A timezone must be specified with time. Example {time Europe/Copenhagen}') if var_args['time'][0] not in pytz.all_timezones: raise Send_error('Invalid timezone. Valid list: https://docs.botashell.com/vars/time') dt = pytz.utc.localize( datetime.utcnow(), ).astimezone(pytz.timezone(var_args['time'][0])) return { 'time': dt.strftime('%H:%M') }
async def quote_add(bot, channel_id, cmd, user, user_id, args, **kwargs): if len(args) == 0: raise Send_error(f'Syntax error, use !{cmd} your quote') c = await bot.db.execute( 'INSERT INTO twitch_quotes ' '(channel_id, created_by_user_id, created_by_user, ' 'message, enabled, created_at, updated_at) ' 'VALUES (%s, %s, %s, %s, %s, %s, %s)', (channel_id, user_id, user, ' '.join(args), 1, datetime.utcnow(), datetime.utcnow())) r = await bot.db.fetchone('SELECT number FROM twitch_quotes WHERE id=%s', (c.lastrowid, )) raise Send_error(f'Quote created with number: {r["number"]}')
async def lol(bot, channel, channel_id, args, var_args, **kwargs): if not var_args or not var_args.get('tft.summoner') or \ len(var_args['tft.summoner']) != 2: raise Send_error('{tft.summoner "<name>" "<region>"} is missing') headers = { 'X-Riot-Token': config['tft_apikey'], } summoner = var_args['tft.summoner'][0] region = var_args['tft.summoner'][1].lower() if region not in regions: raise Send_error( f'Invalid region "{region}". Valid regions: {", ".join(regions)}') url = f'https://{region.lower()}.api.riotgames.com/tft/summoner/v1/summoners/by-name/{summoner}' async with bot.ahttp.get(url, headers=headers) as r: if r.status >= 400: error = await r.text() raise Send_error(f'Riot error: {error}') d = await r.json() encrypted_id = d['id'] account_id = d['accountId'] result = { 'tft.summoner': '', 'tft.rank': 'Unranked', 'tft.tier': '', 'tft.lp': 0, 'tft.wins': 0, 'tft.losses': 0, } url = f'https://{region}.api.riotgames.com/tft/league/v1/entries/by-summoner/{encrypted_id}' async with bot.ahttp.get(url, headers=headers) as r: if r.status >= 400: error = await r.text() raise Send_error(f'Riot error: {error}') d = await r.json() for a in d: if a['queueType'] == 'RANKED_TFT': result['tft.wins'] = a['wins'] result['tft.losses'] = a['losses'] result['tft.rank'] = a['rank'] result['tft.tier'] = a['tier'].title() result['tft.lp'] = a['leaguePoints'] break return result
async def set_language(bot, channel_id, args, var_args, **kwargs): language = ' '.join(args) or ' '.join(var_args['set_language']) if not language: raise Send_error('You must specify a language') try: await utils.twitch_channel_token_request( bot, channel_id, f'https://api.twitch.tv/helix/channels?broadcaster_id={channel_id}', method='PATCH', json={ 'broadcaster_language': language, }, ) except utils.Twitch_request_error as e: raise Send_error(f'Unable to change language. Error: {str(e)}') raise Send_error(f'Language changed to: {language}')
async def followage(bot, user_id, display_name, channel_id, channel, args, **kwargs): uid = user_id user = display_name if len(args) > 0: user = utils.safe_username(args[0]) uid = await utils.twitch_lookup_user_id(bot.ahttp, bot.db, user) if not uid: uid = user_id user = display_name data = await utils.twitch_request( bot.ahttp, 'https://api.twitch.tv/helix/users/follows', params={ 'from_id': uid, 'to_id': channel_id, }) if not data['data']: raise Send_error('{} does not follow {}'.format(user, channel)) followed_at = parse(data['data'][0]['followed_at']).replace(tzinfo=None) return { 'followage': utils.seconds_to_pretty(dt1=datetime.utcnow(), dt2=followed_at), 'followage_date': followed_at.strftime('%Y-%m-%d'), 'followage_datetime': followed_at.strftime('%Y-%m-%d %H:%M:%S UTC'), }
async def countdown(bot, var_args, args, **kwargs): from_ = 1 to = 100 try: if 'randint' in var_args: if len(var_args['randint']) == 1: to = int(var_args['randint'][0]) elif len(var_args['randint']) > 1: from_ = int(var_args['randint'][0]) to = int(var_args['randint'][1]) except ValueError: pass try: if len(args) == 1: to = int(args[0]) elif len(args) > 1: from_ = int(args[0]) to = int(args[1]) except ValueError: pass if to <= from_: raise Send_error('First argument must be lower than the second') return { 'randint': randint(from_, to), }
async def accountage(bot, user_id, display_name, args, **kwargs): uid = user_id user = display_name if len(args) > 0: user = utils.safe_username(args[0]) uid = await utils.twitch_lookup_user_id(bot.ahttp, bot.db, user) if not uid: uid = user_id user = display_name data = await utils.twitch_request( bot.ahttp, 'https://api.twitch.tv/helix/users', params={'id': uid}, ) if not data or not data['data']: raise Send_error('Found no data on {}'.format(user, )) created_at = parse(data['data'][0]['created_at']).replace(tzinfo=None) return { 'accountage': utils.seconds_to_pretty(dt1=datetime.utcnow(), dt2=created_at), 'accountage_date': created_at.strftime('%Y-%m-%d'), 'accountage_datetime': created_at.strftime('%Y-%m-%d %H:%M:%S UTC'), }
async def permit_manager(bot, cmd, args, channel, channel_id, var_args, **kwargs): if len(args) < 1: raise Send_error('Invalid syntax, use: !{} <user>'.format(cmd)) user = utils.safe_username(args[0]) user_id = await utils.twitch_lookup_user_id(bot.ahttp, bot.db, user) if not user_id: raise Send_error('Unknown user') time = int(var_args['permit'][0]) if 'permit' in var_args and var_args['permit'] else 60 key = 'tbot:filter:permit:{}:{}'.format( channel_id, user_id, ) permit = await bot.redis.setex(key, time, '1') bot.send("PRIVMSG", target='#'+channel, message='@{}, you will not receive a timeout for the next {}'.format( user, utils.seconds_to_pretty(time) if time > 60 else '{} seconds'.format(time), )) raise Send_break()
async def playlist(bot, channel_id, **kwargs): data = await request( bot, channel_id, 'https://api.spotify.com/v1/me/player/currently-playing') if data == None: raise Send_error('ERROR: Spotify failed to load') if not data or not data['is_playing']: raise Send_error('Spotify is not playing') playlist = await request(bot, channel_id, data['context']['href']) playlistname = '' if playlist: playlistname = playlist['name'] return { 'spotify.playlist_name': playlistname, 'spotify.playlist_url': data['context']['external_urls']['spotify'], }
async def title_game_views_followers(bot, channel_id, **kwargs): data = await utils.twitch_request(bot.ahttp, f'https://api.twitch.tv/helix/users/follows?to_id={channel_id}&first=1', ) if not data['data']: raise Send_error('No data') return { 'followers': str(data['total']), }
async def set_delay(bot, channel_id, args, var_args, **kwargs): delay = ' '.join(args) or ' '.join(var_args['set_delay']) try: delay = int(delay) except ValueError: raise Send_error('Delay must be a number') try: await utils.twitch_channel_token_request( bot, channel_id, f'https://api.twitch.tv/helix/channels?broadcaster_id={channel_id}', method='PATCH', json={ 'delay': delay, }, ) except utils.Twitch_request_error as e: raise Send_error(f'Unable to change delay. Error: {str(e)}') raise Send_error(f'Delay changed to: {delay}')
async def lol(bot, channel, channel_id, args, var_args, **kwargs): if not var_args or not var_args.get('lol.summoner') or \ len(var_args['lol.summoner']) != 2: raise Send_error('{lol.summoner "<name>" "<region>"} is missing') headers = { 'X-Riot-Token': config['lol_apikey'], } summoner = var_args['lol.summoner'][0] region = var_args['lol.summoner'][1].lower() if region not in regions: raise Send_error(f'Invalid region "{region}". Valid regions: {", ".join(regions)}') url = f'https://{region.lower()}.api.riotgames.com/lol/summoner/v4/summoners/by-name/{summoner}' async with bot.ahttp.get(url, headers=headers) as r: if r.status >= 400: error = await r.text() raise Send_error(f'Riot error: {error}') d = await r.json() encrypted_id = d['id'] puuid = d['puuid'] r = { 'lol.summoner': '', 'lol.rank': 'Unranked', 'lol.tier': '', 'lol.lp': 0, 'lol.wins': 0, 'lol.losses': 0, 'lol.live_wins': 0, 'lol.live_losses': 0, } rank_vars = ['lol.rank', 'lol.tier', 'lol.lp', 'lol.wins', 'lol.losses'] if any(a in var_args for a in rank_vars): await get_rank(bot, headers, region, encrypted_id, r) live_vars = ['lol.live_wins', 'lol.live_losses'] if any(a in var_args for a in live_vars): await get_live(channel_id, bot, headers, region, puuid, r) return r
async def title_game_views_followers(bot, channel_id, **kwargs): data = await utils.twitch_request(bot.ahttp, f'https://api.twitch.tv/helix/channels?broadcaster_id={channel_id}', ) if not data['data']: raise Send_error('No data') return { 'title': data['data'][0]['title'] or '<No title>', 'game': data['data'][0]['game_name'] or '<Not in any game category>', }
async def countdown(bot, var_args, **kwargs): d = ' '.join(var_args['countdown']) try: dt = parse(d).astimezone(tz.UTC).replace(tzinfo=None) except ValueError: raise Send_error(f'Invalid date format: "{d}". Use ISO 8601 format.') return { 'countdown': utils.seconds_to_pretty(dt1=datetime.utcnow(), dt2=dt), }
async def song(bot, channel_id, **kwargs): data = await request( bot, channel_id, 'https://api.spotify.com/v1/me/player/currently-playing') if data == None: raise Send_error('ERROR: Spotify failed to load') if not data or not data['is_playing']: raise Send_error('Spotify is not playing') artists = [r['name'] for r in data['item']['artists']] duration = '{}:{:02d}'.format( *divmod(round(data['item']['duration_ms'] / 1000), 60)) progress = '{}:{:02d}'.format(*divmod(round(data['progress_ms'] / 1000), 60)) return { 'spotify.song_name': data['item']['name'], 'spotify.song_artists': ', '.join(artists), 'spotify.song_progress': progress, 'spotify.song_duration': duration, }
async def prev_song(bot, channel_id, args, **kwargs): num = utils.find_int(args) or 1 if num > 10: raise Send_error('Only allowed to go back 10 songs') data = await request( bot, channel_id, 'https://api.spotify.com/v1/me/player/recently-played?limit={}'.format( num)) if data == None: raise Send_error('ERROR: Spotify failed to load') if 'items' not in data or not data['items']: raise Send_error('No previous song found') if num > len(data['items']): raise Send_error('No previous song found') data = data['items'][num - 1] artists = [r['name'] for r in data['track']['artists']] return { 'spotify.prev_song_name': data['track']['name'], 'spotify.prev_song_artists': ', '.join(artists), }
async def count_match(bot, game_id, region, headers, puuid): url = f'https://{region}.api.riotgames.com/lol/match/v5/matches/{game_id}' async with bot.ahttp.get(url, headers=headers) as r: if r.status >= 400: error = await r.text() raise Send_error(f'Riot error: {error}') match = await r.json() if match['info']['gameDuration'] < 60*10: return for p in match['info']['participants']: if p['puuid'] == puuid: return p['win']
async def request(bot, channel_id, url, params=None, headers={}): t = await bot.db.fetchone( 'SELECT * FROM twitch_spotify WHERE channel_id=%s;', (channel_id)) if not t: raise Send_error( 'ERROR: Spotify has not been configured for this channel') headers.update({'Authorization': 'Bearer {}'.format(t['token'])}) async with bot.ahttp.get(url, params=params, headers=headers) as r: if r.status == 200: data = await r.json() return data elif r.status == 204: return {} elif r.status == 401: await refresh_token(bot, channel_id, t['refresh_token']) return await request(bot, channel_id, url, params, headers) error = await r.text() raise Send_error('ERROR: Spotify request error {}: {}'.format( r.status, error, ))
async def subs(bot, channel_id, **kwargs): try: data = await utils.twitch_channel_token_request( bot, channel_id, 'https://api.twitch.tv/subscriptions', params={'broadcaster_id': channel_id}) return { 'subs': data['total'], } except utils.Twitch_request_error as e: raise Send_error('Unable to get sub count. Error: {}'.format(str(e)))
async def weather(bot, var_args, args, cmd, **kwargs): if not config['openweathermap_apikey']: raise Send_error('`openweathermap_apikey` is missing in the config') url = 'https://api.openweathermap.org/data/2.5/weather' city = None if len(args) > 0: city = ' '.join(args) if not city: city = ' '.join(var_args.get('weather.lookup_city', [''])) if not city: raise Send_error(f'Use !{cmd} <city>') units = var_args.get('weather.units', ['metric'])[0] params = { 'q': city, 'APPID': config['openweathermap_apikey'], 'units': units, } async with bot.ahttp.get(url, params=params) as r: d = await r.json() if r.status == 200: return { 'weather.description': d['weather'][0]['description']\ if d['weather'] else 'Unknown', 'weather.temp': d['main']['temp'], 'weather.temp_min': d['main']['temp_min'], 'weather.temp_max': d['main']['temp_max'], 'weather.humidity': d['main']['humidity'], 'weather.city': d['name'], 'weather.wind_speed': d['wind']['speed'], 'weather.lookup_city': '', 'weather.units': '', } else: raise Send_error(d['message'])
async def quote_get(bot, args, channel_id, cmd, **kwargs): if (len(args) == 0) or (args[0].lower() == 'random'): r = await bot.db.fetchone( 'SELECT * FROM twitch_quotes WHERE channel_id=%s and enabled=1 ORDER BY RAND()', (channel_id, )) else: r = await bot.db.fetchone( 'SELECT * FROM twitch_quotes WHERE channel_id=%s and number=%s', ( channel_id, args[0], )) if not r: raise Send_error('Unknown quote') if r['enabled'] == 0: raise Send_error('This quote is deleted') return { 'quote.message': r['message'], 'quote.number': r['number'], 'quote.user': r['created_by_user'], 'quote.date': r['created_at'].strftime('%Y-%m-%d'), }
async def quote_edit(bot, channel_id, cmd, args, **kwargs): if len(args) < 2: raise Send_error(f'Syntax error, use !{cmd} <number> <new text>') n = args.pop(0) r = await bot.db.fetchone( 'SELECT enabled FROM twitch_quotes WHERE channel_id=%s and number=%s', ( channel_id, n, )) if not r: raise Send_error('Unknown quote') if r['enabled'] == 0: raise Send_error('This quote is deleted') await bot.db.execute( 'UPDATE twitch_quotes SET ' 'message=%s, updated_at=%s ' 'WHERE channel_id=%s and number=%s', (' '.join(args), datetime.utcnow(), channel_id, n)) raise Send_error(f'Quote updated')
async def get_rank(bot, headers, region, encrypted_id, result): url = f'https://{region}.api.riotgames.com/lol/league/v4/entries/by-summoner/{encrypted_id}' async with bot.ahttp.get(url, headers=headers) as r: if r.status >= 400: error = await r.text() raise Send_error(f'Riot error: {error}') d = await r.json() for a in d: if a['queueType'] == 'RANKED_SOLO_5x5': result['lol.wins'] = a['wins'] result['lol.losses'] = a['losses'] result['lol.rank'] = a['rank'] result['lol.tier'] = a['tier'].title() result['lol.lp'] = a['leaguePoints'] break
async def get_live(channel_id, bot, headers, region, puuid, result): if not bot.channels_check[channel_id]['went_live_at']: return new_region = new_regions[region] url = f'https://{new_region}.api.riotgames.com/lol/match/v5/matches/by-puuid/{puuid}/ids?start=0&count=100&startTime={int(bot.channels_check[channel_id]["went_live_at"].timestamp())}' async with bot.ahttp.get(url, headers=headers) as r: if r.status >= 400: error = await r.text() raise Send_error(f'Riot error: {error}') matches = await r.json() w = [] for m in matches: w.append(count_match(bot, m, new_region, headers, puuid)) r = await asyncio.gather(*w) for t in r: if t: result['lol.live_wins'] += 1 else: result['lol.live_losses'] += 1
async def refresh_token(bot, channel_id, refresh_token): url = 'https://accounts.spotify.com/api/token' body = { 'grant_type': 'refresh_token', 'refresh_token': refresh_token, 'client_id': config['spotify']['client_id'], 'client_secret': config['spotify']['client_secret'], } headers = { 'Content-Type': 'application/x-www-form-urlencoded', } async with bot.ahttp.post(url, params=body, headers=headers) as r: if r.status == 200: data = await r.json() await bot.db.execute( 'UPDATE twitch_spotify SET token=%s WHERE channel_id=%s;', (data['access_token'], channel_id)) if 'refresh_token' in data: await bot.db.execute( 'UPDATE twitch_spotify SET refresh_token=%s WHERE channel_id=%s;', (data['refresh_token'], channel_id)) else: raise Send_error('ERROR: Soptify needs to be reauthorized')
async def cmd_manager(bot, display_name, args, cmd, channel_id, **kwargs): if len(args) < 2: raise Send_error( 'Syntax error, use: !{} add/edit/get/delete [cmd] <response>'. format(cmd)) method = args.pop(0).lower() cmd_name = args.pop(0).lower().strip('!') if method == 'add' or method == 'edit': if len(args) == 0: raise Send_error('Syntax error, use: !{} {} {} <reponse>'.format( cmd, method, cmd_name)) try: utils.validate_cmd(cmd_name) utils.validate_cmd_response(' '.join(args)) except Exception as e: raise Send_error(str(e)) if method == 'add': r = await get_cmd(bot, channel_id, cmd_name) if r: raise Send_error( 'Cmd "{0}" already exists, use: !{1} edit {0} <response>'. format(cmd_name, cmd)) await bot.db.execute( 'INSERT INTO twitch_commands (channel_id, cmd, response, created_at, updated_at) ' 'VALUES (%s, %s, %s, %s, %s)', (channel_id, cmd_name, ' '.join(args), datetime.utcnow(), datetime.utcnow())) raise Send_error('!{} successfully saved'.format(cmd_name)) elif method == 'edit': r = await get_cmd(bot, channel_id, cmd_name) if not r: raise Send_error( 'Cmd "{0}" does not exist, use !{1} add {0} <response>'.format( cmd_name, cmd)) await bot.db.execute( 'UPDATE twitch_commands SET response=%s, updated_at=%s WHERE channel_id=%s and cmd=%s', ( ' '.join(args), datetime.utcnow(), channel_id, cmd_name, )) raise Send_error('!{} successfully saved'.format(cmd_name)) elif method == 'delete': r = await get_cmd(bot, channel_id, cmd_name) if not r: raise Send_error('Cmd "{0}" does not exist'.format(cmd_name)) await bot.db.execute( 'DELETE FROM twitch_commands WHERE channel_id=%s and cmd=%s', ( channel_id, cmd_name, )) raise Send_error('!{} successfully deleted'.format(cmd_name)) elif method == 'get': r = await get_cmd(bot, channel_id, cmd_name) if not r: raise Send_error('Cmd "{0}" does not exist'.format(cmd_name)) raise Send_error('{}'.format(r['response'])) else: raise Send_error( 'Syntax error, use: !{} add/edit/get/delete [cmd] <response>'. format(cmd))