]> git.netwichtig.de Git - user/henk/code/inspircd.git/commitdiff
Add support for the IRCv3 batch specification.
authorPeter Powell <petpow@saberuk.com>
Mon, 13 Aug 2018 20:48:06 +0000 (21:48 +0100)
committerPeter Powell <petpow@saberuk.com>
Mon, 13 Aug 2018 21:01:42 +0000 (22:01 +0100)
Co-authored-by: Attila Molnar <attilamolnar@hush.com>
docs/conf/modules.conf.example
include/modules/ircv3_batch.h [new file with mode: 0644]
src/modules/m_chanhistory.cpp
src/modules/m_ircv3_batch.cpp [new file with mode: 0644]

index 5995580f90ae27b4784081ce7d02ee5c250dee18..9e023e9ed1bd774f9a0491a14b3b2fb4011ae484 100644 (file)
 # and host cycling.
 #<ircv3 accountnotify="on" awaynotify="on" extendedjoin="on">
 
+#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
+# IRCv3 cap-notify module: Provides the cap-notify IRCv3.2 extension.
+# Required for IRCv3.2 conformance.
+#<module name="ircv3_capnotify">
+
 #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
 # IRCv3 account-tag module. Adds the 'account' tag which contains the
 # services account name of the message sender.
 #<module name="ircv3_accounttag">
 
+#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
+# IRCv3 batch module: Provides the batch IRCv3 extension which allows
+# the server to inform a client that a group of messages are related to
+# each other.
+#<module name="ircv3_batch">
+
 #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
 # IRCv3 cap-notify module: Provides the cap-notify IRCv3.2 extension.
 # Required for IRCv3.2 conformance.
