Merge pull request #21 from Senpos/add-followage-command

Add !followage command
This commit is contained in:
Anthony Sottile 2020-05-10 19:37:00 -07:00 committed by GitHub
commit c7e953743d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 142 additions and 10 deletions

View File

@ -1,31 +1,32 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
rev: v2.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-docstring-first
- id: check-yaml
- id: debug-statements
- id: double-quote-string-fixer
- repo: https://gitlab.com/pycqa/flake8
rev: 3.7.9
rev: 3.8.0a2
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.4.4
rev: v1.5.2
hooks:
- id: autopep8
- repo: https://github.com/asottile/reorder_python_imports
rev: v1.8.0
rev: v2.3.0
hooks:
- id: reorder-python-imports
args: [--py3-plus]
- repo: https://github.com/asottile/add-trailing-comma
rev: v1.5.0
rev: v2.0.1
hooks:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.750
rev: v0.770
hooks:
- id: mypy

View File

@ -85,3 +85,18 @@ Show the discord url
anthonywritescode: !discord
anthonywritescodebot: We do have Discord, you are welcome to join: https://discord.gg/HxpQ3px
```
### `!followage [username]`
Show how long you or a user you specified have been following the channel
```
not_cool_user: !followage
anthonywritescodebot: not_cool_user is not a follower!
cool_user: !followage
anthonywritescodebot: cool_user has been following for 3 hours!
some_user: !followage another_user
anthonywritescodebot: another_user has been following for 5 years!
```

120
bot.py
View File

@ -11,6 +11,7 @@ import struct
import sys
import tempfile
import traceback
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
@ -23,6 +24,8 @@ from typing import Tuple
import aiohttp
import aiosqlite
import async_lru
from humanize import naturaldelta
# TODO: allow host / port to be configurable
HOST = 'irc.chat.twitch.tv'
@ -202,7 +205,7 @@ _TEXT_COMMANDS = (
(
'!github',
"anthony's github is https://github.com/asottile -- stream github is "
"https://github.com/anthonywritescode",
'https://github.com/anthonywritescode',
),
('!homeland', 'WE WILL PROTECT OUR HOMELAND!'),
(
@ -230,8 +233,6 @@ for _cmd, _msg in _TEXT_COMMANDS:
@handle_message('!still')
def cmd_still(match: Match[str]) -> Response:
_, _, rest = match['msg'].partition(' ')
if rest.startswith('/disconnect'):
rest = f' {rest}'
year = datetime.date.today().year
lol = random.choice(['LOL', 'LOLW', 'LMAO', 'NUUU'])
return MessageResponse(match, f'{esc(rest)}, in {year} - {lol}!')
@ -387,6 +388,119 @@ def cmd_uptime(match: Match[str]) -> Response:
return UptimeResponse()
@async_lru.alru_cache(maxsize=32)
async def fetch_twitch_user(
user: str,
*,
oauth_token: str,
client_id: str
) -> Optional[List[Dict[str, Any]]]:
url = 'https://api.twitch.tv/helix/users'
params = [('login', user)]
headers = {
'Authorization': f'Bearer {oauth_token}',
'Client-ID': client_id,
}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers) as resp:
json_resp = await resp.json()
return json_resp.get('data')
async def fetch_twitch_user_follows(
*,
from_id: int,
to_id: int,
oauth_token: str,
client_id: str
) -> Optional[List[Dict[str, Any]]]:
url = 'https://api.twitch.tv/helix/users/follows'
params = [('from_id', from_id), ('to_id', to_id)]
headers = {
'Authorization': f'Bearer {oauth_token}',
'Client-ID': client_id,
}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, headers=headers) as resp:
json_resp = await resp.json()
return json_resp.get('data')
class FollowageResponse(Response):
def __init__(self, username: str) -> None:
self.username = username
async def __call__(self, config: Config) -> Optional[str]:
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(
self.username,
oauth_token=token,
client_id=config.client_id,
)
if not fetched_users:
msg = f'user {esc(self.username)} not found!'
return PRIVMSG.format(channel=config.channel, msg=msg)
target_user, = fetched_users
# if streamer wants to check the followage to their own channel
if me['id'] == target_user['id']:
msg = (
f"@{esc(target_user['login'])}, you can't check !followage "
f'to your own channel. But I appreciate your curiosity!'
)
return PRIVMSG.format(channel=config.channel, msg=msg)
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:
msg = f'{esc(target_user["login"])} is not a follower!'
return PRIVMSG.format(channel=config.channel, msg=msg)
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
msg = (
f'{esc(follow_age["from_name"])} has been following for '
f'{esc(naturaldelta(delta))}!'
)
return PRIVMSG.format(channel=config.channel, msg=msg)
# !followage -> valid, checks the caller
# !followage anthonywritescode -> valid, checks the user passed in payload
# !followage foo bar -> still valid, however the whole
# "foo bar" will be processed as a username
@handle_message(r'!followage(?P<payload> .*)?')
def cmd_followage(match: Match[str]) -> Response:
user = match['user']
# "" is a default value if group is missing
groupdict = match.groupdict('')
payload = groupdict['payload'].strip()
if payload:
user = payload.lstrip('@')
return FollowageResponse(user)
@handle_message(r'!pep[ ]?(?P<pep_num>\d{1,4})')
def cmd_pep(match: Match[str]) -> Response:
n = str(int(match['pep_num'])).zfill(4)

View File

@ -1,2 +1,4 @@
aiohttp
aiosqlite
async-lru
humanize