diff --git a/data/nickserv.example.conf b/data/nickserv.example.conf index dd87d29ae..bd6cbb6d5 100644 --- a/data/nickserv.example.conf +++ b/data/nickserv.example.conf @@ -777,6 +777,17 @@ command { service = "NickServ"; name = "SASET PROTECT"; command = "nickserv/sase command { service = "NickServ"; name = "SET KILL"; command = "nickserv/set/protect"; hide = yes; } command { service = "NickServ"; name = "SASET KILL"; command = "nickserv/saset/protect"; permission = "nickserv/saset/protect"; hide = yes; } +/* + * ns_set_timezone + * + * Provides the command nickserv/set/timezone and nickserv/saset/timezone. + * + * Allows configuring the timezone that services uses. + */ +module { name = "ns_set_timezone" } +command { service = "NickServ"; name = "SET TIMEZONE"; command = "nickserv/set/timezone"; } +command { service = "NickServ"; name = "SASET TIMEZONE"; command = "nickserv/saset/timezone"; permission = "nickserv/saset/timezone"; } + /* * ns_suspend * diff --git a/include/anope.h b/include/anope.h index 3686e2c3f..29d256db5 100644 --- a/include/anope.h +++ b/include/anope.h @@ -53,6 +53,7 @@ namespace Anope string(const char *_str, size_type n) : _string(_str, n) { } string(const std::string &_str) : _string(_str) { } string(const ci::string &_str) : _string(_str.c_str()) { } + string(const std::string_view &_sv) : _string(_sv.begin(), _sv.end()) { } string(const string &_str, size_type pos, size_type n = npos) : _string(_str._string, pos, n) { } template string(InputIterator first, InputIterator last) : _string(first, last) { } string(const string &) = default; diff --git a/modules/nickserv/ns_set_timezone.cpp b/modules/nickserv/ns_set_timezone.cpp new file mode 100644 index 000000000..a42990599 --- /dev/null +++ b/modules/nickserv/ns_set_timezone.cpp @@ -0,0 +1,235 @@ +/* NickServ core functions + * + * (C) 2003-2025 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + * + * Based on the original code of Epona by Lara. + * Based on the original code of Services by Andy Church. + */ + + +/// BEGIN CMAKE +/// target_compile_features(${SO} PRIVATE "cxx_std_20") +/// END CMAKE + +#if __cplusplus >= 202002L +# include +# define HAS_CXX20 +#endif + +#include "module.h" + +namespace +{ + Anope::map> timeregions; + std::vector timezones; +} + +class CommandNSSetTimezone + : public Command +{ +protected: + SerializableExtensibleItem &timezone; + + bool SendZones(CommandSource &source, const Anope::string &subcommand) + { + auto timeregion = timeregions.find(subcommand); + if (timeregion == timeregions.end()) + return false; + + source.Reply(_("Available timezones in the \002%s\002 region:"), + timeregion->first.c_str()); + + const auto max_length = Config->GetBlock("options").Get("linelength", "100"); + Anope::string buffer; + for (const auto &timezone : timeregion->second) + { + if (buffer.length() + 2 + timezone.length() >= max_length) + { + source.Reply(buffer); + buffer.clear(); + } + + buffer.append(buffer.empty() ? " " : ", "); + buffer.append(timezone); + } + + if (!buffer.empty()) + source.Reply(buffer); + + return true; + } + +public: + CommandNSSetTimezone(Module *creator, SerializableExtensibleItem &tz, const Anope::string &sname = "nickserv/set/timezone", size_t min = 1) + : Command(creator, sname, min, min + 1) + , timezone(tz) + { + this->SetDesc(_("Set the timezone services will use when messaging you")); + this->SetSyntax(_("\037timezone\037")); + } + + void Run(CommandSource &source, const Anope::string &user, const Anope::string ¶m) + { + if (Anope::ReadOnly) + { + source.Reply(READ_ONLY_MODE); + return; + } + + const NickAlias *na = NickAlias::Find(user); + if (!na) + { + source.Reply(NICK_X_NOT_REGISTERED, user.c_str()); + return; + } + NickCore *nc = na->nc; + + EventReturn MOD_RESULT; + FOREACH_RESULT(OnSetNickOption, MOD_RESULT, (source, this, nc, param)); + if (MOD_RESULT == EVENT_STOP) + return; + + Anope::string usertz; + for (const auto &timezone : timezones) + { + if (timezone.find_ci(param) != 0) + continue; // Timezone does not match. + + if (!usertz.empty()) + { + source.Reply(_("Multiple timezones matched \002%s\002. Please be more specific."), param.c_str()); + return; + } + + usertz = timezone; + } + + if (usertz.empty()) + { + this->OnSyntaxError(source, ""); + return; + } + + Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to change the timezone of " << nc->display << " to " << usertz; + + timezone.Set(nc, usertz); + if (source.GetAccount() == nc) + source.Reply(_("Timezone changed to \002%s\002."), usertz.c_str()); + else + source.Reply(_("Timezone for \002%s\002 changed to \002%s\002."), nc->display.c_str(), usertz.c_str()); + } + + void Execute(CommandSource &source, const std::vector ¶m) override + { + this->Run(source, source.nc->display, param[0]); + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + if (subcommand.empty()) + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_( + "Changes the timezone services uses when sending messages to you (for example, " + "when responding to a command you send). \037timezone\037 should be chosen from " + "an entry in one of the supported timezone regions:" + )); + + for (const auto &[timeregion, timezone] : timeregions) + source.Reply(" %s (%zu timezones)", timeregion.c_str(), timezone.size()); + + source.Reply(_("Type \002%s\032\037region\037\002 to list timezones for a region."), + source.service->GetQueryCommand("generic/help", source.command).c_str()); + } + else if (!SendZones(source, subcommand)) + this->OnSyntaxError(source, subcommand); + + return true; + } +}; + +class CommandNSSASetTimezone final + : public CommandNSSetTimezone +{ +public: + CommandNSSASetTimezone(Module *creator, SerializableExtensibleItem &tz) + : CommandNSSetTimezone(creator, tz, "nickserv/saset/timezone", 2) + { + this->ClearSyntax(); + this->SetSyntax(_("\037nickname\037 \037timezone\037")); + } + + void Execute(CommandSource &source, const std::vector ¶ms) override + { + this->Run(source, params[0], params[1]); + } + + bool OnHelp(CommandSource &source, const Anope::string &subcommand) override + { + if (subcommand.empty()) + { + this->SendSyntax(source); + source.Reply(" "); + source.Reply(_( + "Changes the timezone services uses when sending messages to the given user (for " + "example, when responding to a command they send). \037timezone\037 should be " + "chosen from an entry in one of the supported timezone regions:" + )); + + for (const auto &[timeregion, timezone] : timeregions) + source.Reply(" %s (%zu timezones)", timeregion.c_str(), timezone.size()); + + source.Reply(_("Type \002%s\032\037region\037\002 to list timezones for a region."), + source.service->GetQueryCommand("generic/help", source.command).c_str()); + } + else if (!SendZones(source, subcommand)) + this->OnSyntaxError(source, subcommand); + + return true; + } +}; + +class NSSetTimezone final + : public Module +{ +private: + SerializableExtensibleItem timezone; + CommandNSSetTimezone commandnssettimezone; + CommandNSSASetTimezone commandnssasettimezone; + +public: + NSSetTimezone(const Anope::string &modname, const Anope::string &creator) + : Module(modname, creator, VENDOR) + , timezone(this, "timezone") + , commandnssettimezone(this, timezone) + , commandnssasettimezone(this, timezone) + { +#ifndef HAS_CXX20 + throw ModuleException("A compiler with C++20 support is required by this module"); +#else + // Build the zone list. + const auto& tzdb = std::chrono::get_tzdb(); + for (const auto &tz : tzdb.zones) + timezones.emplace_back(tz.name()); + for (const auto &tz : tzdb.links) + timezones.emplace_back(tz.name()); + std::sort(timezones.begin(), timezones.end()); + + // Build the region list. + for (const auto &timezone : timezones) + { + auto tzsep = timezone.find('/'); + auto region = tzsep == Anope::string::npos ? "Misc" : timezone.substr(0, tzsep); + timeregions[region].push_back(timezone); + } + for (auto &[_, timeregion] : timeregions) + std::sort(timeregion.begin(), timeregion.end()); +#endif + } +}; + +MODULE_INIT(NSSetTimezone) diff --git a/src/misc.cpp b/src/misc.cpp index 2f151fd77..99663df01 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -619,14 +619,23 @@ Anope::string Anope::Duration(time_t t, const NickCore *nc, bool round) Anope::string Anope::strftime(time_t t, const NickCore *nc, bool short_output) { + static ExtensibleRef timezone("timezone"); if (nc) + { Language::SetLocale(nc->language.c_str()); + auto *tz = timezone ? timezone->Get(nc) : nullptr; + setenv("TZ", tz ? tz->c_str() : "UTC", 1); + tzset(); + } char buf[BUFSIZE]; - strftime(buf, sizeof(buf), "%b %d %Y %H:%M:%S %Z", gmtime(&t)); + strftime(buf, sizeof(buf), "%b %d %Y %H:%M:%S %Z", (nc ? localtime(&t) : gmtime(&t))); if (nc) + { + unsetenv("TZ"); Language::ResetLocale(); + } if (short_output) return buf;