diff --git a/include/modules/ircv3_batch.h b/include/modules/ircv3_batch.h
new file mode 100644 (file)
index 0000000..841554b
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * InspIRCd -- Internet Relay Chat Daemon
+ *
+ *   Copyright (C) 2016 Attila Molnar <attilamolnar@hush.com>
+ *
+ * This file is part of InspIRCd.  InspIRCd is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, version 2.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#pragma once
+
+// For CapReference
+#include "modules/cap.h"
+
+namespace IRCv3
+{
+       namespace Batch
+       {
+               typedef uint64_t RefTag;
+               class Manager;
+               class ManagerImpl;
+               class Batch;
+               struct BatchInfo;
+               class API;
+               class CapReference;
+
+               static const unsigned int MAX_BATCHES = (sizeof(intptr_t) * 8) - 1;
+       }
+}
+
+/** Batch Manager.
+ * Implements batch starting and stopping. When it becomes unavailable (due to e.g. module unload)
+ * all running batches are stopped.
+ */
+class IRCv3::Batch::Manager : public DataProvider, public ClientProtocol::MessageTagProvider
+{
+ public:
+       /** Constructor.
+        * @param mod Module that owns the Manager.
+        */
+       Manager(Module* mod)
+               : DataProvider(mod, "batchapi")
+               , ClientProtocol::MessageTagProvider(mod)
+       {
+       }
+
+       /** Start a batch.
+        * Check Batch::IsRunning() to learn if the batch has been started.
+        * @param batch Batch to start.
+        */
+       virtual void Start(Batch& batch) = 0;
+
+       /** End a batch.
+        * @param batch Batch to end.
+        */
+       virtual void End(Batch& batch) = 0;
+};
+
+/** Represents a batch.
+ * Batches are used to group together physically separate client protocol messages that logically belong
+ * together for one reason or another. The type of a batch, if provided, indicates what kind of grouping
+ * it does.
+ *
+ * Batch objects have two states: running and stopped. If a batch is running, messages can be added to it.
+ * If a message has been added to a batch and that message is sent to a client that negotiated the batch
+ * capability then the client will receive a message tag attached to the message indicating the batch that
+ * the message is a part of. If a message M is part of a batch B and M is sent to a client that hasn't yet
+ * received any message from batch B it will get a batch start message for B before M. When a batch B is
+ * stopped, every client that received at least one message which was in batch B will receive an end of
+ * batch message for B.
+ * A message may only be part of a single batch at any given time.
+ */
+class IRCv3::Batch::Batch
+{
+       Manager* manager;
+       const std::string type;
+       RefTag reftag;
+       std::string reftagstr;
+       unsigned int bit;
+       BatchInfo* batchinfo;
+       ClientProtocol::Message* batchstartmsg;
+
+       void Setup(unsigned int b)
+       {
+               bit = b;
+               reftag = (1 << bit);
+               reftagstr = ConvToStr(reftag);
+       }
+
+       unsigned int GetId() const { return bit; }
+       intptr_t GetBit() const { return reftag; }
+
+ public:
+       /** Constructor.
+        * The batch is initially stopped. To start it, pass it to Manager::Start().
+        * @param Type Batch type string, used to indicate what kind of grouping the batch does. May be empty.
+        */
+       Batch(const std::string& Type)
+               : manager(NULL)
+               , type(Type)
+               , batchinfo(NULL)
+               , batchstartmsg(NULL)
+       {
+       }
+
+       /** Destructor.
+        * If the batch is running, it is ended.
+        */
+       ~Batch()
+       {
+               if (manager)
+                       manager->End(*this);
+       }
+
+       /** Add a message to the batch.
+        * If the batch isn't running then this method does nothing.
+        * @param msg Message to add to the batch. If it is already part of any batch, this method is a no-op.
+        */
+       void AddToBatch(ClientProtocol::Message& msg)
+       {
+               if (manager)
+                       msg.AddTag("batch", manager, reftagstr, this);
+       }
+
+       /** Get batch reference tag which is an opaque id for the batch and is used in the client protocol.
+        * Only running batches have a reference tag assigned.
+        * @return Reference tag as a string, only valid if the batch is running.
+        */
+       const std::string& GetRefTagStr() const { return reftagstr; }
+
+       /** Get batch type.
+        * @return Batch type string.
+        */
+       const std::string& GetType() const { return type; }
+
+       /** Check whether the batch is running.
+        * Batches can be started with Manager::Start() and stopped with Manager::End().
+        * @return True if the batch is running, false otherwise.
+        */
+       bool IsRunning() const { return (manager != NULL); }
+
+       /** Get the batch start client protocol message.
+        * The returned message object can be manipulated to add extra parameters or labels to the message. The first
+        * parameter of the message is the batch reference tag generated by the module providing batch support.
+        * If the batch type string was specified, it will be the second parameter of the message.
+        * May only be called if IsRunning() == true.
+        * @return Mutable batch start client protocol message.
+        */
+       ClientProtocol::Message& GetBatchStartMessage() { return *batchstartmsg; }
+
+       friend class ManagerImpl;
+};
+
+/** Batch API. Use this to access the Manager.
+ */
+class IRCv3::Batch::API : public dynamic_reference_nocheck<Manager>
+{
+ public:
+       API(Module* mod)
+               : dynamic_reference_nocheck<Manager>(mod, "batchapi")
+       {
+       }
+};
+
+/** Reference to the batch cap.
+ * Can be used to check whether a user has the batch client cap enabled.
+ */
+class IRCv3::Batch::CapReference : public Cap::Reference
+{
+ public:
+       CapReference(Module* mod)
+               : Cap::Reference(mod, "batch")
+       {
+       }
+};
index 57db002a894c6d71bb69ac4cc7ff9a5e189cc27b..fe4bd94777374897551940fef5c0b058e43d4b0b 100644 (file)
@@ -19,6 +19,7 @@
 
 #include "inspircd.h"
 #include "modules/ircv3_servertime.h"
