diff --git a/bot.conf.example b/bot.conf.example index b96e530..5d892f1 100644 --- a/bot.conf.example +++ b/bot.conf.example @@ -1,10 +1,11 @@ [server] -host = irc.freenode.net +host = irc.libera.chat port = 6667 nick = taash-e-aakramak username = o3-base realname = IRC bot based on o3-base channels = ###cards -[nickserv] +[auth] +user = password = diff --git a/botcmd.py b/botcmd.py index 0675fab..795c2e8 100644 --- a/botcmd.py +++ b/botcmd.py @@ -9,7 +9,6 @@ import gameloop import random import re -nickserv_pass = None irc_chan = None game_channel = None @@ -372,8 +371,7 @@ def parse_command(message, nick, irc): # Runs before even logger is brought up, and blocks further bringup until it's done # config is a configpatser.ConfigParser object containig contents of bot.conf def initialize(*, config): - global nickserv_pass, irc_chan - nickserv_pass = config['nickserv']['password'] + global irc_chan irc_chan = config['server']['channels'].split()[0].encode() # on_connect(*, irc) @@ -382,12 +380,6 @@ def initialize(*, config): # Blocks the bot until it's done, including PING/PONG handling # irc is the IRC API object def on_connect(*, irc): - global nickserv_pass - - if nickserv_pass != '': - irc.msg(b'nickserv', b'IDENTIFY ' + nickserv_pass.encode()) - time.sleep(30) # One day I will do this correctly. Today is not the day - stop_gameloop() start_gameloop(irc) diff --git a/constants.py b/constants.py index 402a494..7b624cc 100644 --- a/constants.py +++ b/constants.py @@ -4,7 +4,7 @@ class logmessage_types(enum.Enum): sent, received, internal, status = range(4) class internal_submessage_types(enum.Enum): - quit, error = range(2) + quit, error, server = range(3) class controlmessage_types(enum.Enum): quit, reconnect, send_line, ping, ping_timeout = range(5) diff --git a/ircbot.py b/ircbot.py index 5af87fb..7ec5f50 100644 --- a/ircbot.py +++ b/ircbot.py @@ -2,6 +2,7 @@ import configparser import select import socket +import sys import threading import time from collections import namedtuple @@ -16,7 +17,8 @@ import line_handling Server = namedtuple('Server', ['host', 'port', 'nick', 'username', 'realname', 'channels']) class LoggerThread(threading.Thread): - def __init__(self, logging_channel, dead_notify_channel): + def __init__(self, interactive_console, logging_channel, dead_notify_channel): + self.interactive_console = interactive_console self.logging_channel = logging_channel self.dead_notify_channel = dead_notify_channel @@ -29,17 +31,18 @@ class LoggerThread(threading.Thread): # Lines that were sent between server and client if message_type == logmessage_types.sent: assert len(message_data) == 1 - print('>' + message_data[0]) + if self.interactive_console: print('>' + message_data[0]) elif message_type == logmessage_types.received: assert len(message_data) == 1 - print('<' + message_data[0]) + if self.interactive_console: print('<' + message_data[0]) # Messages that are from internal components elif message_type == logmessage_types.internal: if message_data[0] == internal_submessage_types.quit: assert len(message_data) == 1 print('--- Quit') + sys.stdout.flush() self.dead_notify_channel.send((controlmessage_types.quit,)) break @@ -47,18 +50,28 @@ class LoggerThread(threading.Thread): elif message_data[0] == internal_submessage_types.error: assert len(message_data) == 2 print('--- Error', message_data[1]) + sys.stdout.flush() + + elif message_data[0] == internal_submessage_types.server: + assert len(message_data) == 2 + assert len(message_data[1]) == 2 + print(f'--- Connecting to server {message_data[1][0]}:{message_data[1][1]}') + sys.stdout.flush() else: print('--- ???', message_data) + sys.stdout.flush() # Messages about status from the bot code elif message_type == logmessage_types.status: assert len(message_data) == 2 print('*', end='') print(*message_data[0], **message_data[1]) + sys.stdout.flush() else: print('???', message_type, message_data) + sys.stdout.flush() # API(serverthread_object) # Create a new API object corresponding to given ServerThread object @@ -142,11 +155,12 @@ class API: self.serverthread_object.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, message)) -# ServerThread(server, control_channel, cron_control_channel, logging_channel) +# ServerThread(server, auth, control_channel, cron_control_channel, logging_channel) # Creates a new server main loop thread class ServerThread(threading.Thread): - def __init__(self, server, control_channel, cron_control_channel, logging_channel): + def __init__(self, server, auth, control_channel, cron_control_channel, logging_channel): self.server = server + self.auth = auth self.control_channel = control_channel self.cron_control_channel = cron_control_channel self.logging_channel = logging_channel @@ -183,7 +197,10 @@ class ServerThread(threading.Thread): time.sleep(wait) with self.server_socket_write_lock: - self.server_socket.sendall(line + b'\r\n') + if self.server_socket is not None: + self.server_socket.sendall(line + b'\r\n') + else: + return # Don't log PINGs or PONGs if not (len(line) >= 5 and (line[:5] == b'PING ' or line[:5] == b'PONG ')): @@ -289,6 +306,7 @@ class ServerThread(threading.Thread): while True: # Connect to given server address = (self.server.host, self.server.port) + self.logging_channel.send((logmessage_types.internal, internal_submessage_types.server, address)) try: self.server_socket = socket.create_connection(address) except (ConnectionRefusedError, socket.gaierror): @@ -327,10 +345,15 @@ class ServerThread(threading.Thread): try: # Run initialization - self.send_line_raw(b'USER %s a a :%s' % (self.server.username.encode('utf-8'), self.server.realname.encode('utf-8'))) - # Set up nick + # Use server-password based authentication if it's set up + user, password = self.auth + if user is not None: + self.send_line_raw(b'PASS %s:%s' % (user.encode(), password.encode())) + + # Set up nick and username self.api.nick(self.server.nick.encode('utf-8')) + self.send_line_raw(b'USER %s a a :%s' % (self.server.username.encode('utf-8'), self.server.realname.encode('utf-8'))) # Run the on_connect hook, to allow further setup botcmd.on_connect(irc = self.api) @@ -351,6 +374,7 @@ class ServerThread(threading.Thread): # Tell the server we're quiting self.send_line_raw(b'QUIT :%s exiting normally' % self.server.username.encode('utf-8')) + self.server_socket.close() break @@ -358,7 +382,9 @@ class ServerThread(threading.Thread): else: # Tell server we're reconnecting self.send_line_raw(b'QUIT :Reconnecting') - self.server_socket.close() + with self.server_socket_write_lock: + self.server_socket.close() + self.server_socket = None except (BrokenPipeError, TimeoutError) as err: # Connection broke, log it and try to reconnect @@ -371,20 +397,20 @@ class ServerThread(threading.Thread): # Tell cron we're quiting cron.quit(cron_control_channel) -# spawn_serverthread(server, cron_control_channel, logging_channel) → control_channel +# spawn_serverthread(server, auth, cron_control_channel, logging_channel) → control_channel # Creates a ServerThread for given server and returns the channel for controlling it -def spawn_serverthread(server, cron_control_channel, logging_channel): +def spawn_serverthread(server, auth, cron_control_channel, logging_channel): thread_control_socket, spawner_control_socket = socket.socketpair() control_channel = channel.Channel() - ServerThread(server, control_channel, cron_control_channel, logging_channel).start() + ServerThread(server, auth, control_channel, cron_control_channel, logging_channel).start() return control_channel -# spawn_loggerthread() → logging_channel, dead_notify_channel +# spawn_loggerthread(interactive_console) → logging_channel, dead_notify_channel # Spawn logger thread and returns the channel it logs and the channel it uses to notify about quiting -def spawn_loggerthread(): +def spawn_loggerthread(interactive_console): logging_channel = channel.Channel() dead_notify_channel = channel.Channel() - LoggerThread(logging_channel, dead_notify_channel).start() + LoggerThread(interactive_console, logging_channel, dead_notify_channel).start() return logging_channel, dead_notify_channel # read_config() → config, server @@ -400,20 +426,28 @@ def read_config(): realname = config['server']['realname'] channels = config['server']['channels'].split() + user = None + password = None + if 'auth' in config: + user = config['auth']['user'] + password = config['auth']['password'] + server = Server(host = host, port = port, nick = nick, username = username, realname = realname, channels = channels) - return config, server + return config, server, (user, password) if __name__ == '__main__': - config, server = read_config() + interactive_console = sys.stdin.isatty() + + config, server, auth = read_config() botcmd.initialize(config = config) cron_control_channel = cron.start() - logging_channel, dead_notify_channel = spawn_loggerthread() - control_channel = spawn_serverthread(server, cron_control_channel, logging_channel) + logging_channel, dead_notify_channel = spawn_loggerthread(interactive_console) + control_channel = spawn_serverthread(server, auth, cron_control_channel, logging_channel) - while True: + while interactive_console: message = dead_notify_channel.recv(blocking = False) if message is not None: if message[0] == controlmessage_types.quit: