wip caps are broken??
This commit is contained in:
parent
8749c9586a
commit
f99bcf83d9
3 changed files with 141 additions and 644 deletions
240
irc.py
240
irc.py
|
@ -20,7 +20,7 @@ from generic import PesterList
|
|||
from version import _pcVersion
|
||||
|
||||
from oyoyo import services
|
||||
from oyoyo.parse import parse_raw_irc_command
|
||||
from oyoyo.ircevents import numeric_events
|
||||
|
||||
import scripts.irc.outgoing
|
||||
|
||||
|
@ -99,7 +99,6 @@ class PesterIRC(QtCore.QThread):
|
|||
self.host = self.config.server()
|
||||
self.port = self.config.port()
|
||||
self.ssl = self.config.ssl()
|
||||
self.nick = self.mainwindow.profile().handle
|
||||
self.timeout = 120
|
||||
self.blocking = True
|
||||
self._end = False
|
||||
|
@ -107,7 +106,7 @@ class PesterIRC(QtCore.QThread):
|
|||
self.command_handler = self
|
||||
self.parent = self
|
||||
|
||||
self.send_irc = scripts.irc.outgoing.send_irc()
|
||||
self.send_irc = scripts.irc.outgoing.SendIRC()
|
||||
|
||||
def get_ssl_context(self):
|
||||
"""Returns an SSL context for connecting over SSL/TLS.
|
||||
|
@ -176,99 +175,42 @@ class PesterIRC(QtCore.QThread):
|
|||
elif self.blocking:
|
||||
self.socket.setblocking(True)
|
||||
|
||||
self.send_irc.nick(self.nick)
|
||||
self.send_irc.nick(self.mainwindow.profile().handle)
|
||||
self.send_irc.user("pcc31", "pcc31")
|
||||
# if self.connect_cb:
|
||||
# self.connect_cb(self)
|
||||
|
||||
def conn(self):
|
||||
def conn_generator(self):
|
||||
"""returns a generator object."""
|
||||
try:
|
||||
buffer = b""
|
||||
while not self._end:
|
||||
# Block for connection-killing exceptions
|
||||
try:
|
||||
tries = 1
|
||||
while tries < 10:
|
||||
# Check if alive
|
||||
if self._end == True:
|
||||
break
|
||||
if self.socket.fileno() == -1:
|
||||
self._end = True
|
||||
break
|
||||
try:
|
||||
ready_to_read, ready_to_write, in_error = select.select(
|
||||
[self.socket], [], []
|
||||
)
|
||||
for x in ready_to_read:
|
||||
buffer += x.recv(1024)
|
||||
break
|
||||
except ssl.SSLWantReadError as e:
|
||||
PchumLog.warning("ssl.SSLWantReadError on send, " + str(e))
|
||||
select.select([self.socket], [], [])
|
||||
if tries >= 9:
|
||||
raise e
|
||||
except ssl.SSLWantWriteError as e:
|
||||
PchumLog.warning("ssl.SSLWantWriteError on send, " + str(e))
|
||||
select.select([], [self.socket], [])
|
||||
if tries >= 9:
|
||||
raise e
|
||||
except ssl.SSLEOFError as e:
|
||||
# ssl.SSLEOFError guarantees a broken connection.
|
||||
PchumLog.warning("ssl.SSLEOFError in on send, " + str(e))
|
||||
raise e
|
||||
except (socket.timeout, TimeoutError) as e:
|
||||
# socket.timeout is deprecated in 3.10
|
||||
PchumLog.warning("TimeoutError in on send, " + str(e))
|
||||
raise socket.timeout
|
||||
except (OSError, IndexError, ValueError, Exception) as e:
|
||||
PchumLog.debug("Miscellaneous exception in conn, " + str(e))
|
||||
if tries >= 9:
|
||||
raise e
|
||||
tries += 1
|
||||
PchumLog.debug(
|
||||
"Possibly retrying recv. (attempt %s)" % str(tries)
|
||||
)
|
||||
time.sleep(0.1)
|
||||
|
||||
except socket.timeout as e:
|
||||
PchumLog.warning("timeout in client.py, " + str(e))
|
||||
if self._end:
|
||||
break
|
||||
raise e
|
||||
except ssl.SSLEOFError as e:
|
||||
raise e
|
||||
buffer += self.socket.recv(1024)
|
||||
except OSError as e:
|
||||
PchumLog.warning("conn exception {} in {}".format(e, self))
|
||||
if self._end:
|
||||
break
|
||||
if not self.blocking and e.errno == 11:
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
raise e
|
||||
else:
|
||||
if self._end:
|
||||
break
|
||||
if len(buffer) == 0 and self.blocking:
|
||||
if not buffer and self.blocking:
|
||||
PchumLog.debug("len(buffer) = 0")
|
||||
raise OSError("Connection closed")
|
||||
|
||||
data = buffer.split(bytes("\n", "UTF-8"))
|
||||
buffer = data.pop()
|
||||
|
||||
PchumLog.debug("data = " + str(data))
|
||||
|
||||
for el in data:
|
||||
tags, prefix, command, args = parse_raw_irc_command(el)
|
||||
data, buffer = self.parse_buffer(buffer)
|
||||
for line in data:
|
||||
tags, prefix, command, args = self.parse_irc_line(line)
|
||||
# print(tags, prefix, command, args)
|
||||
try:
|
||||
# Only need tags with tagmsg
|
||||
if command.upper() == "TAGMSG":
|
||||
if command.casefold() == "tagmsg":
|
||||
self.run_command(command, prefix, tags, *args)
|
||||
else:
|
||||
self.run_command(command, prefix, *args)
|
||||
except CommandError as e:
|
||||
PchumLog.warning("CommandError %s" % str(e))
|
||||
PchumLog.warning(f"CommandError: {e}")
|
||||
|
||||
yield True
|
||||
except socket.timeout as se:
|
||||
|
@ -281,7 +223,7 @@ class PesterIRC(QtCore.QThread):
|
|||
self.socket.close()
|
||||
raise se
|
||||
except Exception as e:
|
||||
PchumLog.debug("other exception: %s" % str(e))
|
||||
PchumLog.exception("Non-socket related exception in conn_generator().")
|
||||
raise e
|
||||
else:
|
||||
PchumLog.debug("ending while, end is %s" % self._end)
|
||||
|
@ -290,6 +232,61 @@ class PesterIRC(QtCore.QThread):
|
|||
self.socket.close()
|
||||
yield False
|
||||
|
||||
def parse_buffer(self, buffer):
|
||||
"""Parse lines from bytes buffer, returns lines and emptied buffer."""
|
||||
try:
|
||||
decoded_buffer = buffer.decode(encoding="utf-8")
|
||||
except UnicodeDecodeError as exception:
|
||||
PchumLog.warning(f"Failed to decode with utf-8, falling back to latin-1.")
|
||||
try:
|
||||
decoded_buffer = buffer.decode(encoding="latin-1")
|
||||
except ValueError as exception:
|
||||
PchumLog.warning("latin-1 failed too xd")
|
||||
return "", buffer # throw it back in the cooker
|
||||
|
||||
data = decoded_buffer.split("\r\n")
|
||||
if data[-1]:
|
||||
# Last entry has incomplete data, add back to buffer
|
||||
buffer = data[-1].encode("utf-8") + buffer
|
||||
return data[:-1], buffer
|
||||
|
||||
def parse_irc_line(self, line: str):
|
||||
parts = line.split(" ")
|
||||
tags = None
|
||||
prefix = None
|
||||
print(line)
|
||||
if parts[0].startswith(":"):
|
||||
prefix = parts[0][1:]
|
||||
command = parts[1]
|
||||
args = parts[2:]
|
||||
elif parts[0].startswith("@"):
|
||||
tags = parts[0] # IRCv3 message tag
|
||||
prefix = parts[1][1:]
|
||||
command = parts[2]
|
||||
args = parts[3:]
|
||||
else:
|
||||
command = parts[0]
|
||||
args = parts[1:]
|
||||
|
||||
if command.isdigit():
|
||||
try:
|
||||
command = numeric_events[command]
|
||||
except KeyError:
|
||||
PchumLog.info("Server send unknown numeric event {command}.")
|
||||
command = command.lower()
|
||||
|
||||
# If ':' is present the subsequent args are one parameter.
|
||||
fused_args = []
|
||||
for idx, arg in enumerate(args):
|
||||
if arg.startswith(":"):
|
||||
final_param = ' '.join(args[idx:])
|
||||
fused_args.append(final_param[1:])
|
||||
break
|
||||
else:
|
||||
fused_args.append(arg)
|
||||
|
||||
return (tags, prefix, command, fused_args)
|
||||
|
||||
def close(self):
|
||||
# with extreme prejudice
|
||||
if self.socket:
|
||||
|
@ -316,7 +313,7 @@ class PesterIRC(QtCore.QThread):
|
|||
# Ask if users wants to connect anyway
|
||||
self.askToConnect.emit(e)
|
||||
raise e
|
||||
self.conn = self.conn()
|
||||
self.conn = self.conn_generator()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
|
@ -367,15 +364,47 @@ class PesterIRC(QtCore.QThread):
|
|||
except (OSError, ValueError, IndexError) as se:
|
||||
raise se
|
||||
except StopIteration:
|
||||
self.conn = self.conn()
|
||||
self.conn = self.conn_generator()
|
||||
return True
|
||||
else:
|
||||
return res
|
||||
|
||||
@QtCore.pyqtSlot(PesterProfile)
|
||||
def getMood(self, *chums):
|
||||
if hasattr(self, "cli"):
|
||||
self.command_handler.getMood(*chums)
|
||||
"""Get mood via metadata if supported"""
|
||||
|
||||
# Get via metadata or via legacy method
|
||||
if self.parent.metadata_supported:
|
||||
# Metadata
|
||||
for chum in chums:
|
||||
try:
|
||||
self.send_irc.metadata(chum.handle, "get", "mood")
|
||||
except OSError as e:
|
||||
PchumLog.warning(e)
|
||||
self.parent.setConnectionBroken()
|
||||
else:
|
||||
# Legacy
|
||||
PchumLog.warning(
|
||||
"Server doesn't seem to support metadata, using legacy GETMOOD."
|
||||
)
|
||||
chumglub = "GETMOOD "
|
||||
for chum in chums:
|
||||
if len(chumglub + chum.handle) >= 350:
|
||||
try:
|
||||
self.send_irc.msg("#pesterchum", chumglub)
|
||||
except OSError as e:
|
||||
PchumLog.warning(e)
|
||||
self.parent.setConnectionBroken()
|
||||
chumglub = "GETMOOD "
|
||||
# No point in GETMOOD-ing services
|
||||
if chum.handle.casefold() not in SERVICES:
|
||||
chumglub += chum.handle
|
||||
if chumglub != "GETMOOD ":
|
||||
try:
|
||||
self.send_irc.msg("#pesterchum", chumglub)
|
||||
except OSError as e:
|
||||
PchumLog.warning(e)
|
||||
self.parent.setConnectionBroken()
|
||||
|
||||
@QtCore.pyqtSlot(PesterList)
|
||||
def getMoods(self, chums):
|
||||
|
@ -462,8 +491,7 @@ class PesterIRC(QtCore.QThread):
|
|||
if hasattr(self, "cli"):
|
||||
h = str(handle)
|
||||
try:
|
||||
self.send_irc.msg(
|
||||
self, h, "COLOR >%s" % (self.mainwindow.profile().colorcmd())
|
||||
self.send_irc.msg(h, "COLOR >%s" % (self.mainwindow.profile().colorcmd())
|
||||
)
|
||||
if initiated:
|
||||
self.send_irc.msg(h, "PESTERCHUM:BEGIN")
|
||||
|
@ -530,7 +558,6 @@ class PesterIRC(QtCore.QThread):
|
|||
for h in list(self.mainwindow.convos.keys()):
|
||||
try:
|
||||
self.send_irc.msg(
|
||||
self,
|
||||
h,
|
||||
"COLOR >%s" % (self.mainwindow.profile().colorcmd()),
|
||||
)
|
||||
|
@ -766,13 +793,11 @@ class PesterIRC(QtCore.QThread):
|
|||
PchumLog.info('---> recv "CTCP {} :{}"'.format(handle, msg[1:-1]))
|
||||
# VERSION, return version
|
||||
if msg[1:-1].startswith("VERSION"):
|
||||
self.send_irc.ctcp_reply(
|
||||
self.parent.cli, handle, "VERSION", "Pesterchum %s" % (_pcVersion)
|
||||
self.send_irc.ctcp(handle, "VERSION", "Pesterchum %s" % (_pcVersion)
|
||||
)
|
||||
# CLIENTINFO, return supported CTCP commands.
|
||||
elif msg[1:-1].startswith("CLIENTINFO"):
|
||||
self.send_irc.ctcp_reply(
|
||||
self.parent.cli,
|
||||
self.send_irc.ctcp(
|
||||
handle,
|
||||
"CLIENTINFO",
|
||||
"ACTION VERSION CLIENTINFO PING SOURCE NOQUIRKS GETMOOD",
|
||||
|
@ -780,15 +805,13 @@ class PesterIRC(QtCore.QThread):
|
|||
# PING, return pong
|
||||
elif msg[1:-1].startswith("PING"):
|
||||
if len(msg[1:-1].split("PING ")) > 1:
|
||||
self.send_irc.ctcp_reply(
|
||||
self.parent.cli, handle, "PING", msg[1:-1].split("PING ")[1]
|
||||
self.send_irc.ctcp(handle, "PING", msg[1:-1].split("PING ")[1]
|
||||
)
|
||||
else:
|
||||
self.send_irc.ctcp_reply(self.parent.cli, handle, "PING")
|
||||
self.send_irc.ctcp(handle, "PING")
|
||||
# SOURCE, return source
|
||||
elif msg[1:-1].startswith("SOURCE"):
|
||||
self.send_irc.ctcp_reply(
|
||||
self.parent.cli,
|
||||
self.send_irc.ctcp(
|
||||
handle,
|
||||
"SOURCE",
|
||||
"https://github.com/Dpeta/pesterchum-alt-servers",
|
||||
|
@ -802,7 +825,7 @@ class PesterIRC(QtCore.QThread):
|
|||
# GETMOOD via CTCP
|
||||
# Maybe we can do moods like this in the future...
|
||||
mymood = self.mainwindow.profile().mood.value()
|
||||
self.send_irc.ctcp_reply(self.parent.cli, handle, "MOOD >%d" % (mymood))
|
||||
self.send_irc.ctcp(handle, "MOOD >%d" % (mymood))
|
||||
# Backwards compatibility
|
||||
self.send_irc.msg("#pesterchum", "MOOD >%d" % (mymood))
|
||||
return
|
||||
|
@ -1237,42 +1260,6 @@ class PesterIRC(QtCore.QThread):
|
|||
"""Respond to server PING with PONG."""
|
||||
self.send_irc.pong(token)
|
||||
|
||||
def getMood(self, *chums):
|
||||
"""Get mood via metadata if supported"""
|
||||
|
||||
# Get via metadata or via legacy method
|
||||
if self.parent.metadata_supported:
|
||||
# Metadata
|
||||
for chum in chums:
|
||||
try:
|
||||
self.send_irc.metadata(chum.handle, "get", "mood")
|
||||
except OSError as e:
|
||||
PchumLog.warning(e)
|
||||
self.parent.setConnectionBroken()
|
||||
else:
|
||||
# Legacy
|
||||
PchumLog.warning(
|
||||
"Server doesn't seem to support metadata, using legacy GETMOOD."
|
||||
)
|
||||
chumglub = "GETMOOD "
|
||||
for chum in chums:
|
||||
if len(chumglub + chum.handle) >= 350:
|
||||
try:
|
||||
self.send_irc.msg("#pesterchum", chumglub)
|
||||
except OSError as e:
|
||||
PchumLog.warning(e)
|
||||
self.parent.setConnectionBroken()
|
||||
chumglub = "GETMOOD "
|
||||
# No point in GETMOOD-ing services
|
||||
if chum.handle.casefold() not in SERVICES:
|
||||
chumglub += chum.handle
|
||||
if chumglub != "GETMOOD ":
|
||||
try:
|
||||
self.send_irc.msg("#pesterchum", chumglub)
|
||||
except OSError as e:
|
||||
PchumLog.warning(e)
|
||||
self.parent.setConnectionBroken()
|
||||
|
||||
def get(self, in_command_parts):
|
||||
PchumLog.debug("in_command_parts: %s" % in_command_parts)
|
||||
""" finds a command
|
||||
|
@ -1310,10 +1297,7 @@ class PesterIRC(QtCore.QThread):
|
|||
|
||||
def run_command(self, command, *args):
|
||||
"""finds and runs a command"""
|
||||
arguments_str = ""
|
||||
for x in args:
|
||||
arguments_str += str(x) + " "
|
||||
PchumLog.debug("processCommand {}({})".format(command, arguments_str.strip()))
|
||||
PchumLog.debug("processCommand {}({})".format(command, args))
|
||||
|
||||
try:
|
||||
f = self.get(command)
|
||||
|
|
489
oyoyo/client.py
489
oyoyo/client.py
|
@ -1,489 +0,0 @@
|
|||
# Copyright (c) 2008 Duncan Fordyce
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import sys
|
||||
import time
|
||||
import ssl
|
||||
import socket
|
||||
import select
|
||||
import logging
|
||||
import datetime
|
||||
import traceback
|
||||
|
||||
from oyoyo.parse import parse_raw_irc_command
|
||||
from oyoyo import helpers
|
||||
from oyoyo.cmdhandler import CommandError
|
||||
|
||||
PchumLog = logging.getLogger("pchumLogger")
|
||||
|
||||
try:
|
||||
import certifi
|
||||
except ImportError:
|
||||
if sys.platform == "darwin":
|
||||
# Certifi is required to validate certificates on MacOS with pyinstaller builds.
|
||||
PchumLog.warning(
|
||||
"Failed to import certifi, which is recommended on MacOS. "
|
||||
"Pesterchum might not be able to validate certificates unless "
|
||||
"Python's root certs are installed."
|
||||
)
|
||||
else:
|
||||
PchumLog.info(
|
||||
"Failed to import certifi, Pesterchum will not be able to validate "
|
||||
"certificates if the system-provided root certificates are invalid."
|
||||
)
|
||||
|
||||
|
||||
class IRCClientError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IRCClient:
|
||||
"""IRC Client class. This handles one connection to a server.
|
||||
This can be used either with or without IRCApp ( see connect() docs )
|
||||
"""
|
||||
|
||||
def __init__(self, cmd_handler, **kwargs):
|
||||
"""the first argument should be an object with attributes/methods named
|
||||
as the irc commands. You may subclass from one of the classes in
|
||||
oyoyo.cmdhandler for convenience but it is not required. The
|
||||
methods should have arguments (prefix, args). prefix is
|
||||
normally the sender of the command. args is a list of arguments.
|
||||
Its recommened you subclass oyoyo.cmdhandler.DefaultCommandHandler,
|
||||
this class provides defaults for callbacks that are required for
|
||||
normal IRC operation.
|
||||
|
||||
all other arguments should be keyword arguments. The most commonly
|
||||
used will be nick, host and port. You can also specify an "on connect"
|
||||
callback. ( check the source for others )
|
||||
|
||||
Warning: By default this class will not block on socket operations, this
|
||||
means if you use a plain while loop your app will consume 100% cpu.
|
||||
To enable blocking pass blocking=True.
|
||||
|
||||
>>> class My_Handler(DefaultCommandHandler):
|
||||
... def privmsg(self, prefix, command, args):
|
||||
... print "%s said %s" % (prefix, args[1])
|
||||
...
|
||||
>>> def connect_callback(c):
|
||||
... helpers.join(c, '#myroom')
|
||||
...
|
||||
>>> cli = IRCClient(My_Handler,
|
||||
... host="irc.freenode.net",
|
||||
... port=6667,
|
||||
... nick="myname",
|
||||
... connect_cb=connect_callback)
|
||||
...
|
||||
>>> cli_con = cli.connect()
|
||||
>>> while 1:
|
||||
... cli_con.next()
|
||||
...
|
||||
"""
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
self.nick = None
|
||||
self.realname = None
|
||||
self.username = None
|
||||
self.host = None
|
||||
self.port = None
|
||||
# self.connect_cb = None
|
||||
self.timeout = None
|
||||
self.blocking = None
|
||||
self.ssl = None
|
||||
|
||||
self.__dict__.update(kwargs)
|
||||
self.command_handler = cmd_handler(self)
|
||||
|
||||
self._end = False
|
||||
|
||||
def send(self, *args, **kwargs):
|
||||
"""send a message to the connected server. all arguments are joined
|
||||
with a space for convenience, for example the following are identical
|
||||
|
||||
>>> cli.send("JOIN %s" % some_room)
|
||||
>>> cli.send("JOIN", some_room)
|
||||
|
||||
In python 2, all args must be of type str or unicode, *BUT* if they are
|
||||
unicode they will be converted to str with the encoding specified by
|
||||
the 'encoding' keyword argument (default 'utf8').
|
||||
In python 3, all args must be of type str or bytes, *BUT* if they are
|
||||
str they will be converted to bytes with the encoding specified by the
|
||||
'encoding' keyword argument (default 'utf8').
|
||||
"""
|
||||
if self._end == True:
|
||||
return
|
||||
# Convert all args to bytes if not already
|
||||
encoding = kwargs.get("encoding") or "utf8"
|
||||
bargs = []
|
||||
for arg in args:
|
||||
if isinstance(arg, str):
|
||||
bargs.append(bytes(arg, encoding))
|
||||
elif isinstance(arg, bytes):
|
||||
bargs.append(arg)
|
||||
elif type(arg).__name__ == "unicode":
|
||||
bargs.append(arg.encode(encoding))
|
||||
else:
|
||||
PchumLog.warning(
|
||||
"Refusing to send one of the args from provided: %s"
|
||||
% repr([(type(arg), arg) for arg in args])
|
||||
)
|
||||
raise IRCClientError(
|
||||
"Refusing to send one of the args from provided: %s"
|
||||
% repr([(type(arg), arg) for arg in args])
|
||||
)
|
||||
|
||||
msg = bytes(" ", "UTF-8").join(bargs)
|
||||
PchumLog.info('---> send "%s"' % msg)
|
||||
try:
|
||||
tries = 1
|
||||
while tries < 10:
|
||||
# Check if alive
|
||||
if self._end == True:
|
||||
break
|
||||
if self.socket.fileno() == -1:
|
||||
self._end = True
|
||||
break
|
||||
try:
|
||||
ready_to_read, ready_to_write, in_error = select.select(
|
||||
[], [self.socket], []
|
||||
)
|
||||
for x in ready_to_write:
|
||||
x.sendall(msg + bytes("\r\n", "UTF-8"))
|
||||
break
|
||||
except ssl.SSLWantReadError as e:
|
||||
PchumLog.warning("ssl.SSLWantReadError on send, " + str(e))
|
||||
select.select([self.socket], [], [])
|
||||
if tries >= 9:
|
||||
raise e
|
||||
except ssl.SSLWantWriteError as e:
|
||||
PchumLog.warning("ssl.SSLWantWriteError on send, " + str(e))
|
||||
select.select([], [self.socket], [])
|
||||
if tries >= 9:
|
||||
raise e
|
||||
except ssl.SSLEOFError as e:
|
||||
# ssl.SSLEOFError guarantees a broken connection.
|
||||
PchumLog.warning("ssl.SSLEOFError in on send, " + str(e))
|
||||
raise ssl.SSLEOFError
|
||||
except (socket.timeout, TimeoutError) as e:
|
||||
# socket.timeout is deprecated in 3.10
|
||||
PchumLog.warning("TimeoutError in on send, " + str(e))
|
||||
raise socket.timeout
|
||||
except (OSError, IndexError, ValueError, Exception) as e:
|
||||
PchumLog.warning("Unkown error on send, " + str(e))
|
||||
if tries >= 9:
|
||||
raise e
|
||||
tries += 1
|
||||
PchumLog.warning("Retrying send. (attempt %s)" % str(tries))
|
||||
time.sleep(0.1)
|
||||
|
||||
PchumLog.debug(
|
||||
"ready_to_write (len %s): " % str(len(ready_to_write))
|
||||
+ str(ready_to_write)
|
||||
)
|
||||
except Exception as se:
|
||||
PchumLog.warning("Send Exception %s" % str(se))
|
||||
try:
|
||||
if not self.blocking and se.errno == 11:
|
||||
pass
|
||||
else:
|
||||
# raise se
|
||||
self._end = True # This ok?
|
||||
except AttributeError:
|
||||
# raise se
|
||||
self._end = True # This ok?
|
||||
|
||||
def get_ssl_context(self):
|
||||
"""Returns an SSL context for connecting over SSL/TLS.
|
||||
Loads the certifi root certificate bundle if the certifi module is less
|
||||
than a year old or if the system certificate store is empty.
|
||||
|
||||
The cert store on Windows also seems to have issues, so it's better
|
||||
to use the certifi provided bundle assuming it's a recent version.
|
||||
|
||||
On MacOS the system cert store is usually empty, as Python does not use
|
||||
the system provided ones, instead relying on a bundle installed with the
|
||||
python installer."""
|
||||
default_context = ssl.create_default_context()
|
||||
if "certifi" not in sys.modules:
|
||||
return default_context
|
||||
|
||||
# Get age of certifi module
|
||||
certifi_date = datetime.datetime.strptime(certifi.__version__, "%Y.%m.%d")
|
||||
current_date = datetime.datetime.now()
|
||||
certifi_age = current_date - certifi_date
|
||||
|
||||
empty_cert_store = (
|
||||
list(default_context.cert_store_stats().values()).count(0) == 3
|
||||
)
|
||||
# 31557600 seconds is approximately 1 year
|
||||
if empty_cert_store or certifi_age.total_seconds() <= 31557600:
|
||||
PchumLog.info(
|
||||
"Using SSL/TLS context with certifi-provided root certificates."
|
||||
)
|
||||
return ssl.create_default_context(cafile=certifi.where())
|
||||
PchumLog.info("Using SSL/TLS context with system-provided root certificates.")
|
||||
return default_context
|
||||
|
||||
def connect(self, verify_hostname=True):
|
||||
"""initiates the connection to the server set in self.host:self.port
|
||||
self.ssl decides whether the connection uses ssl.
|
||||
|
||||
Certificate validation when using SSL/TLS may be disabled by
|
||||
passing the 'verify_hostname' parameter. The user is asked if they
|
||||
want to disable it if this functions raises a certificate validation error,
|
||||
in which case the function may be called again with 'verify_hostname'."""
|
||||
PchumLog.info("connecting to {}:{}".format(self.host, self.port))
|
||||
|
||||
# Open connection
|
||||
plaintext_socket = socket.create_connection((self.host, self.port))
|
||||
|
||||
if self.ssl:
|
||||
# Upgrade connection to use SSL/TLS if enabled
|
||||
context = self.get_ssl_context()
|
||||
context.check_hostname = verify_hostname
|
||||
self.socket = context.wrap_socket(
|
||||
plaintext_socket, server_hostname=self.host
|
||||
)
|
||||
else:
|
||||
# SSL/TLS is disabled, connection is plaintext
|
||||
self.socket = plaintext_socket
|
||||
|
||||
# setblocking is a shorthand for timeout,
|
||||
# we shouldn't use both.
|
||||
if self.timeout:
|
||||
self.socket.settimeout(self.timeout)
|
||||
elif not self.blocking:
|
||||
self.socket.setblocking(False)
|
||||
elif self.blocking:
|
||||
self.socket.setblocking(True)
|
||||
|
||||
helpers.nick(self, self.nick)
|
||||
helpers.user(self, self.username, self.realname)
|
||||
# if self.connect_cb:
|
||||
# self.connect_cb(self)
|
||||
|
||||
def conn(self):
|
||||
"""returns a generator object."""
|
||||
try:
|
||||
buffer = b""
|
||||
while not self._end:
|
||||
# Block for connection-killing exceptions
|
||||
try:
|
||||
tries = 1
|
||||
while tries < 10:
|
||||
# Check if alive
|
||||
if self._end == True:
|
||||
break
|
||||
if self.socket.fileno() == -1:
|
||||
self._end = True
|
||||
break
|
||||
try:
|
||||
ready_to_read, ready_to_write, in_error = select.select(
|
||||
[self.socket], [], []
|
||||
)
|
||||
for x in ready_to_read:
|
||||
buffer += x.recv(1024)
|
||||
break
|
||||
except ssl.SSLWantReadError as e:
|
||||
PchumLog.warning("ssl.SSLWantReadError on send, " + str(e))
|
||||
select.select([self.socket], [], [])
|
||||
if tries >= 9:
|
||||
raise e
|
||||
except ssl.SSLWantWriteError as e:
|
||||
PchumLog.warning("ssl.SSLWantWriteError on send, " + str(e))
|
||||
select.select([], [self.socket], [])
|
||||
if tries >= 9:
|
||||
raise e
|
||||
except ssl.SSLEOFError as e:
|
||||
# ssl.SSLEOFError guarantees a broken connection.
|
||||
PchumLog.warning("ssl.SSLEOFError in on send, " + str(e))
|
||||
raise e
|
||||
except (socket.timeout, TimeoutError) as e:
|
||||
# socket.timeout is deprecated in 3.10
|
||||
PchumLog.warning("TimeoutError in on send, " + str(e))
|
||||
raise socket.timeout
|
||||
except (OSError, IndexError, ValueError, Exception) as e:
|
||||
PchumLog.debug("Miscellaneous exception in conn, " + str(e))
|
||||
if tries >= 9:
|
||||
raise e
|
||||
tries += 1
|
||||
PchumLog.debug(
|
||||
"Possibly retrying recv. (attempt %s)" % str(tries)
|
||||
)
|
||||
time.sleep(0.1)
|
||||
|
||||
except socket.timeout as e:
|
||||
PchumLog.warning("timeout in client.py, " + str(e))
|
||||
if self._end:
|
||||
break
|
||||
raise e
|
||||
except ssl.SSLEOFError as e:
|
||||
raise e
|
||||
except OSError as e:
|
||||
PchumLog.warning("conn exception {} in {}".format(e, self))
|
||||
if self._end:
|
||||
break
|
||||
if not self.blocking and e.errno == 11:
|
||||
pass
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
if self._end:
|
||||
break
|
||||
if len(buffer) == 0 and self.blocking:
|
||||
PchumLog.debug("len(buffer) = 0")
|
||||
raise OSError("Connection closed")
|
||||
|
||||
data = buffer.split(bytes("\n", "UTF-8"))
|
||||
buffer = data.pop()
|
||||
|
||||
PchumLog.debug("data = " + str(data))
|
||||
|
||||
for el in data:
|
||||
tags, prefix, command, args = parse_raw_irc_command(el)
|
||||
# print(tags, prefix, command, args)
|
||||
try:
|
||||
# Only need tags with tagmsg
|
||||
if command.upper() == "TAGMSG":
|
||||
self.command_handler.run(command, prefix, tags, *args)
|
||||
else:
|
||||
self.command_handler.run(command, prefix, *args)
|
||||
except CommandError as e:
|
||||
PchumLog.warning("CommandError %s" % str(e))
|
||||
|
||||
yield True
|
||||
except socket.timeout as se:
|
||||
PchumLog.debug("passing timeout")
|
||||
raise se
|
||||
except (OSError, ssl.SSLEOFError) as se:
|
||||
PchumLog.debug("problem: %s" % (str(se)))
|
||||
if self.socket:
|
||||
PchumLog.info("error: closing socket")
|
||||
self.socket.close()
|
||||
raise se
|
||||
except Exception as e:
|
||||
PchumLog.debug("other exception: %s" % str(e))
|
||||
raise e
|
||||
else:
|
||||
PchumLog.debug("ending while, end is %s" % self._end)
|
||||
if self.socket:
|
||||
PchumLog.info("finished: closing socket")
|
||||
self.socket.close()
|
||||
yield False
|
||||
|
||||
def close(self):
|
||||
# with extreme prejudice
|
||||
if self.socket:
|
||||
PchumLog.info("shutdown socket")
|
||||
# print("shutdown socket")
|
||||
self._end = True
|
||||
try:
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except OSError as e:
|
||||
PchumLog.debug(
|
||||
"Error while shutting down socket, already broken? %s" % str(e)
|
||||
)
|
||||
try:
|
||||
self.socket.close()
|
||||
except OSError as e:
|
||||
PchumLog.debug(
|
||||
"Error while closing socket, already broken? %s" % str(e)
|
||||
)
|
||||
|
||||
|
||||
class IRCApp:
|
||||
"""This class manages several IRCClient instances without the use of threads.
|
||||
(Non-threaded) Timer functionality is also included.
|
||||
"""
|
||||
|
||||
class _ClientDesc:
|
||||
def __init__(self, **kwargs):
|
||||
self.con = None
|
||||
self.autoreconnect = False
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __init__(self):
|
||||
self._clients = {}
|
||||
self._timers = []
|
||||
self.running = False
|
||||
self.sleep_time = 0.5
|
||||
|
||||
def addClient(self, client, autoreconnect=False):
|
||||
"""add a client object to the application. setting autoreconnect
|
||||
to true will mean the application will attempt to reconnect the client
|
||||
after every disconnect. you can also set autoreconnect to a number
|
||||
to specify how many reconnects should happen.
|
||||
|
||||
warning: if you add a client that has blocking set to true,
|
||||
timers will no longer function properly"""
|
||||
PchumLog.info("added client {} (ar={})".format(client, autoreconnect))
|
||||
self._clients[client] = self._ClientDesc(autoreconnect=autoreconnect)
|
||||
|
||||
def addTimer(self, seconds, cb):
|
||||
"""add a timed callback. accuracy is not specified, you can only
|
||||
garuntee the callback will be called after seconds has passed.
|
||||
( the only advantage to these timers is they dont use threads )
|
||||
"""
|
||||
assert callable(cb)
|
||||
PchumLog.info("added timer to call {} in {}s".format(cb, seconds))
|
||||
self._timers.append((time.time() + seconds, cb))
|
||||
|
||||
def run(self):
|
||||
"""run the application. this will block until stop() is called"""
|
||||
# TODO: convert this to use generators too?
|
||||
self.running = True
|
||||
while self.running:
|
||||
found_one_alive = False
|
||||
|
||||
for client, clientdesc in self._clients.items():
|
||||
if clientdesc.con is None:
|
||||
clientdesc.con = client.connect()
|
||||
|
||||
try:
|
||||
next(clientdesc.con)
|
||||
except Exception as e:
|
||||
PchumLog.error("client error %s" % str(e))
|
||||
PchumLog.error(traceback.format_exc())
|
||||
if clientdesc.autoreconnect:
|
||||
clientdesc.con = None
|
||||
if isinstance(clientdesc.autoreconnect, (int, float)):
|
||||
clientdesc.autoreconnect -= 1
|
||||
found_one_alive = True
|
||||
else:
|
||||
clientdesc.con = False
|
||||
else:
|
||||
found_one_alive = True
|
||||
|
||||
if not found_one_alive:
|
||||
PchumLog.info("nothing left alive... quiting")
|
||||
self.stop()
|
||||
|
||||
now = time.time()
|
||||
timers = self._timers[:]
|
||||
self._timers = []
|
||||
for target_time, cb in timers:
|
||||
if now > target_time:
|
||||
PchumLog.info("calling timer cb %s" % cb)
|
||||
cb()
|
||||
else:
|
||||
self._timers.append((target_time, cb))
|
||||
|
||||
time.sleep(self.sleep_time)
|
||||
|
||||
def stop(self):
|
||||
"""stop the application"""
|
||||
self.running = False
|
|
@ -1,16 +1,14 @@
|
|||
"""Class and functions for sending outgoing IRC commands."""
|
||||
import socket
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("pchumLogger")
|
||||
|
||||
|
||||
class send_irc:
|
||||
class SendIRC:
|
||||
"""Provides functions for outgoing IRC commands."""
|
||||
|
||||
def __init__(self):
|
||||
self.socket = None # INET socket connected with server.
|
||||
self._end = None # Keep track of if we're disconnected.
|
||||
|
||||
def send(self, *args: str, text=None):
|
||||
"""Send a command to the IRC server.
|
||||
|
@ -19,7 +17,9 @@ class send_irc:
|
|||
The 'text' argument is for the final parameter, which can have spaces."""
|
||||
# Return if disconnected
|
||||
if not self.socket or self.socket.fileno() == -1:
|
||||
log.error(f"Send attempted while disconnected, args: {args}, text: {text}.")
|
||||
log.error(
|
||||
"Send attempted while disconnected, args: %s, text: %s.", args, text
|
||||
)
|
||||
return
|
||||
|
||||
command = ""
|
||||
|
@ -35,17 +35,17 @@ class send_irc:
|
|||
outgoing_bytes = command.encode(encoding="utf-8", errors="replace")
|
||||
|
||||
try:
|
||||
log.debug(f"Sending: {command}")
|
||||
log.debug("Sending: %s", command)
|
||||
self.socket.send(outgoing_bytes)
|
||||
except OSError:
|
||||
log.exception(f"Error while sending: '{command.strip()}'")
|
||||
log.exception("Error while sending: '%s'", command.strip())
|
||||
self.socket.close()
|
||||
|
||||
def ping(self, token: str):
|
||||
def ping(self, token):
|
||||
"""Send PING command to server to check for connectivity."""
|
||||
self.send("PING", text=token)
|
||||
|
||||
def pong(self, token: str):
|
||||
def pong(self, token):
|
||||
"""Send PONG command to reply to server PING."""
|
||||
self.send("PONG", token)
|
||||
|
||||
|
@ -59,10 +59,10 @@ class send_irc:
|
|||
|
||||
def msg(self, target, text):
|
||||
"""Send PRIVMSG command to send a message."""
|
||||
for line in msg.split("\n"):
|
||||
self.send("PRIVMSG", target, text=text)
|
||||
for line in text.split("\n"):
|
||||
self.send("PRIVMSG", target, text=line)
|
||||
|
||||
def names(self, channel: str):
|
||||
def names(self, channel):
|
||||
"""Send NAMES command to view channel members."""
|
||||
self.send("NAMES", channel)
|
||||
|
||||
|
@ -80,37 +80,43 @@ class send_irc:
|
|||
|
||||
def ctcp(self, target, command, msg=""):
|
||||
"""Send Client-to-Client Protocol message."""
|
||||
outgoing_ctcp = " ".join([command, msg]).strip() # Extra spaces break protocol, so strip.
|
||||
outgoing_ctcp = " ".join(
|
||||
[command, msg]
|
||||
).strip() # Extra spaces break protocol, so strip.
|
||||
self.msg(target, f"\x01{outgoing_ctcp}\x01")
|
||||
|
||||
def metadata(self, target: str, subcommand: str, *params: str):
|
||||
# IRC metadata draft specification
|
||||
# https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237
|
||||
def metadata(self, target, subcommand, *params):
|
||||
"""Send Metadata command to get or set metadata.
|
||||
|
||||
See IRC metadata draft specification:
|
||||
https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237
|
||||
"""
|
||||
self.send("METADATA", target, subcommand, *params)
|
||||
|
||||
def cap(self, subcommand: str, *params: str):
|
||||
# Capability Negotiation
|
||||
# https://ircv3.net/specs/extensions/capability-negotiation.html
|
||||
def cap(self, subcommand, *params):
|
||||
"""Send IRCv3 CAP command for capability negotiation.
|
||||
|
||||
See: https://ircv3.net/specs/extensions/capability-negotiation.html"""
|
||||
self.send("CAP", subcommand, *params)
|
||||
|
||||
def join(self, channel: str, key=""):
|
||||
def join(self, channel, key=""):
|
||||
"""Send JOIN command to join a channel/memo.
|
||||
|
||||
Keys or joining multiple channels is possible in the specification, but unused."""
|
||||
channel_and_key = " ".join([channel, key]).strip()
|
||||
self.send("JOIN", channel_and_key)
|
||||
|
||||
def part(self, channel: str):
|
||||
def part(self, channel):
|
||||
"""Send PART command to leave a channel/memo.
|
||||
|
||||
Providing a reason or leaving multiple channels is possible in the specification."""
|
||||
self.send("PART", channel)
|
||||
|
||||
def notice(self, target: str, text: str):
|
||||
def notice(self, target, text):
|
||||
"""Send a NOTICE to a user or channel."""
|
||||
self.send("NOTICE", target, text=text)
|
||||
|
||||
def invite(self, nick: str, channel: str):
|
||||
def invite(self, nick, channel):
|
||||
"""Send INVITE command to invite a user to a channel."""
|
||||
self.send("INVITE", nick, channel)
|
||||
|
||||
|
@ -122,7 +128,3 @@ class send_irc:
|
|||
self.send("AWAY", text=text)
|
||||
else:
|
||||
self.send("AWAY")
|
||||
|
||||
def banana(self):
|
||||
"""Do you want a banana? diz bananan 4 u"""
|
||||
self.join("#banana_kingdom", key="Shoplift8723")
|
||||
|
|
Loading…
Reference in a new issue