1 /*
2 * Hangouts Plugin for libpurple/Pidgin
3 * Copyright (c) 2015-2016 Eion Robb, Mike Ruprecht
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #define PURPLE_PLUGINS
20
21
22 #include "hangouts_connection.h"
23
24
25 #include <stdlib.h>
26 #include <string.h>
27 #include <stdio.h>
28 #include <unistd.h>
29
30 #include "debug.h"
31 #include "request.h"
32
33 #include "hangouts_pblite.h"
34 #include "hangouts_json.h"
35 #include "hangouts.pb-c.h"
36 #include "hangouts_conversation.h"
37
38 #include "gmail.pb-c.h"
39
40 void
hangouts_process_data_chunks(HangoutsAccount * ha,const gchar * data,gsize len)41 hangouts_process_data_chunks(HangoutsAccount *ha, const gchar *data, gsize len)
42 {
43 JsonArray *chunks;
44 guint i, num_chunks;
45
46 chunks = json_decode_array(data, len);
47
48 for (i = 0, num_chunks = json_array_get_length(chunks); i < num_chunks; i++) {
49 JsonArray *chunk;
50 JsonArray *array;
51 JsonNode *array0;
52
53 chunk = json_array_get_array_element(chunks, i);
54
55 array = json_array_get_array_element(chunk, 1);
56 array0 = json_array_get_element(array, 0);
57 if (JSON_NODE_HOLDS_VALUE(array0)) {
58 //probably a nooooop
59 if (g_strcmp0(json_node_get_string(array0), "noop") == 0) {
60 //A nope ninja delivers a wicked dragon kick
61 #ifdef DEBUG
62 printf("noop\n");
63 #endif
64 }
65 } else {
66 const gchar *p = json_object_get_string_member(json_node_get_object(array0), "p");
67 JsonObject *wrapper = json_decode_object(p, -1);
68
69 if (wrapper == NULL) {
70 continue;
71 }
72
73 if (json_object_has_member(wrapper, "3")) {
74 const gchar *new_client_id = json_object_get_string_member(json_object_get_object_member(wrapper, "3"), "2");
75 purple_debug_info("hangouts", "Received new client_id: %s\n", new_client_id);
76
77 g_free(ha->client_id);
78 ha->client_id = g_strdup(new_client_id);
79
80 hangouts_add_channel_services(ha);
81 hangouts_set_active_client(ha->pc);
82 hangouts_set_status(ha->account, purple_account_get_active_status(ha->account));
83 }
84 if (json_object_has_member(wrapper, "2")) {
85 const gchar *wrapper22 = json_object_get_string_member(json_object_get_object_member(wrapper, "2"), "2");
86 JsonArray *pblite_message = json_decode_array(wrapper22, -1);
87 const gchar *message_type;
88
89 if (pblite_message == NULL) {
90 #ifdef DEBUG
91 printf("bad wrapper22 %s\n", wrapper22);
92 #endif
93 json_object_unref(wrapper);
94 continue;
95 }
96
97 message_type = json_array_get_string_element(pblite_message, 0);
98
99 //cbu == ClientBatchUpdate
100 if (purple_strequal(message_type, "cbu")) {
101 BatchUpdate batch_update = BATCH_UPDATE__INIT;
102 guint j;
103
104 #ifdef DEBUG
105 printf("----------------------\n");
106 #endif
107 pblite_decode((ProtobufCMessage *) &batch_update, pblite_message, TRUE);
108 #ifdef DEBUG
109 printf("======================\n");
110 printf("Is valid? %s\n", protobuf_c_message_check((ProtobufCMessage *) &batch_update) ? "Yes" : "No");
111 printf("======================\n");
112 printf("CBU %s", pblite_dump_json((ProtobufCMessage *)&batch_update));
113 JsonArray *debug = pblite_encode((ProtobufCMessage *) &batch_update);
114 JsonNode *node = json_node_new(JSON_NODE_ARRAY);
115 json_node_take_array(node, debug);
116 gchar *json = json_encode(node, NULL);
117 printf("Old: %s\nNew: %s\n", wrapper22, json);
118
119
120 pblite_decode((ProtobufCMessage *) &batch_update, debug, TRUE);
121 debug = pblite_encode((ProtobufCMessage *) &batch_update);
122 json_node_take_array(node, debug);
123 gchar *json2 = json_encode(node, NULL);
124 printf("Mine1: %s\nMine2: %s\n", json, json2);
125
126 g_free(json);
127 g_free(json2);
128 printf("----------------------\n");
129 #endif
130 for(j = 0; j < batch_update.n_state_update; j++) {
131 purple_signal_emit(purple_connection_get_protocol(ha->pc), "hangouts-received-stateupdate", ha->pc, batch_update.state_update[j]);
132 }
133 } else if (purple_strequal(message_type, "n_nm")) {
134 GmailNotification gmail_notification = GMAIL_NOTIFICATION__INIT;
135 const gchar *username = json_object_get_string_member(json_object_get_object_member(json_object_get_object_member(wrapper, "2"), "1"), "2");
136
137 pblite_decode((ProtobufCMessage *) &gmail_notification, pblite_message, TRUE);
138 purple_signal_emit(purple_connection_get_protocol(ha->pc), "hangouts-gmail-notification", ha->pc, username, &gmail_notification);
139 }
140
141 json_array_unref(pblite_message);
142 }
143
144 json_object_unref(wrapper);
145 }
146 }
147
148 json_array_unref(chunks);
149 }
150
151 static int
read_all(int fd,void * buf,size_t len)152 read_all(int fd, void *buf, size_t len)
153 {
154 unsigned int rs = 0;
155 while(rs < len)
156 {
157 int rval = read(fd, buf + rs, len - rs);
158 if (rval == 0)
159 break;
160 if (rval < 0)
161 return rval;
162
163 rs += rval;
164 }
165 return rs;
166 }
167
168 void
hangouts_process_channel(int fd)169 hangouts_process_channel(int fd)
170 {
171 gsize len, lenpos = 0;
172 gchar len_str[256];
173 gchar *chunk;
174
175 while(read(fd, len_str + lenpos, 1) > 0) {
176 //read up until \n
177 if (len_str[lenpos] == '\n') {
178 //convert to int, use as length of string to read
179 len_str[lenpos] = '\0';
180 #ifdef DEBUG
181 printf("len_str is %s\n", len_str);
182 #endif
183 len = atoi(len_str);
184
185 chunk = g_new(gchar, len * 2);
186 //XX - could be a utf-16 length*2 though, so read up until \n????
187
188 if (read_all(fd, chunk, len) > 0) {
189 //throw chunk to hangouts_process_data_chunks
190 hangouts_process_data_chunks(NULL, chunk, len);
191 }
192
193 g_free(chunk);
194
195 lenpos = 0;
196 } else {
197 lenpos = lenpos + 1;
198 }
199 }
200 }
201
202 void
hangouts_process_channel_buffer(HangoutsAccount * ha)203 hangouts_process_channel_buffer(HangoutsAccount *ha)
204 {
205 const gchar *bufdata;
206 gsize bufsize;
207 gchar *len_end;
208 gchar *len_str;
209 guint len_len; //len len len len len len len len len
210 gsize len;
211
212 g_return_if_fail(ha);
213 g_return_if_fail(ha->channel_buffer);
214
215 while (ha->channel_buffer->len) {
216 bufdata = (gchar *) ha->channel_buffer->data;
217 bufsize = ha->channel_buffer->len;
218
219 len_end = g_strstr_len(bufdata, bufsize, "\n");
220 if (len_end == NULL) {
221 // Not enough data to read
222 if (purple_debug_is_verbose()) {
223 purple_debug_info("hangouts", "Couldn't find length of chunk\n");
224 }
225 return;
226 }
227 len_len = len_end - bufdata;
228 len_str = g_strndup(bufdata, len_len);
229 len = (gsize) atoi(len_str);
230 g_free(len_str);
231
232 // Len was 0 ? Must have been a bad read :(
233 g_return_if_fail(len);
234
235 bufsize = bufsize - len_len - 1;
236
237 if (len > bufsize) {
238 // Not enough data to read
239 if (purple_debug_is_verbose()) {
240 purple_debug_info("hangouts", "Couldn't read %" G_GSIZE_FORMAT " bytes when we only have %" G_GSIZE_FORMAT "\n", len, bufsize);
241 }
242 return;
243 }
244
245 hangouts_process_data_chunks(ha, bufdata + len_len + 1, len);
246
247 g_byte_array_remove_range(ha->channel_buffer, 0, len + len_len + 1);
248
249 }
250 }
251
252 static void
hangouts_set_auth_headers(HangoutsAccount * ha,PurpleHttpRequest * request)253 hangouts_set_auth_headers(HangoutsAccount *ha, PurpleHttpRequest *request)
254 {
255 gint64 mstime;
256 gchar *mstime_str;
257 GTimeVal time;
258 GChecksum *hash;
259 const gchar *sha1;
260 gchar *sapisid_cookie;
261
262 g_get_current_time(&time);
263 mstime = (((gint64) time.tv_sec) * 1000) + (time.tv_usec / 1000);
264 mstime_str = g_strdup_printf("%" G_GINT64_FORMAT, mstime);
265 sapisid_cookie = purple_http_cookie_jar_get(ha->cookie_jar, "SAPISID");
266
267 hash = g_checksum_new(G_CHECKSUM_SHA1);
268 g_checksum_update(hash, (guchar *) mstime_str, strlen(mstime_str));
269 g_checksum_update(hash, (guchar *) " ", 1);
270 if (sapisid_cookie && *sapisid_cookie) {
271 // Should we just bail out if we dont have the cookie?
272 g_checksum_update(hash, (guchar *) sapisid_cookie, strlen(sapisid_cookie));
273 }
274 g_checksum_update(hash, (guchar *) " ", 1);
275 g_checksum_update(hash, (guchar *) HANGOUTS_PBLITE_XORIGIN_URL, strlen(HANGOUTS_PBLITE_XORIGIN_URL));
276 sha1 = g_checksum_get_string(hash);
277
278 purple_http_request_header_set_printf(request, "Authorization", "SAPISIDHASH %s_%s", mstime_str, sha1);
279 purple_http_request_header_set(request, "X-Origin", HANGOUTS_PBLITE_XORIGIN_URL);
280 purple_http_request_header_set(request, "X-Goog-AuthUser", "0");
281
282 g_free(sapisid_cookie);
283 g_free(mstime_str);
284 g_checksum_free(hash);
285 }
286
287
288 static gboolean
hangouts_longpoll_request_content(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,const gchar * buffer,size_t offset,size_t length,gpointer user_data)289 hangouts_longpoll_request_content(PurpleHttpConnection *http_conn, PurpleHttpResponse *response, const gchar *buffer, size_t offset, size_t length, gpointer user_data)
290 {
291 HangoutsAccount *ha = user_data;
292
293 ha->last_data_received = time(NULL);
294
295 if (!purple_http_response_is_successful(response)) {
296 purple_debug_error("hangouts", "longpoll_request_content had error: '%s'\n", purple_http_response_get_error(response));
297 return FALSE;
298 }
299
300 g_byte_array_append(ha->channel_buffer, (guint8 *) buffer, length);
301
302 hangouts_process_channel_buffer(ha);
303
304 return TRUE;
305 }
306
307 static void
hangouts_longpoll_request_closed(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,gpointer user_data)308 hangouts_longpoll_request_closed(PurpleHttpConnection *http_conn, PurpleHttpResponse *response, gpointer user_data)
309 {
310 HangoutsAccount *ha = user_data;
311
312 if (!PURPLE_IS_CONNECTION(purple_http_conn_get_purple_connection(http_conn))) {
313 return;
314 }
315
316 if (ha->channel_watchdog) {
317 g_source_remove(ha->channel_watchdog);
318 ha->channel_watchdog = 0;
319 }
320
321 // remaining data 'should' have been dealt with in hangouts_longpoll_request_content
322 g_byte_array_free(ha->channel_buffer, TRUE);
323 ha->channel_buffer = g_byte_array_sized_new(HANGOUTS_BUFFER_DEFAULT_SIZE);
324
325 if (purple_http_response_get_error(response) != NULL) {
326 //TODO error checking
327 purple_debug_error("hangouts", "longpoll_request_closed %d %s\n", purple_http_response_get_code(response), purple_http_response_get_error(response));
328 hangouts_fetch_channel_sid(ha);
329 } else {
330 hangouts_longpoll_request(ha);
331 }
332 }
333
334 static gboolean
channel_watchdog_check(gpointer data)335 channel_watchdog_check(gpointer data)
336 {
337 PurpleConnection *pc = data;
338 HangoutsAccount *ha;
339 PurpleHttpConnection *conn;
340
341 if (PURPLE_IS_CONNECTION(pc)) {
342 ha = purple_connection_get_protocol_data(pc);
343 conn = ha->channel_connection;
344
345 if (ha->last_data_received && ha->last_data_received < (time(NULL) - 60)) {
346 // should have been something within the last 60 seconds
347 purple_http_conn_cancel(conn);
348 ha->last_data_received = 0;
349 }
350
351 if (!purple_http_conn_is_running(conn)) {
352 hangouts_longpoll_request(ha);
353 }
354
355 return TRUE;
356 }
357
358 return FALSE;
359 }
360
361 void
hangouts_longpoll_request(HangoutsAccount * ha)362 hangouts_longpoll_request(HangoutsAccount *ha)
363 {
364 PurpleHttpRequest *request;
365 GString *url;
366
367
368 url = g_string_new(HANGOUTS_CHANNEL_URL_PREFIX "channel/bind" "?");
369 g_string_append(url, "VER=8&"); // channel protocol version
370 g_string_append_printf(url, "gsessionid=%s&", purple_url_encode(ha->gsessionid_param));
371 g_string_append(url, "RID=rpc&"); // request identifier
372 g_string_append(url, "t=1&"); // trial
373 g_string_append_printf(url, "SID=%s&", purple_url_encode(ha->sid_param)); // session ID
374 g_string_append(url, "CI=0&"); // 0 if streaming/chunked requests should be used
375 g_string_append(url, "ctype=hangouts&"); // client type
376 g_string_append(url, "TYPE=xmlhttp&"); // type of request
377
378 request = purple_http_request_new(NULL);
379 purple_http_request_set_cookie_jar(request, ha->cookie_jar);
380 purple_http_request_set_url(request, url->str);
381 purple_http_request_set_timeout(request, -1); // to infinity and beyond!
382 purple_http_request_set_response_writer(request, hangouts_longpoll_request_content, ha);
383 purple_http_request_set_keepalive_pool(request, ha->channel_keepalive_pool);
384
385 hangouts_set_auth_headers(ha, request);
386
387 ha->channel_connection = purple_http_request(ha->pc, request, hangouts_longpoll_request_closed, ha);
388
389 g_string_free(url, TRUE);
390
391 if (ha->channel_watchdog) {
392 g_source_remove(ha->channel_watchdog);
393 }
394 ha->channel_watchdog = g_timeout_add_seconds(1, channel_watchdog_check, ha->pc);
395 }
396
397
398
399 static void
hangouts_send_maps_cb(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,gpointer user_data)400 hangouts_send_maps_cb(PurpleHttpConnection *http_conn, PurpleHttpResponse *response, gpointer user_data)
401 {
402 /*111
403 * [
404 * [0,["c","<sid>","",8]],
405 * [1,[{"gsid":"<gsid>"}]]
406 * ]
407 */
408 JsonNode *node;
409 HangoutsAccount *ha = user_data;
410 const gchar *res_raw;
411 gchar *json_start;
412 size_t res_len;
413 gchar *gsid;
414 gchar *sid;
415
416 if (purple_http_response_get_error(response) != NULL) {
417 purple_connection_error(ha->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, purple_http_response_get_error(response));
418 return;
419 }
420
421 res_raw = purple_http_response_get_data(response, &res_len);
422 json_start = g_strstr_len(res_raw, res_len, "\n");
423 if (json_start == NULL) {
424 purple_connection_error(ha->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, "Blank maps response");
425 return;
426 }
427 *json_start = '\0';
428 json_start++;
429 node = json_decode(json_start, atoi(res_raw));
430 sid = hangouts_json_path_query_string(node, "$[0][1][1]", NULL);
431 gsid = hangouts_json_path_query_string(node, "$[1][1][0].gsid", NULL);
432
433 if (sid != NULL) {
434 g_free(ha->sid_param);
435 ha->sid_param = sid;
436 }
437 if (gsid != NULL) {
438 g_free(ha->gsessionid_param);
439 ha->gsessionid_param = gsid;
440 }
441
442 json_node_free(node);
443
444 hangouts_longpoll_request(ha);
445 }
446
447 void
hangouts_fetch_channel_sid(HangoutsAccount * ha)448 hangouts_fetch_channel_sid(HangoutsAccount *ha)
449 {
450 g_free(ha->sid_param);
451 g_free(ha->gsessionid_param);
452 ha->sid_param = NULL;
453 ha->gsessionid_param = NULL;
454
455 hangouts_send_maps(ha, NULL, hangouts_send_maps_cb);
456 }
457
458 void
hangouts_send_maps(HangoutsAccount * ha,JsonArray * map_list,PurpleHttpCallback send_maps_callback)459 hangouts_send_maps(HangoutsAccount *ha, JsonArray *map_list, PurpleHttpCallback send_maps_callback)
460 {
461 PurpleHttpRequest *request;
462 GString *url, *postdata;
463 guint map_list_len, i;
464
465 url = g_string_new(HANGOUTS_CHANNEL_URL_PREFIX "channel/bind" "?");
466 g_string_append(url, "VER=8&"); // channel protocol version
467 g_string_append(url, "RID=81188&"); // request identifier
468 g_string_append(url, "ctype=hangouts&"); // client type
469 if (ha->gsessionid_param)
470 g_string_append_printf(url, "gsessionid=%s&", purple_url_encode(ha->gsessionid_param));
471 if (ha->sid_param)
472 g_string_append_printf(url, "SID=%s&", purple_url_encode(ha->sid_param)); // session ID
473
474 request = purple_http_request_new(NULL);
475 purple_http_request_set_cookie_jar(request, ha->cookie_jar);
476 purple_http_request_set_url(request, url->str);
477 purple_http_request_set_method(request, "POST");
478 purple_http_request_header_set(request, "Content-Type", "application/x-www-form-urlencoded");
479
480 hangouts_set_auth_headers(ha, request);
481
482 postdata = g_string_new(NULL);
483 if (map_list != NULL) {
484 map_list_len = json_array_get_length(map_list);
485 g_string_append_printf(postdata, "count=%u&", map_list_len);
486 g_string_append(postdata, "ofs=0&");
487 for(i = 0; i < map_list_len; i++) {
488 JsonObject *obj = json_array_get_object_element(map_list, i);
489 GList *members = json_object_get_members(obj);
490 GList *l;
491
492 for (l = members; l != NULL; l = l->next) {
493 const gchar *member_name = l->data;
494 JsonNode *value = json_object_get_member(obj, member_name);
495
496 g_string_append_printf(postdata, "req%u_%s=", i, purple_url_encode(member_name));
497 g_string_append_printf(postdata, "%s&", purple_url_encode(json_node_get_string(value)));
498 }
499
500 g_list_free(members);
501 }
502 }
503 purple_http_request_set_contents(request, postdata->str, postdata->len);
504
505 purple_http_request(ha->pc, request, send_maps_callback, ha);
506 purple_http_request_unref(request);
507
508 g_string_free(postdata, TRUE);
509 g_string_free(url, TRUE);
510 }
511
512 void
hangouts_add_channel_services(HangoutsAccount * ha)513 hangouts_add_channel_services(HangoutsAccount *ha)
514 {
515 JsonArray *map_list = json_array_new();
516 JsonObject *obj;
517
518 // TODO Work out what this is for
519 obj = json_object_new();
520 json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"tango_service\"}}}");
521 json_array_add_object_element(map_list, obj);
522
523 // This is for the chat messages
524 obj = json_object_new();
525 json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"babel\"}}}");
526 json_array_add_object_element(map_list, obj);
527
528 // This is for the presence updates
529 obj = json_object_new();
530 json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"babel_presence_last_seen\"}}}");
531 json_array_add_object_element(map_list, obj);
532
533 // TODO Work out what this is for
534 obj = json_object_new();
535 json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"hangout_invite\"}}}");
536 json_array_add_object_element(map_list, obj);
537
538 obj = json_object_new();
539 json_object_set_string_member(obj, "p", "{\"3\":{\"1\":{\"1\":\"gmail\"}}}");
540 json_array_add_object_element(map_list, obj);
541
542 hangouts_send_maps(ha, map_list, NULL);
543
544 json_array_unref(map_list);
545 }
546
547
548 typedef struct {
549 HangoutsAccount *ha;
550 HangoutsPbliteResponseFunc callback;
551 ProtobufCMessage *response_message;
552 gpointer user_data;
553 } LazyPblistRequestStore;
554
555 static void
hangouts_pblite_request_cb(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,gpointer user_data)556 hangouts_pblite_request_cb(PurpleHttpConnection *http_conn, PurpleHttpResponse *response, gpointer user_data)
557 {
558 LazyPblistRequestStore *request_info = user_data;
559 HangoutsAccount *ha = request_info->ha;
560 HangoutsPbliteResponseFunc callback = request_info->callback;
561 gpointer real_user_data = request_info->user_data;
562 ProtobufCMessage *response_message = request_info->response_message;
563 ProtobufCMessage *unpacked_message;
564 const gchar *raw_response;
565 guchar *decoded_response;
566 gsize response_len;
567 const gchar *content_type;
568
569 if (purple_http_response_get_error(response) != NULL) {
570 g_free(request_info);
571 g_free(response_message);
572 purple_debug_error("hangouts", "Error from server: (%s) %s\n", purple_http_response_get_error(response), purple_http_response_get_data(response, NULL));
573 return; //TODO should we send NULL to the callee?
574 }
575
576 if (callback != NULL) {
577 raw_response = purple_http_response_get_data(response, NULL);
578
579 content_type = purple_http_response_get_header(response, "X-Goog-Safety-Content-Type");
580 if (g_strcmp0(content_type, "application/x-protobuf") == 0) {
581 decoded_response = g_base64_decode(raw_response, &response_len);
582 unpacked_message = protobuf_c_message_unpack(response_message->descriptor, NULL, response_len, decoded_response);
583
584 if (unpacked_message != NULL) {
585 if (purple_debug_is_verbose()) {
586 gchar *pretty_json = pblite_dump_json(unpacked_message);
587 purple_debug_misc("hangouts", "Response: %s", pretty_json);
588 g_free(pretty_json);
589 }
590
591 callback(ha, unpacked_message, real_user_data);
592 protobuf_c_message_free_unpacked(unpacked_message, NULL);
593 } else {
594 purple_debug_error("hangouts", "Error decoding protobuf!\n");
595 }
596 } else {
597 gchar *tidied_json = hangouts_json_tidy_blank_arrays(raw_response);
598 JsonArray *response_array = json_decode_array(tidied_json, -1);
599 const gchar *first_element = json_array_get_string_element(response_array, 0);
600 gboolean ignore_first_element = (first_element != NULL);
601
602 pblite_decode(response_message, response_array, ignore_first_element);
603 if (ignore_first_element) {
604 purple_debug_info("hangouts", "A '%s' says '%s'\n", response_message->descriptor->name, first_element);
605 }
606
607 if (purple_debug_is_verbose()) {
608 gchar *pretty_json = pblite_dump_json(response_message);
609 purple_debug_misc("hangouts", "Response: %s", pretty_json);
610 g_free(pretty_json);
611 }
612
613 callback(ha, response_message, real_user_data);
614
615 json_array_unref(response_array);
616 g_free(tidied_json);
617 }
618 }
619
620 g_free(request_info);
621 g_free(response_message);
622 }
623
624 PurpleHttpConnection *
hangouts_client6_request(HangoutsAccount * ha,const gchar * path,HangoutsContentType request_type,const gchar * request_data,gssize request_len,HangoutsContentType response_type,PurpleHttpCallback callback,gpointer user_data)625 hangouts_client6_request(HangoutsAccount *ha, const gchar *path, HangoutsContentType request_type, const gchar *request_data, gssize request_len, HangoutsContentType response_type, PurpleHttpCallback callback, gpointer user_data)
626 {
627 PurpleHttpRequest *request;
628 PurpleHttpConnection *connection;
629 const gchar *response_type_str;
630
631 switch (response_type) {
632 default:
633 case HANGOUTS_CONTENT_TYPE_NONE:
634 case HANGOUTS_CONTENT_TYPE_JSON:
635 response_type_str = "json";
636 break;
637 case HANGOUTS_CONTENT_TYPE_PBLITE:
638 response_type_str = "protojson";
639 break;
640 case HANGOUTS_CONTENT_TYPE_PROTOBUF:
641 response_type_str = "proto";
642 break;
643 }
644
645 request = purple_http_request_new(NULL);
646 purple_http_request_set_url_printf(request, HANGOUTS_PBLITE_API_URL "%s%ckey=" GOOGLE_GPLUS_KEY "&alt=%s", path, (strchr(path, '?') ? '&' : '?'), response_type_str);
647 purple_http_request_set_cookie_jar(request, ha->cookie_jar);
648 purple_http_request_set_keepalive_pool(request, ha->client6_keepalive_pool);
649 purple_http_request_set_max_len(request, G_MAXINT32 - 1);
650
651 purple_http_request_header_set(request, "X-Goog-Encode-Response-If-Executable", "base64");
652 if (request_type != HANGOUTS_CONTENT_TYPE_NONE) {
653 purple_http_request_set_method(request, "POST");
654 purple_http_request_set_contents(request, request_data, request_len);
655 if (request_type == HANGOUTS_CONTENT_TYPE_PROTOBUF) {
656 purple_http_request_header_set(request, "Content-Type", "application/x-protobuf");
657 } else if (request_type == HANGOUTS_CONTENT_TYPE_PBLITE) {
658 purple_http_request_header_set(request, "Content-Type", "application/json+protobuf");
659 } else if (request_type == HANGOUTS_CONTENT_TYPE_JSON) {
660 purple_http_request_header_set(request, "Content-Type", "application/json");
661 }
662 }
663
664 hangouts_set_auth_headers(ha, request);
665 connection = purple_http_request(ha->pc, request, callback, user_data);
666 purple_http_request_unref(request);
667
668 return connection;
669 }
670
671 void
hangouts_pblite_request(HangoutsAccount * ha,const gchar * endpoint,ProtobufCMessage * request_message,HangoutsPbliteResponseFunc callback,ProtobufCMessage * response_message,gpointer user_data)672 hangouts_pblite_request(HangoutsAccount *ha, const gchar *endpoint, ProtobufCMessage *request_message, HangoutsPbliteResponseFunc callback, ProtobufCMessage *response_message, gpointer user_data)
673 {
674 gsize request_len;
675 gchar *request_data;
676 LazyPblistRequestStore *request_info = g_new0(LazyPblistRequestStore, 1);
677
678 JsonArray *request_encoded = pblite_encode(request_message);
679 JsonNode *node = json_node_new(JSON_NODE_ARRAY);
680 json_node_take_array(node, request_encoded);
681 request_data = json_encode(node, &request_len);
682 json_node_free(node);
683
684 request_info->ha = ha;
685 request_info->callback = callback;
686 request_info->response_message = response_message;
687 request_info->user_data = user_data;
688
689 if (purple_debug_is_verbose()) {
690 gchar *pretty_json = pblite_dump_json(request_message);
691 purple_debug_misc("hangouts", "Request: %s", pretty_json);
692 g_free(pretty_json);
693 }
694
695 hangouts_client6_request(ha, endpoint, HANGOUTS_CONTENT_TYPE_PBLITE, request_data, request_len, HANGOUTS_CONTENT_TYPE_PBLITE, hangouts_pblite_request_cb, request_info);
696
697 g_free(request_data);
698 }
699
700
701 void
hangouts_default_response_dump(HangoutsAccount * ha,ProtobufCMessage * response,gpointer user_data)702 hangouts_default_response_dump(HangoutsAccount *ha, ProtobufCMessage *response, gpointer user_data)
703 {
704 gchar *dump = pblite_dump_json(response);
705 purple_debug_info("hangouts", "%s\n", dump);
706 g_free(dump);
707 }
708
709 gboolean
hangouts_set_active_client(PurpleConnection * pc)710 hangouts_set_active_client(PurpleConnection *pc)
711 {
712 HangoutsAccount *ha;
713 SetActiveClientRequest request;
714
715 switch(purple_connection_get_state(pc)) {
716 case PURPLE_CONNECTION_DISCONNECTED:
717 // I couldn't eat another bite
718 return FALSE;
719 case PURPLE_CONNECTION_CONNECTING:
720 // Come back for more later
721 return TRUE;
722 default:
723 break;
724 }
725
726 ha = purple_connection_get_protocol_data(pc);
727 if (ha == NULL) {
728 g_warn_if_reached();
729 return TRUE;
730 }
731
732 if (ha->active_client_state == ACTIVE_CLIENT_STATE__ACTIVE_CLIENT_STATE_IS_ACTIVE) {
733 //We're already the active client
734 return TRUE;
735 }
736 if (ha->idle_time > HANGOUTS_ACTIVE_CLIENT_TIMEOUT) {
737 //We've gone idle
738 return TRUE;
739 }
740 if (!purple_presence_is_status_primitive_active(purple_account_get_presence(ha->account), PURPLE_STATUS_AVAILABLE)) {
741 //We're marked as not available somehow
742 return TRUE;
743 }
744 ha->active_client_state = ACTIVE_CLIENT_STATE__ACTIVE_CLIENT_STATE_IS_ACTIVE;
745
746 set_active_client_request__init(&request);
747
748 request.request_header = hangouts_get_request_header(ha);
749 request.has_is_active = TRUE;
750 request.is_active = TRUE;
751 request.full_jid = g_strdup_printf("%s/%s", purple_account_get_username(ha->account), ha->client_id);
752 request.has_timeout_secs = TRUE;
753 request.timeout_secs = HANGOUTS_ACTIVE_CLIENT_TIMEOUT;
754
755 hangouts_pblite_set_active_client(ha, &request, (HangoutsPbliteSetActiveClientResponseFunc)hangouts_default_response_dump, NULL);
756
757 hangouts_request_header_free(request.request_header);
758 g_free(request.full_jid);
759
760 return TRUE;
761 }
762
763
764 void
hangouts_search_results_send_im(PurpleConnection * pc,GList * row,void * user_data)765 hangouts_search_results_send_im(PurpleConnection *pc, GList *row, void *user_data)
766 {
767 PurpleAccount *account = purple_connection_get_account(pc);
768 const gchar *who = g_list_nth_data(row, 0);
769 PurpleIMConversation *imconv;
770
771 imconv = purple_conversations_find_im_with_account(who, account);
772 if (imconv == NULL) {
773 imconv = purple_im_conversation_new(account, who);
774 }
775 purple_conversation_present(PURPLE_CONVERSATION(imconv));
776 }
777
778 void
hangouts_search_results_get_info(PurpleConnection * pc,GList * row,void * user_data)779 hangouts_search_results_get_info(PurpleConnection *pc, GList *row, void *user_data)
780 {
781 hangouts_get_info(pc, g_list_nth_data(row, 0));
782 }
783
784 void
hangouts_search_results_add_buddy(PurpleConnection * pc,GList * row,void * user_data)785 hangouts_search_results_add_buddy(PurpleConnection *pc, GList *row, void *user_data)
786 {
787 PurpleAccount *account = purple_connection_get_account(pc);
788
789 if (!purple_blist_find_buddy(account, g_list_nth_data(row, 0)))
790 purple_blist_request_add_buddy(account, g_list_nth_data(row, 0), "Hangouts", g_list_nth_data(row, 1));
791 }
792
793 void
hangouts_search_users_text_cb(PurpleHttpConnection * connection,PurpleHttpResponse * response,gpointer user_data)794 hangouts_search_users_text_cb(PurpleHttpConnection *connection, PurpleHttpResponse *response, gpointer user_data)
795 {
796 HangoutsAccount *ha = user_data;
797 const gchar *response_data;
798 size_t response_size;
799 JsonArray *resultsarray;
800 JsonObject *node;
801 gint index, length;
802 gchar *search_term;
803 JsonObject *status;
804
805 PurpleNotifySearchResults *results;
806 PurpleNotifySearchColumn *column;
807
808 if (purple_http_response_get_error(response) != NULL) {
809 purple_notify_error(ha->pc, _("Search Error"), _("There was an error searching for the user"), purple_http_response_get_error(response), purple_request_cpar_from_connection(ha->pc));
810 g_dataset_destroy(connection);
811 return;
812 }
813
814 response_data = purple_http_response_get_data(response, &response_size);
815 node = json_decode_object(response_data, response_size);
816
817 search_term = g_dataset_get_data(connection, "search_term");
818 resultsarray = json_object_get_array_member(node, "results");
819 length = json_array_get_length(resultsarray);
820
821 if (length == 0) {
822 status = json_object_get_object_member(node, "status");
823
824 if (!json_object_has_member(status, "personalResultsNotReady") || json_object_get_boolean_member(status, "personalResultsNotReady") == TRUE) {
825 //Not ready yet, retry
826 hangouts_search_users_text(ha, search_term);
827
828 } else {
829 gchar *primary_text = g_strdup_printf(_("Your search for the user \"%s\" returned no results"), search_term);
830 purple_notify_warning(ha->pc, _("No users found"), primary_text, "", purple_request_cpar_from_connection(ha->pc));
831 g_free(primary_text);
832 }
833
834 g_dataset_destroy(connection);
835 json_object_unref(node);
836 return;
837 }
838
839 results = purple_notify_searchresults_new();
840 if (results == NULL)
841 {
842 g_dataset_destroy(connection);
843 json_object_unref(node);
844 return;
845 }
846
847 /* columns: Friend ID, Name, Network */
848 column = purple_notify_searchresults_column_new(_("ID"));
849 purple_notify_searchresults_column_add(results, column);
850 column = purple_notify_searchresults_column_new(_("Display Name"));
851 purple_notify_searchresults_column_add(results, column);
852
853 purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_ADD, hangouts_search_results_add_buddy);
854 purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_INFO, hangouts_search_results_get_info);
855 purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_IM, hangouts_search_results_send_im);
856
857 for(index = 0; index < length; index++)
858 {
859 JsonNode *result = json_array_get_element(resultsarray, index);
860
861 gchar *id = hangouts_json_path_query_string(result, "$.person.personId", NULL);
862 gchar *displayname = hangouts_json_path_query_string(result, "$.person.name[*].displayName", NULL);
863 GList *row = NULL;
864
865 row = g_list_append(row, id);
866 row = g_list_append(row, displayname);
867
868 purple_notify_searchresults_row_add(results, row);
869 }
870
871 purple_notify_searchresults(ha->pc, NULL, search_term, NULL, results, NULL, NULL);
872
873 g_dataset_destroy(connection);
874 json_object_unref(node);
875 }
876
877 /*
878
879 POST https://people-pa.clients6.google.com/v2/people/lookup
880 id=actual_email_address%40gmail.com&type=EMAIL&matchType=LENIENT&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_LOOKUP&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&coreIdParams.useRealtimeNotificationExpandedAcls=true&key=AIzaSyAfFJCeph-euFSwtmqFZi0kaKk-cZ5wufM
881
882 id=%2B123456789&type=PHONE&matchType=LENIENT&requestMask.includeField.paths=person.email&requestMask.includeField.paths=person.gender&requestMask.includeField.paths=person.in_app_reachability&requestMask.includeField.paths=person.metadata&requestMask.includeField.paths=person.name&requestMask.includeField.paths=person.phone&requestMask.includeField.paths=person.photo&requestMask.includeField.paths=person.read_only_profile_info&extensionSet.extensionNames=HANGOUTS_ADDITIONAL_DATA&extensionSet.extensionNames=HANGOUTS_OFF_NETWORK_GAIA_LOOKUP&extensionSet.extensionNames=HANGOUTS_PHONE_DATA&coreIdParams.useRealtimeNotificationExpandedAcls=true"aFilterType=PHONE&key=AIzaSyAfFJCeph-euFSwtmqFZi0kaKk-cZ5wufM
883
884 */
885
886
887 void
hangouts_search_users_text(HangoutsAccount * ha,const gchar * text)888 hangouts_search_users_text(HangoutsAccount *ha, const gchar *text)
889 {
890 PurpleHttpRequest *request;
891 GString *url = g_string_new("https://people-pa.clients6.google.com/v2/people/autocomplete?");
892 PurpleHttpConnection *connection;
893
894 g_string_append_printf(url, "query=%s&", purple_url_encode(text));
895 g_string_append(url, "client=HANGOUTS_WITH_DATA&");
896 g_string_append(url, "pageSize=20&");
897 g_string_append_printf(url, "key=%s&", purple_url_encode(GOOGLE_GPLUS_KEY));
898
899 request = purple_http_request_new(NULL);
900 purple_http_request_set_cookie_jar(request, ha->cookie_jar);
901 purple_http_request_set_url(request, url->str);
902
903 hangouts_set_auth_headers(ha, request);
904
905 connection = purple_http_request(ha->pc, request, hangouts_search_users_text_cb, ha);
906 purple_http_request_unref(request);
907
908 g_dataset_set_data_full(connection, "search_term", g_strdup(text), g_free);
909
910 g_string_free(url, TRUE);
911 }
912
913 void
hangouts_search_users(PurpleProtocolAction * action)914 hangouts_search_users(PurpleProtocolAction *action)
915 {
916 PurpleConnection *pc = purple_protocol_action_get_connection(action);
917 HangoutsAccount *ha = purple_connection_get_protocol_data(pc);
918
919 purple_request_input(pc, _("Search for friends..."),
920 _("Search for friends..."),
921 NULL,
922 NULL, FALSE, FALSE, NULL,
923 _("_Search"), G_CALLBACK(hangouts_search_users_text),
924 _("_Cancel"), NULL,
925 purple_request_cpar_from_connection(pc),
926 ha);
927
928 }
929