Added coroutines system to components

This commit is contained in:
aewens 2019-05-15 00:13:18 +02:00
parent e47d8d0441
commit 0cddba95e7
5 changed files with 325 additions and 141 deletions

2
.gitignore vendored
View File

@ -3,6 +3,8 @@ abots-git/
abots/
abots
*.log
*.swp
settings.json
# ---> Python
# Byte-compiled / optimized / DLL files

258
client.py
View File

@ -1,7 +1,8 @@
#!/opt/abots/env/bin/python3
from components import IRCSocketClient
from abots.helpers import Logger, infinitedict, isnumeric
from components import IRCSocketClient, Composer
from abots.net import PrefixSocketClient
from abots.helpers import Logger, infinitedict, isnumeric, coroutine
from time import sleep
from os import remove as delete_file
@ -17,141 +18,126 @@ settings["stream"]["formatter"] = "%(message)s"
logger = Logger(logname, settings=settings)
logger.start()
irc_host = "localhost"
irc_port = 6667
composer = Composer(logger)
irc_host = "irc.tilde.chat"#"localhost"
irc_port = 6697
irc_timeout = 3
irc_client = IRCSocketClient(irc_host, irc_port, timeout=irc_timeout)
irc_client.start()
inbox = irc_client.queues.get("inbox")
outbox = irc_client.queues.get("outbox")
events = irc_client.queues.get("events")
#sleep(2)
irc_args = irc_host, irc_port, irc_timeout, True
irc_client, inbox, outbox, events = composer.start_socket(*irc_args)
irc_client.ready.wait()
# from section 2.3.1 of rfc1459
def parse_message(message):
meta = dict()
meta["raw"] = message
if message is None or len(message) == 0:
return meta
has_tags = message[0] == "@"
meta["has_tags"] = has_tags
if has_tags:
tags, message = message.lstrip("@").split(" ", 1)
meta["tags"] = dict()
for tag in tags.split(";"):
key, value = tag.split("=", 1)
meta["tags"][key] = value
has_prefix = message[0] == ":"
message = message.lstrip(":")
meta["has_prefix"] = has_prefix
if not has_prefix:
command, params = message.lstrip(":").split(" ", 1)
meta["type"] = "alpha"
else:
prefix, command, params = message.split(" ", 2)
source = "nick" if "!" in prefix else "server"
meta["prefix"] = prefix
meta["source"] = source
meta["type"] = "numeric" if isnumeric(command) else "alpha"
if source == "nick":
nick, user_info = prefix.split("!", 1)
user, host = user_info.split("@", 1)
meta["nick"] = nick
meta["user"] = user
meta["host"] = host
meta["command"] = command
meta["params"] = params
return meta
def cleanup(parameters, dirt):
params = parameters.partition(dirt)
return params[0] + params[2]
# NOTE - collect *might* not be needed, most testing is needed
def request(collect=None):
response = irc_client.recv()
if collect is not None:
response = collect + response
complete = response[-2:] == "\r\n"
lines = response.split("\r\n")
collect = None if complete else lines[-1]
if not complete:
lines = lines[:-1]
return lines, collect
def need(collect, values, matches=dict()):
assert isinstance(collect, str) or collect is None, "Expected str|None"
assert isinstance(values, str), "Expected str"
assert isinstance(matches, dict), "Expected dict"
if "," in values:
values = values.split(",")
elif values != "*":
values = [values]
lines, collect = request(collect)
results = list()
for line in lines:
meta = parse_message(line)
matched = True
for key, match in matches.items():
if meta.get(key, None) != match:
matched = False
break
if matched:
if values == "*":
results.append(meta)
continue
results.append({value: meta.get(value, None) for value in values})
return results, collect
def need_one(collect, values, matches=dict()):
results, collect = need(collect, values, matches)
if len(results) == 1:
return results[0], collect
return dict(), collect
def need_param(collect, matches=dict(), default=""):
result, collect = need_one(collect, "params", matches)
return result.get("params", default), collect
def extract(response, key, after, dirt=":"):
value = response.get(key, None)
if value is None:
return None
bisect = cleanup(value, dirt).split(after, 1)
if len(bisect) != 2:
return None
before, result = bisect
return result.strip()
def extract_params(collect, after, matches, dirt=":"):
result, collect = need_one(collect, "params", matches)
return extract(result, "params", after), collect
name = "babili"
collect = None
irc_client.send("CAP LS")
caps, collect = extract_params(collect, "* LS",
{"source": "server", "command": "CAP"})
if caps is not None:
logger.debug(caps)
irc_client.send(f"CAP REQ :{caps}")
status, collect = need_param(collect, {"source": "server", "command": "CAP"})
ack = "ACK" in status
logger.debug("ACK!")
irc_client.send("CAP END")
irc_client.send(f"USER {name} - - -")
irc_client.send(f"NICK {name}")
responses, collect = need(collect, "params",
{"source": "nick", "nick": "NickServ"})
nickserv_complain = "please choose a different nick"
register = False
for response in responses:
if nickserv_complain in response.get("params", ""):
register = True
break
if register:
logger.debug("Needs to register bot now")
irc_client.stop()
@coroutine
def log(cancel):
try:
while True:
sock, line = (yield)
meta = composer.parse_message(line)
if meta.get("command", None) == "ERROR":
logger.error(meta.get("raw"))
continue
logger.debug(meta.get("raw"))
except GeneratorExit:
pass
@coroutine
def stopper(cancel):
try:
while True:
sock, line = (yield)
meta = composer.parse_message(line)
if meta.get("nick", None) != "NickServ":
continue
if meta.get("command", None) != "NOTICE":
continue
params = composer.cleanup(meta.get("params", ""))
nickserv_complain = "please choose a different nick"
if nickserv_complain not in params:
continue
cancel()
sleep(0.01)
sock.send("QUIT bye-bye!")
sleep(1)
sock.stop()
composer.kill_switch.set()
break
except GeneratorExit:
pass
@coroutine
def init(cancel):
try:
while True:
sock, line = (yield)
meta = composer.parse_message(line)
if meta.get("source", None) != "server":
continue
if meta.get("command", None) != "NOTICE":
continue
if "hostname" not in meta.get("params", ""):
continue
cancel()
logger.debug(meta["raw"])
sleep(0.01)
sock.send("CAP LS")
break
except GeneratorExit:
pass
@coroutine
def caps(cancel):
try:
while True:
sock, line = (yield)
meta = composer.parse_message(line)
if meta.get("source", None) != "server":
continue
if meta.get("command", None) != "CAP":
continue
params = composer.cleanup(meta.get("params", ""))
if "* LS" in params:
capabilities = params.split("* LS", 1)[1].strip()
logger.debug(f"Capabilities: {capabilities}")
sock.send(f"CAP REQ :{capabilities}")
elif "* ACK" in params:
cancel()
composer.register_many([log, stopper])
logger.debug(f"ACK!")
sleep(0.01)
sock.send("CAP END")
sock.send(f"USER {name} - - -")
sock.send(f"NICK {name}")
break
except GeneratorExit:
pass
composer.register_many([init, caps])
composer.get_messages(irc_client)
#irc_client.send("CAP LS")
#caps, collect = extract_params(collect, "* LS",
# {"source": "server", "command": "CAP"})
#if caps is not None:
# logger.debug(caps)
# irc_client.send(f"CAP REQ :{caps}")
#status, collect = need_param(collect, {"source": "server", "command": "CAP"})
#ack = "ACK" in status
#logger.debug("ACK!")
#irc_client.send("CAP END")
#irc_client.send(f"USER {name} - - -")
#irc_client.send(f"NICK {name}")
#sleep(2)
#irc_client.send("VERSION")
#responses, collect = need(collect, "params",
# {"source": "nick", "nick": "NickServ"})
#nickserv_complain = "please choose a different nick"
#register = False
#for response in responses:
# if nickserv_complain in response.get("params", ""):
# register = True
# break
#if register:
# logger.debug("Needs to register bot now")
#irc_client.stop()

