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