Merge pull request #21 from Senpos/add-followage-command
Add !followage command
This commit is contained in:
commit
c7e953743d
|
|
@ -1,31 +1,32 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v2.4.0
|
rev: v2.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-docstring-first
|
- id: check-docstring-first
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: debug-statements
|
- id: debug-statements
|
||||||
|
- id: double-quote-string-fixer
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 3.7.9
|
rev: 3.8.0a2
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
- repo: https://github.com/pre-commit/mirrors-autopep8
|
- repo: https://github.com/pre-commit/mirrors-autopep8
|
||||||
rev: v1.4.4
|
rev: v1.5.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: autopep8
|
- id: autopep8
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder_python_imports
|
||||||
rev: v1.8.0
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
args: [--py3-plus]
|
args: [--py3-plus]
|
||||||
- repo: https://github.com/asottile/add-trailing-comma
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
rev: v1.5.0
|
rev: v2.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: add-trailing-comma
|
- id: add-trailing-comma
|
||||||
args: [--py36-plus]
|
args: [--py36-plus]
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.750
|
rev: v0.770
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -85,3 +85,18 @@ Show the discord url
|
||||||
anthonywritescode: !discord
|
anthonywritescode: !discord
|
||||||
anthonywritescodebot: We do have Discord, you are welcome to join: https://discord.gg/HxpQ3px
|
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 sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
@ -23,13 +24,15 @@ from typing import Tuple
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import aiosqlite
|
import aiosqlite
|
||||||
|
import async_lru
|
||||||
|
from humanize import naturaldelta
|
||||||
|
|
||||||
# TODO: allow host / port to be configurable
|
# TODO: allow host / port to be configurable
|
||||||
HOST = 'irc.chat.twitch.tv'
|
HOST = 'irc.chat.twitch.tv'
|
||||||
PORT = 6697
|
PORT = 6697
|
||||||
|
|
||||||
MSG_RE = re.compile('^@([^ ]+) :([^!]+).* PRIVMSG #[^ ]+ :([^\r]+)')
|
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]+)')
|
SEND_MSG_RE = re.compile('^PRIVMSG #[^ ]+ :(?P<msg>[^\r]+)')
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -202,7 +205,7 @@ _TEXT_COMMANDS = (
|
||||||
(
|
(
|
||||||
'!github',
|
'!github',
|
||||||
"anthony's github is https://github.com/asottile -- stream github is "
|
"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!'),
|
('!homeland', 'WE WILL PROTECT OUR HOMELAND!'),
|
||||||
(
|
(
|
||||||
|
|
@ -230,8 +233,6 @@ for _cmd, _msg in _TEXT_COMMANDS:
|
||||||
@handle_message('!still')
|
@handle_message('!still')
|
||||||
def cmd_still(match: Match[str]) -> Response:
|
def cmd_still(match: Match[str]) -> Response:
|
||||||
_, _, rest = match['msg'].partition(' ')
|
_, _, rest = match['msg'].partition(' ')
|
||||||
if rest.startswith('/disconnect'):
|
|
||||||
rest = f' {rest}'
|
|
||||||
year = datetime.date.today().year
|
year = datetime.date.today().year
|
||||||
lol = random.choice(['LOL', 'LOLW', 'LMAO', 'NUUU'])
|
lol = random.choice(['LOL', 'LOLW', 'LMAO', 'NUUU'])
|
||||||
return MessageResponse(match, f'{esc(rest)}, in {year} - {lol}!')
|
return MessageResponse(match, f'{esc(rest)}, in {year} - {lol}!')
|
||||||
|
|
@ -387,6 +388,119 @@ def cmd_uptime(match: Match[str]) -> Response:
|
||||||
return UptimeResponse()
|
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})')
|
@handle_message(r'!pep[ ]?(?P<pep_num>\d{1,4})')
|
||||||
def cmd_pep(match: Match[str]) -> Response:
|
def cmd_pep(match: Match[str]) -> Response:
|
||||||
n = str(int(match['pep_num'])).zfill(4)
|
n = str(int(match['pep_num'])).zfill(4)
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
aiohttp
|
aiohttp
|
||||||
aiosqlite
|
aiosqlite
|
||||||
|
async-lru
|
||||||
|
humanize
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue