mirror of
https://github.com/anope/anope.git
synced 2026-06-12 19:14:47 +02:00
940 lines
23 KiB
C++
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> ¶ms)
|
|
{
|
|
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> ¶ms)
|
|
{
|
|
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> ¶ms, 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> ¶ms) 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> ¶ms)
|
|
{
|
|
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> ¶ms)
|
|
{
|
|
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> ¶ms) 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)
|