unrealircd

- supernets unrealircd source & configuration
git clone git://git.acid.vegas/unrealircd.git
Log | Files | Refs | Archive | README | LICENSE

join.c (19215B)

      1 /*
      2  *   IRC - Internet Relay Chat, src/modules/join.c
      3  *   (C) 2005 The UnrealIRCd Team
      4  *
      5  *   See file AUTHORS in IRC package for additional names of
      6  *   the programmers.
      7  *
      8  *   This program is free software; you can redistribute it and/or modify
      9  *   it under the terms of the GNU General Public License as published by
     10  *   the Free Software Foundation; either version 1, or (at your option)
     11  *   any later version.
     12  *
     13  *   This program is distributed in the hope that it will be useful,
     14  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
     15  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16  *   GNU General Public License for more details.
     17  *
     18  *   You should have received a copy of the GNU General Public License
     19  *   along with this program; if not, write to the Free Software
     20  *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     21  */
     22 
     23 #include "unrealircd.h"
     24 
     25 /* Forward declarations */
     26 CMD_FUNC(cmd_join);
     27 void _join_channel(Channel *channel, Client *client, MessageTag *mtags, const char *member_modes);
     28 void _do_join(Client *client, int parc, const char *parv[]);
     29 int _can_join(Client *client, Channel *channel, const char *key, char **errmsg);
     30 void _send_join_to_local_users(Client *client, Channel *channel, MessageTag *mtags);
     31 char *_get_chmodes_for_user(Client *client, const char *flags);
     32 void send_cannot_join_error(Client *client, int numeric, char *fmtstr, char *channel_name);
     33 
     34 /* Externs */
     35 extern MODVAR int spamf_ugly_vchanoverride;
     36 extern int find_invex(Channel *channel, Client *client);
     37 
     38 /* Local vars */
     39 static int bouncedtimes = 0;
     40 long CAP_EXTENDED_JOIN = 0L;
     41 
     42 /* Macros */
     43 #define MAXBOUNCE   5 /** Most sensible */
     44 #define MSG_JOIN 	"JOIN"	
     45 
     46 ModuleHeader MOD_HEADER
     47   = {
     48 	"join",
     49 	"5.0",
     50 	"command /join", 
     51 	"UnrealIRCd Team",
     52 	"unrealircd-6",
     53     };
     54 
     55 MOD_TEST()
     56 {
     57 	MARK_AS_OFFICIAL_MODULE(modinfo);
     58 	EfunctionAddVoid(modinfo->handle, EFUNC_JOIN_CHANNEL, _join_channel);
     59 	EfunctionAddVoid(modinfo->handle, EFUNC_DO_JOIN, _do_join);
     60 	EfunctionAdd(modinfo->handle, EFUNC_CAN_JOIN, _can_join);
     61 	EfunctionAddVoid(modinfo->handle, EFUNC_SEND_JOIN_TO_LOCAL_USERS, _send_join_to_local_users);
     62 	EfunctionAddPVoid(modinfo->handle, EFUNC_GET_CHMODES_FOR_USER, TO_PVOIDFUNC(_get_chmodes_for_user));
     63 
     64 	return MOD_SUCCESS;
     65 }
     66 
     67 MOD_INIT()
     68 {
     69 	ClientCapabilityInfo c;
     70 	memset(&c, 0, sizeof(c));
     71 	c.name = "extended-join";
     72 	ClientCapabilityAdd(modinfo->handle, &c, &CAP_EXTENDED_JOIN);
     73 
     74 	CommandAdd(modinfo->handle, MSG_JOIN, cmd_join, MAXPARA, CMD_USER);
     75 	MARK_AS_OFFICIAL_MODULE(modinfo);
     76 	return MOD_SUCCESS;
     77 }
     78 
     79 MOD_LOAD()
     80 {
     81 	return MOD_SUCCESS;
     82 }
     83 
     84 MOD_UNLOAD()
     85 {
     86 	return MOD_SUCCESS;
     87 }
     88 
     89 /* This function checks if a locally connected user may join the channel.
     90  * It also provides an number of hooks where modules can plug in to.
     91  * Note that the order of checking has been carefully thought of
     92  * (eg: bans at the end), so don't change it unless you have a good reason
     93  * to do so -- Syzop.
     94  */
     95 int _can_join(Client *client, Channel *channel, const char *key, char **errmsg)
     96 {
     97 	Hook *h;
     98 
     99 	/* An /INVITE lets you bypass all restrictions */
    100 	if (is_invited(client, channel))
    101 	{
    102 		int j = 0;
    103 		for (h = Hooks[HOOKTYPE_INVITE_BYPASS]; h; h = h->next)
    104 		{
    105 			j = (*(h->func.intfunc))(client,channel);
    106 			if (j != 0)
    107 				break;
    108 		}
    109 		/* Bypass is OK, unless a HOOKTYPE_INVITE_BYPASS hook returns HOOK_DENY */
    110 		if (j != HOOK_DENY)
    111 			return 0;
    112 	}
    113 
    114 	for (h = Hooks[HOOKTYPE_CAN_JOIN]; h; h = h->next)
    115 	{
    116 		int i = (*(h->func.intfunc))(client,channel,key, errmsg);
    117 		if (i != 0)
    118 			return i;
    119 	}
    120 
    121 	/* See if we can evade this ban */
    122 	if (is_banned(client, channel, BANCHK_JOIN, NULL, NULL))
    123 	{
    124 		*errmsg = STR_ERR_BANNEDFROMCHAN;
    125 		return ERR_BANNEDFROMCHAN;
    126 	}
    127 
    128 #ifndef NO_OPEROVERRIDE
    129 #ifdef OPEROVERRIDE_VERIFY
    130 	if (ValidatePermissionsForPath("channel:override:privsecret",client,NULL,channel,NULL) && (channel->mode.mode & MODE_SECRET ||
    131 	    channel->mode.mode & MODE_PRIVATE) && !is_autojoin_chan(channel->name))
    132 	{
    133 		*errmsg = STR_ERR_OPERSPVERIFY;
    134 		return (ERR_OPERSPVERIFY);
    135 	}
    136 #endif
    137 #endif
    138 
    139 	return 0;
    140 }
    141 
    142 /*
    143 ** cmd_join
    144 **	parv[1] = channel
    145 **	parv[2] = channel password (key)
    146 **
    147 ** Due to message tags, remote servers should only send 1 channel
    148 ** per JOIN. Or even better, use SJOIN instead.
    149 ** Otherwise we cannot use unique msgid's and such.
    150 ** UnrealIRCd 4 and probably UnrealIRCd 3.2.something already do
    151 ** this, so this comment is mostly for services coders, I guess.
    152 */
    153 CMD_FUNC(cmd_join)
    154 {
    155 	int r;
    156 
    157 	if (bouncedtimes)
    158 	{
    159 		unreal_log(ULOG_ERROR, "join", "BUG_JOIN_BOUNCEDTIMES", NULL,
    160 		           "[BUG] join: bouncedtimes is not initialized to zero ($bounced_times)!! "
    161 		           "Please report at https://bugs.unrealircd.org/",
    162 		           log_data_integer("bounced_times", bouncedtimes));
    163 	}
    164 
    165 	bouncedtimes = 0;
    166 	if (IsServer(client))
    167 		return;
    168 	do_join(client, parc, parv);
    169 	bouncedtimes = 0;
    170 }
    171 
    172 /** Send JOIN message for 'client' to all users in 'channel'.
    173  * Taking into account that not everyone in channel should see the JOIN (mode +D)
    174  * and taking into account the different types of JOIN (due to CAP extended-join).
    175  */
    176 void _send_join_to_local_users(Client *client, Channel *channel, MessageTag *mtags)
    177 {
    178 	int chanops_only = invisible_user_in_channel(client, channel);
    179 	Member *lp;
    180 	Client *acptr;
    181 	char joinbuf[512];
    182 	char exjoinbuf[512];
    183 
    184 	ircsnprintf(joinbuf, sizeof(joinbuf), ":%s!%s@%s JOIN :%s",
    185 		client->name, client->user->username, GetHost(client), channel->name);
    186 
    187 	ircsnprintf(exjoinbuf, sizeof(exjoinbuf), ":%s!%s@%s JOIN %s %s :%s",
    188 		client->name, client->user->username, GetHost(client), channel->name,
    189 		IsLoggedIn(client) ? client->user->account : "*",
    190 		client->info);
    191 
    192 	for (lp = channel->members; lp; lp = lp->next)
    193 	{
    194 		acptr = lp->client;
    195 
    196 		if (!MyConnect(acptr))
    197 			continue; /* only locally connected clients */
    198 
    199 		if (chanops_only && !check_channel_access_member(lp, "hoaq") && (client != acptr))
    200 			continue; /* skip non-ops if requested to (used for mode +D), but always send to 'client' */
    201 
    202 		if (HasCapabilityFast(acptr, CAP_EXTENDED_JOIN))
    203 			sendto_one(acptr, mtags, "%s", exjoinbuf);
    204 		else
    205 			sendto_one(acptr, mtags, "%s", joinbuf);
    206 	}
    207 }
    208 
    209 /* Routine that actually makes a user join the channel
    210  * this does no actual checking (banned, etc.) it just adds the user.
    211  * Note: this is called for local JOIN and remote JOIN, but not for SJOIN.
    212  */
    213 void _join_channel(Channel *channel, Client *client, MessageTag *recv_mtags, const char *member_modes)
    214 {
    215 	MessageTag *mtags = NULL; /** Message tags to send to local users (sender is :user) */
    216 	MessageTag *mtags_sjoin = NULL; /* Message tags to send to remote servers for SJOIN (sender is :me.id) */
    217 	const char *parv[3];
    218 
    219 	/* Same way as in SJOIN */
    220 	new_message_special(client, recv_mtags, &mtags, ":%s JOIN %s", client->name, channel->name);
    221 
    222 	new_message(&me, recv_mtags, &mtags_sjoin);
    223 
    224 	add_user_to_channel(channel, client, member_modes);
    225 
    226 	send_join_to_local_users(client, channel, mtags);
    227 
    228 	sendto_server(client, 0, 0, mtags_sjoin, ":%s SJOIN %lld %s :%s%s ",
    229 		me.id, (long long)channel->creationtime,
    230 		channel->name, modes_to_sjoin_prefix(member_modes), client->id);
    231 
    232 	if (MyUser(client))
    233 	{
    234 		/*
    235 		   ** Make a (temporal) creationtime, if someone joins
    236 		   ** during a net.reconnect : between remote join and
    237 		   ** the mode with TS. --Run
    238 		 */
    239 		if (channel->creationtime == 0)
    240 		{
    241 			channel->creationtime = TStime();
    242 			sendto_server(client, 0, 0, NULL, ":%s MODE %s + %lld",
    243 			    me.id, channel->name, (long long)channel->creationtime);
    244 		}
    245 
    246 		if (channel->topic)
    247 		{
    248 			sendnumeric(client, RPL_TOPIC, channel->name, channel->topic);
    249 			sendnumeric(client, RPL_TOPICWHOTIME, channel->name, channel->topic_nick, (long long)channel->topic_time);
    250 		}
    251 		
    252 		/* Set default channel modes (set::modes-on-join).
    253 		 * Set only if it's the 1st user and only if no other modes have been set
    254 		 * already (eg: +P, permanent).
    255 		 */
    256 		if ((channel->users == 1) && !channel->mode.mode && MODES_ON_JOIN)
    257 		{
    258 			MessageTag *mtags_mode = NULL;
    259 			Cmode *cm;
    260 			char modebuf[BUFSIZE], parabuf[BUFSIZE];
    261 			int should_destroy = 0;
    262 
    263 			channel->mode.mode = MODES_ON_JOIN;
    264 
    265 			/* Param fun */
    266 			for (cm=channelmodes; cm; cm = cm->next)
    267 			{
    268 				if (!cm->letter || !cm->paracount)
    269 					continue;
    270 				if (channel->mode.mode & cm->mode)
    271 				        cm_putparameter(channel, cm->letter, iConf.modes_on_join.extparams[cm->letter]);
    272 			}
    273 
    274 			*modebuf = *parabuf = 0;
    275 			channel_modes(client, modebuf, parabuf, sizeof(modebuf), sizeof(parabuf), channel, 0);
    276 			/* This should probably be in the SJOIN stuff */
    277 			new_message_special(&me, recv_mtags, &mtags_mode, ":%s MODE %s %s %s", me.name, channel->name, modebuf, parabuf);
    278 			sendto_server(NULL, 0, 0, mtags_mode, ":%s MODE %s %s %s %lld",
    279 			    me.id, channel->name, modebuf, parabuf, (long long)channel->creationtime);
    280 			sendto_one(client, mtags_mode, ":%s MODE %s %s %s", me.name, channel->name, modebuf, parabuf);
    281 			RunHook(HOOKTYPE_LOCAL_CHANMODE, &me, channel, mtags_mode, modebuf, parabuf, 0, 0, &should_destroy);
    282 			free_message_tags(mtags_mode);
    283 		}
    284 
    285 		parv[0] = NULL;
    286 		parv[1] = channel->name;
    287 		parv[2] = NULL;
    288 		do_cmd(client, NULL, "NAMES", 2, parv);
    289 
    290 		unreal_log(ULOG_INFO, "join", "LOCAL_CLIENT_JOIN", client,
    291 			   "User $client joined $channel",
    292 			   log_data_channel("channel", channel),
    293 			   log_data_string("modes", member_modes));
    294 
    295 		RunHook(HOOKTYPE_LOCAL_JOIN, client, channel, mtags);
    296 	} else {
    297 		unreal_log(ULOG_INFO, "join", "REMOTE_CLIENT_JOIN", client,
    298 			   "User $client joined $channel",
    299 			   log_data_channel("channel", channel),
    300 			   log_data_string("modes", member_modes));
    301 		RunHook(HOOKTYPE_REMOTE_JOIN, client, channel, mtags);
    302 	}
    303 
    304 	free_message_tags(mtags);
    305 	free_message_tags(mtags_sjoin);
    306 }
    307 
    308 /** User request to join a channel.
    309  * This routine is normally called from cmd_join but can also be called from
    310  * do_join->can_join->link module->do_join if the channel is 'linked' (chmode +L).
    311  * We therefore use a counter 'bouncedtimes' which is set to 0 in cmd_join,
    312  * increased every time we enter this loop and decreased anytime we leave the
    313  * loop. So be carefull not to use a simple 'return' after bouncedtimes++. -- Syzop
    314  */
    315 void _do_join(Client *client, int parc, const char *parv[])
    316 {
    317 	char request[BUFSIZE];
    318 	char request_key[BUFSIZE];
    319 	char jbuf[BUFSIZE], jbuf2[BUFSIZE];
    320 	const char *orig_parv1;
    321 	Membership *lp;
    322 	Channel *channel;
    323 	char *name, *key = NULL;
    324 	int i, ishold;
    325 	char *p = NULL, *p2 = NULL;
    326 	TKL *tklban;
    327 	int ntargets = 0;
    328 	int maxtargets = max_targets_for_command("JOIN");
    329 	const char *member_modes = "";
    330 
    331 #define RET() do { bouncedtimes--; parv[1] = orig_parv1; return; } while(0)
    332 
    333 	if (parc < 2 || *parv[1] == '\0')
    334 	{
    335 		sendnumeric(client, ERR_NEEDMOREPARAMS, "JOIN");
    336 		return;
    337 	}
    338 
    339 	/* For our tests we need super accurate time for JOINs or they mail fail. */
    340 	gettimeofday(&timeofday_tv, NULL);
    341 	timeofday = timeofday_tv.tv_sec;
    342 
    343 	bouncedtimes++;
    344 	orig_parv1 = parv[1];
    345 	/* don't use 'return;' but 'RET();' from here ;p */
    346 
    347 	if (bouncedtimes > MAXBOUNCE)
    348 	{
    349 		/* bounced too many times. yeah.. should be in the link module, I know.. then again, who cares.. */
    350 		sendnotice(client, "*** Couldn't join %s ! - Link setting was too bouncy", parv[1]);
    351 		RET();
    352 	}
    353 
    354 	*jbuf = '\0';
    355 	/*
    356 	   ** Rebuild list of channels joined to be the actual result of the
    357 	   ** JOIN.  Note that "JOIN 0" is the destructive problem.
    358 	 */
    359 	strlcpy(request, parv[1], sizeof(request));
    360 	for (i = 0, name = strtoken(&p, request, ","); name; i++, name = strtoken(&p, NULL, ","))
    361 	{
    362 		if (MyUser(client) && (++ntargets > maxtargets))
    363 		{
    364 			sendnumeric(client, ERR_TOOMANYTARGETS, name, maxtargets, "JOIN");
    365 			break;
    366 		}
    367 		if (*name == '0' && !atoi(name))
    368 		{
    369 			/* UnrealIRCd 5+: we only support "JOIN 0",
    370 			 * "JOIN 0,#somechan" etc... so only at the beginning.
    371 			 * We do not support it half-way like "JOIN #a,0,#b"
    372 			 * since that doesn't make sense, unless you are flooding...
    373 			 * We still support it in remote joins for compatibility.
    374 			 */
    375 			if (MyUser(client) && (i != 0))
    376 				continue;
    377 			strlcpy(jbuf, "0", sizeof(jbuf));
    378 			continue;
    379 		} else
    380 		if (MyConnect(client) && !valid_channelname(name))
    381 		{
    382 			send_invalid_channelname(client, name);
    383 			if (IsOper(client) && find_channel(name))
    384 			{
    385 				/* Give IRCOps a bit more information */
    386 				sendnotice(client, "Channel '%s' is unjoinable because it contains illegal characters. "
    387 				                   "However, it does exist because another server in your "
    388 				                   "network, which has a more loose restriction, created it. "
    389 				                   "See https://www.unrealircd.org/docs/Set_block#set::allowed-channelchars",
    390 				                   name);
    391 			}
    392 			continue;
    393 		}
    394 		else if (!IsChannelName(name))
    395 		{
    396 			if (MyUser(client))
    397 				sendnumeric(client, ERR_NOSUCHCHANNEL, name);
    398 			continue;
    399 		}
    400 		if (*jbuf)
    401 			strlcat(jbuf, ",", sizeof jbuf);
    402 		strlcat(jbuf, name, sizeof(jbuf));
    403 	}
    404 
    405 	/* We are going to overwrite 'jbuf' with the calls to strtoken()
    406 	 * a few lines further down. Copy it to 'jbuf2' and make that
    407 	 * the new parv[1].. or at least temporarily.
    408 	 */
    409 	strlcpy(jbuf2, jbuf, sizeof(jbuf2));
    410 	parv[1] = jbuf2;
    411 
    412 	p = NULL;
    413 	if (parv[2])
    414 	{
    415 		strlcpy(request_key, parv[2], sizeof(request_key));
    416 		key = strtoken(&p2, request_key, ",");
    417 	}
    418 	parv[2] = NULL;		/* for cmd_names call later, parv[parc] must == NULL */
    419 
    420 	for (name = strtoken(&p, jbuf, ",");
    421 	     name;
    422 	     key = key ? strtoken(&p2, NULL, ",") : NULL, name = strtoken(&p, NULL, ","))
    423 	{
    424 		MessageTag *mtags = NULL;
    425 
    426 		/*
    427 		   ** JOIN 0 sends out a part for all channels a user
    428 		   ** has joined.
    429 		 */
    430 		if (*name == '0' && !atoi(name))
    431 		{
    432 			/* Rewritten so to generate a PART for each channel to servers,
    433 			 * so the same msgid is used for each part on all servers. -- Syzop
    434 			 */
    435 			while ((lp = client->user->channel))
    436 			{
    437 				MessageTag *mtags = NULL;
    438 				channel = lp->channel;
    439 
    440 				new_message(client, NULL, &mtags);
    441 
    442 				sendto_channel(channel, client, NULL, 0, 0, SEND_LOCAL, mtags,
    443 				               ":%s PART %s :%s",
    444 				               client->name, channel->name, "Left all channels");
    445 				sendto_server(client, 0, 0, mtags, ":%s PART %s :Left all channels", client->name, channel->name);
    446 
    447 				if (MyConnect(client))
    448 					RunHook(HOOKTYPE_LOCAL_PART, client, channel, mtags, "Left all channels");
    449 
    450 				remove_user_from_channel(client, channel, 0);
    451 				free_message_tags(mtags);
    452 			}
    453 			continue;
    454 		}
    455 
    456 		if (MyConnect(client))
    457 		{
    458 			member_modes = (ChannelExists(name)) ? "" : LEVEL_ON_JOIN;
    459 
    460 			if (!ValidatePermissionsForPath("immune:maxchannelsperuser",client,NULL,NULL,NULL))	/* opers can join unlimited chans */
    461 				if (client->user->joined >= MAXCHANNELSPERUSER)
    462 				{
    463 					sendnumeric(client, ERR_TOOMANYCHANNELS, name);
    464 					RET();
    465 				}
    466 /* RESTRICTCHAN */
    467 			if (conf_deny_channel)
    468 			{
    469 				if (!ValidatePermissionsForPath("immune:server-ban:deny-channel",client,NULL,NULL,NULL))
    470 				{
    471 					ConfigItem_deny_channel *d;
    472 					if ((d = find_channel_allowed(client, name)))
    473 					{
    474 						if (d->warn)
    475 						{
    476 							unreal_log(ULOG_INFO, "join", "JOIN_DENIED_FORBIDDEN_CHANNEL", client,
    477 							           "Client $client.details tried to join forbidden channel $channel",
    478 							           log_data_string("channel", name));
    479 						}
    480 						if (d->reason)
    481 							sendnumeric(client, ERR_FORBIDDENCHANNEL, name, d->reason);
    482 						if (d->redirect)
    483 						{
    484 							sendnotice(client, "*** Redirecting you to %s", d->redirect);
    485 							parv[0] = NULL;
    486 							parv[1] = d->redirect;
    487 							do_join(client, 2, parv);
    488 						}
    489 						if (d->class)
    490 							sendnotice(client, "*** Can not join %s: Your class is not allowed", name);
    491 						continue;
    492 					}
    493 				}
    494 			}
    495 			if (!ValidatePermissionsForPath("immune:server-ban:deny-channel",client,NULL,NULL,NULL) && (tklban = find_qline(client, name, &ishold)))
    496 			{
    497 				sendnumeric(client, ERR_FORBIDDENCHANNEL, name, tklban->ptr.nameban->reason);
    498 				continue;
    499 			}
    500 			/* ugly set::spamfilter::virus-help-channel-deny hack.. */
    501 			if (SPAMFILTER_VIRUSCHANDENY && SPAMFILTER_VIRUSCHAN &&
    502 			    !strcasecmp(name, SPAMFILTER_VIRUSCHAN) &&
    503 			    !ValidatePermissionsForPath("immune:server-ban:viruschan",client,NULL,NULL,NULL) && !spamf_ugly_vchanoverride)
    504 			{
    505 				Channel *channel = find_channel(name);
    506 				
    507 				if (!channel || !is_invited(client, channel))
    508 				{
    509 					sendnotice(client, "*** Cannot join '%s' because it's the virus-help-channel "
    510 					                   "which is reserved for infected users only", name);
    511 					continue;
    512 				}
    513 			}
    514 		}
    515 
    516 		channel = make_channel(name);
    517 		if (channel && (lp = find_membership_link(client->user->channel, channel)))
    518 			continue;
    519 
    520 		if (!channel)
    521 			continue;
    522 
    523 		i = HOOK_CONTINUE;
    524 		if (!MyConnect(client))
    525 			member_modes = "";
    526 		else
    527 		{
    528 			Hook *h;
    529 			char *errmsg = NULL;
    530 			for (h = Hooks[HOOKTYPE_PRE_LOCAL_JOIN]; h; h = h->next) 
    531 			{
    532 				i = (*(h->func.intfunc))(client,channel,key);
    533 				if (i == HOOK_DENY || i == HOOK_ALLOW)
    534 					break;
    535 			}
    536 			/* Denied, get out now! */
    537 			if (i == HOOK_DENY)
    538 			{
    539 				/* Rejected... if we just created a new chan we should destroy it too. -- Syzop */
    540 				if (!channel->users)
    541 					sub1_from_channel(channel);
    542 				continue;
    543 			}
    544 			/* If they are allowed, don't check can_join */
    545 			if (i != HOOK_ALLOW && 
    546 			   (i = can_join(client, channel, key, &errmsg)))
    547 			{
    548 				if (i != -1)
    549 					send_cannot_join_error(client, i, errmsg, name);
    550 				continue;
    551 			}
    552 		}
    553 
    554 		/* Generate a new message without inheritance.
    555 		 * We can do this because remote joins don't follow this code path,
    556 		 * or are highly discouraged to anyway.
    557 		 * Remote servers use SJOIN and never reach this function.
    558 		 * Locally we do follow this code path with JOIN and then generating
    559 		 * a new_message() here is exactly what we want:
    560 		 * Each "JOIN #a,#b,#c" gets processed individually in this loop
    561 		 * and is sent by join_channel() as a SJOIN for #a, then SJOIN for #b,
    562 		 * and so on, each with their own unique msgid and such.
    563 		 */
    564 		new_message(client, NULL, &mtags);
    565 		join_channel(channel, client, mtags, member_modes);
    566 		free_message_tags(mtags);
    567 	}
    568 	RET();
    569 #undef RET
    570 }
    571 
    572 #if defined(__GNUC__)
    573 #pragma GCC diagnostic push
    574 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
    575 #endif
    576 void send_cannot_join_error(Client *client, int numeric, char *fmtstr, char *channel_name)
    577 {
    578 	// TODO: add single %s validation !
    579 	sendnumericfmt(client, numeric, fmtstr, channel_name);
    580 }
    581 #if defined(__GNUC__)
    582 #pragma GCC diagnostic pop
    583 #endif
    584 
    585 /* Additional channel-related functions. I've put it here instead
    586  * of the core so it could be upgraded on the fly should it be necessary.
    587  */
    588 
    589 char *_get_chmodes_for_user(Client *client, const char *member_flags)
    590 {
    591 	static char modebuf[512]; /* returned */
    592 	char flagbuf[8]; /* For holding "vhoaq" */
    593 	char parabuf[512];
    594 	int n, i;
    595 
    596 	if (BadPtr(member_flags))
    597 		return "";
    598 
    599 	parabuf[0] = '\0';
    600 	n = strlen(member_flags);
    601 	if (n)
    602 	{
    603 		for (i=0; i < n; i++)
    604 		{
    605 			strlcat(parabuf, client->name, sizeof(parabuf));
    606 			if (i < n - 1)
    607 				strlcat(parabuf, " ", sizeof(parabuf));
    608 		}
    609 		/* And we have our mode line! */
    610 		snprintf(modebuf, sizeof(modebuf), "+%s %s", member_flags, parabuf);
    611 		return modebuf;
    612 	}
    613 
    614 	return "";
    615 }