2019-03-03 01:00:14 +00:00
|
|
|
import argparse
|
2020-05-04 19:35:44 +00:00
|
|
|
import asyncio.subprocess
|
2019-03-03 01:00:14 +00:00
|
|
|
import datetime
|
|
|
|
|
import json
|
2020-05-04 19:35:44 +00:00
|
|
|
import os.path
|
2019-07-20 21:00:08 +00:00
|
|
|
import random
|
2019-03-03 01:00:14 +00:00
|
|
|
import re
|
|
|
|
|
import sys
|
2020-05-04 19:35:44 +00:00
|
|
|
import tempfile
|
2019-03-03 01:00:14 +00:00
|
|
|
import traceback
|
|
|
|
|
from typing import Callable
|
|
|
|
|
from typing import List
|
|
|
|
|
from typing import Match
|
|
|
|
|
from typing import NamedTuple
|
|
|
|
|
from typing import NoReturn
|
|
|
|
|
from typing import Optional
|
|
|
|
|
from typing import Pattern
|
|
|
|
|
from typing import Tuple
|
|
|
|
|
|
|
|
|
|
import aiohttp
|
2019-08-11 00:19:15 +00:00
|
|
|
import aiosqlite
|
2019-03-03 01:00:14 +00:00
|
|
|
|
|
|
|
|
# TODO: allow host / port to be configurable
|
|
|
|
|
HOST = 'irc.chat.twitch.tv'
|
|
|
|
|
PORT = 6697
|
|
|
|
|
|
2019-12-09 04:28:39 +00:00
|
|
|
MSG_RE = re.compile('^:([^!]+).* PRIVMSG #[^ ]+ :([^\r]+)')
|
|
|
|
|
PRIVMSG = 'PRIVMSG #{channel} :{msg}\r\n'
|
|
|
|
|
SEND_MSG_RE = re.compile('^PRIVMSG #[^ ]+ :(?P<msg>[^\r]+)')
|
|
|
|
|
|
2019-03-03 01:00:14 +00:00
|
|
|
|
|
|
|
|
class Config(NamedTuple):
|
|
|
|
|
username: str
|
|
|
|
|
channel: str
|
|
|
|
|
oauth_token: str
|
|
|
|
|
client_id: str
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
return (
|
|
|
|
|
f'{type(self).__name__}('
|
|
|
|
|
f'username={self.username!r}, '
|
|
|
|
|
f'channel={self.channel!r}, '
|
|
|
|
|
f'oauth_token={"***"!r}, '
|
|
|
|
|
f'client_id={"***"!r}, '
|
|
|
|
|
f')'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def esc(s: str) -> str:
|
|
|
|
|
return s.replace('{', '{{').replace('}', '}}')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send(
|
|
|
|
|
writer: asyncio.StreamWriter,
|
|
|
|
|
msg: str,
|
|
|
|
|
*,
|
|
|
|
|
quiet: bool = False,
|
|
|
|
|
) -> None:
|
|
|
|
|
if not quiet:
|
|
|
|
|
print(f'< {msg}', end='', flush=True, file=sys.stderr)
|
|
|
|
|
writer.write(msg.encode())
|
|
|
|
|
return await writer.drain()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def recv(
|
|
|
|
|
reader: asyncio.StreamReader,
|
|
|
|
|
*,
|
|
|
|
|
quiet: bool = False,
|
|
|
|
|
) -> bytes:
|
|
|
|
|
data = await reader.readline()
|
2020-05-04 19:28:30 +00:00
|
|
|
if not data:
|
|
|
|
|
raise SystemExit('unexpected EOF')
|
2019-03-03 01:00:14 +00:00
|
|
|
if not quiet:
|
|
|
|
|
sys.stderr.buffer.write(b'> ')
|
|
|
|
|
sys.stderr.buffer.write(data)
|
|
|
|
|
sys.stderr.flush()
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Response:
|
|
|
|
|
async def __call__(self, config: Config) -> Optional[str]:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CmdResponse(Response):
|
|
|
|
|
def __init__(self, cmd: str) -> None:
|
|
|
|
|
self.cmd = cmd
|
|
|
|
|
|
|
|
|
|
async def __call__(self, config: Config) -> Optional[str]:
|
|
|
|
|
return self.cmd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MessageResponse(Response):
|
|
|
|
|
def __init__(self, match: Match[str], msg_fmt: str) -> None:
|
|
|
|
|
self.match = match
|
|
|
|
|
self.msg_fmt = msg_fmt
|
|
|
|
|
|
|
|
|
|
async def __call__(self, config: Config) -> Optional[str]:
|
|
|
|
|
params = self.match.groupdict()
|
|
|
|
|
params['msg'] = self.msg_fmt.format(**params)
|
|
|
|
|
return PRIVMSG.format(**params)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Callback = Callable[[Match[str]], Response]
|
|
|
|
|
HANDLERS: List[Tuple[Pattern[str], Callable[[Match[str]], Response]]]
|
|
|
|
|
HANDLERS = []
|
|
|
|
|
|
|
|
|
|
|
2019-10-19 19:10:47 +00:00
|
|
|
def handler(
|
|
|
|
|
*prefixes: str,
|
|
|
|
|
flags: re.RegexFlag = re.U,
|
|
|
|
|
) -> Callable[[Callback], Callback]:
|
2019-03-03 01:00:14 +00:00
|
|
|
def handler_decorator(func: Callback) -> Callback:
|
2019-08-30 04:19:09 +00:00
|
|
|
for prefix in prefixes:
|
2019-09-07 19:40:22 +00:00
|
|
|
HANDLERS.append((re.compile(prefix + '\r\n$', flags=flags), func))
|
2019-03-03 01:00:14 +00:00
|
|
|
return func
|
|
|
|
|
return handler_decorator
|
|
|
|
|
|
|
|
|
|
|
2019-10-19 19:10:47 +00:00
|
|
|
def handle_message(
|
|
|
|
|
*message_prefixes: str,
|
|
|
|
|
flags: re.RegexFlag = re.U,
|
|
|
|
|
) -> Callable[[Callback], Callback]:
|
2019-03-03 01:00:14 +00:00
|
|
|
return handler(
|
2019-08-30 04:19:09 +00:00
|
|
|
*(
|
|
|
|
|
f'^:(?P<user>[^!]+).* '
|
|
|
|
|
f'PRIVMSG #(?P<channel>[^ ]+) '
|
|
|
|
|
f':(?P<msg>{message_prefix}.*)'
|
|
|
|
|
for message_prefix in message_prefixes
|
2019-09-07 19:40:22 +00:00
|
|
|
), flags=flags,
|
2019-03-03 01:00:14 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@handler('^PING (.*)')
|
|
|
|
|
def pong(match: Match[str]) -> Response:
|
|
|
|
|
return CmdResponse(f'PONG {match.group(1)}\r\n')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@handle_message('!ohai')
|
|
|
|
|
def cmd_ohai(match: Match[str]) -> Response:
|
|
|
|
|
return MessageResponse(match, 'ohai, {user}!')
|
|
|
|
|
|
|
|
|
|
|
2019-08-30 03:55:52 +00:00
|
|
|
@handle_message('!lurk')
|
|
|
|
|
def cmd_lurk(match: Match[str]) -> Response:
|
|
|
|
|
return MessageResponse(match, 'thanks for lurking, {user}!')
|
|
|
|
|
|
|
|
|
|
|
2019-06-08 20:26:57 +00:00
|
|
|
@handle_message('!discord')
|
|
|
|
|
def cmd_discord(match: Match[str]) -> Response:
|
2019-06-08 20:51:36 +00:00
|
|
|
return MessageResponse(
|
|
|
|
|
match,
|
|
|
|
|
'We do have Discord, you are welcome to join: '
|
|
|
|
|
'https://discord.gg/HxpQ3px',
|
|
|
|
|
)
|
2019-06-08 20:26:57 +00:00
|
|
|
|
|
|
|
|
|
2019-09-28 23:47:16 +00:00
|
|
|
@handle_message('!homeland')
|
|
|
|
|
def cmd_russians(match: Match[str]) -> Response:
|
|
|
|
|
return MessageResponse(match, 'WE WILL PROTECT OUR HOMELAND!')
|
|
|
|
|
|
|
|
|
|
|
2019-07-20 18:56:17 +00:00
|
|
|
@handle_message('!emoji')
|
|
|
|
|
def cmd_emoji(match: Match[str]) -> Response:
|
2019-08-30 20:43:39 +00:00
|
|
|
return MessageResponse(match, 'anthon63DumpsterFire anthon63Pythonk')
|
2019-07-20 18:56:17 +00:00
|
|
|
|
|
|
|
|
|
2020-04-04 17:51:28 +00:00
|
|
|
@handle_message('!explain')
|
|
|
|
|
def cmd_explain(match: Match[str]) -> Response:
|
|
|
|
|
return MessageResponse(
|
|
|
|
|
match,
|
|
|
|
|
'https://www.youtube.com/playlist?list=PLWBKAf81pmOaP9naRiNAqug6EBnkPakvY', # noqa: E501
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2019-07-13 23:37:25 +00:00
|
|
|
@handle_message('!keyboard2')
|
|
|
|
|
def keyboard2(match: Match[str]) -> Response:
|
|
|
|
|
return MessageResponse(
|
|
|
|
|
match,
|
|
|
|
|
'this is my second mechanical keyboard: '
|
|
|
|
|
'https://i.fluffy.cc/CDtRzWX1JZTbqzKswHrZsF7HPX2zfLL1.png',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@handle_message('!keyboard')
|
|
|
|
|
def keyboard(match: Match[str]) -> Response:
|
|
|
|
|
return MessageResponse(
|
|
|
|
|
match,
|
|
|
|
|
'this is my streaming keyboard (contributed by PhillipWei): '
|
2019-11-17 00:00:30 +00:00
|
|
|
'https://www.wasdkeyboards.com/code-v3-87-key-mechanical-keyboard-zealio-67g.html', # noqa: E501
|
2019-07-13 23:37:25 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2019-10-12 21:06:44 +00:00
|
|
|
@handle_message('!github')
|
|
|
|
|
def github(match: Match[str]) -> Response:
|
|
|
|
|
return MessageResponse(
|
|
|
|
|
match,
|
|
|
|
|
"anthony's github is https://github.com/asottile -- stream github is "
|
|
|
|
|
"https://github.com/anthonywritescode",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2019-07-20 21:00:08 +00:00
|
|
|
@handle_message('!still')
|
|
|
|
|
def cmd_still(match: Match[str]) -> Response:
|
|
|
|
|
_, _, msg = match.groups()
|
|
|
|
|
_, _, rest = msg.partition(' ')
|
|
|
|
|
year = datetime.date.today().year
|
|
|
|
|
lol = random.choice(['LOL', 'LOLW', 'LMAO', 'NUUU'])
|
|
|
|
|
return MessageResponse(match, f'{esc(rest)}, in {year} - {lol}!')
|
|
|
|
|
|
|
|
|
|
|
2019-08-11 00:19:15 +00:00
|
|
|
async def ensure_table_exists(db: aiosqlite.Connection) -> None:
|
|
|
|
|
await db.execute(
|
|
|
|
|
'CREATE TABLE IF NOT EXISTS today ('
|
|
|
|
|
' msg TEXT NOT NULL,'
|
|
|
|
|
' timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP'
|
|
|
|
|
')',
|
|
|
|
|
)
|
|
|
|
|
await db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def set_today(db: aiosqlite.Connection, msg: str) -> None:
|
|
|
|
|
await ensure_table_exists(db)
|
|
|
|
|
await db.execute('INSERT INTO today (msg) VALUES (?)', (msg,))
|
|
|
|
|
await db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_today(db: aiosqlite.Connection) -> str:
|
|
|
|
|
await ensure_table_exists(db)
|
|
|
|
|
query = 'SELECT msg FROM today ORDER BY ROWID DESC LIMIT 1'
|
|
|
|
|
async with db.execute(query) as cursor:
|
|
|
|
|
row = await cursor.fetchone()
|
|
|
|
|
if row is None:
|
|
|
|
|
return 'not working on anything?'
|
|
|
|
|
else:
|
|
|
|
|
return esc(row[0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TodayResponse(MessageResponse):
|
|
|
|
|
def __init__(self, match: Match[str]) -> None:
|
|
|
|
|
super().__init__(match, '')
|
|
|
|
|
|
|
|
|
|
async def __call__(self, config: Config) -> Optional[str]:
|
|
|
|
|
async with aiosqlite.connect('db.db') as db:
|
|
|
|
|
self.msg_fmt = await get_today(db)
|
|
|
|
|
return await super().__call__(config)
|
|
|
|
|
|
|
|
|
|
|
2019-08-30 04:19:09 +00:00
|
|
|
@handle_message('!today', '!project')
|
2019-08-11 00:19:15 +00:00
|
|
|
def cmd_today(match: Match[str]) -> Response:
|
|
|
|
|
return TodayResponse(match)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SetTodayResponse(MessageResponse):
|
|
|
|
|
def __init__(self, match: Match[str], msg: str) -> None:
|
|
|
|
|
super().__init__(match, 'updated!')
|
|
|
|
|
self.msg = msg
|
|
|
|
|
|
|
|
|
|
async def __call__(self, config: Config) -> Optional[str]:
|
|
|
|
|
async with aiosqlite.connect('db.db') as db:
|
|
|
|
|
await set_today(db, self.msg)
|
|
|
|
|
return await super().__call__(config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@handle_message('!settoday')
|
|
|
|
|
def cmd_settoday(match: Match[str]) -> Response:
|
|
|
|
|
if match['user'] != match['channel']:
|
|
|
|
|
return MessageResponse(
|
|
|
|
|
match, 'https://www.youtube.com/watch?v=RfiQYRn7fBg',
|
|
|
|
|
)
|
|
|
|
|
_, _, msg = match.groups()
|
|
|
|
|
_, _, rest = msg.partition(' ')
|
|
|
|
|
return SetTodayResponse(match, rest)
|
|
|
|
|
|
|
|
|
|
|
2020-05-04 19:35:44 +00:00
|
|
|
async def check_call(*cmd: str) -> None:
|
|
|
|
|
proc = await asyncio.subprocess.create_subprocess_exec(
|
|
|
|
|
*cmd, stdout=asyncio.subprocess.DEVNULL,
|
|
|
|
|
)
|
|
|
|
|
await proc.communicate()
|
|
|
|
|
if proc.returncode != 0:
|
|
|
|
|
raise ValueError(cmd, proc.returncode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VideoIdeaResponse(MessageResponse):
|
|
|
|
|
def __init__(self, match: Match[str], videoidea: str) -> None:
|
|
|
|
|
super().__init__(
|
|
|
|
|
match,
|
|
|
|
|
'added! https://github.com/asottile/scratch/wiki/anthony-explains-ideas', # noqa: E501
|
|
|
|
|
)
|
|
|
|
|
self.videoidea = videoidea
|
|
|
|
|
|
|
|
|
|
async def __call__(self, config: Config) -> Optional[str]:
|
|
|
|
|
async def _git(*cmd: str) -> None:
|
|
|
|
|
await check_call('git', '-C', tmpdir, *cmd)
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
|
await _git(
|
|
|
|
|
'clone', '--depth=1', '--quiet',
|
|
|
|
|
'git@github.com: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'- {self.videoidea}\n'.encode())
|
|
|
|
|
await _git('add', '.')
|
|
|
|
|
await _git('commit', '-q', '-m', 'idea added by !videoidea')
|
|
|
|
|
await _git('push', '-q', 'origin', 'HEAD')
|
|
|
|
|
return await super().__call__(config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@handle_message('![wv]ideoidea')
|
|
|
|
|
def cmd_videoidea(match: Match[str]) -> Response:
|
|
|
|
|
if match['user'] != match['channel']:
|
|
|
|
|
return MessageResponse(
|
|
|
|
|
match, 'https://www.youtube.com/watch?v=RfiQYRn7fBg',
|
|
|
|
|
)
|
|
|
|
|
_, _, rest = match['msg'].partition(' ')
|
|
|
|
|
return VideoIdeaResponse(match, rest)
|
|
|
|
|
|
|
|
|
|
|
2019-03-03 01:00:14 +00:00
|
|
|
class UptimeResponse(Response):
|
|
|
|
|
async def __call__(self, config: Config) -> Optional[str]:
|
|
|
|
|
url = f'https://api.twitch.tv/helix/streams?user_login={config.channel}' # noqa: E501
|
|
|
|
|
headers = {'Client-ID': config.client_id}
|
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
|
|
|
text = await response.text()
|
2019-03-07 01:31:25 +00:00
|
|
|
data = json.loads(text)['data']
|
|
|
|
|
if not data:
|
|
|
|
|
msg = 'not currently streaming!'
|
|
|
|
|
return PRIVMSG.format(channel=config.channel, msg=msg)
|
|
|
|
|
start_time_s = data[0]['started_at']
|
2019-03-03 01:00:14 +00:00
|
|
|
start_time = datetime.datetime.strptime(
|
|
|
|
|
start_time_s, '%Y-%m-%dT%H:%M:%SZ',
|
|
|
|
|
)
|
|
|
|
|
elapsed = (datetime.datetime.utcnow() - start_time).seconds
|
|
|
|
|
|
|
|
|
|
parts = []
|
|
|
|
|
for n, unit in (
|
|
|
|
|
(60 * 60, 'hours'),
|
|
|
|
|
(60, 'minutes'),
|
|
|
|
|
(1, 'seconds'),
|
|
|
|
|
):
|
|
|
|
|
if elapsed // n:
|
|
|
|
|
parts.append(f'{elapsed // n} {unit}')
|
|
|
|
|
elapsed %= n
|
|
|
|
|
msg = f'streaming for: {", ".join(parts)}'
|
|
|
|
|
return PRIVMSG.format(channel=config.channel, msg=msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@handle_message('!uptime')
|
|
|
|
|
def cmd_uptime(match: Match[str]) -> Response:
|
|
|
|
|
return UptimeResponse()
|
|
|
|
|
|
|
|
|
|
|
2019-11-23 23:46:06 +00:00
|
|
|
@handle_message(r'!pep[ ]?(?P<pep_num>\d{1,4})')
|
2019-11-16 22:07:27 +00:00
|
|
|
def cmd_pep(match: Match[str]) -> Response:
|
2019-11-23 23:17:47 +00:00
|
|
|
*_, number = match.groups()
|
2019-11-23 23:47:20 +00:00
|
|
|
n = str(int(number)).zfill(4)
|
|
|
|
|
return MessageResponse(match, f'https://www.python.org/dev/peps/pep-{n}/')
|
2019-11-16 22:19:36 +00:00
|
|
|
|
2019-11-16 22:07:27 +00:00
|
|
|
|
2019-06-08 20:51:36 +00:00
|
|
|
COMMAND_RE = re.compile(r'!\w+')
|
2019-08-11 00:19:15 +00:00
|
|
|
SECRET_CMDS = frozenset(('!settoday',))
|
2019-06-08 20:51:36 +00:00
|
|
|
|
|
|
|
|
|
2019-03-03 01:00:14 +00:00
|
|
|
@handle_message(r'!\w')
|
|
|
|
|
def cmd_help(match: Match[str]) -> Response:
|
2019-06-08 20:51:36 +00:00
|
|
|
possible = [COMMAND_RE.search(reg.pattern) for reg, _ in HANDLERS]
|
2019-08-11 00:19:15 +00:00
|
|
|
possible_cmds = {match[0] for match in possible if match} - SECRET_CMDS
|
|
|
|
|
commands = ['!help'] + sorted(possible_cmds)
|
2019-06-08 20:51:36 +00:00
|
|
|
msg = f'possible commands: {", ".join(commands)}'
|
2019-03-03 01:00:14 +00:00
|
|
|
if not match['msg'].startswith('!help'):
|
|
|
|
|
msg = f'unknown command ({esc(match["msg"].split()[0])}), {msg}'
|
|
|
|
|
return MessageResponse(match, msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@handle_message('PING')
|
|
|
|
|
def msg_ping(match: Match[str]) -> Response:
|
|
|
|
|
_, _, msg = match.groups()
|
|
|
|
|
_, _, rest = msg.partition(' ')
|
|
|
|
|
return MessageResponse(match, f'PONG {esc(rest)}')
|
|
|
|
|
|
|
|
|
|
|
2019-10-19 21:10:46 +00:00
|
|
|
@handle_message(r'.*\b(nano|linux|windows|emacs)\b', flags=re.IGNORECASE)
|
2019-05-25 23:12:27 +00:00
|
|
|
def msg_gnu_please(match: Match[str]) -> Response:
|
|
|
|
|
msg, word = match[3], match[4]
|
2019-10-26 18:53:29 +00:00
|
|
|
query = re.search(f'gnu[/+]{word}', msg, flags=re.IGNORECASE)
|
2019-10-19 21:48:21 +00:00
|
|
|
if query:
|
2019-10-19 22:03:52 +00:00
|
|
|
return MessageResponse(match, f'YES! {query[0]}')
|
2019-05-25 23:12:27 +00:00
|
|
|
else:
|
2019-10-19 22:04:50 +00:00
|
|
|
return MessageResponse(match, f"Um please, it's GNU+{esc(word)}!")
|
2019-05-25 23:12:27 +00:00
|
|
|
|
|
|
|
|
|
2019-12-09 04:30:55 +00:00
|
|
|
@handle_message(r'.*\bth[oi]nk(?:ing)?\b', flags=re.IGNORECASE)
|
|
|
|
|
def msg_think(match: Match[str]) -> Response:
|
|
|
|
|
return MessageResponse(match, 'anthon63Pythonk ' * 5)
|
|
|
|
|
|
|
|
|
|
|
2019-03-03 01:00:14 +00:00
|
|
|
# TODO: !tags, only allowed by stream admin / mods????
|
|
|
|
|
|
2019-12-09 04:28:39 +00:00
|
|
|
def dt_str() -> str:
|
|
|
|
|
dt_now = datetime.datetime.now()
|
2019-12-09 07:05:00 +00:00
|
|
|
return f'[{dt_now.hour:02}:{dt_now.minute:02}]'
|
2019-03-03 01:00:14 +00:00
|
|
|
|
|
|
|
|
|
2019-12-09 04:28:39 +00:00
|
|
|
async def amain(config: Config, *, quiet: bool) -> NoReturn:
|
2019-03-03 01:00:14 +00:00
|
|
|
reader, writer = await asyncio.open_connection(HOST, PORT, ssl=True)
|
|
|
|
|
|
|
|
|
|
await send(writer, f'PASS {config.oauth_token}\r\n', quiet=True)
|
2019-12-09 04:28:39 +00:00
|
|
|
await send(writer, f'NICK {config.username}\r\n', quiet=quiet)
|
|
|
|
|
await send(writer, f'JOIN #{config.channel}\r\n', quiet=quiet)
|
2019-03-03 01:00:14 +00:00
|
|
|
|
|
|
|
|
while True:
|
2019-12-09 04:28:39 +00:00
|
|
|
data = await recv(reader, quiet=quiet)
|
2019-03-03 01:00:14 +00:00
|
|
|
msg = data.decode('UTF-8', errors='backslashreplace')
|
2019-12-09 04:28:39 +00:00
|
|
|
|
|
|
|
|
msg_match = MSG_RE.match(msg)
|
|
|
|
|
if msg_match:
|
|
|
|
|
print(f'{dt_str()}<{msg_match[1]}> {msg_match[2]}')
|
|
|
|
|
|
2019-03-03 01:00:14 +00:00
|
|
|
for pattern, handler in HANDLERS:
|
|
|
|
|
match = pattern.match(msg)
|
|
|
|
|
if match:
|
|
|
|
|
try:
|
|
|
|
|
res = await handler(match)(config)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
res = PRIVMSG.format(
|
|
|
|
|
channel=config.channel,
|
|
|
|
|
msg=f'*** unhandled {type(e).__name__} -- see logs',
|
|
|
|
|
)
|
|
|
|
|
if res is not None:
|
2019-12-09 04:28:39 +00:00
|
|
|
send_match = SEND_MSG_RE.match(res)
|
|
|
|
|
if send_match:
|
|
|
|
|
print(f'{dt_str()}<{config.username}> {send_match[1]}')
|
|
|
|
|
await send(writer, res, quiet=quiet)
|
2019-03-03 01:00:14 +00:00
|
|
|
break
|
2019-12-09 04:28:39 +00:00
|
|
|
else:
|
|
|
|
|
if not quiet:
|
|
|
|
|
print(f'UNHANDLED: {msg}', end='')
|
2019-03-03 01:00:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
parser.add_argument('--config', default='config.json')
|
2019-12-09 04:28:39 +00:00
|
|
|
parser.add_argument('--verbose', action='store_true')
|
2019-03-03 01:00:14 +00:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
with open(args.config) as f:
|
|
|
|
|
config = Config(**json.load(f))
|
|
|
|
|
|
2019-12-09 04:28:39 +00:00
|
|
|
asyncio.run(amain(config, quiet=not args.verbose))
|
2019-03-03 01:00:14 +00:00
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
exit(main())
|