2023-02-09 17:58:03 -05:00
|
|
|
|
"""Provides classes and functions for communicating with an IRC server.
|
|
|
|
|
|
|
|
|
|
References for the IRC protocol:
|
|
|
|
|
- Original IRC protocol specification: https://www.rfc-editor.org/rfc/rfc1459
|
|
|
|
|
- Updated IRC Client specification: https://www.rfc-editor.org/rfc/rfc2812
|
|
|
|
|
- Modern IRC client protocol writeup: https://modern.ircdocs.horse
|
|
|
|
|
- IRCv3 protocol additions: https://ircv3.net/irc/
|
2023-02-11 19:25:06 -05:00
|
|
|
|
- Draft of metadata spec: https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237
|
2023-02-09 19:29:23 -05:00
|
|
|
|
|
|
|
|
|
Some code in this file may be based on the oyoyo IRC library,
|
|
|
|
|
the license notice included with oyoyo source files is indented here:
|
2023-02-11 19:25:06 -05:00
|
|
|
|
|
2023-02-09 19:29:23 -05:00
|
|
|
|
# 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.
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""
|
2011-02-15 12:10:57 -05:00
|
|
|
|
import socket
|
2023-02-09 17:58:03 -05:00
|
|
|
|
import random
|
|
|
|
|
import logging
|
2023-02-11 18:36:36 -05:00
|
|
|
|
from ssl import SSLCertVerificationError
|
2011-02-05 12:17:33 -05:00
|
|
|
|
|
2022-08-19 07:12:58 -04:00
|
|
|
|
try:
|
|
|
|
|
from PyQt6 import QtCore, QtGui
|
|
|
|
|
except ImportError:
|
|
|
|
|
print("PyQt5 fallback (irc.py)")
|
|
|
|
|
from PyQt5 import QtCore, QtGui
|
2022-03-19 19:48:19 -04:00
|
|
|
|
|
2011-09-15 03:09:56 -04:00
|
|
|
|
from mood import Mood
|
|
|
|
|
from dataobjs import PesterProfile
|
2011-02-05 12:17:33 -05:00
|
|
|
|
from generic import PesterList
|
2011-05-26 03:40:30 -04:00
|
|
|
|
from version import _pcVersion
|
2023-02-09 20:33:01 -05:00
|
|
|
|
from scripts.irc_protocol import SendIRC, parse_irc_line
|
|
|
|
|
from scripts.ssl_context import get_ssl_context
|
2023-02-11 18:36:36 -05:00
|
|
|
|
from scripts.input_validation import is_valid_mood, is_valid_rgb_color
|
2011-02-05 12:17:33 -05:00
|
|
|
|
|
2022-10-07 16:51:40 -04:00
|
|
|
|
PchumLog = logging.getLogger("pchumLogger")
|
2022-11-17 04:31:04 -05:00
|
|
|
|
SERVICES = [
|
|
|
|
|
"nickserv",
|
|
|
|
|
"chanserv",
|
|
|
|
|
"memoserv",
|
|
|
|
|
"operserv",
|
|
|
|
|
"helpserv",
|
|
|
|
|
"hostserv",
|
|
|
|
|
"botserv",
|
|
|
|
|
]
|
2022-03-19 19:48:19 -04:00
|
|
|
|
|
2023-02-09 14:02:29 -05:00
|
|
|
|
|
2011-02-19 18:06:54 -05:00
|
|
|
|
class PesterIRC(QtCore.QThread):
|
2023-02-09 19:29:23 -05:00
|
|
|
|
"""Class for making a thread that manages the connection to server."""
|
2023-02-09 20:33:01 -05:00
|
|
|
|
|
2023-02-12 14:33:05 -05:00
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
window,
|
|
|
|
|
server: str,
|
|
|
|
|
port: int,
|
|
|
|
|
ssl: bool,
|
|
|
|
|
password="",
|
|
|
|
|
verify_hostname=True,
|
|
|
|
|
):
|
2011-02-19 18:06:54 -05:00
|
|
|
|
QtCore.QThread.__init__(self)
|
2011-02-05 12:17:33 -05:00
|
|
|
|
self.mainwindow = window
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-01-31 19:12:43 -05:00
|
|
|
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self.server = server # Server to connect to.
|
|
|
|
|
self.port = port # Port on server to connect to.
|
2023-02-12 14:33:05 -05:00
|
|
|
|
self.password = password # Optional password for PASS.
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self.ssl = ssl # Whether to connect over SSL/TLS.
|
|
|
|
|
self.verify_hostname = (
|
|
|
|
|
verify_hostname # Whether to verify server hostname. (SSL-only)
|
|
|
|
|
)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc = SendIRC()
|
2023-02-09 18:14:33 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self.unresponsive = False
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.registered_irc = False
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self.metadata_supported = False
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.stop_irc = None
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._conn = None
|
|
|
|
|
self._end = False # Set to True when ending connection.
|
2023-02-09 14:02:29 -05:00
|
|
|
|
self.joined = False
|
2023-02-09 17:58:03 -05:00
|
|
|
|
self.channelnames = {}
|
|
|
|
|
self.channel_list = []
|
|
|
|
|
self.channel_field = None
|
2023-02-09 14:02:29 -05:00
|
|
|
|
|
2023-02-28 17:32:35 -05:00
|
|
|
|
# Dict for connection server commands/replies to handling functions.
|
2023-02-09 14:02:29 -05:00
|
|
|
|
self.commands = {
|
2023-02-11 18:36:36 -05:00
|
|
|
|
"001": self._welcome,
|
|
|
|
|
"005": self._featurelist,
|
|
|
|
|
"221": self._umodeis,
|
|
|
|
|
"321": self._liststart,
|
|
|
|
|
"322": self._list,
|
|
|
|
|
"323": self._listend,
|
|
|
|
|
"324": self._channelmodeis,
|
|
|
|
|
"353": self._namreply,
|
|
|
|
|
"366": self._endofnames,
|
|
|
|
|
"432": self._erroneusnickname,
|
|
|
|
|
"433": self._nicknameinuse,
|
|
|
|
|
"436": self._nickcollision,
|
|
|
|
|
"448": self._forbiddenchannel, # non-standard
|
|
|
|
|
"473": self._inviteonlychan,
|
|
|
|
|
"761": self._keyvalue, # 7XX is ircv3 deprecated metadata spec
|
|
|
|
|
"766": self._nomatchingkey,
|
|
|
|
|
"768": self._keynotset,
|
|
|
|
|
"769": self._keynopermission,
|
|
|
|
|
"770": self._metadatasubok,
|
2023-02-25 15:22:36 -05:00
|
|
|
|
"902": self._sasl_skill_issue, # ERR_NICKLOCKED, account is not available...
|
2023-02-24 22:46:59 -05:00
|
|
|
|
"903": self._saslsuccess, # We did a SASL!! woo yeah!! (RPL_SASLSUCCESS)
|
2023-02-25 15:22:36 -05:00
|
|
|
|
"904": self._sasl_skill_issue, # oh no,,, cringe,,, (ERR_SASLFAIL)
|
|
|
|
|
"905": self._sasl_skill_issue, # ERR_SASLTOOLONG, we don't split so end.
|
2023-02-11 18:36:36 -05:00
|
|
|
|
"error": self._error,
|
|
|
|
|
"join": self._join,
|
|
|
|
|
"kick": self._kick,
|
|
|
|
|
"mode": self._mode,
|
|
|
|
|
"part": self._part,
|
|
|
|
|
"ping": self._ping,
|
|
|
|
|
"privmsg": self._privmsg,
|
|
|
|
|
"notice": self._notice,
|
|
|
|
|
"quit": self._quit,
|
|
|
|
|
"invite": self._invite,
|
|
|
|
|
"nick": self._nick, # We can get svsnicked
|
|
|
|
|
"metadata": self._metadata, # Metadata specification
|
|
|
|
|
"tagmsg": self._tagmsg, # IRCv3 message tags extension
|
|
|
|
|
"cap": self._cap, # IRCv3 Client Capability Negotiation
|
2023-02-24 22:46:59 -05:00
|
|
|
|
"authenticate": self._authenticate, # IRCv3 SASL authentication
|
2023-02-09 14:02:29 -05:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def run(self):
|
|
|
|
|
"""Implements the main loop for the thread.
|
|
|
|
|
|
|
|
|
|
This function reimplements QThread::run() and is ran after self.irc.start()
|
|
|
|
|
is called on the main thread. Returning from this method ends the thread."""
|
|
|
|
|
try:
|
2023-02-11 19:12:42 -05:00
|
|
|
|
self.irc_connect()
|
2023-02-11 19:25:06 -05:00
|
|
|
|
except OSError as socket_exception:
|
|
|
|
|
self.stop_irc = socket_exception
|
2023-02-11 18:36:36 -05:00
|
|
|
|
return
|
|
|
|
|
while True:
|
|
|
|
|
res = True
|
|
|
|
|
try:
|
2023-02-11 19:12:42 -05:00
|
|
|
|
PchumLog.debug("update_irc()")
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self.mainwindow.sincerecv = 0
|
2023-02-11 19:12:42 -05:00
|
|
|
|
res = self.update_irc()
|
2023-02-11 19:25:06 -05:00
|
|
|
|
except socket.timeout as timeout:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.debug("timeout in thread %s", self)
|
|
|
|
|
self._close()
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.stop_irc = f"{type(timeout)}, {timeout}"
|
2023-02-11 18:36:36 -05:00
|
|
|
|
return
|
2023-02-11 19:25:06 -05:00
|
|
|
|
except (OSError, IndexError, ValueError) as exception:
|
|
|
|
|
self.stop_irc = f"{type(exception)}, {exception}"
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.debug("Socket error, exiting thread.")
|
|
|
|
|
return
|
|
|
|
|
else:
|
|
|
|
|
if not res:
|
|
|
|
|
PchumLog.debug("False Yield: %s, returning", res)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def _connect(self, verify_hostname=True):
|
|
|
|
|
"""Initiates the connection to the server set in self.server:self.port
|
2023-01-31 19:12:43 -05:00
|
|
|
|
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'."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.info("Connecting to %s:%s", self.server, self.port)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
|
|
|
|
|
# Open connection
|
2023-02-11 18:36:36 -05:00
|
|
|
|
plaintext_socket = socket.create_connection((self.server, self.port))
|
2023-01-31 19:12:43 -05:00
|
|
|
|
|
|
|
|
|
if self.ssl:
|
|
|
|
|
# Upgrade connection to use SSL/TLS if enabled
|
2023-02-09 20:33:01 -05:00
|
|
|
|
context = get_ssl_context()
|
2023-01-31 19:12:43 -05:00
|
|
|
|
context.check_hostname = verify_hostname
|
|
|
|
|
self.socket = context.wrap_socket(
|
2023-02-11 18:36:36 -05:00
|
|
|
|
plaintext_socket, server_hostname=self.server
|
2023-01-31 19:12:43 -05:00
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# SSL/TLS is disabled, connection is plaintext
|
|
|
|
|
self.socket = plaintext_socket
|
|
|
|
|
|
2023-02-09 09:46:46 -05:00
|
|
|
|
self.socket.settimeout(90)
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.socket = self.socket
|
2023-01-31 19:12:43 -05:00
|
|
|
|
|
2023-02-12 14:33:05 -05:00
|
|
|
|
if self.password:
|
|
|
|
|
self._send_irc.pass_(self.password)
|
2023-02-24 22:46:59 -05:00
|
|
|
|
|
|
|
|
|
# Negotiate capabilities
|
|
|
|
|
self._send_irc.cap("REQ", "message-tags")
|
|
|
|
|
self._send_irc.cap(
|
|
|
|
|
"REQ",
|
|
|
|
|
"draft/metadata-notify-2", # <--- Not required for the unreal5 module.
|
|
|
|
|
)
|
|
|
|
|
self._send_irc.cap("REQ", "pesterchum-tag") # <--- Currently not using this
|
|
|
|
|
self._send_irc.cap("REQ", "twitch.tv/membership") # Twitch silly
|
|
|
|
|
|
|
|
|
|
# This should not be here.
|
|
|
|
|
profile = self.mainwindow.profile()
|
|
|
|
|
# Do SASL!!
|
2023-02-25 12:18:19 -05:00
|
|
|
|
self._send_irc.cap("REQ", "sasl")
|
2023-02-24 22:46:59 -05:00
|
|
|
|
if self.mainwindow.userprofile.getAutoIdentify():
|
2023-02-25 12:18:19 -05:00
|
|
|
|
# Send plain, send end later when 903 or 904 is received.
|
2023-02-24 22:46:59 -05:00
|
|
|
|
self._send_irc.authenticate("PLAIN")
|
2023-02-28 17:32:35 -05:00
|
|
|
|
# Always call CAP END after 5 seconds.
|
|
|
|
|
self.cap_negotation_started.emit()
|
2023-02-24 22:46:59 -05:00
|
|
|
|
else:
|
|
|
|
|
# Without SASL, end caps here.
|
|
|
|
|
self._send_irc.cap("END")
|
|
|
|
|
|
|
|
|
|
# Send NICK & USER :3
|
|
|
|
|
self._send_irc.nick(profile.handle)
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.user("pcc31", "pcc31")
|
2023-01-31 19:12:43 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _conn_generator(self):
|
2023-02-09 19:29:23 -05:00
|
|
|
|
"""Returns a generator object."""
|
2022-09-01 23:34:37 -04:00
|
|
|
|
try:
|
2023-01-31 19:12:43 -05:00
|
|
|
|
buffer = b""
|
|
|
|
|
while not self._end:
|
|
|
|
|
try:
|
2023-02-03 14:39:16 -05:00
|
|
|
|
buffer += self.socket.recv(1024)
|
2023-02-12 16:43:25 -05:00
|
|
|
|
except OSError as socket_exception:
|
|
|
|
|
PchumLog.warning(
|
|
|
|
|
"Socket exception in conn_generator: '%s'.", socket_exception
|
|
|
|
|
)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
if self._end:
|
|
|
|
|
break
|
2023-02-12 16:43:25 -05:00
|
|
|
|
raise socket_exception
|
2023-01-31 19:12:43 -05:00
|
|
|
|
else:
|
|
|
|
|
if self._end:
|
|
|
|
|
break
|
2023-02-09 09:46:46 -05:00
|
|
|
|
split_buffer = buffer.split(b"\r\n")
|
|
|
|
|
buffer = b""
|
|
|
|
|
if split_buffer[-1]:
|
|
|
|
|
# Incomplete line, add it back to the buffer.
|
|
|
|
|
buffer = split_buffer.pop()
|
|
|
|
|
for line in split_buffer:
|
|
|
|
|
line = line.decode(encoding="utf-8", errors="replace")
|
2023-02-09 20:33:01 -05:00
|
|
|
|
tags, prefix, command, args = parse_irc_line(line)
|
2023-02-09 17:58:03 -05:00
|
|
|
|
if command:
|
2023-01-31 19:12:43 -05:00
|
|
|
|
# Only need tags with tagmsg
|
2023-02-03 14:39:16 -05:00
|
|
|
|
if command.casefold() == "tagmsg":
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._run_command(command, prefix, tags, *args)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
else:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._run_command(command, prefix, *args)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
yield True
|
2023-02-11 18:36:36 -05:00
|
|
|
|
except OSError as socket_exception:
|
|
|
|
|
PchumLog.warning(
|
|
|
|
|
"OSError raised in _conn, closing socket. (%s)", socket_exception
|
|
|
|
|
)
|
|
|
|
|
self._close()
|
|
|
|
|
raise socket_exception
|
|
|
|
|
except Exception as exception:
|
|
|
|
|
PchumLog.exception("Non-socket exception in _conn.")
|
|
|
|
|
raise exception
|
2023-01-31 19:12:43 -05:00
|
|
|
|
else:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.debug("Ending _conn while loop, end is %s.", self._end)
|
|
|
|
|
self._close()
|
2023-01-31 19:12:43 -05:00
|
|
|
|
yield False
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _run_command(self, command, *args):
|
|
|
|
|
"""Finds and runs a command if it has a matching function in the self.commands dict."""
|
|
|
|
|
PchumLog.debug("_run_command %s(%s)", command, args)
|
2023-02-09 20:33:01 -05:00
|
|
|
|
if command in self.commands:
|
|
|
|
|
command_function = self.commands[command]
|
2023-02-03 14:39:16 -05:00
|
|
|
|
else:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.debug("No matching function for command: %s(%s)", command, args)
|
2023-02-09 20:33:01 -05:00
|
|
|
|
return
|
|
|
|
|
try:
|
|
|
|
|
command_function(*args)
|
|
|
|
|
except TypeError:
|
|
|
|
|
PchumLog.exception(
|
|
|
|
|
"Failed to pass command, did the server pass an unsupported paramater?"
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
PchumLog.exception("Exception while parsing command.")
|
2023-02-09 14:02:29 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _close(self):
|
2023-02-09 19:29:23 -05:00
|
|
|
|
"""Kill the socket 'with extreme prejudice'."""
|
2023-01-31 19:12:43 -05:00
|
|
|
|
if self.socket:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.info("_close() was called, shutting down socket.")
|
2023-01-31 19:12:43 -05:00
|
|
|
|
self._end = True
|
|
|
|
|
try:
|
|
|
|
|
self.socket.shutdown(socket.SHUT_RDWR)
|
2023-02-12 16:43:25 -05:00
|
|
|
|
except OSError as exception:
|
|
|
|
|
PchumLog.info(
|
|
|
|
|
"Error while shutting down socket, already broken? %s", exception
|
|
|
|
|
)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
try:
|
|
|
|
|
self.socket.close()
|
2023-02-12 16:43:25 -05:00
|
|
|
|
except OSError as exception:
|
|
|
|
|
PchumLog.info(
|
|
|
|
|
"Error while closing socket, already broken? %s", exception
|
|
|
|
|
)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def irc_connect(self):
|
2023-02-11 18:36:36 -05:00
|
|
|
|
"""Try to connect and signal for connect-anyway prompt on cert fail."""
|
2023-01-31 19:12:43 -05:00
|
|
|
|
try:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._connect(self.verify_hostname)
|
2023-02-12 16:43:25 -05:00
|
|
|
|
except SSLCertVerificationError as ssl_cert_fail:
|
2022-09-01 23:34:37 -04:00
|
|
|
|
# Ask if users wants to connect anyway
|
2023-02-12 16:43:25 -05:00
|
|
|
|
self.askToConnect.emit(ssl_cert_fail)
|
|
|
|
|
raise ssl_cert_fail
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._conn = self._conn_generator()
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 19:25:06 -05:00
|
|
|
|
def set_connection_broken(self):
|
2023-02-09 18:14:33 -05:00
|
|
|
|
"""Called when the connection is broken."""
|
2023-02-11 19:25:06 -05:00
|
|
|
|
PchumLog.critical("set_connection_broken() got called, disconnecting.")
|
2022-07-05 15:45:16 -04:00
|
|
|
|
self.disconnectIRC()
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-28 17:32:35 -05:00
|
|
|
|
def end_cap_negotiation(self):
|
|
|
|
|
"""Send CAP END to end capability negotation.
|
|
|
|
|
|
|
|
|
|
Called from SASL-related functions here,
|
|
|
|
|
but also from a timer on the main thread that always triggers after 5 seconds.
|
|
|
|
|
"""
|
|
|
|
|
if not self.registered_irc:
|
|
|
|
|
self._send_irc.cap("END")
|
|
|
|
|
|
2011-02-19 18:06:54 -05:00
|
|
|
|
@QtCore.pyqtSlot()
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def update_irc(self):
|
2023-02-09 19:29:23 -05:00
|
|
|
|
"""Get a silly scrunkler from the generator!!"""
|
2011-02-19 18:06:54 -05:00
|
|
|
|
try:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
res = next(self._conn)
|
2023-02-11 19:25:06 -05:00
|
|
|
|
except socket.timeout as socket_exception:
|
|
|
|
|
if self.registered_irc:
|
2011-02-19 21:38:06 -05:00
|
|
|
|
return True
|
2023-02-11 19:25:06 -05:00
|
|
|
|
raise socket_exception
|
2011-02-19 18:06:54 -05:00
|
|
|
|
except StopIteration:
|
2023-02-28 17:32:35 -05:00
|
|
|
|
self._conn = self._conn_generator()
|
2011-02-19 21:38:06 -05:00
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
return res
|
2011-02-19 18:06:54 -05:00
|
|
|
|
|
2011-02-05 12:17:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(PesterProfile)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def get_mood(self, *chums):
|
2023-02-03 14:39:16 -05:00
|
|
|
|
"""Get mood via metadata if supported"""
|
|
|
|
|
|
|
|
|
|
# Get via metadata or via legacy method
|
2023-02-03 16:46:48 -05:00
|
|
|
|
if self.metadata_supported:
|
2023-02-03 14:39:16 -05:00
|
|
|
|
# Metadata
|
|
|
|
|
for chum in chums:
|
|
|
|
|
try:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.metadata(chum.handle, "get", "mood")
|
2023-02-12 16:43:25 -05:00
|
|
|
|
except OSError as socket_exception:
|
|
|
|
|
PchumLog.warning(socket_exception)
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.set_connection_broken()
|
2023-02-03 14:39:16 -05:00
|
|
|
|
else:
|
|
|
|
|
# Legacy
|
|
|
|
|
PchumLog.warning(
|
2023-02-11 20:28:31 -05:00
|
|
|
|
"Server doesn't seem to support metadata, using legacy GETMOOD."
|
2023-02-03 14:39:16 -05:00
|
|
|
|
)
|
2023-02-11 20:28:31 -05:00
|
|
|
|
chumglub = "GETMOOD "
|
2023-02-03 14:39:16 -05:00
|
|
|
|
for chum in chums:
|
|
|
|
|
if len(chumglub + chum.handle) >= 350:
|
|
|
|
|
try:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg("#pesterchum", chumglub)
|
2023-02-03 14:39:16 -05:00
|
|
|
|
except OSError as e:
|
|
|
|
|
PchumLog.warning(e)
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.set_connection_broken()
|
2023-02-11 20:28:31 -05:00
|
|
|
|
chumglub = "GETMOOD "
|
|
|
|
|
# No point in GETMOOD-ing services
|
2023-02-03 14:39:16 -05:00
|
|
|
|
if chum.handle.casefold() not in SERVICES:
|
|
|
|
|
chumglub += chum.handle
|
2023-02-11 20:28:31 -05:00
|
|
|
|
if chumglub != "GETMOOD ":
|
2023-02-03 14:39:16 -05:00
|
|
|
|
try:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg("#pesterchum", chumglub)
|
2023-02-03 14:39:16 -05:00
|
|
|
|
except OSError as e:
|
|
|
|
|
PchumLog.warning(e)
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.set_connection_broken()
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2011-02-05 12:17:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(PesterList)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def get_moods(self, chums):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Get mood, slot is called from main thread."""
|
2023-02-11 19:12:42 -05:00
|
|
|
|
self.get_mood(*chums)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str, str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def send_notice(self, text, handle):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send notice, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.notice(handle, text)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str, str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def send_message(self, text, handle):
|
2023-02-09 18:14:33 -05:00
|
|
|
|
"""......sends a message? this is a tad silly;;;"""
|
|
|
|
|
textl = [text]
|
2023-02-03 16:46:48 -05:00
|
|
|
|
|
|
|
|
|
def splittext(l):
|
|
|
|
|
if len(l[0]) > 450:
|
|
|
|
|
space = l[0].rfind(" ", 0, 430)
|
|
|
|
|
if space == -1:
|
|
|
|
|
space = 450
|
|
|
|
|
elif l[0][space + 1 : space + 5] == "</c>":
|
|
|
|
|
space = space + 4
|
|
|
|
|
a = l[0][0 : space + 1]
|
|
|
|
|
b = l[0][space + 1 :]
|
|
|
|
|
if a.count("<c") > a.count("</c>"):
|
|
|
|
|
# oh god ctags will break!! D=
|
|
|
|
|
hanging = []
|
|
|
|
|
usedends = []
|
|
|
|
|
c = a.rfind("<c")
|
|
|
|
|
while c != -1:
|
|
|
|
|
d = a.find("</c>", c)
|
|
|
|
|
while d in usedends:
|
|
|
|
|
d = a.find("</c>", d + 1)
|
|
|
|
|
if d != -1:
|
|
|
|
|
usedends.append(d)
|
|
|
|
|
else:
|
|
|
|
|
f = a.find(">", c) + 1
|
|
|
|
|
hanging.append(a[c:f])
|
|
|
|
|
c = a.rfind("<c", 0, c)
|
|
|
|
|
|
|
|
|
|
# end all ctags in first part
|
|
|
|
|
for _ in range(a.count("<c") - a.count("</c>")):
|
|
|
|
|
a = a + "</c>"
|
|
|
|
|
# start them up again in the second part
|
|
|
|
|
for c in hanging:
|
|
|
|
|
b = c + b
|
2023-02-09 18:14:33 -05:00
|
|
|
|
if b: # len > 0
|
2023-02-03 16:46:48 -05:00
|
|
|
|
return [a] + splittext([b])
|
2023-02-11 18:54:11 -05:00
|
|
|
|
return [a]
|
2023-02-11 19:25:06 -05:00
|
|
|
|
return l
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-03 16:46:48 -05:00
|
|
|
|
textl = splittext(textl)
|
2023-02-09 18:14:33 -05:00
|
|
|
|
for t in textl:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg(handle, t)
|
2023-02-09 18:14:33 -05:00
|
|
|
|
|
|
|
|
|
@QtCore.pyqtSlot(str, str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def send_ctcp(self, handle, text):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send CTCP message, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.ctcp(handle, text)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str, bool)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def start_convo(self, handle, initiated):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send convo begin message and color, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg(handle, f"COLOR >{self.mainwindow.profile().colorcmd()}")
|
2023-02-03 16:46:48 -05:00
|
|
|
|
if initiated:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg(handle, "PESTERCHUM:BEGIN")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def end_convo(self, handle):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send convo cease message, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg(handle, "PESTERCHUM:CEASE")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2011-02-05 12:17:33 -05:00
|
|
|
|
@QtCore.pyqtSlot()
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def update_mood(self):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Update and send mood, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
mood = self.mainwindow.profile().mood.value_str()
|
2023-02-03 16:46:48 -05:00
|
|
|
|
# Moods via metadata
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.metadata("*", "set", "mood", mood)
|
2023-02-03 16:46:48 -05:00
|
|
|
|
# Backwards compatibility
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg("#pesterchum", f"MOOD >{mood}")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2011-02-05 12:17:33 -05:00
|
|
|
|
@QtCore.pyqtSlot()
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def update_color(self):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Update and send color, slot is called from main thread."""
|
2023-02-03 16:46:48 -05:00
|
|
|
|
# Update color metadata field
|
|
|
|
|
color = self.mainwindow.profile().color
|
2023-02-19 15:10:35 -05:00
|
|
|
|
self._send_irc.metadata("*", "set", "color", color.name())
|
2023-02-03 16:46:48 -05:00
|
|
|
|
# Send color messages
|
2023-02-09 18:14:33 -05:00
|
|
|
|
for convo in list(self.mainwindow.convos.keys()):
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg(
|
2023-02-09 18:14:33 -05:00
|
|
|
|
convo,
|
2023-02-09 19:29:23 -05:00
|
|
|
|
f"COLOR >{self.mainwindow.profile().colorcmd()}",
|
2023-02-09 14:02:29 -05:00
|
|
|
|
)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def blocked_chum(self, handle):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send block message, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg(handle, "PESTERCHUM:BLOCK")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def unblocked_chum(self, handle):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send unblock message, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.privmsg(handle, "PESTERCHUM:UNBLOCK")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def request_names(self, channel):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send NAMES to request channel members, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.names(channel)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2011-02-05 12:17:33 -05:00
|
|
|
|
@QtCore.pyqtSlot()
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def request_channel_list(self):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send LIST to request list of channels, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.list()
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def join_channel(self, channel):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send JOIN and MODE to join channel and get modes, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.join(channel)
|
|
|
|
|
self._send_irc.mode(channel)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def left_channel(self, channel):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send PART to leave channel, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.part(channel)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str, str, str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def kick_user(self, channel, user, reason=""):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send KICK message to kick user from channel, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.kick(channel, user, reason)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str, str, str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def set_channel_mode(self, channel, mode, command):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send MODE to set channel mode, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.mode(channel, mode, command)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def channel_names(self, channel):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send block message, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.names(channel)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str, str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def invite_chum(self, handle, channel):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send INVITE message to invite someone to a channel, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.invite(handle, channel)
|
2011-02-05 12:17:33 -05:00
|
|
|
|
|
2011-06-23 12:02:20 -04:00
|
|
|
|
@QtCore.pyqtSlot()
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def ping_server(self):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send PING to server to verify connectivity, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.ping("B33")
|
2011-06-23 12:02:20 -04:00
|
|
|
|
|
2011-06-28 19:26:13 -04:00
|
|
|
|
@QtCore.pyqtSlot(bool)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def set_away(self, away=True):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send AWAY to update away status, slot is called from main thread."""
|
2023-02-03 16:46:48 -05:00
|
|
|
|
if away:
|
2023-02-12 15:13:02 -05:00
|
|
|
|
self._send_irc.away("Idle")
|
2023-02-03 16:46:48 -05:00
|
|
|
|
else:
|
2023-02-12 15:13:02 -05:00
|
|
|
|
self._send_irc.away()
|
2011-06-28 19:26:13 -04:00
|
|
|
|
|
2023-02-09 18:14:33 -05:00
|
|
|
|
@QtCore.pyqtSlot(str, str)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def kill_some_quirks(self, channel, handle):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send NOQUIRKS ctcp message, disables quirks. Slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.ctcp(channel, "NOQUIRKS", handle)
|
2022-07-05 15:45:16 -04:00
|
|
|
|
|
|
|
|
|
@QtCore.pyqtSlot()
|
2023-02-11 19:12:42 -05:00
|
|
|
|
def disconnect_irc(self):
|
2023-02-11 18:54:11 -05:00
|
|
|
|
"""Send QUIT and close connection, slot is called from main thread."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.quit(f"{_pcVersion} <3")
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self._end = True
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._close()
|
2023-02-09 14:02:29 -05:00
|
|
|
|
|
2023-02-19 15:38:00 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
|
|
|
|
def send_nick(self, nick: str):
|
|
|
|
|
self._send_irc.nick(nick)
|
|
|
|
|
|
2023-02-25 12:18:19 -05:00
|
|
|
|
@QtCore.pyqtSlot(str)
|
|
|
|
|
def send_authenticate(self, msg):
|
|
|
|
|
"""Called from main thread via signal, send requirements."""
|
|
|
|
|
self._send_irc.authenticate(msg)
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _notice(self, nick, chan, msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Standard IRC 'NOTICE' message, primarily used for automated replies from services."""
|
2022-10-07 16:51:40 -04:00
|
|
|
|
handle = nick[0 : nick.find("!")]
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.info('---> recv "NOTICE %s :%s"', handle, msg)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
if (
|
|
|
|
|
handle == "ChanServ"
|
2023-02-03 16:46:48 -05:00
|
|
|
|
and chan == self.mainwindow.profile().handle
|
2022-10-07 16:51:40 -04:00
|
|
|
|
and msg[0:2] == "[#"
|
|
|
|
|
):
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.memoReceived.emit(msg[1 : msg.index("]")], handle, msg)
|
2011-05-09 20:19:44 -04:00
|
|
|
|
else:
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.noticeReceived.emit(handle, msg)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _metadata(self, _target, nick, key, _visibility, value):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""METADATA DRAFT metadata message from server.
|
|
|
|
|
|
|
|
|
|
The format of the METADATA server notication is:
|
|
|
|
|
METADATA <Target> <Key> <Visibility> <Value>
|
|
|
|
|
"""
|
2023-02-09 19:29:23 -05:00
|
|
|
|
if key.casefold() == "mood":
|
2023-02-11 18:36:36 -05:00
|
|
|
|
if is_valid_mood(value[0]):
|
|
|
|
|
mood = Mood(int(value[0]))
|
|
|
|
|
else:
|
|
|
|
|
PchumLog.warning(
|
|
|
|
|
"Mood index '%s' from '%s' is not valid.", value[0], nick
|
|
|
|
|
)
|
|
|
|
|
mood = Mood(0)
|
|
|
|
|
self.moodUpdated.emit(nick, mood)
|
2023-02-09 19:29:23 -05:00
|
|
|
|
elif key.casefold() == "color":
|
2023-02-17 13:41:13 -05:00
|
|
|
|
try:
|
|
|
|
|
if QtGui.QColor.isValidColorName(value):
|
|
|
|
|
color = QtGui.QColor.fromString(value)
|
|
|
|
|
else:
|
|
|
|
|
color = QtGui.QColor(0, 0, 0)
|
|
|
|
|
except AttributeError:
|
|
|
|
|
# PyQt5?
|
|
|
|
|
color = QtGui.QColor(value)
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.colorUpdated.emit(nick, color)
|
2022-06-05 20:05:00 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _tagmsg(self, prefix, tags, *args):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""IRCv3 'TAGMSG' message/command, contains a tag without a command.
|
|
|
|
|
|
|
|
|
|
For reference see:
|
|
|
|
|
https://ircv3.net/specs/extensions/message-tags.html#the-tagmsg-tag-only-message
|
|
|
|
|
"""
|
|
|
|
|
PchumLog.info("TAGMSG: %s %s %s", prefix, tags, args)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
message_tags = tags[1:].split(";")
|
2023-02-09 17:58:03 -05:00
|
|
|
|
for tag in message_tags:
|
|
|
|
|
if tag.startswith("+pesterchum"):
|
2022-06-05 20:05:00 -04:00
|
|
|
|
# Pesterchum tag
|
|
|
|
|
try:
|
2023-02-09 17:58:03 -05:00
|
|
|
|
key, value = tag.split("=")
|
2022-06-05 20:05:00 -04:00
|
|
|
|
except ValueError:
|
|
|
|
|
return
|
2023-02-09 17:58:03 -05:00
|
|
|
|
PchumLog.info("Pesterchum tag: %s=%s", key, value)
|
2022-06-06 15:10:31 -04:00
|
|
|
|
# PESTERCHUM: syntax check
|
2023-02-09 18:14:33 -05:00
|
|
|
|
if value in [
|
|
|
|
|
"BEGIN",
|
|
|
|
|
"BLOCK",
|
|
|
|
|
"CEASE",
|
|
|
|
|
"BLOCK",
|
|
|
|
|
"BLOCKED",
|
|
|
|
|
"UNBLOCK",
|
|
|
|
|
"IDLE",
|
|
|
|
|
"ME",
|
|
|
|
|
]:
|
2022-06-06 15:10:31 -04:00
|
|
|
|
# Process like it's a PESTERCHUM: PRIVMSG
|
|
|
|
|
msg = "PESTERCHUM:" + value
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._privmsg(prefix, args[0], msg)
|
2022-06-06 15:10:31 -04:00
|
|
|
|
elif value.startswith("COLOR>"):
|
|
|
|
|
# Process like it's a COLOR >0,0,0 PRIVMSG
|
2022-10-07 16:51:40 -04:00
|
|
|
|
msg = value.replace(">", " >")
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._privmsg(prefix, args[0], msg)
|
2022-06-06 15:10:31 -04:00
|
|
|
|
elif value.startswith("TIME>"):
|
|
|
|
|
# Process like it's a PESTERCHUM:TIME> PRIVMSG
|
|
|
|
|
msg = "PESTERCHUM:" + value
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._privmsg(prefix, args[0], msg)
|
2022-06-06 15:10:31 -04:00
|
|
|
|
else:
|
2022-06-05 20:05:00 -04:00
|
|
|
|
# Invalid syntax
|
|
|
|
|
PchumLog.warning("TAGMSG with invalid syntax.")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _ping(self, _prefix, token):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""'PING' command from server, we respond with PONG and a matching token."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.pong(token)
|
2023-02-09 17:58:03 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _error(self, *params):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""'ERROR' message from server, the server is terminating our connection."""
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.stop_irc = " ".join(params).strip()
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.disconnectIRC()
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def __ctcp(self, nick: str, chan: str, msg: str):
|
|
|
|
|
"""Client-to-client protocol handling.
|
|
|
|
|
|
|
|
|
|
Called by _privmsg. CTCP messages are PRIVMSG messages wrapped in '\x01' characters.
|
|
|
|
|
"""
|
|
|
|
|
msg = msg.strip("\x01") # We already know this is a CTCP message.
|
2022-10-07 16:51:40 -04:00
|
|
|
|
handle = nick[0 : nick.find("!")]
|
2023-02-11 18:36:36 -05:00
|
|
|
|
# ACTION, IRC /me (The CTCP kind)
|
|
|
|
|
if msg.startswith("ACTION "):
|
|
|
|
|
self._privmsg(nick, chan, f"/me {msg[7:]}")
|
|
|
|
|
# VERSION, return version.
|
|
|
|
|
elif msg.startswith("VERSION"):
|
|
|
|
|
self._send_irc.ctcp_reply(handle, "VERSION", f"Pesterchum {_pcVersion}")
|
|
|
|
|
# CLIENTINFO, return supported CTCP commands.
|
|
|
|
|
elif msg.startswith("CLIENTINFO"):
|
|
|
|
|
self._send_irc.ctcp_reply(
|
|
|
|
|
handle,
|
|
|
|
|
"CLIENTINFO",
|
|
|
|
|
"ACTION VERSION CLIENTINFO PING SOURCE NOQUIRKS",
|
|
|
|
|
)
|
|
|
|
|
# PING, return pong.
|
|
|
|
|
elif msg.startswith("PING"):
|
|
|
|
|
self._send_irc.ctcp_reply(handle, "PING", msg[4:])
|
|
|
|
|
# SOURCE, return source code link.
|
|
|
|
|
elif msg.startswith("SOURCE"):
|
|
|
|
|
self._send_irc.ctcp_reply(
|
|
|
|
|
handle,
|
|
|
|
|
"SOURCE",
|
|
|
|
|
"https://github.com/Dpeta/pesterchum-alt-servers",
|
|
|
|
|
)
|
|
|
|
|
# ???
|
2023-02-11 20:28:31 -05:00
|
|
|
|
else:
|
|
|
|
|
PchumLog.warning("Unknown CTCP command '%s' from %s to %s", msg, nick, chan)
|
2023-02-11 18:36:36 -05:00
|
|
|
|
|
|
|
|
|
def _privmsg(self, nick: str, chan: str, msg: str):
|
|
|
|
|
"""'PRIVMSG' message from server, the standard message."""
|
2023-02-03 16:46:48 -05:00
|
|
|
|
if not msg: # Length 0
|
2011-02-07 13:40:05 -05:00
|
|
|
|
return
|
2023-02-11 18:36:36 -05:00
|
|
|
|
handle = nick[0 : nick.find("!")]
|
|
|
|
|
chan = (
|
|
|
|
|
chan.lower()
|
|
|
|
|
) # Channel capitalization not guarenteed, casefold() too aggressive.
|
|
|
|
|
|
|
|
|
|
# CTCP, indicated by a message wrapped in '\x01' characters.
|
|
|
|
|
# Only checking for the first character is recommended by the protocol.
|
|
|
|
|
if msg[0] == "\x01":
|
|
|
|
|
self.__ctcp(nick, chan, msg)
|
2011-02-05 12:17:33 -05:00
|
|
|
|
return
|
2016-12-11 04:01:48 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
if chan.startswith("#"):
|
|
|
|
|
# PRIVMSG to chnnale
|
|
|
|
|
if chan == "#pesterchum":
|
|
|
|
|
# follow instructions
|
|
|
|
|
if msg.startswith("MOOD >"):
|
|
|
|
|
if is_valid_mood(msg[6:]):
|
|
|
|
|
mood = Mood(int(msg[6:]))
|
|
|
|
|
else:
|
|
|
|
|
PchumLog.warning(
|
|
|
|
|
"Mood index '%s' from '%s' is not valid.", msg[6:], handle
|
|
|
|
|
)
|
|
|
|
|
mood = Mood(0)
|
|
|
|
|
self.moodUpdated.emit(handle, mood)
|
2023-02-11 20:28:31 -05:00
|
|
|
|
elif msg.startswith("GETMOOD"):
|
2023-02-11 18:36:36 -05:00
|
|
|
|
mychumhandle = self.mainwindow.profile().handle
|
|
|
|
|
if mychumhandle in msg:
|
|
|
|
|
mymood = self.mainwindow.profile().mood.value_str()
|
|
|
|
|
self._send_irc.privmsg("#pesterchum", f"MOOD >{mymood}")
|
2011-02-05 12:17:33 -05:00
|
|
|
|
else:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
if msg.startswith("PESTERCHUM:TIME>"):
|
|
|
|
|
self.timeCommand.emit(chan, handle, msg[16:])
|
|
|
|
|
else:
|
|
|
|
|
self.memoReceived.emit(chan, handle, msg)
|
2011-02-05 12:17:33 -05:00
|
|
|
|
else:
|
2023-02-11 18:36:36 -05:00
|
|
|
|
# Direct person-to-person PRIVMSG messages
|
|
|
|
|
if msg.startswith("COLOR >"):
|
|
|
|
|
if is_valid_rgb_color(msg[7:]):
|
|
|
|
|
colors = msg[7:].split(",")
|
2011-02-05 12:17:33 -05:00
|
|
|
|
colors = [int(d) for d in colors]
|
2023-02-11 18:36:36 -05:00
|
|
|
|
color = QtGui.QColor(*colors)
|
|
|
|
|
elif QtGui.QColor.isValidColorName(msg[7:]):
|
|
|
|
|
color = QtGui.QColor.fromString(msg[7:])
|
|
|
|
|
else:
|
|
|
|
|
color = QtGui.QColor(0, 0, 0)
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.colorUpdated.emit(handle, color)
|
2011-02-05 12:17:33 -05:00
|
|
|
|
else:
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.messageReceived.emit(handle, msg)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _quit(self, nick, reason):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""QUIT message from server, a client has quit the server."""
|
2022-10-07 16:51:40 -04:00
|
|
|
|
handle = nick[0 : nick.find("!")]
|
2023-02-09 17:58:03 -05:00
|
|
|
|
PchumLog.info('---> recv "QUIT %s: %s"', handle, reason)
|
2023-02-03 16:46:48 -05:00
|
|
|
|
if handle == self.mainwindow.randhandler.randNick:
|
|
|
|
|
self.mainwindow.randhandler.setRunning(False)
|
|
|
|
|
server = self.mainwindow.config.server()
|
2022-10-07 16:51:40 -04:00
|
|
|
|
baseserver = server[server.rfind(".", 0, server.rfind(".")) :]
|
2011-07-17 04:58:19 -04:00
|
|
|
|
if reason.count(baseserver) == 2:
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.userPresentUpdate.emit(handle, "", "netsplit")
|
2011-07-17 04:58:19 -04:00
|
|
|
|
else:
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.userPresentUpdate.emit(handle, "", "quit")
|
|
|
|
|
self.moodUpdated.emit(handle, Mood("offline"))
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-12 16:43:25 -05:00
|
|
|
|
def _kick(self, channel_operator, channel, handle, reason):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""'KICK' message from server, someone got kicked from a channel."""
|
2023-02-12 16:43:25 -05:00
|
|
|
|
channel_operator_nick = channel_operator[0 : channel_operator.find("!")]
|
|
|
|
|
self.userPresentUpdate.emit(
|
|
|
|
|
handle, channel, f"kick:{channel_operator_nick}:{reason}"
|
|
|
|
|
)
|
2011-02-06 01:02:39 -05:00
|
|
|
|
# ok i shouldnt be overloading that but am lazy
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _part(self, nick, channel, _reason="nanchos"):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""'PART' message from server, someone left a channel."""
|
2022-10-07 16:51:40 -04:00
|
|
|
|
handle = nick[0 : nick.find("!")]
|
2023-02-09 19:29:23 -05:00
|
|
|
|
PchumLog.info('---> recv "PART %s: %s"', handle, channel)
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.userPresentUpdate.emit(handle, channel, "left")
|
2011-02-05 12:17:33 -05:00
|
|
|
|
if channel == "#pesterchum":
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.moodUpdated.emit(handle, Mood("offline"))
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _join(self, nick, channel):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""'JOIN' message from server, someone joined a channel."""
|
2022-10-07 16:51:40 -04:00
|
|
|
|
handle = nick[0 : nick.find("!")]
|
2023-02-09 19:29:23 -05:00
|
|
|
|
PchumLog.info('---> recv "JOIN %s: %s"', handle, channel)
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.userPresentUpdate.emit(handle, channel, "join")
|
2011-02-05 12:17:33 -05:00
|
|
|
|
if channel == "#pesterchum":
|
2023-02-03 16:46:48 -05:00
|
|
|
|
if handle == self.mainwindow.randhandler.randNick:
|
|
|
|
|
self.mainwindow.randhandler.setRunning(True)
|
|
|
|
|
self.moodUpdated.emit(handle, Mood("chummy"))
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-12 16:43:25 -05:00
|
|
|
|
def _mode(self, op, channel, mode_msg, *handles):
|
2023-02-12 11:25:07 -05:00
|
|
|
|
"""'MODE' message from server, a user or a channel's mode changed.
|
|
|
|
|
|
|
|
|
|
This and the functions it calls to in the main thread seem pretty broken,
|
|
|
|
|
modes that aren't internally tracked aren't updated correctly."""
|
|
|
|
|
|
2023-02-12 16:43:25 -05:00
|
|
|
|
if not handles: # len 0
|
2022-10-07 16:51:40 -04:00
|
|
|
|
handles = [""]
|
2023-02-12 11:25:07 -05:00
|
|
|
|
|
2021-08-24 09:49:50 -04:00
|
|
|
|
# Channel section
|
2023-02-12 16:43:25 -05:00
|
|
|
|
# Okay so, as I understand it channel modes will always be applied to a
|
|
|
|
|
# channel even if the commands also sets a mode to a user.
|
|
|
|
|
# So "MODE #channel +ro handleHandle" will set +r to channel #channel
|
|
|
|
|
# as well as set +o to handleHandle.
|
|
|
|
|
#
|
2023-02-12 11:25:07 -05:00
|
|
|
|
# EXPIRIMENTAL FIX
|
2023-02-12 16:43:25 -05:00
|
|
|
|
# No clue how stable this is,
|
|
|
|
|
# but since it doesn't seem to cause a crash it's probably an improvement.
|
2021-08-24 09:49:50 -04:00
|
|
|
|
# This might be clunky with non-unrealircd IRC servers
|
|
|
|
|
channel_mode = ""
|
2023-02-09 14:44:48 -05:00
|
|
|
|
unrealircd_channel_modes = "cCdfGHikKLlmMNnOPpQRrsSTtVzZ"
|
2023-02-12 16:43:25 -05:00
|
|
|
|
if any(md in mode_msg for md in unrealircd_channel_modes):
|
2021-08-24 09:49:50 -04:00
|
|
|
|
PchumLog.debug("Channel mode in string.")
|
2023-02-03 16:46:48 -05:00
|
|
|
|
modes = list(self.mainwindow.modes)
|
2021-08-24 09:49:50 -04:00
|
|
|
|
for md in unrealircd_channel_modes:
|
2023-02-12 16:43:25 -05:00
|
|
|
|
if mode_msg.find(md) != -1: # -1 means not found
|
|
|
|
|
if mode_msg[0] == "+":
|
2021-08-24 09:49:50 -04:00
|
|
|
|
modes.extend(md)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
channel_mode = "+" + md
|
2023-02-12 16:43:25 -05:00
|
|
|
|
elif mode_msg[0] == "-":
|
2021-08-24 09:49:50 -04:00
|
|
|
|
try:
|
|
|
|
|
modes.remove(md)
|
|
|
|
|
channel_mode = "-" + md
|
|
|
|
|
except ValueError:
|
2022-10-07 16:51:40 -04:00
|
|
|
|
PchumLog.warning(
|
|
|
|
|
"Can't remove channel mode that isn't set."
|
|
|
|
|
)
|
2023-02-12 16:43:25 -05:00
|
|
|
|
self.userPresentUpdate.emit("", channel, f"{channel_mode}:{op}")
|
|
|
|
|
mode_msg = mode_msg.replace(md, "")
|
2011-05-12 02:28:07 -04:00
|
|
|
|
modes.sort()
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.mainwindow.modes = "+" + "".join(modes)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2011-05-25 01:51:51 -04:00
|
|
|
|
modes = []
|
|
|
|
|
cur = "+"
|
2023-02-12 16:43:25 -05:00
|
|
|
|
for l in mode_msg:
|
2022-10-07 16:51:40 -04:00
|
|
|
|
if l in ["+", "-"]:
|
|
|
|
|
cur = l
|
2011-05-25 01:51:51 -04:00
|
|
|
|
else:
|
2023-02-12 16:43:25 -05:00
|
|
|
|
modes.append(f"{cur}{l}")
|
|
|
|
|
for index, mode in enumerate(modes):
|
2021-08-24 09:49:50 -04:00
|
|
|
|
# Server-set usermodes don't need to be passed.
|
2023-02-12 16:43:25 -05:00
|
|
|
|
if not (handles == [""]) & (("x" in mode) | ("z" in mode) | ("o" in mode)):
|
2022-03-17 00:36:14 -04:00
|
|
|
|
try:
|
2023-02-12 16:43:25 -05:00
|
|
|
|
self.userPresentUpdate.emit(handles[index], channel, f"{mode}:{op}")
|
|
|
|
|
except IndexError as index_except:
|
|
|
|
|
PchumLog.exception("modeSetIndexError: %s", index_except)
|
2023-02-09 17:58:03 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _invite(self, sender, _you, channel):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""'INVITE' message from server, someone invited us to a channel.
|
|
|
|
|
|
|
|
|
|
Pizza party everyone invited!!!"""
|
|
|
|
|
handle = sender.split("!")[0]
|
|
|
|
|
self.inviteReceived.emit(handle, channel)
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _nick(self, oldnick, newnick, _hopcount=0):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""'NICK' message from server, signifies a nick change.
|
|
|
|
|
|
|
|
|
|
Is send when our or someone else's nick got changed willingly or unwillingly."""
|
2023-02-12 16:43:25 -05:00
|
|
|
|
PchumLog.debug("NICK change from '%s' to '%s'.", oldnick, newnick)
|
2022-08-17 05:24:50 -04:00
|
|
|
|
# svsnick
|
2022-07-05 15:45:16 -04:00
|
|
|
|
if oldnick == self.mainwindow.profile().handle:
|
|
|
|
|
# Server changed our handle, svsnick?
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.getSvsnickedOn.emit(oldnick, newnick)
|
2022-07-05 15:45:16 -04:00
|
|
|
|
|
2022-08-17 05:24:50 -04:00
|
|
|
|
# etc.
|
2022-10-07 16:51:40 -04:00
|
|
|
|
oldhandle = oldnick[0 : oldnick.find("!")]
|
2023-02-09 17:58:03 -05:00
|
|
|
|
if self.mainwindow.profile().handle in [newnick, oldhandle]:
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.myHandleChanged.emit(newnick)
|
2011-02-05 12:17:33 -05:00
|
|
|
|
newchum = PesterProfile(newnick, chumdb=self.mainwindow.chumdb)
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.moodUpdated.emit(oldhandle, Mood("offline"))
|
2023-02-09 17:58:03 -05:00
|
|
|
|
self.userPresentUpdate.emit(f"{oldhandle}:{newnick}", "", "nick")
|
2011-02-05 12:17:33 -05:00
|
|
|
|
if newnick in self.mainwindow.chumList.chums:
|
2023-02-11 19:12:42 -05:00
|
|
|
|
self.get_mood(newchum)
|
2023-02-03 16:46:48 -05:00
|
|
|
|
if oldhandle == self.mainwindow.randhandler.randNick:
|
|
|
|
|
self.mainwindow.randhandler.setRunning(False)
|
|
|
|
|
elif newnick == self.mainwindow.randhandler.randNick:
|
|
|
|
|
self.mainwindow.randhandler.setRunning(True)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _welcome(self, _server, _nick, _msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 001 RPL_WELCOME, send when we've connected to the server."""
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.registered_irc = (
|
2023-02-11 18:36:36 -05:00
|
|
|
|
True # Registered as in, the server has accepted our nick & user.
|
|
|
|
|
)
|
2023-02-09 20:33:01 -05:00
|
|
|
|
self.connected.emit() # Alert main thread that we've connected.
|
|
|
|
|
profile = self.mainwindow.profile()
|
2023-02-15 14:57:31 -05:00
|
|
|
|
# Get mood
|
|
|
|
|
mood = profile.mood.value_str()
|
|
|
|
|
# Moods via metadata
|
|
|
|
|
self._send_irc.metadata("*", "sub", "mood")
|
|
|
|
|
self._send_irc.metadata("*", "set", "mood", mood)
|
|
|
|
|
# Color via metadata
|
|
|
|
|
self._send_irc.metadata("*", "sub", "color")
|
|
|
|
|
self._send_irc.metadata("*", "set", "color", profile.color.name())
|
|
|
|
|
# Backwards compatible moods
|
2023-02-17 14:45:09 -05:00
|
|
|
|
if self.mainwindow.config.irc_compatibility_mode():
|
|
|
|
|
return
|
|
|
|
|
self._send_irc.join("#pesterchum")
|
2023-02-15 14:57:31 -05:00
|
|
|
|
self._send_irc.privmsg("#pesterchum", f"MOOD >{mood}")
|
2023-02-09 17:58:03 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _featurelist(self, _target, _handle, *params):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numerical reply 005 RPL_ISUPPORT to communicate supported server features.
|
|
|
|
|
|
|
|
|
|
Not in the original specification.
|
|
|
|
|
Metadata support could be confirmed via CAP ACK/CAP NEK.
|
2023-02-09 18:14:33 -05:00
|
|
|
|
"""
|
2023-02-09 17:58:03 -05:00
|
|
|
|
features = params[:-1]
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.info("Server _featurelist: %s", features)
|
2023-02-15 14:57:31 -05:00
|
|
|
|
if not self.metadata_supported:
|
|
|
|
|
if any(feature.startswith("METADATA") for feature in features):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
PchumLog.info("Server supports metadata.")
|
|
|
|
|
self.metadata_supported = True
|
2023-02-15 16:49:52 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _cap(self, server, nick, subcommand, tag):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""IRCv3 capabilities command from server.
|
|
|
|
|
|
|
|
|
|
See: https://ircv3.net/specs/extensions/capability-negotiation
|
|
|
|
|
"""
|
|
|
|
|
PchumLog.info("CAP %s %s %s %s", server, nick, subcommand, tag)
|
2023-02-24 22:46:59 -05:00
|
|
|
|
if subcommand.casefold() == "nak" and tag.casefold() == "sasl":
|
|
|
|
|
# SASL isn't supported, end CAP negotation.
|
|
|
|
|
self._send_irc.cap("END")
|
2023-02-09 17:58:03 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _umodeis(self, _server, _handle, modes):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 221 RPL_UMODEIS, shows us our user modes."""
|
|
|
|
|
self.mainwindow.modes = modes
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _liststart(self, _server, _handle, *info):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 321 RPL_LISTSTART, start of list of channels."""
|
|
|
|
|
self.channel_list = []
|
|
|
|
|
info = list(info)
|
|
|
|
|
self.channel_field = info.index("Channel") # dunno if this is protocol
|
|
|
|
|
PchumLog.info('---> recv "CHANNELS: %s ', self.channel_field)
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _list(self, _server, _handle, *info):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 322 RPL_LIST, returns part of the list of channels."""
|
|
|
|
|
channel = info[self.channel_field]
|
|
|
|
|
usercount = info[1]
|
|
|
|
|
if channel not in self.channel_list and channel != "#pesterchum":
|
|
|
|
|
self.channel_list.append((channel, usercount))
|
|
|
|
|
PchumLog.info('---> recv "CHANNELS: %s ', channel)
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _listend(self, _server, _handle, _msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 323 RPL_LISTEND, end of a series of LIST replies."""
|
|
|
|
|
PchumLog.info('---> recv "CHANNELS END"')
|
|
|
|
|
self.channelListReceived.emit(PesterList(self.channel_list))
|
|
|
|
|
self.channel_list = []
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _channelmodeis(self, _server, _handle, channel, modes, _mode_params=""):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 324 RPL_CHANNELMODEIS, gives channel modes."""
|
2023-02-12 11:25:07 -05:00
|
|
|
|
PchumLog.debug("324 RPL_CHANNELMODEIS %s: %s", channel, modes)
|
2023-02-09 17:58:03 -05:00
|
|
|
|
self.modesUpdated.emit(channel, modes)
|
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _namreply(self, _server, _nick, _op, channel, names):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 353 RPL_NAMREPLY, part of a NAMES list of members, usually of a channel."""
|
2011-02-15 12:10:57 -05:00
|
|
|
|
namelist = names.split(" ")
|
2023-02-09 17:58:03 -05:00
|
|
|
|
PchumLog.info('---> recv "NAMES %s: %s names"', channel, len(namelist))
|
2022-10-07 16:51:40 -04:00
|
|
|
|
if not hasattr(self, "channelnames"):
|
2011-02-15 12:10:57 -05:00
|
|
|
|
self.channelnames = {}
|
2011-06-09 05:23:11 -04:00
|
|
|
|
if channel not in self.channelnames:
|
2011-02-15 12:10:57 -05:00
|
|
|
|
self.channelnames[channel] = []
|
|
|
|
|
self.channelnames[channel].extend(namelist)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _endofnames(self, _server, _nick, channel, _msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 366 RPL_ENDOFNAMES, end of NAMES list of members, usually of a channel."""
|
2023-02-28 18:01:38 -05:00
|
|
|
|
namelist = None
|
2022-08-17 05:24:50 -04:00
|
|
|
|
try:
|
|
|
|
|
namelist = self.channelnames[channel]
|
|
|
|
|
except KeyError:
|
|
|
|
|
# EON seems to return with wrong capitalization sometimes?
|
2023-02-09 17:58:03 -05:00
|
|
|
|
for channel_name in self.channelnames:
|
|
|
|
|
if channel.casefold() == channel_name.casefold():
|
|
|
|
|
channel = channel_name
|
2022-08-17 05:24:50 -04:00
|
|
|
|
namelist = self.channelnames[channel]
|
2023-02-28 18:01:38 -05:00
|
|
|
|
if channel in self.channelnames:
|
|
|
|
|
self.channelnames.pop(channel)
|
|
|
|
|
if not namelist:
|
|
|
|
|
return
|
2023-02-09 17:58:03 -05:00
|
|
|
|
self.namesReceived.emit(channel, PesterList(namelist))
|
2023-02-09 14:02:29 -05:00
|
|
|
|
if channel == "#pesterchum" and not self.joined:
|
2011-06-13 11:13:56 -04:00
|
|
|
|
self.joined = True
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.mainwindow.randhandler.setRunning(
|
|
|
|
|
self.mainwindow.randhandler.randNick in namelist
|
2022-10-07 16:51:40 -04:00
|
|
|
|
)
|
2011-06-09 05:23:11 -04:00
|
|
|
|
chums = self.mainwindow.chumList.chums
|
2021-02-23 10:21:57 -05:00
|
|
|
|
lesschums = []
|
2023-02-09 17:58:03 -05:00
|
|
|
|
for chum in chums:
|
|
|
|
|
if chum.handle in namelist:
|
|
|
|
|
lesschums.append(chum)
|
2023-02-11 19:12:42 -05:00
|
|
|
|
self.get_mood(*lesschums)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _cannotsendtochan(self, _server, _handle, channel, msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 404 ERR_CANNOTSENDTOCHAN, we aren't in the channel or don't have voice."""
|
|
|
|
|
self.cannotSendToChan.emit(channel, msg)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _erroneusnickname(self, *args):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 432 ERR_ERRONEUSNICKNAME, we have a forbidden or protocol-breaking nick."""
|
|
|
|
|
# Server is not allowing us to connect.
|
|
|
|
|
reason = "Handle is not allowed on this server.\n" + " ".join(args)
|
2023-02-11 19:25:06 -05:00
|
|
|
|
self.stop_irc = reason.strip()
|
2023-02-09 17:58:03 -05:00
|
|
|
|
self.disconnectIRC()
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _nicknameinuse(self, _server, _cmd, nick, _msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numerical reply 433 ERR_NICKNAMEINUSE, raised when changing nick to nick in use."""
|
2023-02-09 19:29:23 -05:00
|
|
|
|
self._reset_nick(nick)
|
2011-04-25 04:04:09 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _nickcollision(self, _server, _cmd, nick, _msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numerical reply 436 ERR_NICKCOLLISION, raised during connect when nick is in use."""
|
2023-02-09 19:29:23 -05:00
|
|
|
|
self._reset_nick(nick)
|
|
|
|
|
|
|
|
|
|
def _reset_nick(self, oldnick):
|
|
|
|
|
"""Set our nick to a random pesterClient."""
|
2023-02-12 11:25:07 -05:00
|
|
|
|
random_number = int(random.random() * 9999) # Random int in range 0 <---> 9999
|
2023-02-09 19:29:23 -05:00
|
|
|
|
newnick = f"pesterClient{random_number}"
|
2023-02-11 18:36:36 -05:00
|
|
|
|
self._send_irc.nick(newnick)
|
2023-02-09 19:29:23 -05:00
|
|
|
|
self.nickCollision.emit(oldnick, newnick)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _forbiddenchannel(self, _server, handle, channel, msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 448 'forbiddenchannel' reply, channel is forbidden.
|
|
|
|
|
|
|
|
|
|
Not in the specification but used by UnrealIRCd."""
|
|
|
|
|
self.signal_forbiddenchannel.emit(channel, msg)
|
|
|
|
|
self.userPresentUpdate.emit(handle, channel, "left")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _inviteonlychan(self, _server, _handle, channel, _msg):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""Numeric reply 473 ERR_INVITEONLYCHAN, can't join channel (+i)."""
|
2023-02-03 16:46:48 -05:00
|
|
|
|
self.chanInviteOnly.emit(channel)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _keyvalue(self, _target, _handle_us, handle_owner, key, _visibility, *value):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""METADATA DRAFT numeric reply 761 RPL_KEYVALUE, we received the value of a key.
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-09 17:58:03 -05:00
|
|
|
|
The format of the METADATA server notication is:
|
|
|
|
|
METADATA <Target> <Key> <Visibility> <Value>
|
|
|
|
|
"""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
if key.casefold() == "mood":
|
|
|
|
|
if is_valid_mood(value[0]):
|
|
|
|
|
mood = Mood(int(value[0]))
|
|
|
|
|
else:
|
|
|
|
|
PchumLog.warning(
|
|
|
|
|
"Mood index '%s' from '%s' is not valid.", value[0], handle_owner
|
|
|
|
|
)
|
|
|
|
|
mood = Mood(0)
|
2023-02-09 17:58:03 -05:00
|
|
|
|
self.moodUpdated.emit(handle_owner, mood)
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _nomatchingkey(self, _target, _our_handle, failed_handle, _key, *_error):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""METADATA DRAFT numeric reply 766 ERR_NOMATCHINGKEY, no matching key."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.info("_nomatchingkey: %s", failed_handle)
|
2023-02-11 20:28:31 -05:00
|
|
|
|
# No point in GETMOOD-ing services
|
|
|
|
|
# Fallback to the normal GETMOOD method if getting mood via metadata fails.
|
2023-02-09 17:58:03 -05:00
|
|
|
|
if failed_handle.casefold() not in SERVICES:
|
2023-02-11 20:28:31 -05:00
|
|
|
|
self._send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _keynotset(self, _target, _our_handle, failed_handle, _key, *_error):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""METADATA DRAFT numeric reply 768 ERR_KEYNOTSET, key isn't set."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.info("_keynotset: %s", failed_handle)
|
2023-02-11 20:28:31 -05:00
|
|
|
|
# Fallback to the normal GETMOOD method if getting mood via metadata fails.
|
2023-02-09 17:58:03 -05:00
|
|
|
|
if failed_handle.casefold() not in SERVICES:
|
2023-02-11 20:28:31 -05:00
|
|
|
|
self._send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}")
|
2023-02-09 14:48:03 -05:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _keynopermission(self, _target, _our_handle, failed_handle, _key, *_error):
|
2023-02-09 17:58:03 -05:00
|
|
|
|
"""METADATA DRAFT numeric reply 769 ERR_KEYNOPERMISSION, no permission for key."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.info("_keynopermission: %s", failed_handle)
|
2023-02-11 20:28:31 -05:00
|
|
|
|
# Fallback to the normal GETMOOD method if getting mood via metadata fails.
|
2023-02-09 17:58:03 -05:00
|
|
|
|
if failed_handle.casefold() not in SERVICES:
|
2023-02-11 20:28:31 -05:00
|
|
|
|
self._send_irc.privmsg("#pesterchum", f"GETMOOD {failed_handle}")
|
2022-10-07 16:51:40 -04:00
|
|
|
|
|
2023-02-11 18:36:36 -05:00
|
|
|
|
def _metadatasubok(self, *params):
|
2023-02-09 18:14:33 -05:00
|
|
|
|
""" "METADATA DRAFT numeric reply 770 RPL_METADATASUBOK, we subbed to a key."""
|
2023-02-11 18:36:36 -05:00
|
|
|
|
PchumLog.info("_metadatasubok: %s", params)
|
2011-06-23 12:02:20 -04:00
|
|
|
|
|
2023-02-24 22:46:59 -05:00
|
|
|
|
def _authenticate(self, _, token):
|
|
|
|
|
"""Handle IRCv3 SASL authneticate command from server."""
|
|
|
|
|
if token == "+":
|
|
|
|
|
# Try to send password now
|
|
|
|
|
self._send_irc.authenticate(
|
|
|
|
|
nick=self.mainwindow.profile().handle,
|
|
|
|
|
password=self.mainwindow.userprofile.getNickServPass(),
|
|
|
|
|
)
|
|
|
|
|
|
2023-02-25 15:22:36 -05:00
|
|
|
|
def _sasl_skill_issue(self, *_msg):
|
|
|
|
|
"""Handles all responses from server that indicate SASL authentication failed.
|
|
|
|
|
|
2023-02-28 17:32:35 -05:00
|
|
|
|
Replies that indicate we can't authenticate include: 902, 904, 905.
|
|
|
|
|
Aborts SASL by sending CAP END, ending capability negotiation."""
|
|
|
|
|
self.end_cap_negotiation()
|
2023-02-24 22:46:59 -05:00
|
|
|
|
|
|
|
|
|
def _saslsuccess(self, *_msg):
|
2023-02-25 15:22:36 -05:00
|
|
|
|
"""Handle 'RPL_SASLSUCCESS' reply from server, SASL authentication succeeded! woo yeah!!"""
|
2023-02-28 17:32:35 -05:00
|
|
|
|
self.end_cap_negotiation()
|
2023-02-24 22:46:59 -05:00
|
|
|
|
|
2023-02-19 13:04:16 -05:00
|
|
|
|
moodUpdated = QtCore.pyqtSignal(str, Mood)
|
|
|
|
|
colorUpdated = QtCore.pyqtSignal(str, QtGui.QColor)
|
|
|
|
|
messageReceived = QtCore.pyqtSignal(str, str)
|
|
|
|
|
memoReceived = QtCore.pyqtSignal(str, str, str)
|
|
|
|
|
noticeReceived = QtCore.pyqtSignal(str, str)
|
|
|
|
|
inviteReceived = QtCore.pyqtSignal(str, str)
|
|
|
|
|
timeCommand = QtCore.pyqtSignal(str, str, str)
|
|
|
|
|
namesReceived = QtCore.pyqtSignal(str, PesterList)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
channelListReceived = QtCore.pyqtSignal(PesterList)
|
2023-02-19 13:04:16 -05:00
|
|
|
|
nickCollision = QtCore.pyqtSignal(str, str)
|
|
|
|
|
getSvsnickedOn = QtCore.pyqtSignal(str, str)
|
|
|
|
|
myHandleChanged = QtCore.pyqtSignal(str)
|
|
|
|
|
chanInviteOnly = QtCore.pyqtSignal(str)
|
|
|
|
|
modesUpdated = QtCore.pyqtSignal(str, str)
|
2023-01-31 19:12:43 -05:00
|
|
|
|
connected = QtCore.pyqtSignal()
|
|
|
|
|
askToConnect = QtCore.pyqtSignal(Exception)
|
2023-02-19 13:04:16 -05:00
|
|
|
|
userPresentUpdate = QtCore.pyqtSignal(str, str, str)
|
|
|
|
|
cannotSendToChan = QtCore.pyqtSignal(str, str)
|
|
|
|
|
signal_forbiddenchannel = QtCore.pyqtSignal(str, str)
|
2023-02-28 17:32:35 -05:00
|
|
|
|
cap_negotation_started = QtCore.pyqtSignal()
|