wip caps are broken??
This commit is contained in:
parent
8749c9586a
commit
f99bcf83d9
3 changed files with 141 additions and 644 deletions
242
irc.py
242
irc.py
|
@ -20,7 +20,7 @@ from generic import PesterList
|
||||||
from version import _pcVersion
|
from version import _pcVersion
|
||||||
|
|
||||||
from oyoyo import services
|
from oyoyo import services
|
||||||
from oyoyo.parse import parse_raw_irc_command
|
from oyoyo.ircevents import numeric_events
|
||||||
|
|
||||||
import scripts.irc.outgoing
|
import scripts.irc.outgoing
|
||||||
|
|
||||||
|
@ -99,7 +99,6 @@ class PesterIRC(QtCore.QThread):
|
||||||
self.host = self.config.server()
|
self.host = self.config.server()
|
||||||
self.port = self.config.port()
|
self.port = self.config.port()
|
||||||
self.ssl = self.config.ssl()
|
self.ssl = self.config.ssl()
|
||||||
self.nick = self.mainwindow.profile().handle
|
|
||||||
self.timeout = 120
|
self.timeout = 120
|
||||||
self.blocking = True
|
self.blocking = True
|
||||||
self._end = False
|
self._end = False
|
||||||
|
@ -107,7 +106,7 @@ class PesterIRC(QtCore.QThread):
|
||||||
self.command_handler = self
|
self.command_handler = self
|
||||||
self.parent = self
|
self.parent = self
|
||||||
|
|
||||||
self.send_irc = scripts.irc.outgoing.send_irc()
|
self.send_irc = scripts.irc.outgoing.SendIRC()
|
||||||
|
|
||||||
def get_ssl_context(self):
|
def get_ssl_context(self):
|
||||||
"""Returns an SSL context for connecting over SSL/TLS.
|
"""Returns an SSL context for connecting over SSL/TLS.
|
||||||
|
@ -176,99 +175,42 @@ class PesterIRC(QtCore.QThread):
|
||||||
elif self.blocking:
|
elif self.blocking:
|
||||||
self.socket.setblocking(True)
|
self.socket.setblocking(True)
|
||||||
|
|
||||||
self.send_irc.nick(self.nick)
|
self.send_irc.nick(self.mainwindow.profile().handle)
|
||||||
self.send_irc.user("pcc31", "pcc31")
|
self.send_irc.user("pcc31", "pcc31")
|
||||||
# if self.connect_cb:
|
# if self.connect_cb:
|
||||||
# self.connect_cb(self)
|
# self.connect_cb(self)
|
||||||
|
|
||||||
def conn(self):
|
def conn_generator(self):
|
||||||
"""returns a generator object."""
|
"""returns a generator object."""
|
||||||
try:
|
try:
|
||||||
buffer = b""
|
buffer = b""
|
||||||
while not self._end:
|
while not self._end:
|
||||||
# Block for connection-killing exceptions
|
|
||||||
try:
|
try:
|
||||||
tries = 1
|
buffer += self.socket.recv(1024)
|
||||||
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:
|
except OSError as e:
|
||||||
PchumLog.warning("conn exception {} in {}".format(e, self))
|
PchumLog.warning("conn exception {} in {}".format(e, self))
|
||||||
if self._end:
|
if self._end:
|
||||||
break
|
break
|
||||||
if not self.blocking and e.errno == 11:
|
raise e
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
else:
|
else:
|
||||||
if self._end:
|
if self._end:
|
||||||
break
|
break
|
||||||
if len(buffer) == 0 and self.blocking:
|
if not buffer and self.blocking:
|
||||||
PchumLog.debug("len(buffer) = 0")
|
PchumLog.debug("len(buffer) = 0")
|
||||||
raise OSError("Connection closed")
|
raise OSError("Connection closed")
|
||||||
|
|
||||||
data = buffer.split(bytes("\n", "UTF-8"))
|
data, buffer = self.parse_buffer(buffer)
|
||||||
buffer = data.pop()
|
for line in data:
|
||||||
|
tags, prefix, command, args = self.parse_irc_line(line)
|
||||||
PchumLog.debug("data = " + str(data))
|
|
||||||
|
|
||||||
for el in data:
|
|
||||||
tags, prefix, command, args = parse_raw_irc_command(el)
|
|
||||||
# print(tags, prefix, command, args)
|
# print(tags, prefix, command, args)
|
||||||
try:
|
try:
|
||||||
# Only need tags with tagmsg
|
# Only need tags with tagmsg
|
||||||
if command.upper() == "TAGMSG":
|
if command.casefold() == "tagmsg":
|
||||||
self.run_command(command, prefix, tags, *args)
|
self.run_command(command, prefix, tags, *args)
|
||||||
else:
|
else:
|
||||||
self.run_command(command, prefix, *args)
|
self.run_command(command, prefix, *args)
|
||||||
except CommandError as e:
|
except CommandError as e:
|
||||||
PchumLog.warning("CommandError %s" % str(e))
|
PchumLog.warning(f"CommandError: {e}")
|
||||||
|
|
||||||
yield True
|
yield True
|
||||||
except socket.timeout as se:
|
except socket.timeout as se:
|
||||||
|
@ -281,7 +223,7 @@ class PesterIRC(QtCore.QThread):
|
||||||
self.socket.close()
|
self.socket.close()
|
||||||
raise se
|
raise se
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
PchumLog.debug("other exception: %s" % str(e))
|
PchumLog.exception("Non-socket related exception in conn_generator().")
|
||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
PchumLog.debug("ending while, end is %s" % self._end)
|
PchumLog.debug("ending while, end is %s" % self._end)
|
||||||
|
@ -290,6 +232,61 @@ class PesterIRC(QtCore.QThread):
|
||||||
self.socket.close()
|
self.socket.close()
|
||||||
yield False
|
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):
|
def close(self):
|
||||||
# with extreme prejudice
|
# with extreme prejudice
|
||||||
if self.socket:
|
if self.socket:
|
||||||
|
@ -316,7 +313,7 @@ class PesterIRC(QtCore.QThread):
|
||||||
# Ask if users wants to connect anyway
|
# Ask if users wants to connect anyway
|
||||||
self.askToConnect.emit(e)
|
self.askToConnect.emit(e)
|
||||||
raise e
|
raise e
|
||||||
self.conn = self.conn()
|
self.conn = self.conn_generator()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
|
@ -367,15 +364,47 @@ class PesterIRC(QtCore.QThread):
|
||||||
except (OSError, ValueError, IndexError) as se:
|
except (OSError, ValueError, IndexError) as se:
|
||||||
raise se
|
raise se
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
self.conn = self.conn()
|
self.conn = self.conn_generator()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@QtCore.pyqtSlot(PesterProfile)
|
@QtCore.pyqtSlot(PesterProfile)
|
||||||
def getMood(self, *chums):
|
def getMood(self, *chums):
|
||||||
if hasattr(self, "cli"):
|
"""Get mood via metadata if supported"""
|
||||||
self.command_handler.getMood(*chums)
|
|
||||||
|
# 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)
|
@QtCore.pyqtSlot(PesterList)
|
||||||
def getMoods(self, chums):
|
def getMoods(self, chums):
|
||||||
|
@ -462,8 +491,7 @@ class PesterIRC(QtCore.QThread):
|
||||||
if hasattr(self, "cli"):
|
if hasattr(self, "cli"):
|
||||||
h = str(handle)
|
h = str(handle)
|
||||||
try:
|
try:
|
||||||
self.send_irc.msg(
|
self.send_irc.msg(h, "COLOR >%s" % (self.mainwindow.profile().colorcmd())
|
||||||
self, h, "COLOR >%s" % (self.mainwindow.profile().colorcmd())
|
|
||||||
)
|
)
|
||||||
if initiated:
|
if initiated:
|
||||||
self.send_irc.msg(h, "PESTERCHUM:BEGIN")
|
self.send_irc.msg(h, "PESTERCHUM:BEGIN")
|
||||||
|
@ -530,7 +558,6 @@ class PesterIRC(QtCore.QThread):
|
||||||
for h in list(self.mainwindow.convos.keys()):
|
for h in list(self.mainwindow.convos.keys()):
|
||||||
try:
|
try:
|
||||||
self.send_irc.msg(
|
self.send_irc.msg(
|
||||||
self,
|
|
||||||
h,
|
h,
|
||||||
"COLOR >%s" % (self.mainwindow.profile().colorcmd()),
|
"COLOR >%s" % (self.mainwindow.profile().colorcmd()),
|
||||||
)
|
)
|
||||||
|
@ -766,13 +793,11 @@ class PesterIRC(QtCore.QThread):
|
||||||
PchumLog.info('---> recv "CTCP {} :{}"'.format(handle, msg[1:-1]))
|
PchumLog.info('---> recv "CTCP {} :{}"'.format(handle, msg[1:-1]))
|
||||||
# VERSION, return version
|
# VERSION, return version
|
||||||
if msg[1:-1].startswith("VERSION"):
|
if msg[1:-1].startswith("VERSION"):
|
||||||
self.send_irc.ctcp_reply(
|
self.send_irc.ctcp(handle, "VERSION", "Pesterchum %s" % (_pcVersion)
|
||||||
self.parent.cli, handle, "VERSION", "Pesterchum %s" % (_pcVersion)
|
|
||||||
)
|
)
|
||||||
# CLIENTINFO, return supported CTCP commands.
|
# CLIENTINFO, return supported CTCP commands.
|
||||||
elif msg[1:-1].startswith("CLIENTINFO"):
|
elif msg[1:-1].startswith("CLIENTINFO"):
|
||||||
self.send_irc.ctcp_reply(
|
self.send_irc.ctcp(
|
||||||
self.parent.cli,
|
|
||||||
handle,
|
handle,
|
||||||
"CLIENTINFO",
|
"CLIENTINFO",
|
||||||
"ACTION VERSION CLIENTINFO PING SOURCE NOQUIRKS GETMOOD",
|
"ACTION VERSION CLIENTINFO PING SOURCE NOQUIRKS GETMOOD",
|
||||||
|
@ -780,15 +805,13 @@ class PesterIRC(QtCore.QThread):
|
||||||
# PING, return pong
|
# PING, return pong
|
||||||
elif msg[1:-1].startswith("PING"):
|
elif msg[1:-1].startswith("PING"):
|
||||||
if len(msg[1:-1].split("PING ")) > 1:
|
if len(msg[1:-1].split("PING ")) > 1:
|
||||||
self.send_irc.ctcp_reply(
|
self.send_irc.ctcp(handle, "PING", msg[1:-1].split("PING ")[1]
|
||||||
self.parent.cli, handle, "PING", msg[1:-1].split("PING ")[1]
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.send_irc.ctcp_reply(self.parent.cli, handle, "PING")
|
self.send_irc.ctcp(handle, "PING")
|
||||||
# SOURCE, return source
|
# SOURCE, return source
|
||||||
elif msg[1:-1].startswith("SOURCE"):
|
elif msg[1:-1].startswith("SOURCE"):
|
||||||
self.send_irc.ctcp_reply(
|
self.send_irc.ctcp(
|
||||||
self.parent.cli,
|
|
||||||
handle,
|
handle,
|
||||||
"SOURCE",
|
"SOURCE",
|
||||||
"https://github.com/Dpeta/pesterchum-alt-servers",
|
"https://github.com/Dpeta/pesterchum-alt-servers",
|
||||||
|
@ -802,7 +825,7 @@ class PesterIRC(QtCore.QThread):
|
||||||
# GETMOOD via CTCP
|
# GETMOOD via CTCP
|
||||||
# Maybe we can do moods like this in the future...
|
# Maybe we can do moods like this in the future...
|
||||||
mymood = self.mainwindow.profile().mood.value()
|
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
|
# Backwards compatibility
|
||||||
self.send_irc.msg("#pesterchum", "MOOD >%d" % (mymood))
|
self.send_irc.msg("#pesterchum", "MOOD >%d" % (mymood))
|
||||||
return
|
return
|
||||||
|
@ -1237,42 +1260,6 @@ class PesterIRC(QtCore.QThread):
|
||||||
"""Respond to server PING with PONG."""
|
"""Respond to server PING with PONG."""
|
||||||
self.send_irc.pong(token)
|
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):
|
def get(self, in_command_parts):
|
||||||
PchumLog.debug("in_command_parts: %s" % in_command_parts)
|
PchumLog.debug("in_command_parts: %s" % in_command_parts)
|
||||||
""" finds a command
|
""" finds a command
|
||||||
|
@ -1310,10 +1297,7 @@ class PesterIRC(QtCore.QThread):
|
||||||
|
|
||||||
def run_command(self, command, *args):
|
def run_command(self, command, *args):
|
||||||
"""finds and runs a command"""
|
"""finds and runs a command"""
|
||||||
arguments_str = ""
|
PchumLog.debug("processCommand {}({})".format(command, args))
|
||||||
for x in args:
|
|
||||||
arguments_str += str(x) + " "
|
|
||||||
PchumLog.debug("processCommand {}({})".format(command, arguments_str.strip()))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = self.get(command)
|
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."""
|
"""Class and functions for sending outgoing IRC commands."""
|
||||||
import socket
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
log = logging.getLogger("pchumLogger")
|
log = logging.getLogger("pchumLogger")
|
||||||
|
|
||||||
|
|
||||||
class send_irc:
|
class SendIRC:
|
||||||
"""Provides functions for outgoing IRC commands."""
|
"""Provides functions for outgoing IRC commands."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.socket = None # INET socket connected with server.
|
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):
|
def send(self, *args: str, text=None):
|
||||||
"""Send a command to the IRC server.
|
"""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."""
|
The 'text' argument is for the final parameter, which can have spaces."""
|
||||||
# Return if disconnected
|
# Return if disconnected
|
||||||
if not self.socket or self.socket.fileno() == -1:
|
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
|
return
|
||||||
|
|
||||||
command = ""
|
command = ""
|
||||||
|
@ -35,17 +35,17 @@ class send_irc:
|
||||||
outgoing_bytes = command.encode(encoding="utf-8", errors="replace")
|
outgoing_bytes = command.encode(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log.debug(f"Sending: {command}")
|
log.debug("Sending: %s", command)
|
||||||
self.socket.send(outgoing_bytes)
|
self.socket.send(outgoing_bytes)
|
||||||
except OSError:
|
except OSError:
|
||||||
log.exception(f"Error while sending: '{command.strip()}'")
|
log.exception("Error while sending: '%s'", command.strip())
|
||||||
self.socket.close()
|
self.socket.close()
|
||||||
|
|
||||||
def ping(self, token: str):
|
def ping(self, token):
|
||||||
"""Send PING command to server to check for connectivity."""
|
"""Send PING command to server to check for connectivity."""
|
||||||
self.send("PING", text=token)
|
self.send("PING", text=token)
|
||||||
|
|
||||||
def pong(self, token: str):
|
def pong(self, token):
|
||||||
"""Send PONG command to reply to server PING."""
|
"""Send PONG command to reply to server PING."""
|
||||||
self.send("PONG", token)
|
self.send("PONG", token)
|
||||||
|
|
||||||
|
@ -59,10 +59,10 @@ class send_irc:
|
||||||
|
|
||||||
def msg(self, target, text):
|
def msg(self, target, text):
|
||||||
"""Send PRIVMSG command to send a message."""
|
"""Send PRIVMSG command to send a message."""
|
||||||
for line in msg.split("\n"):
|
for line in text.split("\n"):
|
||||||
self.send("PRIVMSG", target, text=text)
|
self.send("PRIVMSG", target, text=line)
|
||||||
|
|
||||||
def names(self, channel: str):
|
def names(self, channel):
|
||||||
"""Send NAMES command to view channel members."""
|
"""Send NAMES command to view channel members."""
|
||||||
self.send("NAMES", channel)
|
self.send("NAMES", channel)
|
||||||
|
|
||||||
|
@ -80,37 +80,43 @@ class send_irc:
|
||||||
|
|
||||||
def ctcp(self, target, command, msg=""):
|
def ctcp(self, target, command, msg=""):
|
||||||
"""Send Client-to-Client Protocol message."""
|
"""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")
|
self.msg(target, f"\x01{outgoing_ctcp}\x01")
|
||||||
|
|
||||||
def metadata(self, target: str, subcommand: str, *params: str):
|
def metadata(self, target, subcommand, *params):
|
||||||
# IRC metadata draft specification
|
"""Send Metadata command to get or set metadata.
|
||||||
# https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237
|
|
||||||
|
See IRC metadata draft specification:
|
||||||
|
https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237
|
||||||
|
"""
|
||||||
self.send("METADATA", target, subcommand, *params)
|
self.send("METADATA", target, subcommand, *params)
|
||||||
|
|
||||||
def cap(self, subcommand: str, *params: str):
|
def cap(self, subcommand, *params):
|
||||||
# Capability Negotiation
|
"""Send IRCv3 CAP command for capability negotiation.
|
||||||
# https://ircv3.net/specs/extensions/capability-negotiation.html
|
|
||||||
|
See: https://ircv3.net/specs/extensions/capability-negotiation.html"""
|
||||||
self.send("CAP", subcommand, *params)
|
self.send("CAP", subcommand, *params)
|
||||||
|
|
||||||
def join(self, channel: str, key=""):
|
def join(self, channel, key=""):
|
||||||
"""Send JOIN command to join a channel/memo.
|
"""Send JOIN command to join a channel/memo.
|
||||||
|
|
||||||
Keys or joining multiple channels is possible in the specification, but unused."""
|
Keys or joining multiple channels is possible in the specification, but unused."""
|
||||||
channel_and_key = " ".join([channel, key]).strip()
|
channel_and_key = " ".join([channel, key]).strip()
|
||||||
self.send("JOIN", channel_and_key)
|
self.send("JOIN", channel_and_key)
|
||||||
|
|
||||||
def part(self, channel: str):
|
def part(self, channel):
|
||||||
"""Send PART command to leave a channel/memo.
|
"""Send PART command to leave a channel/memo.
|
||||||
|
|
||||||
Providing a reason or leaving multiple channels is possible in the specification."""
|
Providing a reason or leaving multiple channels is possible in the specification."""
|
||||||
self.send("PART", channel)
|
self.send("PART", channel)
|
||||||
|
|
||||||
def notice(self, target: str, text: str):
|
def notice(self, target, text):
|
||||||
"""Send a NOTICE to a user or channel."""
|
"""Send a NOTICE to a user or channel."""
|
||||||
self.send("NOTICE", target, text=text)
|
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."""
|
"""Send INVITE command to invite a user to a channel."""
|
||||||
self.send("INVITE", nick, channel)
|
self.send("INVITE", nick, channel)
|
||||||
|
|
||||||
|
@ -122,7 +128,3 @@ class send_irc:
|
||||||
self.send("AWAY", text=text)
|
self.send("AWAY", text=text)
|
||||||
else:
|
else:
|
||||||
self.send("AWAY")
|
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