async def command_theme(config: Config, match: Match[str]) -> str: theme_file = os.path.expanduser('~/.config/babi/theme.json') if not os.path.exists(theme_file): return format_msg( match, 'awcBabi this is vs dark plus in !babi with one modification to ' 'highlight ini headers: ' 'https://github.com/asottile/babi#setting-up-syntax-highlighting', ) with open(theme_file) as f: contents = json.load(f) try: name = contents.get('name', '(unknown)') user = contents['user'] url = contents['url'] except KeyError: return format_msg(match, "awcBabi I don't know what this theme is!?") else: return format_msg( match, f'awcBabi this theme was set by {esc(user)} using channel points! ' f'it is called {esc(name)!r} and can be download from {esc(url)}', )
async def cmd_aqi(config: Config, match: Match[str]) -> str: _, _, rest = match['msg'].partition(' ') if rest: zip_code = rest.split()[0] if not ZIP_CODE_RE.match(zip_code): return format_msg(match, '(invalid zip) usage: !aqi [US_ZIP_CODE]') else: zip_code = '94401' params = { 'format': 'application/json', 'zipCode': zip_code, 'API_KEY': config.airnow_api_key, } url = 'https://www.airnowapi.org/aq/observation/zipCode/current/' async with aiohttp.ClientSession() as session: async with session.get(url, params=params) as resp: json_resp = await resp.json() pm_25 = [d for d in json_resp if d['ParameterName'] == 'PM2.5'] if not pm_25: return format_msg( match, 'No PM2.5 info -- is this a US zip code?', ) else: data, = pm_25 return format_msg( match, f'Current AQI ({esc(data["ParameterName"])}) in ' f'{esc(data["ReportingArea"])}, ' f'{esc(data["StateCode"])}: ' f'{esc(str(data["AQI"]))} ' f'({esc(data["Category"]["Name"])})', )
async def change_theme(config: Config, match: Match[str]) -> str: url = match['msg'].strip() try: loaded = await _load_theme(url) except ThemeError as e: return format_msg(match, str(e)) loaded['user'] = match['user'] loaded['url'] = url os.makedirs(THEME_DIR, exist_ok=True) theme_file = f'{match["user"]}-{uuid.uuid4()}.json' theme_file = os.path.join(THEME_DIR, theme_file) with open(theme_file, 'w') as f: json.dump(loaded, f) themedir = os.path.expanduser('~/.config/babi') os.makedirs(themedir, exist_ok=True) dest = os.path.join(themedir, 'theme.json') proc = await asyncio.create_subprocess_exec('ln', '-sf', theme_file, dest) await proc.communicate() assert proc.returncode == 0 proc = await asyncio.create_subprocess_exec('pkill', '-USR1', 'babi') await proc.communicate() # ignore the return code, if there are no editors running it'll be `1` # assert proc.returncode == 0 return format_msg(match, 'theme updated!')
async def giveawayend(config: Config, match: Match[str]) -> Optional[str]: if not is_moderator(match) and match['user'] != match['channel']: return None async with aiosqlite.connect('db.db') as db: await ensure_giveaway_tables_exist(db) async with db.execute('SELECT active FROM giveaway') as cursor: row = await cursor.fetchone() if row is None or not row[0]: return format_msg(match, 'no current giveaway active!') query = 'SELECT user FROM giveaway_users' async with db.execute(query) as cursor: users = [user for user, in await cursor.fetchall()] if users: await db.execute('INSERT OR REPLACE INTO giveaway VALUES (0)') await db.commit() await db.execute('DROP TABLE giveaway_users') await db.execute('DROP TABLE giveaway') await db.commit() if not users: return format_msg(match, 'no users entered giveaway!') winner = random.choice(users) return format_msg(match, f'!giveaway winner is {esc(winner)}')
async def vim_bits_handler(config: Config, match: Match[str]) -> str: info = parse_badge_info(match['info']) async with aiosqlite.connect('db.db') as db: await ensure_vim_tables_exist(db) enabled = await get_enabled(db) bits = int(info['bits']) if enabled: time_left = await add_bits(db, match['user'], bits) else: time_left = await add_bits_off(db, match['user'], bits) if enabled: await _set_symlink(should_be_vim=True) return format_msg( match, f'MOAR VIM: {seconds_to_readable(time_left)} remaining', ) else: return format_msg( match, f'vim is currently disabled ' f'{seconds_to_readable(time_left)} banked', )
async def cmd_videoidea(config: Config, match: Match[str]) -> str: if not is_moderator(match) and match['user'] != match['channel']: return format_msg(match, 'https://youtu.be/RfiQYRn7fBg') _, _, rest = match['msg'].partition(' ') async def _git(*cmd: str) -> None: await _check_call('git', '-C', tmpdir, *cmd) with tempfile.TemporaryDirectory() as tmpdir: await _git( 'clone', '--depth=1', '--quiet', '[email protected]:asottile/scratch.wiki', '.', ) ideas_file = os.path.join(tmpdir, 'anthony-explains-ideas.md') with open(ideas_file, 'rb+') as f: f.seek(-1, os.SEEK_END) c = f.read() if c != b'\n': f.write(b'\n') f.write(f'- {rest}\n'.encode()) await _git('add', '.') await _git('commit', '-q', '-m', 'idea added by !videoidea') await _git('push', '-q', 'origin', 'HEAD') return format_msg( match, 'added! https://github.com/asottile/scratch/wiki/anthony-explains-ideas', # noqa: E501 )
async def command_themevalidate(config: Config, match: Match[str]) -> str: _, _, url = match['msg'].partition(' ') try: await _load_theme(url.strip()) except ThemeError as e: return format_msg(match, str(e)) else: return format_msg(match, 'theme is ok!')
async def msg_gnu_please(config: Config, match: Match[str]) -> str | None: if random.randrange(0, 100) < 90: return None msg, word = match['msg'], match['word'] query = re.search(f'gnu[/+]{word}', msg, flags=re.IGNORECASE) if query: return format_msg(match, f'YES! {query[0]}') else: return format_msg(match, f"Um please, it's GNU+{esc(word)}!")
async def cmd_settoday(config: Config, match: Match[str]) -> str: if not is_moderator(match) and match['user'] != match['channel']: return format_msg(match, 'https://youtu.be/RfiQYRn7fBg') _, _, rest = match['msg'].partition(' ') async with aiosqlite.connect('db.db') as db: await set_today(db, rest) return format_msg(match, 'updated!')
async def cmd_bonkrank(config: Config, match: Match[str]) -> str: user = optional_user_arg(match) ret = _rank(user, BONKER_RE) if ret is None: return format_msg(match, f'user not found {esc(user)}') else: rank, n = ret return format_msg( match, f'{esc(user)} is ranked #{rank}, has bonked others {n} times', )
async def cmd_disablevim(config: Config, match: Match[str]) -> str: if not is_moderator(match) and match['user'] != match['channel']: return format_msg(match, 'https://youtu.be/RfiQYRn7fBg') async with aiosqlite.connect('db.db') as db: await ensure_vim_tables_exist(db) await db.execute('INSERT INTO vim_enabled VALUES (0)') await db.commit() return format_msg(match, 'vim has been disabled')
async def cmd_bonkedrank(config: Config, match: Match[str]) -> str: user = optional_user_arg(match) ret = _user_rank_by_line_type(user, BONKED_RE) if ret is None: return format_msg(match, f'user not found {esc(user)}') else: rank, n = ret return format_msg( match, f'{esc(user)} is ranked #{rank}, has been bonked {n} times', )
async def cmd_vimbitsrank(config: Config, match: Match[str]) -> str: user = optional_user_arg(match) async with aiosqlite.connect('db.db') as db: ret = await _user_rank_by_bits(user, db) if ret is None: return format_msg(match, f'user not found {esc(user)}') else: rank, n = ret return format_msg( match, f'{esc(user)} is ranked #{rank} with {n} vim bits', )
async def cmd_followage(config: Config, match: Match[str]) -> str: username = optional_user_arg(match) token = config.oauth_token.split(':')[1] fetched_users = await fetch_twitch_user( config.channel, oauth_token=token, client_id=config.client_id, ) assert fetched_users is not None me, = fetched_users fetched_users = await fetch_twitch_user( username, oauth_token=token, client_id=config.client_id, ) if not fetched_users: return format_msg(match, f'user {esc(username)} not found!') target_user, = fetched_users # if streamer wants to check the followage to their own channel if me['id'] == target_user['id']: return format_msg( match, f"@{esc(target_user['login'])}, you can't check !followage " f'to your own channel. But I appreciate your curiosity!', ) follow_age_results = await fetch_twitch_user_follows( from_id=target_user['id'], to_id=me['id'], oauth_token=token, client_id=config.client_id, ) if not follow_age_results: return format_msg( match, f'{esc(target_user["login"])} is not a follower!', ) follow_age, = follow_age_results now = datetime.datetime.utcnow() date_of_follow = datetime.datetime.fromisoformat( # twitch sends ISO date string with "Z" at the end, # which python's fromisoformat method does not like follow_age['followed_at'].rstrip('Z'), ) delta = now - date_of_follow return format_msg( match, f'{esc(follow_age["from_name"])} has been following for ' f'{esc(humanize.naturaldelta(delta))}!', )
async def cmd_chatrank(config: Config, match: Match[str]) -> str: user = optional_user_arg(match) ret = _rank(user, CHAT_LOG_RE) if ret is None: return format_msg(match, f'user not found {esc(user)}') else: rank, n = ret return format_msg( match, f'{esc(user)} is ranked #{rank} with {n} messages ' f'(since {_log_start_date()})', )
async def cmd_vimtimeleft(config: Config, match: Match[str]) -> str: async with aiosqlite.connect('db.db') as db: await ensure_vim_tables_exist(db) if not await get_enabled(db): return format_msg(match, 'vim is currently disabled') time_left = await get_time_left(db) if time_left == 0: return format_msg(match, 'not currently using vim') else: return format_msg( match, f'vim time remaining: {seconds_to_readable(time_left)}', )
async def giveaway(config: Config, match: Match[str]) -> str: async with aiosqlite.connect('db.db') as db: await ensure_giveaway_tables_exist(db) async with db.execute('SELECT active FROM giveaway') as cursor: row = await cursor.fetchone() if row is None or not row[0]: return format_msg(match, 'no current giveaway active!') await ensure_giveaway_tables_exist(db) query = 'INSERT OR REPLACE INTO giveaway_users VALUES (?)' await db.execute(query, (match['user'], )) await db.commit() return format_msg(match, f'{esc(match["user"])} has been entered!')
async def cmd_bonk(config: Config, match: Match[str]) -> str: _, _, rest = match['msg'].partition(' ') rest = rest.strip() or 'marsha_socks' return format_msg( match, f'awcBonk awcBonk awcBonk {esc(rest)} awcBonk awcBonk awcBonk', )
async def cmd_top_5_bonked(config: Config, match: Match[str]) -> str: total = _chat_rank_counts(BONKED_RE) user_list = ', '.join( f'{rank}. {user}({n})' for rank, (user, n) in enumerate(total.most_common(5), start=1) ) return format_msg(match, user_list)
async def cmd_top_10_chat(config: Config, match: Match[str]) -> str: total = _chat_rank_counts(CHAT_LOG_RE) user_list = ', '.join( f'{rank}. {user}({n})' for rank, (user, n) in enumerate(total.most_common(10), start=1) ) return format_msg(match, f'{user_list} (since {_log_start_date()})')
async def cmd_pep_no_arg(config: Config, match: Match[str]) -> str: _, _, rest = match['msg'].strip().partition(' ') digits_match = DIGITS_RE.match(rest) if digits_match is not None: return _pep_msg(match, digits_match[0]) else: return format_msg(match, '!pep: expected argument <number>')
async def cmd_set_motd(config: Config, match: Match[str]) -> str: async with aiosqlite.connect('db.db') as db: await set_motd(db, match['user'], match['msg']) msg = 'motd updated! thanks for spending points!' if match['msg'] == '!motd': motd_count = await msg_count(db, match['msg']) msg = f'{msg} it has been set to !motd {motd_count} times!' return format_msg(match, msg)
async def cmd_bonk(config: Config, match: Match[str]) -> str: _, _, rest = match['msg'].partition(' ') rest = rest.strip() or 'Makayla_Fox' return format_msg( match, f'{esc(rest)}: ' f'https://i.fluffy.cc/DM4QqzjR7wCpkGPwTl6zr907X50XgtBL.png', )
async def cmd_editor(config: Config, match: Match[str]) -> str: async with aiosqlite.connect('db.db') as db: await ensure_vim_tables_exist(db) if await get_time_left(db): return format_msg( match, 'I am currently being forced to use vim by viewers. ' 'awcBabi I normally use my text editor I made, called babi! ' 'https://github.com/asottile/babi more info in this video: ' 'https://www.youtube.com/watch?v=WyR1hAGmR3g', ) else: return format_msg( match, 'awcBabi this is my text editor I made, called babi! ' 'https://github.com/asottile/babi more info in this video: ' 'https://www.youtube.com/watch?v=WyR1hAGmR3g', )
async def vim_normalize_state(config: Config, match: Match[str]) -> str | None: async with aiosqlite.connect('db.db') as db: await ensure_vim_tables_exist(db) time_left = await get_time_left(db) cleared_vim = await _set_symlink(should_be_vim=time_left > 0) if cleared_vim: return format_msg(match, 'vim no more! you are free!') else: return None
async def cmd_bongo(config: Config, match: Match[str]) -> str: _, _, rest = match['msg'].partition(' ') rest = rest.strip() if rest: rest = f'{rest} ' return format_msg( match, f'awcBongo awcBongo awcBongo {esc(rest)}awcBongo awcBongo awcBongo', )
async def cmd_uptime(config: Config, match: Match[str]) -> str: url = f'https://api.twitch.tv/helix/streams?user_login={config.channel}' headers = { 'Authorization': f'Bearer {config.oauth_token_token}', 'Client-ID': config.client_id, } async with aiohttp.ClientSession() as session: async with session.get(url, headers=headers) as response: json_resp = await response.json() if not json_resp['data']: return format_msg(match, 'not currently streaming!') start_time_s = json_resp['data'][0]['started_at'] start_time = datetime.datetime.strptime( start_time_s, '%Y-%m-%dT%H:%M:%SZ', ) elapsed = (datetime.datetime.utcnow() - start_time).seconds readable_time = seconds_to_readable(elapsed) return format_msg(match, f'streaming for: {readable_time}')
async def givewawaystart(config: Config, match: Match[str]) -> str | None: if not is_moderator(match) and match['user'] != match['channel']: return None async with aiosqlite.connect('db.db') as db: await ensure_giveaway_tables_exist(db) await db.execute('INSERT OR REPLACE INTO giveaway VALUES (1)') await db.commit() return format_msg(match, 'giveaway started! use !giveaway to enter')
async def cmd_enablevim(config: Config, match: Match[str]) -> str: if not is_moderator(match) and match['user'] != match['channel']: return format_msg(match, 'https://youtu.be/RfiQYRn7fBg') async with aiosqlite.connect('db.db') as db: await ensure_vim_tables_exist(db) await db.execute('INSERT INTO vim_enabled VALUES (1)') move_query = 'INSERT INTO vim_bits SELECT * FROM vim_bits_disabled' await db.execute(move_query) time_left = await add_time(db, await disabled_seconds(db)) await db.execute('DELETE FROM vim_bits_disabled') await db.commit() if time_left == 0: return format_msg(match, 'vim has been enabled') else: await _set_symlink(should_be_vim=True) return format_msg( match, f'vim has been enabled: ' f'time remaining {seconds_to_readable(time_left)}', )
async def msg_ping(config: Config, match: Match[str]) -> str: _, _, rest = match['msg'].partition(' ') return format_msg(match, f'PONG {esc(rest)}')