+#include "modules/ircv3_batch.h"
 
 struct HistoryItem
 {
@@ -123,12 +124,18 @@ class ModuleChanHistory : public Module
        bool sendnotice;
        UserModeReference botmode;
        bool dobots;
+       IRCv3::Batch::CapReference batchcap;
+       IRCv3::Batch::API batchmanager;
+       IRCv3::Batch::Batch batch;
        IRCv3::ServerTime::API servertimemanager;
 
  public:
        ModuleChanHistory()
                : m(this)
                , botmode(this, "bot")
+               , batchcap(this)
+               , batchmanager(this)
+               , batch("chathistory")
                , servertimemanager(this)
        {
        }
@@ -172,7 +179,7 @@ class ModuleChanHistory : public Module
                if (list->maxtime)
                        mintime = ServerInstance->Time() - list->maxtime;
 
-               if (sendnotice)
+               if ((sendnotice) && (!batchcap.get(localuser)))
                {
                        std::string message("Replaying up to " + ConvToStr(list->maxlen) + " lines of pre-join history");
                        if (list->maxtime > 0)
@@ -180,6 +187,12 @@ class ModuleChanHistory : public Module
                        memb->WriteNotice(message);
                }
 
+               if (batchmanager)
+               {
+                       batchmanager->Start(batch);
+                       batch.GetBatchStartMessage().PushParamRef(memb->chan->name);
+               }
+
                for(std::deque<HistoryItem>::iterator i = list->lines.begin(); i != list->lines.end(); ++i)
                {
                        const HistoryItem& item = *i;
@@ -188,9 +201,13 @@ class ModuleChanHistory : public Module
                                ClientProtocol::Messages::Privmsg msg(ClientProtocol::Messages::Privmsg::nocopy, item.sourcemask, memb->chan, item.text);
                                if (servertimemanager)
                                        servertimemanager->Set(msg, item.ts);
+                               batch.AddToBatch(msg);
                                localuser->Send(ServerInstance->GetRFCEvents().privmsg, msg);
                        }
                }
+
+               if (batchmanager)
+                       batchmanager->End(batch);
        }
 
        Version GetVersion() CXX11_OVERRIDE