View File

@ -1 +1,2 @@
from components.socket import IRCSocketClient
from components.composer import Composer

194
components/composer.py Normal file
View File

@ -0,0 +1,194 @@
from components import IRCSocketClient
from abots.events import AsyncEvent
from abots.helpers import Logger, infinitedict, isnumeric, coroutine, sha256
from time import sleep
from os import remove as delete_file
from os.path import isfile
class Composer:
def __init__(self, logger):
self.logger = logger
self._targets = dict()
self.kill_switch = AsyncEvent()
def start_socket(self, host, port, timeout=None, secure=False):
sock = IRCSocketClient(host, port, timeout=timeout, secure=secure)
sock.start()
inbox = sock.queues.get("inbox")
outbox = sock.queues.get("outbox")
events = sock.queues.get("events")
return sock, inbox, outbox, events
def register(self, target):
key = sha256()
self._targets[key] = target(self.withdraw(key))
def register_many(self, targets):
for target in targets:
self.register(target)
def withdraw(self, key):
def cancel():
try:
del self._targets[key]
except KeyError:
pass
return cancel
@coroutine
def send_to_targets(self, sock):
while True:
line = (yield)
for key in list(self._targets):
target = self._targets.get(key, None)
if target is None:
continue
try:
target.send((sock, line))
except StopIteration:
cancel = self.withdraw(key)
cancel()
def get_messages(self, sock):
cache = ""
targets = self.send_to_targets(sock)
while not self.kill_switch.is_set():
for recv in sock.recv():
if recv is None or len(recv) == 0:
continue
if "\n" in recv:
lines = recv.split("\n")
targets.send(cache + lines[0].strip("\r"))
for line in lines[1:-1]:
targets.send(line.strip("\r"))
cache = lines[-1]
else:
cache = cache + recv
return cache
# from section 2.3.1 of rfc1459
def parse_message(self, message):
meta = dict()
meta["raw"] = message
if message is None or len(message) == 0:
return meta
has_tags = message[0] == "@"
meta["has_tags"] = has_tags
if has_tags:
tags, message = message.lstrip("@").split(" ", 1)
meta["tags"] = dict()
for tag in tags.split(";"):
key, value = tag.split("=", 1)
meta["tags"][key] = value
has_prefix = message[0] == ":"
message = message.lstrip(":")
meta["has_prefix"] = has_prefix
if not has_prefix:
command, params = message.lstrip(":").split(" ", 1)
meta["type"] = "alpha"
else:
prefix, command, params = message.split(" ", 2)
source = "nick" if "!" in prefix else "server"
meta["prefix"] = prefix
meta["source"] = source
meta["type"] = "numeric" if isnumeric(command) else "alpha"
if source == "nick":
nick, user_info = prefix.split("!", 1)
user, host = user_info.split("@", 1)
meta["nick"] = nick
meta["user"] = user
meta["host"] = host
meta["command"] = command
meta["params"] = params
return meta
def cleanup(self, parameters, dirt=":"):
params = parameters.partition(dirt)
return params[0] + params[2]
# NOTE - collect *might* not be needed, most testing is needed
def request(self, collect=None):
response = irc_client.recv()
if collect is not None:
response = collect + response
complete = response[-2:] == "\r\n"
lines = response.split("\r\n")
collect = None if complete else lines[-1]
if not complete:
lines = lines[:-1]
return lines, collect
def need(self, collect, values, matches=dict()):
assert isinstance(collect, str) or collect is None, "Expected str|None"
assert isinstance(values, str), "Expected str"
assert isinstance(matches, dict), "Expected dict"
if "," in values:
values = values.split(",")
elif values != "*":
values = [values]
lines, collect = request(collect)
results = list()
for line in lines:
meta = parse_message(line)
matched = True
for key, match in matches.items():
if meta.get(key, None) != match:
matched = False
break
if matched:
if values == "*":
results.append(meta)
continue
results.append({value: meta.get(value, None) for value in values})
return results, collect
def need_one(self, collect, values, matches=dict()):
results, collect = need(collect, values, matches)
if len(results) == 1:
return results[0], collect
return dict(), collect
def need_param(self, collect, matches=dict(), default=""):
result, collect = need_one(collect, "params", matches)
return result.get("params", default), collect
def extract(self, response, key, after, dirt=":"):
value = response.get(key, None)
if value is None:
return None
bisect = cleanup(value, dirt).split(after, 1)
if len(bisect) != 2:
return None
before, result = bisect
return result.strip()
def extract_params(self, collect, after, matches, dirt=":"):
result, collect = need_one(collect, "params", matches)
return extract(result, "params", after), collect
#name = "babili"
#collect = None
#irc_client.send("CAP LS")
#caps, collect = extract_params(collect, "* LS",
# {"source": "server", "command": "CAP"})
#if caps is not None:
# logger.debug(caps)
# irc_client.send(f"CAP REQ :{caps}")
#status, collect = need_param(collect, {"source": "server", "command": "CAP"})
#ack = "ACK" in status
#logger.debug("ACK!")
#irc_client.send("CAP END")
#irc_client.send(f"USER {name} - - -")
#irc_client.send(f"NICK {name}")
#responses, collect = need(collect, "params",
# {"source": "nick", "nick": "NickServ"})
#nickserv_complain = "please choose a different nick"
#register = False
#for response in responses:
# if nickserv_complain in response.get("params", ""):
# register = True
# break
#if register:
# logger.debug("Needs to register bot now")
#irc_client.stop()

View File

@ -13,6 +13,7 @@ from socket import timeout as sock_timeout
class IRCSocketClient(SocketClient):
def __init__(self, *args, **kwargs):
kwargs["buffer_size"] = 512
super().__init__(*args, **kwargs)
def _format_message(self, message, *args):
@ -23,11 +24,11 @@ class IRCSocketClient(SocketClient):
packaged = f"{formatted}\r\n"
return packaged.encode()
def recv(self):
data = ""
for buf in self._obtain(self._outbox):
data = data + buf
return data
#def recv(self):
# data = ""
# for buf in self._obtain(self._outbox):
# data = data + buf
# return data
# See rfc2812
#max_message_length = 512