/*
* 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;
BatchMessage endmsg;
ClientProtocol::Event endevent;
BatchInfo(ClientProtocol::EventProvider& protoevprov, IRCv3::Batch::Batch& b)
: startmsg(b, true)
, startevent(protoevprov, startmsg)
, endmsg(b, false)
, endevent(protoevprov, endmsg)
{
}
};
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;
batch.batchendmsg = &batch.batchinfo->endmsg;
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
for (std::vector<LocalUser*>::const_iterator i = batchinfo.users.begin(); i != batchinfo.users.end(); ++i)
{
LocalUser* const user = *i;
user->Send(batchinfo.endevent);
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)