diff --git a/src/modules/m_ircv3_batch.cpp b/src/modules/m_ircv3_batch.cpp
new file mode 100644 (file)
index 0000000..df2b00f
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * InspIRCd -- Internet Relay Chat Daemon
+ *
+ *   Copyright (C) 2016 Attila Molnar <attilamolnar@hush.com>
+ *
+ * This file is part of InspIRCd.  InspIRCd is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, version 2.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#include "inspircd.h"
+#include "modules/cap.h"
+#include "modules/ircv3_batch.h"
+
+class BatchMessage : public ClientProtocol::Message
+{
+ public:
+       BatchMessage(const IRCv3::Batch::Batch& batch, bool start)
+               : ClientProtocol::Message("BATCH", ServerInstance->Config->ServerName)
+       {
+               char c = (start ? '+' : '-');
+               PushParam(std::string(1, c) + batch.GetRefTagStr());
+               if ((start) && (!batch.GetType().empty()))
+                       PushParamRef(batch.GetType());
+       }
+};
+
+/** Extra structure allocated only for running batches, containing objects only relevant for
+ * that specific run of the batch.
+ */
+struct IRCv3::Batch::BatchInfo
+{
+       /** List of users that have received the batch start message
+        */
+       std::vector<LocalUser*> users;
+       BatchMessage startmsg;
+       ClientProtocol::Event startevent;
+
+       BatchInfo(ClientProtocol::EventProvider& protoevprov, IRCv3::Batch::Batch& b)
+               : startmsg(b, true)
+               , startevent(protoevprov, startmsg)
+       {
+       }
+};
+
+class IRCv3::Batch::ManagerImpl : public Manager
+{
+       typedef std::vector<Batch*> BatchList;
+
+       Cap::Capability cap;
+       ClientProtocol::EventProvider protoevprov;
+       LocalIntExt batchbits;
+       BatchList active_batches;
+       bool unloading;
+
+       bool ShouldSendTag(LocalUser* user, const ClientProtocol::MessageTagData& tagdata) CXX11_OVERRIDE
+       {
+               if (!cap.get(user))
+                       return false;
+
+               Batch& batch = *static_cast<Batch*>(tagdata.provdata);
+               // Check if this is the first message the user is getting that is part of the batch
+               const intptr_t bits = batchbits.get(user);
+               if (!(bits & batch.GetBit()))
+               {
+                       // Send the start batch command ("BATCH +reftag TYPE"), remember the user so we can send them a
+                       // "BATCH -reftag" message later when the batch ends and set the flag we just checked so this is
+                       // only done once per user per batch.
+                       batchbits.set(user, (bits | batch.GetBit()));
+                       batch.batchinfo->users.push_back(user);
+                       user->Send(batch.batchinfo->startevent);
+               }
+
+               return true;
+       }
+
+       unsigned int NextFreeId() const
+       {
+               if (active_batches.empty())
+                       return 0;
+               return active_batches.back()->GetId()+1;
+       }
+
+ public:
+       ManagerImpl(Module* mod)
+               : Manager(mod)
+               , cap(mod, "batch")
+               , protoevprov(mod, "BATCH")
+               , batchbits("batchbits", ExtensionItem::EXT_USER, mod)
+               , unloading(false)
+       {
+       }
+
+       void Init()
+       {
+               // Set batchbits to 0 for all users in case we were reloaded and the previous, now meaningless,
+               // batchbits are set on users
+               const UserManager::LocalList& users = ServerInstance->Users.GetLocalUsers();
+               for (UserManager::LocalList::const_iterator i = users.begin(); i != users.end(); ++i)
+               {
+                       LocalUser* const user = *i;
+                       batchbits.set(user, 0);
+               }
+       }
+
+       void Shutdown()
+       {
+               unloading = true;
+               while (!active_batches.empty())
+                       ManagerImpl::End(*active_batches.back());
+       }
+
+       void RemoveFromAll(LocalUser* user)
+       {
+               const intptr_t bits = batchbits.get(user);
+
+               // User is quitting, remove them from all lists
+               for (BatchList::iterator i = active_batches.begin(); i != active_batches.end(); ++i)
+               {
+                       Batch& batch = **i;
+                       // Check the bit first to avoid list scan in case they're not on the list
+                       if ((bits & batch.GetBit()) != 0)
+                               stdalgo::vector::swaperase(batch.batchinfo->users, user);
+               }
+       }
+
+       void Start(Batch& batch) CXX11_OVERRIDE
+       {
+               if (unloading)
+                       return;
+
+               if (batch.IsRunning())
+                       return; // Already started, don't start again
+
+               const size_t id = NextFreeId();
+               if (id >= MAX_BATCHES)
+                       return;
+
+               batch.Setup(id);
+               // Set the manager field which Batch::IsRunning() checks and is also used by AddToBatch()
+               // to set the message tag
+               batch.manager = this;
+               batch.batchinfo = new IRCv3::Batch::BatchInfo(protoevprov, batch);
+               batch.batchstartmsg = &batch.batchinfo->startmsg;
+               active_batches.push_back(&batch);
+       }
+
+       void End(Batch& batch) CXX11_OVERRIDE
+       {
+               if (!batch.IsRunning())
+                       return;
+
+               // Mark batch as stopped
+               batch.manager = NULL;
+
+               BatchInfo& batchinfo = *batch.batchinfo;
+               // Send end batch message to all users who got the batch start message and unset bit so it can be reused
+               BatchMessage endbatchmsg(batch, false);
+               ClientProtocol::Event endbatchevent(protoevprov, endbatchmsg);
+               for (std::vector<LocalUser*>::const_iterator i = batchinfo.users.begin(); i != batchinfo.users.end(); ++i)
+               {
+                       LocalUser* const user = *i;
+                       user->Send(endbatchevent);
+                       batchbits.set(user, batchbits.get(user) & ~batch.GetBit());
+               }
+
+               // erase() not swaperase because the reftag generation logic depends on the order of the elements
+               stdalgo::erase(active_batches, &batch);
+               delete batch.batchinfo;
+               batch.batchinfo = NULL;
+       }
+};
+
+class ModuleIRCv3Batch : public Module
+{
+       IRCv3::Batch::ManagerImpl manager;
+
+ public:
+       ModuleIRCv3Batch()
+               : manager(this)
+       {
+       }
+
+       void init() CXX11_OVERRIDE
+       {
+               manager.Init();
+       }
+
+       void OnUnloadModule(Module* mod) CXX11_OVERRIDE
+       {
+               if (mod == this)
+                       manager.Shutdown();
+       }
+
+       void OnUserDisconnect(LocalUser* user) CXX11_OVERRIDE
+       {
+               // Remove the user from all internal lists
+               manager.RemoveFromAll(user);
+       }
+
+       Version GetVersion() CXX11_OVERRIDE
+       {
+               return Version("Provides the batch IRCv3 extension", VF_VENDOR);
+       }
+};
+
+MODULE_INIT(ModuleIRCv3Batch)