1 /*
2  * GoogleChat 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 "googlechat_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 "googlechat_conversation.h"
32 #include "googlechat.pb-c.h"
33 
34 // From googlechat_pblite
35 gchar *pblite_dump_json(ProtobufCMessage *message);
36 
37 
38 void googlechat_received_other_notification(PurpleConnection *pc, Event *event);
39 void googlechat_received_presence_notification(PurpleConnection *pc, Event *event);
40 void googlechat_received_typing_notification(PurpleConnection *pc, Event *event);
41 void googlechat_received_message_event(PurpleConnection *pc, Event *event);
42 void googlechat_received_read_receipt(PurpleConnection *pc, Event *event);
43 void googlechat_received_group_viewed(PurpleConnection *pc, Event *event);
44 
45 //purple_signal_emit(purple_connection_get_protocol(ha->pc), "googlechat-received-event", ha->pc, events_response.event);
46 
47 void
googlechat_register_events(gpointer plugin)48 googlechat_register_events(gpointer plugin)
49 {
50 	purple_signal_connect(plugin, "googlechat-received-event", plugin, PURPLE_CALLBACK(googlechat_received_typing_notification), NULL);
51 	purple_signal_connect(plugin, "googlechat-received-event", plugin, PURPLE_CALLBACK(googlechat_received_presence_notification), NULL);
52 	purple_signal_connect(plugin, "googlechat-received-event", plugin, PURPLE_CALLBACK(googlechat_received_message_event), NULL);
53 	purple_signal_connect(plugin, "googlechat-received-event", plugin, PURPLE_CALLBACK(googlechat_received_other_notification), NULL);
54 	purple_signal_connect(plugin, "googlechat-received-event", plugin, PURPLE_CALLBACK(googlechat_received_read_receipt), NULL);
55 	purple_signal_connect(plugin, "googlechat-received-event", plugin, PURPLE_CALLBACK(googlechat_received_group_viewed), NULL);
56 }
57 
58 void
googlechat_process_received_event(GoogleChatAccount * ha,Event * event)59 googlechat_process_received_event(GoogleChatAccount *ha, Event *event)
60 {
61 	// Can't just use this ;(
62 	//purple_signal_emit(purple_connection_get_protocol(ha->pc), "googlechat-received-event", ha->pc, event);
63 
64 	size_t n_bodies = 0;
65 	Event__EventBody **bodies = NULL;
66 
67 	//An event can have multiple events within it.  Mangle the structs to split multiples out into singles
68 	if (event->n_bodies) {
69 		n_bodies = event->n_bodies;
70 		bodies = event->bodies;
71 
72 		event->n_bodies = 0;
73 		event->bodies = NULL;
74 	}
75 
76 	// Send an initial 'bare' event, if there is one
77 	if (event->body) {
78 		purple_signal_emit(purple_connection_get_protocol(ha->pc), "googlechat-received-event", ha->pc, event);
79 	}
80 
81 	if (n_bodies > 0) {
82 		Event__EventBody *orig_body = event->body;
83 		guint i;
84 
85 		// loop through all the sub-bodies and make them a primary
86 		for (i = 0; i < n_bodies; i++) {
87 			Event__EventBody *body = bodies[i];
88 
89 			event->body = body;
90 
91 			event->has_type = TRUE;
92 			event->type = body->event_type;
93 
94 			purple_signal_emit(purple_connection_get_protocol(ha->pc), "googlechat-received-event", ha->pc, event);
95 		}
96 
97 		// put everything back the way it was to let memory be free'd
98 		event->body = orig_body;
99 		event->n_bodies = n_bodies;
100 		event->bodies = bodies;
101 	}
102 
103 	gint64 event_time = 0;
104 	if (event->user_revision) {
105 		event_time = event->user_revision->timestamp;
106 	}
107 	if (event->group_revision) {
108 		event_time = event->group_revision->timestamp;
109 	}
110 
111 	if (event_time && event_time > ha->last_event_timestamp) {
112 		// libpurple can't store a 64bit int on a 32bit machine, so convert to something more usable instead (puke)
113 		//  also needs to work cross platform, in case the accounts.xml is being shared (double puke)
114 		purple_account_set_int(ha->account, "last_event_timestamp_high", event_time >> 32);
115 		purple_account_set_int(ha->account, "last_event_timestamp_low", event_time & 0xFFFFFFFF);
116 	}
117 }
118 
119 
120 /*
121 static void
122 googlechat_remove_conversation(GoogleChatAccount *ha, const gchar *conv_id)
123 {
124 	if (g_hash_table_contains(ha->one_to_ones, conv_id)) {
125 		const gchar *buddy_id = g_hash_table_lookup(ha->one_to_ones, conv_id);
126 		PurpleBuddy *buddy = purple_blist_find_buddy(ha->account, buddy_id);
127 
128 		purple_blist_remove_buddy(buddy);
129 		g_hash_table_remove(ha->one_to_ones, conv_id);
130 		g_hash_table_remove(ha->one_to_ones_rev, buddy_id);
131 
132 	} else if (g_hash_table_contains(ha->group_chats, conv_id)) {
133 		PurpleChat *chat = purple_blist_find_chat(ha->account, conv_id);
134 		purple_blist_remove_chat(chat);
135 
136 		g_hash_table_remove(ha->group_chats, conv_id);
137 
138 	} else {
139 		// Unknown conversation!
140 		return;
141 	}
142 }*/
143 
144 
145 void
googlechat_received_other_notification(PurpleConnection * pc,Event * event)146 googlechat_received_other_notification(PurpleConnection *pc, Event *event)
147 {
148 	gchar *json_dump;
149 
150 	if (event->type == EVENT__EVENT_TYPE__MESSAGE_POSTED ||
151 		event->type == EVENT__EVENT_TYPE__TYPING_STATE_CHANGED ||
152 		event->type == EVENT__EVENT_TYPE__GROUP_VIEWED ||
153 		event->type == EVENT__EVENT_TYPE__USER_STATUS_UPDATED_EVENT ||
154 		event->type == EVENT__EVENT_TYPE__READ_RECEIPT_CHANGED
155 	) {
156 		return;
157 	}
158 
159 	purple_debug_info("googlechat", "Received new other event %p\n", event);
160 	json_dump = pblite_dump_json((ProtobufCMessage *)event);
161 	purple_debug_info("googlechat", "%s\n", json_dump);
162 
163 	g_free(json_dump);
164 }
165 
166 void
googlechat_process_presence_result(GoogleChatAccount * ha,UserPresence * presence)167 googlechat_process_presence_result(GoogleChatAccount *ha, UserPresence *presence)
168 {
169 
170 }
171 
172 void
googlechat_received_presence_notification(PurpleConnection * pc,Event * event)173 googlechat_received_presence_notification(PurpleConnection *pc, Event *event)
174 {
175 	GoogleChatAccount *ha;
176 	UserStatusUpdatedEvent *user_status_updated_event;
177 
178 	if (event->type != EVENT__EVENT_TYPE__USER_STATUS_UPDATED_EVENT) {
179 		return;
180 	}
181 	user_status_updated_event = event->body->user_status_updated_event;
182 
183 	ha = purple_connection_get_protocol_data(pc);
184 
185 	const gchar *status_id = NULL;
186 	gboolean reachable = FALSE;
187 	gboolean available = FALSE;
188 	gchar *message = NULL;
189 	UserStatus *user_status = user_status_updated_event->user_status;
190 	const gchar *user_id = user_status->user_id->id;
191 	PurpleBuddy *buddy = purple_blist_find_buddy(ha->account, user_id);
192 
193 	if (buddy != NULL) {
194 		status_id = purple_status_get_id(purple_presence_get_active_status(purple_buddy_get_presence(buddy)));
195 	}
196 
197 	if (user_status->dnd_settings && user_status->dnd_settings->has_dnd_state) {
198 		DndSettings__DndStateState dnd_state = user_status->dnd_settings->dnd_state;
199 
200 		available = TRUE;
201 		if (dnd_state ==  DND_SETTINGS__DND_STATE__STATE__AVAILABLE) {
202 			//TODO fetch presence separately from status
203 			reachable = TRUE;
204 		}
205 
206 		if (reachable && available) {
207 			status_id = purple_primitive_get_id_from_type(PURPLE_STATUS_AVAILABLE);
208 		} else if (reachable) {
209 			status_id = purple_primitive_get_id_from_type(PURPLE_STATUS_AWAY);
210 		} else if (available) {
211 			status_id = purple_primitive_get_id_from_type(PURPLE_STATUS_EXTENDED_AWAY);
212 		} else if (purple_account_get_bool(ha->account, "treat_invisible_as_offline", FALSE)) {
213 			status_id = "gone";
214 		} else {
215 			// GoogleChat contacts are never really unreachable, just invisible
216 			status_id = purple_primitive_get_id_from_type(PURPLE_STATUS_INVISIBLE);
217 		}
218 	} else if (buddy == NULL) {
219 		return;
220 	}
221 
222 	if (user_status != NULL && user_status->custom_status) {
223 		const gchar *status_text = user_status->custom_status->status_text;
224 
225 		if (status_text && *status_text) {
226 			message = g_strdup(status_text);
227 		}
228 	}
229 
230 	if (message != NULL) {
231 		purple_protocol_got_user_status(ha->account, user_id, status_id, "message", message, NULL);
232 	} else {
233 		purple_protocol_got_user_status(ha->account, user_id, status_id, NULL);
234 	}
235 
236 	g_free(message);
237 }
238 
239 static void
googlechat_got_http_image_for_conv(PurpleHttpConnection * connection,PurpleHttpResponse * response,gpointer user_data)240 googlechat_got_http_image_for_conv(PurpleHttpConnection *connection, PurpleHttpResponse *response, gpointer user_data)
241 {
242 	GoogleChatAccount *ha = user_data;
243 	const gchar *url;
244 	const gchar *drive_url;
245 	const gchar *sender_id;
246 	const gchar *conv_id;
247 	PurpleMessageFlags msg_flags;
248 	time_t message_timestamp;
249 	PurpleImage *image;
250 	const gchar *response_data;
251 	size_t response_size;
252 	guint image_id;
253 	gchar *msg;
254 	gchar *escaped_image_url;
255 
256 	if (purple_http_response_get_error(response) != NULL) {
257 		g_dataset_destroy(connection);
258 		return;
259 	}
260 
261 	url = g_dataset_get_data(connection, "url");
262 	drive_url = g_dataset_get_data(connection, "drive_url");
263 	sender_id = g_dataset_get_data(connection, "sender_id");
264 	conv_id = g_dataset_get_data(connection, "conv_id");
265 	msg_flags = GPOINTER_TO_INT(g_dataset_get_data(connection, "msg_flags"));
266 	message_timestamp = GPOINTER_TO_INT(g_dataset_get_data(connection, "message_timestamp"));
267 
268 	response_data = purple_http_response_get_data(response, &response_size);
269 	image = purple_image_new_from_data(g_memdup(response_data, response_size), response_size);
270 	image_id = purple_image_store_add(image);
271 	escaped_image_url = g_markup_escape_text(purple_http_request_get_url(purple_http_conn_get_request(connection)), -1);
272 	if (drive_url) {
273 		msg = g_strdup_printf("<a href='%s'>View in Drive <img id='%u' src='%s' /></a>", drive_url, image_id, escaped_image_url);
274 	} else {
275 		msg = g_strdup_printf("<a href='%s'>View full image <img id='%u' src='%s' /></a>", url, image_id, escaped_image_url);
276 	}
277 	msg_flags |= PURPLE_MESSAGE_IMAGES;
278 
279 	if (g_hash_table_contains(ha->group_chats, conv_id)) {
280 		purple_serv_got_chat_in(ha->pc, g_str_hash(conv_id), sender_id, msg_flags, msg, message_timestamp);
281 	} else {
282 		if (msg_flags & PURPLE_MESSAGE_RECV) {
283 			purple_serv_got_im(ha->pc, sender_id, msg, msg_flags, message_timestamp);
284 		} else {
285 			sender_id = g_hash_table_lookup(ha->one_to_ones, conv_id);
286 			if (sender_id) {
287 				PurpleIMConversation *imconv = purple_conversations_find_im_with_account(sender_id, ha->account);
288 				PurpleMessage *message = purple_message_new_outgoing(sender_id, msg, msg_flags);
289 				if (imconv == NULL) {
290 					imconv = purple_im_conversation_new(ha->account, sender_id);
291 				}
292 				purple_message_set_time(message, message_timestamp);
293 				purple_conversation_write_message(PURPLE_CONVERSATION(imconv), message);
294 			}
295 		}
296 	}
297 
298 	g_free(escaped_image_url);
299 	g_free(msg);
300 	g_dataset_destroy(connection);
301 }
302 
303 static const gchar *
googlechat_format_type_to_string(FormatMetadata__FormatType format_type,gboolean close)304 googlechat_format_type_to_string(FormatMetadata__FormatType format_type, gboolean close)
305 {
306 	static gchar output[16];
307 	int p = 1;
308 
309 	output[0] = '<';
310 	if (close) {
311 		output[1] = '/';
312 		p = 2;
313 	}
314 
315 	if (format_type == FORMAT_METADATA__FORMAT_TYPE__BOLD) {
316 		output[p] = 'B';  p++;
317 	} else if (format_type == FORMAT_METADATA__FORMAT_TYPE__ITALIC) {
318 		output[p] = 'I';  p++;
319 	} else if (format_type == FORMAT_METADATA__FORMAT_TYPE__UNDERLINE) {
320 		output[p] = 'U';  p++;
321 	} // else //TODO
322 
323 	output[p] = '>';
324 	output[p + 1] = '\0';
325 
326 	return output;
327 }
328 
329 void
googlechat_received_message_event(PurpleConnection * pc,Event * event)330 googlechat_received_message_event(PurpleConnection *pc, Event *event)
331 {
332 	GoogleChatAccount *ha;
333 	MessageEvent *message_event;
334 	const gchar *conv_id;
335 	const gchar *sender_id;
336 
337 	if (event->type != EVENT__EVENT_TYPE__MESSAGE_POSTED) {
338 		return;
339 	}
340 	message_event = event->body->message_posted;
341 
342 	ha = purple_connection_get_protocol_data(pc);
343 
344 	Message *message = message_event->message;
345 	guint i;
346 
347 	if (message->local_id && g_hash_table_remove(ha->sent_message_ids, message->local_id)) {
348 		// This probably came from us
349 		return;
350 	}
351 
352 	//TODO safety checks
353 	sender_id = message->creator->user_id->id;
354 	GroupId *group_id = message->id->parent_id->topic_id->group_id;
355 	gboolean is_dm = !!group_id->dm_id;
356 	if (is_dm) {
357 		conv_id = group_id->dm_id->dm_id;
358 	} else {
359 		conv_id = group_id->space_id->space_id;
360 	}
361 
362 
363 	time_t message_timestamp = (message->create_time / 1000000) - ha->server_time_offset;
364 	PurpleMessageFlags msg_flags = (g_strcmp0(sender_id, ha->self_gaia_id) ? PURPLE_MESSAGE_RECV : (PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_REMOTE_SEND | PURPLE_MESSAGE_DELAYED));
365 	if (((message->create_time / 1000000) - time(NULL) - ha->server_time_offset) > 120) {
366 		msg_flags |= PURPLE_MESSAGE_DELAYED;
367 	}
368 	PurpleConversation *pconv = NULL;
369 
370 	//TODO process Annotations to add formatting
371 	gchar *msg = NULL;
372 	GList *format_annotations = NULL;
373 	for (i = 0; i < message->n_annotations; i++) {
374 		Annotation *annotation = message->annotations[i];
375 		if (annotation->type == ANNOTATION_TYPE__FORMAT_DATA || annotation->type == ANNOTATION_TYPE__URL) {
376 			format_annotations = g_list_prepend(format_annotations, annotation);
377 		}
378 	}
379 
380 	if (format_annotations == NULL) {
381 		//shortcut
382 		msg = purple_markup_escape_text(message->text_body, -1);
383 
384 	} else {
385 		GString *msg_out = g_string_new(NULL);
386 		const gchar *current_char = message->text_body;
387 		gunichar c;
388 		gint32 pos = 0;
389 		GList *format;
390 		gint hidden_output = 0;
391 
392 		do {
393 			if (format_annotations != NULL) {
394 				format = format_annotations;
395 				while(format != NULL) {
396 					GList *next_format = format->next;
397 					Annotation *annotation = format->data;
398 					FormatMetadata__FormatType format_type = annotation->format_metadata ? annotation->format_metadata->format_type : 0;
399 
400 					// Opening
401 					if (annotation->start_index == pos) {
402 						if (format_type == FORMAT_METADATA__FORMAT_TYPE__HIDDEN) {
403 							hidden_output++;
404 
405 						} else if (annotation->type == ANNOTATION_TYPE__URL) {
406 							UrlMetadata *url_metadata = annotation->url_metadata;
407 							if (url_metadata && url_metadata->url && url_metadata->url->url) {
408 								gchar *escaped = g_markup_escape_text(url_metadata->url->url, -1);
409 								g_string_append_printf(msg_out, "<A HREF=\"%s\">", escaped);
410 								g_free(escaped);
411 							}
412 
413 						} else {
414 							g_string_append(msg_out, googlechat_format_type_to_string(format_type, FALSE));
415 						}
416 
417 					// Closing
418 					} else if (annotation->length + annotation->start_index == pos) {
419 						if (format_type == FORMAT_METADATA__FORMAT_TYPE__HIDDEN) {
420 							hidden_output--;
421 						} else if (annotation->type == ANNOTATION_TYPE__URL) {
422 							g_string_append(msg_out, "</A>");
423 						} else {
424 							g_string_append(msg_out, googlechat_format_type_to_string(format_type, TRUE));
425 						}
426 
427 						format_annotations = g_list_delete_link(format_annotations, format);
428 					}
429 
430 					format = next_format;
431 				}
432 			}
433 
434 			if (hidden_output == 0) {
435 				// from libpurple/util.c
436 				switch (*current_char)
437 				{
438 					case '&':
439 						g_string_append(msg_out, "&amp;");
440 						break;
441 
442 					case '<':
443 						g_string_append(msg_out, "&lt;");
444 						break;
445 
446 					case '>':
447 						g_string_append(msg_out, "&gt;");
448 						break;
449 
450 					case '"':
451 						g_string_append(msg_out, "&quot;");
452 						break;
453 
454 					default:
455 						c = g_utf8_get_char(current_char);
456 						if ((0x1 <= c && c <= 0x8) ||
457 								(0xb <= c && c <= 0xc) ||
458 								(0xe <= c && c <= 0x1f) ||
459 								(0x7f <= c && c <= 0x84) ||
460 								(0x86 <= c && c <= 0x9f))
461 							g_string_append_printf(msg_out, "&#x%x;", c);
462 						else
463 							g_string_append_unichar(msg_out, c);
464 						break;
465 				}
466 			}
467 			pos++;
468 
469 		} while ((current_char = g_utf8_next_char(current_char)) && *current_char);
470 
471 		msg = g_string_free(msg_out, FALSE);
472 	}
473 
474 	if (!is_dm) {
475 		PurpleChatConversation *chatconv = purple_conversations_find_chat_with_account(conv_id, ha->account);
476 		if (chatconv == NULL) {
477 			//TODO /api/get_group
478 			chatconv = purple_serv_got_joined_chat(ha->pc, g_str_hash(conv_id), conv_id);
479 			purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "conv_id", g_strdup(conv_id));
480 
481 			googlechat_lookup_group_info(ha, conv_id);
482 		}
483 		pconv = PURPLE_CONVERSATION(chatconv);
484 		purple_serv_got_chat_in(pc, g_str_hash(conv_id), sender_id, msg_flags, msg, message_timestamp);
485 
486 	} else {
487 		PurpleIMConversation *imconv = NULL;
488 		// It's most likely a one-to-one message
489 		if (msg_flags & PURPLE_MESSAGE_RECV) {
490 			purple_serv_got_im(pc, sender_id, msg, msg_flags, message_timestamp);
491 		} else {
492 			sender_id = g_hash_table_lookup(ha->one_to_ones, conv_id);
493 			if (sender_id) {
494 				imconv = purple_conversations_find_im_with_account(sender_id, ha->account);
495 				PurpleMessage *pmessage = purple_message_new_outgoing(sender_id, msg, msg_flags);
496 				if (imconv == NULL)
497 				{
498 					imconv = purple_im_conversation_new(ha->account, sender_id);
499 				}
500 				purple_message_set_time(pmessage, message_timestamp);
501 				purple_conversation_write_message(PURPLE_CONVERSATION(imconv), pmessage);
502 			}
503 		}
504 
505 		if (imconv == NULL) {
506 			imconv = purple_conversations_find_im_with_account(sender_id, ha->account);
507 		}
508 		pconv = PURPLE_CONVERSATION(imconv);
509 	}
510 
511 	if (purple_conversation_has_focus(pconv)) {
512 		googlechat_mark_conversation_seen(pconv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
513 	}
514 
515 	g_free(msg);
516 
517 	// Process join/part's
518 	for (i = 0; i < message->n_annotations; i++) {
519 		Annotation *annotation = message->annotations[i];
520 
521 		if (annotation->membership_changed) { //if (annotation->type == ANNOTATION_TYPE__MEMBERSHIP_CHANGED) {
522 			PurpleChatConversation *chatconv = purple_conversations_find_chat_with_account(conv_id, ha->account);
523 			MembershipChangedMetadata *membership_change = annotation->membership_changed;
524 			guint j;
525 
526 			if (membership_change->type == MEMBERSHIP_CHANGED_METADATA__TYPE__LEFT ||
527 				membership_change->type == MEMBERSHIP_CHANGED_METADATA__TYPE__REMOVED ||
528 				membership_change->type == MEMBERSHIP_CHANGED_METADATA__TYPE__BOT_REMOVED) {
529 				//TODO
530 				const gchar *reason = NULL;
531 
532 				for (j = 0; j < membership_change->n_affected_members; j++) {
533 					MemberId *member_id = membership_change->affected_members[j];
534 
535 					purple_chat_conversation_remove_user(chatconv, member_id->user_id->id, reason);
536 
537 					if (g_strcmp0(member_id->user_id->id, ha->self_gaia_id) == 0) {
538 						purple_serv_got_chat_left(ha->pc, g_str_hash(conv_id));
539 						g_hash_table_remove(ha->group_chats, conv_id);
540 						purple_blist_remove_chat(purple_blist_find_chat(ha->account, conv_id));
541 					}
542 				}
543 			} else {
544 				PurpleChatUserFlags cbflags = PURPLE_CHAT_USER_NONE; //TODO
545 
546 				for (j = 0; j < membership_change->n_affected_members; j++) {
547 					MemberId *member_id = membership_change->affected_members[j];
548 
549 					purple_chat_conversation_add_user(chatconv, member_id->user_id->id, NULL, cbflags, TRUE);
550 				}
551 			}
552 		}
553 	}
554 
555 	// Add images
556 	for (i = 0; i < message->n_annotations; i++) {
557 		Annotation *annotation = message->annotations[i];
558 		gchar *image_url = NULL; // Direct image URL
559 		const gchar *url = NULL; // Display URL
560 		const gchar *drive_url = NULL; // Google Drive URL
561 
562 		if (annotation->upload_metadata) {
563 			UploadMetadata *upload_metadata = annotation->upload_metadata;
564 
565 			//const gchar *content_type = upload_metadata->content_type;
566 			const gchar *attachment_token = upload_metadata->attachment_token;
567 
568 			GString *image_url_str = g_string_new(NULL);
569 
570 			g_string_append(image_url_str, "https://chat.google.com/api/get_attachment_url" "?");
571 			//could also be THUMBNAIL_URL
572 			// or DOWNLOAD_URL for a file
573 			g_string_append(image_url_str, "url_type=FIFE_URL&");
574 			//g_string_append_printf(image_url_str, "sz=w%d-h%d&", 0, 0); //TODO
575 			//g_string_append_printf(image_url_str, "content_type=%s&", purple_url_encode(content_type));
576 			g_string_append_printf(image_url_str, "attachment_token=%s&", purple_url_encode(attachment_token));
577 
578 			// this url redirects to the actual url
579 			url = image_url = image_url_str->str;
580 			g_string_free(image_url_str, FALSE);
581 		}
582 
583 		if (annotation->drive_metadata) {
584 			DriveMetadata *drive_metadata = annotation->drive_metadata;
585 
586 			if (drive_metadata->thumbnail_url) {
587 				image_url = g_strdup(drive_metadata->thumbnail_url);
588 				url = drive_metadata->url_fragment;
589 
590 			} else {
591 				const gchar *drive_id = drive_metadata->id;
592 
593 				GString *image_url_str = g_string_new(NULL);
594 
595 				g_string_append(image_url_str, "https://lh3.googleusercontent.com/d/");
596 				g_string_append(image_url_str, purple_url_encode(drive_id));
597 
598 				// the preview
599 				url = image_url = image_url_str->str;
600 
601 				GString *drive_url_str = g_string_new(NULL);
602 				g_string_append(drive_url_str, "https://drive.google.com/open?id=");
603 				g_string_append(drive_url_str, purple_url_encode(drive_id));
604 
605 				// the link
606 				drive_url = drive_url_str->str;
607 
608 				g_string_free(image_url_str, FALSE);
609 				g_string_free(drive_url_str, FALSE);
610 			}
611 		}
612 
613 		if (annotation->url_metadata) {
614 			UrlMetadata *url_metadata = annotation->url_metadata;
615 
616 			if (url_metadata->image_url) {
617 				image_url = g_strdup(url_metadata->image_url);
618 				url = url_metadata->url ? url_metadata->url->url : url_metadata->image_url;
619 
620 			}
621 		}
622 
623 		if (image_url != NULL) {
624 			PurpleHttpConnection *connection;
625 
626 			if (g_strcmp0(purple_core_get_ui(), "BitlBee") == 0) {
627 				// Bitlbee doesn't support images, so just plop a url to the image instead
628 				if (g_hash_table_contains(ha->group_chats, conv_id)) {
629 					purple_serv_got_chat_in(pc, g_str_hash(conv_id), sender_id, msg_flags, url, message_timestamp);
630 				} else {
631 					if (msg_flags & PURPLE_MESSAGE_RECV) {
632 						purple_serv_got_im(pc, sender_id, url, msg_flags, message_timestamp);
633 					} else {
634 						PurpleMessage *img_message = purple_message_new_outgoing(sender_id, url, msg_flags);
635 						purple_message_set_time(img_message, message_timestamp);
636 						purple_conversation_write_message(pconv, img_message);
637 					}
638 				}
639 			} else {
640 				PurpleHttpRequest *request = purple_http_request_new(image_url);
641 
642 				purple_http_request_header_set_printf(request, "Authorization", "Bearer %s", ha->access_token);
643 				purple_http_request_set_max_len(request, -1);
644 
645 				connection = purple_http_request(ha->pc, request, googlechat_got_http_image_for_conv, ha);
646 
647 				g_dataset_set_data_full(connection, "url", g_strdup(url), g_free);
648 				g_dataset_set_data_full(connection, "drive_url", g_strdup(drive_url), g_free);
649 				g_dataset_set_data_full(connection, "sender_id", g_strdup(sender_id), g_free);
650 				g_dataset_set_data_full(connection, "conv_id", g_strdup(conv_id), g_free);
651 				g_dataset_set_data(connection, "msg_flags", GINT_TO_POINTER(msg_flags));
652 				g_dataset_set_data(connection, "message_timestamp", GINT_TO_POINTER(message_timestamp));
653 
654 				purple_http_request_unref(request);
655 			}
656 		}
657 
658 		g_free(image_url);
659 	}
660 
661 
662 	if (pconv != NULL) {
663 		gint64 *last_event_timestamp_ptr = (gint64 *)purple_conversation_get_data(pconv, "last_event_timestamp");
664 		if (last_event_timestamp_ptr == NULL) {
665 			last_event_timestamp_ptr = g_new0(gint64, 1);
666 		}
667 		if (message->create_time > *last_event_timestamp_ptr) {
668 			*last_event_timestamp_ptr = message->create_time;
669 			purple_conversation_set_data(pconv, "last_event_timestamp", last_event_timestamp_ptr);
670 		}
671 	}
672 }
673 
674 void
googlechat_received_read_receipt(PurpleConnection * pc,Event * event)675 googlechat_received_read_receipt(PurpleConnection *pc, Event *event)
676 {
677 	const gchar *user_id;
678 	GroupId *group_id;
679 	const gchar *conv_id;
680 	GoogleChatAccount *ha;
681 	ReadReceiptSet *receipt_set;
682 
683 	if (event->type != EVENT__EVENT_TYPE__READ_RECEIPT_CHANGED ||
684 		!event->body->read_receipt_changed ||
685 		!event->body->read_receipt_changed->read_receipt_set ||
686 		!event->body->read_receipt_changed->read_receipt_set->enabled ||
687 		!event->body->read_receipt_changed->group_id
688 	) {
689 		return;
690 	}
691 
692 	receipt_set = event->body->read_receipt_changed->read_receipt_set;
693 	guint i;
694 	for (i = 0; i < receipt_set->n_read_receipts; i++) {
695 		if (receipt_set->read_receipts[i]->user &&
696 			receipt_set->read_receipts[i]->user->user_id &&
697 			receipt_set->read_receipts[i]->user->user_id->id
698 		) {
699 			user_id = receipt_set->read_receipts[i]->user->user_id->id;
700 			ha = purple_connection_get_protocol_data(pc);
701 
702 			// don't emit our own receipts
703 			if (ha->self_gaia_id && g_strcmp0(user_id, ha->self_gaia_id) != 0) {
704 				group_id = event->body->read_receipt_changed->group_id;
705 				gboolean is_dm = !!group_id->dm_id;
706 				if (is_dm) {
707 					conv_id = group_id->dm_id->dm_id;
708 				} else {
709 					conv_id = group_id->space_id->space_id;
710 				}
711 				if (conv_id) {
712 					if (is_dm) {
713 						// PurpleChat *chat = purple_blist_find_chat(ha->account, conv_id);
714 						PurpleBuddy *buddy = purple_blist_find_buddy(ha->account, user_id);
715 						if (buddy) {
716 							purple_debug_warning("googlechat", "TODO: username %s read DM\n", purple_buddy_get_alias(buddy)); //purple_chat_get_name(chat));
717 						} else {
718 							purple_debug_warning("googlechat", "TODO: userid %s read DM\n", user_id);
719 						}
720 					} else {
721 						PurpleChatConversation *chatconv = purple_conversations_find_chat_with_account(conv_id, ha->account);
722 
723 						if (chatconv) {
724 							PurpleChatUser *cb = purple_chat_conversation_find_user(chatconv, user_id);
725 							if (cb) {
726 								purple_debug_warning("googlechat", "TODO: username %s read chat\n", cb->name);
727 							} else {
728 								purple_debug_warning("googlechat", "TODO: userid %s read chat\n", user_id);
729 							}
730 						}
731 					}
732 				}
733 			}
734 		}
735 	}
736 }
737 
738 
739 void
googlechat_received_group_viewed(PurpleConnection * pc,Event * event)740 googlechat_received_group_viewed(PurpleConnection *pc, Event *event)
741 {
742 	const gchar *user_id;
743 	GroupId *group_id;
744 	const gchar *conv_id;
745 	const gchar *sender_id;
746 	GoogleChatAccount *ha;
747 	PurpleConversation *pconv = NULL;
748 
749 	if (event->type != EVENT__EVENT_TYPE__GROUP_VIEWED ||
750 		!event->user_id ||
751 		!event->user_id->id ||
752 		!event->body->group_viewed->group_id
753 	) {
754 		return;
755 	}
756 
757 	user_id = event->user_id->id;
758 	ha = purple_connection_get_protocol_data(pc);
759 
760 	purple_debug_warning("googlechat", "Received groupview %p from userid %s\n", event, user_id);
761 
762 	// we only expect to receive GROUP_VIEWED messages for our own user
763 	if (ha->self_gaia_id && g_strcmp0(user_id, ha->self_gaia_id) == 0) {
764 		purple_debug_info("googlechat", "...it's us %s\n", user_id);
765 		group_id = event->body->group_viewed->group_id;
766 		gboolean is_dm = !!group_id->dm_id;
767 
768 		if (is_dm) {
769 			conv_id = group_id->dm_id->dm_id;
770 		} else {
771 			conv_id = group_id->space_id->space_id;
772 		}
773 
774 		if (!is_dm) {
775 			purple_debug_info("googlechat", "...it's not a DM\n");
776 			PurpleChatConversation *chatconv = purple_conversations_find_chat_with_account(conv_id, ha->account);
777 			if (chatconv == NULL) {
778 				//TODO /api/get_group
779 				chatconv = purple_serv_got_joined_chat(ha->pc, g_str_hash(conv_id), conv_id);
780 				purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "conv_id", g_strdup(conv_id));
781 				googlechat_lookup_group_info(ha, conv_id);
782 			}
783 			if (chatconv) {
784 				pconv = PURPLE_CONVERSATION(chatconv);
785 			} else {
786 				purple_debug_info("googlechat", "...couldn't find chatconv\n");
787 			}
788 		} else {
789 			purple_debug_info("googlechat", "...it's a DM\n");
790 			PurpleIMConversation *imconv = NULL;
791 			// It's most likely a one-to-one message
792 			sender_id = g_hash_table_lookup(ha->one_to_ones, conv_id);
793 			if (sender_id) {
794 				imconv = purple_conversations_find_im_with_account(sender_id, ha->account);
795 				if (imconv == NULL)
796 				{
797 					imconv = purple_im_conversation_new(ha->account, sender_id);
798 				}
799 			}
800 			if (imconv == NULL) {
801 				imconv = purple_conversations_find_im_with_account(sender_id, ha->account);
802 			}
803 			if (imconv) {
804 				pconv = PURPLE_CONVERSATION(imconv);
805 			} else {
806 				purple_debug_info("googlechat", "...couldn't find imconv\n");
807 			}
808 		}
809 
810 		// the whole point of GROUP_VIEWED seems to be to sync our own seen status
811 		if (pconv) {
812 			purple_debug_warning("googlechat", "TODO: mark conversation '%s' as seen \n", pconv->title);
813 			// googlechat_mark_conversation_seen(pconv, PURPLE_CONVERSATION_UPDATE_UNSEEN);
814 			// warning: this makes infinite loops
815 		} else {
816 			purple_debug_info("googlechat", "...pconv was null\n");
817 		}
818 	} else {
819 		purple_debug_info("googlechat", "...it's not us (%s)\n", user_id);
820 	}
821 }
822 
823 void
googlechat_received_typing_notification(PurpleConnection * pc,Event * event)824 googlechat_received_typing_notification(PurpleConnection *pc, Event *event)
825 {
826 	GoogleChatAccount *ha;
827 	TypingStateChangedEvent *typing_notification;
828 	const gchar *user_id;
829 	const gchar *conv_id;
830 	PurpleIMTypingState typing_state;
831 
832 	if (event->type != EVENT__EVENT_TYPE__TYPING_STATE_CHANGED) {
833 		return;
834 	}
835 	typing_notification = event->body->typing_state_changed_event;
836 
837 	ha = purple_connection_get_protocol_data(pc);
838 
839 	user_id = typing_notification->user_id->id;
840 	if (ha->self_gaia_id && g_strcmp0(user_id, ha->self_gaia_id) == 0)
841 		return;
842 
843 	if (!typing_notification->context->group_id) {
844 		//TODO handle topic_id -> group_id conversion
845 		return;
846 	}
847 
848 	GroupId *group_id = typing_notification->context->group_id;
849 	gboolean is_dm = !!group_id->dm_id;
850 	if (is_dm) {
851 		conv_id = group_id->dm_id->dm_id;
852 	} else {
853 		conv_id = group_id->space_id->space_id;
854 	}
855 
856 	if (!is_dm) {
857 		// This is a group conversation
858 		PurpleChatConversation *chatconv = purple_conversations_find_chat_with_account(conv_id, ha->account);
859 		if (chatconv != NULL) {
860 			PurpleChatUser *cb = purple_chat_conversation_find_user(chatconv, user_id);
861 			PurpleChatUserFlags cbflags;
862 
863 			if (cb == NULL) {
864 				// Getting notified about a buddy we dont know about yet
865 				//TODO add buddy
866 				return;
867 			}
868 			cbflags = purple_chat_user_get_flags(cb);
869 
870 			if (typing_notification->state == TYPING_STATE__TYPING)
871 				cbflags |= PURPLE_CHAT_USER_TYPING;
872 			else
873 				cbflags &= ~PURPLE_CHAT_USER_TYPING;
874 
875 			purple_chat_user_set_flags(cb, cbflags);
876 		}
877 		return;
878 	}
879 
880 	switch(typing_notification->state) {
881 		case TYPING_STATE__TYPING:
882 			typing_state = PURPLE_IM_TYPING;
883 			break;
884 
885 		default:
886 		case TYPING_STATE__STOPPED:
887 			typing_state = PURPLE_IM_NOT_TYPING;
888 			break;
889 	}
890 
891 	purple_serv_got_typing(pc, user_id, 7, typing_state);
892 }
893