diff --git a/bot-async.py b/bot-async.py new file mode 100644 index 0000000..9070fe4 --- /dev/null +++ b/bot-async.py @@ -0,0 +1,366 @@ +#!/usr/bin/python3 +from socket import socket, AF_INET, SOCK_STREAM +from overrides import bytes, bbytes +import logs +import re +from typing import NoReturn, Union +import commands as cmds +import config as conf +from time import sleep +from importlib import reload +import threads +import random as r +import handlers +import bare +from threading import Thread +from markov import MarkovBot +from traceback import format_exc + + +def mfind(message: str, find: list, usePrefix: bool = True) -> bool: + if usePrefix: + return any(message[: len(match) + 1] == conf.prefix + match for match in find) + else: + return any(message[: len(match)] == match for match in find) + + +class bot(bare.bot): + def __init__(self, server: str): + self.gmode = False + self.server = server + self.nicklen = 30 + self.address = conf.servers[server]["address"] + self.port = ( + conf.servers[server]["port"] if "port" in conf.servers[server] else 6667 + ) + self.channels = conf.servers[server]["channels"] + self.adminnames = ( + conf.servers[server]["admins"] if "admins" in conf.servers[server] else [] + ) + self.ignores = ( + conf.servers[server]["ignores"] if "ignores" in conf.servers[server] else [] + ) + self.__version__ = conf.__version__ + self.npallowed = conf.npallowed + self.interval = ( + conf.servers[server]["interval"] + if "interval" in conf.servers[server] + else 50 + ) + self.nick = ( + conf.servers[server]["nick"] + if "nick" in conf.servers[server] + else "FireBot" + ) + self.queue: list[bbytes] = [] # pyright: ignore [reportInvalidTypeForm] + self.statuses = {"firepup": {}} + self.ops = {} + self.sock = socket(AF_INET, SOCK_STREAM) + self.current = "user" + self.threads = ( + conf.servers[server]["threads"] if "threads" in conf.servers[server] else [] + ) + self.onIdntCmds = ( + conf.servers[server]["onIdntCmds"] + if "onIdntCmds" in conf.servers[server] + else [] + ) + self.onJoinCmds = ( + conf.servers[server]["onJoinCmds"] + if "onJoinCmds" in conf.servers[server] + else [] + ) + self.onStrtCmds = ( + conf.servers[server]["onStrtCmds"] + if "onStrtCmds" in conf.servers[server] + else [] + ) + self.autoMethod = ( + conf.servers[server]["autoMethod"] + if "autoMethod" in conf.servers[server] + else "QUOTE" + ) + self.dnsblMode = ( + conf.servers[server]["dnsblMode"] + if "dnsblMode" in conf.servers[server] + else "none" + ) + self.dns = {} + self.lastfmLink = conf.lastfmLink + with open("mastermessages.txt") as f: + TMFeed = [] + for line in f.readlines(): + TMFeed.extend([line.strip().split()]) + self.markov = MarkovBot(TMFeed) + conf.prefix = ( + conf.servers[server]["prefix"] if "prefix" in conf.servers[server] else "." + ) + self.log(f"Start init for {self.server}") + + def connect(self) -> None: + self.log(f"Joining {self.server}...") + self.sock.connect((self.address, self.port)) + self.send("\n") # Just for sanity + if self.onStrtCmds: + for cmd in self.onStrtCmds: + self.send(cmd + "\n") + if "serverPass" in conf.servers[self.server]: + self.send(f"PASS {conf.servers[self.server]['serverPass']}\n") + self.send(f"USER {self.nick} {self.nick} {self.nick} {self.nick}\n") + self.send(f"NICK {self.nick}\n") + ircmsg = "" + while True: + ircmsg = self.recv().safe_decode() + if ircmsg != "": + code = 0 + try: + code = int(ircmsg.split(" ", 2)[1].strip()) + except (IndexError, ValueError): + pass + print(bytes(ircmsg).lazy_decode()) + if "NICKLEN" in ircmsg: + self.nicklen = int(ircmsg.split("NICKLEN=")[1].split(" ")[0]) + self.log(f"NICKLEN set to {self.nicklen}") + if code == 433: + self.log("Nickname in use", "WARN") + self.nick = f"{self.nick}{r.randint(0,1000)}" + self.send(f"NICK {self.nick}\n") + self.log(f"nick is now {self.nick}") + if code in [376, 422]: + self.log(f"Success by code: {code}") + break + if " MODE " in ircmsg or " PRIVMSG " in ircmsg: + self.log(f"Success by MSG/MODE") + break + if ircmsg.startswith("PING "): + self.ping(ircmsg) + if len(ircmsg.split("\x01")) == 3: + handlers.CTCP(self, ircmsg) + if "Closing link" in ircmsg: + self.exit("Closing Link") + else: + self.exit("Lost connection to the server") + self.log(f"Joined {self.server} successfully!") + + def join(self, chan: str, origin: str, lock: bool = True) -> None: + self.log(f"Joining {chan}...") + chan = chan.replace(" ", "") + if "," in chan: + chans = chan.split(",") + for subchan in chans: + self.join(subchan, origin) + return + if chan.startswith("0") or ( + chan == "#main" and lock and self.server != "replirc" + ): + if origin != "null": + self.msg(f"Refusing to join channel {chan} (protected)", origin) + return + if chan in self.channels and lock: + if origin != "null": + self.msg(f"I'm already in {chan}.", origin) + return + self.send(f"JOIN {chan}\n") + while True: + ircmsg = self.recv().safe_decode() + if ircmsg != "": + code = 0 + try: + code = int(ircmsg.split(" ", 2)[1].strip()) + except (IndexError, ValueError): + pass + print(bytes(ircmsg).lazy_decode()) + if ircmsg.startswith("PING "): + self.ping(ircmsg) + elif ircmsg.startswith("ERROR "): + self.exit("Lost connection to the server while joining a channel") + elif len(ircmsg.split("\x01")) == 3: + handlers.CTCP(self, ircmsg) + elif code == 403: + self.log(f"Joining {chan} failed", "WARN") + if origin != "null": + self.msg(f"{chan} is an invalid channel", origin) + break + elif code == 473: + self.log(f"Joining {chan} failed (+i)", "WARN") + if origin != "null": + self.msg(f"{chan} is +i, and I'm not invited.", origin) + break + elif code == 474: + self.log(f"Joining {chan} failed (+b)", "WARN") + if origin != "null": + self.msg(f"I'm banned from {chan}.", origin) + break + elif code == 480: + self.log(f"Joining {chan} failed (+S)", "WARN") + if origin != "null": + self.msg( + f"{chan} is +S, and I'm not connected over SSL.", origin + ) + break + elif code == 519: + self.log(f"Joining {chan} failed (+A)", "WARN") + if origin != "null": + self.msg(f"{chan} is +A, and I'm not an admin.", origin) + break + elif code == 520: + self.log(f"Joining {chan} failed (+O)", "WARN") + if origin != "null": + self.msg(f"{chan} is +O, and I'm not an operator.", origin) + break + elif code == 405: + self.log(f"Joining {chan} failed (too many channels)", "WARN") + if origin != "null": + self.msg(f"I'm in too many channels to join {chan}", origin) + break + elif code == 471: + self.log(f"Joining {chan} failed (+l)", "WARN") + if origin != "null": + self.msg(f"{chan} is +l, and is full", origin) + break + elif code == 366: + self.log(f"Joining {chan} succeeded") + if origin != "null": + self.msg(f"Joined {chan}", origin) + self.channels[chan] = 0 + break + + def ping(self, ircmsg: str) -> int: + pong = f"PONG :{ircmsg.split('PING :')[1]}\n" + print(pong, end="") + return self.send(pong) + + def send(self, command: str) -> int: + return self.sock.send(bytes(command)) + + def recv(self) -> bytes: + if self.queue: + return bytes(self.queue.pop(0)) + data = bytes(self.sock.recv(2048)) + if data.lazy_decode() == "": + return data + while not data.endswith(b"\r\n"): + data += bytes(self.sock.recv(2048)) + data = bytes(data.strip(b"\r\n")) + if b"\r\n" in data: + self.queue.extend(data.split(b"\r\n")) + return bytes(self.queue.pop(0)) + return data + + def log(self, message: str, level: str = "LOG") -> None: + logs.log(message, self.server, level) + + def exit(self, message: str) -> NoReturn: + logs.log(message, self.server, "EXIT") + exit(1) + + def msg(self, msg: str, target: str) -> None: + if not (target == "NickServ" and mfind(msg, ["IDENTIFY"], False)): + self.log(f"Sending {bytes(msg).lazy_decode()} to {target}") + else: + self.log("Identifying myself...") + self.send(f"PRIVMSG {target} :{msg}\n") + + def op(self, name: str, chan: str) -> Union[int, None]: + if name != "": + self.log(f"Attempting op of {name} in {chan}...") + return self.send(f"MODE {chan} +o {name}\n") + return None + + def notice(self, msg: str, target: str, silent: bool = False) -> int: + if not silent: + self.log(f"Sending {bytes(msg).lazy_decode()} to {target} (NOTICE)") + return self.send(f"NOTICE {target} :{msg}\n") + + def sendraw(self, command: str) -> int: + self.log(f"RAW sending {command}") + command = f"{command}\n" + return self.send(command.replace("$BOTNICK", self.nick)) + + def mainloop(self) -> NoReturn: + self.log("Starting connection..") + self.connect() + if "pass" in conf.servers[self.server]: + self.msg( + f"IDENTIFY FireBot {conf.servers[self.server]['pass']}", "NickServ" + ) + sleep(0.5) + if self.onIdntCmds: + for cmd in self.onIdntCmds: + self.send(cmd + "\n") + for chan in self.channels: + self.join(chan, "null", False) + if self.onJoinCmds: + for cmd in self.onJoinCmds: + self.send(cmd + "\n") + tMgr = None + if self.threads: + tdict = {} + for thread in self.threads: + tdict[thread] = threads.data[thread] + if tdict[thread]["passInstance"]: + tdict[thread]["args"] = [self] + tMgr = Thread(target=threads.threadManager, args=(tdict,)) + tMgr.daemon = True + tMgr.start() + while 1: + raw = self.recv() + ircmsg = raw.safe_decode() + if ircmsg == "": + self.exit("Probably a netsplit") + else: + print(raw.lazy_decode(), sep="\n") + action = "Unknown" + try: + action = ircmsg.split(" ", 2)[1].strip() + except IndexError: + pass + self.tmpHost = "" + if action in handlers.handles: + res, chan = handlers.handles[action](self, ircmsg) + if res == "reload" and type(chan) == str: + try: + reload(conf) + self.adminnames = ( + conf.servers[self.server]["admins"] + if "admins" in conf.servers[self.server] + else [] + ) + self.ignores = ( + conf.servers[self.server]["ignores"] + if "ignores" in conf.servers[self.server] + else [] + ) + self.__version__ = conf.__version__ + self.npallowed = conf.npallowed + self.interval = ( + conf.servers[self.server]["interval"] + if "interval" in conf.servers[self.server] + else 50 + ) + conf.prefix = ( + conf.servers[self.server]["prefix"] + if "prefix" in conf.servers[self.server] + else "." + ) + reload(cmds) + reload(handlers) + self.msg("Reloaded successfully", chan) + except Exception: + Err = format_exc() + for line in Err.split("\n"): + self.log(line, "ERROR") + self.msg( + "Reload failed, likely partially reloaded. Please check error logs.", + chan, + ) + else: + if ircmsg.startswith("PING "): + self.ping(ircmsg) + elif ircmsg.startswith("ERROR :Closing Link"): + self.exit("I got killed :'(") + elif ircmsg.startswith("ERROR :Ping "): + self.exit("Ping timeout") + else: + self.log("Unrecognized server request!", "WARN") + self.exit("While loop broken")