diff --git a/Changes b/Changes
index 19aa4b161..40d3d4297 100644
--- a/Changes
+++ b/Changes
@@ -589,3 +589,7 @@
- Fixed a bug with channel alias{}'s where using the format syntax caused a crash (#0002323)
reported by Snake.
- Made channel mode +S strip RGB color codes.
+- Added channelmode +j (jointhrottle), syntax: /mode #chan +j X:Y, and then it will
+ throttle the number of joins per-user to X in Y seconds. Idea from Angrywolf (who
+ wrote a module that did this before). This might need some more testing :).
+ It's enabled by default but can be #undef'ed in include/config.h (line 449).
diff --git a/doc/unreal32docs.html b/doc/unreal32docs.html
index 18b23d327..dde5f9e76 100644
--- a/doc/unreal32docs.html
+++ b/doc/unreal32docs.html
@@ -2375,6 +2375,10 @@ set {
Makes channel G rated. Checks for words listed in the Badword Blocks,
and replaces them with the words specified |
+
+ j <joins:seconds> |
+ Throttles joins per-user to joins per seconds seconds |
+
M |
A registered nickname (+r) is required to talk |
diff --git a/help.conf b/help.conf
index d5bdfbb4f..3a2d62c2a 100644
--- a/help.conf
+++ b/help.conf
@@ -165,6 +165,7 @@ help Chmodes {
" I = Overrides +i for matching users [h]";
" f = Flood protection (for more info see /HELPOP CHMODEF) [o]";
" i = A user must be invited to join the channel [h]";
+ " j = Throttle joins per-user to 'joins' per 'sec' seconds";
" k = Users must specify to join [h]";
" l = Channel may hold at most of users [o]";
" m = Moderated channel (only +vhoaq users may speak) [h]";
diff --git a/include/config.h b/include/config.h
index f5c37f935..2284fac31 100644
--- a/include/config.h
+++ b/include/config.h
@@ -431,6 +431,7 @@
/*
* Extended channel modes. This extends the channel modes with yet another
* 32 possible modes which can also be used in modules.
+ * This is now pretty much required.
*/
#define EXTCMODE
@@ -440,6 +441,13 @@
*/
#define NEWCHFLOODPROT
+/* JoinThrottle (chanmode +j): +j x:y throttles users to X joins per Y seconds (per-user).
+ * In peak situations (eg: just after a server restart with thousand clients joining
+ * hundreds of channels) it can use like ~200k, but in normal circumstances you should
+ * count on just ~10-50k.
+ */
+#define JOINTHROTTLE
+
/* ------------------------- END CONFIGURATION SECTION -------------------- */
#define MOTD MPATH
#define RULES RPATH
diff --git a/include/h.h b/include/h.h
index 76a7a045c..ab0a12bdf 100644
--- a/include/h.h
+++ b/include/h.h
@@ -467,6 +467,7 @@ extern MODVAR Cmode_t EXTMODE_NONOTICE;
#ifdef STRIPBADWORDS
extern MODVAR Cmode_t EXTMODE_STRIPBADWORDS;
#endif
+extern MODVAR Cmode_t EXTMODE_JOINTHROTTLE;
#endif
#ifndef HAVE_STRLCPY
@@ -739,3 +740,9 @@ extern char *clean_ban_mask(char *, int, aClient *);
extern void chanfloodtimer_stopchantimers(aChannel *chptr);
extern int find_invex(aChannel *chptr, aClient *sptr);
extern void DoMD5(unsigned char *mdout, unsigned char *src, unsigned long n);
+#ifdef JOINTHROTTLE
+aJFlood *cmodej_addentry(aClient *cptr, aChannel *chptr);
+void cmodej_delentry(aJFlood *e);
+void cmodej_deluserentries(aClient *cptr);
+void cmodej_delchannelentries(aChannel *chptr);
+#endif
diff --git a/include/numeric.h b/include/numeric.h
index 1c7c9727e..277d08cde 100644
--- a/include/numeric.h
+++ b/include/numeric.h
@@ -129,6 +129,7 @@
#define ERR_CHANOWNPRIVNEEDED 499
+#define ERR_TOOMANYJOINS 500
#define ERR_UMODEUNKNOWNFLAG 501
#define ERR_USERSDONTMATCH 502
diff --git a/include/struct.h b/include/struct.h
index 7dd77abba..4ce7b2cd4 100644
--- a/include/struct.h
+++ b/include/struct.h
@@ -143,6 +143,7 @@ typedef struct _cmdoverride Cmdoverride;
typedef struct SMember Member;
typedef struct SMembership Membership;
typedef struct SMembershipL MembershipL;
+typedef struct JFlood aJFlood;
#ifdef ZIP_LINKS
typedef struct Zdata aZdata;
@@ -754,6 +755,9 @@ struct User {
unsigned char away_c; /* number of times away has been set */
#endif
} flood;
+#ifdef JOINTHROTTLE
+ aJFlood *jflood;
+#endif
};
struct Server {
@@ -873,6 +877,12 @@ extern Cmode *CmodeAdd(Module *reserved, CmodeInfo req, Cmode_t *mode);
extern void CmodeDel(Cmode *cmode);
#endif
+typedef struct {
+ EXTCM_PAR_HEADER
+ unsigned short num;
+ unsigned short t;
+} aModejEntry;
+
#define LISTENER_NORMAL 0x000001
#define LISTENER_CLIENTSONLY 0x000002
#define LISTENER_SERVERSONLY 0x000004
@@ -1486,6 +1496,9 @@ struct Channel {
Ban *banlist;
Ban *exlist; /* exceptions */
Ban *invexlist; /* invite list */
+#ifdef JOINTHROTTLE
+ aJFlood *jflood;
+#endif
char chname[1];
};
@@ -1735,6 +1748,42 @@ typedef struct {
unsigned parameters : 1;
} aCtab;
+#ifdef JOINTHROTTLE
+/** A jointhrottle item, this is a double linked list.
+ * prev_u Previous entry of user
+ * next_u Next entry of user
+ * prev_c Previous entry of channel
+ * next_c Next entry of channel
+ * chptr The channel this entry applies to
+ * cptr The user this entry applies to
+ * firstjoin Timestamp of "first join" (since last timer reset)
+ * numjoin Number of joins since that period
+ * CLARIFICATION:
+ * Why a double linked list? Well, the following operations need to be performed:
+ * - if user quits, entry must be removed
+ * - if channel is destroyed, entry must be removed
+ * (and of course, more, but these are the most important ones affecting this decision)
+ * While it would be possible to have a linked list only by user (for example),
+ * that would mean that upon channel destroy ALL entries would have to be searched
+ * trough, which might mean for example 800*8=6400 entries in a peak situation
+ * (such as after a server restart and hundreds of clients connecting&joining).
+ * For obvious reasons, that would be a very bad idea :).
+ * So this costs us 2 pointers (8b on ia32) per entry, but in case of channel destroy
+ * it means we only have for example 20 entries to scan trough rather than 2000.
+ * Worth the extra memory :). -- Syzop
+ * Note that in normal situations it won't be that bad since we will try to
+ * regulary free up some entries.
+ */
+struct JFlood {
+ aJFlood *prev_u, *next_u;
+ aJFlood *prev_c, *next_c;
+ aChannel *chptr;
+ aClient *cptr;
+ time_t firstjoin;
+ unsigned short numjoins;
+};
+#endif
+
void init_throttling_hash();
int hash_throttling(struct IN_ADDR *in);
struct ThrottlingBucket *find_throttling_bucket(struct IN_ADDR *in);
diff --git a/src/channel.c b/src/channel.c
index b717d2cd6..75c43047d 100644
--- a/src/channel.c
+++ b/src/channel.c
@@ -1523,6 +1523,9 @@ void sub1_from_channel(aChannel *chptr)
chanfloodtimer_stopchantimers(chptr);
if (chptr->mode.floodprot)
MyFree(chptr->mode.floodprot);
+#endif
+#ifdef JOINTHROTTLE
+ cmodej_delchannelentries(chptr);
#endif
if (chptr->topic)
MyFree(chptr->topic);
diff --git a/src/events.c b/src/events.c
index ed0650b3d..d13bc8b7c 100644
--- a/src/events.c
+++ b/src/events.c
@@ -45,6 +45,10 @@ ID_Copyright("(C) Carsten Munk 2001");
MODVAR Event *events = NULL;
+#ifdef JOINTHROTTLE
+extern EVENT(cmodej_cleanup_structs);
+#endif
+
void LockEventSystem(void)
{
}
@@ -213,6 +217,9 @@ void SetupEvents(void)
EventAddEx(NULL, "loop", 0, 0, loop_event, NULL);
#ifndef NO_FDLIST
EventAddEx(NULL, "fdlistcheck", 1, 0, e_check_fdlists, NULL);
+#endif
+#ifdef JOINTHROTTLE
+ EventAddEx(NULL, "cmodej_cleanup_structs", 60, 0, cmodej_cleanup_structs, NULL);
#endif
UnlockEventSystem();
}
diff --git a/src/extcmodes.c b/src/extcmodes.c
index 94aad5371..1c1ee2ae2 100644
--- a/src/extcmodes.c
+++ b/src/extcmodes.c
@@ -58,6 +58,18 @@ Cmode_t EXTMODE_NONOTICE = 0L;
Cmode_t EXTMODE_STRIPBADWORDS = 0L;
#endif
+#ifdef JOINTHROTTLE
+/* cmode j stuff... */
+Cmode_t EXTMODE_JOINTHROTTLE = 0L;
+int cmodej_is_ok(aClient *sptr, aChannel *chptr, char *para, int type, int what);
+CmodeParam *cmodej_put_param(CmodeParam *r_in, char *param);
+char *cmodej_get_param(CmodeParam *r_in);
+char *cmodej_conv_param(char *param_in);
+void cmodej_free_param(CmodeParam *r);
+CmodeParam *cmodej_dup_struct(CmodeParam *r_in);
+int cmodej_sjoin_check(aChannel *chptr, CmodeParam *ourx, CmodeParam *theirx);
+#endif
+
void make_extcmodestr()
{
char *p;
@@ -99,6 +111,21 @@ static void load_extendedchanmodes(void)
req.flag = 'G';
CmodeAdd(NULL, req, &EXTMODE_STRIPBADWORDS);
#endif
+
+#ifdef JOINTHROTTLE
+ /* +j */
+ memset(&req, 0, sizeof(req));
+ req.paracount = 1;
+ req.is_ok = cmodej_is_ok;
+ req.flag = 'j';
+ req.put_param = cmodej_put_param;
+ req.get_param = cmodej_get_param;
+ req.conv_param = cmodej_conv_param;
+ req.free_param = cmodej_free_param;
+ req.dup_struct = cmodej_dup_struct;
+ req.sjoin_check = cmodej_sjoin_check;
+ CmodeAdd(NULL, req, &EXTMODE_JOINTHROTTLE);
+#endif
}
void extcmode_init(void)
@@ -268,4 +295,152 @@ int extcmode_default_requirehalfop(aClient *cptr, aChannel *chptr, char *para, i
return EX_DENY;
}
+#ifdef JOINTHROTTLE
+/*** CHANNEL MODE +j STUFF ******/
+int cmodej_is_ok(aClient *sptr, aChannel *chptr, char *para, int type, int what)
+{
+ if ((type == EXCHK_ACCESS) || (type == EXCHK_ACCESS_ERR))
+ {
+ if (IsPerson(sptr) && is_chan_op(sptr, chptr))
+ return EX_ALLOW;
+ return EX_DENY;
+ } else
+ if (type == EXCHK_PARAM)
+ {
+ /* Check parameter.. syntax should be X:Y, X should be 1-255, Y should be 1-999 */
+ char buf[32], *p;
+ int num, t, fail = 0;
+
+ strlcpy(buf, para, sizeof(buf));
+ p = strchr(buf, ':');
+ if (!p)
+ {
+ fail = 1;
+ } else {
+ *p++ = '\0';
+ num = atoi(buf);
+ t = atoi(p);
+ if ((num < 1) || (num > 255) || (t < 1) || (t > 999))
+ fail = 1;
+ }
+ if (fail)
+ {
+ sendnotice(sptr, "Error in setting +j, syntax: +j :, where must be 1-255, and 1-999");
+ return EX_DENY;
+ }
+ return EX_ALLOW;
+ }
+
+ /* falltrough -- should not be used */
+ return EX_DENY;
+}
+
+CmodeParam *cmodej_put_param(CmodeParam *r_in, char *param)
+{
+aModejEntry *r = (aModejEntry *)r_in;
+char buf[32], *p;
+int num, t;
+
+ if (!r)
+ {
+ /* Need to create one */
+ r = (aModejEntry *)malloc(sizeof(aModejEntry));
+ memset(r, 0, sizeof(aModejEntry));
+ r->flag = 'j';
+ }
+ strlcpy(buf, param, sizeof(buf));
+ p = strchr(buf, ':');
+ if (p)
+ {
+ *p++ = '\0';
+ num = atoi(buf);
+ t = atoi(p);
+ if (num < 1) num = 1;
+ if (num > 255) num = 255;
+ if (t < 1) t = 1;
+ if (t > 999) t = 999;
+ r->num = num;
+ r->t = t;
+ } else {
+ r->num = 0;
+ r->t = 0;
+ }
+ return (CmodeParam *)r;
+}
+
+char *cmodej_get_param(CmodeParam *r_in)
+{
+aModejEntry *r = (aModejEntry *)r_in;
+static char retbuf[16];
+
+ if (!r)
+ return NULL;
+
+ snprintf(retbuf, sizeof(retbuf), "%hu:%hu", r->num, r->t);
+ return retbuf;
+}
+
+char *cmodej_conv_param(char *param_in)
+{
+static char retbuf[32];
+char param[32], *p;
+int num, t, fail = 0;
+
+ strlcpy(param, param_in, sizeof(param));
+ p = strchr(param, ':');
+ if (!p)
+ return NULL;
+ *p++ = '\0';
+ num = atoi(param);
+ t = atoi(p);
+ if (num < 1)
+ num = 1;
+ if (num > 255)
+ num = 255;
+ if (t < 1)
+ t = 1;
+ if (t > 999)
+ t = 999;
+
+ snprintf(retbuf, sizeof(retbuf), "%d:%d", num, t);
+ return retbuf;
+}
+
+void cmodej_free_param(CmodeParam *r)
+{
+ MyFree(r);
+}
+
+CmodeParam *cmodej_dup_struct(CmodeParam *r_in)
+{
+aModejEntry *r = (aModejEntry *)r_in;
+aModejEntry *w = (aModejEntry *)MyMalloc(sizeof(aModejEntry));
+
+ memcpy(w, r, sizeof(aModejEntry));
+ return (CmodeParam *)w;
+}
+
+int cmodej_sjoin_check(aChannel *chptr, CmodeParam *ourx, CmodeParam *theirx)
+{
+aModejEntry *our = (aModejEntry *)ourx;
+aModejEntry *their = (aModejEntry *)theirx;
+
+ if (our->t != their->t)
+ {
+ if (our->t > their->t)
+ return EXSJ_WEWON;
+ else
+ return EXSJ_THEYWON;
+ }
+ else if (our->num != their->num)
+ {
+ if (our->num > their->num)
+ return EXSJ_WEWON;
+ else
+ return EXSJ_THEYWON;
+ } else
+ return EXSJ_SAME;
+}
+#endif /* JOINTHROTTLE */
+
#endif /* EXTCMODE */
diff --git a/src/ircd.c b/src/ircd.c
index 9c40897e1..1ca752234 100644
--- a/src/ircd.c
+++ b/src/ircd.c
@@ -1208,7 +1208,7 @@ int InitwIRCD(int argc, char *argv[])
#ifndef _WIN32
fprintf(stderr, "%s", unreallogo);
fprintf(stderr, " v%s\n", VERSIONONLY);
- fprintf(stderr, " using TRE %s\n", tre_version());
+ fprintf(stderr, " using %s\n", tre_version());
#ifdef USE_SSL
fprintf(stderr, " using %s\n", OPENSSL_VERSION_TEXT);
#endif
diff --git a/src/list.c b/src/list.c
index ac1fa7689..0128b4b31 100644
--- a/src/list.c
+++ b/src/list.c
@@ -550,5 +550,154 @@ ListStruct *del_ListItem(ListStruct *item, ListStruct **list) {
}
return NULL;
}
+
+#ifdef JOINTHROTTLE
+/** Adds a aJFlood entry to user & channel and returns entry.
+ * NOTE: Does not check for already-existing-entry
+ */
+aJFlood *cmodej_addentry(aClient *cptr, aChannel *chptr)
+{
+aJFlood *e;
+
+#ifdef DEBUGMODE
+ if (!IsPerson(cptr))
+ abort();
+
+ for (e=cptr->user->jflood; e; e=e->next_u)
+ if (e->chptr == chptr)
+ abort();
+
+ for (e=chptr->jflood; e; e=e->next_c)
+ if (e->cptr == cptr)
+ abort();
+#endif
+
+ e = MyMallocEx(sizeof(aJFlood));
+ e->cptr = cptr;
+ e->chptr = chptr;
+ e->prev_u = e->prev_c = NULL;
+ e->next_u = cptr->user->jflood;
+ e->next_c = chptr->jflood;
+ cptr->user->jflood = chptr->jflood = e;
+
+ return e;
+}
+
+/** Removes an individual entry from list and frees it.
+ */
+void cmodej_delentry(aJFlood *e)
+{
+ /* remove from user.. */
+ if (e->prev_u)
+ e->prev_u->next_u = e->next_u;
+ else
+ e->cptr->user->jflood = e->next_u; /* new head */
+ if (e->next_u)
+ e->next_u->prev_u = e->prev_u;
+
+ /* remove from channel.. */
+ if (e->prev_c)
+ e->prev_c->next_c = e->next_c;
+ else
+ e->chptr->jflood = e->next_c; /* new head */
+ if (e->next_c)
+ e->next_c->prev_c = e->prev_c;
+
+ /* actually free it */
+ MyFree(e);
+}
+
+/** Removes all entries belonging to user from all lists and free them. */
+void cmodej_deluserentries(aClient *cptr)
+{
+aJFlood *e, *e_next;
+
+ for (e=cptr->user->jflood; e; e=e_next)
+ {
+ e_next = e->next_u;
+
+ /* remove from channel.. */
+ if (e->prev_c)
+ e->prev_c->next_c = e->next_c;
+ else
+ e->chptr->jflood = e->next_c; /* new head */
+ if (e->next_c)
+ e->next_c->prev_c = e->prev_c;
+
+ /* actually free it */
+ MyFree(e);
+ }
+}
+
+/** Removes all entries belonging to channel from all lists and free them. */
+void cmodej_delchannelentries(aChannel *chptr)
+{
+aJFlood *e, *e_next;
+
+ for (e=chptr->jflood; e; e=e_next)
+ {
+ e_next = e->next_c;
+ /* remove from user.. */
+ if (e->prev_u)
+ e->prev_u->next_u = e->next_u;
+ else
+ e->cptr->user->jflood = e->next_u; /* new head */
+ if (e->next_u)
+ e->next_u->prev_u = e->prev_u;
+
+ /* actually free it */
+ MyFree(e);
+ }
+}
+
+/** Regulary cleans up cmode-j user/chan structs */
+EVENT(cmodej_cleanup_structs)
+{
+aJFlood *e, *e_next;
+int i;
+aClient *cptr;
+aChannel *chptr;
+int t;
+CmodeParam *cmp;
+#ifdef DEBUGMODE
+int freed=0;
+#endif
+
+ for (chptr = channel; chptr; chptr=chptr->nextch)
+ {
+ if (!chptr->jflood)
+ continue;
+ t=0;
+ /* t will be kept at 0 if not found or if mode not set,
+ * but DO still check since there are entries left as indicated by ->jflood!
+ */
+ if (chptr->mode.extmode & EXTMODE_JOINTHROTTLE)
+ {
+ for (cmp = chptr->mode.extmodeparam; cmp; cmp=cmp->next)
+ if (cmp->flag == 'j')
+ t = ((aModejEntry *)cmp)->t;
+ }
+
+ for (e = chptr->jflood; e; e = e_next)
+ {
+ e_next = e->next_c;
+
+ if (e->firstjoin + t < TStime())
+ {
+ cmodej_delentry(e);
+#ifdef DEBUGMODE
+ freed++;
+#endif
+ }
+ }
+ }
+
+#ifdef DEBUGMODE
+ if (freed)
+ ircd_log(LOG_ERROR, "cmodej_cleanup_structs: %d entries freed [%d bytes]", freed, freed * sizeof(aJFlood));
+#endif
+}
+
+#endif
diff --git a/src/modules/m_join.c b/src/modules/m_join.c
index f040a4ec6..4eb3eca79 100644
--- a/src/modules/m_join.c
+++ b/src/modules/m_join.c
@@ -51,6 +51,10 @@ DLLFUNC CMD_FUNC(_do_join);
DLLFUNC int _can_join(aClient *cptr, aClient *sptr, aChannel *chptr, char *key, char *link, char *parv[]);
static int extended_operoverride(aClient *sptr, aChannel *chptr, char *key, int mval, char mchar);
#define MAXBOUNCE 5 /** Most sensible */
+#ifdef JOINTHROTTLE
+static int isjthrottled(aClient *cptr, aChannel *chptr);
+static void cmodej_increase_usercounter(aClient *cptr, aChannel *chptr);
+#endif
/* Externs */
extern MODVAR int spamf_ugly_vchanoverride;
@@ -220,9 +224,97 @@ Ban *banned;
#endif
#endif
+#ifdef JOINTHROTTLE
+ if (!IsAnOper(cptr) &&
+ (chptr->mode.extmode & EXTMODE_JOINTHROTTLE) && isjthrottled(cptr, chptr))
+ return ERR_TOOMANYJOINS;
+#endif
+
return 0;
}
+#ifdef JOINTHROTTLE
+static int isjthrottled(aClient *cptr, aChannel *chptr)
+{
+CmodeParam *m;
+aJFlood *e;
+int num=0, t=0;
+
+ if (!MyClient(cptr))
+ return 0;
+
+ for (m = chptr->mode.extmodeparam; m; m=m->next)
+ if (m->flag == 'j')
+ {
+ num = ((aModejEntry *)m)->num;
+ t = ((aModejEntry *)m)->t;
+ break;
+ }
+
+ if (!num || !t)
+ return 0;
+
+ /* Grab user<->chan entry.. */
+ for (e = cptr->user->jflood; e; e=e->next_u)
+ if (e->chptr == chptr)
+ break;
+
+ if (!e)
+ return 0; /* Not present, so cannot be throttled */
+
+ /* Ok... now the actual check:
+ * if ([timer valid] && [one more join would exceed num])
+ */
+ if (((TStime() - e->firstjoin) < t) && (e->numjoins == num))
+ return 1; /* Throttled */
+
+ return 0;
+}
+
+static void cmodej_increase_usercounter(aClient *cptr, aChannel *chptr)
+{
+CmodeParam *m;
+aJFlood *e;
+int num=0, t=0;
+
+ if (!MyClient(cptr))
+ return;
+
+ for (m = chptr->mode.extmodeparam; m; m=m->next)
+ if (m->flag == 'j')
+ {
+ num = ((aModejEntry *)m)->num;
+ t = ((aModejEntry *)m)->t;
+ break;
+ }
+
+ if (!num || !t)
+ return;
+
+ /* Grab user<->chan entry.. */
+ for (e = cptr->user->jflood; e; e=e->next_u)
+ if (e->chptr == chptr)
+ break;
+
+ if (!e)
+ {
+ /* Allocate one */
+ e = cmodej_addentry(cptr, chptr);
+ e->firstjoin = TStime();
+ e->numjoins = 1;
+ } else
+ if ((TStime() - e->firstjoin) < t) /* still valid? */
+ {
+ e->numjoins++;
+ } else {
+ /* reset :p */
+ e->firstjoin = TStime();
+ e->numjoins = 1;
+ }
+}
+
+#endif
+
/*
** m_join
** parv[0] = sender prefix
@@ -617,6 +709,9 @@ DLLFUNC CMD_FUNC(_do_join)
me.name, parv[0], name);
continue;
}
+#ifdef JOINTHROTTLE
+ cmodej_increase_usercounter(cptr, chptr);
+#endif
}
join_channel(chptr, cptr, sptr, flags);
diff --git a/src/s_err.c b/src/s_err.c
index 61899023d..626cd2275 100644
--- a/src/s_err.c
+++ b/src/s_err.c
@@ -556,7 +556,7 @@ static char *replies[] = {
/* 497 */ NULL, /* ircu */
/* 498 */ NULL, /* ircu */
/* 499 ERR_CHANOWNPRIVNEEDED */ ":%s 499 %s %s :You're not a channel owner",
-/* 500 */ NULL,
+/* 500 ERR_TOOMANYJOINS */ ":%s 500 %s %s :Too many join requests. Please wait a while and try again.",
/* 501 ERR_UMODEUNKNOWNFLAG */ ":%s 501 %s :Unknown MODE flag",
/* 502 ERR_USERSDONTMATCH */ ":%s 502 %s :Cant change mode for other users",
/* 503 */ NULL, /* austhex */
diff --git a/src/s_misc.c b/src/s_misc.c
index ebf6be008..17d12fec6 100644
--- a/src/s_misc.c
+++ b/src/s_misc.c
@@ -727,6 +727,9 @@ static void exit_one_client(aClient *cptr, aClient *sptr, aClient *from, char *c
{
RunHook2(HOOKTYPE_REMOTE_QUIT, sptr, comment);
}
+#ifdef JOINTHROTTLE
+ cmodej_deluserentries(sptr);
+#endif
while ((mp = sptr->user->channel))
remove_user_from_channel(sptr, mp->chptr);