Compare commits

...

10 Commits

Author SHA1 Message Date
Juhani Krekelä 8b460ad442 Update o3-base 2022-03-23 20:45:54 +02:00
Juhani Krekelä 9f5f74c456 Flush log output after each line in case output is buffered 2021-12-12 17:07:00 +02:00
Juhani Krekelä 2c6d35347c Revert "Log to stderr if run non-interactively"
This reverts commit deae8043e1.
2021-12-12 17:05:31 +02:00
Juhani Krekelä deae8043e1 Log to stderr if run non-interactively 2021-12-12 17:01:54 +02:00
Juhani Krekelä 424e6c7b67 Log connection attempts to server 2021-12-12 16:45:52 +02:00
Juhani Krekelä ed314735f9 Disable interactive console if stdin is not a tty 2021-12-12 16:37:58 +02:00
Juhani Krekelä de139b14b8 Bye Freenode 2021-12-12 16:37:35 +02:00
Juhani Krekelä f6fe53385d Add built-in authentication to o3-base 2021-05-02 00:40:22 +03:00
Juhani Krekelä 635cab7440 Don't allow sending to the closed server socket if we are reconnecting 2019-05-11 15:32:27 +03:00
Juhani Krekelä 13c62c8577 Use the CC0 file 2019-05-11 14:37:22 +03:00
3 changed files with 58 additions and 23 deletions

View File

@ -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 =

View File

@ -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)

View File

@ -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: