From 979173c33d07c6d32a87aaa054bc0dba905cf6ab Mon Sep 17 00:00:00 2001
From: Dpeta <jasprose@protonmail.com>
Date: Mon, 6 Jun 2022 02:05:00 +0200
Subject: [PATCH] PESTERCHUM: via TAGMSG (incl. Client Capability Negotiation +
 Message Tags)

---
 irc.py             | 41 +++++++++++++++++++++++++++++++++++++++++
 oyoyo/client.py    |  8 ++++++--
 oyoyo/helpers.py   |  5 +++++
 oyoyo/ircevents.py |  3 ++-
 oyoyo/parse.py     | 26 +++++++++++++++++++++++++-
 5 files changed, 79 insertions(+), 4 deletions(-)

diff --git a/irc.py b/irc.py
index 9c1e10c..6bd4d98 100644
--- a/irc.py
+++ b/irc.py
@@ -23,6 +23,11 @@ PchumLog = logging.getLogger('pchumLogger')
 # Python 3
 QString = str
 
+# Copied from pesterchum.py
+CUSTOMBOTS = ["CALSPRITE", "RANDOMENCOUNTER"]
+BOTNAMES = ["NICKSERV", "CHANSERV", "MEMOSERV", "OPERSERV", "HELPSERV", "HOSTSERV", "BOTSERV"]
+BOTNAMES.extend(CUSTOMBOTS)
+
 #if ostools.isOSXBundle():
 #    logging.basicConfig(level=logging.WARNING)
 #else:
@@ -399,6 +404,33 @@ class PesterHandler(DefaultCommandHandler):
         if key == "mood":
             mood = Mood(int(value))
             self.parent.moodUpdated.emit(nick, mood)
+
+    def tagmsg(self, prefix, tags, *args):
+        PchumLog.info('TAGMSG: %s %s %s' % (prefix, tags, str(args)))
+        message_tags = tags[1:].split(';')
+        for m in message_tags:
+            if m.startswith("+pesterchum"):
+                # Pesterchum tag
+                try:
+                    key, value = m.split('=')
+                except ValueError:
+                    return
+                PchumLog.info('Pesterchum tag: %s=%s' % (key, value))
+                # Uses PESTERCHUM: syntax?
+                if ((value.upper().startswith("BEGIN") == False)
+                    & (value.upper().startswith("BLOCK") == False)
+                    & (value.upper().startswith("CEASE") == False)
+                    & (value.upper().startswith("TIME") == False)):
+                    # Invalid syntax
+                    PchumLog.warning("TAGMSG with invalid syntax.")
+                    return
+                
+                # Process like it's a PESTERCHUM: message
+                # Easiest option since we gotta be backwards compatible anyway :"3
+                msg = "PESTERCHUM:" + value
+                self.privmsg(prefix, args[0], msg)
+                
+            
         
     def privmsg(self, nick, chan, msg):
         handle = nick[0:nick.find("!")]
@@ -496,6 +528,8 @@ class PesterHandler(DefaultCommandHandler):
             # Backwards compatible moods
             helpers.msg(self.client, "#pesterchum", "MOOD >%d" % (mymood))
 
+            helpers.cap(self.client, "REQ", "message-tags")
+
     def keyvalue(self, target, handle_us, handle_owner, key, visibility, *value):
         # The format of the METADATA server notication is:
         # METADATA <Target> <Key> <Visibility> <Value>
@@ -734,6 +768,13 @@ class PesterHandler(DefaultCommandHandler):
         # Try to get mood via metadata get.
         # If it fails the old code is excecuted.
 
+        # If services/bot, assume mood 18.
+        for c in chums:
+            if c.handle.upper() in BOTNAMES:
+                print("True")
+                print(c.handle)
+                PchumLog.info("%s is a bot, setting mood to 18." % (c.handle))
+                self.parent.moodUpdated.emit(c.handle, Mood(18))
         # Wait for server to send welcome to verify RPL_ISUPPORT has been send.
         # Apparently 005 is send after 001 so nvm we gotta wait longer :"3
         timeout = 0
