pissircd/src/modules/message.c

707 lines
19 KiB
C

/*
* Unreal Internet Relay Chat Daemon, src/modules/message.c
* (C) 2000-2001 Carsten V. Munk and the UnrealIRCd Team
* Moved to modules by Fish (Justin Hammond)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 1, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "unrealircd.h"
/* Forward declarations */
const char *_StripColors(const char *text);
int ban_version(Client *client, const char *text);
CMD_FUNC(cmd_private);
CMD_FUNC(cmd_notice);
CMD_FUNC(cmd_tagmsg);
void cmd_message(Client *client, MessageTag *recv_mtags, int parc, const char *parv[], SendType sendtype);
int _can_send_to_channel(Client *client, Channel *channel, const char **msgtext, const char **errmsg, SendType sendtype);
int can_send_to_user(Client *client, Client *target, const char **msgtext, const char **errmsg, SendType sendtype);
/* Variables */
long CAP_MESSAGE_TAGS = 0; /**< Looked up at MOD_LOAD, may stay 0 if message-tags support is absent */
ModuleHeader MOD_HEADER
= {
"message", /* Name of module */
"6.0.2", /* Version */
"private message and notice", /* Short description of module */
"UnrealIRCd Team",
"unrealircd-6",
};
MOD_TEST()
{
MARK_AS_OFFICIAL_MODULE(modinfo);
EfunctionAddConstString(modinfo->handle, EFUNC_STRIPCOLORS, _StripColors);
EfunctionAdd(modinfo->handle, EFUNC_CAN_SEND_TO_CHANNEL, _can_send_to_channel);
return MOD_SUCCESS;
}
/* This is called on module init, before Server Ready */
MOD_INIT()
{
CommandAdd(modinfo->handle, "PRIVMSG", cmd_private, 2, CMD_USER|CMD_SERVER|CMD_RESETIDLE|CMD_VIRUS);
CommandAdd(modinfo->handle, "NOTICE", cmd_notice, 2, CMD_USER|CMD_SERVER);
CommandAdd(modinfo->handle, "TAGMSG", cmd_tagmsg, 1, CMD_USER|CMD_SERVER);
MARK_AS_OFFICIAL_MODULE(modinfo);
return MOD_SUCCESS;
}
/* Is first run when server is 100% ready */
MOD_LOAD()
{
CAP_MESSAGE_TAGS = ClientCapabilityBit("message-tags");
return MOD_SUCCESS;
}
/* Called when module is unloaded */
MOD_UNLOAD()
{
return MOD_SUCCESS;
}
#define CANPRIVMSG_CONTINUE 100
#define CANPRIVMSG_SEND 101
/** Check if PRIVMSG's are permitted from a person to another person.
* client: source client
* target: target client
* sendtype: One of SEND_TYPE_*
* text: Pointer to a pointer to a text [in, out]
* cmd: Pointer to a pointer which contains the command to use [in, out]
*/
int can_send_to_user(Client *client, Client *target, const char **msgtext, const char **errmsg, SendType sendtype)
{
int ret;
Hook *h;
int n;
static char errbuf[256];
*errmsg = NULL;
if (IsVirus(client))
{
ircsnprintf(errbuf, sizeof(errbuf), "You are only allowed to talk in '%s'", SPAMFILTER_VIRUSCHAN);
*errmsg = errbuf;
return 0;
}
if (MyUser(client) && target_limit_exceeded(client, target, target->name))
{
/* target_limit_exceeded() is an exception, in the sense that
* it will send a different numeric. So we don't set errmsg.
*/
return 0;
}
if (is_silenced(client, target))
{
RunHook(HOOKTYPE_SILENCED, client, target, sendtype);
/* Silently discarded, no error message */
return 0;
}
// Possible FIXME: make match_spamfilter also use errmsg, or via a wrapper? or use same numeric?
if (MyUser(client) && (sendtype != SEND_TYPE_TAGMSG))
{
int spamtype = (sendtype == SEND_TYPE_NOTICE ? SPAMF_USERNOTICE : SPAMF_USERMSG);
const char *cmd = sendtype_to_cmd(sendtype);
if (match_spamfilter(client, *msgtext, spamtype, cmd, target->name, 0, NULL))
return 0;
}
n = HOOK_CONTINUE;
for (h = Hooks[HOOKTYPE_CAN_SEND_TO_USER]; h; h = h->next)
{
n = (*(h->func.intfunc))(client, target, msgtext, errmsg, sendtype);
if (n == HOOK_DENY)
{
if (!*errmsg)
{
unreal_log(ULOG_ERROR, "main", "BUG_CAN_SEND_TO_USER_NO_ERRMSG", client,
"[BUG] Module $module did not set errmsg!!!",
log_data_string("module", h->owner->header->name));
abort();
}
return 0;
}
if (!*msgtext || !**msgtext)
{
if (sendtype != SEND_TYPE_TAGMSG)
return 0;
else
*msgtext = "";
}
}
return 1;
}
/** Check if user is allowed to send to a prefix (eg: @#channel).
* @param client The client (sender)
* @param channel The target channel
* @param mode The member mode to send to (eg: 'o')
*/
int can_send_to_member_mode(Client *client, Channel *channel, char mode)
{
Membership *lp;
if (op_can_override("channel:override:message:prefix",client,channel,NULL))
return 1;
lp = find_membership_link(client->user->channel, channel);
/* Check if user is allowed to send. RULES:
* Need at least voice (+) in order to send to +,% or @
* Need at least ops (@) in order to send to & or ~
*/
if (!lp || !check_channel_access_membership(lp, "vhoaq"))
{
sendnumeric(client, ERR_CHANOPRIVSNEEDED, channel->name);
return 0;
}
#if 0
if (!(prefix & PREFIX_OP) && ((prefix & PREFIX_OWNER) || (prefix & PREFIX_ADMIN)) &&
!check_channel_access_membership(lp, "oaq"))
{
sendnumeric(client, ERR_CHANOPRIVSNEEDED, channel->name);
return 0;
}
#endif
return 1;
}
int has_client_mtags(MessageTag *mtags)
{
MessageTag *m;
for (m = mtags; m; m = m->next)
if (*m->name == '+')
return 1;
return 0;
}
/* General message handler to users and channels. Used by PRIVMSG, NOTICE, etc.
*/
void cmd_message(Client *client, MessageTag *recv_mtags, int parc, const char *parv[], SendType sendtype)
{
Client *target;
Channel *channel;
char targets[BUFSIZE];
char *targetstr, *p, *p2, *pc;
const char *text, *errmsg;
int ret;
int ntargets = 0;
const char *cmd = sendtype_to_cmd(sendtype);
int maxtargets = max_targets_for_command(cmd);
Hook *h;
MessageTag *mtags;
int sendflags;
/* Force a labeled-response, even if we don't send anything
* and the request was sent to other servers (which won't
* reply either :D).
*/
labeled_response_force = 1;
if (parc < 2 || *parv[1] == '\0')
{
sendnumeric(client, ERR_NORECIPIENT, cmd);
return;
}
if ((sendtype != SEND_TYPE_TAGMSG) && (parc < 3 || *parv[2] == '\0'))
{
sendnumeric(client, ERR_NOTEXTTOSEND);
return;
}
if (MyConnect(client))
parv[1] = (char *)canonize(parv[1]);
strlcpy(targets, parv[1], sizeof(targets));
for (p = NULL, targetstr = strtoken(&p, targets, ","); targetstr; targetstr = strtoken(&p, NULL, ","))
{
if (MyUser(client) && (++ntargets > maxtargets))
{
sendnumeric(client, ERR_TOOMANYTARGETS, targetstr, maxtargets, cmd);
break;
}
/* The nicks "ircd" and "irc" are special (and reserved) */
if (!strcasecmp(targetstr, "ircd") && MyUser(client))
return;
if (!strcasecmp(targetstr, "irc") && MyUser(client))
{
/* When ban version { } is enabled the IRCd sends a CTCP VERSION request
* from the "IRC" nick. So we need to handle CTCP VERSION replies to "IRC".
*/
if (!strncmp(parv[2], "\1VERSION ", 9))
ban_version(client, parv[2] + 9);
else if (!strncmp(parv[2], "\1SCRIPT ", 8))
ban_version(client, parv[2] + 8);
return;
}
p2 = strchr(targetstr, '#');
/* Message to channel */
if (p2 && (channel = find_channel(p2)))
{
char pfixchan[CHANNELLEN + 4];
int replaced = 0;
char member_modes_tmp[2];
char *member_modes = NULL;
if (p2 - targetstr > 0)
{
/* There is (posssibly) a prefix involved... */
char prefix_tmp[32];
char prefix;
strlncpy(prefix_tmp, targetstr, sizeof(prefix_tmp), p2 - targetstr);
prefix = lowest_ranking_prefix(prefix_tmp);
if (prefix)
{
/* Rewrite the target. Eg: @&~#chan becomes @#chan */
snprintf(pfixchan, sizeof(pfixchan), "%c%s", prefix, channel->name);
targetstr = pfixchan;
replaced = 1;
/* And set 'member_modes' */
member_modes_tmp[0] = prefix_to_mode(prefix);
member_modes_tmp[1] = '\0';
member_modes = member_modes_tmp;
/* Oh, and some access check */
if (MyUser(client) && !can_send_to_member_mode(client, channel, *member_modes))
continue;
}
}
if (!replaced)
{
/* Replace target so the privmsg always goes to the "official" channel name */
strlcpy(pfixchan, channel->name, sizeof(pfixchan));
targetstr = pfixchan;
}
if (IsVirus(client) && strcasecmp(channel->name, SPAMFILTER_VIRUSCHAN))
{
sendnotice(client, "You are only allowed to talk in '%s'", SPAMFILTER_VIRUSCHAN);
continue;
}
text = parv[2];
errmsg = NULL;
if (MyUser(client) && !IsULine(client))
{
if (!can_send_to_channel(client, channel, &text, &errmsg, sendtype))
{
/* Send the error message, but only if:
* 1) The user has not been killed
* 2) It is not a NOTICE
*/
if (IsDead(client))
return;
if (!IsDead(client) && (sendtype != SEND_TYPE_NOTICE) && !BadPtr(errmsg))
sendnumeric(client, ERR_CANNOTSENDTOCHAN, channel->name, errmsg, p2);
continue; /* skip delivery to this target */
}
}
mtags = NULL;
sendflags = SEND_ALL;
if (!strchr(CHANCMDPFX,parv[2][0]))
sendflags |= SKIP_DEAF;
if ((*parv[2] == '\001') && strncmp(&parv[2][1], "ACTION ", 7))
sendflags |= SKIP_CTCP;
if (MyUser(client) && (sendtype != SEND_TYPE_TAGMSG))
{
int spamtype = (sendtype == SEND_TYPE_NOTICE ? SPAMF_CHANNOTICE : SPAMF_CHANMSG);
if (match_spamfilter(client, text, spamtype, cmd, channel->name, 0, NULL))
return;
}
new_message(client, recv_mtags, &mtags);
RunHook(HOOKTYPE_PRE_CHANMSG, client, channel, &mtags, text, sendtype);
if (!text)
{
free_message_tags(mtags);
continue;
}
if (sendtype != SEND_TYPE_TAGMSG)
{
/* PRIVMSG or NOTICE */
sendto_channel(channel, client, client->direction,
member_modes, 0, sendflags, mtags,
":%s %s %s :%s",
client->name, cmd, targetstr, text);
} else {
/* TAGMSG:
* Only send if the message includes any user message tags
* and if the 'message-tags' module is loaded.
* Do not allow empty and useless TAGMSG.
*/
if (!CAP_MESSAGE_TAGS || !has_client_mtags(mtags))
{
free_message_tags(mtags);
continue;
}
sendto_channel(channel, client, client->direction,
member_modes, CAP_MESSAGE_TAGS, sendflags, mtags,
":%s TAGMSG %s",
client->name, targetstr);
}
RunHook(HOOKTYPE_CHANMSG, client, channel, sendflags, member_modes, targetstr, mtags, text, sendtype);
free_message_tags(mtags);
continue;
}
else if (p2)
{
sendnumeric(client, ERR_NOSUCHNICK, p2);
continue;
}
/* Message to $servermask */
if (*targetstr == '$')
{
MessageTag *mtags = NULL;
if (!ValidatePermissionsForPath("chat:notice:global", client, NULL, NULL, NULL))
{
/* Apparently no other IRCd does this, but I think it's confusing not to
* send an error message, especially with our new privilege system.
* Error message could be more descriptive perhaps.
*/
sendnumeric(client, ERR_NOPRIVILEGES);
continue;
}
new_message(client, recv_mtags, &mtags);
sendto_match_butone(IsServer(client->direction) ? client->direction : NULL,
client, targetstr + 1,
(*targetstr == '#') ? MATCH_HOST :
MATCH_SERVER,
mtags,
":%s %s %s :%s", client->name, cmd, targetstr, parv[2]);
free_message_tags(mtags);
continue;
}
/* nickname addressed? */
target = hash_find_nickatserver(targetstr, NULL);
if (target)
{
const char *errmsg = NULL;
text = parv[2];
if (!can_send_to_user(client, target, &text, &errmsg, sendtype))
{
/* Message is discarded */
if (IsDead(client))
return;
if ((sendtype != SEND_TYPE_NOTICE) && !BadPtr(errmsg))
sendnumeric(client, ERR_CANTSENDTOUSER, target->name, errmsg);
} else
{
/* We may send the message */
MessageTag *mtags = NULL;
/* Inform sender that recipient is away, if this is so */
if ((sendtype == SEND_TYPE_PRIVMSG) && MyConnect(client) && target->user && target->user->away)
sendnumeric(client, RPL_AWAY, target->name, target->user->away);
new_message(client, recv_mtags, &mtags);
if ((sendtype == SEND_TYPE_TAGMSG) && !has_client_mtags(mtags))
{
free_message_tags(mtags);
continue;
}
labeled_response_inhibit = 1;
if (MyUser(target))
{
/* Deliver to end-user */
if (sendtype == SEND_TYPE_TAGMSG)
{
if (HasCapability(target, "message-tags"))
{
sendto_prefix_one(target, client, mtags, ":%s %s %s",
client->name, cmd, target->name);
}
} else {
sendto_prefix_one(target, client, mtags, ":%s %s %s :%s",
client->name, cmd, target->name, text);
}
} else {
/* Send to another server */
if (sendtype == SEND_TYPE_TAGMSG)
{
sendto_prefix_one(target, client, mtags, ":%s %s %s",
client->id, cmd, target->id);
} else {
sendto_prefix_one(target, client, mtags, ":%s %s %s :%s",
client->id, cmd, target->id, text);
}
}
labeled_response_inhibit = 0;
RunHook(HOOKTYPE_USERMSG, client, target, mtags, text, sendtype);
free_message_tags(mtags);
continue;
}
continue; /* Message has been delivered or rejected, continue with next target */
}
/* If nick@server -and- the @server portion was set::services-server then send a special message */
if (!target && SERVICES_NAME)
{
char *server = strchr(targetstr, '@');
if (server && strncasecmp(server + 1, SERVICES_NAME, strlen(SERVICES_NAME)) == 0)
{
sendnumeric(client, ERR_SERVICESDOWN, targetstr);
continue;
}
}
/* nothing, nada, not anything found */
sendnumeric(client, ERR_NOSUCHNICK, targetstr);
continue;
}
}
/*
** cmd_private
** parv[1] = receiver list
** parv[2] = message text
*/
CMD_FUNC(cmd_private)
{
cmd_message(client, recv_mtags, parc, parv, SEND_TYPE_PRIVMSG);
}
/*
** cmd_notice
** parv[1] = receiver list
** parv[2] = notice text
*/
CMD_FUNC(cmd_notice)
{
cmd_message(client, recv_mtags, parc, parv, SEND_TYPE_NOTICE);
}
/*
** cmd_tagmsg
** parv[1] = receiver list
*/
CMD_FUNC(cmd_tagmsg)
{
/* compatibility hack */
parv[2] = "";
parv[3] = NULL;
cmd_message(client, recv_mtags, parc, parv, SEND_TYPE_TAGMSG);
}
/* Taken from xchat by Peter Zelezny
* changed very slightly by codemastr
* RGB color stripping support added -- codemastr
*
* NOTE: if you change/update/enhance StripColors() then consider changing
* the StripControlCodes() function as well (in misc.c) !!
*/
const char *_StripColors(const char *text)
{
int i = 0, len = strlen(text), save_len=0;
char nc = 0, col = 0, rgb = 0;
const char *save_text=NULL;
static char new_str[4096];
while (len > 0)
{
if ((col && isdigit(*text) && nc < 2) ||
((col == 1) && (*text == ',') && isdigit(text[1]) && (nc > 0) && (nc < 3)))
{
nc++;
if (*text == ',')
{
nc = 0;
col++;
}
}
/* Syntax for RGB is ^DHHHHHH where H is a hex digit.
* If < 6 hex digits are specified, the code is displayed
* as text
*/
else if ((rgb && isxdigit(*text) && nc < 6) || (rgb && *text == ',' && nc < 7))
{
nc++;
if (*text == ',')
nc = 0;
}
else
{
if (col)
col = 0;
if (rgb)
{
if (nc != 6)
{
text = save_text+1;
len = save_len-1;
rgb = 0;
continue;
}
rgb = 0;
}
if (*text == '\003')
{
col = 1;
nc = 0;
}
else if (*text == '\004')
{
save_text = text;
save_len = len;
rgb = 1;
nc = 0;
}
else if (*text != '\026') /* (strip reverse too) */
{
new_str[i] = *text;
i++;
}
}
text++;
len--;
}
new_str[i] = 0;
if (new_str[0] == '\0')
return NULL;
return new_str;
}
/** Check ban version { } blocks, returns 1 if banned and 0 if not. */
int ban_version(Client *client, const char *text)
{
int len;
ConfigItem_ban *ban;
char ctcp_reply[BUFSIZE];
strlcpy(ctcp_reply, text, sizeof(ctcp_reply));
len = strlen(ctcp_reply);
if (!len)
return 0;
if (ctcp_reply[len-1] == '\1')
ctcp_reply[len-1] = '\0'; /* remove CTCP REPLY terminator (ASCII 1) */
if ((ban = find_ban(NULL, ctcp_reply, CONF_BAN_VERSION)))
{
if (find_tkl_exception(TKL_BAN_VERSION, client))
return 0; /* we are exempt */
return take_action(client, ban->action, ban->reason, BAN_VERSION_TKL_TIME, 0, NULL);
}
return 0;
}
/** Can user send a message to this channel?
* @param client The client
* @param channel The channel
* @param msgtext The message to send (MAY be changed, even if user is allowed to send)
* @param errmsg The error message (will be filled in)
* @param sendtype One of SEND_TYPE_*
* @returns Returns 1 if the user is allowed to send, otherwise 0.
* (note that this behavior was reversed in UnrealIRCd versions <5.x.
*/
int _can_send_to_channel(Client *client, Channel *channel, const char **msgtext, const char **errmsg, SendType sendtype)
{
Membership *lp;
int member, i = 0;
Hook *h;
if (!MyUser(client))
return 1;
*errmsg = NULL;
member = IsMember(client, channel);
lp = find_membership_link(client->user->channel, channel);
/* Modules can plug in as well */
for (h = Hooks[HOOKTYPE_CAN_SEND_TO_CHANNEL]; h; h = h->next)
{
i = (*(h->func.intfunc))(client, channel, lp, msgtext, errmsg, sendtype);
if (i != HOOK_CONTINUE)
{
if (!*errmsg)
{
unreal_log(ULOG_ERROR, "main", "BUG_CAN_SEND_TO_CHANNEL_NO_ERRMSG", client,
"[BUG] Module $module did not set errmsg!!!",
log_data_string("module", h->owner->header->name));
abort();
}
break;
}
if (!*msgtext || !**msgtext)
{
if (sendtype != SEND_TYPE_TAGMSG)
return 0;
else
*msgtext = "";
}
}
if (i != HOOK_CONTINUE)
{
if (!*errmsg)
*errmsg = "You are banned";
/* Don't send message if the user was previously a member
* and isn't anymore, so if the user is KICK'ed, eg by floodprot.
*/
if (member && !IsDead(client) && !find_membership_link(client->user->channel, channel))
*errmsg = NULL;
return 0;
}
/* Now we are going to check bans */
/* ..but first: exempt ircops */
if (op_can_override("channel:override:message:ban",client,channel,NULL))
return 1;
/* If local client is banned and not +vhoaq... */
if (MyUser(client) &&
!check_channel_access_membership(lp, "vhoaq") &&
is_banned(client, channel, BANCHK_MSG, msgtext, errmsg))
{
/* Modules can set 'errmsg', otherwise we default to this: */
if (!*errmsg)
*errmsg = "You are banned";
return 0;
}
return 1;
}