summaryrefslogtreecommitdiffstats
path: root/src/irc/core/channels-query.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/irc/core/channels-query.c')
-rw-r--r--src/irc/core/channels-query.c674
1 files changed, 674 insertions, 0 deletions
diff --git a/src/irc/core/channels-query.c b/src/irc/core/channels-query.c
new file mode 100644
index 0000000..05c9768
--- /dev/null
+++ b/src/irc/core/channels-query.c
@@ -0,0 +1,674 @@
+/*
+ channels-query.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.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+/*
+
+ How the thing works:
+
+ - After channel is joined and NAMES list is got, send "channel joined" signal
+ - "channel joined" : add channel to server->queries lists
+
+loop:
+ - Wait for NAMES list from all channels before doing anything else..
+ - After got the last NAMES list, start sending the queries ..
+ - find the query to send, check where server->queries list isn't NULL
+ (mode, who, banlist, ban exceptions, invite list)
+ - if not found anything -> all channels are synced
+ - send "command #chan1,#chan2,#chan3,.." command to server
+ - wait for reply from server, then check if it was last query to be sent to
+ channel. If it was, send "channel sync" signal
+ - check if the reply was for last channel in the command list. If so,
+ goto loop
+*/
+
+#include "module.h"
+#include <irssi/src/core/misc.h>
+#include <irssi/src/core/signals.h>
+#include <irssi/src/core/settings.h>
+
+#include <irssi/src/irc/core/modes.h>
+#include <irssi/src/irc/core/mode-lists.h>
+#include <irssi/src/core/nicklist.h>
+#include <irssi/src/irc/core/irc-servers.h>
+#include <irssi/src/irc/core/irc-channels.h>
+#include <irssi/src/irc/core/servers-redirect.h>
+
+/* here are the WHOX commands we send. the full spec can be found on [1].
+
+ (1) WHOX_CHANNEL_FULL_CMD for getting the user list when we join a channel. we request the fields
+ c (channel), u (user), h (host), n (nick), f (flags), d (hops), a (account), and r (the real
+ name goes last because it is the only that can contain spaces.) we request all those fields
+ as they are also included in the "regular" WHO reply we would get without WHOX.
+
+ (2) WHOX_USERACCOUNT_CMD for getting the account names of people that joined. this code is
+ obviously only used when we don't have extended-joins. we request n (nick) and a (account)
+ only, and we only send WHO nick with this command.
+
+ [1] https://github.com/UndernetIRC/ircu2/blob/u2_10_12_branch/doc/readme.who
+ */
+#define WHOX_CHANNEL_FULL_CMD "WHO %s %%tcuhnfdar," WHOX_CHANNEL_FULL_ID
+#define WHOX_USERACCOUNT_CMD "WHO %s %%tna," WHOX_USERACCOUNT_ID
+
+static void sig_connected(IRC_SERVER_REC *server)
+{
+ SERVER_QUERY_REC *rec;
+
+ g_return_if_fail(server != NULL);
+ if (!IS_IRC_SERVER(server))
+ return;
+
+ rec = g_new0(SERVER_QUERY_REC, 1);
+ rec->accountqueries = g_hash_table_new_full(
+ (GHashFunc) i_istr_hash, (GCompareFunc) i_istr_equal, (GDestroyNotify) g_free, NULL);
+ server->chanqueries = rec;
+}
+
+static void sig_disconnected(IRC_SERVER_REC *server)
+{
+ SERVER_QUERY_REC *rec;
+ int n;
+
+ g_return_if_fail(server != NULL);
+ if (!IS_IRC_SERVER(server))
+ return;
+
+ rec = server->chanqueries;
+ if (rec == NULL)
+ return;
+ g_return_if_fail(rec != NULL);
+
+ g_hash_table_destroy(rec->accountqueries);
+ for (n = 0; n < CHANNEL_QUERIES; n++)
+ g_slist_free(rec->queries[n]);
+ g_slist_free(rec->current_queries);
+ g_free(rec);
+
+ server->chanqueries = NULL;
+}
+
+/* Add channel to query list */
+static void query_add_channel(IRC_CHANNEL_REC *channel, int query_type)
+{
+ SERVER_QUERY_REC *rec;
+
+ g_return_if_fail(channel != NULL);
+
+ rec = channel->server->chanqueries;
+ rec->queries[query_type] =
+ g_slist_append(rec->queries[query_type], channel);
+}
+
+static void query_check(IRC_SERVER_REC *server);
+
+static void query_remove_all(IRC_CHANNEL_REC *channel)
+{
+ SERVER_QUERY_REC *rec;
+ int n;
+
+ rec = channel->server->chanqueries;
+ if (rec == NULL) return;
+
+ /* remove channel from query lists */
+ for (n = 0; n < CHANNEL_QUERIES; n++)
+ rec->queries[n] = g_slist_remove(rec->queries[n], channel);
+ rec->current_queries = g_slist_remove(rec->current_queries, channel);
+
+ if (!channel->server->disconnected)
+ query_check(channel->server);
+}
+
+static void sig_channel_destroyed(IRC_CHANNEL_REC *channel)
+{
+ g_return_if_fail(channel != NULL);
+
+ if (IS_IRC_CHANNEL(channel))
+ query_remove_all(channel);
+}
+
+static int channels_have_all_names(IRC_SERVER_REC *server)
+{
+ GSList *tmp;
+
+ for (tmp = server->channels; tmp != NULL; tmp = tmp->next) {
+ IRC_CHANNEL_REC *rec = tmp->data;
+
+ if (IS_IRC_CHANNEL(rec) && !rec->names_got)
+ return 0;
+ }
+
+ return 1;
+}
+
+static int query_find_next(SERVER_QUERY_REC *server)
+{
+ int n;
+
+ for (n = 0; n < CHANNEL_QUERIES; n++) {
+ if (server->queries[n] != NULL)
+ return n;
+ }
+
+ return -1;
+}
+
+static void query_send(IRC_SERVER_REC *server, int query)
+{
+ SERVER_QUERY_REC *rec;
+ IRC_CHANNEL_REC *chanrec;
+ GSList *chans;
+ char *cmd, *chanstr_commas, *chanstr;
+ int onlyone, count;
+
+ rec = server->chanqueries;
+
+ /* get the list of channels to query */
+ onlyone = (server->no_multi_who && query == CHANNEL_QUERY_WHO) ||
+ (server->no_multi_mode && CHANNEL_IS_MODE_QUERY(query));
+
+ if (onlyone) {
+ chans = rec->queries[query];
+ rec->queries[query] =
+ g_slist_remove_link(rec->queries[query], chans);
+
+ chanrec = chans->data;
+ chanstr_commas = g_strdup(chanrec->name);
+ chanstr = g_strdup(chanrec->name);
+ count = 1;
+ } else {
+ char *chanstr_spaces;
+
+ chans = rec->queries[query];
+ count = g_slist_length(chans);
+
+ if (count > server->max_query_chans) {
+ GSList *lastchan;
+
+ lastchan = g_slist_nth(rec->queries[query],
+ server->max_query_chans-1);
+ count = server->max_query_chans;
+ rec->queries[query] = lastchan->next;
+ lastchan->next = NULL;
+ } else {
+ rec->queries[query] = NULL;
+ }
+
+ chanstr_commas = gslistptr_to_string(chans, G_STRUCT_OFFSET(IRC_CHANNEL_REC, name), ",");
+ chanstr_spaces = gslistptr_to_string(chans, G_STRUCT_OFFSET(IRC_CHANNEL_REC, name), " ");
+
+ chanstr = g_strconcat(chanstr_commas, " ", chanstr_spaces, NULL);
+ g_free(chanstr_spaces);
+ }
+
+ rec->current_query_type = query;
+ rec->current_queries = chans;
+
+ switch (query) {
+ case CHANNEL_QUERY_MODE:
+ cmd = g_strdup_printf("MODE %s", chanstr_commas);
+
+ /* the stop-event is received once for each channel,
+ and we want to print 329 event (channel created). */
+ server_redirect_event(server, "mode channel", count,
+ chanstr, -1, "chanquery abort",
+ "event 324", "chanquery mode",
+ "event 329", "event 329",
+ "", "chanquery abort", NULL);
+ break;
+
+ case CHANNEL_QUERY_WHO:
+ if (server->isupport != NULL &&
+ g_hash_table_lookup(server->isupport, "whox") != NULL) {
+ cmd = g_strdup_printf(WHOX_CHANNEL_FULL_CMD, chanstr_commas);
+ } else {
+ cmd = g_strdup_printf("WHO %s", chanstr_commas);
+ }
+
+ server_redirect_event(server, "who", server->one_endofwho ? 1 : count, chanstr, -1,
+ "chanquery abort", /* failure signal */
+ "event 315", "chanquery who end", /* */
+ "event 352", "silent event who", /* */
+ "event 354", "silent event whox", /* */
+ "", "chanquery abort", NULL);
+ break;
+
+ case CHANNEL_QUERY_BMODE:
+ cmd = g_strdup_printf("MODE %s b", chanstr_commas);
+ /* check all the multichannel problems with all
+ mode requests - if channels are joined manually
+ irssi could ask modes separately but afterwards
+ join the two b/e/I modes together */
+ server_redirect_event(server, "mode b", count, chanstr, -1,
+ "chanquery abort",
+ "event 367", "chanquery ban",
+ "event 368", "chanquery ban end",
+ "", "chanquery abort", NULL);
+ break;
+
+ default:
+ cmd = NULL;
+ }
+
+ irc_send_cmd_later(server, cmd);
+
+ g_free(chanstr);
+ g_free(chanstr_commas);
+ g_free(cmd);
+}
+
+static void query_check(IRC_SERVER_REC *server)
+{
+ SERVER_QUERY_REC *rec;
+ int query;
+
+ g_return_if_fail(server != NULL);
+
+ rec = server->chanqueries;
+ if (rec->current_queries != NULL)
+ return; /* old queries haven't been answered yet */
+
+ if (server->max_query_chans > 1 && !server->no_multi_who && !server->no_multi_mode && !channels_have_all_names(server)) {
+ /* all channels haven't sent /NAMES list yet */
+ /* only do this if there would be a benefit in combining
+ * queries -- jilles */
+ return;
+ }
+
+ query = query_find_next(rec);
+ if (query == -1) {
+ /* no queries left */
+ return;
+ }
+
+ query_send(server, query);
+}
+
+/* if there's no more queries in queries in buffer, send the sync signal */
+static void channel_checksync(IRC_CHANNEL_REC *channel)
+{
+ SERVER_QUERY_REC *rec;
+ int n;
+
+ g_return_if_fail(channel != NULL);
+
+ if (channel->synced)
+ return; /* already synced */
+
+ rec = channel->server->chanqueries;
+ for (n = 0; n < CHANNEL_QUERIES; n++) {
+ if (g_slist_find(rec->queries[n], channel))
+ return;
+ }
+
+ channel->synced = TRUE;
+ signal_emit("channel sync", 1, channel);
+}
+
+/* Error occurred when trying to execute query - abort and try again. */
+static void query_current_error(IRC_SERVER_REC *server)
+{
+ SERVER_QUERY_REC *rec;
+ GSList *tmp;
+ int query, abort_query;
+
+ rec = server->chanqueries;
+
+ /* fix the thing that went wrong - or if it was already fixed,
+ then all we can do is abort. */
+ abort_query = FALSE;
+
+ query = rec->current_query_type;
+ if (query == CHANNEL_QUERY_WHO) {
+ if (server->no_multi_who)
+ abort_query = TRUE;
+ else
+ server->no_multi_who = TRUE;
+ } else {
+ if (server->no_multi_mode)
+ abort_query = TRUE;
+ else
+ server->no_multi_mode = TRUE;
+ }
+
+ if (!abort_query) {
+ /* move all currently queried channels to main query lists */
+ for (tmp = rec->current_queries; tmp != NULL; tmp = tmp->next) {
+ rec->queries[query] =
+ g_slist_append(rec->queries[query], tmp->data);
+ }
+ } else {
+ /* check if failed channels are synced after this error */
+ g_slist_foreach(rec->current_queries,
+ (GFunc) channel_checksync, NULL);
+ }
+
+ g_slist_free(rec->current_queries);
+ rec->current_queries = NULL;
+
+ query_check(server);
+}
+
+static void sig_channel_joined(IRC_CHANNEL_REC *channel)
+{
+ if (!IS_IRC_CHANNEL(channel))
+ return;
+
+ if (!settings_get_bool("channel_sync"))
+ return;
+
+ /* Add channel to query lists */
+ if (!channel->no_modes)
+ query_add_channel(channel, CHANNEL_QUERY_MODE);
+ if (g_hash_table_size(channel->nicks) <
+ settings_get_int("channel_max_who_sync"))
+ query_add_channel(channel, CHANNEL_QUERY_WHO);
+ if (!channel->no_modes)
+ query_add_channel(channel, CHANNEL_QUERY_BMODE);
+
+ query_check(channel->server);
+}
+
+static void channel_got_query(IRC_CHANNEL_REC *chanrec, int query_type)
+{
+ SERVER_QUERY_REC *rec;
+
+ g_return_if_fail(chanrec != NULL);
+
+ rec = chanrec->server->chanqueries;
+ if (query_type != rec->current_query_type)
+ return; /* shouldn't happen */
+
+ /* got the query for channel.. */
+ rec->current_queries =
+ g_slist_remove(rec->current_queries, chanrec);
+ channel_checksync(chanrec);
+
+ /* check if we need to send another query.. */
+ query_check(chanrec->server);
+}
+
+void irc_channels_query_purge_accountquery(IRC_SERVER_REC *server, const char *nick)
+{
+ GSList *tmp, *next, *prev;
+ REDIRECT_REC *redirect;
+ char *cmd, *target_cmd;
+ gboolean was_removed;
+
+ /* remove the marker */
+ was_removed = g_hash_table_remove(server->chanqueries->accountqueries, nick);
+
+ /* if it was removed we may have an outstanding query */
+ if (was_removed) {
+ target_cmd = g_strdup_printf(WHOX_USERACCOUNT_CMD "\r\n", nick);
+
+ /* remove queued WHO command */
+ prev = NULL;
+ for (tmp = server->cmdqueue; tmp != NULL; tmp = next) {
+ next = tmp->next->next;
+ cmd = tmp->data;
+ redirect = tmp->next->data;
+
+ if (g_strcmp0(cmd, target_cmd) == 0) {
+ if (prev != NULL)
+ prev->next = next;
+ else
+ server->cmdqueue = next;
+
+ /* remove the redirection */
+ g_slist_free_1(tmp->next);
+ if (redirect != NULL)
+ server_redirect_destroy(redirect);
+
+ /* remove the command */
+ g_slist_free_1(tmp);
+ g_free(cmd);
+
+ server->cmdcount--;
+ server->cmdlater--;
+ } else {
+ prev = tmp->next;
+ }
+ }
+
+ g_free(target_cmd);
+ }
+}
+
+static void query_useraccount_error(IRC_SERVER_REC *server, const char *cmd, const char *arg)
+{
+ /* query failed, ignore it but remove the marker */
+ g_hash_table_remove(server->chanqueries->accountqueries, arg);
+}
+
+static void sig_event_join(IRC_SERVER_REC *server, const char *data, const char *nick,
+ const char *address)
+{
+ char *params, *channel, *ptr, *account;
+ GSList *nicks, *tmp;
+ IRC_CHANNEL_REC *chanrec;
+ NICK_REC *nickrec;
+
+ g_return_if_fail(data != NULL);
+
+ if (i_slist_find_string(server->cap_active, CAP_EXTENDED_JOIN)) {
+ /* no need to chase accounts */
+ return;
+ }
+
+ if (g_ascii_strcasecmp(nick, server->nick) == 0) {
+ /* You joined, do nothing */
+ return;
+ }
+
+ params = event_get_params(data, 3, &channel, NULL, NULL);
+
+ ptr = strchr(channel, 7); /* ^G does something weird.. */
+ if (ptr != NULL)
+ *ptr = '\0';
+
+ /* find channel */
+ chanrec = irc_channel_find(server, channel);
+ if (chanrec == NULL) {
+ g_free(params);
+ return;
+ }
+
+ g_free(params);
+
+ if (!chanrec->wholist) {
+ return;
+ }
+
+ /* find nick */
+ nickrec = nicklist_find(CHANNEL(chanrec), nick);
+ if (nickrec == NULL) {
+ return;
+ }
+
+ if (nickrec->account != NULL) {
+ return;
+ }
+
+ if (g_hash_table_contains(server->chanqueries->accountqueries, nick)) {
+ /* query already sent */
+ return;
+ }
+ account = NULL;
+
+ /* Check if user is already in some other channel, get the account from there */
+ nicks = nicklist_get_same(SERVER(server), nick);
+ for (tmp = nicks; tmp != NULL; tmp = tmp->next->next) {
+ NICK_REC *rec = tmp->next->data;
+
+ if (rec->account != NULL) {
+ account = rec->account;
+ break;
+ }
+ }
+ g_slist_free(nicks);
+
+ if (account != NULL) {
+ nicklist_set_account(CHANNEL(chanrec), nickrec, account);
+ return;
+ }
+
+ if (g_hash_table_size(chanrec->nicks) < settings_get_int("channel_max_who_sync") &&
+ server->isupport != NULL && g_hash_table_lookup(server->isupport, "whox") != NULL &&
+ server->split_servers == NULL &&
+ g_hash_table_size(server->chanqueries->accountqueries) <
+ settings_get_int("account_max_chase")) {
+ char *cmd;
+ server_redirect_event(server, "who user", 1, nick, -1,
+ "chanquery useraccount abort", /* failure signal */
+ "event 354", "silent event whox useraccount", /* */
+ "", "event empty", /* */
+ NULL);
+ cmd = g_strdup_printf(WHOX_USERACCOUNT_CMD, nick);
+ g_hash_table_add(server->chanqueries->accountqueries, g_strdup(nick));
+ /* queue the command */
+ irc_send_cmd_later(server, cmd);
+ g_free(cmd);
+ }
+}
+
+static void event_channel_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, 3 | PARAM_FLAG_GETREST,
+ NULL, &channel, &mode);
+ chanrec = irc_channel_find(server, channel);
+ if (chanrec != NULL) {
+ if (chanrec->key != NULL && strchr(mode, 'k') == NULL) {
+ /* we joined the channel with a key,
+ but it didn't have +k mode.. */
+ parse_channel_modes(chanrec, NULL, "-k", TRUE);
+ }
+ parse_channel_modes(chanrec, nick, mode, FALSE);
+ channel_got_query(chanrec, CHANNEL_QUERY_MODE);
+ }
+
+ g_free(params);
+}
+
+static void event_end_of_who(IRC_SERVER_REC *server, const char *data)
+{
+ SERVER_QUERY_REC *rec;
+ GSList *tmp, *next;
+ char *params, *channel, **channels;
+ int failed, multiple;
+
+ g_return_if_fail(data != NULL);
+
+ params = event_get_params(data, 2, NULL, &channel);
+ multiple = strchr(channel, ',') != NULL;
+ channels = g_strsplit(channel, ",", -1);
+
+ failed = FALSE;
+ rec = server->chanqueries;
+ for (tmp = rec->current_queries; tmp != NULL; tmp = next) {
+ IRC_CHANNEL_REC *chanrec = tmp->data;
+
+ next = tmp->next;
+ if (strarray_find(channels, chanrec->name) == -1)
+ continue;
+
+ if (chanrec->ownnick->host == NULL && multiple &&
+ !server->one_endofwho) {
+ /* we should receive our own host for each channel.
+ However, some servers really are stupid enough
+ not to reply anything to /WHO requests.. */
+ failed = TRUE;
+ } else {
+ chanrec->wholist = TRUE;
+ signal_emit("channel wholist", 1, chanrec);
+ channel_got_query(chanrec, CHANNEL_QUERY_WHO);
+ }
+ }
+
+ g_strfreev(channels);
+ if (multiple)
+ server->one_endofwho = TRUE;
+
+ if (failed) {
+ /* server didn't understand multiple WHO replies,
+ send them again separately */
+ query_current_error(server);
+ }
+
+ g_free(params);
+}
+
+static void event_end_of_banlist(IRC_SERVER_REC *server, const char *data)
+{
+ IRC_CHANNEL_REC *chanrec;
+ char *params, *channel;
+
+ g_return_if_fail(data != NULL);
+
+ params = event_get_params(data, 2, NULL, &channel);
+ chanrec = irc_channel_find(server, channel);
+
+ if (chanrec != NULL)
+ channel_got_query(chanrec, CHANNEL_QUERY_BMODE);
+
+ g_free(params);
+}
+
+void channels_query_init(void)
+{
+ settings_add_bool("misc", "channel_sync", TRUE);
+ settings_add_int("misc", "channel_max_who_sync", 1000);
+ settings_add_int("misc", "account_max_chase", 10);
+
+ signal_add("server connected", (SIGNAL_FUNC) sig_connected);
+ signal_add("server disconnected", (SIGNAL_FUNC) sig_disconnected);
+ signal_add("channel joined", (SIGNAL_FUNC) sig_channel_joined);
+ signal_add("channel destroyed", (SIGNAL_FUNC) sig_channel_destroyed);
+
+ signal_add("event join", (SIGNAL_FUNC) sig_event_join);
+
+ signal_add("chanquery mode", (SIGNAL_FUNC) event_channel_mode);
+ signal_add("chanquery who end", (SIGNAL_FUNC) event_end_of_who);
+
+ signal_add("chanquery ban end", (SIGNAL_FUNC) event_end_of_banlist);
+ signal_add("chanquery abort", (SIGNAL_FUNC) query_current_error);
+ signal_add("chanquery useraccount abort", (SIGNAL_FUNC) query_useraccount_error);
+}
+
+void channels_query_deinit(void)
+{
+ signal_remove("server connected", (SIGNAL_FUNC) sig_connected);
+ signal_remove("server disconnected", (SIGNAL_FUNC) sig_disconnected);
+ signal_remove("channel joined", (SIGNAL_FUNC) sig_channel_joined);
+ signal_remove("channel destroyed", (SIGNAL_FUNC) sig_channel_destroyed);
+
+ signal_remove("event join", (SIGNAL_FUNC) sig_event_join);
+
+ signal_remove("chanquery mode", (SIGNAL_FUNC) event_channel_mode);
+ signal_remove("chanquery who end", (SIGNAL_FUNC) event_end_of_who);
+
+ signal_remove("chanquery ban end", (SIGNAL_FUNC) event_end_of_banlist);
+ signal_remove("chanquery abort", (SIGNAL_FUNC) query_current_error);
+ signal_remove("chanquery useraccount abort", (SIGNAL_FUNC) query_useraccount_error);
+}