diff --git a/oyoyo/client.py b/oyoyo/client.py
index 34a3146..4992ad6 100644
--- a/oyoyo/client.py
+++ b/oyoyo/client.py
@@ -234,9 +234,13 @@ class IRCClient:
 
                     for el in data:
                         PchumLog.debug("el=%s, data=%s" % (el,data))
-                        prefix, command, args = parse_raw_irc_command(el)
+                        tags, prefix, command, args = parse_raw_irc_command(el)
                         try:
-                            self.command_handler.run(command, prefix, *args)
+                            # 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.debug("CommandError %s" % str(e))
 
diff --git a/oyoyo/helpers.py b/oyoyo/helpers.py
index e991b6f..1012aee 100644
--- a/oyoyo/helpers.py
+++ b/oyoyo/helpers.py
@@ -71,6 +71,11 @@ def metadata(cli, target, subcommand, *params):
     # https://gist.github.com/k4bek4be/92c2937cefd49990fbebd001faf2b237
     cli.send("METADATA", target, subcommand, *params)
 
+def cap(cli, subcommand, *params):
+    # Capability Negotiation
+    # https://ircv3.net/specs/extensions/capability-negotiation.html
+    cli.send("CAP", subcommand, *params)
+
 def msgrandom(cli, choices, dest, user=None):
     o = "%s: " % user if user else ""
     o += random.choice(choices)
diff --git a/oyoyo/ircevents.py b/oyoyo/ircevents.py
index 10dabd0..d4150ae 100644
--- a/oyoyo/ircevents.py
+++ b/oyoyo/ircevents.py
@@ -215,7 +215,8 @@ protocol_events = [
     "quit",
     "invite",
     "pong",
-    "metadata" # Metadata specification
+    "metadata", # Metadata specification
+    "tagmsg", # IRCv3 message tags extension
 ]
 
 all_events = generated_events + protocol_events + list(numeric_events.values())
diff --git a/oyoyo/parse.py b/oyoyo/parse.py
index 4edc6df..15525aa 100644
--- a/oyoyo/parse.py
+++ b/oyoyo/parse.py
@@ -26,6 +26,7 @@ logging.config.fileConfig(_datadir + "logging.ini")
 PchumLog = logging.getLogger('pchumLogger')
 
 def parse_raw_irc_command(element):
+    print(element)
     """
     This function parses a raw irc command and returns a tuple
     of (prefix, command, args).
@@ -43,6 +44,21 @@ def parse_raw_irc_command(element):
                      NUL or CR or LF>
 
     <crlf>     ::= CR LF
+    """
+    """
+    When message-tags are enabled, the message pseudo-BNF,
+    as defined in RFC 1459, section 2.3.1 is extended as follows:
+
+    <message>       ::= ['@' <tags> <SPACE>] [':' <prefix> <SPACE> ] <command> [params] <crlf>
+    <tags>          ::= <tag> [';' <tag>]*
+    <tag>           ::= <key> ['=' <escaped_value>]
+    <key>           ::= [ <client_prefix> ] [ <vendor> '/' ] <key_name>
+    <client_prefix> ::= '+'
+    <key_name>      ::= <non-empty sequence of ascii letters, digits, hyphens ('-')>
+    <escaped_value> ::= <sequence of zero or more utf8 characters except NUL, CR, LF, semicolon (`;`) and SPACE>
+    <vendor>        ::= <host>
+
+
     """
     
     try:
@@ -53,10 +69,18 @@ def parse_raw_irc_command(element):
     
     parts = element.strip().split(" ")
     if parts[0].startswith(':'):
+        tags = None
         prefix = parts[0][1:]
         command = parts[1]
         args = parts[2:]
+    elif parts[0].startswith('@'):
+        # Message tag
+        tags = parts[0]
+        prefix = parts[1][1:]
+        command = parts[2]
+        args = parts[3:]
     else:
+        tags = None
         prefix = None
         command = parts[0]
         args = parts[1:]
@@ -76,7 +100,7 @@ def parse_raw_irc_command(element):
                 args = args[:idx] + [" ".join(args[idx:])[1:]]
                 break
 
-    return (prefix, command, args)
+    return (tags, prefix, command, args)
 
 
 def parse_nick(name):