summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--columbo/__init__.py24
-rw-r--r--columbo/irc_client.py70
3 files changed, 95 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bee8a64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/columbo/__init__.py b/columbo/__init__.py
new file mode 100644
index 0000000..6318e6d
--- /dev/null
+++ b/columbo/__init__.py
@@ -0,0 +1,24 @@
+from . import irc_client
+from dataclasses import dataclass
+import irctokens
+import trio
+
+@dataclass
+class Config:
+ clients: list[irc_client.Config]
+
+async def start(config):
+ async with trio.open_nursery() as nursery:
+ channel, inbox = trio.open_memory_channel(0)
+ for client in config.clients:
+ nursery.start_soon(irc_client.connect, client, channel)
+ async for source, line in inbox:
+ outbox, client = source
+ print(f'Got line: {line.format()}')
+ reply_target = None
+ if line.params[0] == client.current_nick:
+ reply_target = line.hostmask.nickname
+ elif line.params[1].startswith(','):
+ reply_target = line.params[0]
+ if reply_target:
+ await outbox.send(irctokens.build('PRIVMSG', [reply_target, f'Hi {line.hostmask.nickname}']))
diff --git a/columbo/irc_client.py b/columbo/irc_client.py
new file mode 100644
index 0000000..e227941
--- /dev/null
+++ b/columbo/irc_client.py
@@ -0,0 +1,70 @@
+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)