2018-12-02 09:43:57 +00:00
|
|
|
import queue, os, select, socket, threading, time, traceback, typing, uuid
|
2018-10-07 06:54:10 +00:00
|
|
|
from src import EventManager, Exports, IRCServer, Logging, ModuleManager
|
|
|
|
from src import Socket, utils
|
2018-09-28 15:51:36 +00:00
|
|
|
|
2018-11-27 15:06:10 +00:00
|
|
|
TRIGGER_RETURN = 1
|
|
|
|
TRIGGER_EXCEPTION = 2
|
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
class Bot(object):
|
2018-09-29 11:53:39 +00:00
|
|
|
def __init__(self, directory, args, cache, config, database, events,
|
2018-09-30 18:43:20 +00:00
|
|
|
exports, log, modules, timers):
|
2018-09-29 08:24:26 +00:00
|
|
|
self.directory = directory
|
2018-09-28 15:51:36 +00:00
|
|
|
self.args = args
|
2018-09-29 11:53:39 +00:00
|
|
|
self.cache = cache
|
2018-09-28 15:51:36 +00:00
|
|
|
self.config = config
|
|
|
|
self.database = database
|
|
|
|
self._events = events
|
|
|
|
self._exports = exports
|
|
|
|
self.log = log
|
|
|
|
self.modules = modules
|
2018-10-12 17:07:23 +00:00
|
|
|
self._timers = timers
|
2018-09-28 15:51:36 +00:00
|
|
|
|
2018-09-11 17:24:34 +00:00
|
|
|
self.start_time = time.time()
|
2016-03-29 11:56:58 +00:00
|
|
|
self.lock = threading.Lock()
|
|
|
|
self.running = True
|
|
|
|
self.poll = select.epoll()
|
|
|
|
|
2018-10-06 14:37:05 +00:00
|
|
|
self.servers = {}
|
|
|
|
self.other_sockets = {}
|
2018-10-07 06:54:10 +00:00
|
|
|
self._trigger_server, self._trigger_client = socket.socketpair()
|
2018-10-07 06:56:11 +00:00
|
|
|
self.add_socket(Socket.Socket(self._trigger_server, lambda _, s: None))
|
2018-10-06 14:45:56 +00:00
|
|
|
|
2018-10-07 07:01:54 +00:00
|
|
|
self._trigger_functions = []
|
2018-11-05 12:34:18 +00:00
|
|
|
self._events.on("timer.reconnect").hook(self._timed_reconnect)
|
2018-10-07 07:01:54 +00:00
|
|
|
|
2018-11-27 14:25:12 +00:00
|
|
|
def trigger(self,
|
2018-11-27 17:29:38 +00:00
|
|
|
func: typing.Optional[typing.Callable[[], typing.Any]]=None
|
|
|
|
) -> typing.Any:
|
2018-11-27 14:25:12 +00:00
|
|
|
func = func or (lambda: None)
|
|
|
|
if threading.current_thread() is threading.main_thread():
|
|
|
|
returned = func()
|
|
|
|
self._trigger_client.send(b"TRIGGER")
|
|
|
|
return returned
|
|
|
|
|
2018-10-07 07:06:41 +00:00
|
|
|
self.lock.acquire()
|
2018-11-27 14:25:12 +00:00
|
|
|
|
2018-12-02 10:08:58 +00:00
|
|
|
func_queue = queue.Queue(1) # type: queue.Queue[str]
|
2018-11-27 14:25:12 +00:00
|
|
|
self._trigger_functions.append([func, func_queue])
|
|
|
|
|
2018-10-07 07:06:41 +00:00
|
|
|
self.lock.release()
|
2018-11-27 14:25:12 +00:00
|
|
|
self._trigger_client.send(b"TRIGGER")
|
|
|
|
|
2018-12-10 13:34:53 +00:00
|
|
|
type, returned = func_queue.get(block=True)
|
2018-11-27 15:06:10 +00:00
|
|
|
if type == TRIGGER_EXCEPTION:
|
|
|
|
raise returned
|
|
|
|
elif type == TRIGGER_RETURN:
|
|
|
|
return returned
|
2018-10-06 14:45:56 +00:00
|
|
|
|
2018-11-05 18:23:02 +00:00
|
|
|
def add_server(self, server_id: int, connect: bool = True,
|
|
|
|
connection_params: typing.Optional[
|
|
|
|
utils.irc.IRCConnectionParameters]=None) -> IRCServer.Server:
|
|
|
|
if not connection_params:
|
|
|
|
connection_params = utils.irc.IRCConnectionParameters(
|
|
|
|
*self.database.servers.get(server_id))
|
|
|
|
|
|
|
|
new_server = IRCServer.Server(self, self._events,
|
2018-11-05 18:30:14 +00:00
|
|
|
connection_params.id, connection_params.alias, connection_params)
|
2018-09-19 12:25:12 +00:00
|
|
|
self._events.on("new.server").call(server=new_server)
|
2018-11-05 11:53:33 +00:00
|
|
|
|
2018-11-24 12:14:36 +00:00
|
|
|
if not connect:
|
2018-11-05 11:53:33 +00:00
|
|
|
return new_server
|
|
|
|
|
|
|
|
self.connect(new_server)
|
|
|
|
|
2016-07-05 11:18:13 +00:00
|
|
|
return new_server
|
2018-09-30 12:28:26 +00:00
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def add_socket(self, sock: socket.socket):
|
2018-10-06 14:37:05 +00:00
|
|
|
self.other_sockets[sock.fileno()] = sock
|
|
|
|
self.poll.register(sock.fileno(), select.EPOLLIN)
|
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def remove_socket(self, sock: socket.socket):
|
2018-10-06 14:37:05 +00:00
|
|
|
del self.other_sockets[sock.fileno()]
|
|
|
|
self.poll.unregister(sock.fileno())
|
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def get_server(self, id: int) -> typing.Optional[IRCServer.Server]:
|
2018-09-30 12:28:26 +00:00
|
|
|
for server in self.servers.values():
|
|
|
|
if server.id == id:
|
|
|
|
return server
|
2018-10-31 15:12:46 +00:00
|
|
|
return None
|
2018-09-30 12:28:26 +00:00
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def connect(self, server: IRCServer.Server) -> bool:
|
2016-03-29 11:56:58 +00:00
|
|
|
try:
|
|
|
|
server.connect()
|
|
|
|
except:
|
2018-12-02 09:43:57 +00:00
|
|
|
self.log.warn("Failed to connect to %s", [str(server)],
|
|
|
|
exc_info=True)
|
2016-03-29 11:56:58 +00:00
|
|
|
return False
|
2018-07-02 11:08:26 +00:00
|
|
|
self.servers[server.fileno()] = server
|
2016-03-30 18:32:14 +00:00
|
|
|
self.poll.register(server.fileno(), select.EPOLLOUT)
|
2016-03-29 11:56:58 +00:00
|
|
|
return True
|
2017-01-27 21:41:47 +00:00
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def next_send(self) -> typing.Optional[float]:
|
2018-08-28 13:36:16 +00:00
|
|
|
next = None
|
|
|
|
for server in self.servers.values():
|
2018-08-28 14:32:50 +00:00
|
|
|
timeout = server.send_throttle_timeout()
|
2018-08-29 13:33:27 +00:00
|
|
|
if server.waiting_send() and (next == None or timeout < next):
|
2018-08-28 13:36:16 +00:00
|
|
|
next = timeout
|
|
|
|
return next
|
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def next_ping(self) -> typing.Optional[float]:
|
2018-08-29 13:33:27 +00:00
|
|
|
timeouts = []
|
|
|
|
for server in self.servers.values():
|
2018-09-11 17:25:27 +00:00
|
|
|
timeout = server.until_next_ping()
|
|
|
|
if not timeout == None:
|
|
|
|
timeouts.append(timeout)
|
2018-08-30 16:20:55 +00:00
|
|
|
if not timeouts:
|
|
|
|
return None
|
2018-08-29 13:33:27 +00:00
|
|
|
return min(timeouts)
|
2018-10-30 14:58:48 +00:00
|
|
|
|
|
|
|
def next_read_timeout(self) -> typing.Optional[float]:
|
2018-08-29 13:33:27 +00:00
|
|
|
timeouts = []
|
|
|
|
for server in self.servers.values():
|
|
|
|
timeouts.append(server.until_read_timeout())
|
2018-08-30 16:20:55 +00:00
|
|
|
if not timeouts:
|
|
|
|
return None
|
2018-08-29 13:33:27 +00:00
|
|
|
return min(timeouts)
|
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def get_poll_timeout(self) -> float:
|
2018-08-29 13:33:27 +00:00
|
|
|
timeouts = []
|
2018-10-12 17:07:23 +00:00
|
|
|
timeouts.append(self._timers.next())
|
2018-08-29 13:33:27 +00:00
|
|
|
timeouts.append(self.next_send())
|
|
|
|
timeouts.append(self.next_ping())
|
|
|
|
timeouts.append(self.next_read_timeout())
|
2018-09-29 11:53:39 +00:00
|
|
|
timeouts.append(self.cache.next_expiration())
|
2018-08-29 13:33:27 +00:00
|
|
|
return min([timeout for timeout in timeouts if not timeout == None])
|
2016-03-29 11:56:58 +00:00
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def register_read(self, server: IRCServer.Server):
|
2016-03-29 11:56:58 +00:00
|
|
|
self.poll.modify(server.fileno(), select.EPOLLIN)
|
2018-10-30 14:58:48 +00:00
|
|
|
def register_write(self, server: IRCServer.Server):
|
2016-03-29 11:56:58 +00:00
|
|
|
self.poll.modify(server.fileno(), select.EPOLLOUT)
|
2018-10-30 14:58:48 +00:00
|
|
|
def register_both(self, server: IRCServer.Server):
|
2016-03-29 11:56:58 +00:00
|
|
|
self.poll.modify(server.fileno(),
|
|
|
|
select.EPOLLIN|select.EPOLLOUT)
|
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def disconnect(self, server: IRCServer.Server):
|
2016-07-14 08:17:41 +00:00
|
|
|
try:
|
|
|
|
self.poll.unregister(server.fileno())
|
|
|
|
except FileNotFoundError:
|
|
|
|
pass
|
2016-03-30 18:32:14 +00:00
|
|
|
del self.servers[server.fileno()]
|
|
|
|
|
2018-11-05 12:27:11 +00:00
|
|
|
def _timed_reconnect(self, event: EventManager.Event):
|
2018-11-14 13:08:57 +00:00
|
|
|
if not self.reconnect(event["server_id"],
|
|
|
|
event.get("connection_params", None)):
|
2018-11-05 12:27:11 +00:00
|
|
|
event["timer"].redo()
|
2018-11-05 18:23:02 +00:00
|
|
|
def reconnect(self, server_id: int, connection_params: typing.Optional[
|
|
|
|
utils.irc.IRCConnectionParameters]=None) -> bool:
|
|
|
|
server = self.add_server(server_id, False, connection_params)
|
2016-03-30 18:32:14 +00:00
|
|
|
if self.connect(server):
|
|
|
|
self.servers[server.fileno()] = server
|
2018-11-05 12:27:11 +00:00
|
|
|
return True
|
|
|
|
return False
|
2016-04-14 15:48:17 +00:00
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def set_setting(self, setting: str, value: typing.Any):
|
2018-08-05 21:41:38 +00:00
|
|
|
self.database.bot_settings.set(setting, value)
|
2018-10-30 14:58:48 +00:00
|
|
|
def get_setting(self, setting: str, default: typing.Any=None) -> typing.Any:
|
2018-08-05 21:41:38 +00:00
|
|
|
return self.database.bot_settings.get(setting, default)
|
2018-10-30 14:58:48 +00:00
|
|
|
def find_settings(self, pattern: str, default: typing.Any=[]
|
|
|
|
) -> typing.List[typing.Any]:
|
2018-08-05 21:41:38 +00:00
|
|
|
return self.database.bot_settings.find(pattern, default)
|
2018-10-30 14:58:48 +00:00
|
|
|
def find_settings_prefix(self, prefix: str, default: typing.Any=[]
|
|
|
|
) -> typing.List[typing.Any]:
|
2018-08-05 21:41:38 +00:00
|
|
|
return self.database.bot_settings.find_prefix(
|
2018-08-03 12:43:45 +00:00
|
|
|
prefix, default)
|
2018-10-30 14:58:48 +00:00
|
|
|
def del_setting(self, setting: str):
|
2018-08-06 13:10:14 +00:00
|
|
|
self.database.bot_settings.delete(setting)
|
2016-04-14 15:48:17 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
def run(self):
|
|
|
|
while self.running:
|
2018-11-27 11:56:03 +00:00
|
|
|
if not self.servers:
|
|
|
|
break
|
|
|
|
|
2018-08-28 13:36:16 +00:00
|
|
|
events = self.poll.poll(self.get_poll_timeout())
|
2018-10-04 13:44:50 +00:00
|
|
|
self.lock.acquire()
|
2018-10-12 17:07:23 +00:00
|
|
|
self._timers.call()
|
2018-09-29 11:53:39 +00:00
|
|
|
self.cache.expire()
|
2018-09-28 15:51:36 +00:00
|
|
|
|
2018-11-27 14:25:12 +00:00
|
|
|
for func, func_queue in self._trigger_functions:
|
2018-11-27 15:06:10 +00:00
|
|
|
try:
|
|
|
|
returned = func()
|
|
|
|
type = TRIGGER_RETURN
|
|
|
|
except Exception as e:
|
|
|
|
returned = e
|
|
|
|
type = TRIGGER_EXCEPTION
|
|
|
|
func_queue.put([type, returned])
|
2018-10-07 07:06:41 +00:00
|
|
|
self._trigger_functions.clear()
|
2018-10-07 07:01:54 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
for fd, event in events:
|
2018-10-06 14:37:05 +00:00
|
|
|
sock = None
|
|
|
|
irc = False
|
2016-03-29 11:56:58 +00:00
|
|
|
if fd in self.servers:
|
2018-10-06 14:37:05 +00:00
|
|
|
sock = self.servers[fd]
|
|
|
|
irc = True
|
|
|
|
elif fd in self.other_sockets:
|
|
|
|
sock = self.other_sockets[fd]
|
|
|
|
|
|
|
|
if sock:
|
2016-03-29 11:56:58 +00:00
|
|
|
if event & select.EPOLLIN:
|
2018-10-06 14:37:05 +00:00
|
|
|
data = sock.read()
|
|
|
|
if data == None:
|
|
|
|
sock.disconnect()
|
2018-10-08 22:03:49 +00:00
|
|
|
continue
|
|
|
|
|
2018-10-06 14:37:05 +00:00
|
|
|
for piece in data:
|
|
|
|
sock.parse_data(piece)
|
2016-03-29 11:56:58 +00:00
|
|
|
elif event & select.EPOLLOUT:
|
2018-10-06 14:37:05 +00:00
|
|
|
sock._send()
|
2018-11-27 11:56:03 +00:00
|
|
|
if sock.fileno() in self.servers:
|
|
|
|
self.register_read(sock)
|
2016-03-29 11:56:58 +00:00
|
|
|
elif event & select.EPULLHUP:
|
2018-12-02 09:43:57 +00:00
|
|
|
self.log.warn("Recieved EPOLLHUP for %s", [str(sock)])
|
2018-10-06 14:37:05 +00:00
|
|
|
sock.disconnect()
|
2016-04-10 16:29:03 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
for server in list(self.servers.values()):
|
2018-08-29 13:33:27 +00:00
|
|
|
if server.read_timed_out():
|
2018-12-02 09:43:57 +00:00
|
|
|
self.log.warn("Pinged out from %s", [str(server)])
|
2018-08-29 13:33:27 +00:00
|
|
|
server.disconnect()
|
|
|
|
elif server.ping_due() and not server.ping_sent:
|
2018-09-11 17:25:01 +00:00
|
|
|
server.send_ping()
|
|
|
|
server.ping_sent = True
|
2016-03-29 11:56:58 +00:00
|
|
|
if not server.connected:
|
2018-11-05 14:12:21 +00:00
|
|
|
self._events.on("server.disconnect").call(server=server)
|
2016-03-30 18:32:14 +00:00
|
|
|
self.disconnect(server)
|
|
|
|
|
2018-11-05 20:33:45 +00:00
|
|
|
if not self.get_server(server.id):
|
|
|
|
reconnect_delay = self.config.get("reconnect-delay", 10)
|
|
|
|
self._timers.add("reconnect", reconnect_delay,
|
2018-11-05 20:51:51 +00:00
|
|
|
server_id=server.id)
|
2018-12-02 09:43:57 +00:00
|
|
|
self.log.warn(
|
2018-11-05 20:33:45 +00:00
|
|
|
"Disconnected from %s, reconnecting in %d seconds",
|
|
|
|
[str(server), reconnect_delay])
|
2018-08-28 14:32:50 +00:00
|
|
|
elif server.waiting_send() and server.throttle_done():
|
2016-03-29 11:56:58 +00:00
|
|
|
self.register_both(server)
|
2018-10-06 14:37:05 +00:00
|
|
|
|
|
|
|
for sock in list(self.other_sockets.values()):
|
|
|
|
if not sock.connected:
|
|
|
|
self.remove_socket(sock)
|
|
|
|
elif sock.waiting_send():
|
|
|
|
self.register_both(sock)
|
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
self.lock.release()
|