Merge pull request #116 from Dpeta/network-integration-testing
Rewrite networking and IRC handling
This commit is contained in:
commit
ca992feb21
24 changed files with 1366 additions and 2490 deletions
4
convo.py
4
convo.py
|
@ -235,6 +235,7 @@ class PesterTabWindow(QtWidgets.QFrame):
|
|||
i, QtGui.QColor(self.mainwindow.theme["%s/tabs/newmsgcolor" % (self.type)])
|
||||
)
|
||||
convo = self.convos[handle]
|
||||
|
||||
# Create a function for the icon to use
|
||||
# TODO: Let us disable this.
|
||||
def func():
|
||||
|
@ -286,7 +287,7 @@ class PesterTabWindow(QtWidgets.QFrame):
|
|||
del self.convos[handle]
|
||||
del self.tabIndices[handle]
|
||||
self.tabs.removeTab(i)
|
||||
for (h, j) in self.tabIndices.items():
|
||||
for h, j in self.tabIndices.items():
|
||||
if j > i:
|
||||
self.tabIndices[h] = j - 1
|
||||
self.layout.removeWidget(convo)
|
||||
|
@ -954,6 +955,7 @@ class PesterConvo(QtWidgets.QFrame):
|
|||
else:
|
||||
self.newmessage = True
|
||||
self.setWindowTitle(title + "*")
|
||||
|
||||
# karxi: The order of execution here is a bit unclear...I'm not
|
||||
# entirely sure how much of this directly affects what we see.
|
||||
def func():
|
||||
|
|
|
@ -146,7 +146,7 @@ class pesterQuirks:
|
|||
# suffix = [q for q in self.quirklist if q.type == "suffix"]
|
||||
|
||||
newlist = []
|
||||
for (i, o) in enumerate(lexed):
|
||||
for i, o in enumerate(lexed):
|
||||
if type(o) not in [str, str]:
|
||||
if i == 0:
|
||||
string = " "
|
||||
|
|
|
@ -119,7 +119,7 @@ class MultiTextDialog(QtWidgets.QDialog):
|
|||
r = self.exec()
|
||||
if r == QtWidgets.QDialog.DialogCode.Accepted:
|
||||
retval = {}
|
||||
for (name, widget) in self.inputs.items():
|
||||
for name, widget in self.inputs.items():
|
||||
retval[name] = str(widget.text())
|
||||
return retval
|
||||
else:
|
||||
|
|
|
@ -194,7 +194,7 @@ SYSTEM = [
|
|||
"umount2",
|
||||
"vhangup",
|
||||
]
|
||||
CALL_BLACKLIST = SETUID + SYSTEM
|
||||
CALL_BLACKLIST = SYSTEM # + SETUID
|
||||
|
||||
"""
|
||||
# Optional
|
||||
|
|
|
@ -78,7 +78,7 @@ class PesterLogUserSelect(QtWidgets.QDialog):
|
|||
self.chumsBox.setStyleSheet(self.theme["main/chums/style"])
|
||||
self.chumsBox.optionsMenu = QtWidgets.QMenu(self)
|
||||
|
||||
for (_, t) in enumerate(chumMemoList):
|
||||
for _, t in enumerate(chumMemoList):
|
||||
item = QtWidgets.QListWidgetItem(t)
|
||||
item.setForeground(
|
||||
QtGui.QBrush(QtGui.QColor(self.theme["main/chums/userlistcolor"]))
|
||||
|
@ -244,7 +244,7 @@ class PesterLogViewer(QtWidgets.QDialog):
|
|||
child_1 = None
|
||||
last = ["", ""]
|
||||
# blackbrush = QtGui.QBrush(QtCore.Qt.GlobalColor.black)
|
||||
for (i, l) in enumerate(self.logList):
|
||||
for i, l in enumerate(self.logList):
|
||||
my = self.fileToMonthYear(l)
|
||||
if my[0] != last[0]:
|
||||
child_1 = QtWidgets.QTreeWidgetItem(["{} {}".format(my[0], my[1])])
|
||||
|
|
9
memos.py
9
memos.py
|
@ -973,6 +973,7 @@ class PesterMemo(PesterConvo):
|
|||
self.userlist.addItem(u)
|
||||
|
||||
def updateChanModes(self, modes, op):
|
||||
PchumLog.debug("updateChanModes(%s, %s)", modes, op)
|
||||
if not hasattr(self, "modes"):
|
||||
self.modes = ""
|
||||
chanmodes = list(str(self.modes))
|
||||
|
@ -1413,8 +1414,8 @@ class PesterMemo(PesterConvo):
|
|||
|
||||
@QtCore.pyqtSlot(QString, QString)
|
||||
def modesUpdated(self, channel, modes):
|
||||
c = str(channel)
|
||||
if c.lower() == self.channel.lower():
|
||||
PchumLog.debug(f"modesUpdated(%s, %s)", channel, modes)
|
||||
if channel.lower() == self.channel.lower():
|
||||
self.updateChanModes(modes, None)
|
||||
|
||||
@QtCore.pyqtSlot(QString)
|
||||
|
@ -1871,9 +1872,7 @@ class PesterMemo(PesterConvo):
|
|||
self, "Ban User", "Enter the reason you are banning this user (optional):"
|
||||
)
|
||||
if ok:
|
||||
self.mainwindow.kickUser.emit(
|
||||
"{}:{}".format(currentHandle, reason), self.channel
|
||||
)
|
||||
self.mainwindow.kickUser.emit(currentHandle, reason, self.channel)
|
||||
|
||||
@QtCore.pyqtSlot()
|
||||
def opSelectedUser(self):
|
||||
|
|
10
menus.py
10
menus.py
|
@ -576,7 +576,7 @@ class PesterQuirkTypes(QtWidgets.QDialog):
|
|||
|
||||
if quirk:
|
||||
types = ["prefix", "suffix", "replace", "regexp", "random", "spelling"]
|
||||
for (i, r) in enumerate(self.radios):
|
||||
for i, r in enumerate(self.radios):
|
||||
if i == types.index(quirk.quirk.type):
|
||||
r.setChecked(True)
|
||||
self.changePage(types.index(quirk.quirk.type) + 1)
|
||||
|
@ -648,7 +648,7 @@ class PesterQuirkTypes(QtWidgets.QDialog):
|
|||
return
|
||||
cur = self.pages.currentIndex()
|
||||
if cur == 0:
|
||||
for (i, r) in enumerate(self.radios):
|
||||
for i, r in enumerate(self.radios):
|
||||
if r.isChecked():
|
||||
self.changePage(i + 1)
|
||||
else:
|
||||
|
@ -933,7 +933,7 @@ class PesterChooseTheme(QtWidgets.QDialog):
|
|||
|
||||
avail_themes = config.availableThemes()
|
||||
self.themeBox = QtWidgets.QComboBox(self)
|
||||
for (i, t) in enumerate(avail_themes):
|
||||
for i, t in enumerate(avail_themes):
|
||||
self.themeBox.addItem(t)
|
||||
if t == theme.name:
|
||||
self.themeBox.setCurrentIndex(i)
|
||||
|
@ -1416,7 +1416,7 @@ class PesterOptions(QtWidgets.QDialog):
|
|||
avail_themes = self.config.availableThemes()
|
||||
self.themeBox = QtWidgets.QComboBox(self)
|
||||
notheme = theme.name not in avail_themes
|
||||
for (i, t) in enumerate(avail_themes):
|
||||
for i, t in enumerate(avail_themes):
|
||||
self.themeBox.addItem(t)
|
||||
if (not notheme and t == theme.name) or (notheme and t == "pesterchum"):
|
||||
self.themeBox.setCurrentIndex(i)
|
||||
|
@ -1454,7 +1454,7 @@ class PesterOptions(QtWidgets.QDialog):
|
|||
types = self.parent().tm.availableTypes()
|
||||
cur = self.parent().tm.currentType()
|
||||
self.notifyOptions.addItems(types)
|
||||
for (i, t) in enumerate(types):
|
||||
for i, t in enumerate(types):
|
||||
if t == cur:
|
||||
self.notifyOptions.setCurrentIndex(i)
|
||||
break
|
||||
|
|
|
@ -7,8 +7,8 @@ kbloc = [
|
|||
[x for x in "zxcvbnm,.>/?"],
|
||||
]
|
||||
kbdict = {}
|
||||
for (i, l) in enumerate(kbloc):
|
||||
for (j, k) in enumerate(l):
|
||||
for i, l in enumerate(kbloc):
|
||||
for j, k in enumerate(l):
|
||||
kbdict[k] = (i, j)
|
||||
|
||||
sounddict = {
|
||||
|
|
6
mood.py
6
mood.py
|
@ -61,11 +61,15 @@ class Mood:
|
|||
}
|
||||
|
||||
def __init__(self, mood):
|
||||
if type(mood) is int:
|
||||
if isinstance(mood, int):
|
||||
self.mood = mood
|
||||
else:
|
||||
self.mood = self.moods.index(mood)
|
||||
|
||||
def value_str(self):
|
||||
"""Return mood index as str."""
|
||||
return str(self.mood)
|
||||
|
||||
def value(self):
|
||||
return self.mood
|
||||
|
||||
|
|
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,212 +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 logging
|
||||
import inspect
|
||||
|
||||
from oyoyo import helpers
|
||||
from oyoyo.parse import parse_nick
|
||||
|
||||
PchumLog = logging.getLogger("pchumLogger")
|
||||
|
||||
|
||||
def protected(func):
|
||||
"""decorator to protect functions from being called"""
|
||||
func.protected = True
|
||||
return func
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
def __init__(self, cmd):
|
||||
self.cmd = cmd
|
||||
|
||||
|
||||
class NoSuchCommandError(CommandError):
|
||||
def __str__(self):
|
||||
return 'No such command "%s"' % ".".join(self.cmd)
|
||||
|
||||
|
||||
class ProtectedCommandError(CommandError):
|
||||
def __str__(self):
|
||||
return 'Command "%s" is protected' % ".".join(self.cmd)
|
||||
|
||||
|
||||
class CommandHandler:
|
||||
"""The most basic CommandHandler"""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@protected
|
||||
def get(self, in_command_parts):
|
||||
PchumLog.debug("in_command_parts: %s" % in_command_parts)
|
||||
""" finds a command
|
||||
commands may be dotted. each command part is checked that it does
|
||||
not start with and underscore and does not have an attribute
|
||||
"protected". if either of these is true, ProtectedCommandError
|
||||
is raised.
|
||||
its possible to pass both "command.sub.func" and
|
||||
["command", "sub", "func"].
|
||||
"""
|
||||
if isinstance(in_command_parts, (str, bytes)):
|
||||
in_command_parts = in_command_parts.split(".")
|
||||
command_parts = in_command_parts[:]
|
||||
|
||||
p = self
|
||||
while command_parts:
|
||||
cmd = command_parts.pop(0)
|
||||
if cmd.startswith("_"):
|
||||
raise ProtectedCommandError(in_command_parts)
|
||||
|
||||
try:
|
||||
f = getattr(p, cmd)
|
||||
except AttributeError:
|
||||
raise NoSuchCommandError(in_command_parts)
|
||||
|
||||
if hasattr(f, "protected"):
|
||||
raise ProtectedCommandError(in_command_parts)
|
||||
|
||||
if isinstance(f, CommandHandler) and command_parts:
|
||||
return f.get(command_parts)
|
||||
p = f
|
||||
|
||||
return f
|
||||
|
||||
@protected
|
||||
def run(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()))
|
||||
|
||||
try:
|
||||
f = self.get(command)
|
||||
except NoSuchCommandError as e:
|
||||
PchumLog.info(e)
|
||||
self.__unhandled__(command, *args)
|
||||
return
|
||||
|
||||
PchumLog.debug("f %s" % f)
|
||||
|
||||
try:
|
||||
f(*args)
|
||||
except TypeError as e:
|
||||
PchumLog.info(
|
||||
"Failed to pass command, did the server pass an unsupported paramater? "
|
||||
+ str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
# logging.info("Failed to pass command, %s" % str(e))
|
||||
PchumLog.exception("Failed to pass command")
|
||||
|
||||
@protected
|
||||
def __unhandled__(self, cmd, *args):
|
||||
"""The default handler for commands. Override this method to
|
||||
apply custom behavior (example, printing) unhandled commands.
|
||||
"""
|
||||
PchumLog.debug("unhandled command {}({})".format(cmd, args))
|
||||
|
||||
|
||||
class DefaultCommandHandler(CommandHandler):
|
||||
"""CommandHandler that provides methods for the normal operation of IRC.
|
||||
If you want your bot to properly respond to pings, etc, you should subclass this.
|
||||
"""
|
||||
|
||||
def ping(self, prefix, server):
|
||||
self.client.send("PONG", server)
|
||||
|
||||
|
||||
class DefaultBotCommandHandler(CommandHandler):
|
||||
"""default command handler for bots. methods/attributes are made
|
||||
available as commands"""
|
||||
|
||||
@protected
|
||||
def getVisibleCommands(self, obj=None):
|
||||
test = (
|
||||
lambda x: isinstance(x, CommandHandler)
|
||||
or inspect.ismethod(x)
|
||||
or inspect.isfunction(x)
|
||||
)
|
||||
members = inspect.getmembers(obj or self, test)
|
||||
return [
|
||||
m
|
||||
for m, _ in members
|
||||
if (not m.startswith("_") and not hasattr(getattr(obj, m), "protected"))
|
||||
]
|
||||
|
||||
def help(self, sender, dest, arg=None):
|
||||
"""list all available commands or get help on a specific command"""
|
||||
PchumLog.info("help sender={} dest={} arg={}".format(sender, dest, arg))
|
||||
if not arg:
|
||||
commands = self.getVisibleCommands()
|
||||
commands.sort()
|
||||
helpers.msg(
|
||||
self.client, dest, "available commands: %s" % " ".join(commands)
|
||||
)
|
||||
else:
|
||||
try:
|
||||
f = self.get(arg)
|
||||
except CommandError as e:
|
||||
helpers.msg(self.client, dest, str(e))
|
||||
return
|
||||
|
||||
doc = f.__doc__.strip() if f.__doc__ else "No help available"
|
||||
|
||||
if not inspect.ismethod(f):
|
||||
subcommands = self.getVisibleCommands(f)
|
||||
if subcommands:
|
||||
doc += " [sub commands: %s]" % " ".join(subcommands)
|
||||
|
||||
helpers.msg(self.client, dest, "{}: {}".format(arg, doc))
|
||||
|
||||
|
||||
class BotCommandHandler(DefaultCommandHandler):
|
||||
"""complete command handler for bots"""
|
||||
|
||||
def __init__(self, client, command_handler):
|
||||
DefaultCommandHandler.__init__(self, client)
|
||||
self.command_handler = command_handler
|
||||
|
||||
def privmsg(self, prefix, dest, msg):
|
||||
self.tryBotCommand(prefix, dest, msg)
|
||||
|
||||
@protected
|
||||
def tryBotCommand(self, prefix, dest, msg):
|
||||
"""tests a command to see if its a command for the bot, returns True
|
||||
and calls self.processBotCommand(cmd, sender) if its is.
|
||||
"""
|
||||
|
||||
PchumLog.debug("tryBotCommand('{}' '{}' '{}')".format(prefix, dest, msg))
|
||||
|
||||
if dest == self.client.nick:
|
||||
dest = parse_nick(prefix)[0]
|
||||
elif msg.startswith(self.client.nick):
|
||||
msg = msg[len(self.client.nick) + 1 :]
|
||||
else:
|
||||
return False
|
||||
|
||||
msg = msg.strip()
|
||||
|
||||
parts = msg.split(" ", 1)
|
||||
command = parts[0]
|
||||
arg = parts[1:]
|
||||
|
||||
try:
|
||||
self.command_handler.run(command, prefix, dest, *arg)
|
||||
except CommandError as e:
|
||||
helpers.msg(self.client, dest, str(e))
|
||||
return True
|
159
oyoyo/helpers.py
159
oyoyo/helpers.py
|
@ -1,159 +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.
|
||||
|
||||
""" contains helper functions for common irc commands """
|
||||
|
||||
import logging
|
||||
import random
|
||||
|
||||
PchumLog = logging.getLogger("pchumLogger")
|
||||
|
||||
|
||||
def msg(cli, user, msg):
|
||||
for line in msg.split("\n"):
|
||||
cli.send("PRIVMSG", user, ":%s" % line)
|
||||
|
||||
|
||||
def names(cli, *channels):
|
||||
tmp = __builtins__["list"](channels)
|
||||
msglist = []
|
||||
while len(tmp) > 0:
|
||||
msglist.append(tmp.pop())
|
||||
if len(",".join(msglist)) > 490:
|
||||
tmp.append(msglist.pop())
|
||||
cli.send("NAMES %s" % (",".join(msglist)))
|
||||
msglist = []
|
||||
if len(msglist) > 0:
|
||||
cli.send("NAMES %s" % (",".join(msglist)))
|
||||
|
||||
|
||||
def channel_list(cli):
|
||||
cli.send("LIST")
|
||||
|
||||
|
||||
def kick(cli, handle, channel, reason=""):
|
||||
cli.send("KICK {} {} {}".format(channel, handle, reason))
|
||||
|
||||
|
||||
def mode(cli, channel, mode, options=None):
|
||||
PchumLog.debug("mode = " + str(mode))
|
||||
PchumLog.debug("options = " + str(options))
|
||||
cmd = "MODE {} {}".format(channel, mode)
|
||||
if options:
|
||||
cmd += " %s" % (options)
|
||||
cli.send(cmd)
|
||||
|
||||
|
||||
def ctcp(cli, handle, cmd, msg=""):
|
||||
# Space breaks protocol if msg is absent
|
||||
if msg == "":
|
||||
cli.send("PRIVMSG", handle, "\x01%s\x01" % (cmd))
|
||||
else:
|
||||
cli.send("PRIVMSG", handle, "\x01{} {}\x01".format(cmd, msg))
|
||||
|
||||
|
||||
def ctcp_reply(cli, handle, cmd, msg=""):
|
||||
notice(cli, str(handle), "\x01{} {}\x01".format(cmd.upper(), msg))
|
||||
|
||||
|
||||
def metadata(cli, target, subcommand, *params):
|
||||
# IRC metadata draft specification
|
||||
# https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237
|
||||
cli.send("METADATA", target, subcommand, *params)
|
||||
|
||||
|
||||
def cap(cli, subcommand, *params):
|
||||
# Capability Negotiation
|
||||
# https://ircv3.net/specs/extensions/capability-negotiation.html
|
||||
cli.send("CAP", subcommand, *params)
|
||||
|
||||
|
||||
def msgrandom(cli, choices, dest, user=None):
|
||||
o = "%s: " % user if user else ""
|
||||
o += random.choice(choices)
|
||||
msg(cli, dest, o)
|
||||
|
||||
|
||||
def _makeMsgRandomFunc(choices):
|
||||
def func(cli, dest, user=None):
|
||||
msgrandom(cli, choices, dest, user)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
msgYes = _makeMsgRandomFunc(["yes", "alright", "ok"])
|
||||
msgOK = _makeMsgRandomFunc(["ok", "done"])
|
||||
msgNo = _makeMsgRandomFunc(["no", "no-way"])
|
||||
|
||||
|
||||
def ns(cli, *args):
|
||||
msg(cli, "NickServ", " ".join(args))
|
||||
|
||||
|
||||
def cs(cli, *args):
|
||||
msg(cli, "ChanServ", " ".join(args))
|
||||
|
||||
|
||||
def identify(cli, passwd, authuser="NickServ"):
|
||||
msg(cli, authuser, "IDENTIFY %s" % passwd)
|
||||
|
||||
|
||||
def quit(cli, msg):
|
||||
cli.send("QUIT %s" % (msg))
|
||||
|
||||
|
||||
def nick(cli, nick):
|
||||
cli.send("NICK", nick)
|
||||
|
||||
|
||||
def user(cli, username, realname):
|
||||
cli.send("USER", username, "0", "*", ":" + realname)
|
||||
|
||||
|
||||
def join(cli, channel):
|
||||
"""Protocol potentially allows multiple channels or keys."""
|
||||
cli.send("JOIN", channel)
|
||||
|
||||
|
||||
def part(cli, channel):
|
||||
cli.send("PART", channel)
|
||||
|
||||
|
||||
def notice(cli, target, text):
|
||||
cli.send("NOTICE", target, text)
|
||||
|
||||
|
||||
def invite(cli, nick, channel):
|
||||
cli.send("INVITE", nick, channel)
|
||||
|
||||
|
||||
def _addNumerics():
|
||||
import sys
|
||||
from oyoyo import ircevents
|
||||
|
||||
def numericcmd(cmd_num, cmd_name):
|
||||
def f(cli, *args):
|
||||
cli.send(cmd_num, *args)
|
||||
|
||||
return f
|
||||
|
||||
m = sys.modules[__name__]
|
||||
for num, name in ircevents.numeric_events.items():
|
||||
setattr(m, name, numericcmd(num, name))
|
||||
|
||||
|
||||
_addNumerics()
|
|
@ -1,232 +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.
|
||||
|
||||
# taken from python irclib.. who took it from...
|
||||
# Numeric table mostly stolen from the Perl IRC module (Net::IRC).
|
||||
numeric_events = {
|
||||
"001": "welcome",
|
||||
"002": "yourhost",
|
||||
"003": "created",
|
||||
"004": "myinfo",
|
||||
"005": "featurelist", # XXX
|
||||
"010": "toomanypeeps",
|
||||
"200": "tracelink",
|
||||
"201": "traceconnecting",
|
||||
"202": "tracehandshake",
|
||||
"203": "traceunknown",
|
||||
"204": "traceoperator",
|
||||
"205": "traceuser",
|
||||
"206": "traceserver",
|
||||
"207": "traceservice",
|
||||
"208": "tracenewtype",
|
||||
"209": "traceclass",
|
||||
"210": "tracereconnect",
|
||||
"211": "statslinkinfo",
|
||||
"212": "statscommands",
|
||||
"213": "statscline",
|
||||
"214": "statsnline",
|
||||
"215": "statsiline",
|
||||
"216": "statskline",
|
||||
"217": "statsqline",
|
||||
"218": "statsyline",
|
||||
"219": "endofstats",
|
||||
"221": "umodeis",
|
||||
"231": "serviceinfo",
|
||||
"232": "endofservices",
|
||||
"233": "service",
|
||||
"234": "servlist",
|
||||
"235": "servlistend",
|
||||
"241": "statslline",
|
||||
"242": "statsuptime",
|
||||
"243": "statsoline",
|
||||
"244": "statshline",
|
||||
"250": "luserconns",
|
||||
"251": "luserclient",
|
||||
"252": "luserop",
|
||||
"253": "luserunknown",
|
||||
"254": "luserchannels",
|
||||
"255": "luserme",
|
||||
"256": "adminme",
|
||||
"257": "adminloc1",
|
||||
"258": "adminloc2",
|
||||
"259": "adminemail",
|
||||
"261": "tracelog",
|
||||
"262": "endoftrace",
|
||||
"263": "tryagain",
|
||||
"265": "n_local",
|
||||
"266": "n_global",
|
||||
"300": "none",
|
||||
"301": "away",
|
||||
"302": "userhost",
|
||||
"303": "ison",
|
||||
"305": "unaway",
|
||||
"306": "nowaway",
|
||||
"311": "whoisuser",
|
||||
"312": "whoisserver",
|
||||
"313": "whoisoperator",
|
||||
"314": "whowasuser",
|
||||
"315": "endofwho",
|
||||
"316": "whoischanop",
|
||||
"317": "whoisidle",
|
||||
"318": "endofwhois",
|
||||
"319": "whoischannels",
|
||||
"321": "liststart",
|
||||
"322": "list",
|
||||
"323": "listend",
|
||||
"324": "channelmodeis",
|
||||
"329": "channelcreate",
|
||||
"331": "notopic",
|
||||
"332": "currenttopic",
|
||||
"333": "topicinfo",
|
||||
"341": "inviting",
|
||||
"342": "summoning",
|
||||
"346": "invitelist",
|
||||
"347": "endofinvitelist",
|
||||
"348": "exceptlist",
|
||||
"349": "endofexceptlist",
|
||||
"351": "version",
|
||||
"352": "whoreply",
|
||||
"353": "namreply",
|
||||
"361": "killdone",
|
||||
"362": "closing",
|
||||
"363": "closeend",
|
||||
"364": "links",
|
||||
"365": "endoflinks",
|
||||
"366": "endofnames",
|
||||
"367": "banlist",
|
||||
"368": "endofbanlist",
|
||||
"369": "endofwhowas",
|
||||
"371": "info",
|
||||
"372": "motd",
|
||||
"373": "infostart",
|
||||
"374": "endofinfo",
|
||||
"375": "motdstart",
|
||||
"376": "endofmotd",
|
||||
"377": "motd2", # 1997-10-16 -- tkil
|
||||
"381": "youreoper",
|
||||
"382": "rehashing",
|
||||
"384": "myportis",
|
||||
"391": "time",
|
||||
"392": "usersstart",
|
||||
"393": "users",
|
||||
"394": "endofusers",
|
||||
"395": "nousers",
|
||||
"396": "hosthidden",
|
||||
"401": "nosuchnick",
|
||||
"402": "nosuchserver",
|
||||
"403": "nosuchchannel",
|
||||
"404": "cannotsendtochan",
|
||||
"405": "toomanychannels",
|
||||
"406": "wasnosuchnick",
|
||||
"407": "toomanytargets",
|
||||
"409": "noorigin",
|
||||
"411": "norecipient",
|
||||
"412": "notexttosend",
|
||||
"413": "notoplevel",
|
||||
"414": "wildtoplevel",
|
||||
"421": "unknowncommand",
|
||||
"422": "nomotd",
|
||||
"423": "noadmininfo",
|
||||
"424": "fileerror",
|
||||
"431": "nonicknamegiven",
|
||||
"432": "erroneusnickname", # Thiss iz how its speld in thee RFC.
|
||||
"433": "nicknameinuse",
|
||||
"436": "nickcollision",
|
||||
"437": "unavailresource", # "Nick temporally unavailable"
|
||||
"441": "usernotinchannel",
|
||||
"442": "notonchannel",
|
||||
"443": "useronchannel",
|
||||
"444": "nologin",
|
||||
"445": "summondisabled",
|
||||
"446": "usersdisabled",
|
||||
"451": "notregistered",
|
||||
"461": "needmoreparams",
|
||||
"462": "alreadyregistered",
|
||||
"463": "nopermforhost",
|
||||
"464": "passwdmismatch",
|
||||
"465": "yourebannedcreep", # I love this one...
|
||||
"466": "youwillbebanned",
|
||||
"467": "keyset",
|
||||
"471": "channelisfull",
|
||||
"472": "unknownmode",
|
||||
"473": "inviteonlychan",
|
||||
"474": "bannedfromchan",
|
||||
"475": "badchannelkey",
|
||||
"476": "badchanmask",
|
||||
"477": "nochanmodes", # "Channel doesn't support modes"
|
||||
"478": "banlistfull",
|
||||
"481": "noprivileges",
|
||||
"482": "chanoprivsneeded",
|
||||
"483": "cantkillserver",
|
||||
"484": "restricted", # Connection is restricted
|
||||
"485": "uniqopprivsneeded",
|
||||
"491": "nooperhost",
|
||||
"492": "noservicehost",
|
||||
"501": "umodeunknownflag",
|
||||
"502": "usersdontmatch",
|
||||
}
|
||||
|
||||
# Unrealircd extras
|
||||
unrealircd_events = {
|
||||
"448": "forbiddenchannel",
|
||||
}
|
||||
numeric_events.update(unrealircd_events)
|
||||
|
||||
# IRC metadata draft specification
|
||||
# https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237
|
||||
metadata_numeric_events = {
|
||||
"761": "keyvalue",
|
||||
"762": "metadataend",
|
||||
"766": "nomatchingkey",
|
||||
"768": "keynotset",
|
||||
"769": "keynopermission",
|
||||
"770": "metadatasubok",
|
||||
}
|
||||
numeric_events.update(metadata_numeric_events)
|
||||
|
||||
generated_events = [
|
||||
# Generated events
|
||||
"dcc_connect",
|
||||
"dcc_disconnect",
|
||||
"dccmsg",
|
||||
"disconnect",
|
||||
"ctcp",
|
||||
"ctcpreply",
|
||||
]
|
||||
|
||||
protocol_events = [
|
||||
# IRC protocol events
|
||||
"error",
|
||||
"join",
|
||||
"kick",
|
||||
"mode",
|
||||
"part",
|
||||
"ping",
|
||||
"privmsg",
|
||||
"privnotice",
|
||||
"pubmsg",
|
||||
"pubnotice",
|
||||
"quit",
|
||||
"invite",
|
||||
"pong",
|
||||
"nick", # We can get svsnicked
|
||||
"metadata", # Metadata specification
|
||||
"tagmsg", # IRCv3 message tags extension
|
||||
"cap", # IRCv3 Client Capability Negotiation
|
||||
]
|
||||
|
||||
all_events = generated_events + protocol_events + list(numeric_events.values())
|
121
oyoyo/parse.py
121
oyoyo/parse.py
|
@ -1,121 +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 logging
|
||||
|
||||
from oyoyo.ircevents import numeric_events
|
||||
|
||||
PchumLog = logging.getLogger("pchumLogger")
|
||||
|
||||
|
||||
def parse_raw_irc_command(element):
|
||||
"""
|
||||
This function parses a raw irc command and returns a tuple
|
||||
of (prefix, command, args).
|
||||
The following is a psuedo BNF of the input text:
|
||||
|
||||
<message> ::= [':' <prefix> <SPACE> ] <command> <params> <crlf>
|
||||
<prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
|
||||
<command> ::= <letter> { <letter> } | <number> <number> <number>
|
||||
<SPACE> ::= ' ' { ' ' }
|
||||
<params> ::= <SPACE> [ ':' <trailing> | <middle> <params> ]
|
||||
|
||||
<middle> ::= <Any *non-empty* sequence of octets not including SPACE
|
||||
or NUL or CR or LF, the first of which may not be ':'>
|
||||
<trailing> ::= <Any, possibly *empty*, sequence of octets not including
|
||||
NUL or CR or LF>
|
||||
|
||||
<crlf> ::= CR LF
|
||||
"""
|
||||
"""
|
||||
When message-tags are enabled, the message pseudo-BNF,
|
||||
as defined in RFC 1459, section 2.3.1 is extended as follows:
|
||||
|
||||
<message> ::= ['@' <tags> <SPACE>] [':' <prefix> <SPACE> ] <command> [params] <crlf>
|
||||
<tags> ::= <tag> [';' <tag>]*
|
||||
<tag> ::= <key> ['=' <escaped_value>]
|
||||
<key> ::= [ <client_prefix> ] [ <vendor> '/' ] <key_name>
|
||||
<client_prefix> ::= '+'
|
||||
<key_name> ::= <non-empty sequence of ascii letters, digits, hyphens ('-')>
|
||||
<escaped_value> ::= <sequence of zero or more utf8 characters except NUL, CR, LF, semicolon (`;`) and SPACE>
|
||||
<vendor> ::= <host>
|
||||
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
element = element.decode("utf-8")
|
||||
except UnicodeDecodeError as e:
|
||||
PchumLog.debug("utf-8 error %s" % str(e))
|
||||
element = element.decode("latin-1", "replace")
|
||||
|
||||
parts = element.strip().split(" ")
|
||||
if parts[0].startswith(":"):
|
||||
tags = None
|
||||
prefix = parts[0][1:]
|
||||
command = parts[1]
|
||||
args = parts[2:]
|
||||
elif parts[0].startswith("@"):
|
||||
# Message tag
|
||||
tags = parts[0]
|
||||
prefix = parts[1][1:]
|
||||
command = parts[2]
|
||||
args = parts[3:]
|
||||
else:
|
||||
tags = None
|
||||
prefix = None
|
||||
command = parts[0]
|
||||
args = parts[1:]
|
||||
|
||||
if command.isdigit():
|
||||
try:
|
||||
command = numeric_events[command]
|
||||
except KeyError:
|
||||
PchumLog.info("unknown numeric event %s" % command)
|
||||
command = command.lower()
|
||||
|
||||
if args[0].startswith(":"):
|
||||
args = [" ".join(args)[1:]]
|
||||
else:
|
||||
for idx, arg in enumerate(args):
|
||||
if arg.startswith(":"):
|
||||
args = args[:idx] + [" ".join(args[idx:])[1:]]
|
||||
break
|
||||
|
||||
return (tags, prefix, command, args)
|
||||
|
||||
|
||||
def parse_nick(name):
|
||||
"""parse a nickname and return a tuple of (nick, mode, user, host)
|
||||
|
||||
<nick> [ '!' [<mode> = ] <user> ] [ '@' <host> ]
|
||||
"""
|
||||
|
||||
try:
|
||||
nick, rest = name.split("!")
|
||||
except ValueError:
|
||||
return (name, None, None, None)
|
||||
try:
|
||||
mode, rest = rest.split("=")
|
||||
except ValueError:
|
||||
mode, rest = None, rest
|
||||
try:
|
||||
user, host = rest.split("@")
|
||||
except ValueError:
|
||||
return (name, mode, rest, None)
|
||||
|
||||
return (name, mode, user, host)
|
|
@ -1,125 +0,0 @@
|
|||
# NickServ basic functions
|
||||
_nickservfuncs = (
|
||||
"register",
|
||||
"group",
|
||||
"glist",
|
||||
"identify",
|
||||
"access",
|
||||
"drop",
|
||||
"recover",
|
||||
"release",
|
||||
"sendpass",
|
||||
"ghost",
|
||||
"alist",
|
||||
"info",
|
||||
"list",
|
||||
"logout",
|
||||
"status",
|
||||
"update",
|
||||
)
|
||||
|
||||
# NickServ SET functions
|
||||
_nickservsetfuncs = (
|
||||
"display",
|
||||
"password",
|
||||
"language",
|
||||
"url",
|
||||
"email",
|
||||
"icq",
|
||||
"greet",
|
||||
"kill",
|
||||
"secure",
|
||||
"private",
|
||||
"hide",
|
||||
"msg",
|
||||
"autoop",
|
||||
)
|
||||
|
||||
# ChanServ basic functions
|
||||
_chanservfuncs = (
|
||||
"register",
|
||||
"identify",
|
||||
"sop",
|
||||
"aop",
|
||||
"hop",
|
||||
"vop",
|
||||
"access",
|
||||
"levels",
|
||||
"akick",
|
||||
"drop",
|
||||
"sendpass",
|
||||
"ban",
|
||||
"unban",
|
||||
"clear",
|
||||
"owner",
|
||||
"deowner",
|
||||
"protect",
|
||||
"deprotect",
|
||||
"op",
|
||||
"deop",
|
||||
"halfop",
|
||||
"dehalfop",
|
||||
"voice",
|
||||
"devoice",
|
||||
"getkey",
|
||||
"invite",
|
||||
"kick",
|
||||
"list",
|
||||
"logout",
|
||||
"topic",
|
||||
"info",
|
||||
"appendtopic",
|
||||
"enforce",
|
||||
)
|
||||
|
||||
_chanservsetfuncs = (
|
||||
"founder",
|
||||
"successor",
|
||||
"password",
|
||||
"desc",
|
||||
"url",
|
||||
"email",
|
||||
"entrymsg",
|
||||
"bantype",
|
||||
"mlock",
|
||||
"keeptopic",
|
||||
"opnotice",
|
||||
"peace",
|
||||
"private",
|
||||
"restricted",
|
||||
"secure",
|
||||
"secureops",
|
||||
"securefounder",
|
||||
"signkick",
|
||||
"topiclock",
|
||||
"xop",
|
||||
)
|
||||
|
||||
|
||||
def _addServ(serv, funcs, prefix=""):
|
||||
def simplecmd(cmd_name):
|
||||
if prefix:
|
||||
cmd_name = prefix.upper() + " " + cmd_name
|
||||
|
||||
def f(cli, *args):
|
||||
print(cmd_name, " ".join(args))
|
||||
# cli.send(cmd_name, serv.name, *args)
|
||||
|
||||
return f
|
||||
|
||||
for t in funcs:
|
||||
setattr(serv, t, simplecmd(t.upper()))
|
||||
|
||||
|
||||
class NickServ:
|
||||
def __init__(self, nick="NickServ"):
|
||||
self.name = nick
|
||||
_addServ(self, _nickservfuncs)
|
||||
_addServ(self, _nickservsetfuncs, "set")
|
||||
|
||||
|
||||
class ChanServ:
|
||||
def __init__(self, nick="ChanServ"):
|
||||
self.name = nick
|
||||
_addServ(self, _chanservfuncs)
|
||||
_addServ(self, _chanservsetfuncs, "set")
|
|
@ -64,9 +64,9 @@ def reloadQuirkFunctions():
|
|||
def lexer(string, objlist):
|
||||
"""objlist is a list: [(objecttype, re),...] list is in order of preference"""
|
||||
stringlist = [string]
|
||||
for (oType, regexp) in objlist:
|
||||
for oType, regexp in objlist:
|
||||
newstringlist = []
|
||||
for (stri, s) in enumerate(stringlist):
|
||||
for stri, s in enumerate(stringlist):
|
||||
if type(s) not in [str, str]:
|
||||
newstringlist.append(s)
|
||||
continue
|
||||
|
@ -339,7 +339,7 @@ def convertTags(lexed, format="html"):
|
|||
lexed = lexMessage(lexed)
|
||||
escaped = ""
|
||||
# firststr = True
|
||||
for (i, o) in enumerate(lexed):
|
||||
for i, o in enumerate(lexed):
|
||||
if type(o) in [str, str]:
|
||||
if format == "html":
|
||||
escaped += (
|
||||
|
@ -429,9 +429,7 @@ def kxsplitMsg(lexed, ctx, fmt="pchum", maxlen=None, debug=False):
|
|||
curlen = 0
|
||||
# Maximum number of characters *to* use.
|
||||
if not maxlen:
|
||||
maxlen = _max_msg_len(
|
||||
None, None, ctx.mainwindow.profile().handle, ctx.mainwindow.irc.cli.realname
|
||||
)
|
||||
maxlen = _max_msg_len(None, None, ctx.mainwindow.profile().handle, "pcc31")
|
||||
elif maxlen < 0:
|
||||
# Subtract the (negative) length, giving us less leeway in this
|
||||
# function.
|
||||
|
@ -440,7 +438,7 @@ def kxsplitMsg(lexed, ctx, fmt="pchum", maxlen=None, debug=False):
|
|||
None,
|
||||
None,
|
||||
ctx.mainwindow.profile().handle,
|
||||
ctx.mainwindow.irc.cli.realname,
|
||||
"pcc31",
|
||||
)
|
||||
+ maxlen
|
||||
)
|
||||
|
@ -805,11 +803,9 @@ def kxhandleInput(ctx, text=None, flavor=None):
|
|||
# We'll use those later.
|
||||
|
||||
# Split the messages so we don't go over the buffer and lose text.
|
||||
maxlen = _max_msg_len(
|
||||
None, None, ctx.mainwindow.profile().handle, ctx.mainwindow.irc.cli.realname
|
||||
)
|
||||
maxlen = _max_msg_len(None, None, ctx.mainwindow.profile().handle, "pcc31")
|
||||
# ctx.mainwindow.profile().handle ==> Get handle
|
||||
# ctx.mainwindow.irc.cli.realname ==> Get ident (Same as realname in this case.)
|
||||
# "pcc31" ==> Get ident (Same as realname in this case.)
|
||||
# Since we have to do some post-processing, we need to adjust the maximum
|
||||
# length we can use.
|
||||
if flavor == "convo":
|
||||
|
|
184
pesterchum.py
184
pesterchum.py
|
@ -2971,7 +2971,7 @@ class PesterWindow(MovingWindow):
|
|||
# Tell everyone we're in a chat with that we just went idle.
|
||||
sysColor = QtGui.QColor(self.theme["convo/systemMsgColor"])
|
||||
verb = self.theme["convo/text/idle"]
|
||||
for (h, convo) in self.convos.items():
|
||||
for h, convo in self.convos.items():
|
||||
# karxi: There's an irritating issue here involving a lack of
|
||||
# consideration for case-sensitivity.
|
||||
# This fix is a little sloppy, and I need to look into what it
|
||||
|
@ -3051,7 +3051,6 @@ class PesterWindow(MovingWindow):
|
|||
|
||||
@QtCore.pyqtSlot()
|
||||
def joinSelectedMemo(self):
|
||||
|
||||
time = str(self.memochooser.timeinput.text())
|
||||
secret = self.memochooser.secretChannel.isChecked()
|
||||
invite = self.memochooser.inviteChannel.isChecked()
|
||||
|
@ -3246,7 +3245,7 @@ class PesterWindow(MovingWindow):
|
|||
# combine
|
||||
self.createTabWindow()
|
||||
newconvos = {}
|
||||
for (h, c) in self.convos.items():
|
||||
for h, c in self.convos.items():
|
||||
c.setParent(self.tabconvo)
|
||||
self.tabconvo.addChat(c)
|
||||
self.tabconvo.show()
|
||||
|
@ -3276,7 +3275,7 @@ class PesterWindow(MovingWindow):
|
|||
# combine
|
||||
newmemos = {}
|
||||
self.createMemoTabWindow()
|
||||
for (h, m) in self.memos.items():
|
||||
for h, m in self.memos.items():
|
||||
m.setParent(self.tabmemo)
|
||||
self.tabmemo.addChat(m)
|
||||
self.tabmemo.show()
|
||||
|
@ -3806,9 +3805,10 @@ class PesterWindow(MovingWindow):
|
|||
"port": int(
|
||||
server_and_port[1]
|
||||
), # to make sure port is a valid integer, and raise an exception if it cannot be converted.
|
||||
"pass": self.auth_pass_qline.text(),
|
||||
"TLS": self.TLS_checkbox.isChecked(),
|
||||
}
|
||||
PchumLog.info("server: " + str(server))
|
||||
PchumLog.info("server: %s", server)
|
||||
except:
|
||||
msgbox = QtWidgets.QMessageBox()
|
||||
msgbox.setStyleSheet(
|
||||
|
@ -3951,12 +3951,24 @@ class PesterWindow(MovingWindow):
|
|||
layout.addWidget(cancel)
|
||||
layout.addWidget(ok)
|
||||
main_layout = QtWidgets.QVBoxLayout()
|
||||
|
||||
nep_prompt = QtWidgets.QLabel(
|
||||
":33 < Please put in the server's address in the format HOSTNAME:PORT\n:33 < Fur example, irc.pesterchum.xyz:6697"
|
||||
)
|
||||
nep_prompt.setStyleSheet("QLabel { color: #416600; font-weight: bold;}")
|
||||
|
||||
auth_pass_prompt = QtWidgets.QLabel(":33 < type the password!! (optional)")
|
||||
auth_pass_prompt.setStyleSheet(
|
||||
"QLabel { color: #416600; font-weight: bold;}"
|
||||
)
|
||||
|
||||
self.auth_pass_qline = QtWidgets.QLineEdit(self)
|
||||
self.auth_pass_qline.setMinimumWidth(200)
|
||||
|
||||
main_layout.addWidget(nep_prompt)
|
||||
main_layout.addWidget(self.customServerPrompt_qline)
|
||||
main_layout.addWidget(auth_pass_prompt)
|
||||
main_layout.addWidget(self.auth_pass_qline)
|
||||
main_layout.addLayout(TLS_layout)
|
||||
main_layout.addLayout(layout)
|
||||
|
||||
|
@ -4049,6 +4061,11 @@ class PesterWindow(MovingWindow):
|
|||
|
||||
try:
|
||||
selected_entry = self.serverBox.currentIndex()
|
||||
PchumLog.debug(
|
||||
"'%s' == '%s'",
|
||||
server_obj[selected_entry]["server"],
|
||||
self.serverBox.currentText(),
|
||||
)
|
||||
assert (
|
||||
server_obj[selected_entry]["server"] == self.serverBox.currentText()
|
||||
)
|
||||
|
@ -4061,9 +4078,13 @@ class PesterWindow(MovingWindow):
|
|||
|
||||
try:
|
||||
with open(_datadir + "server.json", "w") as server_file:
|
||||
password = ""
|
||||
if "pass" in server_obj[selected_entry]:
|
||||
password = server_obj[selected_entry]["pass"]
|
||||
json_server_file = {
|
||||
"server": server_obj[selected_entry]["server"],
|
||||
"port": server_obj[selected_entry]["port"],
|
||||
"pass": password,
|
||||
"TLS": server_obj[selected_entry]["TLS"],
|
||||
}
|
||||
server_file.write(json.dumps(json_server_file, indent=4))
|
||||
|
@ -4090,12 +4111,13 @@ class PesterWindow(MovingWindow):
|
|||
for i in range(len(server_obj)):
|
||||
server_list_items.append(server_obj[i]["server"])
|
||||
except:
|
||||
PchumLog.exception("")
|
||||
if not self.chooseServerAskedToReset:
|
||||
self.chooseServerAskedToReset = True
|
||||
self.resetServerlist()
|
||||
return 1
|
||||
|
||||
PchumLog.info("server_list_items: " + str(server_list_items))
|
||||
PchumLog.info("server_list_items: %s", server_list_items)
|
||||
|
||||
# Widget 1
|
||||
self.chooseServerWidged = QtWidgets.QDialog()
|
||||
|
@ -4188,7 +4210,7 @@ class PesterWindow(MovingWindow):
|
|||
trayIconSignal = QtCore.pyqtSignal(int)
|
||||
blockedChum = QtCore.pyqtSignal("QString")
|
||||
unblockedChum = QtCore.pyqtSignal("QString")
|
||||
kickUser = QtCore.pyqtSignal("QString", "QString")
|
||||
kickUser = QtCore.pyqtSignal("QString", "QString", "QString")
|
||||
joinChannel = QtCore.pyqtSignal("QString")
|
||||
leftChannel = QtCore.pyqtSignal("QString")
|
||||
setChannelMode = QtCore.pyqtSignal("QString", "QString", "QString")
|
||||
|
@ -4293,7 +4315,7 @@ class MainProgram(QtCore.QObject):
|
|||
for k in Mood.moodcats:
|
||||
moodCategories[k] = moodMenu.addMenu(k.upper())
|
||||
self.moodactions = {}
|
||||
for (i, m) in enumerate(Mood.moods):
|
||||
for i, m in enumerate(Mood.moods):
|
||||
maction = QAction(m.upper(), self)
|
||||
mobj = PesterMoodAction(i, self.widget.moods.updateMood)
|
||||
maction.triggered.connect(mobj.updateMood)
|
||||
|
@ -4321,7 +4343,14 @@ class MainProgram(QtCore.QObject):
|
|||
self.attempts = 0
|
||||
|
||||
# but it's at least better than the way it was before.
|
||||
self.irc = PesterIRC(self.widget.config, self.widget)
|
||||
# FIXME: we should not pass widget here
|
||||
self.irc = PesterIRC(
|
||||
self.widget,
|
||||
self.widget.config.server(),
|
||||
self.widget.config.port(),
|
||||
self.widget.config.ssl(),
|
||||
password=self.widget.config.password(),
|
||||
)
|
||||
self.connectWidgets(self.irc, self.widget)
|
||||
|
||||
self.widget.passIRC(
|
||||
|
@ -4364,107 +4393,33 @@ class MainProgram(QtCore.QObject):
|
|||
def trayMessageClick(self):
|
||||
self.widget.config.set("traymsg", False)
|
||||
|
||||
widget2irc = [
|
||||
("sendMessage(QString, QString)", "sendMessage(QString, QString)"),
|
||||
("sendNotice(QString, QString)", "sendNotice(QString, QString)"),
|
||||
("sendCTCP(QString, QString)", "sendCTCP(QString, QString)"),
|
||||
("newConvoStarted(QString, bool)", "startConvo(QString, bool)"),
|
||||
("convoClosed(QString)", "endConvo(QString)"),
|
||||
("profileChanged()", "updateProfile()"),
|
||||
("moodRequest(PyQt_PyObject)", "getMood(PyQt_PyObject)"),
|
||||
("moodsRequest(PyQt_PyObject)", "getMoods(PyQt_PyObject)"),
|
||||
("moodUpdated()", "updateMood()"),
|
||||
("mycolorUpdated()", "updateColor()"),
|
||||
("blockedChum(QString)", "blockedChum(QString)"),
|
||||
("unblockedChum(QString)", "unblockedChum(QString)"),
|
||||
("requestNames(QString)", "requestNames(QString)"),
|
||||
("requestChannelList()", "requestChannelList()"),
|
||||
("joinChannel(QString)", "joinChannel(QString)"),
|
||||
("leftChannel(QString)", "leftChannel(QString)"),
|
||||
("kickUser(QString, QString)", "kickUser(QString, QString)"),
|
||||
(
|
||||
"setChannelMode(QString, QString, QString)",
|
||||
"setChannelMode(QString, QString, QString)",
|
||||
),
|
||||
("channelNames(QString)", "channelNames(QString)"),
|
||||
("inviteChum(QString, QString)", "inviteChum(QString, QString)"),
|
||||
("pingServer()", "pingServer()"),
|
||||
("setAway(bool)", "setAway(bool)"),
|
||||
("killSomeQuirks(QString, QString)", "killSomeQuirks(QString, QString)"),
|
||||
("disconnectIRC()", "disconnectIRC()"),
|
||||
]
|
||||
# IRC --> Main window
|
||||
irc2widget = [
|
||||
("connected()", "connected()"),
|
||||
(
|
||||
"moodUpdated(QString, PyQt_PyObject)",
|
||||
"updateMoodSlot(QString, PyQt_PyObject)",
|
||||
),
|
||||
(
|
||||
"colorUpdated(QString, QtGui.QColor)",
|
||||
"updateColorSlot(QString, QtGui.QColor)",
|
||||
),
|
||||
("messageReceived(QString, QString)", "deliverMessage(QString, QString)"),
|
||||
(
|
||||
"memoReceived(QString, QString, QString)",
|
||||
"deliverMemo(QString, QString, QString)",
|
||||
),
|
||||
("noticeReceived(QString, QString)", "deliverNotice(QString, QString)"),
|
||||
("inviteReceived(QString, QString)", "deliverInvite(QString, QString)"),
|
||||
("nickCollision(QString, QString)", "nickCollision(QString, QString)"),
|
||||
("getSvsnickedOn(QString, QString)", "getSvsnickedOn(QString, QString)"),
|
||||
("myHandleChanged(QString)", "myHandleChanged(QString)"),
|
||||
(
|
||||
"namesReceived(QString, PyQt_PyObject)",
|
||||
"updateNames(QString, PyQt_PyObject)",
|
||||
),
|
||||
(
|
||||
"userPresentUpdate(QString, QString, QString)",
|
||||
"userPresentUpdate(QString, QString, QString)",
|
||||
),
|
||||
("channelListReceived(PyQt_PyObject)", "updateChannelList(PyQt_PyObject)"),
|
||||
(
|
||||
"timeCommand(QString, QString, QString)",
|
||||
"timeCommand(QString, QString, QString)",
|
||||
),
|
||||
("chanInviteOnly(QString)", "chanInviteOnly(QString)"),
|
||||
("modesUpdated(QString, QString)", "modesUpdated(QString, QString)"),
|
||||
("cannotSendToChan(QString, QString)", "cannotSendToChan(QString, QString)"),
|
||||
("tooManyPeeps()", "tooManyPeeps()"),
|
||||
(
|
||||
"quirkDisable(QString, QString, QString)",
|
||||
"quirkDisable(QString, QString, QString)",
|
||||
),
|
||||
("forbiddenchannel(QString)", "forbiddenchannel(QString)"),
|
||||
]
|
||||
|
||||
def ircQtConnections(self, irc, widget):
|
||||
# IRC --> Main window
|
||||
return (
|
||||
(widget.sendMessage, irc.sendMessage),
|
||||
(widget.sendNotice, irc.sendNotice),
|
||||
(widget.sendCTCP, irc.sendCTCP),
|
||||
(widget.newConvoStarted, irc.startConvo),
|
||||
(widget.convoClosed, irc.endConvo),
|
||||
(widget.profileChanged, irc.updateProfile),
|
||||
(widget.moodRequest, irc.getMood),
|
||||
(widget.moodsRequest, irc.getMoods),
|
||||
(widget.moodUpdated, irc.updateMood),
|
||||
(widget.mycolorUpdated, irc.updateColor),
|
||||
(widget.blockedChum, irc.blockedChum),
|
||||
(widget.unblockedChum, irc.unblockedChum),
|
||||
(widget.requestNames, irc.requestNames),
|
||||
(widget.requestChannelList, irc.requestChannelList),
|
||||
(widget.joinChannel, irc.joinChannel),
|
||||
(widget.leftChannel, irc.leftChannel),
|
||||
(widget.kickUser, irc.kickUser),
|
||||
(widget.setChannelMode, irc.setChannelMode),
|
||||
(widget.channelNames, irc.channelNames),
|
||||
(widget.inviteChum, irc.inviteChum),
|
||||
(widget.pingServer, irc.pingServer),
|
||||
(widget.setAway, irc.setAway),
|
||||
(widget.killSomeQuirks, irc.killSomeQuirks),
|
||||
(widget.disconnectIRC, irc.disconnectIRC),
|
||||
(widget.sendMessage, irc.send_message),
|
||||
(widget.sendNotice, irc.send_notice),
|
||||
(widget.sendCTCP, irc.send_ctcp),
|
||||
(widget.newConvoStarted, irc.start_convo),
|
||||
(widget.convoClosed, irc.end_convo),
|
||||
(widget.profileChanged, irc.update_profile),
|
||||
(widget.moodRequest, irc.get_mood),
|
||||
(widget.moodsRequest, irc.get_moods),
|
||||
(widget.moodUpdated, irc.update_mood),
|
||||
(widget.mycolorUpdated, irc.update_color),
|
||||
(widget.blockedChum, irc.blocked_chum),
|
||||
(widget.unblockedChum, irc.unblocked_chum),
|
||||
(widget.requestNames, irc.request_names),
|
||||
(widget.requestChannelList, irc.request_channel_list),
|
||||
(widget.joinChannel, irc.join_channel),
|
||||
(widget.leftChannel, irc.left_channel),
|
||||
(widget.kickUser, irc.kick_user),
|
||||
(widget.setChannelMode, irc.set_channel_mode),
|
||||
(widget.channelNames, irc.channel_names),
|
||||
(widget.inviteChum, irc.invite_chum),
|
||||
(widget.pingServer, irc.ping_server),
|
||||
(widget.setAway, irc.set_away),
|
||||
(widget.killSomeQuirks, irc.kill_some_quirks),
|
||||
(widget.disconnectIRC, irc.disconnect_irc),
|
||||
# Main window --> IRC
|
||||
(irc.connected, widget.connected),
|
||||
(irc.askToConnect, widget.connectAnyway),
|
||||
|
@ -4484,9 +4439,7 @@ class MainProgram(QtCore.QObject):
|
|||
(irc.chanInviteOnly, widget.chanInviteOnly),
|
||||
(irc.modesUpdated, widget.modesUpdated),
|
||||
(irc.cannotSendToChan, widget.cannotSendToChan),
|
||||
(irc.forbiddenchannel, widget.forbiddenchannel),
|
||||
(irc.tooManyPeeps, widget.tooManyPeeps),
|
||||
(irc.quirkDisable, widget.quirkDisable),
|
||||
(irc.signal_forbiddenchannel, widget.forbiddenchannel),
|
||||
)
|
||||
|
||||
def connectWidgets(self, irc, widget):
|
||||
|
@ -4533,7 +4486,7 @@ class MainProgram(QtCore.QObject):
|
|||
self.widget.loadingscreen.tryAgain.connect(self.tryAgain)
|
||||
if (
|
||||
hasattr(self, "irc")
|
||||
and self.irc.registeredIRC
|
||||
and self.irc.registered_irc
|
||||
and not self.irc.unresponsive
|
||||
):
|
||||
return
|
||||
|
@ -4564,13 +4517,18 @@ class MainProgram(QtCore.QObject):
|
|||
def restartIRC(self, verify_hostname=True):
|
||||
if hasattr(self, "irc") and self.irc:
|
||||
self.disconnectWidgets(self.irc, self.widget)
|
||||
stop = self.irc.stopIRC
|
||||
stop = self.irc.stop_irc
|
||||
del self.irc
|
||||
else:
|
||||
stop = None
|
||||
if stop is None:
|
||||
self.irc = PesterIRC(
|
||||
self.widget.config, self.widget, verify_hostname=verify_hostname
|
||||
self.widget,
|
||||
self.widget.config.server(),
|
||||
self.widget.config.port(),
|
||||
self.widget.config.ssl(),
|
||||
password=self.widget.config.password(),
|
||||
verify_hostname=verify_hostname,
|
||||
)
|
||||
self.connectWidgets(self.irc, self.widget)
|
||||
self.irc.start()
|
||||
|
|
|
@ -27,6 +27,7 @@ class Color:
|
|||
# The threshold at which to consider two colors noticeably different, even
|
||||
# if only barely
|
||||
jnd = 2.3
|
||||
|
||||
# TODO: Either subclass (this is probably best) or add a .native_type; in
|
||||
# the case of the former, just make sure each type is geared towards using
|
||||
# a certain kind of color space as a starting point, e.g. RGB, XYZ, HSV,
|
||||
|
|
34
profile.py
34
profile.py
|
@ -64,7 +64,7 @@ class PesterLog:
|
|||
if handle not in self.convos:
|
||||
log_time = datetime.now().strftime("%Y-%m-%d.%H.%M")
|
||||
self.convos[handle] = {}
|
||||
for (format, t) in modes.items():
|
||||
for format, t in modes.items():
|
||||
if not os.path.exists(
|
||||
"{}/{}/{}/{}".format(self.logpath, self.handle, handle, format)
|
||||
):
|
||||
|
@ -81,7 +81,7 @@ class PesterLog:
|
|||
)
|
||||
self.convos[handle][format] = fp
|
||||
|
||||
for (format, t) in modes.items():
|
||||
for format, t in modes.items():
|
||||
f = self.convos[handle][format]
|
||||
f.write(t + "\r\n")
|
||||
# flush + fsync force a write,
|
||||
|
@ -446,10 +446,10 @@ with a backup from: <a href='%s'>%s</a></h3></html>"
|
|||
try:
|
||||
with open(_datadir + "server.json") as server_file:
|
||||
read_file = server_file.read()
|
||||
server_file.close()
|
||||
server_obj = json.loads(read_file)
|
||||
return server_obj["server"]
|
||||
except:
|
||||
PchumLog.exception("Failed to load server, falling back to default.")
|
||||
try:
|
||||
with open(_datadir + "server.json", "w") as server_file:
|
||||
json_server_file = {
|
||||
|
@ -458,7 +458,6 @@ with a backup from: <a href='%s'>%s</a></h3></html>"
|
|||
"TLS": True,
|
||||
}
|
||||
server_file.write(json.dumps(json_server_file, indent=4))
|
||||
server_file.close()
|
||||
server = "irc.pesterchum.xyz"
|
||||
except:
|
||||
return self.config.get("server", "irc.pesterchum.xyz")
|
||||
|
@ -469,11 +468,11 @@ with a backup from: <a href='%s'>%s</a></h3></html>"
|
|||
try:
|
||||
with open(_datadir + "server.json") as server_file:
|
||||
read_file = server_file.read()
|
||||
server_file.close()
|
||||
server_obj = json.loads(read_file)
|
||||
server_obj = json.loads(read_file)
|
||||
port = server_obj["port"]
|
||||
return port
|
||||
except:
|
||||
PchumLog.exception("Failed to load port, falling back to default.")
|
||||
return self.config.get("port", "6697")
|
||||
|
||||
def ssl(self):
|
||||
|
@ -482,10 +481,23 @@ with a backup from: <a href='%s'>%s</a></h3></html>"
|
|||
try:
|
||||
with open(_datadir + "server.json") as server_file:
|
||||
read_file = server_file.read()
|
||||
server_file.close()
|
||||
server_obj = json.loads(read_file)
|
||||
server_obj = json.loads(read_file)
|
||||
return server_obj["TLS"]
|
||||
except:
|
||||
PchumLog.exception("Failed to load TLS setting, falling back to default.")
|
||||
return self.config.get("TLS", True)
|
||||
|
||||
def password(self):
|
||||
try:
|
||||
with open(_datadir + "server.json") as server_file:
|
||||
read_file = server_file.read()
|
||||
server_obj = json.loads(read_file)
|
||||
password = ""
|
||||
if "pass" in server_obj:
|
||||
password = server_obj["pass"]
|
||||
return password
|
||||
except:
|
||||
PchumLog.exception("Failed to load TLS setting, falling back to default.")
|
||||
return self.config.get("TLS", True)
|
||||
|
||||
def soundOn(self):
|
||||
|
@ -737,7 +749,7 @@ class userProfile:
|
|||
def setMentions(self, mentions):
|
||||
i = None
|
||||
try:
|
||||
for (i, m) in enumerate(mentions):
|
||||
for i, m in enumerate(mentions):
|
||||
re.compile(m)
|
||||
except re.error as e:
|
||||
PchumLog.error("#{} Not a valid regular expression: {}".format(i, e))
|
||||
|
@ -835,7 +847,7 @@ class PesterProfileDB(dict):
|
|||
json.dump(chumdict, fp)
|
||||
|
||||
u = []
|
||||
for (handle, c) in chumdict.items():
|
||||
for handle, c in chumdict.items():
|
||||
options = dict()
|
||||
if "group" in c:
|
||||
options["group"] = c["group"]
|
||||
|
@ -960,7 +972,7 @@ class pesterTheme(dict):
|
|||
return v
|
||||
|
||||
def pathHook(self, d):
|
||||
for (k, v) in d.items():
|
||||
for k, v in d.items():
|
||||
if isinstance(v, str):
|
||||
s = Template(v)
|
||||
d[k] = s.safe_substitute(path=self.path)
|
||||
|
|
55
scripts/input_validation.py
Normal file
55
scripts/input_validation.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
"""Provides functions for validating input from the server and other clients."""
|
||||
# import re
|
||||
|
||||
# _color_rgb = re.compile(r"^\d{1,3},\d{1,3},\d{1,3}$")
|
||||
# _color_hex = re.compile(r"^#(?:[0-9a-fA-F]{3}){1,2}$")
|
||||
|
||||
|
||||
def is_valid_mood(value: str):
|
||||
"""Returns True if an unparsed value (str) is a valid mood index."""
|
||||
if value in [
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"13",
|
||||
"14",
|
||||
"15",
|
||||
"16",
|
||||
"17",
|
||||
"18",
|
||||
"19",
|
||||
"20",
|
||||
"21",
|
||||
"22",
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_rgb_color(value: str):
|
||||
"""Returns True if an unparsed value (str) is a valid rgb color as "r,g,b".
|
||||
|
||||
Yeah you could do this via re but this is faster."""
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
if 4 > len(value) > 11:
|
||||
return False
|
||||
components = value.split(",")
|
||||
if len(components) != 3:
|
||||
return False
|
||||
for component in components:
|
||||
if not component.isnumeric():
|
||||
return False
|
||||
if int(component) > 255:
|
||||
return False
|
||||
return True
|
189
scripts/irc_protocol.py
Normal file
189
scripts/irc_protocol.py
Normal file
|
@ -0,0 +1,189 @@
|
|||
"""IRC-related functions and classes to be imported by irc.py"""
|
||||
import logging
|
||||
|
||||
PchumLog = logging.getLogger("pchumLogger")
|
||||
|
||||
|
||||
class SendIRC:
|
||||
"""Provides functions for outgoing IRC commands.
|
||||
|
||||
Functions are protocol compliant but don't implement all valid uses of certain commands.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.socket = None # INET socket connected with server.
|
||||
|
||||
def _send(self, *args: str, text=None):
|
||||
"""Send a command to the IRC server.
|
||||
|
||||
Takes either a string or a list of strings.
|
||||
The 'text' argument is for the final parameter, which can have spaces.
|
||||
|
||||
Since this checks if the socket is alive, it's best to send via this method."""
|
||||
# Return if disconnected
|
||||
if not self.socket or self.socket.fileno() == -1:
|
||||
PchumLog.error(
|
||||
"Send attempted while disconnected, args: %s, text: %s.", args, text
|
||||
)
|
||||
return
|
||||
|
||||
command = ""
|
||||
# Convert command arguments to a single string if passed.
|
||||
if args:
|
||||
command += " ".join(args)
|
||||
# If text is passed, add ':' to imply everything after it is one parameter.
|
||||
if text:
|
||||
command += f" :{text}"
|
||||
# Add characters for end of line in IRC.
|
||||
command += "\r\n"
|
||||
# UTF-8 is the prefered encoding in 2023.
|
||||
outgoing_bytes = command.encode(encoding="utf-8", errors="replace")
|
||||
|
||||
try:
|
||||
PchumLog.debug("Sending: %s", command)
|
||||
self.socket.sendall(outgoing_bytes)
|
||||
except OSError:
|
||||
PchumLog.exception("Error while sending: '%s'", command.strip())
|
||||
self.socket.close()
|
||||
|
||||
def ping(self, token):
|
||||
"""Send PING command to server to check for connectivity."""
|
||||
self._send("PING", text=token)
|
||||
|
||||
def pong(self, token):
|
||||
"""Send PONG command to reply to server PING."""
|
||||
self._send("PONG", token)
|
||||
|
||||
def pass_(self, password):
|
||||
"""Send a 'connection password' to the server.
|
||||
|
||||
Function is 'pass_' because 'pass' is reserved."""
|
||||
self._send("PASS", text=password)
|
||||
|
||||
def nick(self, nick):
|
||||
"""Send USER command to communicate nick to server."""
|
||||
self._send("NICK", nick)
|
||||
|
||||
def user(self, username, realname):
|
||||
"""Send USER command to communicate username and realname to server."""
|
||||
self._send("USER", username, "0", "*", text=realname)
|
||||
|
||||
def privmsg(self, target, text):
|
||||
"""Send PRIVMSG command to send a message."""
|
||||
for line in text.split("\n"):
|
||||
self._send("PRIVMSG", target, text=line)
|
||||
|
||||
def names(self, channel):
|
||||
"""Send NAMES command to view channel members."""
|
||||
self._send("NAMES", channel)
|
||||
|
||||
def kick(self, channel, user, reason=""):
|
||||
"""Send KICK command to force user from channel."""
|
||||
if reason:
|
||||
self._send(f"KICK {channel} {user}", text=reason)
|
||||
else:
|
||||
self._send(f"KICK {channel} {user}")
|
||||
|
||||
def mode(self, target, modestring="", mode_arguments=""):
|
||||
"""Set or remove modes from target."""
|
||||
outgoing_mode = " ".join([target, modestring, mode_arguments]).strip()
|
||||
self._send("MODE", outgoing_mode)
|
||||
|
||||
def ctcp(self, target, command, msg=""):
|
||||
"""Send Client-to-Client Protocol message."""
|
||||
outgoing_ctcp = " ".join(
|
||||
[command, msg]
|
||||
).strip() # Extra spaces break protocol, so strip.
|
||||
self.privmsg(target, f"\x01{outgoing_ctcp}\x01")
|
||||
|
||||
def ctcp_reply(self, target, command, msg=""):
|
||||
"""Send Client-to-Client Protocol reply message, responding to a CTCP message."""
|
||||
outgoing_ctcp = " ".join(
|
||||
[command, msg]
|
||||
).strip() # Extra spaces break protocol, so strip.
|
||||
self.notice(target, f"\x01{outgoing_ctcp}\x01")
|
||||
|
||||
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, *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, 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):
|
||||
"""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, text):
|
||||
"""Send a NOTICE to a user or channel."""
|
||||
self._send("NOTICE", target, text=text)
|
||||
|
||||
def invite(self, nick, channel):
|
||||
"""Send INVITE command to invite a user to a channel."""
|
||||
self._send("INVITE", nick, channel)
|
||||
|
||||
def away(self, text=None):
|
||||
"""AWAY command to mark client as away or no longer away.
|
||||
|
||||
No 'text' parameter means the client is no longer away."""
|
||||
if text:
|
||||
self._send("AWAY", text=text)
|
||||
else:
|
||||
self._send("AWAY")
|
||||
|
||||
def list(self):
|
||||
"""Send LIST command to get list of channels."""
|
||||
self._send("LIST")
|
||||
|
||||
def quit(self, reason=""):
|
||||
"""Send QUIT to terminate connection."""
|
||||
self._send("QUIT", text=reason)
|
||||
|
||||
|
||||
def parse_irc_line(line: str):
|
||||
"""Retrieves tags, prefix, command, and arguments from an unparsed IRC line."""
|
||||
parts = line.split(" ")
|
||||
tags = None
|
||||
prefix = None
|
||||
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:]
|
||||
command = command.casefold()
|
||||
|
||||
# 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
|
||||
fused_args.append(arg)
|
||||
|
||||
return (tags, prefix, command, fused_args)
|
52
scripts/ssl_context.py
Normal file
52
scripts/ssl_context.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
"""Provides a function for creating an appropriate SSL context."""
|
||||
import ssl
|
||||
import sys
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
|
||||
def get_ssl_context():
|
||||
"""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
|
2
toast.py
2
toast.py
|
@ -103,7 +103,7 @@ class ToastMachine:
|
|||
def realShow(self):
|
||||
self.machine.displaying = True
|
||||
t = None
|
||||
for (k, v) in self.machine.types.items():
|
||||
for k, v in self.machine.types.items():
|
||||
if self.machine.type == k:
|
||||
try:
|
||||
args = inspect.getargspec(v.__init__).args
|
||||
|
|
Loading…
Reference in a new issue