from base64 import b64encode from dataclasses import dataclass from irctokens import StatefulDecoder, build from typing import Optional import trio @dataclass class Config: host: str nick: str password: str port: int = 6667 leader: str = ',' user: str = 'columbo' channels: Optional[list[str]] = None realname: str = 'columbo-irc_client v0.1' current_nick: Optional[str] = None encoding: str = 'utf-8' async def _sender(stream, config, inbox): def prepare(line): print(f'>{line}') return f'{line.format()}\r\n'.encode(config.encoding) config.current_nick = config.nick preamble = [ ('CAP', ['REQ', 'sasl']), ('USER', [config.user, '0', '*', config.realname]), ('NICK', [config.nick]), ] for args in preamble: await stream.send_all(prepare(build(*args))) async with inbox: async for line in inbox: await stream.send_all(prepare(line)) async def _handle(config, channel, outbox, line): print(f'<{line}') if line.command == 'PING': await outbox.send(build('PONG', [line.params[0]])) elif line.command == 'CAP': if line.params[1] == 'ACK' and line.params[2] == 'sasl': await outbox.send(build('AUTHENTICATE', ['PLAIN'])) elif line.command == 'AUTHENTICATE': if line.params[0] == '+': creds = b64encode(f'\0{config.nick}\0{config.password}'.encode(config.encoding)).decode() await outbox.send(build('AUTHENTICATE', [creds])) elif line.command == '900': # RPL_LOGGEDIN await outbox.send(build('CAP', ['END'])) elif line.command == '001': # RPL_WELCOME config.current_nick = line.params[0] if config.channels: for channel in config.channels: await outbox.send(build('JOIN', [channel])) elif line.command == 'PRIVMSG': await channel.send(((outbox, config), line)) async def _receiver(stream, config, channel, outbox): decoder = StatefulDecoder() async with channel, outbox: async for data in stream: for line in decoder.push(data): await _handle(config, channel, outbox, line) async def connect(config, channel): stream = await trio.open_tcp_stream(config.host, config.port) outbox_send, outbox_recv = trio.open_memory_channel(0) async with stream: async with trio.open_nursery() as nursery: nursery.start_soon(_sender, stream, config, outbox_recv) nursery.start_soon(_receiver, stream, config, channel, outbox_send)