Compare commits
No commits in common. "e4cf248afd3f21350bb7c64abefcfa4995fcff88" and "9b27bc728feb8b5d317c88a71f7581982efc04e1" have entirely different histories.
e4cf248afd
...
9b27bc728f
|
@ -1,5 +1,5 @@
|
|||
[server]
|
||||
host = irc.libera.chat
|
||||
host = irc.freenode.net
|
||||
port = 6667
|
||||
nick = cockatric4
|
||||
username = cockatric4
|
||||
|
|
13
botcmd.py
13
botcmd.py
|
@ -138,20 +138,11 @@ def handle_message(*, prefix, message, nick, channel, irc):
|
|||
|
||||
try:
|
||||
try:
|
||||
headers = {
|
||||
'User-Agent': 'Cockatric4 (like Lynx)',
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'en,*;q=0.1',
|
||||
'Accept-Charset': 'utf-8',
|
||||
}
|
||||
request = urllib.request.Request(url, headers = headers)
|
||||
with urllib.request.urlopen(request, timeout = 5) as response:
|
||||
with urllib.request.urlopen(url, timeout = 5) as response:
|
||||
if response.info().get_content_type() == 'text/html':
|
||||
# First 4KiB of a page should be enough for any <title>
|
||||
# Turns out it's not, so download 64KiB
|
||||
# As of 2023-09-10, youtube requires up to 320KiB (!),
|
||||
# so download first 512KiB
|
||||
page_source_fragment = response.read(512 * 1024)
|
||||
page_source_fragment = response.read(64 * 1024)
|
||||
title = sanitize(extract_title(page_source_fragment))
|
||||
|
||||
if title is not None:
|
||||
|
|
|
@ -4,7 +4,7 @@ class logmessage_types(enum.Enum):
|
|||
sent, received, internal, status = range(4)
|
||||
|
||||
class internal_submessage_types(enum.Enum):
|
||||
quit, error, server = range(3)
|
||||
quit, error = range(2)
|
||||
|
||||
class controlmessage_types(enum.Enum):
|
||||
quit, reconnect, send_line, ping, ping_timeout = range(5)
|
||||
|
|
85
ircbot.py
85
ircbot.py
|
@ -2,7 +2,6 @@
|
|||
import configparser
|
||||
import select
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from collections import namedtuple
|
||||
|
@ -17,8 +16,7 @@ import line_handling
|
|||
Server = namedtuple('Server', ['host', 'port', 'nick', 'username', 'realname', 'channels'])
|
||||
|
||||
class LoggerThread(threading.Thread):
|
||||
def __init__(self, interactive_console, logging_channel, dead_notify_channel):
|
||||
self.interactive_console = interactive_console
|
||||
def __init__(self, logging_channel, dead_notify_channel):
|
||||
self.logging_channel = logging_channel
|
||||
self.dead_notify_channel = dead_notify_channel
|
||||
|
||||
|
@ -31,18 +29,17 @@ class LoggerThread(threading.Thread):
|
|||
# Lines that were sent between server and client
|
||||
if message_type == logmessage_types.sent:
|
||||
assert len(message_data) == 1
|
||||
if self.interactive_console: print('>' + message_data[0])
|
||||
print('>' + message_data[0])
|
||||
|
||||
elif message_type == logmessage_types.received:
|
||||
assert len(message_data) == 1
|
||||
if self.interactive_console: print('<' + message_data[0])
|
||||
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
|
||||
|
@ -50,28 +47,18 @@ 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
|
||||
|
@ -155,12 +142,11 @@ class API:
|
|||
self.serverthread_object.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, message))
|
||||
|
||||
|
||||
# ServerThread(server, auth, control_channel, cron_control_channel, logging_channel)
|
||||
# ServerThread(server, control_channel, cron_control_channel, logging_channel)
|
||||
# Creates a new server main loop thread
|
||||
class ServerThread(threading.Thread):
|
||||
def __init__(self, server, auth, control_channel, cron_control_channel, logging_channel):
|
||||
def __init__(self, server, 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
|
||||
|
@ -197,10 +183,7 @@ class ServerThread(threading.Thread):
|
|||
time.sleep(wait)
|
||||
|
||||
with self.server_socket_write_lock:
|
||||
if self.server_socket is not None:
|
||||
self.server_socket.sendall(line + b'\r\n')
|
||||
else:
|
||||
return
|
||||
self.server_socket.sendall(line + b'\r\n')
|
||||
|
||||
# Don't log PINGs or PONGs
|
||||
if not (len(line) >= 5 and (line[:5] == b'PING ' or line[:5] == b'PONG ')):
|
||||
|
@ -238,14 +221,9 @@ class ServerThread(threading.Thread):
|
|||
if fd == self.server_socket.fileno():
|
||||
# Ready to receive, read into buffer and handle full messages
|
||||
if event | select.POLLIN:
|
||||
try:
|
||||
data = self.server_socket.recv(1024)
|
||||
except ConnectionResetError:
|
||||
self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, 'Connection reset while reading'))
|
||||
reconnecting = True
|
||||
break
|
||||
data = self.server_socket.recv(1024)
|
||||
|
||||
# No data to be read even as POLLIN triggered → connection has broken
|
||||
# Mo data to be read even as POLLIN triggered → connection has broken
|
||||
# Log it and try reconnecting
|
||||
if data == b'':
|
||||
self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, 'Empty read'))
|
||||
|
@ -311,7 +289,6 @@ 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):
|
||||
|
@ -350,16 +327,11 @@ class ServerThread(threading.Thread):
|
|||
|
||||
try:
|
||||
# Run initialization
|
||||
|
||||
# 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')))
|
||||
|
||||
# Set up nick
|
||||
self.api.nick(self.server.nick.encode('utf-8'))
|
||||
|
||||
# Run the on_connect hook, to allow further setup
|
||||
botcmd.on_connect(irc = self.api)
|
||||
|
||||
|
@ -379,7 +351,6 @@ 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
|
||||
|
@ -387,9 +358,7 @@ class ServerThread(threading.Thread):
|
|||
else:
|
||||
# Tell server we're reconnecting
|
||||
self.send_line_raw(b'QUIT :Reconnecting')
|
||||
with self.server_socket_write_lock:
|
||||
self.server_socket.close()
|
||||
self.server_socket = None
|
||||
self.server_socket.close()
|
||||
|
||||
except (BrokenPipeError, TimeoutError) as err:
|
||||
# Connection broke, log it and try to reconnect
|
||||
|
@ -402,20 +371,20 @@ class ServerThread(threading.Thread):
|
|||
# Tell cron we're quiting
|
||||
cron.quit(cron_control_channel)
|
||||
|
||||
# spawn_serverthread(server, auth, cron_control_channel, logging_channel) → control_channel
|
||||
# spawn_serverthread(server, cron_control_channel, logging_channel) → control_channel
|
||||
# Creates a ServerThread for given server and returns the channel for controlling it
|
||||
def spawn_serverthread(server, auth, cron_control_channel, logging_channel):
|
||||
def spawn_serverthread(server, cron_control_channel, logging_channel):
|
||||
thread_control_socket, spawner_control_socket = socket.socketpair()
|
||||
control_channel = channel.Channel()
|
||||
ServerThread(server, auth, control_channel, cron_control_channel, logging_channel).start()
|
||||
ServerThread(server, control_channel, cron_control_channel, logging_channel).start()
|
||||
return control_channel
|
||||
|
||||
# spawn_loggerthread(interactive_console) → logging_channel, dead_notify_channel
|
||||
# spawn_loggerthread() → 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(interactive_console):
|
||||
def spawn_loggerthread():
|
||||
logging_channel = channel.Channel()
|
||||
dead_notify_channel = channel.Channel()
|
||||
LoggerThread(interactive_console, logging_channel, dead_notify_channel).start()
|
||||
LoggerThread(logging_channel, dead_notify_channel).start()
|
||||
return logging_channel, dead_notify_channel
|
||||
|
||||
# read_config() → config, server
|
||||
|
@ -431,28 +400,20 @@ 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, (user, password)
|
||||
return config, server
|
||||
|
||||
if __name__ == '__main__':
|
||||
interactive_console = sys.stdin.isatty()
|
||||
|
||||
config, server, auth = read_config()
|
||||
config, server = read_config()
|
||||
|
||||
botcmd.initialize(config = config)
|
||||
|
||||
cron_control_channel = cron.start()
|
||||
logging_channel, dead_notify_channel = spawn_loggerthread(interactive_console)
|
||||
control_channel = spawn_serverthread(server, auth, cron_control_channel, logging_channel)
|
||||
logging_channel, dead_notify_channel = spawn_loggerthread()
|
||||
control_channel = spawn_serverthread(server, cron_control_channel, logging_channel)
|
||||
|
||||
while interactive_console:
|
||||
while True:
|
||||
message = dead_notify_channel.recv(blocking = False)
|
||||
if message is not None:
|
||||
if message[0] == controlmessage_types.quit:
|
||||
|
|
Loading…
Reference in New Issue