2017-09-04 19:49:01 +00:00
|
|
|
#!/usr/bin/env python3
|
2017-09-04 21:10:10 +00:00
|
|
|
import select
|
2017-09-04 19:49:01 +00:00
|
|
|
import socket
|
|
|
|
import threading
|
|
|
|
from collections import namedtuple
|
|
|
|
|
2017-09-05 07:55:33 +00:00
|
|
|
import channel
|
2017-09-05 08:24:58 +00:00
|
|
|
from constants import logmessage_types, internal_submessage_types, controlmessage_types
|
2017-09-05 07:55:33 +00:00
|
|
|
|
2017-09-05 09:47:47 +00:00
|
|
|
import line_handling
|
|
|
|
|
2017-09-04 19:49:01 +00:00
|
|
|
Server = namedtuple('Server', ['host', 'port'])
|
|
|
|
|
|
|
|
# ServerThread(server, control_socket)
|
|
|
|
# Creates a new server main loop thread
|
|
|
|
class ServerThread(threading.Thread):
|
2017-09-05 07:55:33 +00:00
|
|
|
def __init__(self, server, control_channel, logging_channel):
|
2017-09-04 19:49:01 +00:00
|
|
|
self.server = server
|
2017-09-05 07:55:33 +00:00
|
|
|
self.control_channel = control_channel
|
|
|
|
self.logging_channel = logging_channel
|
2017-09-04 19:49:01 +00:00
|
|
|
|
2017-09-05 07:05:02 +00:00
|
|
|
self.server_socket_write_lock = threading.Lock()
|
|
|
|
|
2017-09-04 19:49:01 +00:00
|
|
|
threading.Thread.__init__(self)
|
|
|
|
|
2017-09-04 21:10:10 +00:00
|
|
|
def send_line_raw(self, line):
|
|
|
|
# Sanitize line just in case
|
|
|
|
line = line.replace(b'\r', b'').replace(b'\n', b'')[:510]
|
|
|
|
|
2017-09-05 07:05:02 +00:00
|
|
|
with self.server_socket_write_lock:
|
|
|
|
self.server_socket.sendall(line + b'\r\n')
|
2017-09-04 21:10:10 +00:00
|
|
|
|
2017-09-05 09:47:47 +00:00
|
|
|
# Don't log PONGs
|
|
|
|
if not (len(line) >= 5 and line[:5] == b'PONG '):
|
|
|
|
self.logging_channel.send((logmessage_types.sent, line.decode(encoding = 'utf-8', errors = 'replace')))
|
2017-09-04 21:10:10 +00:00
|
|
|
|
|
|
|
def handle_line(self, line):
|
2017-09-05 08:40:44 +00:00
|
|
|
command, _, arguments = line.partition(b' ')
|
|
|
|
if command.upper() == b'PING':
|
|
|
|
self.send_line_raw(b'PONG ' + arguments)
|
|
|
|
else:
|
|
|
|
self.logging_channel.send((logmessage_types.received, line.decode(encoding = 'utf-8', errors = 'replace')))
|
2017-09-05 09:47:47 +00:00
|
|
|
line_handling.handle_line(line, irc = self.api)
|
2017-09-04 21:10:10 +00:00
|
|
|
|
|
|
|
def mainloop(self):
|
2017-09-05 07:55:33 +00:00
|
|
|
# Register both the server socket and the control channel to or polling object
|
2017-09-04 21:10:10 +00:00
|
|
|
poll = select.poll()
|
|
|
|
poll.register(self.server_socket, select.POLLIN)
|
2017-09-05 07:55:33 +00:00
|
|
|
poll.register(self.control_channel, select.POLLIN)
|
2017-09-04 21:10:10 +00:00
|
|
|
|
2017-09-05 07:55:33 +00:00
|
|
|
# Keep buffer for input
|
2017-09-04 21:10:10 +00:00
|
|
|
server_input_buffer = bytearray()
|
|
|
|
|
2017-09-05 08:40:44 +00:00
|
|
|
# TODO: Implement timeouting
|
2017-09-04 21:10:10 +00:00
|
|
|
quitting = False
|
|
|
|
while not quitting:
|
|
|
|
# Wait until we can do something
|
|
|
|
for fd, event in poll.poll():
|
|
|
|
# Server
|
|
|
|
if fd == self.server_socket.fileno():
|
|
|
|
# Ready to receive, read into buffer and handle full messages
|
|
|
|
if event | select.POLLIN:
|
|
|
|
data = self.server_socket.recv(1024)
|
|
|
|
server_input_buffer.extend(data)
|
|
|
|
|
|
|
|
# Try to see if we have a full line ending with \r\n in the buffer
|
|
|
|
# If yes, handle it
|
2017-09-05 07:58:58 +00:00
|
|
|
while b'\r\n' in server_input_buffer:
|
2017-09-04 21:10:10 +00:00
|
|
|
# Newline was found, split buffer
|
|
|
|
line, _, server_input_buffer = server_input_buffer.partition(b'\r\n')
|
|
|
|
|
|
|
|
self.handle_line(line)
|
|
|
|
|
|
|
|
# Control
|
2017-09-05 07:55:33 +00:00
|
|
|
elif fd == self.control_channel.fileno():
|
2017-09-05 08:24:58 +00:00
|
|
|
command_type, *arguments = self.control_channel.recv()
|
|
|
|
if command_type == controlmessage_types.quit:
|
2017-09-04 21:10:10 +00:00
|
|
|
quitting = True
|
|
|
|
|
2017-09-05 08:24:58 +00:00
|
|
|
elif command_type == controlmessage_types.send_line:
|
|
|
|
assert len(arguments) == 1
|
|
|
|
irc_command, space, arguments = arguments[0].encode('utf-8').partition(b' ')
|
2017-09-05 07:55:33 +00:00
|
|
|
line = irc_command.upper() + space + arguments
|
|
|
|
self.send_line_raw(line)
|
|
|
|
|
|
|
|
else:
|
2017-09-05 09:47:47 +00:00
|
|
|
error_message = 'Unknown control message: %s' % repr((command_type, *arguments))
|
|
|
|
self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, error_message))
|
2017-09-05 07:55:33 +00:00
|
|
|
|
2017-09-04 21:10:10 +00:00
|
|
|
else:
|
|
|
|
assert False #unreachable
|
|
|
|
|
2017-09-04 19:49:01 +00:00
|
|
|
def run(self):
|
|
|
|
# Connect to given server
|
|
|
|
address = (self.server.host, self.server.port)
|
2017-09-04 21:10:10 +00:00
|
|
|
try:
|
|
|
|
self.server_socket = socket.create_connection(address)
|
|
|
|
except ConnectionRefusedError:
|
|
|
|
# Tell controller we failed
|
2017-09-05 09:47:47 +00:00
|
|
|
self.logging_channel.send((logmessage_types.internal, internal_submessage_types.error, "Can't connect to %s:%s" % address))
|
2017-09-05 08:24:58 +00:00
|
|
|
self.logging_channel.send((logmessage_types.internal, internal_submessage_types.quit))
|
2017-09-05 07:55:33 +00:00
|
|
|
return
|
2017-09-04 21:10:10 +00:00
|
|
|
|
2017-09-05 09:47:47 +00:00
|
|
|
# Create an API object to give to outside line handler
|
|
|
|
self.api = line_handling.API(self)
|
|
|
|
|
2017-09-04 21:10:10 +00:00
|
|
|
# Run initialization
|
|
|
|
# TODO: read nick/username/etc. from a config
|
|
|
|
self.send_line_raw(b'NICK HynneFlip')
|
|
|
|
self.send_line_raw(b'USER HynneFlip a a :HynneFlip IRC bot')
|
|
|
|
|
|
|
|
# Run mainloop
|
|
|
|
self.mainloop()
|
|
|
|
|
|
|
|
# Tell the server we're quiting
|
|
|
|
self.send_line_raw(b'QUIT :HynneFlip exiting normally')
|
|
|
|
self.server_socket.close()
|
2017-09-04 19:49:01 +00:00
|
|
|
|
2017-09-04 21:10:10 +00:00
|
|
|
# Tell controller we're quiting
|
2017-09-05 08:24:58 +00:00
|
|
|
self.logging_channel.send((logmessage_types.internal, internal_submessage_types.quit))
|
2017-09-04 19:49:01 +00:00
|
|
|
|
2017-09-05 07:55:33 +00:00
|
|
|
# spawn_serverthread(server) → control_channel, logging_channel
|
|
|
|
# Creates a ServerThread for given server and returns the channels for controlling and monitoring it
|
2017-09-04 19:49:01 +00:00
|
|
|
def spawn_serverthread(server):
|
|
|
|
thread_control_socket, spawner_control_socket = socket.socketpair()
|
2017-09-05 07:55:33 +00:00
|
|
|
control_channel = channel.Channel()
|
|
|
|
logging_channel = channel.Channel()
|
|
|
|
ServerThread(server, control_channel, logging_channel).start()
|
|
|
|
return (control_channel, logging_channel)
|
2017-09-04 19:49:01 +00:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2017-09-05 07:55:33 +00:00
|
|
|
control_channel, logging_channel = spawn_serverthread(Server('irc.freenode.net', 6667))
|
2017-09-04 21:10:10 +00:00
|
|
|
|
|
|
|
while True:
|
|
|
|
cmd = input(': ')
|
|
|
|
if cmd == '':
|
2017-09-05 08:03:41 +00:00
|
|
|
while True:
|
|
|
|
data = logging_channel.recv(blocking = False)
|
|
|
|
if data == None:
|
|
|
|
break
|
2017-09-05 09:47:47 +00:00
|
|
|
message_type, *message_data = data
|
2017-09-05 08:24:58 +00:00
|
|
|
if message_type == logmessage_types.sent:
|
2017-09-05 09:47:47 +00:00
|
|
|
assert len(message_data) == 1
|
|
|
|
print('>' + message_data[0])
|
2017-09-05 08:24:58 +00:00
|
|
|
elif message_type == logmessage_types.received:
|
2017-09-05 09:47:47 +00:00
|
|
|
assert len(message_data) == 1
|
|
|
|
print('<' + message_data[0])
|
2017-09-05 08:24:58 +00:00
|
|
|
elif message_type == logmessage_types.internal:
|
2017-09-05 09:47:47 +00:00
|
|
|
if message_data[0] == internal_submessage_types.quit:
|
|
|
|
assert len(message_data) == 1
|
2017-09-05 08:24:58 +00:00
|
|
|
print('--- Quit')
|
2017-09-05 09:47:47 +00:00
|
|
|
elif message_data[0] == internal_submessage_types.error:
|
|
|
|
assert len(message_data) == 2
|
|
|
|
print('--- Error', message_data[1])
|
2017-09-05 08:24:58 +00:00
|
|
|
else:
|
|
|
|
print('--- ???', message_data)
|
|
|
|
else:
|
|
|
|
print('???', message_type, message_data)
|
2017-09-04 21:10:10 +00:00
|
|
|
|
|
|
|
elif cmd == 'q':
|
2017-09-05 08:24:58 +00:00
|
|
|
control_channel.send((controlmessage_types.quit,))
|
2017-09-04 21:10:10 +00:00
|
|
|
break
|
|
|
|
|
2017-09-05 08:24:58 +00:00
|
|
|
elif len(cmd) > 0 and cmd[0] == '/':
|
|
|
|
control_channel.send((controlmessage_types.send_line, cmd[1:]))
|