/* modes.c : irssi Copyright (C) 1999-2000 Timo Sirainen 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 2 of the License, 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "module.h" #include "signals.h" #include "settings.h" #include "irc-commands.h" #include "irc-servers.h" #include "irc-channels.h" #include "servers-redirect.h" #include "modes.h" #include "mode-lists.h" #include "nicklist.h" /* Change nick's mode in channel */ static void nick_mode_change(IRC_CHANNEL_REC *channel, const char *nick, char mode, int type, const char *setby) { NICK_REC *nickrec; char modestr[2], typestr[2]; g_return_if_fail(IS_IRC_CHANNEL(channel)); g_return_if_fail(nick != NULL); nickrec = nicklist_find(CHANNEL(channel), nick); if (nickrec == NULL) return; /* No /names list got yet */ if (mode == '@') nickrec->op = type == '+'; else if (mode == '+') nickrec->voice = type == '+'; else if (mode == '%') nickrec->halfop = type == '+'; else if (channel->server->prefix[(unsigned char) mode] != '\0') nickrec->other = (type == '+' ? mode : '\0'); modestr[0] = mode; modestr[1] = '\0'; typestr[0] = type; typestr[1] = '\0'; signal_emit("nick mode changed", 5, channel, nickrec, setby, modestr, typestr); } static int mode_is_set(const char *str, char mode) { char *end, *pos; g_return_val_if_fail(str != NULL, FALSE); end = strchr(str, ' '); pos = strchr(str, mode); return pos != NULL && (end == NULL || pos < end); } /* add argument to specified position */ static void mode_add_arg(GString *str, int pos, int updating, const char *arg) { char *p; for (p = str->str; *p != '\0'; p++) { if (*p != ' ') continue; if (pos == 0) break; pos--; } pos = (int) (p-str->str); if (updating && *p != '\0') { /* remove the old argument */ p++; while (*p != '\0' && *p != ' ') p++; g_string_erase(str, pos, (int) (p-str->str)-pos); } /* .. GLib shouldn't fail when inserting at the end of the string */ if (pos == str->len) { g_string_append_c(str, ' '); g_string_append(str, arg); } else { g_string_insert_c(str, pos, ' '); g_string_insert(str, pos+1, arg); } } /* Add mode character to list sorted alphabetically */ static void mode_add_sorted(IRC_SERVER_REC *server, GString *str, char mode, const char *arg, int user) { char *p; int updating, argpos = 0; /* check that mode isn't already set */ if ((!user && !HAS_MODE_ARG_SET(server, mode)) && mode_is_set(str->str, mode)) return; updating = FALSE; for (p = str->str; *p != '\0' && *p != ' '; p++) { if (mode < *p) break; if (mode == *p) { updating = TRUE; break; } if (!user && HAS_MODE_ARG_SET(server, *p)) argpos++; } /* .. GLib shouldn't fail when inserting at the end of the string */ if (!updating) { if (*p == '\0') g_string_append_c(str, mode); else g_string_insert_c(str, (int) (p-str->str), mode); } if (arg != NULL) mode_add_arg(str, argpos, updating, arg); } /* remove the n'th argument */ static void node_remove_arg(GString *str, int pos) { char *p; int startpos; startpos = -1; for (p = str->str; *p != '\0'; p++) { if (*p != ' ') continue; if (pos < 0) break; if (pos == 0) startpos = (int) (p-str->str); pos--; } if (startpos == -1) return; /* not found */ g_string_erase(str, startpos, (int) (p-str->str)-startpos); } /* remove mode (and it's argument) from string */ static void mode_remove(IRC_SERVER_REC *server, GString *str, char mode, int user) { char *p; int argpos = 0; for (p = str->str; *p != '\0' && *p != ' '; p++) { if (mode == *p) { g_string_erase(str, (int) (p-str->str), 1); if (!user && HAS_MODE_ARG_SET(server, mode)) node_remove_arg(str, argpos); break; } if (!user && HAS_MODE_ARG_SET(server, *p)) argpos++; } } static void mode_set(IRC_SERVER_REC *server, GString *str, char type, char mode, int user) { g_return_if_fail(str != NULL); if (type == '-') mode_remove(server, str, mode, user); else mode_add_sorted(server, str, mode, NULL, user); } static void mode_set_arg(IRC_SERVER_REC *server, GString *str, char type, char mode, const char *arg, int user) { g_return_if_fail(str != NULL); g_return_if_fail(type == '-' || arg != NULL); if (type == '-') mode_remove(server, str, mode, user); else mode_add_sorted(server, str, mode, arg, user); } /* Mode that needs a parameter of a mask for both setting and removing (eg: bans) */ void modes_type_a(IRC_CHANNEL_REC *channel, const char *setby, char type, char mode, char *arg, GString *newmode) { if (mode == 'b') { if (type == '+') banlist_add(channel, arg, setby, time(NULL)); else banlist_remove(channel, arg, setby); } } /* Mode that needs parameter for both setting and removing (eg: +k) */ void modes_type_b(IRC_CHANNEL_REC *channel, const char *setby, char type, char mode, char *arg, GString *newmode) { if (mode == 'k') { if (*arg == '\0' && type == '+') arg = channel->key != NULL ? channel->key : "???"; if (arg != channel->key) { g_free_and_null(channel->key); if (type == '+') channel->key = g_strdup(arg); } } mode_set_arg(channel->server, newmode, type, mode, arg, FALSE); } /* Mode that needs parameter only for adding */ void modes_type_c(IRC_CHANNEL_REC *channel, const char *setby, char type, char mode, char *arg, GString *newmode) { if (mode == 'l') { channel->limit = type == '-' ? 0 : atoi(arg); } mode_set_arg(channel->server, newmode, type, mode, arg, FALSE); } /* Mode that takes no parameter */ void modes_type_d(IRC_CHANNEL_REC *channel, const char *setby, char type, char mode, char *arg, GString *newmode) { mode_set(channel->server, newmode, type, mode, FALSE); } void modes_type_prefix(IRC_CHANNEL_REC *channel, const char *setby, char type, char mode, char *arg, GString *newmode) { int umode = (unsigned char) mode; nick_mode_change(channel, arg, channel->server->modes[umode].prefix, type, setby); if (g_strcasecmp(channel->server->nick, arg) == 0) { /* see if we need to update channel->chanop */ const char *prefix = g_hash_table_lookup(channel->server->isupport, "PREFIX"); if (prefix != NULL && *prefix == '(') { prefix++; while (*prefix != ')' && *prefix != '\0') { if (*prefix == mode) { channel->chanop = type == '+'; break; } if (*prefix == 'o') break; prefix++; } } else { if (mode == 'o' || mode == 'O') channel->chanop = type == '+'; } } } int channel_mode_is_set(IRC_CHANNEL_REC *channel, char mode) { g_return_val_if_fail(IS_IRC_CHANNEL(channel), FALSE); return channel->mode == NULL ? FALSE : mode_is_set(channel->mode, mode); } /* Parse channel mode string */ void parse_channel_modes(IRC_CHANNEL_REC *channel, const char *setby, const char *mode, int update_key) { IRC_SERVER_REC *server = channel->server; GString *newmode; char *dup, *modestr, *arg, *curmode, type, *old_key; int umode; g_return_if_fail(IS_IRC_CHANNEL(channel)); g_return_if_fail(mode != NULL); type = '+'; newmode = g_string_new(channel->mode); old_key = update_key ? NULL : g_strdup(channel->key); dup = modestr = g_strdup(mode); curmode = cmd_get_param(&modestr); while (*curmode != '\0') { if (HAS_MODE_ARG(server, type, *curmode)) { /* get the argument for the mode. NOTE: We don't get the +k's argument when joining to channel. */ arg = cmd_get_param(&modestr); } else { arg = NULL; } switch (*curmode) { case '+': case '-': type = *curmode; break; default: umode = (unsigned char) *curmode; if (server->modes[umode].func != NULL) { server->modes[umode].func(channel, setby, type, *curmode, arg, newmode); } else { /* Treat unknown modes as ones without params */ modes_type_d(channel, setby, type, *curmode, arg, newmode); } } curmode++; } g_free(dup); if (channel->key != NULL && strchr(channel->mode, 'k') == NULL && strchr(newmode->str, 'k') == NULL) { /* join was used with key but there's no key set in channel modes.. */ g_free(channel->key); channel->key = NULL; } else if (!update_key && old_key != NULL) { /* get the old one back, just in case it was replaced */ g_free(channel->key); channel->key = old_key; mode_set_arg(channel->server, newmode, '+', 'k', old_key, FALSE); old_key = NULL; } if (strcmp(newmode->str, channel->mode) != 0) { g_free(channel->mode); channel->mode = g_strdup(newmode->str); signal_emit("channel mode changed", 2, channel, setby); } g_string_free(newmode, TRUE); g_free(old_key); } /* add `mode' to `old' - return newly allocated mode. `channel' specifies if we're parsing channel mode and we should try to join mode arguments too. */ char *modes_join(IRC_SERVER_REC *server, const char *old, const char *mode, int channel) { GString *newmode; char *dup, *modestr, *curmode, type; g_return_val_if_fail(mode != NULL, NULL); type = '+'; newmode = g_string_new(old); dup = modestr = g_strdup(mode); curmode = cmd_get_param(&modestr); while (*curmode != '\0' && *curmode != ' ') { if (*curmode == '+' || *curmode == '-') { type = *curmode; curmode++; continue; } if (!channel || !HAS_MODE_ARG(server, type, *curmode)) mode_set(server, newmode, type, *curmode, !channel); else { mode_set_arg(server, newmode, type, *curmode, cmd_get_param(&modestr), !channel); } curmode++; } g_free(dup); modestr = newmode->str; g_string_free(newmode, FALSE); return modestr; } /* Parse user mode string */ static void parse_user_mode(IRC_SERVER_REC *server, const char *modestr) { char *newmode, *oldmode; g_return_if_fail(IS_IRC_SERVER(server)); g_return_if_fail(modestr != NULL); newmode = modes_join(NULL, server->usermode, modestr, FALSE); oldmode = server->usermode; server->usermode = newmode; server->server_operator = ((strchr(newmode, 'o') != NULL) || (strchr(newmode, 'O') != NULL)); signal_emit("user mode changed", 2, server, oldmode); g_free_not_null(oldmode); } static void event_user_mode(IRC_SERVER_REC *server, const char *data) { char *params, *nick, *mode; g_return_if_fail(data != NULL); params = event_get_params(data, 3, NULL, &nick, &mode); parse_user_mode(server, mode); g_free(params); } static void event_mode(IRC_SERVER_REC *server, const char *data, const char *nick) { IRC_CHANNEL_REC *chanrec; char *params, *channel, *mode; g_return_if_fail(data != NULL); params = event_get_params(data, 2 | PARAM_FLAG_GETREST, &channel, &mode); if (!ischannel(*channel)) { /* user mode change */ parse_user_mode(server, mode); } else { /* channel mode change */ chanrec = irc_channel_find(server, channel); if (chanrec != NULL) parse_channel_modes(chanrec, nick, mode, TRUE); } g_free(params); } static void event_oper(IRC_SERVER_REC *server, const char *data) { const char *opermode; opermode = settings_get_str("opermode"); if (*opermode != '\0') irc_send_cmdv(server, "MODE %s %s", server->nick, opermode); } static void event_away(IRC_SERVER_REC *server, const char *data) { g_return_if_fail(server != NULL); server->usermode_away = TRUE; signal_emit("away mode changed", 1, server); } static void event_unaway(IRC_SERVER_REC *server, const char *data) { g_return_if_fail(server != NULL); server->usermode_away = FALSE; g_free_and_null(server->away_reason); signal_emit("away mode changed", 1, server); } static void sig_req_usermode_change(IRC_SERVER_REC *server, const char *data, const char *nick, const char *addr) { char *params, *target, *mode; g_return_if_fail(data != NULL); params = event_get_params(data, 2 | PARAM_FLAG_GETREST, &target, &mode); if (!ischannel(*target)) { /* we requested a user mode change, save this */ mode = modes_join(NULL, server->wanted_usermode, mode, FALSE); g_free_not_null(server->wanted_usermode); server->wanted_usermode = mode; } g_free(params); signal_emit("event mode", 4, server, data, nick, addr); } void channel_set_singlemode(IRC_CHANNEL_REC *channel, const char *nicks, const char *mode) { GString *str; int num, modepos; char **nick, **nicklist; g_return_if_fail(IS_IRC_CHANNEL(channel)); g_return_if_fail(nicks != NULL && mode != NULL); if (*nicks == '\0') return; num = modepos = 0; str = g_string_new(NULL); nicklist = g_strsplit(nicks, " ", -1); for (nick = nicklist; *nick != NULL; nick++) { if (**nick == '\0') continue; if (num == 0) { g_string_sprintf(str, "MODE %s %s", channel->name, mode); modepos = str->len; } else { /* insert the mode string */ g_string_insert(str, modepos, mode); } g_string_sprintfa(str, " %s", *nick); if (++num == channel->server->max_modes_in_cmd) { /* max. modes / command reached, send to server */ irc_send_cmd(channel->server, str->str); num = 0; } } if (num > 0) irc_send_cmd(channel->server, str->str); g_strfreev(nicklist); g_string_free(str, TRUE); } void channel_set_mode(IRC_SERVER_REC *server, const char *channel, const char *mode) { IRC_CHANNEL_REC *chanrec; GString *tmode, *targs; char *modestr, *curmode, *orig, type, prevtype; int count; g_return_if_fail(IS_IRC_SERVER(server)); g_return_if_fail(channel != NULL && mode != NULL); tmode = g_string_new(NULL); targs = g_string_new(NULL); count = 0; chanrec = irc_channel_find(server, channel); if (chanrec != NULL) channel = chanrec->name; orig = modestr = g_strdup(mode); type = '+'; prevtype = '\0'; curmode = cmd_get_param(&modestr); for (;; curmode++) { if (*curmode == '\0') { /* support for +o nick +o nick2 */ curmode = cmd_get_param(&modestr); if (*curmode == '\0') break; } if (*curmode == '+' || *curmode == '-') { type = *curmode; continue; } if (count == server->max_modes_in_cmd && HAS_MODE_ARG(server, type, *curmode)) { irc_send_cmdv(server, "MODE %s %s%s", channel, tmode->str, targs->str); count = 0; prevtype = '\0'; g_string_truncate(tmode, 0); g_string_truncate(targs, 0); } if (type != prevtype) { prevtype = type; g_string_append_c(tmode, type); } g_string_append_c(tmode, *curmode); if (HAS_MODE_ARG(server, type, *curmode)) { char *arg; count++; arg = cmd_get_param(&modestr); if (*arg == '\0' && type == '-' && *curmode == 'k') { /* "/mode #channel -k" - no reason why it shouldn't work really, so append the key */ IRC_CHANNEL_REC *chanrec; chanrec = irc_channel_find(server, channel); if (chanrec != NULL && chanrec->key != NULL) arg = chanrec->key; } if (*arg != '\0') g_string_sprintfa(targs, " %s", arg); } } if (tmode->len > 0) { irc_send_cmdv(server, "MODE %s %s%s", channel, tmode->str, targs->str); } g_string_free(tmode, TRUE); g_string_free(targs, TRUE); g_free(orig); } static int get_wildcard_nicks(GString *output, const char *mask, IRC_CHANNEL_REC *channel, int op, int voice) { GSList *nicks, *tmp; int count; g_return_val_if_fail(output != NULL, 0); g_return_val_if_fail(mask != NULL, 0); g_return_val_if_fail(IS_IRC_CHANNEL(channel), 0); count = 0; nicks = nicklist_find_multiple(CHANNEL(channel), mask); for (tmp = nicks; tmp != NULL; tmp = tmp->next) { NICK_REC *rec = tmp->data; if ((op == 1 && !rec->op) || (op == 0 && rec->op) || (voice == 1 && !rec->voice) || (voice == 0 && rec->voice)) continue; if (g_strcasecmp(rec->nick, channel->server->nick) == 0) continue; g_string_sprintfa(output, "%s ", rec->nick); count++; } g_slist_free(nicks); return count; } static char *get_nicks(IRC_SERVER_REC *server, WI_ITEM_REC *item, const char *data, int op, int voice, IRC_CHANNEL_REC **ret_channel) { IRC_CHANNEL_REC *channel; GString *str; GHashTable *optlist; char **matches, **match, *ret, *channame, *nicks; void *free_arg; int count, max_modes; if (!cmd_get_params(data, &free_arg, 2 | PARAM_FLAG_GETREST | PARAM_FLAG_OPTIONS | PARAM_FLAG_OPTCHAN_NAME, item, "op", &optlist, &channame, &nicks)) return NULL; if (*nicks == '\0') return NULL; channel = irc_channel_find(server, channame); if (channel == NULL) { cmd_params_free(free_arg); return NULL; } str = g_string_new(NULL); matches = g_strsplit(nicks, " ", -1); for (match = matches; *match != NULL; match++) { if (strchr(*match, '*') == NULL && strchr(*match, '?') == NULL) { /* no wildcards */ g_string_sprintfa(str, "%s ", *match); } else { count = get_wildcard_nicks(str, *match, channel, op, voice); max_modes = settings_get_int("max_wildcard_modes"); if (max_modes > 0 && count > max_modes && g_hash_table_lookup(optlist, "yes") == NULL) { /* too many matches */ g_string_free(str, TRUE); cmd_params_free(free_arg); signal_emit("error command", 1, GINT_TO_POINTER(CMDERR_NOT_GOOD_IDEA)); signal_stop(); return NULL; } } } if (str->len > 0) g_string_truncate(str, str->len-1); ret = str->str; g_string_free(str, FALSE); cmd_params_free(free_arg); *ret_channel = channel; return ret; } /* SYNTAX: OP */ static void cmd_op(const char *data, IRC_SERVER_REC *server, WI_ITEM_REC *item) { IRC_CHANNEL_REC *channel; char *nicks; CMD_IRC_SERVER(server); nicks = get_nicks(server, item, data, 0, -1, &channel); if (nicks != NULL && *nicks != '\0') channel_set_singlemode(channel, nicks, "+o"); g_free_not_null(nicks); } /* SYNTAX: DEOP */ static void cmd_deop(const char *data, IRC_SERVER_REC *server, WI_ITEM_REC *item) { IRC_CHANNEL_REC *channel; char *nicks; CMD_IRC_SERVER(server); nicks = get_nicks(server, item, data, 1, -1, &channel); if (nicks != NULL && *nicks != '\0') channel_set_singlemode(channel, nicks, "-o"); g_free_not_null(nicks); } /* SYNTAX: VOICE */ static void cmd_voice(const char *data, IRC_SERVER_REC *server, WI_ITEM_REC *item) { IRC_CHANNEL_REC *channel; char *nicks; CMD_IRC_SERVER(server); nicks = get_nicks(server, item, data, 0, 0, &channel); if (nicks != NULL && *nicks != '\0') channel_set_singlemode(channel, nicks, "+v"); g_free_not_null(nicks); } /* SYNTAX: DEVOICE */ static void cmd_devoice(const char *data, IRC_SERVER_REC *server, WI_ITEM_REC *item) { IRC_CHANNEL_REC *channel; char *nicks; CMD_IRC_SERVER(server); nicks = get_nicks(server, item, data, -1, 1, &channel); if (nicks != NULL && *nicks != '\0') channel_set_singlemode(channel, nicks, "-v"); g_free_not_null(nicks); } /* SYNTAX: MODE | [ []] */ static void cmd_mode(const char *data, IRC_SERVER_REC *server, IRC_CHANNEL_REC *channel) { IRC_CHANNEL_REC *chanrec; char *target, *mode; void *free_arg; CMD_IRC_SERVER(server); if (*data == '+' || *data == '-') { target = "*"; if (!cmd_get_params(data, &free_arg, 1 | PARAM_FLAG_GETREST, &mode)) return; } else { if (!cmd_get_params(data, &free_arg, 2 | PARAM_FLAG_GETREST, &target, &mode)) return; } if (strcmp(target, "*") == 0) { if (!IS_IRC_CHANNEL(channel)) cmd_param_error(CMDERR_NOT_JOINED); target = channel->name; } if (*target == '\0') cmd_param_error(CMDERR_NOT_ENOUGH_PARAMS); if (*mode == '\0') { chanrec = irc_channel_find(server, target); if (chanrec != NULL) target = chanrec->name; irc_send_cmdv(server, "MODE %s", target); } else if (ischannel(*target)) channel_set_mode(server, target, mode); else { if (g_strcasecmp(target, server->nick) == 0) { server_redirect_event(server, "mode user", 1, target, -1, NULL, "event mode", "requested usermode change", NULL); } irc_send_cmdv(server, "MODE %s %s", target, mode); } cmd_params_free(free_arg); } void modes_server_init(IRC_SERVER_REC *server) { server->modes['b'].func = modes_type_a; server->modes['e'].func = modes_type_a; server->modes['I'].func = modes_type_a; server->modes['h'].func = modes_type_prefix; server->modes['h'].prefix = '%'; server->modes['o'].func = modes_type_prefix; server->modes['o'].prefix = '@'; server->modes['O'].func = modes_type_prefix; server->modes['O'].prefix = '@'; server->modes['v'].func = modes_type_prefix; server->modes['v'].prefix = '+'; server->prefix['%'] = 'h'; server->prefix['@'] = 'o'; server->prefix['+'] = 'v'; server->modes['k'].func = modes_type_b; server->modes['l'].func = modes_type_c; } void modes_init(void) { settings_add_str("misc", "opermode", ""); settings_add_int("misc", "max_wildcard_modes", 6); signal_add("event 221", (SIGNAL_FUNC) event_user_mode); signal_add("event 305", (SIGNAL_FUNC) event_unaway); signal_add("event 306", (SIGNAL_FUNC) event_away); signal_add("event 381", (SIGNAL_FUNC) event_oper); signal_add("event mode", (SIGNAL_FUNC) event_mode); signal_add("requested usermode change", (SIGNAL_FUNC) sig_req_usermode_change); command_bind_irc("op", NULL, (SIGNAL_FUNC) cmd_op); command_bind_irc("deop", NULL, (SIGNAL_FUNC) cmd_deop); command_bind_irc("voice", NULL, (SIGNAL_FUNC) cmd_voice); command_bind_irc("devoice", NULL, (SIGNAL_FUNC) cmd_devoice); command_bind_irc("mode", NULL, (SIGNAL_FUNC) cmd_mode); command_set_options("op", "yes"); } void modes_deinit(void) { signal_remove("event 221", (SIGNAL_FUNC) event_user_mode); signal_remove("event 305", (SIGNAL_FUNC) event_unaway); signal_remove("event 306", (SIGNAL_FUNC) event_away); signal_remove("event 381", (SIGNAL_FUNC) event_oper); signal_remove("event mode", (SIGNAL_FUNC) event_mode); signal_remove("requested usermode change", (SIGNAL_FUNC) sig_req_usermode_change); command_unbind("op", (SIGNAL_FUNC) cmd_op); command_unbind("deop", (SIGNAL_FUNC) cmd_deop); command_unbind("voice", (SIGNAL_FUNC) cmd_voice); command_unbind("devoice", (SIGNAL_FUNC) cmd_devoice); command_unbind("mode", (SIGNAL_FUNC) cmd_mode); }