1
0
mirror of https://github.com/anope/anope.git synced 2026-06-12 17:04:47 +02:00
Files

940 lines
23 KiB
C++

// Anope IRC Services <https://www.anope.org/>
//
// Copyright (C) 2003-2026 Anope Contributors
// Copyright (C) 2017-2018 Matt Schatz <genius3000@g3k.solutions>
//
// Anope is free software. You can use, modify, and/or distribute it under the
// terms of version 2 of the GNU General Public License. See docs/LICENSE.txt
// for the complete terms of this license and docs/AUTHORS.txt for a list of
// contributors.
//
// Based on the original code of Epona by Lara
// Based on the original code of Services by Andy Church
//
// SPDX-License-Identifier: GPL-2.0-only
#include "module.h"
#define HOSTOFFER_TYPE "HostOffer"
class HostOfferList;
namespace
{
HostOfferList* host_offers = nullptr;
Anope::string network_name;
time_t take_delay = 0;
Anope::string GetVHostMask(const Anope::string &ident, const Anope::string &host)
{
Anope::string buffer;
if (!ident.empty())
buffer.append(ident).append("@");
buffer.append(host);
return buffer;
}
Anope::string Template(const Anope::string &ih, const Anope::string &nick)
{
if (ih.empty() || ih.find_first_of("{}") == Anope::string::npos)
return ih;
NickAlias *na = nullptr;
if (!nick.empty())
na = NickAlias::Find(nick);
auto regdate = [](NickAlias *na)
{
auto tm = localtime(&(na->registered));
char buf[128];
strftime(buf, sizeof(buf), "%Y-%m-%d", tm);
return Anope::string(buf);
};
// NOTE: when updating this also update dummy_template.
return Anope::Template(ih, {
{ "account", na ? na->nc->display : "" },
{ "nick", na ? na->nick : "" },
{ "network", network_name },
{ "regdate", na ? regdate(na) : "" },
{ "regepoch", na ? Anope::ToString(na->registered) : "" },
});
}
}
class HostOffer final
: public Serializable
{
public:
Anope::string ident;
Anope::string host;
Anope::string creator;
Anope::string reason;
time_t created = 0;
time_t expires = 0;
HostOffer()
: Serializable(HOSTOFFER_TYPE)
{
}
HostOffer(const Anope::string &i, const Anope::string &h, const Anope::string &cr, const Anope::string &r, time_t cd, time_t e)
: Serializable(HOSTOFFER_TYPE)
, ident(i)
, host(h)
, creator(cr)
, reason(r)
, created(cd)
, expires(e)
{
}
Anope::string GetMask() const
{
return GetVHostMask(ident, host);
}
~HostOffer();
};
class HostOfferList final
{
protected:
Serialize::Checker<std::vector<HostOffer *>> offers;
public:
HostOfferList()
: offers(HOSTOFFER_TYPE)
{
}
~HostOfferList()
{
Clear();
}
void Add(HostOffer *ho)
{
offers->push_back(ho);
}
void Del(HostOffer *ho)
{
auto it = std::find(offers->begin(), offers->end(), ho);
if (it != offers->end())
offers->erase(it);
}
void Clear()
{
while (!offers->empty())
delete offers->back();
}
void Expire(const HostOffer *ho)
{
Log(Config->GetClient("HostServ"), "expire/offer") << "Expiring vhost offer " << ho->GetMask();
delete ho;
}
size_t GetCount() const
{
return offers->size();
}
HostOffer *Get(const Anope::string &match)
{
for (auto i = offers->size(); i > 0; --i)
{
auto* ho = offers->at(i - 1);
if (ho->expires && ho->expires <= Anope::CurTime)
Expire(ho);
else if (match.equals_ci(ho->GetMask()))
return ho;
}
return nullptr;
}
HostOffer *Get(size_t number)
{
if (number >= offers->size())
return nullptr;
auto *ho = offers->at(number);
if (ho->expires && ho->expires <= Anope::CurTime)
{
Expire(ho);
return nullptr;
}
return ho;
}
std::vector<HostOffer *> GetAll()
{
std::vector<HostOffer *> list;
for (auto i = offers->size(); i > 0; --i)
{
auto *ho = offers->at(i - 1);
if (ho->expires && ho->expires <= Anope::CurTime)
Expire(ho);
else
list.push_back(ho);
}
std::reverse(list.begin(), list.end());
return list;
}
};
HostOffer::~HostOffer()
{
host_offers->Del(this);
}
class HostOfferType final
: public Serialize::Type
{
public:
HostOfferType()
: Serialize::Type(HOSTOFFER_TYPE)
{
}
void Serialize(Serializable *obj, Serialize::Data &data) const override
{
const auto *ho = static_cast<const HostOffer *>(obj);
data.Store("ident", ho->ident);
data.Store("host", ho->host);
data.Store("creator", ho->creator);
data.Store("reason", ho->reason);
data.Store("created", ho->created);
data.Store("expires", ho->expires);
}
Serializable *Unserialize(Serializable *obj, Serialize::Data &data) const override
{
HostOffer *ho;
if (obj)
ho = anope_dynamic_static_cast<HostOffer *>(obj);
else
ho = new HostOffer();
ho->ident = data.Load("ident");
ho->host = data.Load("host");
ho->reason = data.Load("reason");
ho->creator = data.Load("creator");
ho->created = data.Load<time_t>("created");
ho->expires = data.Load<time_t>("expires");
if (!obj)
host_offers->Add(ho);
return ho;
}
};
class OfferDelCallback final
: public NumberList
{
private:
CommandSource &source;
Command *cmd;
size_t deleted = 0;
Anope::string lastdeleted;
public:
OfferDelCallback(const Anope::string &numlist, CommandSource &_source, Command *c)
: NumberList(numlist, true)
, source(_source)
, cmd(c)
{
}
~OfferDelCallback()
{
switch (deleted)
{
case 0:
source.Reply(_("No matching entries on the host offer list."));
break;
case 1:
source.Reply(_("Deleted %s from the host offer list."), lastdeleted.c_str());
break;
default:
source.Reply(deleted, N_("Deleted %zu entry from the host offer list.", "Deleted %zu entries from the host offer list."), deleted);
break;
}
}
void HandleNumber(unsigned number) override
{
if (!number)
return;
const auto *ho = host_offers->Get(number - 1);
if (!ho)
return;
deleted++;
lastdeleted = ho->GetMask();
Log(LOG_ADMIN, source, cmd) << "to remove " << lastdeleted << " from the host offer list";
delete ho;
}
};
class OfferListCallback final
: public NumberList
{
private:
CommandSource &source;
ListFormatter &list;
public:
OfferListCallback(const Anope::string &numlist, CommandSource &_source, ListFormatter &_list)
: NumberList(numlist, false)
, source(_source)
, list(_list)
{
}
void HandleNumber(unsigned number) override
{
if (!number)
return;
const auto *ho = host_offers->Get(number - 1);
if (!ho)
return;
ListFormatter::ListEntry entry;
entry["Number"] = Anope::ToString(number);
entry["VHost"] = ho->GetMask();
entry["Reason"] = ho->reason;
entry["Creator"] = ho->creator;
entry["Created"] = Anope::strftime(ho->created, source.GetAccount(), true);
entry["Expires"] = Anope::Expires(ho->expires, source.GetAccount());
list.AddEntry(entry);
}
};
class CommandHSOffer final
: public Command
{
private:
void DoAdd(CommandSource &source, const std::vector<Anope::string> &params)
{
if (Anope::ReadOnly)
{
source.Reply(READ_ONLY_MODE);
return;
}
const size_t expiry_idx = params.size() >= 2 && params[1][0] == '+' ? 1 : 0;
const size_t vhost_idx = expiry_idx ? 2 : 1;
const size_t reason_idx = expiry_idx ? 3 : 2;
if (params.size() <= vhost_idx)
{
this->OnSyntaxError(source, "ADD");
return;
}
time_t expiry = 0;
if (expiry_idx != 0)
{
expiry = Anope::DoTime(params[expiry_idx].substr(1));
if (expiry < 0)
{
source.Reply(BAD_EXPIRY_TIME);
return;
}
else if (expiry)
expiry += Anope::CurTime;
}
const auto &vhost = params[vhost_idx];
Anope::string ident, host;
const auto at = vhost.find('@');
if (at == Anope::string::npos)
host = vhost;
else
{
ident = vhost.substr(0, at);
host = vhost.substr(at + 1);
}
if (host.empty())
{
this->OnSyntaxError(source, "ADD");
return;
}
auto dummy_template = [](const Anope::string &v)
{
return Anope::Template(v, {
{ "account", "a" },
{ "nick", "a" },
{ "network", network_name },
{ "regdate", "1111-11-11" },
{ "regepoch", "1111111111" },
});
};
if (!ident.empty())
{
if (!IRCD->CanSetVIdent)
{
source.Reply(HOST_NO_VIDENT);
return;
}
const auto sub_ident = dummy_template(ident);
if (sub_ident.length() > IRCD->MaxUser)
{
source.Reply(HOST_SET_VIDENT_TOO_LONG, IRCD->MaxUser);
return;
}
if (!IRCD->IsIdentValid(sub_ident))
{
source.Reply(HOST_SET_VIDENT_ERROR);
return;
}
}
const auto sub_host = dummy_template(host);
if (sub_host.length() > IRCD->MaxHost)
{
source.Reply(HOST_SET_VHOST_TOO_LONG, IRCD->MaxHost);
return;
}
if (!IRCD->IsHostValid(sub_host))
{
source.Reply(HOST_SET_VHOST_ERROR);
return;
}
const auto full_vhost = GetVHostMask(ident, host);
if (host_offers->Get(full_vhost))
{
source.Reply(_("Host offer \002%s\002 already exists."), full_vhost.c_str());
return;
}
Anope::string reason;
for (auto idx = reason_idx; idx < params.size(); ++idx)
reason.append(reason.empty() ? "" : " ").append(params[idx]);
auto *ho = new HostOffer(ident, host, source.GetNick(), reason, Anope::CurTime, expiry);
host_offers->Add(ho);
Log(LOG_ADMIN, source, this) << "to add a host offer of " << full_vhost << " (reason: " << reason << ")";
source.Reply(_("\002%s\002 added to the host offer list."), full_vhost.c_str());
}
void DoDel(CommandSource &source, const std::vector<Anope::string> &params)
{
if (Anope::ReadOnly)
{
source.Reply(READ_ONLY_MODE);
return;
}
const auto &match = params.size() > 1 ? params[1] : "";
if (match.empty())
{
this->OnSyntaxError(source, "DEL");
return;
}
if (host_offers->GetCount() == 0)
{
source.Reply(_("Host offer list is empty."));
return;
}
if (isdigit(match[0]) && match.find_first_not_of("1234567890,-") == Anope::string::npos)
{
OfferDelCallback nl_list(match, source, this);
nl_list.Process();
}
else
{
const auto *ho = host_offers->Get(match);
if (!ho)
{
source.Reply(_("\002%s\002 not found on the host offer list."), match.c_str());
return;
}
const auto vhost = ho->GetMask();
Log(LOG_ADMIN, source, this) << "to remove " << vhost << " from the list";
source.Reply(_("\002%s\002 deleted from the host offer list."), vhost.c_str());
delete ho;
}
}
void DoList(CommandSource &source, const std::vector<Anope::string> &params, bool view)
{
if (host_offers->GetCount() == 0)
{
source.Reply(_("Host offer list is empty."));
return;
}
const auto &match = params.size() > 1 ? params[1] : "";
ListFormatter list(source.GetAccount());
list.AddColumn(_("Number")).AddColumn(_("VHost")).AddColumn(_("Reason"));
list.SetFlexible([](ListFormatter::ListEntry &row)
{
return row["Reason"].empty()
? _("{number}: \002{vhost}\002")
: _("{number}: \002{vhost}\002 ({reason})");
});
if (view)
{
list.AddColumn(_("Creator")).AddColumn(_("Created")).AddColumn(_("Expires"));
list.SetFlexible([](ListFormatter::ListEntry &row)
{
return row["Reason"].empty()
? _("{number}: \002{vhost}\002 -- created by {creator} on {created}; {expires}")
: _("{number}: \002{vhost}\002 -- created by {creator} on {created}; {expires} ({reason})");
});
}
if (!match.empty() && isdigit(match[0]) && match.find_first_not_of("1234567890,-") == Anope::string::npos)
{
OfferListCallback nl_list(match, source, list);
nl_list.Process();
}
else
{
const auto &list_offers = host_offers->GetAll();
for (size_t i = 0; i < list_offers.size(); ++i)
{
const auto *ho = list_offers.at(i);
const auto vhost = ho->GetMask();
if (match.empty() || match.equals_ci(vhost) || Anope::Match(vhost, match))
{
ListFormatter::ListEntry entry;
entry["Number"] = Anope::ToString(i + 1);
entry["VHost"] = vhost;
entry["Reason"] = ho->reason;
entry["Creator"] = ho->creator;
entry["Created"] = Anope::strftime(ho->created, source.GetAccount(), true);
entry["Expires"] = Anope::Expires(ho->expires, source.GetAccount());
list.AddEntry(entry);
}
}
}
if (list.IsEmpty())
source.Reply(_("No matching entries on the host offer list."));
else
{
source.Reply(_("Current host offer list:"));
list.SendTo(source);
source.Reply(_("End of host offer list."));
}
}
void DoClear(CommandSource &source)
{
if (Anope::ReadOnly)
{
source.Reply(READ_ONLY_MODE);
return;
}
if (host_offers->GetCount() == 0)
{
source.Reply(_("Host offer list is empty."));
return;
}
host_offers->Clear();
Log(LOG_ADMIN, source, this) << "to clear the list";
source.Reply(_("Host offer list has been cleared."));
}
public:
CommandHSOffer(Module *creator)
: Command(creator, "hostserv/offer", 1, 4)
{
this->SetDesc(_("Manipulate the host offer list"));
this->SetSyntax(_("ADD [+\037expiry\037] \037vhost\037 [\037reason\037]"));
this->SetSyntax("CLEAR");
this->SetSyntax(_("DEL {\037vhost\037 | \037entry-num\037 | \037list\037}"));
this->SetSyntax(_("LIST [\037vhost-mask\037 | \037entry-num\037 | \037list\037]"));
this->SetSyntax(_("VIEW [\037vhost-mask\037 | \037entry-num\037 | \037list\037]"));
}
void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
{
const auto &subcmd = params[0];
if (subcmd.equals_ci("ADD"))
this->DoAdd(source, params);
else if (subcmd.equals_ci("CLEAR"))
this->DoClear(source);
else if (subcmd.equals_ci("DEL"))
this->DoDel(source, params);
else if (subcmd.equals_ci("LIST"))
this->DoList(source, params, false);
else if (subcmd.equals_ci("VIEW"))
this->DoList(source, params, true);
else
this->OnSyntaxError(source, subcmd);
}
bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
{
this->SendSyntax(source);
source.Reply(" ");
source.Reply(_(
"Offer stock vhosts to unprivileged users."
"\n"
"The \002%s\033ADD\002 command adds an offered vhost. If "
"\037expiry\037 is provided then the offered vhost will only be "
"available for a limited time. The \037vhost\037 field may "
"contain template variables. The supported template variables "
"are:\n"
" \002{account}\002 - Your current account.\n"
" \002{network}\002 - The name of this IRC network.\n"
" \002{nick}\002 - Your current nickname.\n"
" \002{regdate}\002 - The YYYY-MM-DD date at which your nick was registered.\n"
" \002{regepoch}\002 - The UNIX time at which your nick was registered.\n"
"\n"
"The \002%s\033CLEAR\002 command removes all offered vhosts.\n"
"\n"
"The \002%s\033DEL\002 command removes an offered vhost.\n"
"\n"
"The \002%s\033LIST\002 command displays the offered vhosts, or "
"optionally only those offered vhosts which match the given "
"mask."
"\n\n"
"The \002%s\033VIEW\002 command is a more verbose version of "
"the \002%s\033LIST\002 command."
),
source.command.c_str(),
source.command.c_str(),
source.command.c_str(),
source.command.c_str(),
source.command.c_str(),
source.command.c_str());
return true;
}
};
class OfferListListCallback final
: public NumberList
{
private:
CommandSource &source;
ListFormatter &list;
bool show_all;
public:
OfferListListCallback(const Anope::string &numlist, CommandSource &_source, ListFormatter &_list, bool sa)
: NumberList(numlist, false)
, source(_source)
, list(_list)
, show_all(sa)
{
}
void HandleNumber(unsigned number) override
{
if (!number)
return;
const auto *ho = host_offers->Get(number - 1);
if (!ho)
return;
const auto &ident = Template(ho->ident, source.GetNick());
const auto &host = Template(ho->host, source.GetNick());
auto mask = GetVHostMask(ident, host);
if ((!ident.empty() && !IRCD->IsIdentValid(ident)) || !IRCD->IsHostValid(host))
{
if (!show_all)
return;
mask = Anope::Format(source.Translate(_("%s [Invalid]")), mask.c_str());
}
ListFormatter::ListEntry entry;
entry["Number"] = Anope::ToString(number);
entry["Offered vhost"] = ho->GetMask();
entry["Your vhost"] = mask;
entry["Expires"] = Anope::Expires(ho->expires, source.GetAccount());
entry["Reason"] = ho->reason;
list.AddEntry(entry);
}
};
class CommandHSOfferList final
: public Command
{
private:
void DoTake(CommandSource &source, const std::vector<Anope::string> &params)
{
if (Anope::ReadOnly)
{
source.Reply(READ_ONLY_MODE);
return;
}
auto *na = NickAlias::Find(source.GetNick());
if (!na || na->nc != source.GetAccount())
{
source.Reply(ACCESS_DENIED);
return;
}
if (source.GetAccount()->HasExt("UNCONFIRMED"))
{
source.Reply(_("You must confirm your account before you may take a vhost."));
return;
}
if (take_delay > 0 && na->HasVHost() && na->GetVHostCreated() + take_delay > Anope::CurTime)
{
source.Reply(_("Please wait %s before taking a new vhost."), Anope::Duration(take_delay, source.GetAccount()).c_str());
return;
}
const auto &match = params.size() > 1 ? params[1] : "";
if (match.empty())
{
this->OnSyntaxError(source, "TAKE");
return;
}
if (host_offers->GetCount() == 0)
{
source.Reply(_("Host offer list is empty."));
return;
}
const HostOffer *ho = nullptr;
if (match.find_first_not_of("1234567890") == Anope::string::npos)
{
const auto number = Anope::TryConvert<size_t>(match);
if (number)
ho = host_offers->Get(*number - 1);
if (!ho)
{
source.Reply(_("%s is an invalid host offer entry number."), match.c_str());
return;
}
}
else
{
ho = host_offers->Get(match);
if (!ho)
{
source.Reply(_("\002%s\002 not found on the host offer list."), match.c_str());
return;
}
}
const auto ident = Template(ho->ident, source.GetNick());
if (ident.length() > IRCD->MaxUser)
{
source.Reply(HOST_SET_VIDENT_TOO_LONG, IRCD->MaxUser);
return;
}
if (!ident.empty() && !IRCD->IsIdentValid(ident))
{
source.Reply(HOST_SET_VIDENT_ERROR);
return;
}
const auto host = Template(ho->host, source.GetNick());
if (host.length() > IRCD->MaxHost)
{
source.Reply(HOST_SET_VHOST_TOO_LONG, IRCD->MaxHost);
return;
}
if (!IRCD->IsHostValid(host))
{
source.Reply(HOST_SET_VHOST_ERROR);
return;
}
Log(LOG_COMMAND, source, this) << "to take offer " << ho->GetMask() << " and set their vhost to " << GetVHostMask(ident, host);
na->SetVHost(ident, host, ho->creator);
FOREACH_MOD(OnSetVHost, (na));
}
void DoList(CommandSource &source, const std::vector<Anope::string> &params)
{
const auto *na = NickAlias::Find(source.GetNick());
const auto *sourcenc = source.GetAccount();
if (!na || na->nc != sourcenc)
{
source.Reply(ACCESS_DENIED);
return;
}
if (sourcenc->HasExt("UNCONFIRMED"))
{
source.Reply(_("You must confirm your account before you can view the host offer list."));
return;
}
if (host_offers->GetCount() == 0)
{
source.Reply(_("Host offer list is empty."));
return;
}
ListFormatter list(source.GetAccount());
list.AddColumn(_("Number")).AddColumn(_("Offered vhost")).AddColumn(_("Your vhost"))
.AddColumn(_("Expires")).AddColumn(_("Reason"));
list.SetFlexible([](ListFormatter::ListEntry &row)
{
return row["Reason"].empty()
? _("{number}: \002{offered_vhost}\002 / \002{your_vhost}\002 -- {expires}")
: _("{number}: \002{offered_vhost}\002 / \002{your_vhost}\002 -- {expires} ({reason})");
});
const auto &match = params.size() > 0 ? params[0] : "";
const auto show_all = params.size() > 1 && params[1].equals_ci("ALL");
if (!match.empty() && isdigit(match[0]) && match.find_first_not_of("1234567890,-") == Anope::string::npos)
{
OfferListListCallback nl_list(match, source, list, show_all);
nl_list.Process();
}
else
{
const auto &list_offers = host_offers->GetAll();
for (size_t i = 0; i < list_offers.size(); ++i)
{
const auto *ho = list_offers.at(i);
const auto vhost = ho->GetMask();
if (match.empty() || match.equals_ci(vhost) || Anope::Match(vhost, match, false, true))
{
const auto &ident = Template(ho->ident, source.GetNick());
const auto &host = Template(ho->host, source.GetNick());
auto mask = GetVHostMask(ident, host);
if ((!ident.empty() && !IRCD->IsIdentValid(ident)) || !IRCD->IsHostValid(host))
{
if (!show_all)
continue;
mask = Anope::Format(source.Translate(_("%s [Invalid]")), mask.c_str());
}
ListFormatter::ListEntry entry;
entry["Number"] = Anope::ToString(i + 1);
entry["Offered vhost"] = vhost;
entry["Your vhost"] = mask;
entry["Expires"] = Anope::Expires(ho->expires, source.GetAccount());
entry["Reason"] = ho->reason;
list.AddEntry(entry);
}
}
}
if (list.IsEmpty())
source.Reply(_("No matching entries on the host offer list."));
else
{
source.Reply(_("Current host offer list:"));
list.SendTo(source);
source.Reply(_("End of host offer list."));
}
}
public:
CommandHSOfferList(Module *creator)
: Command(creator, "hostserv/offerlist", 0, 2)
{
this->SetDesc(_("List or take a vhost from the host offer list"));
this->SetSyntax(_("[\037vhost-mask\037 | \037entry-num\037 | \037list\037] [ALL]"));
this->SetSyntax(_("TAKE {\037vhost\037 | \037entry-num\037}"));
this->RequireUser(true);
}
void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
{
if (!params.empty() && params[0].equals_ci("TAKE"))
this->DoTake(source, params);
else
this->DoList(source, params);
}
bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
{
this->SendSyntax(source);
source.Reply(" ");
source.Reply(_(
"List or take an offered vhost.\n"
"\n"
"With no parameters, a user@host or host mask, an entry number, or "
"a list (e.g. 1-3,5) offered vhosts will be shown. If \002ALL\002 "
"is specified vhosts that can not be taken will be labelled with "
"\002[Invalid]\002 instead of being omitted."
"\n"
"The offered vhosts may contain template variables. The supported "
"template variables are:\n"
" \002{account}\002 - Your current account.\n"
" \002{network}\002 - The name of this IRC network.\n"
" \002{nick}\002 - Your current nickname.\n"
" \002{regdate}\002 - The YYYY-MM-DD date at which your nick was registered.\n"
" \002{regepoch}\002 - The UNIX time at which your nick was registered.\n"
"\n"
"With \002%s\033TAKE\002 an offered vhost will be applied. You "
"must specify the offered template variable or the entry number "
"for the offered vhost you want to use. Once a vhost is taken "
"you can not take another one for %s."
),
source.command.c_str(),
Anope::Duration(take_delay, source.GetAccount()).c_str());
return true;
}
};
class HSOffer final
: public Module
{
private:
HostOfferType hostoffer_type;
HostOfferList hostoffers;
CommandHSOffer commandhsoffer;
CommandHSOfferList commandhsofferlist;
public:
HSOffer(const Anope::string &modname, const Anope::string &creator)
: Module(modname, creator, VENDOR)
, commandhsoffer(this)
, commandhsofferlist(this)
{
if (!IRCD || !IRCD->CanSetVHost)
throw ModuleException("Your IRCd does not support vhosts");
host_offers = &hostoffers;
}
void OnReload(Configuration::Conf &conf) override
{
network_name = Config->GetBlock("networkinfo").Get<const Anope::string>("networkname");
take_delay = conf.GetModule(this).Get<time_t>("takedelay");
}
};
MODULE_INIT(HSOffer)