From 584d4569031d9e5d699d72eb3e09307fc84ea2f8 Mon Sep 17 00:00:00 2001 From: Peter Powell Date: Mon, 13 Aug 2018 21:48:06 +0100 Subject: [PATCH] Add support for the IRCv3 batch specification. Co-authored-by: Attila Molnar --- docs/conf/modules.conf.example | 11 ++ include/modules/ircv3_batch.h | 185 ++++++++++++++++++++++++++++ src/modules/m_chanhistory.cpp | 19 ++- src/modules/m_ircv3_batch.cpp | 216 +++++++++++++++++++++++++++++++++ 4 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 include/modules/ircv3_batch.h create mode 100644 src/modules/m_ircv3_batch.cpp diff --git a/docs/conf/modules.conf.example b/docs/conf/modules.conf.example index 5995580f9..9e023e9ed 100644 --- a/docs/conf/modules.conf.example +++ b/docs/conf/modules.conf.example @@ -1036,11 +1036,22 @@ # and host cycling. # +#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# IRCv3 cap-notify module: Provides the cap-notify IRCv3.2 extension. +# Required for IRCv3.2 conformance. +# + #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # IRCv3 account-tag module. Adds the 'account' tag which contains the # services account name of the message sender. # +#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# 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. +# + #-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# # 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 index 000000000..841554bdb --- /dev/null +++ b/include/modules/ircv3_batch.h @@ -0,0 +1,185 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2016 Attila Molnar + * + * 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 . + */ + + +#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 +{ + public: + API(Module* mod) + : dynamic_reference_nocheck(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") + { + } +}; diff --git a/src/modules/m_chanhistory.cpp b/src/modules/m_chanhistory.cpp index 57db002a8..fe4bd9477 100644 --- a/src/modules/m_chanhistory.cpp +++ b/src/modules/m_chanhistory.cpp @@ -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::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 index 000000000..df2b00f49 --- /dev/null +++ b/src/modules/m_ircv3_batch.cpp @@ -0,0 +1,216 @@ +/* + * InspIRCd -- Internet Relay Chat Daemon + * + * Copyright (C) 2016 Attila Molnar + * + * 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 . + */ + + +#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 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 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(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::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) -- 2.39.2