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 #include "hangouts_events.h"
20 
21 #include <string.h>
22 #include <glib.h>
23 
24 #include "core.h"
25 #include "debug.h"
26 #include "glibcompat.h"
27 #include "image.h"
28 #include "image-store.h"
29 #include "mediamanager.h"
30 
31 #include "hangouts_conversation.h"
32 #include "hangouts.pb-c.h"
33 
34 // From hangouts_pblite
35 gchar *pblite_dump_json(ProtobufCMessage *message);
36 
37 //purple_signal_emit(purple_connection_get_protocol(ha->pc), "hangouts-received-stateupdate", ha->pc, batch_update.state_update[j]);
38 
39 void
hangouts_register_events(gpointer plugin)40 hangouts_register_events(gpointer plugin)
41 {
42 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_typing_notification), NULL);
43 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_event_notification), NULL);
44 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_presence_notification), NULL);
45 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_watermark_notification), NULL);
46 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_state_update), NULL);
47 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_view_modification), NULL);
48 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_delete_notification), NULL);
49 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_block_notification), NULL);
50 	purple_signal_connect(plugin, "hangouts-received-stateupdate", plugin, PURPLE_CALLBACK(hangouts_received_other_notification), NULL);
51 
52 	purple_signal_connect(plugin, "hangouts-gmail-notification", plugin, PURPLE_CALLBACK(hangouts_received_gmail_notification), NULL);
53 }
54 
55 /*
56 struct  _StateUpdate
57 {
58   ProtobufCMessage base;
59   StateUpdateHeader *state_update_header;
60   Conversation *conversation;
61   EventNotification *event_notification;
62   SetFocusNotification *focus_notification;
63   SetTypingNotification *typing_notification;
64   SetConversationNotificationLevelNotification *notification_level_notification;
65   ReplyToInviteNotification *reply_to_invite_notification;
66   WatermarkNotification *watermark_notification;
67   ConversationViewModification *view_modification;
68   EasterEggNotification *easter_egg_notification;
69   SelfPresenceNotification *self_presence_notification;
70   DeleteActionNotification *delete_notification;
71   PresenceNotification *presence_notification;
72   BlockNotification *block_notification;
73   SetNotificationSettingNotification *notification_setting_notification;
74   RichPresenceEnabledStateNotification *rich_presence_enabled_state_notification;
75 };*/
76 
77 void
hangouts_received_state_update(PurpleConnection * pc,StateUpdate * state_update)78 hangouts_received_state_update(PurpleConnection *pc, StateUpdate *state_update)
79 {
80 	HangoutsAccount *ha = purple_connection_get_protocol_data(pc);
81 
82 	if (ha != NULL && state_update->state_update_header != NULL) {
83 		gint64 current_server_time = state_update->state_update_header->current_server_time;
84 
85 		ha->active_client_state = state_update->state_update_header->active_client_state;
86 
87 		// libpurple can't store a 64bit int on a 32bit machine, so convert to something more usable instead (puke)
88 		//  also needs to work cross platform, in case the accounts.xml is being shared (double puke)
89 		purple_account_set_int(ha->account, "last_event_timestamp_high", current_server_time >> 32);
90 		purple_account_set_int(ha->account, "last_event_timestamp_low", current_server_time & 0xFFFFFFFF);
91 	}
92 }
93 
94 static void
hangouts_remove_conversation(HangoutsAccount * ha,const gchar * conv_id)95 hangouts_remove_conversation(HangoutsAccount *ha, const gchar *conv_id)
96 {
97 	if (g_hash_table_contains(ha->one_to_ones, conv_id)) {
98 		const gchar *buddy_id = g_hash_table_lookup(ha->one_to_ones, conv_id);
99 		PurpleBuddy *buddy = purple_blist_find_buddy(ha->account, buddy_id);
100 
101 		purple_blist_remove_buddy(buddy);
102 		g_hash_table_remove(ha->one_to_ones, conv_id);
103 		g_hash_table_remove(ha->one_to_ones_rev, buddy_id);
104 
105 	} else if (g_hash_table_contains(ha->group_chats, conv_id)) {
106 		PurpleChat *chat = purple_blist_find_chat(ha->account, conv_id);
107 		purple_blist_remove_chat(chat);
108 
109 		g_hash_table_remove(ha->group_chats, conv_id);
110 
111 	} else {
112 		// Unknown conversation!
113 		return;
114 	}
115 }
116 
117 void
hangouts_received_view_modification(PurpleConnection * pc,StateUpdate * state_update)118 hangouts_received_view_modification(PurpleConnection *pc, StateUpdate *state_update)
119 {
120 	HangoutsAccount *ha;
121 	ConversationViewModification *view_modification = state_update->view_modification;
122 	const gchar *conv_id;
123 
124 	if (view_modification == NULL) {
125 		return;
126 	}
127 
128 	if (view_modification->new_view == CONVERSATION_VIEW__CONVERSATION_VIEW_ARCHIVED) {
129 		ha = purple_connection_get_protocol_data(pc);
130 		conv_id = view_modification->conversation_id->id;
131 
132 		hangouts_remove_conversation(ha, conv_id);
133 	}
134 }
135 
136 void
hangouts_received_delete_notification(PurpleConnection * pc,StateUpdate * state_update)137 hangouts_received_delete_notification(PurpleConnection *pc, StateUpdate *state_update)
138 {
139 	HangoutsAccount *ha;
140 	DeleteActionNotification *delete_notification = state_update->delete_notification;
141 	const gchar *conv_id;
142 
143 	if (delete_notification == NULL) {
144 		return;
145 	}
146 
147 	ha = purple_connection_get_protocol_data(pc);
148 	conv_id = delete_notification->conversation_id->id;
149 
150 	if (delete_notification->delete_action && delete_notification->delete_action->delete_type == DELETE_TYPE__DELETE_TYPE_UPPER_BOUND) {
151 		hangouts_remove_conversation(ha, conv_id);
152 	}
153 }
154 
155 void
hangouts_received_block_notification(PurpleConnection * pc,StateUpdate * state_update)156 hangouts_received_block_notification(PurpleConnection *pc, StateUpdate *state_update)
157 {
158 	HangoutsAccount *ha;
159 	BlockNotification *block_notification = state_update->block_notification;
160 	guint i;
161 
162 	if (block_notification == NULL) {
163 		return;
164 	}
165 
166 	ha = purple_connection_get_protocol_data(pc);
167 
168 	for (i = 0; i < block_notification->n_block_state_change; i++) {
169 		BlockStateChange *block_state_change = block_notification->block_state_change[i];
170 
171 		if (block_state_change->has_new_block_state) {
172 			gchar *gaia_id = block_state_change->participant_id->gaia_id;
173 			if (block_state_change->new_block_state == BLOCK_STATE__BLOCK_STATE_BLOCK) {
174 				purple_account_privacy_deny_add(ha->account, gaia_id, TRUE);
175 			} else if (block_state_change->new_block_state == BLOCK_STATE__BLOCK_STATE_UNBLOCK) {
176 				purple_account_privacy_deny_remove(ha->account, gaia_id, TRUE);
177 			}
178 		}
179 	}
180 }
181 
182 void
hangouts_received_gmail_notification(PurpleConnection * pc,const gchar * username,GmailNotification * msg)183 hangouts_received_gmail_notification(PurpleConnection *pc, const gchar *username, GmailNotification *msg)
184 {
185 	gchar *url;
186 	gchar *subject;
187 	gchar *from;
188 	gchar *to;
189 	gchar *json_dump;
190 	guint i;
191 	gboolean is_unread = FALSE;
192 	gboolean is_inbox = FALSE;
193 
194 	if (!purple_account_get_check_mail(purple_connection_get_account(pc))) {
195 		return;
196 	}
197 
198 	for (i = 0; i < msg->n_labels; i++) {
199 		if (purple_strequal(msg->labels[i], "^u")) {
200 			is_unread = TRUE;
201 		} else if (purple_strequal(msg->labels[i], "^i")) {
202 			is_inbox = TRUE;
203 		}
204 	}
205 	if (is_unread == FALSE || is_inbox == FALSE) {
206 		return;
207 	}
208 
209 	subject = purple_utf8_strip_unprintables(msg->subject);
210 	from = purple_markup_escape_text(msg->sender_name, -1);
211 	to = purple_markup_escape_text(username, -1);
212 
213 	json_dump = pblite_dump_json((ProtobufCMessage *) msg);
214 	purple_debug_info("hangouts", "Received gmail notification %s\n", json_dump);
215 
216 	url = g_strconcat("https://mail.google.com/mail/u/", username, "/#inbox/", purple_url_encode(msg->thread_id), NULL);
217 
218 	purple_notify_email(pc, subject, from, to, url, NULL, NULL);
219 
220 	g_free(json_dump);
221 	g_free(url);
222 	g_free(subject);
223 	g_free(from);
224 	g_free(to);
225 }
226 
227 void
hangouts_received_other_notification(PurpleConnection * pc,StateUpdate * state_update)228 hangouts_received_other_notification(PurpleConnection *pc, StateUpdate *state_update)
229 {
230 	gchar *json_dump;
231 
232 	if (state_update->typing_notification != NULL ||
233 		state_update->presence_notification != NULL ||
234 		state_update->event_notification != NULL ||
235 		state_update->watermark_notification != NULL) {
236 		return;
237 	}
238 
239 	purple_debug_info("hangouts", "Received new other event %p\n", state_update);
240 	json_dump = pblite_dump_json((ProtobufCMessage *)state_update);
241 	purple_debug_info("hangouts", "%s\n", json_dump);
242 
243 	g_free(json_dump);
244 }
245 
246 /*        "conversation" : {
247                 "conversation_id" : {
248                         "id" : "UgxGdpCK_mSrhBX8hrx4AaABAQ"
249                 },
250                 "type" : "CONVERSATION_TYPE_ONE_TO_ONE",
251                 "name" : null,
252                 "self_conversation_state" : {
253                         "client_generated_id" : null,
254                         "self_read_state" : {
255                                 "participant_id" : {
256                                         "gaia_id" : "110174066375061118727",
257                                         "chat_id" : "110174066375061118727"
258                                 },
259                                 "latest_read_timestamp" : null
260                         },
261                         "status" : "CONVERSATION_STATUS_ACTIVE",
262                         "notification_level" : "NOTIFICATION_LEVEL_RING",
263                         "view" : [
264                                 "CONVERSATION_VIEW_INBOX"
265                         ],
266                         "inviter_id" : {
267                                 "gaia_id" : "111523150620250165866",
268                                 "chat_id" : "111523150620250165866"
269                         },
270                         "invite_timestamp" : 1367645831562000,
271                         "sort_timestamp" : 1453809415517871,
272                         "active_timestamp" : 1367645831562000,
273                         "delivery_medium_option" : [
274                                 {
275                                         "delivery_medium" : {
276                                                 "medium_type" : "DELIVERY_MEDIUM_BABEL",
277                                                 "phone" : null
278                                         },
279                                         "current_default" : 1
280                                 }
281                         ]
282                 },
283                 "read_state" : [
284                         {
285                                 "participant_id" : {
286                                         "gaia_id" : "111523150620250165866",
287                                         "chat_id" : "111523150620250165866"
288                                 },
289                                 "latest_read_timestamp" : null
290                         },
291                         {
292                                 "participant_id" : {
293                                         "gaia_id" : "110174066375061118727",
294                                         "chat_id" : "110174066375061118727"
295                                 },
296                                 "latest_read_timestamp" : null
297                         }
298                 ],
299                 "has_active_hangout" : null,
300                 "otr_status" : "OFF_THE_RECORD_STATUS_ON_THE_RECORD",
301                 "otr_toggle" : "OFF_THE_RECORD_TOGGLE_ENABLED",
302                 "conversation_history_supported" : null,
303                 "current_participant" : [
304                         {
305                                 "gaia_id" : "110174066375061118727",
306                                 "chat_id" : "110174066375061118727"
307                         },
308                         {
309                                 "gaia_id" : "111523150620250165866",
310                                 "chat_id" : "111523150620250165866"
311                         }
312                 ],
313                 "participant_data" : [
314                         {
315                                 "id" : {
316                                         "gaia_id" : "111523150620250165866",
317                                         "chat_id" : "111523150620250165866"
318                                 },
319                                 "fallback_name" : "Mike Ruprecht",
320                                 "invitation_status" : "INVITATION_STATUS_ACCEPTED",
321                                 "participant_type" : "PARTICIPANT_TYPE_GAIA",
322                                 "new_invitation_status" : "INVITATION_STATUS_ACCEPTED"
323                         },
324                         {
325                                 "id" : {
326                                         "gaia_id" : "110174066375061118727",
327                                         "chat_id" : "110174066375061118727"
328                                 },
329                                 "fallback_name" : "Eion Robb",
330                                 "invitation_status" : "INVITATION_STATUS_ACCEPTED",
331                                 "participant_type" : "PARTICIPANT_TYPE_GAIA",
332                                 "new_invitation_status" : "INVITATION_STATUS_ACCEPTED"
333                         }
334                 ],
335                 "network_type" : [
336                         "NETWORK_TYPE_BABEL"
337                 ],
338                 "force_history_state" : null
339         },*/
340 
341 
342 void
hangouts_received_watermark_notification(PurpleConnection * pc,StateUpdate * state_update)343 hangouts_received_watermark_notification(PurpleConnection *pc, StateUpdate *state_update)
344 {
345 	HangoutsAccount *ha;
346 	WatermarkNotification *watermark_notification = state_update->watermark_notification;
347 
348 	if (watermark_notification == NULL) {
349 		return;
350 	}
351 
352 	ha = purple_connection_get_protocol_data(pc);
353 
354 	if (FALSE && purple_strequal(watermark_notification->sender_id->gaia_id, ha->self_gaia_id)) {
355 		//We marked this message as read ourselves
356 		PurpleConversation *conv = NULL;
357 		const gchar *conv_id = watermark_notification->conversation_id->id;
358 		gint64 *last_read_timestamp_ptr;
359 		gint64 latest_read_timestamp;
360 
361 		if (g_hash_table_contains(ha->one_to_ones, conv_id)) {
362 			conv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(g_hash_table_lookup(ha->one_to_ones, conv_id), ha->account));
363 		} else if (g_hash_table_contains(ha->group_chats, conv_id)) {
364 			conv = PURPLE_CONVERSATION(purple_conversations_find_chat_with_account(conv_id, ha->account));
365 		} else {
366 			// Unknown conversation!
367 			return;
368 		}
369 		if (conv == NULL) {
370 			return;
371 		}
372 
373 		latest_read_timestamp = watermark_notification->latest_read_timestamp;
374 		last_read_timestamp_ptr = (gint64 *)purple_conversation_get_data(conv, "last_read_timestamp");
375 		if (last_read_timestamp_ptr == NULL) {
376 			last_read_timestamp_ptr = g_new0(gint64, 1);
377 		}
378 		if (latest_read_timestamp > *last_read_timestamp_ptr) {
379 			*last_read_timestamp_ptr = watermark_notification->latest_read_timestamp;
380 			purple_conversation_set_data(conv, "last_read_timestamp", last_read_timestamp_ptr);
381 		}
382 	}
383 }
384 
385 void
hangouts_process_presence_result(HangoutsAccount * ha,PresenceResult * presence_result)386 hangouts_process_presence_result(HangoutsAccount *ha, PresenceResult *presence_result)
387 {
388 	const gchar *gaia_id = presence_result->user_id->gaia_id;
389 	const gchar *status_id = NULL;
390 	const gchar *conv_id = g_hash_table_lookup(ha->one_to_ones_rev, gaia_id);
391 	gboolean reachable = FALSE;
392 	gboolean available = FALSE;
393 	gchar *message = NULL;
394 	PurpleBuddy *buddy = purple_blist_find_buddy(ha->account, gaia_id);
395 	Presence *presence = presence_result->presence;
396 
397 	if (buddy != NULL) {
398 		status_id = purple_status_get_id(purple_presence_get_active_status(purple_buddy_get_presence(buddy)));
399 	}
400 
401 	if (g_strcmp0(status_id, "mobile") == 0 || (conv_id != NULL && g_hash_table_contains(ha->google_voice_conversations, conv_id))) {
402 		// SMS contacts normally appear as 'offline'
403 		status_id = "mobile";
404 	} else if (presence != NULL && (presence->has_reachable || presence->has_available)) {
405 		if (presence->reachable) {
406 			reachable = TRUE;
407 		}
408 		if (presence->available) {
409 			available = TRUE;
410 		}
411 
412 		if (reachable && available) {
413 			status_id = purple_primitive_get_id_from_type(PURPLE_STATUS_AVAILABLE);
414 		} else if (reachable) {
415 			status_id = purple_primitive_get_id_from_type(PURPLE_STATUS_AWAY);
416 		} else if (available) {
417 			status_id = purple_primitive_get_id_from_type(PURPLE_STATUS_EXTENDED_AWAY);
418 		} else if (purple_account_get_bool(ha->account, "treat_invisible_as_offline", FALSE)) {
419 			status_id = "gone";
420 		} else {
421 			// Hangouts contacts are never really unreachable, just invisible
422 			status_id = purple_primitive_get_id_from_type(PURPLE_STATUS_INVISIBLE);
423 		}
424 	} else if (buddy == NULL) {
425 		return;
426 	}
427 
428 	if (presence != NULL && presence->mood_setting) {
429 		MoodMessage *mood_message = presence->mood_setting->mood_message;
430 		MoodContent *mood_content = mood_message ? mood_message->mood_content : NULL;
431 		size_t n_segments;
432 		Segment **segments;
433 		GString *message_str;
434 		guint i;
435 
436 		if (mood_content != NULL && mood_content->n_segment) {
437 			n_segments = mood_content->n_segment;
438 			segments = mood_content->segment;
439 			message_str = g_string_new(NULL);
440 
441 			for (i = 0; i < n_segments; i++) {
442 				Segment *segment = segments[i];
443 				if (segment->type == SEGMENT_TYPE__SEGMENT_TYPE_TEXT) {
444 					g_string_append(message_str, segment->text);
445 					g_string_append_c(message_str, ' ');
446 				}
447 			}
448 			message = g_string_free(message_str, FALSE);
449 		}
450 	}
451 
452 	if (message != NULL) {
453 		purple_protocol_got_user_status(ha->account, gaia_id, status_id, "message", message, NULL);
454 	} else {
455 		purple_protocol_got_user_status(ha->account, gaia_id, status_id, NULL);
456 	}
457 
458 	g_free(message);
459 
460 	if (buddy != NULL && presence != NULL) {
461 		HangoutsBuddy *hbuddy = purple_buddy_get_protocol_data(buddy);
462 		HangoutsDeviceTypeFlags device_type = HANGOUTS_DEVICE_TYPE_UNKNOWN;
463 
464 		if (hbuddy == NULL) {
465 			hbuddy = g_new0(HangoutsBuddy, 1);
466 			hbuddy->buddy = buddy;
467 			purple_buddy_set_protocol_data(buddy, hbuddy);
468 		}
469 
470 		hbuddy->in_call = presence->in_call && presence->in_call->has_call_type && presence->in_call->call_type != CALL_TYPE__CALL_TYPE_NONE;
471 		hbuddy->last_seen = presence->last_seen ? presence->last_seen->last_seen_timestamp / 1000000 : 0;
472 
473 		if (presence->device_status) {
474 			if (presence->device_status->mobile) {
475 				device_type |= HANGOUTS_DEVICE_TYPE_MOBILE;
476 			}
477 			if (presence->device_status->desktop) {
478 				device_type |= HANGOUTS_DEVICE_TYPE_DESKTOP;
479 			}
480 			if (presence->device_status->tablet) {
481 				device_type |= HANGOUTS_DEVICE_TYPE_TABLET;
482 			}
483 		}
484 		hbuddy->device_type = device_type;
485 
486 		if (presence->last_seen && !presence->has_reachable && !presence->has_available) {
487 			GList *user_list = g_list_prepend(NULL, (gchar *)gaia_id);
488 			hangouts_get_users_presence(ha, user_list);
489 			g_list_free(user_list);
490 		}
491 	}
492 }
493 
494 void
hangouts_received_presence_notification(PurpleConnection * pc,StateUpdate * state_update)495 hangouts_received_presence_notification(PurpleConnection *pc, StateUpdate *state_update)
496 {
497 	HangoutsAccount *ha;
498 	PresenceNotification *presence_notification = state_update->presence_notification;
499 	guint i;
500 
501 	if (presence_notification == NULL) {
502 		return;
503 	}
504 
505 	ha = purple_connection_get_protocol_data(pc);
506 
507 	for (i = 0; i < presence_notification->n_presence; i++) {
508 		hangouts_process_presence_result(ha, presence_notification->presence[i]);
509 	}
510 }
511 
512 static void
hangouts_got_http_image_for_conv(PurpleHttpConnection * connection,PurpleHttpResponse * response,gpointer user_data)513 hangouts_got_http_image_for_conv(PurpleHttpConnection *connection, PurpleHttpResponse *response, gpointer user_data)
514 {
515 	HangoutsAccount *ha = user_data;
516 	const gchar *url;
517 	const gchar *gaia_id;
518 	const gchar *conv_id;
519 	PurpleMessageFlags msg_flags;
520 	time_t message_timestamp;
521 	PurpleImage *image;
522 	const gchar *response_data;
523 	size_t response_size;
524 	guint image_id;
525 	gchar *msg;
526 	gchar *escaped_image_url;
527 
528 	if (purple_http_response_get_error(response) != NULL) {
529 		g_dataset_destroy(connection);
530 		return;
531 	}
532 
533 	url = g_dataset_get_data(connection, "url");
534 	gaia_id = g_dataset_get_data(connection, "gaia_id");
535 	conv_id = g_dataset_get_data(connection, "conv_id");
536 	msg_flags = GPOINTER_TO_INT(g_dataset_get_data(connection, "msg_flags"));
537 	message_timestamp = GPOINTER_TO_INT(g_dataset_get_data(connection, "message_timestamp"));
538 
539 	response_data = purple_http_response_get_data(response, &response_size);
540 	image = purple_image_new_from_data(g_memdup(response_data, response_size), response_size);
541 	image_id = purple_image_store_add(image);
542 	escaped_image_url = g_markup_escape_text(purple_http_request_get_url(purple_http_conn_get_request(connection)), -1);
543 	msg = g_strdup_printf("<a href='%s'>View full image <img id='%u' src='%s' /></a>", url, image_id, escaped_image_url);
544 	msg_flags |= PURPLE_MESSAGE_IMAGES;
545 
546 	if (g_hash_table_contains(ha->group_chats, conv_id)) {
547 		purple_serv_got_chat_in(ha->pc, g_str_hash(conv_id), gaia_id, msg_flags, msg, message_timestamp);
548 	} else {
549 		if (msg_flags & PURPLE_MESSAGE_RECV) {
550 			purple_serv_got_im(ha->pc, gaia_id, msg, msg_flags, message_timestamp);
551 		} else {
552 			gaia_id = g_hash_table_lookup(ha->one_to_ones, conv_id);
553 			if (gaia_id) {
554 				PurpleIMConversation *imconv = purple_conversations_find_im_with_account(gaia_id, ha->account);
555 				PurpleMessage *message = purple_message_new_outgoing(gaia_id, msg, msg_flags);
556 				if (imconv == NULL) {
557 					imconv = purple_im_conversation_new(ha->account, gaia_id);
558 				}
559 				purple_message_set_time(message, message_timestamp);
560 				purple_conversation_write_message(PURPLE_CONVERSATION(imconv), message);
561 			}
562 		}
563 	}
564 
565 	g_free(escaped_image_url);
566 	g_free(msg);
567 	g_dataset_destroy(connection);
568 }
569 
570 void
hangouts_received_event_notification(PurpleConnection * pc,StateUpdate * state_update)571 hangouts_received_event_notification(PurpleConnection *pc, StateUpdate *state_update)
572 {
573 	HangoutsAccount *ha;
574 	EventNotification *event_notification = state_update->event_notification;
575 	Conversation *conversation = state_update->conversation;
576 	Event *event;
577 	gint64 current_server_time = state_update->state_update_header->current_server_time;
578 
579 	if (event_notification == NULL) {
580 		return;
581 	}
582 
583 	ha = purple_connection_get_protocol_data(pc);
584 
585 	event = event_notification->event;
586 	if (ha->self_gaia_id == NULL) {
587 		ha->self_gaia_id = g_strdup(event->self_event_state->user_id->gaia_id);
588 		purple_connection_set_display_name(pc, ha->self_gaia_id);
589 	}
590 
591 	hangouts_process_conversation_event(ha, conversation, event, current_server_time);
592 }
593 
594 void
hangouts_process_conversation_event(HangoutsAccount * ha,Conversation * conversation,Event * event,gint64 current_server_time)595 hangouts_process_conversation_event(HangoutsAccount *ha, Conversation *conversation, Event *event, gint64 current_server_time)
596 {
597 	PurpleConnection *pc = ha->pc;
598 	const gchar *gaia_id;
599 	const gchar *conv_id;
600 	gint64 timestamp;
601 	const gchar *client_generated_id;
602 	ChatMessage *chat_message;
603 	PurpleMessageFlags msg_flags;
604 	PurpleConversation *pconv = NULL;
605 
606 	if (conversation && (conv_id = conversation->conversation_id->id) &&
607 			!g_hash_table_contains(ha->one_to_ones, conv_id) &&
608 			!g_hash_table_contains(ha->group_chats, conv_id)) {
609 		// New conversation we ain't seen before
610 
611 		hangouts_add_conversation_to_blist(ha, conversation, NULL);
612 	}
613 
614 	gaia_id = event->sender_id->gaia_id;
615 	conv_id = event->conversation_id->id;
616 	timestamp = event->timestamp;
617 	chat_message = event->chat_message;
618 	client_generated_id = event->self_event_state->client_generated_id;
619 
620 	if (client_generated_id && g_hash_table_remove(ha->sent_message_ids, client_generated_id)) {
621 		// This probably came from us
622 		return;
623 	}
624 
625 	if (conv_id == NULL) {
626 		// Invalid conversation
627 		return;
628 	}
629 
630 	if (event->membership_change != NULL) {
631 		//event->event_type == EVENT_TYPE__EVENT_TYPE_REMOVE_USER || EVENT_TYPE__EVENT_TYPE_ADD_USER
632 		MembershipChange *membership_change = event->membership_change;
633 		guint i;
634 		PurpleChatConversation *chatconv = purple_conversations_find_chat_with_account(conv_id, ha->account);
635 
636 		if (chatconv != NULL) {
637 			for (i = 0; i < membership_change->n_participant_ids; i++) {
638 				ParticipantId *participant_id = membership_change->participant_ids[i];
639 
640 				if (membership_change->type == MEMBERSHIP_CHANGE_TYPE__MEMBERSHIP_CHANGE_TYPE_LEAVE) {
641 					//TODO
642 					//LeaveReason reason = membership_change->leave_reason;
643 
644 					purple_chat_conversation_remove_user(chatconv, participant_id->gaia_id, NULL);
645 					if (g_strcmp0(participant_id->gaia_id, ha->self_gaia_id) == 0) {
646 						purple_serv_got_chat_left(ha->pc, g_str_hash(conv_id));
647 						g_hash_table_remove(ha->group_chats, conv_id);
648 						purple_blist_remove_chat(purple_blist_find_chat(ha->account, conv_id));
649 					}
650 				} else {
651 					PurpleChatUserFlags cbflags = PURPLE_CHAT_USER_NONE;
652 					purple_chat_conversation_add_user(chatconv, participant_id->gaia_id, NULL, cbflags, TRUE);
653 				}
654 			}
655 		}
656 	}
657 
658 	if (chat_message != NULL) {
659 		size_t n_segments = chat_message->message_content->n_segment;
660 		Segment **segments = chat_message->message_content->segment;
661 		guint i;
662 		PurpleXmlNode *html = purple_xmlnode_new("html");
663 		gchar *msg;
664 		time_t message_timestamp;
665 
666 		for (i = 0; i < chat_message->n_annotation; i++) {
667 			EventAnnotation *annotation = chat_message->annotation[i];
668 
669 			if (annotation->type == HANGOUTS_MAGIC_HALF_EIGHT_SLASH_ME_TYPE) {
670 				//TODO strip name off the front of the first segment
671 				purple_xmlnode_insert_data(html, "/me ", -1);
672 				break;
673 			}
674 		}
675 
676 		for (i = 0; i < n_segments; i++) {
677 			Segment *segment = segments[i];
678 			Formatting *formatting = segment->formatting;
679 			PurpleXmlNode *node;
680 
681 			if (segment->type == SEGMENT_TYPE__SEGMENT_TYPE_TEXT) {
682 				node = purple_xmlnode_new_child(html, "span");
683 			} else if (segment->type == SEGMENT_TYPE__SEGMENT_TYPE_LINE_BREAK) {
684 				purple_xmlnode_new_child(html, "br");
685 				continue;
686 			} else if (segment->type == SEGMENT_TYPE__SEGMENT_TYPE_LINK) {
687 				node = purple_xmlnode_new_child(html, "a");
688 				if (segment->link_data) {
689 					const gchar *href = segment->link_data->link_target;
690 					purple_xmlnode_set_attrib(node, "href", href);
691 
692 					// Strip out the www.google.com/url?q= bit
693 					if (purple_account_get_bool(ha->account, "unravel_google_url", FALSE)) {
694 						PurpleHttpURL *url = purple_http_url_parse(href);
695 						if (purple_strequal(purple_http_url_get_host(url), "www.google.com")) {
696 							const gchar *path = purple_http_url_get_path(url);
697 							//apparently the path includes the query string
698 							if (g_str_has_prefix(path, "/url?q=")) {
699 								const gchar *end = strchr(path, '&');
700 								gsize len = (end ? (gsize) (end - path) : (gsize) strlen(path));
701 								gchar *new_href = g_strndup(path + 7, len - 7);
702 								purple_xmlnode_set_attrib(node, "href", purple_url_decode(new_href));
703 								g_free(new_href);
704 							}
705 						}
706 						purple_http_url_free(url);
707 					}
708 				}
709 			} else {
710 				continue;
711 			}
712 
713 			if (formatting) {
714 				if (formatting->bold) {
715 					node = purple_xmlnode_new_child(node, "b");
716 				}
717 				if (formatting->italics) {
718 					node = purple_xmlnode_new_child(node, "i");
719 				}
720 				if (formatting->strikethrough) {
721 					node = purple_xmlnode_new_child(node, "s");
722 				}
723 				if (formatting->underline) {
724 					node = purple_xmlnode_new_child(node, "u");
725 				}
726 			}
727 
728 			purple_xmlnode_insert_data(node, segment->text, -1);
729 		}
730 
731 		msg = purple_xmlnode_to_str(html, NULL);
732 		message_timestamp = time(NULL) - ((current_server_time - timestamp) / 1000000);
733 		msg_flags = (g_strcmp0(gaia_id, ha->self_gaia_id) ? PURPLE_MESSAGE_RECV : (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_REMOTE_SEND | PURPLE_MESSAGE_DELAYED));
734 		if (((current_server_time - timestamp) / 1000000) > 120) {
735 			msg_flags |= PURPLE_MESSAGE_DELAYED;
736 		}
737 
738 		if (g_hash_table_contains(ha->group_chats, conv_id)) {
739 			PurpleChatConversation *chatconv = purple_conversations_find_chat_with_account(conv_id, ha->account);
740 			if (chatconv == NULL) {
741 				chatconv = purple_serv_got_joined_chat(ha->pc, g_str_hash(conv_id), conv_id);
742 				purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "conv_id", g_strdup(conv_id));
743 				if (conversation) {
744 					guint i;
745 					for (i = 0; i < conversation->n_current_participant; i++) {
746 						PurpleChatUserFlags cbflags = PURPLE_CHAT_USER_NONE;
747 						purple_chat_conversation_add_user(chatconv, conversation->current_participant[i]->gaia_id, NULL, cbflags, FALSE);
748 					}
749 				}
750 			}
751 			pconv = PURPLE_CONVERSATION(chatconv);
752 			purple_serv_got_chat_in(pc, g_str_hash(conv_id), gaia_id, msg_flags, msg, message_timestamp);
753 
754 		} else {
755 			PurpleIMConversation *imconv = NULL;
756 			// It's most likely a one-to-one message
757 			if (msg_flags & PURPLE_MESSAGE_RECV) {
758 				purple_serv_got_im(pc, gaia_id, msg, msg_flags, message_timestamp);
759 			} else {
760 				gaia_id = g_hash_table_lookup(ha->one_to_ones, conv_id);
761 				if (gaia_id) {
762 					imconv = purple_conversations_find_im_with_account(gaia_id, ha->account);
763 					PurpleMessage *message = purple_message_new_outgoing(gaia_id, msg, msg_flags);
764 					if (imconv == NULL)
765 					{
766 						imconv = purple_im_conversation_new(ha->account, gaia_id);
767 					}
768 					purple_message_set_time(message, message_timestamp);
769 					purple_conversation_write_message(PURPLE_CONVERSATION(imconv), message);
770 				}
771 			}
772 
773 			if (imconv == NULL) {
774 				imconv = purple_conversations_find_im_with_account(gaia_id, ha->account);
775 			}
776 			pconv = PURPLE_CONVERSATION(imconv);
777 		}
778 
779 		if (purple_conversation_has_focus(pconv)) {
780 			hangouts_mark_conversation_seen(pconv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
781 		}
782 
783 		g_free(msg);
784 		purple_xmlnode_free(html);
785 
786 		if (chat_message->message_content->n_attachment) {
787 			size_t n_attachment = chat_message->message_content->n_attachment;
788 			Attachment **attachments = chat_message->message_content->attachment;
789 			guint i;
790 
791 			for (i = 0; i < n_attachment; i++) {
792 				Attachment *attachment = attachments[i];
793 				EmbedItem *embed_item = attachment->embed_item;
794 				if (embed_item->plus_photo) {
795 					PlusPhoto *plus_photo = embed_item->plus_photo;
796 					const gchar *image_url = plus_photo->thumbnail->image_url;
797 					const gchar *url = plus_photo->url;
798 					PurpleHttpConnection *connection;
799 
800 					// Provide a direct link to the video
801 					if (plus_photo->media_type == PLUS_PHOTO__MEDIA_TYPE__MEDIA_TYPE_VIDEO && plus_photo->download_url != NULL) {
802 						url = plus_photo->download_url;
803 					}
804 
805 					if (g_strcmp0(purple_core_get_ui(), "BitlBee") == 0) {
806 						// Bitlbee doesn't support images, so just plop a url to the image instead
807 						if (g_hash_table_contains(ha->group_chats, conv_id)) {
808 							purple_serv_got_chat_in(pc, g_str_hash(conv_id), gaia_id, msg_flags, url, message_timestamp);
809 						} else {
810 							if (msg_flags & PURPLE_MESSAGE_RECV) {
811 								purple_serv_got_im(pc, gaia_id, url, msg_flags, message_timestamp);
812 							} else {
813 								PurpleMessage *img_message = purple_message_new_outgoing(gaia_id, url, msg_flags);
814 								purple_message_set_time(img_message, message_timestamp);
815 								purple_conversation_write_message(pconv, img_message);
816 							}
817 						}
818 					} else {
819 						connection = purple_http_get(ha->pc, hangouts_got_http_image_for_conv, ha, image_url);
820 						purple_http_request_set_max_len(purple_http_conn_get_request(connection), -1);
821 						g_dataset_set_data_full(connection, "url", g_strdup(url), g_free);
822 						g_dataset_set_data_full(connection, "gaia_id", g_strdup(gaia_id), g_free);
823 						g_dataset_set_data_full(connection, "conv_id", g_strdup(conv_id), g_free);
824 						g_dataset_set_data(connection, "msg_flags", GINT_TO_POINTER(msg_flags));
825 						g_dataset_set_data(connection, "message_timestamp", GINT_TO_POINTER(message_timestamp));
826 					}
827 				}
828 			}
829 		}
830 	}
831 
832 	if (event->hangout_event != NULL) {
833 		//event->event_type == EVENT_TYPE__EVENT_TYPE_HANGOUT || EVENT_TYPE__EVENT_TYPE_PHONE_CALL
834 		//Something to do with calling
835 		const gchar *msg;
836 		HangoutEvent *hangout_event = event->hangout_event;
837 		time_t message_timestamp = time(NULL) - ((current_server_time - timestamp) / 1000000);
838 
839 		switch (hangout_event->event_type) {
840 			case HANGOUT_EVENT_TYPE__HANGOUT_EVENT_TYPE_START:
841 				msg = _("Call started");
842 				break;
843 			case HANGOUT_EVENT_TYPE__HANGOUT_EVENT_TYPE_END:
844 				msg = _("Call ended");
845 				break;
846 			default:
847 				msg = NULL;
848 				break;
849 		}
850 		if (msg != NULL) {
851 			if (g_hash_table_contains(ha->group_chats, conv_id)) {
852 				purple_serv_got_chat_in(ha->pc, g_str_hash(conv_id), gaia_id, PURPLE_MESSAGE_SYSTEM, msg, message_timestamp);
853 			} else {
854 				gaia_id = g_hash_table_lookup(ha->one_to_ones, conv_id);
855 				purple_serv_got_im(ha->pc, gaia_id, msg, PURPLE_MESSAGE_SYSTEM, message_timestamp);
856 			}
857 		}
858 		if (hangout_event->event_type == HANGOUT_EVENT_TYPE__HANGOUT_EVENT_TYPE_START) {
859 			if (purple_account_get_bool(ha->account, "show-call-links", !purple_media_manager_get())) {
860 				//No voice/video support, display URL
861 				gchar *join_message = g_strdup_printf("%s https://plus.google.com/hangouts/_/CONVERSATION/%s", _("To join the call, open "), conv_id);
862 				if (g_hash_table_contains(ha->group_chats, conv_id)) {
863 					purple_serv_got_chat_in(ha->pc, g_str_hash(conv_id), gaia_id, PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG, join_message, message_timestamp);
864 				} else {
865 					purple_serv_got_im(ha->pc, gaia_id, join_message, PURPLE_MESSAGE_SYSTEM | PURPLE_MESSAGE_NO_LOG, message_timestamp);
866 				}
867 				g_free(join_message);
868 			}
869 		}
870 	}
871 
872 	if (conv_id && event->conversation_rename != NULL) {
873 		ConversationRename *conversation_rename = event->conversation_rename;
874 		PurpleChat *chat;
875 
876 		if (g_hash_table_contains(ha->group_chats, conv_id)) {
877 			chat = purple_blist_find_chat(ha->account, conv_id);
878 			if (chat && !purple_strequal(purple_chat_get_alias(chat), conversation_rename->new_name)) {
879 				g_dataset_set_data(ha, "ignore_set_alias", "true");
880 				purple_chat_set_alias(chat, conversation_rename->new_name);
881 				g_dataset_set_data(ha, "ignore_set_alias", NULL);
882 			}
883 		}
884 	}
885 
886 	if (timestamp && conv_id) {
887 		if (pconv == NULL) {
888 			if (g_hash_table_contains(ha->one_to_ones, conv_id)) {
889 				pconv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(g_hash_table_lookup(ha->one_to_ones, conv_id), ha->account));
890 			} else if (g_hash_table_contains(ha->group_chats, conv_id)) {
891 				pconv = PURPLE_CONVERSATION(purple_conversations_find_chat_with_account(conv_id, ha->account));
892 			}
893 		}
894 		if (pconv != NULL) {
895 			gint64 *last_event_timestamp_ptr = (gint64 *)purple_conversation_get_data(pconv, "last_event_timestamp");
896 			if (last_event_timestamp_ptr == NULL) {
897 				last_event_timestamp_ptr = g_new0(gint64, 1);
898 			}
899 			if (timestamp > *last_event_timestamp_ptr) {
900 				*last_event_timestamp_ptr = timestamp;
901 				purple_conversation_set_data(pconv, "last_event_timestamp", last_event_timestamp_ptr);
902 			}
903 		}
904 	}
905 }
906 
907 void
hangouts_received_typing_notification(PurpleConnection * pc,StateUpdate * state_update)908 hangouts_received_typing_notification(PurpleConnection *pc, StateUpdate *state_update)
909 {
910 	HangoutsAccount *ha;
911 	SetTypingNotification *typing_notification = state_update->typing_notification;
912 	const gchar *gaia_id;
913 	const gchar *conv_id;
914 	PurpleIMTypingState typing_state;
915 
916 	if (typing_notification == NULL) {
917 		return;
918 	}
919 
920 	ha = purple_connection_get_protocol_data(pc);
921 
922 	//purple_debug_info("hangouts", "Received new typing event %p\n", typing_notification);
923 	//purple_debug_info("hangouts", "%s\n", pblite_dump_json((ProtobufCMessage *)typing_notification)); //leaky
924 	/* {
925         "state_update_header" : {
926                 "active_client_state" : ACTIVE_CLIENT_STATE_OTHER_ACTIVE,
927                 "request_trace_id" : "-316846338299410553",
928                 "notification_settings" : null,
929                 "current_server_time" : 1453716154770000
930         },
931         "event_notification" : null,
932         "focus_notification" : null,
933         "typing_notification" : {
934                 "conversation_id" : {
935                         "id" : "UgxGdpCK_mSrhBX8hrx4AaABAQ"
936                 },
937                 "sender_id" : {
938                         "gaia_id" : "110174066375061118727",
939                         "chat_id" : "110174066375061118727"
940                 },
941                 "timestamp" : 1453716154770000,
942                 "type" : TYPING_TYPE_STARTED
943         },
944         "notification_level_notification" : null,
945         "reply_to_invite_notification" : null,
946         "watermark_notification" : null,
947         "view_modification" : null,
948         "easter_egg_notification" : null,
949         "conversation" : null,
950         "self_presence_notification" : null,
951         "delete_notification" : null,
952         "presence_notification" : null,
953         "block_notification" : null,
954         "notification_setting_notification" : null,
955         "rich_presence_enabled_state_notification" : null
956 }*/
957 
958 	gaia_id = typing_notification->sender_id->gaia_id;
959 	if (ha->self_gaia_id && g_strcmp0(gaia_id, ha->self_gaia_id) == 0)
960 		return;
961 
962 	conv_id = typing_notification->conversation_id->id;
963 
964 	if (g_hash_table_contains(ha->group_chats, conv_id)) {
965 		// This is a group conversation
966 		PurpleChatConversation *chatconv = purple_conversations_find_chat_with_account(conv_id, ha->account);
967 		if (chatconv != NULL) {
968 			PurpleChatUser *cb = purple_chat_conversation_find_user(chatconv, gaia_id);
969 			PurpleChatUserFlags cbflags;
970 
971 			if (cb == NULL) {
972 				// Getting notified about a buddy we dont know about yet
973 				//TODO add buddy
974 				return;
975 			}
976 			cbflags = purple_chat_user_get_flags(cb);
977 
978 			if (typing_notification->type == TYPING_TYPE__TYPING_TYPE_STARTED)
979 				cbflags |= PURPLE_CHAT_USER_TYPING;
980 			else
981 				cbflags &= ~PURPLE_CHAT_USER_TYPING;
982 
983 			purple_chat_user_set_flags(cb, cbflags);
984 		}
985 		return;
986 	}
987 
988 	switch(typing_notification->type) {
989 		case TYPING_TYPE__TYPING_TYPE_STARTED:
990 			typing_state = PURPLE_IM_TYPING;
991 			break;
992 
993 		case TYPING_TYPE__TYPING_TYPE_PAUSED:
994 			typing_state = PURPLE_IM_TYPED;
995 			break;
996 
997 		default:
998 		case TYPING_TYPE__TYPING_TYPE_STOPPED:
999 			typing_state = PURPLE_IM_NOT_TYPING;
1000 			break;
1001 	}
1002 
1003 	purple_serv_got_typing(pc, gaia_id, 20, typing_state);
1004 }
1005