Merge pull request #21 from Senpos/add-followage-command
Add !followage command
This commit is contained in:
commit
c7e953743d
|
|
@ -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
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -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!
|
||||
```
|
||||
|
|
|
|||
122
bot.py
122
bot.py
|
|
@ -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,13 +24,15 @@ 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'
|
||||
PORT = 6697
|
||||
|
||||
MSG_RE = re.compile('^@([^ ]+) :([^!]+).* PRIVMSG #[^ ]+ :([^\r]+)')
|
||||
PRIVMSG = 'PRIVMSG #{channel} :{msg}\r\n'
|
||||
PRIVMSG = 'PRIVMSG #{channel} : {msg}\r\n'
|
||||
SEND_MSG_RE = re.compile('^PRIVMSG #[^ ]+ :(?P<msg>[^\r]+)')
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
aiohttp
|
||||
aiosqlite
|
||||
async-lru
|
||||
humanize
|
||||
|
|
|
|||
Loading…
Reference in New Issue