summaryrefslogtreecommitdiff
path: root/modules/memoserv
diff options
context:
space:
mode:
authorSadie Powell <sadie@witchery.services>2024-01-24 12:34:03 +0000
committerSadie Powell <sadie@witchery.services>2024-01-24 12:34:34 +0000
commit484160eb4ed560eeda97c94a42dd8e31431ab251 (patch)
tree03600af16b55fa2f268904d7a42a8b87f86c09f4 /modules/memoserv
parent7ac1fe58478d58e2480b6919c4abf3a82929169c (diff)
Shuffle modules around a bit.
Diffstat (limited to 'modules/memoserv')
-rw-r--r--modules/memoserv/memoserv.cpp237
-rw-r--r--modules/memoserv/ms_cancel.cpp103
-rw-r--r--modules/memoserv/ms_check.cpp89
-rw-r--r--modules/memoserv/ms_del.cpp162
-rw-r--r--modules/memoserv/ms_ignore.cpp134
-rw-r--r--modules/memoserv/ms_info.cpp235
-rw-r--r--modules/memoserv/ms_list.cpp167
-rw-r--r--modules/memoserv/ms_read.cpp227
-rw-r--r--modules/memoserv/ms_rsend.cpp106
-rw-r--r--modules/memoserv/ms_send.cpp90
-rw-r--r--modules/memoserv/ms_sendall.cpp70
-rw-r--r--modules/memoserv/ms_set.cpp317
-rw-r--r--modules/memoserv/ms_staff.cpp67
13 files changed, 2004 insertions, 0 deletions
diff --git a/modules/memoserv/memoserv.cpp b/modules/memoserv/memoserv.cpp
new file mode 100644
index 000000000..0961017e0
--- /dev/null
+++ b/modules/memoserv/memoserv.cpp
@@ -0,0 +1,237 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+class MemoServCore final
+ : public Module
+ , public MemoServService
+{
+ Reference<BotInfo> MemoServ;
+
+ bool SendMemoMail(NickCore *nc, MemoInfo *mi, Memo *m)
+ {
+ Anope::string subject = Language::Translate(nc, Config->GetBlock("mail")->Get<const Anope::string>("memo_subject").c_str()),
+ message = Language::Translate(Config->GetBlock("mail")->Get<const Anope::string>("memo_message").c_str());
+
+ subject = subject.replace_all_cs("%n", nc->display);
+ subject = subject.replace_all_cs("%s", m->sender);
+ subject = subject.replace_all_cs("%d", stringify(mi->GetIndex(m) + 1));
+ subject = subject.replace_all_cs("%t", m->text);
+ subject = subject.replace_all_cs("%N", Config->GetBlock("networkinfo")->Get<const Anope::string>("networkname"));
+
+ message = message.replace_all_cs("%n", nc->display);
+ message = message.replace_all_cs("%s", m->sender);
+ message = message.replace_all_cs("%d", stringify(mi->GetIndex(m) + 1));
+ message = message.replace_all_cs("%t", m->text);
+ message = message.replace_all_cs("%N", Config->GetBlock("networkinfo")->Get<const Anope::string>("networkname"));
+
+ return Mail::Send(nc, subject, message);
+ }
+
+public:
+ MemoServCore(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, PSEUDOCLIENT | VENDOR),
+ MemoServService(this)
+ {
+ }
+
+ MemoResult Send(const Anope::string &source, const Anope::string &target, const Anope::string &message, bool force) override
+ {
+ bool ischan;
+ MemoInfo *mi = MemoInfo::GetMemoInfo(target, ischan);
+
+ if (mi == NULL)
+ return MEMO_INVALID_TARGET;
+
+ Anope::string sender_display = source;
+
+ User *sender = User::Find(source, true);
+ if (sender != NULL)
+ {
+ if (!sender->HasPriv("memoserv/no-limit") && !force)
+ {
+ time_t send_delay = Config->GetModule("memoserv")->Get<time_t>("senddelay");
+ if (send_delay > 0 && sender->lastmemosend + send_delay > Anope::CurTime)
+ return MEMO_TOO_FAST;
+ else if (!mi->memomax)
+ return MEMO_TARGET_FULL;
+ else if (mi->memomax > 0 && mi->memos->size() >= static_cast<unsigned>(mi->memomax))
+ return MEMO_TARGET_FULL;
+ else if (mi->HasIgnore(sender))
+ return MEMO_SUCCESS;
+ }
+
+ NickCore *acc = sender->Account();
+ if (acc != NULL)
+ {
+ sender_display = acc->display;
+ }
+ }
+
+ if (sender != NULL)
+ sender->lastmemosend = Anope::CurTime;
+
+ auto *m = new Memo();
+ m->mi = mi;
+ mi->memos->push_back(m);
+ m->owner = target;
+ m->sender = sender_display;
+ m->time = Anope::CurTime;
+ m->text = message;
+ m->unread = true;
+
+ FOREACH_MOD(OnMemoSend, (source, target, mi, m));
+
+ if (ischan)
+ {
+ ChannelInfo *ci = ChannelInfo::Find(target);
+
+ if (ci->c)
+ {
+ for (const auto &[_, cu] : ci->c->users)
+ {
+ if (ci->AccessFor(cu->user).HasPriv("MEMO"))
+ {
+ if (cu->user->Account() && cu->user->Account()->HasExt("MEMO_RECEIVE"))
+ cu->user->SendMessage(MemoServ, MEMO_NEW_X_MEMO_ARRIVED, ci->name.c_str(), Config->StrictPrivmsg.c_str(), MemoServ->nick.c_str(), ci->name.c_str(), mi->memos->size());
+ }
+ }
+ }
+ }
+ else
+ {
+ NickCore *nc = NickAlias::Find(target)->nc;
+
+ if (nc->HasExt("MEMO_RECEIVE"))
+ {
+ for (auto *na : *nc->aliases)
+ {
+ User *user = User::Find(na->nick, true);
+ if (user && user->IsIdentified())
+ user->SendMessage(MemoServ, MEMO_NEW_MEMO_ARRIVED, source.c_str(), Config->StrictPrivmsg.c_str(), MemoServ->nick.c_str(), mi->memos->size());
+ }
+ }
+
+ /* let's get out the mail if set in the nickcore - certus */
+ if (nc->HasExt("MEMO_MAIL"))
+ SendMemoMail(nc, mi, m);
+ }
+
+ return MEMO_SUCCESS;
+ }
+
+ void Check(User *u) override
+ {
+ const NickCore *nc = u->Account();
+ if (!nc)
+ return;
+
+ unsigned i = 0, end = nc->memos.memos->size(), newcnt = 0;
+ for (; i < end; ++i)
+ if (nc->memos.GetMemo(i)->unread)
+ ++newcnt;
+ if (newcnt > 0)
+ u->SendMessage(MemoServ, newcnt == 1 ? _("You have 1 new memo.") : _("You have %d new memos."), newcnt);
+ if (nc->memos.memomax > 0 && nc->memos.memos->size() >= static_cast<unsigned>(nc->memos.memomax))
+ {
+ if (nc->memos.memos->size() > static_cast<unsigned>(nc->memos.memomax))
+ u->SendMessage(MemoServ, _("You are over your maximum number of memos (%d). You will be unable to receive any new memos until you delete some of your current ones."), nc->memos.memomax);
+ else
+ u->SendMessage(MemoServ, _("You have reached your maximum number of memos (%d). You will be unable to receive any new memos until you delete some of your current ones."), nc->memos.memomax);
+ }
+ }
+
+ void OnReload(Configuration::Conf *conf) override
+ {
+ const Anope::string &msnick = conf->GetModule(this)->Get<const Anope::string>("client");
+
+ if (msnick.empty())
+ throw ConfigException(Module::name + ": <client> must be defined");
+
+ BotInfo *bi = BotInfo::Find(msnick, true);
+ if (!bi)
+ throw ConfigException(Module::name + ": no bot named " + msnick);
+
+ MemoServ = bi;
+ }
+
+ void OnNickCoreCreate(NickCore *nc) override
+ {
+ nc->memos.memomax = Config->GetModule(this)->Get<int>("maxmemos");
+ }
+
+ void OnCreateChan(ChannelInfo *ci) override
+ {
+ ci->memos.memomax = Config->GetModule(this)->Get<int>("maxmemos");
+ }
+
+ void OnBotDelete(BotInfo *bi) override
+ {
+ if (bi == MemoServ)
+ MemoServ = NULL;
+ }
+
+ void OnNickIdentify(User *u) override
+ {
+ this->Check(u);
+ }
+
+ void OnJoinChannel(User *u, Channel *c) override
+ {
+ if (c->ci && !c->ci->memos.memos->empty() && c->ci->AccessFor(u).HasPriv("MEMO"))
+ {
+ if (c->ci->memos.memos->size() == 1)
+ u->SendMessage(MemoServ, _("There is \002%zu\002 memo on channel %s."), c->ci->memos.memos->size(), c->ci->name.c_str());
+ else
+ u->SendMessage(MemoServ, _("There are \002%zu\002 memos on channel %s."), c->ci->memos.memos->size(), c->ci->name.c_str());
+ }
+ }
+
+ void OnUserAway(User *u, const Anope::string &message) override
+ {
+ if (message.empty())
+ this->Check(u);
+ }
+
+ void OnNickUpdate(User *u) override
+ {
+ this->Check(u);
+ }
+
+ void OnUserConnect(User *user, bool &exempt) override
+ {
+ this->Check(user);
+ }
+
+ EventReturn OnPreHelp(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (!params.empty() || source.c || source.service != *MemoServ)
+ return EVENT_CONTINUE;
+ source.Reply(_("\002%s\002 is a utility allowing IRC users to send short\n"
+ "messages to other IRC users, whether they are online at\n"
+ "the time or not, or to channels(*). Both the sender's\n"
+ "nickname and the target nickname or channel must be\n"
+ "registered in order to send a memo.\n"
+ "%s's commands include:"), MemoServ->nick.c_str(), MemoServ->nick.c_str());
+ return EVENT_CONTINUE;
+ }
+
+ void OnPostHelp(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (!params.empty() || source.c || source.service != *MemoServ)
+ return;
+ source.Reply(_(" \n"
+ "Type \002%s%s HELP \037command\037\002 for help on any of the\n"
+ "above commands."), Config->StrictPrivmsg.c_str(), MemoServ->nick.c_str());
+ }
+};
+
+MODULE_INIT(MemoServCore)
diff --git a/modules/memoserv/ms_cancel.cpp b/modules/memoserv/ms_cancel.cpp
new file mode 100644
index 000000000..330b55926
--- /dev/null
+++ b/modules/memoserv/ms_cancel.cpp
@@ -0,0 +1,103 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+class CommandMSCancel final
+ : public Command
+{
+public:
+ CommandMSCancel(Module *creator) : Command(creator, "memoserv/cancel", 1, 1)
+ {
+ this->SetDesc(_("Cancel the last memo you sent"));
+ this->SetSyntax(_("{\037nick\037 | \037channel\037}"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (Anope::ReadOnly)
+ {
+ source.Reply(READ_ONLY_MODE);
+ return;
+ }
+
+ const Anope::string &nname = params[0];
+
+ bool ischan;
+ MemoInfo *mi = MemoInfo::GetMemoInfo(nname, ischan);
+
+ if (mi == NULL)
+ {
+ source.Reply(ischan ? CHAN_X_NOT_REGISTERED : NICK_X_NOT_REGISTERED, nname.c_str());
+ return;
+ }
+
+ ChannelInfo *ci = NULL;
+ NickAlias *na = NULL;
+ if (ischan)
+ {
+ ci = ChannelInfo::Find(nname);
+
+ if (ci == NULL)
+ return; // can't happen
+ }
+ else
+ {
+ na = NickAlias::Find(nname);
+
+ if (na == NULL)
+ return; // can't happen
+ }
+
+ for (int i = mi->memos->size() - 1; i >= 0; --i)
+ {
+ Memo *m = mi->GetMemo(i);
+
+ if (!m->unread)
+ continue;
+
+ NickAlias *sender = NickAlias::Find(m->sender);
+
+ if (sender && sender->nc == source.GetAccount())
+ {
+ FOREACH_MOD(OnMemoDel, (ischan ? ci->name : na->nc->display, mi, m));
+ mi->Del(i);
+ source.Reply(_("Last memo to \002%s\002 has been cancelled."), (ischan ? ci->name : na->nc->display).c_str());
+ return;
+ }
+ }
+
+ source.Reply(_("No memo was cancelable."));
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Cancels the last memo you sent to the given nick or channel,\n"
+ "provided it has not been read at the time you use the command."));
+ return true;
+ }
+};
+
+class MSCancel final
+ : public Module
+{
+ CommandMSCancel commandmscancel;
+
+public:
+ MSCancel(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmscancel(this)
+ {
+ }
+};
+
+MODULE_INIT(MSCancel)
diff --git a/modules/memoserv/ms_check.cpp b/modules/memoserv/ms_check.cpp
new file mode 100644
index 000000000..d3556379d
--- /dev/null
+++ b/modules/memoserv/ms_check.cpp
@@ -0,0 +1,89 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+class CommandMSCheck final
+ : public Command
+{
+public:
+ CommandMSCheck(Module *creator) : Command(creator, "memoserv/check", 1, 1)
+ {
+ this->SetDesc(_("Checks if last memo to a nick was read"));
+ this->SetSyntax(_("\037nick\037"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+
+ const Anope::string &recipient = params[0];
+
+ bool found = false;
+
+ const NickAlias *na = NickAlias::Find(recipient);
+ if (!na)
+ {
+ source.Reply(NICK_X_NOT_REGISTERED, recipient.c_str());
+ return;
+ }
+
+ MemoInfo *mi = &na->nc->memos;
+
+ /* Okay, I know this looks strange but we want to get the LAST memo, so we
+ have to loop backwards */
+
+ for (unsigned i = mi->memos->size(); i > 0; --i)
+ {
+ Memo *m = mi->GetMemo(i - 1);
+ NickAlias *na2 = NickAlias::Find(m->sender);
+
+ if (na2 != NULL && na2->nc == source.GetAccount())
+ {
+ found = true; /* Yes, we've found the memo */
+
+ if (m->unread)
+ source.Reply(_("The last memo you sent to %s (sent on %s) has not yet been read."), na->nick.c_str(), Anope::strftime(m->time, source.GetAccount()).c_str());
+ else
+ source.Reply(_("The last memo you sent to %s (sent on %s) has been read."), na->nick.c_str(), Anope::strftime(m->time, source.GetAccount()).c_str());
+ break;
+ }
+ }
+
+ if (!found)
+ source.Reply(_("Nick %s doesn't have a memo from you."), na->nick.c_str());
+
+ return;
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Checks whether the _last_ memo you sent to \037nick\037 has been read\n"
+ "or not. Note that this only works with nicks, not with channels."));
+ return true;
+ }
+};
+
+class MSCheck final
+ : public Module
+{
+ CommandMSCheck commandmscheck;
+
+public:
+ MSCheck(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmscheck(this)
+ {
+
+ }
+};
+
+MODULE_INIT(MSCheck)
diff --git a/modules/memoserv/ms_del.cpp b/modules/memoserv/ms_del.cpp
new file mode 100644
index 000000000..469fdf221
--- /dev/null
+++ b/modules/memoserv/ms_del.cpp
@@ -0,0 +1,162 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+class MemoDelCallback final
+ : public NumberList
+{
+ CommandSource &source;
+ Command *cmd;
+ ChannelInfo *ci;
+ MemoInfo *mi;
+public:
+ MemoDelCallback(CommandSource &_source, Command *c, ChannelInfo *_ci, MemoInfo *_mi, const Anope::string &list) : NumberList(list, true), source(_source), cmd(c), ci(_ci), mi(_mi)
+ {
+ }
+
+ void HandleNumber(unsigned number) override
+ {
+ if (!number || number > mi->memos->size())
+ return;
+
+ FOREACH_MOD(OnMemoDel, (ci ? ci->name : source.nc->display, mi, mi->GetMemo(number - 1)));
+
+ mi->Del(number - 1);
+ source.Reply(_("Memo %d has been deleted."), number);
+ if (ci)
+ Log(LOG_COMMAND, source, cmd, ci) << "on memo " << number;
+ }
+};
+
+class CommandMSDel final
+ : public Command
+{
+public:
+ CommandMSDel(Module *creator) : Command(creator, "memoserv/del", 0, 2)
+ {
+ this->SetDesc(_("Delete a memo or memos"));
+ this->SetSyntax(_("[\037channel\037] {\037num\037 | \037list\037 | LAST | ALL}"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (Anope::ReadOnly)
+ {
+ source.Reply(READ_ONLY_MODE);
+ return;
+ }
+
+ MemoInfo *mi;
+ ChannelInfo *ci = NULL;
+ Anope::string numstr = !params.empty() ? params[0] : "", chan;
+
+ if (!numstr.empty() && numstr[0] == '#')
+ {
+ chan = numstr;
+ numstr = params.size() > 1 ? params[1] : "";
+
+ ci = ChannelInfo::Find(chan);
+ if (!ci)
+ {
+ source.Reply(CHAN_X_NOT_REGISTERED, chan.c_str());
+ return;
+ }
+ else if (!source.AccessFor(ci).HasPriv("MEMO"))
+ {
+ source.Reply(ACCESS_DENIED);
+ return;
+ }
+ mi = &ci->memos;
+ }
+ else
+ mi = &source.nc->memos;
+ if (numstr.empty() || (!isdigit(numstr[0]) && !numstr.equals_ci("ALL") && !numstr.equals_ci("LAST")))
+ this->OnSyntaxError(source, numstr);
+ else if (mi->memos->empty())
+ {
+ if (!chan.empty())
+ source.Reply(MEMO_X_HAS_NO_MEMOS, chan.c_str());
+ else
+ source.Reply(MEMO_HAVE_NO_MEMOS);
+ }
+ else
+ {
+ if (isdigit(numstr[0]))
+ {
+ MemoDelCallback list(source, this, ci, mi, numstr);
+ list.Process();
+ }
+ else if (numstr.equals_ci("LAST"))
+ {
+ /* Delete last memo. */
+ FOREACH_MOD(OnMemoDel, (ci ? ci->name : source.nc->display, mi, mi->GetMemo(mi->memos->size() - 1)));
+ mi->Del(mi->memos->size() - 1);
+ source.Reply(_("Memo %zu has been deleted."), mi->memos->size() + 1);
+ if (ci)
+ Log(LOG_COMMAND, source, this, ci) << "on LAST memo";
+ }
+ else
+ {
+ /* Delete all memos. */
+ for (unsigned i = mi->memos->size(); i > 0; --i)
+ {
+ FOREACH_MOD(OnMemoDel, (ci ? ci->name : source.nc->display, mi, mi->GetMemo(i)));
+ mi->Del(i - 1);
+ }
+ if (!chan.empty())
+ {
+ source.Reply(_("All memos for channel %s have been deleted."), chan.c_str());
+ Log(LOG_COMMAND, source, this, ci) << "on ALL memos";
+ }
+ else
+ source.Reply(_("All of your memos have been deleted."));
+ }
+ }
+ return;
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Deletes the specified memo or memos. You can supply\n"
+ "multiple memo numbers or ranges of numbers instead of a\n"
+ "single number, as in the second example below.\n"
+ " \n"
+ "If \002LAST\002 is given, the last memo will be deleted.\n"
+ "If \002ALL\002 is given, deletes all of your memos.\n"
+ " \n"
+ "Examples:\n"
+ " \n"
+ " \002DEL 1\002\n"
+ " Deletes your first memo.\n"
+ " \n"
+ " \002DEL 2-5,7-9\002\n"
+ " Deletes memos numbered 2 through 5 and 7 through 9."));
+ return true;
+ }
+};
+
+class MSDel final
+ : public Module
+{
+ CommandMSDel commandmsdel;
+
+public:
+ MSDel(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmsdel(this)
+ {
+
+ }
+};
+
+MODULE_INIT(MSDel)
diff --git a/modules/memoserv/ms_ignore.cpp b/modules/memoserv/ms_ignore.cpp
new file mode 100644
index 000000000..c67c1b550
--- /dev/null
+++ b/modules/memoserv/ms_ignore.cpp
@@ -0,0 +1,134 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+class CommandMSIgnore final
+ : public Command
+{
+public:
+ CommandMSIgnore(Module *creator) : Command(creator, "memoserv/ignore", 1, 3)
+ {
+ this->SetDesc(_("Manage the memo ignore list"));
+ this->SetSyntax(_("[\037channel\037] ADD \037entry\037"));
+ this->SetSyntax(_("[\037channel\037] DEL \037entry\037"));
+ this->SetSyntax(_("[\037channel\037] LIST"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (Anope::ReadOnly)
+ {
+ source.Reply(READ_ONLY_MODE);
+ return;
+ }
+
+ Anope::string channel = params[0];
+ Anope::string command = (params.size() > 1 ? params[1] : "");
+ Anope::string param = (params.size() > 2 ? params[2] : "");
+
+ if (channel[0] != '#')
+ {
+ param = command;
+ command = channel;
+ channel = source.GetNick();
+ }
+
+ bool ischan;
+ MemoInfo *mi = MemoInfo::GetMemoInfo(channel, ischan);
+ ChannelInfo *ci = ChannelInfo::Find(channel);
+ if (!mi)
+ source.Reply(ischan ? CHAN_X_NOT_REGISTERED : _(NICK_X_NOT_REGISTERED), channel.c_str());
+ else if (ischan && !source.AccessFor(ci).HasPriv("MEMO"))
+ source.Reply(ACCESS_DENIED);
+ else if (command.equals_ci("ADD") && !param.empty())
+ {
+ if (mi->ignores.size() >= Config->GetModule(this->owner)->Get<unsigned>("max", "32"))
+ {
+ source.Reply(_("Sorry, the memo ignore list for \002%s\002 is full."), channel.c_str());
+ return;
+ }
+
+ if (std::find(mi->ignores.begin(), mi->ignores.end(), param.ci_str()) == mi->ignores.end())
+ {
+ mi->ignores.emplace_back(param.ci_str());
+ source.Reply(_("\002%s\002 added to ignore list."), param.c_str());
+ }
+ else
+ source.Reply(_("\002%s\002 is already on the ignore list."), param.c_str());
+ }
+ else if (command.equals_ci("DEL") && !param.empty())
+ {
+ std::vector<Anope::string>::iterator it = std::find(mi->ignores.begin(), mi->ignores.end(), param.ci_str());
+
+ if (it != mi->ignores.end())
+ {
+ mi->ignores.erase(it);
+ source.Reply(_("\002%s\002 removed from the ignore list."), param.c_str());
+ }
+ else
+ source.Reply(_("\002%s\002 is not on the ignore list."), param.c_str());
+ }
+ else if (command.equals_ci("LIST"))
+ {
+ if (mi->ignores.empty())
+ source.Reply(_("Memo ignore list is empty."));
+ else
+ {
+ ListFormatter list(source.GetAccount());
+ list.AddColumn(_("Mask"));
+ for (const auto &ignore : mi->ignores)
+ {
+ ListFormatter::ListEntry entry;
+ entry["Mask"] = ignore;
+ list.AddEntry(entry);
+ }
+
+ source.Reply(_("Ignore list:"));
+
+ std::vector<Anope::string> replies;
+ list.Process(replies);
+
+ for (const auto &reply : replies)
+ source.Reply(reply);
+ }
+ }
+ else
+ this->OnSyntaxError(source, "");
+
+ return;
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Allows you to ignore users by nick or host from memoing\n"
+ "you or a channel. If someone on the memo ignore list tries\n"
+ "to memo you or a channel, they will not be told that you have\n"
+ "them ignored."));
+ return true;
+ }
+};
+
+class MSIgnore final
+ : public Module
+{
+ CommandMSIgnore commandmsignore;
+
+public:
+ MSIgnore(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmsignore(this)
+ {
+ }
+};
+
+MODULE_INIT(MSIgnore)
diff --git a/modules/memoserv/ms_info.cpp b/modules/memoserv/ms_info.cpp
new file mode 100644
index 000000000..c76a446cd
--- /dev/null
+++ b/modules/memoserv/ms_info.cpp
@@ -0,0 +1,235 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+class CommandMSInfo final
+ : public Command
+{
+public:
+ CommandMSInfo(Module *creator) : Command(creator, "memoserv/info", 0, 1)
+ {
+ this->SetDesc(_("Displays information about your memos"));
+ this->SetSyntax(_("[\037nick\037 | \037channel\037]"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ NickCore *nc = source.nc;
+ const MemoInfo *mi;
+ const NickAlias *na = NULL;
+ ChannelInfo *ci = NULL;
+ const Anope::string &nname = !params.empty() ? params[0] : "";
+ bool hardmax;
+
+ if (!nname.empty() && nname[0] != '#' && source.HasPriv("memoserv/info"))
+ {
+ na = NickAlias::Find(nname);
+ if (!na)
+ {
+ source.Reply(NICK_X_NOT_REGISTERED, nname.c_str());
+ return;
+ }
+ mi = &na->nc->memos;
+ hardmax = na->nc->HasExt("MEMO_HARDMAX");
+ }
+ else if (!nname.empty() && nname[0] == '#')
+ {
+ ci = ChannelInfo::Find(nname);
+ if (!ci)
+ {
+ source.Reply(CHAN_X_NOT_REGISTERED, nname.c_str());
+ return;
+ }
+ else if (!source.AccessFor(ci).HasPriv("MEMO"))
+ {
+ source.Reply(ACCESS_DENIED);
+ return;
+ }
+ mi = &ci->memos;
+ hardmax = ci->HasExt("MEMO_HARDMAX");
+ }
+ else if (!nname.empty()) /* It's not a chan and we aren't services admin */
+ {
+ source.Reply(ACCESS_DENIED);
+ return;
+ }
+ else
+ {
+ mi = &nc->memos;
+ hardmax = nc->HasExt("MEMO_HARDMAX");
+ }
+
+ if (!nname.empty() && (ci || na->nc != nc))
+ {
+ if (mi->memos->empty())
+ source.Reply(_("%s currently has no memos."), nname.c_str());
+ else if (mi->memos->size() == 1)
+ {
+ if (mi->GetMemo(0)->unread)
+ source.Reply(_("%s currently has \0021\002 memo, and it has not yet been read."), nname.c_str());
+ else
+ source.Reply(_("%s currently has \0021\002 memo."), nname.c_str());
+ }
+ else
+ {
+ size_t count = 0;
+ for (size_t i = 0; i < mi->memos->size(); ++i)
+ if (mi->GetMemo(i)->unread)
+ ++count;
+
+ if (count == mi->memos->size())
+ source.Reply(_("%s currently has \002%zu\002 memos; all of them are unread."), nname.c_str(), count);
+ else if (!count)
+ source.Reply(_("%s currently has \002%zu\002 memos."), nname.c_str(), mi->memos->size());
+ else if (count == 1)
+ source.Reply(_("%s currently has \002%zu\002 memos, of which \0021\002 is unread."), nname.c_str(), mi->memos->size());
+ else
+ source.Reply(_("%s currently has \002%zu\002 memos, of which \002%zu\002 are unread."), nname.c_str(), mi->memos->size(), count);
+ }
+ if (!mi->memomax)
+ {
+ if (hardmax)
+ source.Reply(_("%s's memo limit is \002%d\002, and may not be changed."), nname.c_str(), mi->memomax);
+ else
+ source.Reply(_("%s's memo limit is \002%d\002."), nname.c_str(), mi->memomax);
+ }
+ else if (mi->memomax > 0)
+ {
+ if (hardmax)
+ source.Reply(_("%s's memo limit is \002%d\002, and may not be changed."), nname.c_str(), mi->memomax);
+ else
+ source.Reply(_("%s's memo limit is \002%d\002."), nname.c_str(), mi->memomax);
+ }
+ else
+ source.Reply(_("%s has no memo limit."), nname.c_str());
+
+ /* I ripped this code out of ircservices 4.4.5, since I didn't want
+ to rewrite the whole thing (it pisses me off). */
+ if (na)
+ {
+ if (na->nc->HasExt("MEMO_RECEIVE") && na->nc->HasExt("MEMO_SIGNON"))
+ source.Reply(_("%s is notified of new memos at logon and when they arrive."), nname.c_str());
+ else if (na->nc->HasExt("MEMO_RECEIVE"))
+ source.Reply(_("%s is notified when new memos arrive."), nname.c_str());
+ else if (na->nc->HasExt("MEMO_SIGNON"))
+ source.Reply(_("%s is notified of new memos at logon."), nname.c_str());
+ else
+ source.Reply(_("%s is not notified of new memos."), nname.c_str());
+ }
+ }
+ else /* !nname || (!ci || na->nc == nc) */
+ {
+ if (mi->memos->empty())
+ source.Reply(_("You currently have no memos."));
+ else if (mi->memos->size() == 1)
+ {
+ if (mi->GetMemo(0)->unread)
+ source.Reply(_("You currently have \0021\002 memo, and it has not yet been read."));
+ else
+ source.Reply(_("You currently have \0021\002 memo."));
+ }
+ else
+ {
+ size_t count = 0;
+ for (size_t i = 0; i < mi->memos->size(); ++i)
+ if (mi->GetMemo(i)->unread)
+ ++count;
+
+ if (count == mi->memos->size())
+ source.Reply(_("You currently have \002%zu\002 memos; all of them are unread."), count);
+ else if (!count)
+ source.Reply(_("You currently have \002%zu\002 memos."), mi->memos->size());
+ else if (count == 1)
+ source.Reply(_("You currently have \002%zu\002 memos, of which \0021\002 is unread."), mi->memos->size());
+ else
+ source.Reply(_("You currently have \002%zu\002 memos, of which \002%zu\002 are unread."), mi->memos->size(), count);
+ }
+
+ if (!mi->memomax)
+ {
+ if (!source.IsServicesOper() && hardmax)
+ source.Reply(_("Your memo limit is \0020\002; you will not receive any new memos. You cannot change this limit."));
+ else
+ source.Reply(_("Your memo limit is \0020\002; you will not receive any new memos."));
+ }
+ else if (mi->memomax > 0)
+ {
+ if (!source.IsServicesOper() && hardmax)
+ source.Reply(_("Your memo limit is \002%d\002, and may not be changed."), mi->memomax);
+ else
+ source.Reply(_("Your memo limit is \002%d\002."), mi->memomax);
+ }
+ else
+ source.Reply(_("You have no limit on the number of memos you may keep."));
+
+ bool memo_mail = nc->HasExt("MEMO_MAIL");
+ if (nc->HasExt("MEMO_RECEIVE") && nc->HasExt("MEMO_SIGNON"))
+ {
+ if (memo_mail)
+ source.Reply(_("You will be notified of new memos at logon and when they arrive, and by mail when they arrive."));
+ else
+ source.Reply(_("You will be notified of new memos at logon and when they arrive."));
+ }
+ else if (nc->HasExt("MEMO_RECEIVE"))
+ {
+ if (memo_mail)
+ source.Reply(_("You will be notified by message and by mail when new memos arrive."));
+ else
+ source.Reply(_("You will be notified when new memos arrive."));
+ }
+ else if (nc->HasExt("MEMO_SIGNON"))
+ {
+ if (memo_mail)
+ source.Reply(_("You will be notified of new memos at logon, and by mail when they arrive."));
+ else
+ source.Reply(_("You will be notified of new memos at logon."));
+ }
+ else
+ {
+ source.Reply(_("You will not be notified of new memos."));
+ }
+ }
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Without a parameter, displays information on the number of\n"
+ "memos you have, how many of them are unread, and how many\n"
+ "total memos you can receive.\n"
+ " \n"
+ "With a channel parameter, displays the same information for\n"
+ "the given channel.\n"
+ " \n"
+ "With a nickname parameter, displays the same information\n"
+ "for the given nickname. This is limited to \002Services\002\n"
+ "\002Operators\002."));
+
+ return true;
+ }
+};
+
+class MSInfo final
+ : public Module
+{
+ CommandMSInfo commandmsinfo;
+
+public:
+ MSInfo(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmsinfo(this)
+ {
+
+ }
+};
+
+MODULE_INIT(MSInfo)
diff --git a/modules/memoserv/ms_list.cpp b/modules/memoserv/ms_list.cpp
new file mode 100644
index 000000000..127cfdaaf
--- /dev/null
+++ b/modules/memoserv/ms_list.cpp
@@ -0,0 +1,167 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+class CommandMSList final
+ : public Command
+{
+public:
+ CommandMSList(Module *creator) : Command(creator, "memoserv/list", 0, 2)
+ {
+ this->SetDesc(_("List your memos"));
+ this->SetSyntax(_("[\037channel\037] [\037list\037 | NEW]"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+
+ Anope::string param = !params.empty() ? params[0] : "", chan;
+ ChannelInfo *ci = NULL;
+ const MemoInfo *mi;
+
+ if (!param.empty() && param[0] == '#')
+ {
+ chan = param;
+ param = params.size() > 1 ? params[1] : "";
+
+ ci = ChannelInfo::Find(chan);
+ if (!ci)
+ {
+ source.Reply(CHAN_X_NOT_REGISTERED, chan.c_str());
+ return;
+ }
+ else if (!source.AccessFor(ci).HasPriv("MEMO"))
+ {
+ source.Reply(ACCESS_DENIED);
+ return;
+ }
+ mi = &ci->memos;
+ }
+ else
+ mi = &source.nc->memos;
+
+ if (!param.empty() && !isdigit(param[0]) && !param.equals_ci("NEW"))
+ this->OnSyntaxError(source, param);
+ else if (!mi->memos->size())
+ {
+ if (!chan.empty())
+ source.Reply(MEMO_X_HAS_NO_MEMOS, chan.c_str());
+ else
+ source.Reply(MEMO_HAVE_NO_MEMOS);
+ }
+ else
+ {
+ ListFormatter list(source.GetAccount());
+
+ list.AddColumn(_("Number")).AddColumn(_("Sender")).AddColumn(_("Date/Time"));
+
+ if (!param.empty() && isdigit(param[0]))
+ {
+ class MemoListCallback final
+ : public NumberList
+ {
+ ListFormatter &list;
+ CommandSource &source;
+ const MemoInfo *mi;
+ public:
+ MemoListCallback(ListFormatter &_list, CommandSource &_source, const MemoInfo *_mi, const Anope::string &numlist) : NumberList(numlist, false), list(_list), source(_source), mi(_mi)
+ {
+ }
+
+ void HandleNumber(unsigned number) override
+ {
+ if (!number || number > mi->memos->size())
+ return;
+
+ const Memo *m = mi->GetMemo(number - 1);
+
+ ListFormatter::ListEntry entry;
+ entry["Number"] = (m->unread ? "* " : " ") + stringify(number);
+ entry["Sender"] = m->sender;
+ entry["Date/Time"] = Anope::strftime(m->time, source.GetAccount());
+ this->list.AddEntry(entry);
+ }
+ }
+ mlc(list, source, mi, param);
+ mlc.Process();
+ }
+ else
+ {
+ if (!param.empty())
+ {
+ unsigned i, end;
+ for (i = 0, end = mi->memos->size(); i < end; ++i)
+ if (mi->GetMemo(i)->unread)
+ break;
+ if (i == end)
+ {
+ if (!chan.empty())
+ source.Reply(MEMO_X_HAS_NO_NEW_MEMOS, chan.c_str());
+ else
+ source.Reply(MEMO_HAVE_NO_NEW_MEMOS);
+ return;
+ }
+ }
+
+ for (unsigned i = 0, end = mi->memos->size(); i < end; ++i)
+ {
+ if (!param.empty() && !mi->GetMemo(i)->unread)
+ continue;
+
+ const Memo *m = mi->GetMemo(i);
+
+ ListFormatter::ListEntry entry;
+ entry["Number"] = (m->unread ? "* " : " ") + stringify(i + 1);
+ entry["Sender"] = m->sender;
+ entry["Date/Time"] = Anope::strftime(m->time, source.GetAccount());
+ list.AddEntry(entry);
+ }
+ }
+
+ std::vector<Anope::string> replies;
+ list.Process(replies);
+
+ source.Reply(_("Memos for %s:"), ci ? ci->name.c_str() : source.GetNick().c_str());
+ for (const auto &reply : replies)
+ source.Reply(reply);
+ }
+ return;
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Lists any memos you currently have. With \002NEW\002, lists only\n"
+ "new (unread) memos. Unread memos are marked with a \"*\"\n"
+ "to the left of the memo number. You can also specify a list\n"
+ "of numbers, as in the example below:\n"
+ " \002LIST 2-5,7-9\002\n"
+ " Lists memos numbered 2 through 5 and 7 through 9."));
+ return true;
+ }
+};
+
+class MSList final
+ : public Module
+{
+ CommandMSList commandmslist;
+
+public:
+ MSList(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmslist(this)
+ {
+
+ }
+};
+
+MODULE_INIT(MSList)
diff --git a/modules/memoserv/ms_read.cpp b/modules/memoserv/ms_read.cpp
new file mode 100644
index 000000000..5e9c8fd83
--- /dev/null
+++ b/modules/memoserv/ms_read.cpp
@@ -0,0 +1,227 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+static ServiceReference<MemoServService> MemoServService("MemoServService", "MemoServ");
+
+static void rsend_notify(CommandSource &source, MemoInfo *mi, Memo *m, const Anope::string &targ)
+{
+ /* Only send receipt if memos are allowed */
+ if (MemoServService && !Anope::ReadOnly)
+ {
+ /* Get nick alias for sender */
+ const NickAlias *na = NickAlias::Find(m->sender);
+
+ if (!na)
+ return;
+
+ /* Get nick core for sender */
+ const NickCore *nc = na->nc;
+
+ if (!nc)
+ return;
+
+ /* Text of the memo varies if the recipient was a
+ nick or channel */
+ Anope::string text = Anope::printf(Language::Translate(na->nc, _("\002[auto-memo]\002 The memo you sent to %s has been viewed.")), targ.c_str());
+
+ /* Send notification */
+ MemoServService->Send(source.GetNick(), m->sender, text, true);
+
+ /* Notify recipient of the memo that a notification has
+ been sent to the sender */
+ source.Reply(_("A notification memo has been sent to %s informing them you have\n"
+ "read their memo."), nc->display.c_str());
+ }
+
+ /* Remove receipt flag from the original memo */
+ m->receipt = false;
+}
+
+class MemoListCallback final
+ : public NumberList
+{
+ CommandSource &source;
+ MemoInfo *mi;
+ const ChannelInfo *ci;
+ bool found;
+public:
+ MemoListCallback(CommandSource &_source, MemoInfo *_mi, const ChannelInfo *_ci, const Anope::string &numlist) : NumberList(numlist, false), source(_source), mi(_mi), ci(_ci)
+ {
+ found = false;
+ }
+
+ ~MemoListCallback() override
+ {
+ if (!found)
+ source.Reply(_("No memos to display."));
+ }
+
+ void HandleNumber(unsigned number) override
+ {
+ if (!number || number > mi->memos->size())
+ return;
+
+ MemoListCallback::DoRead(source, mi, ci, number - 1);
+ found = true;
+ }
+
+ static void DoRead(CommandSource &source, MemoInfo *mi, const ChannelInfo *ci, unsigned index)
+ {
+ Memo *m = mi->GetMemo(index);
+ if (!m)
+ return;
+
+ if (ci)
+ source.Reply(_("Memo %d from %s (%s)."), index + 1, m->sender.c_str(), Anope::strftime(m->time, source.GetAccount()).c_str());
+ else
+ source.Reply(_("Memo %d from %s (%s)."), index + 1, m->sender.c_str(), Anope::strftime(m->time, source.GetAccount()).c_str());
+
+ BotInfo *bi;
+ Anope::string cmd;
+ if (Command::FindCommandFromService("memoserv/del", bi, cmd))
+ {
+ if (ci)
+ source.Reply(_("To delete, type: \002%s%s %s %s %d\002"), Config->StrictPrivmsg.c_str(), bi->nick.c_str(), cmd.c_str(), ci->name.c_str(), index + 1);
+ else
+ source.Reply(_("To delete, type: \002%s%s %s %d\002"), Config->StrictPrivmsg.c_str(), bi->nick.c_str(), cmd.c_str(), index + 1);
+ }
+
+ source.Reply("%s", m->text.c_str());
+ m->unread = false;
+
+ /* Check if a receipt notification was requested */
+ if (m->receipt)
+ rsend_notify(source, mi, m, ci ? ci->name : source.GetNick());
+ }
+};
+
+class CommandMSRead final
+ : public Command
+{
+public:
+ CommandMSRead(Module *creator) : Command(creator, "memoserv/read", 1, 2)
+ {
+ this->SetDesc(_("Read a memo or memos"));
+ this->SetSyntax(_("[\037channel\037] {\037num\037 | \037list\037 | LAST | NEW | ALL}"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+
+ MemoInfo *mi;
+ ChannelInfo *ci = NULL;
+ Anope::string numstr = params[0], chan;
+
+ if (!numstr.empty() && numstr[0] == '#')
+ {
+ chan = numstr;
+ numstr = params.size() > 1 ? params[1] : "";
+
+ ci = ChannelInfo::Find(chan);
+ if (!ci)
+ {
+ source.Reply(CHAN_X_NOT_REGISTERED, chan.c_str());
+ return;
+ }
+ else if (!source.AccessFor(ci).HasPriv("MEMO"))
+ {
+ source.Reply(ACCESS_DENIED);
+ return;
+ }
+ mi = &ci->memos;
+ }
+ else
+ mi = &source.nc->memos;
+
+ if (numstr.empty() || (!numstr.equals_ci("LAST") && !numstr.equals_ci("NEW") && !numstr.equals_ci("ALL") && numstr.find_first_not_of("0123456789.,-") != Anope::string::npos))
+ this->OnSyntaxError(source, numstr);
+ else if (mi->memos->empty())
+ {
+ if (!chan.empty())
+ source.Reply(MEMO_X_HAS_NO_MEMOS, chan.c_str());
+ else
+ source.Reply(MEMO_HAVE_NO_MEMOS);
+ }
+ else
+ {
+ int i, end;
+
+ if (numstr.equals_ci("NEW"))
+ {
+ int readcount = 0;
+ for (i = 0, end = mi->memos->size(); i < end; ++i)
+ if (mi->GetMemo(i)->unread)
+ {
+ MemoListCallback::DoRead(source, mi, ci, i);
+ ++readcount;
+ }
+ if (!readcount)
+ {
+ if (!chan.empty())
+ source.Reply(MEMO_X_HAS_NO_NEW_MEMOS, chan.c_str());
+ else
+ source.Reply(MEMO_HAVE_NO_NEW_MEMOS);
+ }
+ }
+ else if (numstr.equals_ci("LAST"))
+ {
+ for (i = 0, end = mi->memos->size() - 1; i < end; ++i);
+ MemoListCallback::DoRead(source, mi, ci, i);
+ }
+ else if (numstr.equals_ci("ALL"))
+ {
+ for (i = 0, end = mi->memos->size(); i < end; ++i)
+ {
+ MemoListCallback::DoRead(source, mi, ci, i);
+ }
+ }
+ else /* number[s] */
+ {
+ MemoListCallback list(source, mi, ci, numstr);
+ list.Process();
+ }
+ }
+ return;
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Sends you the text of the memos specified. If LAST is\n"
+ "given, sends you the memo you most recently received. If\n"
+ "NEW is given, sends you all of your new memos. If ALL is\n"
+ "given, sends you all of your memos. Otherwise, sends you\n"
+ "memo number \037num\037. You can also give a list of numbers,\n"
+ "as in this example:\n"
+ " \n"
+ " \002READ 2-5,7-9\002\n"
+ " Displays memos numbered 2 through 5 and 7 through 9."));
+ return true;
+ }
+};
+
+class MSRead final
+ : public Module
+{
+ CommandMSRead commandmsread;
+
+public:
+ MSRead(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmsread(this)
+ {
+
+ }
+};
+
+MODULE_INIT(MSRead)
diff --git a/modules/memoserv/ms_rsend.cpp b/modules/memoserv/ms_rsend.cpp
new file mode 100644
index 000000000..a5de58e56
--- /dev/null
+++ b/modules/memoserv/ms_rsend.cpp
@@ -0,0 +1,106 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+namespace
+{
+ ServiceReference<MemoServService> memoserv("MemoServService", "MemoServ");
+}
+
+class CommandMSRSend final
+ : public Command
+{
+public:
+ CommandMSRSend(Module *creator) : Command(creator, "memoserv/rsend", 2, 2)
+ {
+ this->SetDesc(_("Sends a memo and requests a read receipt"));
+ this->SetSyntax(_("{\037nick\037 | \037channel\037} \037memo-text\037"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (!memoserv)
+ return;
+
+ if (Anope::ReadOnly && !source.IsOper())
+ {
+ source.Reply(MEMO_SEND_DISABLED);
+ return;
+ }
+
+ const Anope::string &nick = params[0];
+ const Anope::string &text = params[1];
+ const NickAlias *na = NULL;
+
+ /* prevent user from rsend to themselves */
+ if ((na = NickAlias::Find(nick)) && na->nc == source.GetAccount())
+ {
+ source.Reply(_("You can not request a receipt when sending a memo to yourself."));
+ return;
+ }
+
+ if (Config->GetModule(this->owner)->Get<bool>("operonly") && !source.IsServicesOper())
+ source.Reply(ACCESS_DENIED);
+ else
+ {
+ MemoServService::MemoResult result = memoserv->Send(source.GetNick(), nick, text);
+ if (result == MemoServService::MEMO_INVALID_TARGET)
+ source.Reply(_("\002%s\002 is not a registered unforbidden nick or channel."), nick.c_str());
+ else if (result == MemoServService::MEMO_TOO_FAST)
+ source.Reply(_("Please wait %lu seconds before using the %s command again."), Config->GetModule("memoserv")->Get<unsigned long>("senddelay"), source.command.c_str());
+ else if (result == MemoServService::MEMO_TARGET_FULL)
+ source.Reply(_("Sorry, %s currently has too many memos and cannot receive more."), nick.c_str());
+ else
+ {
+ source.Reply(_("Memo sent to \002%s\002."), nick.c_str());
+
+ bool ischan;
+ MemoInfo *mi = MemoInfo::GetMemoInfo(nick, ischan);
+ if (mi == NULL)
+ throw CoreException("NULL mi in ms_rsend");
+ Memo *m = (mi->memos->size() ? mi->GetMemo(mi->memos->size() - 1) : NULL);
+ if (m != NULL)
+ m->receipt = true;
+ }
+ }
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Sends the named \037nick\037 or \037channel\037 a memo containing\n"
+ "\037memo-text\037. When sending to a nickname, the recipient will\n"
+ "receive a notice that they have a new memo. The target\n"
+ "nickname/channel must be registered.\n"
+ "Once the memo is read by its recipient, an automatic notification\n"
+ "memo will be sent to the sender informing them that the memo\n"
+ "has been read."));
+ return true;
+ }
+};
+
+class MSRSend final
+ : public Module
+{
+ CommandMSRSend commandmsrsend;
+
+public:
+ MSRSend(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmsrsend(this)
+ {
+ if (!memoserv)
+ throw ModuleException("No MemoServ!");
+ }
+};
+
+MODULE_INIT(MSRSend)
diff --git a/modules/memoserv/ms_send.cpp b/modules/memoserv/ms_send.cpp
new file mode 100644
index 000000000..1335f1ad4
--- /dev/null
+++ b/modules/memoserv/ms_send.cpp
@@ -0,0 +1,90 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+namespace
+{
+ ServiceReference<MemoServService> memoserv("MemoServService", "MemoServ");
+}
+
+class CommandMSSend final
+ : public Command
+{
+public:
+ CommandMSSend(Module *creator) : Command(creator, "memoserv/send", 2, 2)
+ {
+ this->SetDesc(_("Send a memo to a nick or channel"));
+ this->SetSyntax(_("{\037nick\037 | \037channel\037} \037memo-text\037"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (!memoserv)
+ return;
+
+ const Anope::string &nick = params[0];
+ const Anope::string &text = params[1];
+
+ if (Anope::ReadOnly && !source.IsOper())
+ {
+ source.Reply(MEMO_SEND_DISABLED);
+ return;
+ }
+
+ if (source.GetAccount()->HasExt("UNCONFIRMED"))
+ {
+ source.Reply(_("You must confirm your account before you may send a memo."));
+ return;
+ }
+
+ MemoServService::MemoResult result = memoserv->Send(source.GetNick(), nick, text);
+ if (result == MemoServService::MEMO_SUCCESS)
+ {
+ source.Reply(_("Memo sent to \002%s\002."), nick.c_str());
+ Log(LOG_COMMAND, source, this) << "to send a memo to " << nick;
+ }
+ else if (result == MemoServService::MEMO_INVALID_TARGET)
+ source.Reply(_("\002%s\002 is not a registered unforbidden nick or channel."), nick.c_str());
+ else if (result == MemoServService::MEMO_TOO_FAST)
+ source.Reply(_("Please wait %lu seconds before using the %s command again."), Config->GetModule("memoserv")->Get<unsigned long>("senddelay"), source.command.c_str());
+ else if (result == MemoServService::MEMO_TARGET_FULL)
+ source.Reply(_("Sorry, %s currently has too many memos and cannot receive more."), nick.c_str());
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Sends the named \037nick\037 or \037channel\037 a memo containing\n"
+ "\037memo-text\037. When sending to a nickname, the recipient will\n"
+ "receive a notice that they have a new memo. The target\n"
+ "nickname/channel must be registered."));
+ return true;
+ }
+};
+
+class MSSend final
+ : public Module
+{
+ CommandMSSend commandmssend;
+
+public:
+ MSSend(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmssend(this)
+ {
+
+ if (!memoserv)
+ throw ModuleException("No MemoServ!");
+ }
+};
+
+MODULE_INIT(MSSend)
diff --git a/modules/memoserv/ms_sendall.cpp b/modules/memoserv/ms_sendall.cpp
new file mode 100644
index 000000000..c04ff7a3f
--- /dev/null
+++ b/modules/memoserv/ms_sendall.cpp
@@ -0,0 +1,70 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+namespace
+{
+ ServiceReference<MemoServService> memoserv("MemoServService", "MemoServ");
+}
+
+class CommandMSSendAll final
+ : public Command
+{
+public:
+ CommandMSSendAll(Module *creator) : Command(creator, "memoserv/sendall", 1, 1)
+ {
+ this->SetDesc(_("Send a memo to all registered users"));
+ this->SetSyntax(_("\037memo-text\037"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (!memoserv)
+ return;
+
+ const Anope::string &text = params[0];
+
+ Log(LOG_ADMIN, source, this) << "to send " << text;
+
+ for (const auto &[_, nc] : *NickCoreList)
+ {
+ if (nc != source.nc)
+ memoserv->Send(source.GetNick(), nc->display, text);
+ }
+
+ source.Reply(_("A massmemo has been sent to all registered users."));
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Sends all registered users a memo containing \037memo-text\037."));
+ return true;
+ }
+};
+
+class MSSendAll final
+ : public Module
+{
+ CommandMSSendAll commandmssendall;
+
+public:
+ MSSendAll(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmssendall(this)
+ {
+ if (!memoserv)
+ throw ModuleException("No MemoServ!");
+ }
+};
+
+MODULE_INIT(MSSendAll)
diff --git a/modules/memoserv/ms_set.cpp b/modules/memoserv/ms_set.cpp
new file mode 100644
index 000000000..83b28fb20
--- /dev/null
+++ b/modules/memoserv/ms_set.cpp
@@ -0,0 +1,317 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+class CommandMSSet final
+ : public Command
+{
+private:
+ void DoNotify(CommandSource &source, const std::vector<Anope::string> &params, MemoInfo *mi)
+ {
+ const Anope::string &param = params[1];
+ NickCore *nc = source.nc;
+ BotInfo *MemoServ = Config->GetClient("MemoServ");
+
+ if (!MemoServ)
+ return;
+
+ if (param.equals_ci("ON"))
+ {
+ nc->Extend<bool>("MEMO_SIGNON");
+ nc->Extend<bool>("MEMO_RECEIVE");
+ source.Reply(_("%s will now notify you of memos when you log on and when they are sent to you."), MemoServ->nick.c_str());
+ }
+ else if (param.equals_ci("LOGON"))
+ {
+ nc->Extend<bool>("MEMO_SIGNON");
+ nc->Shrink<bool>("MEMO_RECEIVE");
+ source.Reply(_("%s will now notify you of memos when you log on or unset /AWAY."), MemoServ->nick.c_str());
+ }
+ else if (param.equals_ci("NEW"))
+ {
+ nc->Shrink<bool>("MEMO_SIGNON");
+ nc->Extend<bool>("MEMO_RECEIVE");
+ source.Reply(_("%s will now notify you of memos when they are sent to you."), MemoServ->nick.c_str());
+ }
+ else if (param.equals_ci("MAIL"))
+ {
+ if (!nc->email.empty())
+ {
+ nc->Extend<bool>("MEMO_MAIL");
+ source.Reply(_("You will now be informed about new memos via email."));
+ }
+ else
+ source.Reply(_("There's no email address set for your nick."));
+ }
+ else if (param.equals_ci("NOMAIL"))
+ {
+ nc->Shrink<bool>("MEMO_MAIL");
+ source.Reply(_("You will no longer be informed via email."));
+ }
+ else if (param.equals_ci("OFF"))
+ {
+ nc->Shrink<bool>("MEMO_SIGNON");
+ nc->Shrink<bool>("MEMO_RECEIVE");
+ nc->Shrink<bool>("MEMO_MAIL");
+ source.Reply(_("%s will not send you any notification of memos."), MemoServ->nick.c_str());
+ }
+ else
+ this->OnSyntaxError(source, "");
+ }
+
+ void DoLimit(CommandSource &source, const std::vector<Anope::string> &params, MemoInfo *mi)
+ {
+
+ Anope::string p1 = params[1];
+ Anope::string p2 = params.size() > 2 ? params[2] : "";
+ Anope::string p3 = params.size() > 3 ? params[3] : "";
+ Anope::string user, chan;
+ int16_t limit;
+ NickCore *nc = source.nc;
+ ChannelInfo *ci = NULL;
+ bool is_servadmin = source.HasPriv("memoserv/set-limit");
+
+ if (p1[0] == '#')
+ {
+ chan = p1;
+ p1 = p2;
+ p2 = p3;
+ p3 = params.size() > 4 ? params[4] : "";
+
+ ci = ChannelInfo::Find(chan);
+ if (!ci)
+ {
+ source.Reply(CHAN_X_NOT_REGISTERED, chan.c_str());
+ return;
+ }
+ else if (!is_servadmin && !source.AccessFor(ci).HasPriv("MEMO"))
+ {
+ source.Reply(ACCESS_DENIED);
+ return;
+ }
+ mi = &ci->memos;
+ }
+ if (is_servadmin)
+ {
+ if (!p2.empty() && !p2.equals_ci("HARD") && chan.empty())
+ {
+ const NickAlias *na;
+ if (!(na = NickAlias::Find(p1)))
+ {
+ source.Reply(NICK_X_NOT_REGISTERED, p1.c_str());
+ return;
+ }
+ user = p1;
+ mi = &na->nc->memos;
+ nc = na->nc;
+ p1 = p2;
+ p2 = p3;
+ }
+ else if (p1.empty() || (!p1.is_pos_number_only() && !p1.equals_ci("NONE")) || (!p2.empty() && !p2.equals_ci("HARD")))
+ {
+ this->OnSyntaxError(source, "");
+ return;
+ }
+ if (!chan.empty())
+ {
+ if (!p2.empty())
+ ci->Extend<bool>("MEMO_HARDMAX");
+ else
+ ci->Shrink<bool>("MEMO_HARDMAX");
+ }
+ else
+ {
+ if (!p2.empty())
+ nc->Extend<bool>("MEMO_HARDMAX");
+ else
+ nc->Shrink<bool>("MEMO_HARDMAX");
+ }
+ limit = -1;
+ try
+ {
+ limit = convertTo<int16_t>(p1);
+ }
+ catch (const ConvertException &) { }
+ }
+ else
+ {
+ if (p1.empty() || !p2.empty() || !isdigit(p1[0]))
+ {
+ this->OnSyntaxError(source, "");
+ return;
+ }
+ if (!chan.empty() && ci->HasExt("MEMO_HARDMAX"))
+ {
+ source.Reply(_("The memo limit for %s may not be changed."), chan.c_str());
+ return;
+ }
+ else if (chan.empty() && nc->HasExt("MEMO_HARDMAX"))
+ {
+ source.Reply(_("You are not permitted to change your memo limit."));
+ return;
+ }
+ int max_memos = Config->GetModule("memoserv")->Get<int>("maxmemos");
+ limit = -1;
+ try
+ {
+ limit = convertTo<int16_t>(p1);
+ }
+ catch (const ConvertException &) { }
+ /* The first character is a digit, but we could still go negative
+ * from overflow... watch out! */
+ if (limit < 0 || (max_memos > 0 && limit > max_memos))
+ {
+ if (!chan.empty())
+ source.Reply(_("You cannot set the memo limit for %s higher than %d."), chan.c_str(), max_memos);
+ else
+ source.Reply(_("You cannot set your memo limit higher than %d."), max_memos);
+ return;
+ }
+ }
+ mi->memomax = limit;
+ if (limit > 0)
+ {
+ if (chan.empty() && nc == source.nc)
+ source.Reply(_("Your memo limit has been set to \002%d\002."), limit);
+ else
+ source.Reply(_("Memo limit for %s set to \002%d\002."), !chan.empty() ? chan.c_str() : user.c_str(), limit);
+ }
+ else if (!limit)
+ {
+ if (chan.empty() && nc == source.nc)
+ source.Reply(_("You will no longer be able to receive memos."));
+ else
+ source.Reply(_("Memo limit for %s set to \0020\002."), !chan.empty() ? chan.c_str() : user.c_str());
+ }
+ else
+ {
+ if (chan.empty() && nc == source.nc)
+ source.Reply(_("Your memo limit has been disabled."));
+ else
+ source.Reply(_("Memo limit \002disabled\002 for %s."), !chan.empty() ? chan.c_str() : user.c_str());
+ }
+ return;
+ }
+public:
+ CommandMSSet(Module *creator) : Command(creator, "memoserv/set", 2, 5)
+ {
+ this->SetDesc(_("Set options related to memos"));
+ this->SetSyntax(_("\037option\037 \037parameters\037"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ const Anope::string &cmd = params[0];
+ MemoInfo *mi = &source.nc->memos;
+
+ if (Anope::ReadOnly)
+ source.Reply(_("Sorry, memo option setting is temporarily disabled."));
+ else if (cmd.equals_ci("NOTIFY"))
+ return this->DoNotify(source, params, mi);
+ else if (cmd.equals_ci("LIMIT"))
+ return this->DoLimit(source, params, mi);
+ else
+ {
+ this->OnSyntaxError(source, "");
+ }
+
+ return;
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ if (subcommand.empty())
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Sets various memo options. \037option\037 can be one of:\n"
+ " \n"
+ " NOTIFY Changes when you will be notified about\n"
+ " new memos (only for nicknames)\n"
+ " LIMIT Sets the maximum number of memos you can\n"
+ " receive\n"
+ " \n"
+ "Type \002%s%s HELP %s \037option\037\002 for more information\n"
+ "on a specific option."), Config->StrictPrivmsg.c_str(), source.service->nick.c_str(), source.command.c_str());
+ }
+ else if (subcommand.equals_ci("NOTIFY"))
+ source.Reply(_("Syntax: \002NOTIFY {ON | LOGON | NEW | MAIL | NOMAIL | OFF}\002\n"
+ " \n"
+ "Changes when you will be notified about new memos:\n"
+ " \n"
+ " ON You will be notified of memos when you log on,\n"
+ " when you unset /AWAY, and when they are sent\n"
+ " to you.\n"
+ " LOGON You will only be notified of memos when you log\n"
+ " on or when you unset /AWAY.\n"
+ " NEW You will only be notified of memos when they\n"
+ " are sent to you.\n"
+ " MAIL You will be notified of memos by email as well as\n"
+ " any other settings you have.\n"
+ " NOMAIL You will not be notified of memos by email.\n"
+ " OFF You will not receive any notification of memos.\n"
+ " \n"
+ "\002ON\002 is essentially \002LOGON\002 and \002NEW\002 combined."));
+ else if (subcommand.equals_ci("LIMIT"))
+ {
+ int max_memos = Config->GetModule("memoserv")->Get<int>("maxmemos");
+ if (source.IsServicesOper())
+ source.Reply(_("Syntax: \002LIMIT [\037user\037 | \037channel\037] {\037limit\037 | NONE} [HARD]\002\n"
+ " \n"
+ "Sets the maximum number of memos a user or channel is\n"
+ "allowed to have. Setting the limit to 0 prevents the user\n"
+ "from receiving any memos; setting it to \002NONE\002 allows the\n"
+ "user to receive and keep as many memos as they want. If\n"
+ "you do not give a nickname or channel, your own limit is\n"
+ "set.\n"
+ " \n"
+ "Adding \002HARD\002 prevents the user from changing the limit. Not\n"
+ "adding \002HARD\002 has the opposite effect, allowing the user to\n"
+ "change the limit (even if a previous limit was set with\n"
+ "\002HARD\002).\n"
+ " \n"
+ "This use of the \002SET LIMIT\002 command is limited to \002Services\002\n"
+ "\002Operators\002. Other users may only enter a limit for themselves\n"
+ "or a channel on which they have such privileges, may not\n"
+ "remove their limit, may not set a limit above %d, and may\n"
+ "not set a hard limit."), max_memos);
+ else
+ source.Reply(_("Syntax: \002LIMIT [\037channel\037] \037limit\037\002\n"
+ " \n"
+ "Sets the maximum number of memos you (or the given channel)\n"
+ "are allowed to have. If you set this to 0, no one will be\n"
+ "able to send any memos to you. However, you cannot set\n"
+ "this any higher than %d."), max_memos);
+ }
+ else
+ return false;
+
+ return true;
+ }
+};
+
+class MSSet final
+ : public Module
+{
+ CommandMSSet commandmsset;
+ SerializableExtensibleItem<bool> memo_signon, memo_receive, memo_mail, memo_hardmax;
+
+public:
+ MSSet(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmsset(this), memo_signon(this, "MEMO_SIGNON"), memo_receive(this, "MEMO_RECEIVE"), memo_mail(this, "MEMO_MAIL"),
+ memo_hardmax(this, "MEMO_HARDMAX")
+ {
+
+ }
+};
+
+MODULE_INIT(MSSet)
diff --git a/modules/memoserv/ms_staff.cpp b/modules/memoserv/ms_staff.cpp
new file mode 100644
index 000000000..cbd27061d
--- /dev/null
+++ b/modules/memoserv/ms_staff.cpp
@@ -0,0 +1,67 @@
+/* MemoServ core functions
+ *
+ * (C) 2003-2024 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.
+ */
+
+#include "module.h"
+
+namespace
+{
+ ServiceReference<MemoServService> memoserv("MemoServService", "MemoServ");
+}
+
+class CommandMSStaff final
+ : public Command
+{
+public:
+ CommandMSStaff(Module *creator) : Command(creator, "memoserv/staff", 1, 1)
+ {
+ this->SetDesc(_("Send a memo to all opers/admins"));
+ this->SetSyntax(_("\037memo-text\037"));
+ }
+
+ void Execute(CommandSource &source, const std::vector<Anope::string> &params) override
+ {
+ if (!memoserv)
+ return;
+
+ const Anope::string &text = params[0];
+
+ for (const auto &[_, nc] : *NickCoreList)
+ {
+ if (source.nc != nc && nc->IsServicesOper())
+ memoserv->Send(source.GetNick(), nc->display, text, true);
+ }
+ }
+
+ bool OnHelp(CommandSource &source, const Anope::string &subcommand) override
+ {
+ this->SendSyntax(source);
+ source.Reply(" ");
+ source.Reply(_("Sends all services staff a memo containing \037memo-text\037."));
+
+ return true;
+ }
+};
+
+class MSStaff final
+ : public Module
+{
+ CommandMSStaff commandmsstaff;
+
+public:
+ MSStaff(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR),
+ commandmsstaff(this)
+ {
+ if (!memoserv)
+ throw ModuleException("No MemoServ!");
+ }
+};
+
+MODULE_INIT(MSStaff)