/* MemoServ functions. * * IRC Services is copyright (c) 1996-2007 Andrew Church. * E-mail: * Parts written by Andrew Kempe and others. * This program is free but copyrighted software; see the file COPYING for * details. */ #include "services.h" #include "modules.h" #include "conffile.h" #include "language.h" #include "commands.h" #include "modules/nickserv/nickserv.h" #include "modules/chanserv/chanserv.h" #include "modules/operserv/operserv.h" #include "memoserv.h" /*************************************************************************/ static Module *module; static Module *module_nickserv; static Module *module_chanserv; /* Import */ static int (*p_check_access)(User *u, ChannelInfo *ci, int what); static int cb_command = -1; static int cb_receive_memo = -1; static int cb_help = -1; static int cb_help_cmds = -1; static int cb_set = -1; char * s_MemoServ; static char * desc_MemoServ; int32 MSMaxMemos; static time_t MSExpire; static int MSExpireUnread; static time_t MSSendDelay; static int MSNotifyAll; EXPORT_VAR(int32,MSMaxMemos) /*************************************************************************/ /* Error codes for get_memoinfo(). */ #define GMI_NOTFOUND -1 #define GMI_FORBIDDEN -2 #define GMI_SUSPENDED -3 #define GMI_INTERR -99 /* Macro to return the real memo maximum for a `mi->memomax' value (i.e. * convert MEMOMAX_DEFAULT to MSMaxMemos). */ #define REALMAX(n) ((n)==MEMOMAX_DEFAULT ? MSMaxMemos : (n)) /*************************************************************************/ static void check_memos(User *u); static MemoInfo *get_memoinfo(const char *name, void **owner_ret, int *ischan_ret, int *error_ret); static MemoInfo *get_memoinfo_from_cmd(User *u, char **param_ret, char **chan_ret, ChannelInfo **ci_ret); static void expire_memos(MemoInfo *mi); static int list_memo(User *u, int index, MemoInfo *mi, int *sent_header, int new, const char *chan); static int list_memo_callback(User *u, int num, va_list args); static int read_memo(User *u, int index, MemoInfo *mi, const char *chan); static int read_memo_callback(User *u, int num, va_list args); static int del_memo(MemoInfo *mi, int num); static int del_memo_callback(User *u, int num, va_list args); static void do_help(User *u); static void do_send(User *u); static void do_list(User *u); static void do_read(User *u); static void do_save(User *u); static void do_del(User *u); static void do_set(User *u); static void do_set_notify(User *u, MemoInfo *mi, char *param); static void do_set_limit(User *u, MemoInfo *mi, char *param); static void do_info(User *u); /*************************************************************************/ static Command cmds[] = { { "HELP", do_help, NULL, -1, -1,-1 }, { "SEND", do_send, NULL, MEMO_HELP_SEND, -1,-1 }, { "LIST", do_list, NULL, MEMO_HELP_LIST, -1,-1 }, { "READ", do_read, NULL, MEMO_HELP_READ, -1,-1 }, { "SAVE", do_save, NULL, MEMO_HELP_SAVE, -1,-1 }, { "DEL", do_del, NULL, MEMO_HELP_DEL, -1,-1 }, { "SET", do_set, NULL, MEMO_HELP_SET, -1,-1 }, { "SET NOTIFY", NULL, NULL, MEMO_HELP_SET_NOTIFY, -1,-1, "NickServ" }, { "SET LIMIT", NULL, NULL, -1, MEMO_HELP_SET_LIMIT, MEMO_OPER_HELP_SET_LIMIT }, { "INFO", do_info, NULL, -1, MEMO_HELP_INFO, MEMO_OPER_HELP_INFO }, { NULL } }; /*************************************************************************/ /*************************************************************************/ /* Introduce the MemoServ pseudoclient. */ static int introduce_memoserv(const char *nick) { if (!nick || irc_stricmp(nick, s_MemoServ) == 0) { char modebuf[BUFSIZE]; snprintf(modebuf, sizeof(modebuf), "o%s", pseudoclient_modes); send_nick(s_MemoServ, ServiceUser, ServiceHost, ServerName, desc_MemoServ, modebuf); return nick ? 1 : 0; } return 0; } /*************************************************************************/ /* memoserv: Main MemoServ routine. * Note that the User structure passed to the do_* routines will * always be valid (non-NULL) and will always have a valid * NickInfo pointer in the `ni' field. */ static int memoserv(const char *source, const char *target, char *buf) { char *cmd; User *u = get_user(source); if (irc_stricmp(target, s_MemoServ) != 0) return 0; if (!u) { module_log("user record for %s not found", source); notice(s_MemoServ, source, getstring(NULL,INTERNAL_ERROR)); return 1; } cmd = strtok(buf, " "); if (!cmd) { return 1; } else if (stricmp(cmd, "\1PING") == 0) { const char *s; if (!(s = strtok(NULL, ""))) s = "\1"; notice(s_MemoServ, source, "\1PING %s", s); } else { if (!valid_ngi(u) && stricmp(cmd, "HELP") != 0) notice_lang(s_MemoServ, u, NICK_NOT_REGISTERED_HELP, s_NickServ); else if (call_callback_2(module, cb_command, u, cmd) <= 0) run_cmd(s_MemoServ, u, module, cmd); } return 1; } /*************************************************************************/ /* Return a /WHOIS response for MemoServ. */ static int memoserv_whois(const char *source, char *who, char *extra) { if (irc_stricmp(who, s_MemoServ) != 0) return 0; send_cmd(ServerName, "311 %s %s %s %s * :%s", source, who, ServiceUser, ServiceHost, desc_MemoServ); send_cmd(ServerName, "312 %s %s %s :%s", source, who, ServerName, ServerDesc); send_cmd(ServerName, "318 %s %s End of /WHOIS response.", source, who); return 1; } /*************************************************************************/ /* Callback for users connecting to the network. */ static int do_user_create(User *user, int ac, char **av) { if (user_recognized(user)) check_memos(user); return 0; } /*************************************************************************/ /* Callback for users changing nicknames. */ static int do_user_nickchange(User *user, const char *oldnick) { NickInfo *old_ni; uint32 old_nickgroup, new_nickgroup; /* user->{ni,ngi} are already changed, so look it up again */ old_ni = get_nickinfo(oldnick); old_nickgroup = old_ni ? old_ni->nickgroup : 0; new_nickgroup = user->ngi ? user->ngi->id : 0; if (old_nickgroup != new_nickgroup) check_memos(user); return 0; } /*************************************************************************/ /* Callback to check for un-away. */ static int do_receive_message(const char *source, const char *cmd, int ac, char **av) { if (stricmp(cmd, "AWAY") == 0 && (ac == 0 || *av[0] == 0)) { User *u = get_user(source); if (u) check_memos(u); } return 0; } /*************************************************************************/ /* Callback for NickServ REGISTER/LINK check; we disallow * registration/linking of the MemoServ pseudoclient nickname. */ static int do_reglink_check(const User *u, const char *nick, const char *pass, const char *email) { return irc_stricmp(nick, s_MemoServ) == 0; } /*************************************************************************/ /* Callback for users identifying for nicks. */ static int do_nick_identified(User *user, int old_authstat) { if (!(old_authstat & (NA_IDENTIFIED | NA_RECOGNIZED))) check_memos(user); return 0; } /*************************************************************************/ /* check_memos: See if the given user has any unread memos, and send a * NOTICE to that user if so (and if the appropriate flag is * set). */ static void check_memos(User *u) { NickGroupInfo *ngi = u->ngi; int i, newcnt = 0, max; if (!ngi || !user_recognized(u) || !(ngi->flags & NF_MEMO_SIGNON)) return; expire_memos(&ngi->memos); ARRAY_FOREACH (i, ngi->memos.memos) { if (ngi->memos.memos[i].flags & MF_UNREAD) newcnt++; } if (newcnt > 0) { notice_lang(s_MemoServ, u, newcnt==1 ? MEMO_HAVE_NEW_MEMO : MEMO_HAVE_NEW_MEMOS, newcnt); if (newcnt == 1 && (ngi->memos.memos[i-1].flags & MF_UNREAD)) { notice_lang(s_MemoServ, u, MEMO_TYPE_READ_LAST, s_MemoServ); } else if (newcnt == 1) { ARRAY_FOREACH (i, ngi->memos.memos) { if (ngi->memos.memos[i].flags & MF_UNREAD) break; } notice_lang(s_MemoServ, u, MEMO_TYPE_READ_NUM, s_MemoServ, ngi->memos.memos[i].number); } else { notice_lang(s_MemoServ, u, MEMO_TYPE_LIST_NEW, s_MemoServ); } } max = REALMAX(ngi->memos.memomax); if (max > 0 && ngi->memos.memos_count >= max) { if (ngi->memos.memos_count > max) notice_lang(s_MemoServ, u, MEMO_OVER_LIMIT, max); else notice_lang(s_MemoServ, u, MEMO_AT_LIMIT, max); } } /*************************************************************************/ /*********************** MemoServ private routines ***********************/ /*************************************************************************/ /* Return the MemoInfo corresponding to the given nick or channel name. * Return in `owner' the NickGroupInfo or ChannelInfo owning the MemoInfo. * Return in `ischan' 1 if the name was a channel name, else 0. * Return in `error' a GMI_* error code if the return value is NULL. * Also set `error' to GMI_SUSPENDED if the nick/channel is suspended, * even though a valid MemoInfo is returned. */ static MemoInfo *get_memoinfo(const char *name, void **owner_ret, int *ischan_ret, int *error_ret) { MemoInfo *mi = NULL; void *dummy_owner; static int dummy_ischan, dummy_error; void **owner = owner_ret ? owner_ret : &dummy_owner; int *ischan = ischan_ret ? ischan_ret : &dummy_ischan; int *error = error_ret ? error_ret : &dummy_error; *error = 0; if (*name == '#') { ChannelInfo *ci; *ischan = 1; ci = get_channelinfo(name); if (ci) { if (ci->flags & CI_VERBOTEN) { *error = GMI_FORBIDDEN; return NULL; } else { if (ci->suspendinfo) *error = GMI_SUSPENDED; *owner = ci; mi = &ci->memos; } } else { *error = GMI_NOTFOUND; return NULL; } } else { NickInfo *ni; NickGroupInfo *ngi; *ischan = 0; ni = get_nickinfo(name); if (ni) { if (ni->status & NS_VERBOTEN) { *error = GMI_FORBIDDEN; return NULL; } ngi = get_ngi(ni); if (!ngi) { *error = GMI_INTERR; return NULL; } if (ngi->suspendinfo) *error = GMI_SUSPENDED; *owner = ngi; mi = &ngi->memos; } else { *error = GMI_NOTFOUND; return NULL; } } if (!mi) { module_log("BUG: get_memoinfo(): mi==NULL after checks"); *error = GMI_INTERR; return NULL; } expire_memos(mi); return mi; } /*************************************************************************/ /* Retrieve the MemoInfo applicable to a LIST/READ/etc. command based on * parameters: * *param_ret set to the first parameter (excluding any channel name) * *chan_ret set to the channel name (NULL if none) * *ci_ret set to the ChannelInfo for the channel (NULL if none) * All parameters must be non-NULL. */ static MemoInfo *get_memoinfo_from_cmd(User *u, char **param_ret, char **chan_ret, ChannelInfo **ci_ret) { char *param = strtok(NULL, " "); char *chan = NULL; ChannelInfo *ci = NULL; MemoInfo *mi; if (module_chanserv && param && *param == '#') { chan = param; param = strtok(NULL, " "); if (!(ci = get_channelinfo(chan))) { notice_lang(s_MemoServ, u, CHAN_X_NOT_REGISTERED, chan); return NULL; } else if (ci->flags & CI_VERBOTEN) { notice_lang(s_MemoServ, u, CHAN_X_FORBIDDEN, chan); return NULL; } else if (!p_check_access(u, ci, CA_MEMO)) { notice_lang(s_MemoServ, u, ACCESS_DENIED); return NULL; } mi = &ci->memos; } else { if (!user_identified(u)) { notice_lang(s_MemoServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ); return NULL; } mi = &u->ngi->memos; } expire_memos(mi); *param_ret = param; *chan_ret = chan; *ci_ret = ci; return mi; } /*************************************************************************/ /* Expire memos for the given MemoInfo. */ static void expire_memos(MemoInfo *mi) { int i; time_t limit = time(NULL) - MSExpire; if (!MSExpire) return; ARRAY_FOREACH (i, mi->memos) { if ((mi->memos[i].flags & MF_EXPIREOK) && (MSExpireUnread || !(mi->memos[i].flags & MF_UNREAD)) && mi->memos[i].time <= limit ) { free(mi->memos[i].text); ARRAY_REMOVE(mi->memos, i); i--; } } } /*************************************************************************/ /* Display a single memo entry, possibly printing the header first. */ static int list_memo(User *u, int index, MemoInfo *mi, int *sent_header, int new, const char *chan) { Memo *m; char timebuf[64]; if (index < 0 || index >= mi->memos_count) return 0; if (!*sent_header) { if (chan) { notice_lang(s_MemoServ, u, new ? MEMO_LIST_CHAN_NEW_MEMOS : MEMO_LIST_CHAN_MEMOS, chan, s_MemoServ, chan); } else { notice_lang(s_MemoServ, u, new ? MEMO_LIST_NEW_MEMOS : MEMO_LIST_MEMOS, u->nick, s_MemoServ); } notice_lang(s_MemoServ, u, MEMO_LIST_HEADER); *sent_header = 1; } m = &mi->memos[index]; strftime_lang(timebuf, sizeof(timebuf), u->ngi, STRFTIME_DATE_TIME_FORMAT, m->time); timebuf[sizeof(timebuf)-1] = 0; /* just in case */ notice_lang(s_MemoServ, u, MEMO_LIST_FORMAT, (m->flags & MF_UNREAD) ? '*' : ' ', (!MSExpire || (m->flags & MF_EXPIREOK)) ? ' ' : '+', m->number, m->sender, timebuf); return 1; } /* List callback. */ static int list_memo_callback(User *u, int num, va_list args) { MemoInfo *mi = va_arg(args, MemoInfo *); int *sent_header = va_arg(args, int *); const char *chan = va_arg(args, const char *); int i; ARRAY_FOREACH (i, mi->memos) { if (mi->memos[i].number == num) break; } /* Range checking done by list_memo() */ return list_memo(u, i, mi, sent_header, 0, chan); } /*************************************************************************/ /* Send a single memo to the given user. */ static int read_memo(User *u, int index, MemoInfo *mi, const char *chan) { Memo *m; char timebuf[BUFSIZE]; if (index < 0 || index >= mi->memos_count) return 0; m = &mi->memos[index]; strftime_lang(timebuf, sizeof(timebuf), u->ngi, STRFTIME_DATE_TIME_FORMAT, m->time); timebuf[sizeof(timebuf)-1] = 0; if (chan) notice_lang(s_MemoServ, u, MEMO_CHAN_HEADER, m->number, m->sender, timebuf, s_MemoServ, chan, m->number); else notice_lang(s_MemoServ, u, MEMO_HEADER, m->number, m->sender, timebuf, s_MemoServ, m->number); notice(s_MemoServ, u->nick, "%s", m->text); m->flags &= ~MF_UNREAD; return 1; } /* Read callback. */ static int read_memo_callback(User *u, int num, va_list args) { MemoInfo *mi = va_arg(args, MemoInfo *); const char *chan = va_arg(args, const char *); int i; ARRAY_FOREACH (i, mi->memos) { if (mi->memos[i].number == num) break; } /* Range check done in read_memo */ return read_memo(u, i, mi, chan); } /*************************************************************************/ /* Mark a given memo as non-expiring. */ static int save_memo(User *u, int index, MemoInfo *mi) { if (index < 0 || index >= mi->memos_count) return 0; mi->memos[index].flags &= ~MF_EXPIREOK; return 1; } /* Save callback. */ static int save_memo_callback(User *u, int num, va_list args) { MemoInfo *mi = va_arg(args, MemoInfo *); int *last = va_arg(args, int *); int i; ARRAY_FOREACH (i, mi->memos) { if (mi->memos[i].number == num) break; } /* Range check done in save_memo */ if (save_memo(u, i, mi)) { *last = num; return 1; } else { return 0; } } /*************************************************************************/ /* Delete a memo by number. Return 1 if the memo was found, else 0. */ static int del_memo(MemoInfo *mi, int num) { int i; ARRAY_FOREACH (i, mi->memos) { if (mi->memos[i].number == num) break; } if (i < mi->memos_count) { free(mi->memos[i].text); ARRAY_REMOVE(mi->memos, i); return 1; } else { return 0; } } /* Delete a single memo from a MemoInfo. */ static int del_memo_callback(User *u, int num, va_list args) { MemoInfo *mi = va_arg(args, MemoInfo *); int *last = va_arg(args, int *); if (del_memo(mi, num)) { *last = num; return 1; } else { return 0; } } /*************************************************************************/ /*********************** MemoServ command routines ***********************/ /*************************************************************************/ /* Return a help message. */ static void do_help(User *u) { char *cmd = strtok_remaining(); if (!cmd) { const char *def_s_ChanServ = "ChanServ"; const char **p_s_ChanServ = NULL; const char *levstr; if (module_chanserv) p_s_ChanServ = get_module_symbol(module_chanserv, "s_ChanServ"); if (!p_s_ChanServ) p_s_ChanServ = &def_s_ChanServ; if (find_module("chanserv/access-xop")) { if (find_module("chanserv/access-levels")) levstr = getstring(u->ngi, CHAN_HELP_REQSOP_LEVXOP); else levstr = getstring(u->ngi, CHAN_HELP_REQSOP_XOP); } else { levstr = getstring(u->ngi, CHAN_HELP_REQSOP_LEV); } notice_help(s_MemoServ, u, MEMO_HELP); if (MSExpire) { notice_help(s_MemoServ, u, MEMO_HELP_EXPIRES, maketime(u->ngi,MSExpire,MT_DUALUNIT)); } if (find_module("chanserv/access-levels")) { notice_help(s_MemoServ, u, MEMO_HELP_END_LEVELS, levstr, *p_s_ChanServ); } else { notice_help(s_MemoServ, u, MEMO_HELP_END_XOP); } } else if (call_callback_2(module, cb_help, u, cmd) > 0) { return; } else if (stricmp(cmd, "COMMANDS") == 0) { notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS); if (find_module("memoserv/forward")) notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS_FORWARD); if (MSExpire) notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS_SAVE); notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS_DEL); if (find_module("memoserv/ignore")) notice_help(s_MemoServ, u, MEMO_HELP_COMMANDS_IGNORE); call_callback_2(module, cb_help_cmds, u, 0); if (is_oper(u)) { notice_help(s_MemoServ, u, MEMO_OPER_HELP_COMMANDS); call_callback_2(module, cb_help_cmds, u, 1); } } else if (stricmp(cmd, "SET") == 0) { notice_help(s_MemoServ, u, MEMO_HELP_SET); if (find_module("memoserv/forward")) notice_help(s_MemoServ, u, MEMO_HELP_SET_OPTION_FORWARD); notice_help(s_MemoServ, u, MEMO_HELP_SET_END); } else { help_cmd(s_MemoServ, u, module, cmd); } } /*************************************************************************/ /* Send a memo to a nick/channel. */ static void do_send(User *u) { char *source = u->nick; int ischan, error; void *owner; MemoInfo *mi; Memo *m; char *name = strtok(NULL, " "); char *text = strtok_remaining(); time_t now = time(NULL); int is_servadmin = is_services_admin(u); if (readonly) { notice_lang(s_MemoServ, u, MEMO_SEND_DISABLED); } else if (!text) { syntax_error(s_MemoServ, u, "SEND", MEMO_SEND_SYNTAX); } else if (!user_identified(u)) { notice_lang(s_MemoServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ); } else if (!(mi = get_memoinfo(name, &owner, &ischan, &error))) { if (error == GMI_FORBIDDEN) notice_lang(s_MemoServ, u, ischan ? CHAN_X_FORBIDDEN: NICK_X_FORBIDDEN, name); else notice_lang(s_MemoServ, u, ischan ? CHAN_X_NOT_REGISTERED : NICK_X_NOT_REGISTERED, name); } else if (error == GMI_SUSPENDED) { notice_lang(s_MemoServ, u, ischan ? CHAN_X_SUSPENDED_MEMOS : NICK_X_SUSPENDED_MEMOS, name); } else if (MSSendDelay > 0 && u && u->lastmemosend+MSSendDelay > now && !is_servadmin) { u->lastmemosend = now; notice_lang(s_MemoServ, u, MEMO_SEND_PLEASE_WAIT, maketime(u->ngi,MSSendDelay,MT_SECONDS)); } else if (mi->memomax == 0 && !is_servadmin) { notice_lang(s_MemoServ, u, MEMO_X_GETS_NO_MEMOS, name); } else if (mi->memomax != MEMOMAX_UNLIMITED && mi->memos_count >= REALMAX(mi->memomax) && !is_servadmin) { notice_lang(s_MemoServ, u, MEMO_X_HAS_TOO_MANY_MEMOS, name); } else { u->lastmemosend = now; if (call_callback_5(module, cb_receive_memo, ischan, owner, name, u, text) <= 0) { ARRAY_EXTEND(mi->memos); m = &mi->memos[mi->memos_count-1]; strscpy(m->sender, source, NICKMAX); if (mi->memos_count > 1) { m->number = m[-1].number + 1; if (m->number < 1) { int i; ARRAY_FOREACH (i, mi->memos) mi->memos[i].number = i+1; } } else { m->number = 1; } m->time = time(NULL); m->text = sstrdup(text); m->flags = MF_UNREAD; if (MSExpire) m->flags |= MF_EXPIREOK; if (!ischan) { NickInfo *ni = get_nickinfo(name); NickGroupInfo *ngi = ni ? get_ngi(ni) : NULL; if (ngi && (ngi->flags & NF_MEMO_RECEIVE)) { User *u2; if (MSNotifyAll) { int i; ARRAY_FOREACH (i, ngi->nicks) { if (irc_stricmp(ngi->nicks[i], name) == 0) { u2 = ni->user; } else { NickInfo *ni2 = get_nickinfo(ngi->nicks[i]); u2 = ni2 ? ni2->user : NULL; } if (u2 && user_recognized(u2)) { notice_lang(s_MemoServ, u2, MEMO_NEW_MEMO_ARRIVED, source, s_MemoServ, m->number); } } } else { u2 = ni->user; if (u2 && user_recognized(u2)) { notice_lang(s_MemoServ, u2, MEMO_NEW_MEMO_ARRIVED, source, s_MemoServ, m->number); } } /* if (MSNotifyAll) */ } /* if (flags & MEMO_RECEIVE) */ } /* if (!ischan) */ if (ischan) put_channelinfo(owner); else put_nickgroupinfo(owner); notice_lang(s_MemoServ, u, MEMO_SENT, name); } /* call_callback returned <=0 */ } /* if command is valid */ } /*************************************************************************/ /* List memos for the source nick or given channel. */ static void do_list(User *u) { char *param, *chan; ChannelInfo *ci; MemoInfo *mi; int i; mi = get_memoinfo_from_cmd(u, ¶m, &chan, &ci); if (!mi) return; if (param && !isdigit(*param) && stricmp(param, "NEW") != 0) { syntax_error(s_MemoServ, u, "LIST", MEMO_LIST_SYNTAX); } else if (mi->memos_count == 0) { if (chan) notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_MEMOS, chan); else notice_lang(s_MemoServ, u, MEMO_HAVE_NO_MEMOS); } else { int sent_header = 0; if (param && isdigit(*param)) { process_numlist(param, NULL, list_memo_callback, u, mi, &sent_header, chan); } else { if (param) { ARRAY_FOREACH (i, mi->memos) { if (mi->memos[i].flags & MF_UNREAD) break; } if (i == mi->memos_count) { if (chan) notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_NEW_MEMOS, chan); else notice_lang(s_MemoServ, u, MEMO_HAVE_NO_NEW_MEMOS); return; } } ARRAY_FOREACH (i, mi->memos) { if (param && !(mi->memos[i].flags & MF_UNREAD)) continue; list_memo(u, i, mi, &sent_header, param != NULL, chan); } } } } /*************************************************************************/ /* Read memos. */ static void do_read(User *u) { MemoInfo *mi; ChannelInfo *ci = NULL; char *numstr, *chan; int num, count; mi = get_memoinfo_from_cmd(u, &numstr, &chan, &ci); if (!mi) return; num = numstr ? atoi(numstr) : -1; if (!numstr || (stricmp(numstr,"LAST") != 0 && stricmp(numstr,"NEW") != 0 && num <= 0)) { syntax_error(s_MemoServ, u, "READ", MEMO_READ_SYNTAX); } else if (mi->memos_count == 0) { if (chan) notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_MEMOS, chan); else notice_lang(s_MemoServ, u, MEMO_HAVE_NO_MEMOS); } else { int i; if (stricmp(numstr, "NEW") == 0) { int readcount = 0; ARRAY_FOREACH (i, mi->memos) { if (mi->memos[i].flags & MF_UNREAD) { read_memo(u, i, mi, chan); readcount++; } } if (!readcount) { if (chan) notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_NEW_MEMOS, chan); else notice_lang(s_MemoServ, u, MEMO_HAVE_NO_NEW_MEMOS); } } else if (stricmp(numstr, "LAST") == 0) { read_memo(u, mi->memos_count-1, mi, chan); } else { /* number[s] */ if (!process_numlist(numstr, &count, read_memo_callback, u, mi, chan)) { if (count == 1) notice_lang(s_MemoServ, u, MEMO_DOES_NOT_EXIST, num); else notice_lang(s_MemoServ, u, MEMO_LIST_NOT_FOUND, numstr); } } if (chan) put_channelinfo(ci); else put_nickgroupinfo(u->ngi); } } /*************************************************************************/ /* Save memos (mark them as non-expiring). */ static void do_save(User *u) { MemoInfo *mi; ChannelInfo *ci = NULL; char *numstr, *chan; int num, count; mi = get_memoinfo_from_cmd(u, &numstr, &chan, &ci); if (!mi) return; num = numstr ? atoi(numstr) : -1; if (!numstr || num <= 0) { syntax_error(s_MemoServ, u, "SAVE", MEMO_SAVE_SYNTAX); } else if (mi->memos_count == 0) { if (chan) notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_MEMOS, chan); else notice_lang(s_MemoServ, u, MEMO_HAVE_NO_MEMOS); } else { int last = 0; int savecount = process_numlist(numstr, &count, save_memo_callback, u, mi, &last); if (savecount) { /* Some memos got saved. */ if (savecount > 1) notice_lang(s_MemoServ, u, MEMO_SAVED_SEVERAL, savecount); else notice_lang(s_MemoServ, u, MEMO_SAVED_ONE, last); } else { /* No matching memos found. */ if (count == 1) notice_lang(s_MemoServ, u, MEMO_DOES_NOT_EXIST, num); else notice_lang(s_MemoServ, u, MEMO_LIST_NOT_FOUND, numstr); } if (chan) put_channelinfo(ci); else put_nickgroupinfo(u->ngi); } } /*************************************************************************/ /* Delete memos. */ static void do_del(User *u) { MemoInfo *mi; ChannelInfo *ci = NULL; char *numstr, *chan; int last, i; int delcount, count; mi = get_memoinfo_from_cmd(u, &numstr, &chan, &ci); if (!mi) return; if (!numstr || (!isdigit(*numstr) && stricmp(numstr, "ALL") != 0)) { syntax_error(s_MemoServ, u, "DEL", MEMO_DEL_SYNTAX); } else if (mi->memos_count == 0) { if (chan) notice_lang(s_MemoServ, u, MEMO_X_HAS_NO_MEMOS, chan); else notice_lang(s_MemoServ, u, MEMO_HAVE_NO_MEMOS); } else { if (isdigit(*numstr)) { /* Delete a specific memo or memos. */ delcount = process_numlist(numstr, &count, del_memo_callback, u, mi, &last); if (delcount) { /* Some memos got deleted. */ if (delcount > 1) notice_lang(s_MemoServ, u, MEMO_DELETED_SEVERAL, delcount); else notice_lang(s_MemoServ, u, MEMO_DELETED_ONE, last); } else { /* No memos were deleted. */ if (count == 1) notice_lang(s_MemoServ, u, MEMO_DOES_NOT_EXIST, atoi(numstr)); else notice_lang(s_MemoServ, u, MEMO_DELETED_NONE); } } else { /* Delete all memos. */ ARRAY_FOREACH (i, mi->memos) free(mi->memos[i].text); free(mi->memos); mi->memos = NULL; mi->memos_count = 0; notice_lang(s_MemoServ, u, MEMO_DELETED_ALL); } if (chan) put_channelinfo(ci); else put_nickgroupinfo(u->ngi); } } /*************************************************************************/ static void do_set(User *u) { char *cmd = strtok(NULL, " "); char *param = strtok_remaining(); MemoInfo *mi = &u->ngi->memos; if (readonly) { notice_lang(s_MemoServ, u, MEMO_SET_DISABLED); return; } if (!param) { syntax_error(s_MemoServ, u, "SET", MEMO_SET_SYNTAX); } else if (!user_identified(u)) { notice_lang(s_MemoServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ); return; } else if (call_callback_4(module, cb_set, u, mi, cmd, param) > 0) { return; } else if (stricmp(cmd, "NOTIFY") == 0) { do_set_notify(u, mi, param); } else if (stricmp(cmd, "LIMIT") == 0) { do_set_limit(u, mi, param); } else { notice_lang(s_MemoServ, u, MEMO_SET_UNKNOWN_OPTION, strupper(cmd)); notice_lang(s_MemoServ, u, MORE_INFO, s_MemoServ, "SET"); } } /*************************************************************************/ static void do_set_notify(User *u, MemoInfo *mi, char *param) { if (stricmp(param, "ON") == 0) { u->ngi->flags |= NF_MEMO_SIGNON | NF_MEMO_RECEIVE; notice_lang(s_MemoServ, u, MEMO_SET_NOTIFY_ON, s_MemoServ); } else if (stricmp(param, "LOGON") == 0) { u->ngi->flags |= NF_MEMO_SIGNON; u->ngi->flags &= ~NF_MEMO_RECEIVE; notice_lang(s_MemoServ, u, MEMO_SET_NOTIFY_LOGON, s_MemoServ); } else if (stricmp(param, "NEW") == 0) { u->ngi->flags &= ~NF_MEMO_SIGNON; u->ngi->flags |= NF_MEMO_RECEIVE; notice_lang(s_MemoServ, u, MEMO_SET_NOTIFY_NEW, s_MemoServ); } else if (stricmp(param, "OFF") == 0) { u->ngi->flags &= ~(NF_MEMO_SIGNON | NF_MEMO_RECEIVE); notice_lang(s_MemoServ, u, MEMO_SET_NOTIFY_OFF, s_MemoServ); } else { syntax_error(s_MemoServ, u, "SET NOTIFY", MEMO_SET_NOTIFY_SYNTAX); return; } put_nickgroupinfo(u->ngi); } /*************************************************************************/ /* Regular user parameters: [#channel] number * Services admin parameters: [#channel|nick] {number|NONE|DEFAULT} [HARD] */ static void do_set_limit(User *u, MemoInfo *mi, char *param) { char *p1 = strtok(param, " "); char *p2 = strtok(NULL, " "); char *user = NULL, *chan = NULL; int32 limit; NickInfo *ni = u->ni; NickGroupInfo *ngi = u->ngi; ChannelInfo *ci = NULL; int is_servadmin = is_services_admin(u); if (module_chanserv && p1 && *p1 == '#') { chan = p1; p1 = p2; p2 = strtok(NULL, " "); if (!(ci = get_channelinfo(chan))) { notice_lang(s_MemoServ, u, CHAN_X_NOT_REGISTERED, chan); return; } else if (ci->flags & CI_VERBOTEN) { notice_lang(s_MemoServ, u, CHAN_X_FORBIDDEN, chan); return; } else if (!is_servadmin && !p_check_access(u, ci, CA_MEMO)) { notice_lang(s_MemoServ, u, ACCESS_DENIED); return; } mi = &ci->memos; } if (is_servadmin) { if (p2 && stricmp(p2, "HARD") != 0 && !chan) { if (!(ni = get_nickinfo(p1))) { notice_lang(s_MemoServ, u, NICK_X_NOT_REGISTERED, p1); return; } if (!(ngi = get_ngi(ni))) { notice_lang(s_MemoServ, u, INTERNAL_ERROR); return; } user = p1; mi = &ngi->memos; p1 = p2; p2 = strtok(NULL, " "); } else if (!p1) { syntax_error(s_MemoServ, u, "SET LIMIT", MEMO_SET_LIMIT_OPER_SYNTAX); return; } if ((!isdigit(*p1) && stricmp(p1, "NONE") != 0 && stricmp(p1, "DEFAULT") != 0) || (p2 && stricmp(p2, "HARD") != 0) ) { syntax_error(s_MemoServ, u, "SET LIMIT", MEMO_SET_LIMIT_OPER_SYNTAX); return; } if (chan) { if (p2) ci->flags |= CI_MEMO_HARDMAX; else ci->flags &= ~CI_MEMO_HARDMAX; } else { if (p2) ngi->flags |= NF_MEMO_HARDMAX; else ngi->flags &= ~NF_MEMO_HARDMAX; } limit = atoi(p1); if (limit < 0 || limit > MEMOMAX_MAX) { notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_OVERFLOW, MEMOMAX_MAX); limit = MEMOMAX_MAX; } if (stricmp(p1, "NONE") == 0) limit = MEMOMAX_UNLIMITED; else if (stricmp(p1, "DEFAULT") == 0) limit = MEMOMAX_DEFAULT; } else { if (!p1 || p2 || !isdigit(*p1)) { syntax_error(s_MemoServ, u, "SET LIMIT", MEMO_SET_LIMIT_SYNTAX); return; } if (chan && (ci->flags & CI_MEMO_HARDMAX)) { notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_FORBIDDEN, chan); return; } else if (!chan && (ngi->flags & NF_MEMO_HARDMAX)) { notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT_FORBIDDEN); return; } limit = atoi(p1); /* The first character is a digit, but we could still go negative * from overflow... watch out! (Actually, atoi() may not allow * that, but it doesn't hurt to check anyway.) */ if (limit < 0 || (MSMaxMemos > 0 && limit > MSMaxMemos)) { if (chan) { notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_TOO_HIGH, chan, MSMaxMemos); } else { notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT_TOO_HIGH, MSMaxMemos); } return; } else if (limit > MEMOMAX_MAX) { notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_OVERFLOW, MEMOMAX_MAX); limit = MEMOMAX_MAX; } } mi->memomax = limit; if (chan) put_channelinfo(ci); else put_nickgroupinfo(ngi); if (limit > 0) { if (!chan && ni == u->ni) notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT, limit); else notice_lang(s_MemoServ, u, MEMO_SET_LIMIT, chan ? chan : user, limit); } else if (limit == 0) { if (!chan && ni == u->ni) notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT_ZERO); else notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_ZERO, chan ? chan : user); } else if (limit == MEMOMAX_DEFAULT) { if (!chan && ni == u->ni) notice_lang(s_MemoServ, u, MEMO_SET_YOUR_LIMIT_DEFAULT, MSMaxMemos); else notice_lang(s_MemoServ, u, MEMO_SET_LIMIT_DEFAULT, chan ? chan : user, MSMaxMemos); } else { if (!chan && ni == u->ni) notice_lang(s_MemoServ, u, MEMO_UNSET_YOUR_LIMIT); else notice_lang(s_MemoServ, u, MEMO_UNSET_LIMIT, chan ? chan : user); } } /*************************************************************************/ static void do_info(User *u) { MemoInfo *mi; NickInfo *ni = NULL; NickGroupInfo *ngi = NULL; ChannelInfo *ci = NULL; char *name = strtok(NULL, " "); int is_servadmin = is_services_admin(u); int max = 0; int is_hardmax = 0; if (is_servadmin && name && *name != '#') { ni = get_nickinfo(name); if (!ni) { notice_lang(s_MemoServ, u, NICK_X_NOT_REGISTERED, name); return; } else if (ni->status & NS_VERBOTEN) { notice_lang(s_MemoServ, u, NICK_X_FORBIDDEN, name); return; } ngi = get_ngi(ni); if (!ngi) { notice_lang(s_MemoServ, u, INTERNAL_ERROR); return; } mi = &ngi->memos; is_hardmax = ngi->flags & NF_MEMO_HARDMAX ? 1 : 0; } else if (module_chanserv && name && *name == '#') { ci = get_channelinfo(name); if (!ci) { notice_lang(s_MemoServ, u, CHAN_X_NOT_REGISTERED, name); return; } else if (ci->flags & CI_VERBOTEN) { notice_lang(s_MemoServ, u, CHAN_X_FORBIDDEN, name); return; } else if (!p_check_access(u, ci, CA_MEMO)) { notice_lang(s_MemoServ, u, ACCESS_DENIED); return; } mi = &ci->memos; is_hardmax = ci->flags & CI_MEMO_HARDMAX ? 1 : 0; } else { /* !name */ if (!user_identified(u)) { notice_lang(s_MemoServ, u, NICK_IDENTIFY_REQUIRED, s_NickServ); return; } ni = u->ni; ngi = u->ngi; mi = &u->ngi->memos; } max = REALMAX(mi->memomax); if (name && ni != u->ni) { /* Report info for a channel or a nick other than the caller. */ if (!mi->memos_count) { notice_lang(s_MemoServ, u, MEMO_INFO_X_NO_MEMOS, name); } else if (mi->memos_count == 1) { if (mi->memos[0].flags & MF_UNREAD) notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMO_UNREAD, name); else notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMO, name); } else { int count = 0, i; ARRAY_FOREACH (i, mi->memos) { if (mi->memos[i].flags & MF_UNREAD) count++; } if (count == mi->memos_count) notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMOS_ALL_UNREAD, name, count); else if (count == 0) notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMOS, name, mi->memos_count); else if (count == 0) notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMOS_ONE_UNREAD, name, mi->memos_count); else notice_lang(s_MemoServ, u, MEMO_INFO_X_MEMOS_SOME_UNREAD, name, mi->memos_count, count); } if (max >= 0) { if (is_hardmax) notice_lang(s_MemoServ, u, MEMO_INFO_X_HARD_LIMIT, name, max); else notice_lang(s_MemoServ, u, MEMO_INFO_X_LIMIT, name, max); } else { notice_lang(s_MemoServ, u, MEMO_INFO_X_NO_LIMIT, name); } if (ngi) { /* only nicks are notified of new memos */ if ((ngi->flags&NF_MEMO_RECEIVE) && (ngi->flags&NF_MEMO_SIGNON)) { notice_lang(s_MemoServ, u, MEMO_INFO_X_NOTIFY_ON, name); } else if (ngi->flags & NF_MEMO_RECEIVE) { notice_lang(s_MemoServ, u, MEMO_INFO_X_NOTIFY_RECEIVE, name); } else if (ngi->flags & NF_MEMO_SIGNON) { notice_lang(s_MemoServ, u, MEMO_INFO_X_NOTIFY_SIGNON, name); } else { notice_lang(s_MemoServ, u, MEMO_INFO_X_NOTIFY_OFF, name); } } } else { /* !name || ni == u->ni */ if (!mi->memos_count) { notice_lang(s_MemoServ, u, MEMO_INFO_NO_MEMOS); } else if (mi->memos_count == 1) { if (mi->memos[0].flags & MF_UNREAD) notice_lang(s_MemoServ, u, MEMO_INFO_MEMO_UNREAD); else notice_lang(s_MemoServ, u, MEMO_INFO_MEMO); } else { int count = 0, i; ARRAY_FOREACH (i, mi->memos) { if (mi->memos[i].flags & MF_UNREAD) count++; } if (count == mi->memos_count) notice_lang(s_MemoServ, u, MEMO_INFO_MEMOS_ALL_UNREAD, count); else if (count == 0) notice_lang(s_MemoServ, u, MEMO_INFO_MEMOS, mi->memos_count); else if (count == 1) notice_lang(s_MemoServ, u, MEMO_INFO_MEMOS_ONE_UNREAD, mi->memos_count); else notice_lang(s_MemoServ, u, MEMO_INFO_MEMOS_SOME_UNREAD, mi->memos_count, count); } if (max == 0) { if (!is_servadmin && is_hardmax) notice_lang(s_MemoServ, u, MEMO_INFO_HARD_LIMIT_ZERO); else notice_lang(s_MemoServ, u, MEMO_INFO_LIMIT_ZERO); } else if (max > 0) { if (!is_servadmin && is_hardmax) notice_lang(s_MemoServ, u, MEMO_INFO_HARD_LIMIT, max); else notice_lang(s_MemoServ, u, MEMO_INFO_LIMIT, max); } else { notice_lang(s_MemoServ, u, MEMO_INFO_NO_LIMIT); } if ((ngi->flags & NF_MEMO_RECEIVE) && (ngi->flags & NF_MEMO_SIGNON)) { notice_lang(s_MemoServ, u, MEMO_INFO_NOTIFY_ON); } else if (ngi->flags & NF_MEMO_RECEIVE) { notice_lang(s_MemoServ, u, MEMO_INFO_NOTIFY_RECEIVE); } else if (ngi->flags & NF_MEMO_SIGNON) { notice_lang(s_MemoServ, u, MEMO_INFO_NOTIFY_SIGNON); } else { notice_lang(s_MemoServ, u, MEMO_INFO_NOTIFY_OFF); } } /* if (name && ni != u->ni) */ } /*************************************************************************/ /***************************** Module stuff ******************************/ /*************************************************************************/ const int32 module_version = MODULE_VERSION_CODE; ConfigDirective module_config[] = { { "MemoServName", { { CD_STRING, CF_DIRREQ, &s_MemoServ }, { CD_STRING, 0, &desc_MemoServ } } }, { "MSExpire", { { CD_TIME, 0, &MSExpire } } }, { "MSExpireUnread", { { CD_SET, 0, &MSExpireUnread } } }, { "MSMaxMemos", { { CD_POSINT, 0, &MSMaxMemos } } }, { "MSNotifyAll", { { CD_SET, 0, &MSNotifyAll } } }, { "MSSendDelay", { { CD_TIME, 0, &MSSendDelay } } }, { NULL } }; static Command *cmd_SAVE = NULL; /* For restoring if !MSExpire */ static int old_HELP_LIST = -1; /* For restoring if MSExpire */ /*************************************************************************/ static int do_load_module(Module *mod, const char *modname) { if (strcmp(modname, "nickserv/main") == 0) { module_nickserv = mod; use_module(mod); if (!add_callback(module_nickserv, "REGISTER/LINK check", do_reglink_check)) module_log("Unable to register NickServ REGISTER/LINK callback"); if (!add_callback(mod, "identified", do_nick_identified)) module_log("Unable to register NickServ IDENTIFY callback"); } else if (strcmp(modname, "chanserv/main") == 0) { p_check_access = get_module_symbol(mod, "check_access"); if (p_check_access) { module_chanserv = mod; use_module(mod); } else { module_log("Unable to resolve symbol `check_access' in module" " `chanserv/main'; channel memos will not be" " available"); } } return 0; } /*************************************************************************/ static int do_unload_module(Module *mod) { if (mod == module_nickserv) { remove_callback(module_nickserv, "identified", do_nick_identified); remove_callback(module_nickserv, "REGISTER/LINK check", do_reglink_check); unuse_module(module_nickserv); module_nickserv = NULL; } else if (mod == module_chanserv) { p_check_access = NULL; unuse_module(module_chanserv); module_chanserv = NULL; } return 0; } /*************************************************************************/ static int do_reconfigure(int after_configure) { static char old_s_MemoServ[NICKMAX]; static char *old_desc_MemoServ = NULL; if (!after_configure) { /* Before reconfiguration: save old values. */ strscpy(old_s_MemoServ, s_MemoServ, NICKMAX); old_desc_MemoServ = strdup(desc_MemoServ); if (old_HELP_LIST >= 0) { setstring(MEMO_HELP_LIST, old_HELP_LIST); old_HELP_LIST = -1; } } else { /* After reconfiguration: handle value changes. */ if (strcmp(old_s_MemoServ, s_MemoServ) != 0) send_nickchange(old_s_MemoServ, s_MemoServ); if (!old_desc_MemoServ || strcmp(old_desc_MemoServ,desc_MemoServ) != 0) send_namechange(s_MemoServ, desc_MemoServ); free(old_desc_MemoServ); if (MSExpire) old_HELP_LIST = setstring(MEMO_HELP_LIST, MEMO_HELP_LIST_EXPIRE); } /* if (!after_configure) */ return 0; } /*************************************************************************/ int init_module(Module *module_) { Command *cmd; Module *tmpmod; module = module_; if (!new_commandlist(module) || !register_commands(module, cmds)) { module_log("Unable to register commands"); exit_module(0); return 0; } if (MSExpire) { old_HELP_LIST = setstring(MEMO_HELP_LIST, MEMO_HELP_LIST_EXPIRE); } else { /* Disable SAVE command if no expiration */ cmd_SAVE = lookup_cmd(module, "SAVE"); if (cmd_SAVE) cmd_SAVE->name = ""; } cb_command = register_callback(module, "command"); cb_receive_memo = register_callback(module, "receive memo"); cb_help = register_callback(module, "HELP"); cb_help_cmds = register_callback(module, "HELP COMMANDS"); cb_set = register_callback(module, "SET"); if (cb_command < 0 || cb_receive_memo < 0 || cb_help < 0 || cb_help_cmds < 0 || cb_set < 0) { module_log("Unable to register callbacks"); exit_module(0); return 0; } if (!add_callback(NULL, "load module", do_load_module) || !add_callback(NULL, "unload module", do_unload_module) || !add_callback(NULL, "reconfigure", do_reconfigure) || !add_callback(NULL, "introduce_user", introduce_memoserv) || !add_callback(NULL, "m_privmsg", memoserv) || !add_callback(NULL, "m_whois", memoserv_whois) || !add_callback(NULL, "receive message", do_receive_message) || !add_callback(NULL, "user create", do_user_create) || !add_callback(NULL, "user nickchange (after)", do_user_nickchange) ) { module_log("Unable to add callbacks"); exit_module(0); return 0; } tmpmod = find_module("nickserv/main"); if (tmpmod) do_load_module(tmpmod, "nickserv/main"); tmpmod = find_module("chanserv/main"); if (tmpmod) do_load_module(tmpmod, "chanserv/main"); cmd = lookup_cmd(module, "SET NOTIFY"); if (cmd) cmd->help_param1 = s_NickServ; cmd = lookup_cmd(module, "SET LIMIT"); if (cmd) { cmd->help_param1 = (char *)(long)MSMaxMemos; cmd->help_param2 = (char *)(long)MSMaxMemos; } if (linked) introduce_memoserv(NULL); return 1; } /*************************************************************************/ int exit_module(int shutdown_unused) { #ifdef CLEAN_COMPILE shutdown_unused = shutdown_unused; #endif if (linked) send_cmd(s_MemoServ, "QUIT :"); if (module_chanserv) do_unload_module(module_chanserv); if (module_nickserv) do_unload_module(module_nickserv); remove_callback(NULL, "user nickchange (after)", do_user_nickchange); remove_callback(NULL, "user create", do_user_create); remove_callback(NULL, "receive message", do_receive_message); remove_callback(NULL, "m_whois", memoserv_whois); remove_callback(NULL, "m_privmsg", memoserv); remove_callback(NULL, "introduce_user", introduce_memoserv); remove_callback(NULL, "reconfigure", do_reconfigure); remove_callback(NULL, "unload module", do_unload_module); remove_callback(NULL, "load module", do_load_module); unregister_callback(module, cb_set); unregister_callback(module, cb_help_cmds); unregister_callback(module, cb_help); unregister_callback(module, cb_receive_memo); unregister_callback(module, cb_command); if (cmd_SAVE) { cmd_SAVE->name = "SAVE"; cmd_SAVE = NULL; } if (old_HELP_LIST >= 0) { setstring(MEMO_HELP_LIST, old_HELP_LIST); old_HELP_LIST = -1; } unregister_commands(module, cmds); del_commandlist(module); return 1; } /*************************************************************************/