aboutsummaryrefslogtreecommitdiff
path: root/src/IRCServer.py
diff options
context:
space:
mode:
authorGravatar jesopo2018-09-24 15:13:27 +0100
committerGravatar jesopo2018-09-24 15:13:27 +0100
commitecb9d7cb3f4435457560e03201bbed57a469d548 (patch)
tree5a010f97c209558cdd2d40327d41e6806aedde94 /src/IRCServer.py
parentRemove empty spaces in coins.py (diff)
signature
Move most code in root directory to src/
Diffstat (limited to 'src/IRCServer.py')
-rw-r--r--src/IRCServer.py402
1 files changed, 402 insertions, 0 deletions
diff --git a/src/IRCServer.py b/src/IRCServer.py
new file mode 100644
index 00000000..d88cc38a
--- /dev/null
+++ b/src/IRCServer.py
@@ -0,0 +1,402 @@
+import collections, socket, ssl, sys, time
+from . import IRCChannel, IRCUser, Utils
+
+THROTTLE_LINES = 4
+THROTTLE_SECONDS = 1
+READ_TIMEOUT_SECONDS = 120
+PING_INTERVAL_SECONDS = 30
+
+class Server(object):
+ def __init__(self, bot, events, id, alias, hostname, port, password,
+ ipv4, tls, nickname, username, realname):
+ self.connected = False
+ self.bot = bot
+ self.events = events
+ self.id = id
+ self.alias = alias
+ self.target_hostname = hostname
+ self.port = port
+ self.tls = tls
+ self.password = password
+ self.ipv4 = ipv4
+ self.original_nickname = nickname
+ self.original_username = username or nickname
+ self.original_realname = realname or nickname
+ self.name = None
+
+ self._capability_queue = set([])
+ self._capabilities_waiting = set([])
+ self.capabilities = set([])
+ self.server_capabilities = {}
+ self.batches = {}
+
+ self.write_buffer = b""
+ self.buffered_lines = []
+ self.read_buffer = b""
+ self.recent_sends = []
+
+ self.users = {}
+ self.new_users = set([])
+ self.channels = {}
+
+ self.own_modes = {}
+ self.mode_prefixes = collections.OrderedDict(
+ {"@": "o", "+": "v"})
+ self.channel_modes = []
+ self.channel_types = ["#"]
+ self.case_mapping = "rfc1459"
+
+ self.last_read = time.monotonic()
+ self.last_send = None
+
+ self.attempted_join = {}
+ self.ping_sent = False
+
+ if ipv4:
+ self.socket = socket.socket(socket.AF_INET,
+ socket.SOCK_STREAM)
+ else:
+ self.socket = socket.socket(socket.AF_INET6,
+ socket.SOCK_STREAM)
+
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+ self.socket.settimeout(5.0)
+
+ if self.tls:
+ self.tls_wrap()
+ self.cached_fileno = self.socket.fileno()
+ self.events.on("timer.rejoin").hook(self.try_rejoin)
+
+ def __repr__(self):
+ return "IRCServer.Server(%s)" % self.__str__()
+ def __str__(self):
+ if self.alias:
+ return self.alias
+ return "%s:%s%s" % (self.target_hostname, "+" if self.tls else "",
+ self.port)
+ def fileno(self):
+ fileno = self.socket.fileno()
+ return self.cached_fileno if fileno == -1 else fileno
+
+ def tls_wrap(self):
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS)
+ context.options |= ssl.OP_NO_SSLv2
+ context.options |= ssl.OP_NO_SSLv3
+ context.options |= ssl.OP_NO_TLSv1
+
+ context.load_default_certs()
+ if self.get_setting("ssl-verify", True):
+ context.verify_mode = ssl.CERT_REQUIRED
+
+ client_certificate = self.bot.config.get("ssl-certificate", None)
+ client_key = self.bot.config.get("ssl-key", None)
+ if client_certificate and client_key:
+ context.load_cert_chain(client_certificate, keyfile=client_key)
+
+ self.socket = context.wrap_socket(self.socket)
+
+ def connect(self):
+ self.socket.connect((self.target_hostname, self.port))
+ self.send_capibility_ls()
+
+ if self.password:
+ self.send_pass(self.password)
+
+ self.send_user(self.original_username, self.original_realname)
+ self.send_nick(self.original_nickname)
+ self.connected = True
+ def disconnect(self):
+ self.connected = False
+ try:
+ self.socket.shutdown(socket.SHUT_RDWR)
+ except:
+ pass
+ try:
+ self.socket.close()
+ except:
+ pass
+
+ def set_setting(self, setting, value):
+ self.bot.database.server_settings.set(self.id, setting,
+ value)
+ def get_setting(self, setting, default=None):
+ return self.bot.database.server_settings.get(self.id,
+ setting, default)
+ def find_settings(self, pattern, default=[]):
+ return self.bot.database.server_settings.find(self.id,
+ pattern, default)
+ def find_settings_prefix(self, prefix, default=[]):
+ return self.bot.database.server_settings.find_prefix(
+ self.id, prefix, default)
+ def del_setting(self, setting):
+ self.bot.database.server_settings.delete(self.id, setting)
+ def get_all_user_settings(self, setting, default=[]):
+ return self.bot.database.user_settings.find_all_by_setting(
+ self.id, setting, default)
+ def find_all_user_channel_settings(self, setting, default=[]):
+ return self.bot.database.user_channel_settings.find_all_by_setting(
+ self.id, setting, default)
+
+ def set_own_nickname(self, nickname):
+ self.nickname = nickname
+ self.nickname_lower = Utils.irc_lower(self, nickname)
+ def is_own_nickname(self, nickname):
+ return Utils.irc_equals(self, nickname, self.nickname)
+
+ def add_own_mode(self, mode, arg=None):
+ self.own_modes[mode] = arg
+ def remove_own_mode(self, mode):
+ del self.own_modes[mode]
+ def change_own_mode(self, remove, mode, arg=None):
+ if remove:
+ self.remove_own_mode(mode)
+ else:
+ self.add_own_mode(mode, arg)
+
+ def has_user(self, nickname):
+ return Utils.irc_lower(self, nickname) in self.users
+ def get_user(self, nickname):
+ if not self.has_user(nickname):
+ user_id = self.get_user_id(nickname)
+ new_user = IRCUser.User(nickname, user_id, self, self.bot)
+ self.events.on("new.user").call(user=new_user, server=self)
+ self.users[new_user.nickname_lower] = new_user
+ self.new_users.add(new_user)
+ return self.users[Utils.irc_lower(self, nickname)]
+ def get_user_id(self, nickname):
+ self.bot.database.users.add(self.id, nickname)
+ return self.bot.database.users.get_id(self.id, nickname)
+ def remove_user(self, user):
+ del self.users[user.nickname_lower]
+ for channel in user.channels:
+ channel.remove_user(user)
+
+ def change_user_nickname(self, old_nickname, new_nickname):
+ user = self.users.pop(Utils.irc_lower(self, old_nickname))
+ user._id = self.get_user_id(new_nickname)
+ self.users[Utils.irc_lower(self, new_nickname)] = user
+ def has_channel(self, channel_name):
+ return channel_name[0] in self.channel_types and Utils.irc_lower(
+ self, channel_name) in self.channels
+ def get_channel(self, channel_name):
+ if not self.has_channel(channel_name):
+ channel_id = self.get_channel_id(channel_name)
+ new_channel = IRCChannel.Channel(channel_name, channel_id,
+ self, self.bot)
+ self.events.on("new.channel").call(channel=new_channel,
+ server=self)
+ self.channels[new_channel.name] = new_channel
+ return self.channels[Utils.irc_lower(self, channel_name)]
+ def get_channel_id(self, channel_name):
+ self.bot.database.channels.add(self.id, channel_name)
+ return self.bot.database.channels.get_id(self.id, channel_name)
+ def remove_channel(self, channel):
+ for user in channel.users:
+ user.part_channel(channel)
+ del self.channels[channel.name]
+ def parse_line(self, line):
+ if not line:
+ return
+ self.bot.line_handler.handle(self, line)
+ self.check_users()
+ def check_users(self):
+ for user in self.new_users:
+ if not len(user.channels):
+ self.remove_user(user)
+ self.new_users.clear()
+ def read(self):
+ data = b""
+ try:
+ data = self.read_buffer + self.socket.recv(4096)
+ except (ConnectionResetError, socket.timeout):
+ self.disconnect()
+ return []
+ self.read_buffer = b""
+ data_lines = [line.strip(b"\r") for line in data.split(b"\n")]
+ if data_lines[-1]:
+ self.read_buffer = data_lines[-1]
+ data_lines.pop(-1)
+ decoded_lines = []
+ for line in data_lines:
+ try:
+ line = line.decode(self.get_setting(
+ "encoding", "utf8"))
+ except:
+ try:
+ line = line.decode(self.get_setting(
+ "fallback-encoding", "latin-1"))
+ except:
+ continue
+ decoded_lines.append(line)
+ if not decoded_lines:
+ self.disconnect()
+ self.last_read = time.monotonic()
+ self.ping_sent = False
+ return decoded_lines
+
+ def until_next_ping(self):
+ if self.ping_sent:
+ return None
+ return max(0, (self.last_read+PING_INTERVAL_SECONDS
+ )-time.monotonic())
+ def ping_due(self):
+ return self.until_next_ping() == 0
+
+ def until_read_timeout(self):
+ return max(0, (self.last_read+READ_TIMEOUT_SECONDS
+ )-time.monotonic())
+ def read_timed_out(self):
+ return self.until_read_timeout == 0
+
+ def send(self, data):
+ encoded = data.split("\n")[0].strip("\r").encode("utf8")
+ if len(encoded) > 450:
+ encoded = encoded[:450]
+ self.buffered_lines.append(encoded + b"\r\n")
+ if self.bot.args.verbose:
+ self.bot.log.info(">%s | %s", [str(self), encoded.decode("utf8")])
+ def _send(self):
+ if not len(self.write_buffer):
+ self.write_buffer = self.buffered_lines.pop(0)
+ self.write_buffer = self.write_buffer[self.socket.send(
+ self.write_buffer):]
+
+ now = time.monotonic()
+ self.recent_sends.append(now)
+ self.last_send = now
+ def waiting_send(self):
+ return bool(len(self.write_buffer)) or bool(len(self.buffered_lines))
+ def throttle_done(self):
+ return self.send_throttle_timeout() == 0
+ def send_throttle_timeout(self):
+ if len(self.write_buffer):
+ return 0
+
+ now = time.monotonic()
+ popped = 0
+ for i, recent_send in enumerate(self.recent_sends[:]):
+ time_since = now-recent_send
+ if time_since >= THROTTLE_SECONDS:
+ self.recent_sends.pop(i-popped)
+ popped += 1
+
+ if len(self.recent_sends) < THROTTLE_LINES:
+ return 0
+
+ time_left = self.recent_sends[0]+THROTTLE_SECONDS
+ time_left = time_left-now
+ return time_left
+
+ def send_user(self, username, realname):
+ self.send("USER %s 0 * :%s" % (username, realname))
+ def send_nick(self, nickname):
+ self.send("NICK %s" % nickname)
+
+ def send_capibility_ls(self):
+ self.send("CAP LS 302")
+ def queue_capability(self, capability):
+ self._capability_queue.add(capability)
+ def queue_capabilities(self, capabilities):
+ self._capability_queue.update(capabilities)
+ def send_capability_queue(self):
+ if self.has_capability_queue():
+ capabilities = " ".join(self._capability_queue)
+ self._capability_queue.clear()
+ self.send_capability_request(capabilities)
+ def has_capability_queue(self):
+ return bool(len(self._capability_queue))
+ def send_capability_request(self, capability):
+ self.send("CAP REQ :%s" % capability)
+ def send_capability_end(self):
+ self.send("CAP END")
+ def send_authenticate(self, text):
+ self.send("AUTHENTICATE %s" % text)
+ def send_starttls(self):
+ self.send("STARTTLS")
+
+ def waiting_for_capabilities(self):
+ return bool(len(self._capabilities_waiting))
+ def wait_for_capability(self, capability):
+ self._capabilities_waiting.add(capability)
+ def capability_done(self, capability):
+ self._capabilities_waiting.remove(capability)
+ if not self._capabilities_waiting:
+ self.send_capability_end()
+
+ def send_pass(self, password):
+ self.send("PASS %s" % password)
+
+ def send_ping(self, nonce="hello"):
+ self.send("PING :%s" % nonce)
+ def send_pong(self, nonce="hello"):
+ self.send("PONG :%s" % nonce)
+
+ def try_rejoin(self, event):
+ if event["server_id"] == self.id and event["channel_name"
+ ] in self.attempted_join:
+ self.send_join(event["channel_name"], event["key"])
+ def send_join(self, channel_name, key=None):
+ self.send("JOIN %s%s" % (channel_name,
+ "" if key == None else " %s" % key))
+ def send_part(self, channel_name, reason=None):
+ self.send("PART %s%s" % (channel_name,
+ "" if reason == None else " %s" % reason))
+ def send_quit(self, reason="Leaving"):
+ self.send("QUIT :%s" % reason)
+
+ def send_message(self, target, message, prefix=None):
+ full_message = message if not prefix else prefix+message
+ self.send("PRIVMSG %s :%s" % (target, full_message))
+
+ action = full_message.startswith("\01ACTION "
+ ) and full_message.endswith("\01")
+
+ if action:
+ message = full_message.split("\01ACTION ", 1)[1][:-1]
+
+ full_message_split = full_message.split()
+ if self.has_channel(target):
+ channel = self.get_channel(target)
+ channel.buffer.add_line(None, message, action, True)
+ self.events.on("self.message.channel").call(
+ message=full_message, message_split=full_message_split,
+ channel=channel, action=action, server=self)
+ else:
+ user = self.get_user(target)
+ user.buffer.add_line(None, message, action, True)
+ self.events.on("self.message.private").call(
+ message=full_message, message_split=full_message_split,
+ user=user, action=action, server=self)
+
+ def send_notice(self, target, message):
+ self.send("NOTICE %s :%s" % (target, message))
+
+ def send_mode(self, target, mode=None, args=None):
+ self.send("MODE %s%s%s" % (target, "" if mode == None else " %s" % mode,
+ "" if args == None else " %s" % args))
+
+ def send_topic(self, channel_name, topic):
+ self.send("TOPIC %s :%s" % (channel_name, topic))
+ def send_kick(self, channel_name, target, reason=None):
+ self.send("KICK %s %s%s" % (channel_name, target,
+ "" if reason == None else " :%s" % reason))
+ def send_names(self, channel_name):
+ self.send("NAMES %s" % channel_name)
+ def send_list(self, search_for=None):
+ self.send(
+ "LIST%s" % "" if search_for == None else " %s" % search_for)
+ def send_invite(self, target, channel_name):
+ self.send("INVITE %s %s" % (target, channel_name))
+
+ def send_whois(self, target):
+ self.send("WHOIS %s" % target)
+ def send_whowas(self, target, amount=None, server=None):
+ self.send("WHOWAS %s%s%s" % (target,
+ "" if amount == None else " %s" % amount,
+ "" if server == None else " :%s" % server))
+ def send_who(self, filter=None):
+ self.send("WHO%s" % ("" if filter == None else " %s" % filter))
+ def send_whox(self, mask, filter, fields, label=None):
+ self.send("WHO %s %s%%%s%s" % (mask, filter, fields,
+ ","+label if label else ""))