summaryrefslogtreecommitdiff
path: root/src/irc/core/irc-cap.c
blob: c5bf4e674f42988f85ff557e213069bd02616fc5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#include "module.h"
#include "signals.h"
#include "misc.h"

#include "irc-cap.h"
#include "irc-servers.h"

int cap_toggle (IRC_SERVER_REC *server, char *cap, int enable)
{
	if (cap == NULL || *cap == '\0')
		return FALSE;

	/* If the negotiation hasn't been completed yet just queue the requests */
	if (!server->cap_complete) {
		if (enable && !gslist_find_string(server->cap_queue, cap)) {
			server->cap_queue = g_slist_prepend(server->cap_queue, g_strdup(cap));
			return TRUE;
		}
		else if (!enable && gslist_find_string(server->cap_queue, cap)) {
			server->cap_queue = gslist_remove_string(server->cap_queue, cap);
			return TRUE;
		}

		return FALSE;
	}

	if (enable && !gslist_find_string(server->cap_active, cap)) {
		/* Make sure the required cap is supported by the server */
		if (!gslist_find_string(server->cap_supported, cap))
			return FALSE;

		irc_send_cmdv(server, "CAP REQ %s", cap);
		return TRUE;
	}
	else if (!enable && gslist_find_string(server->cap_active, cap)) {
		irc_send_cmdv(server, "CAP REQ -%s", cap);
		return TRUE;
	}

	return FALSE;
}

void cap_finish_negotiation (IRC_SERVER_REC *server)
{
	if (server->cap_complete)
		return;

	server->cap_complete = TRUE;
	irc_send_cmd_now(server, "CAP END");

	signal_emit("server cap end", 1, server);
}

static void cap_emit_signal (IRC_SERVER_REC *server, char *cmd, char *args)
{
	char *signal_name;

	signal_name = g_strdup_printf("server cap %s %s", cmd, args? args: "");
	signal_emit(signal_name, 1, server);
	g_free(signal_name);
}

static void event_cap (IRC_SERVER_REC *server, char *args, char *nick, char *address)
{
	GSList *tmp;
	GString *cmd;
	char *params, *evt, *list, **caps;
	int i, caps_length, disable, avail_caps;

	params = event_get_params(args, 3, NULL, &evt, &list);
	if (params == NULL)
		return;

	/* Strip the trailing whitespaces before splitting the string, some servers send responses with
	 * superfluous whitespaces that g_strsplit the interprets as tokens */
	caps = g_strsplit(g_strchomp(list), " ", -1);
	caps_length = g_strv_length(caps);

	if (!g_strcmp0(evt, "LS")) {
		/* Create a list of the supported caps */
		for (i = 0; i < caps_length; i++)
			server->cap_supported = g_slist_prepend(server->cap_supported, g_strdup(caps[i]));

		/* Request the required caps, if any */
		if (server->cap_queue == NULL) {
			cap_finish_negotiation(server);
		}
		else {
			cmd = g_string_new("CAP REQ :");

			avail_caps = 0;

			/* Check whether the cap is supported by the server */
			for (tmp = server->cap_queue; tmp != NULL; tmp = tmp->next) {
				if (gslist_find_string(server->cap_supported, tmp->data)) {
					g_string_append_c(cmd, ' ');
					g_string_append(cmd, tmp->data);

					avail_caps++;
				}
			}

			/* Clear the queue here */
			gslist_free_full(server->cap_queue, (GDestroyNotify) g_free);
			server->cap_queue = NULL;

			/* If the server doesn't support any cap we requested close the negotiation here */
			if (avail_caps > 0)
				irc_send_cmd_now(server, cmd->str);
			else
				cap_finish_negotiation(server);

			g_string_free(cmd, TRUE);
		}
	}
	else if (!g_strcmp0(evt, "ACK")) {
		int got_sasl = FALSE;

		/* Emit a signal for every ack'd cap */
		for (i = 0; i < caps_length; i++) {
			disable = (*caps[i] == '-');

			if (disable)
				server->cap_active = gslist_remove_string(server->cap_active, caps[i] + 1);
			else
				server->cap_active = g_slist_prepend(server->cap_active, g_strdup(caps[i]));

			if (!g_strcmp0(caps[i], "sasl"))
				got_sasl = TRUE;

			cap_emit_signal(server, "ack", caps[i]);
		}

		/* Hopefully the server has ack'd all the caps requested and we're ready to terminate the
		 * negotiation, unless sasl was requested. In this case we must not terminate the negotiation
		 * until the sasl handshake is over. */
		if (got_sasl == FALSE)
			cap_finish_negotiation(server);
	}
	else if (!g_strcmp0(evt, "NAK")) {
		g_warning("The server answered with a NAK to our CAP request, this should not happen");

		/* A NAK'd request means that a required cap can't be enabled or disabled, don't update the
		 * list of active caps and notify the listeners. */
		for (i = 0; i < caps_length; i++)
			cap_emit_signal(server, "nak", caps[i]);
	}

	g_strfreev(caps);
	g_free(params);
}

static void event_invalid_cap (IRC_SERVER_REC *server, const char *data, const char *from)
{
	/* The server didn't understand one (or more) requested caps, terminate the negotiation.
	 * This could be handled in a graceful way but since it shouldn't really ever happen this seems a
	 * good way to deal with 410 errors. */
	server->cap_complete = FALSE;
	irc_send_cmd_now(server, "CAP END");
}

void cap_init (void)
{
	signal_add_first("event cap", (SIGNAL_FUNC) event_cap);
	signal_add_first("event 410", (SIGNAL_FUNC) event_invalid_cap);
}

void cap_deinit (void)
{
	signal_remove("event cap", (SIGNAL_FUNC) event_cap);
	signal_remove("event 410", (SIGNAL_FUNC) event_invalid_cap);
}