1 #include <debug.h>
2 #include <version.h>
3 
4 #include "slack-json.h"
5 #include "slack-rtm.h"
6 #include "slack-api.h"
7 #include "slack-user.h"
8 #include "slack-channel.h"
9 #include "slack-conversation.h"
10 #include "slack-message.h"
11 
slack_html_to_message(SlackAccount * sa,const char * s,PurpleMessageFlags flags)12 gchar *slack_html_to_message(SlackAccount *sa, const char *s, PurpleMessageFlags flags) {
13 
14 	if (flags & PURPLE_MESSAGE_RAW)
15 		return g_strdup(s);
16 
17 	GString *msg = g_string_sized_new(strlen(s));
18 	while (*s) {
19 		const char *ent;
20 		int len;
21 		if ((*s == '@' || *s == '#') && !(flags & PURPLE_MESSAGE_NO_LINKIFY)) {
22 			const char *e = s+1;
23 			/* try to find the end of this command, but not very well -- not sure what characters are valid and eventually will need to deal with spaces */
24 			while (g_ascii_isalnum(*e) || *e == '-' || *e == '_' || (*e == '.' && g_ascii_isalnum(e[1]))) e++;
25 			if (*s == '@') {
26 #define COMMAND(CMD, CMDL) \
27 				if (e-(s+1) == CMDL && !strncmp(s+1, CMD, CMDL)) { \
28 					g_string_append_len(msg, "<!" CMD ">", CMDL+3); \
29 					s = e; \
30 					continue; \
31 				}
32 				COMMAND("here", 4)
33 				COMMAND("channel", 7)
34 				COMMAND("everyone", 8)
35 			}
36 #undef COMMAND
37 			char *t = g_strndup(s+1, e-(s+1));
38 			SlackObject *obj = g_hash_table_lookup(*s == '@' ? sa->user_names : sa->channel_names, t);
39 			g_free(t);
40 			if (obj) {
41 				g_string_append_c(msg, '<');
42 				g_string_append_c(msg, *s);
43 				g_string_append(msg, obj->id);
44 				g_string_append_c(msg, '|');
45 				g_string_append_len(msg, s+1, e-(s+1));
46 				g_string_append_c(msg, '>');
47 				s = e;
48 				continue;
49 			}
50 		}
51 		if ((ent = purple_markup_unescape_entity(s, &len))) {
52 			if (!strcmp(ent, "&"))
53 				g_string_append(msg, "&amp;");
54 			else if (!strcmp(ent, "<"))
55 				g_string_append(msg, "&lt;");
56 			else if (!strcmp(ent, ">"))
57 				g_string_append(msg, "&gt;");
58 			else
59 				g_string_append(msg, ent);
60 			s += len;
61 			continue;
62 		}
63 		if (!g_ascii_strncasecmp(s, "<br>", 4)) {
64 			g_string_append_c(msg, '\n');
65 			s += 4;
66 			continue;
67 		}
68 		/* what about other tags? urls (auto-detected server-side)? dates? */
69 		g_string_append_c(msg, *s++);
70 	}
71 
72 	return g_string_free(msg, FALSE);
73 }
74 
slack_message_to_html(GString * html,SlackAccount * sa,gchar * s,PurpleMessageFlags * flags,gchar * prepend_newline_str)75 void slack_message_to_html(GString *html, SlackAccount *sa, gchar *s, PurpleMessageFlags *flags, gchar *prepend_newline_str) {
76 	if (!s)
77 		return;
78 
79 	if (flags)
80 		*flags |= PURPLE_MESSAGE_NO_LINKIFY;
81 
82 	size_t l = strlen(s);
83 	char *end = &s[l];
84 
85 	while (s < end) {
86 		char c = *s++;
87 		if (c == '\n') {
88 			g_string_append(html, "<BR>");
89 
90 			// This is here for attachments.  If this message is part of an attachment,
91 			// we must add the preprend string after every newline.
92 			if (prepend_newline_str) {
93 				g_string_append(html, prepend_newline_str);
94 			}
95 			continue;
96 		}
97 		if (c != '<') {
98 			g_string_append_c(html, c);
99 			continue;
100 		}
101 
102 		/* found a <tag> */
103 		char *r = memchr(s, '>', end-s);
104 		if (!r)
105 			/* should really be error */
106 			r = end;
107 		else
108 			*r = 0;
109 		char *b = memchr(s, '|', r-s);
110 		if (b) {
111 			*b = 0;
112 			b++;
113 		}
114 		switch (*s) {
115 			case '#':
116 				s++;
117 				g_string_append_c(html, '#');
118 				if (!b) {
119 					SlackChannel *chan = (SlackChannel*)slack_object_hash_table_lookup(sa->channels, s);
120 					if (chan)
121 						b = chan->object.name;
122 				}
123 				g_string_append(html, b ?: s);
124 				break;
125 			case '@':
126 				s++;
127 				g_string_append_c(html, '@');
128 				SlackUser *user = NULL;
129 				if (slack_object_id_is(sa->self->object.id, s)) {
130 					user = sa->self;
131 					if (flags)
132 						*flags |= PURPLE_MESSAGE_NICK;
133 				}
134 				if (!b) {
135 					if (!user)
136 						user = (SlackUser*)slack_object_hash_table_lookup(sa->users, s);
137 					if (user)
138 						b = user->object.name;
139 				}
140 				g_string_append(html, b ?: s);
141 				break;
142 			case '!':
143 				s++;
144 				if (!strcmp(s, "channel") || !strcmp(s, "group") || !strcmp(s, "here") || !strcmp(s, "everyone")) {
145 					if (flags)
146 						*flags |= PURPLE_MESSAGE_NICK;
147 					g_string_append_c(html, '@');
148 					g_string_append(html, b ?: s);
149 				} else {
150 					g_string_append(html, "&lt;");
151 					g_string_append(html, b ?: s);
152 					g_string_append(html, "&gt;");
153 				}
154 				break;
155 			default:
156 				/* URL */
157 				g_string_append(html, "<A HREF=\"");
158 				g_string_append(html, s); /* XXX embedded quotes? */
159 				g_string_append(html, "\">");
160 				g_string_append(html, b ?: s);
161 				g_string_append(html, "</A>");
162 		}
163 		s = r+1;
164 	}
165 }
166 
167 /*
168  *	Changes a "slack color" (i.e. "good", "warning", "danger") to the correct
169  *  RGB color.  From https://api.slack.com/docs/message-attachments
170  */
get_color(const char * c)171 static const gchar *get_color(const char *c) {
172 	if (c == NULL) {
173 		return (gchar * ) "#717274";
174 	} else if (!strcmp(c, "good")) {
175 		return (gchar * ) "#2fa44f";
176 	} else if (!strcmp(c, "warning")) {
177 		return (gchar * ) "#de9e31";
178 	} else if (!strcmp(c, "danger")) {
179 		return (gchar * ) "#d50200";
180 	} else {
181 		return (gchar * ) c;
182 	}
183 }
184 
185 /*
186  * make a link if url is not NULL.  Otherwise, just give the text back.
187  */
link_html(GString * html,char * url,char * text)188 static void link_html(GString *html, char *url, char *text) {
189 	if (!text) {
190 		return;
191 	} else if (url) {
192 		g_string_append_printf(
193 			html,
194 			"<a href=\"%s\">%s</a>",
195 			url,
196 			text
197 		);
198 	} else {
199 		g_string_append(html, text);
200 	}
201 }
202 
203 /*
204  * Converts a single attachment to HTML.  The shape of an attachment is
205  * documented at https://api.slack.com/docs/message-attachments
206  */
slack_attachment_to_html(GString * html,SlackAccount * sa,json_value * attachment)207 static void slack_attachment_to_html(GString *html, SlackAccount *sa, json_value *attachment) {
208 	char *service_name = json_get_prop_strptr(attachment, "service_name");
209 	char *service_link = json_get_prop_strptr(attachment, "service_link");
210 	char *author_name = json_get_prop_strptr(attachment, "author_name");
211 	char *author_subname = json_get_prop_strptr(attachment, "author_subname");
212 
213 	char *author_link = json_get_prop_strptr(attachment, "author_link");
214 	char *text = json_get_prop_strptr(attachment, "text");
215 
216 	//char *fallback = json_get_prop_strptr(attachment, "fallback");
217 	char *pretext = json_get_prop_strptr(attachment, "pretext");
218 
219 	char *title = json_get_prop_strptr(attachment, "title");
220 	char *title_link = json_get_prop_strptr(attachment, "title_link");
221 	char *footer = json_get_prop_strptr(attachment, "footer");
222 	GString *attachment_prefix = g_string_new(NULL);
223 
224 	g_string_printf(attachment_prefix,
225 		"<font color=\"%s\">%s</font>",
226 		get_color(json_get_prop_strptr(attachment, "color")),
227 		purple_account_get_string(sa->account, "attachment_prefix", "▎ ")
228 	);
229 
230 	GString *brtag = g_string_new("<br/>");
231 	g_string_append(brtag,
232 		attachment_prefix->str
233 	);
234 
235 	time_t ts = slack_parse_time(json_get_prop(attachment, "ts"));
236 
237 
238 	// Sometimes, the text of the attachment can be *really* large.  The official
239 	// Slack client will truncate the text at x-characters and have a "Read More"
240 	// link so the user can read the rest of the text.  I wasn't sure what the
241 	// right thing to do for the plugin, so I implemented both the truncated
242 	// version as well as the "just dump all the text naively" version (the
243 	// latter of which is what is uncommented .. I am leaving the truncated
244 	// version commented in in case it is decided that this is the right thing to
245 	// do.
246 
247 	/* GString *truncated_text = g_string_sized_new(0);
248 	g_string_printf(
249 		truncated_text,
250 		"%.480s%s",
251 		(char *) formatted_text,
252 		(strlen(formatted_text) > 480) ? "…" : ""
253 	); */
254 
255 	// pretext
256 	if (pretext) {
257 		g_string_append(html, brtag->str);
258 		slack_message_to_html(html, sa, pretext, NULL, attachment_prefix->str);
259 	}
260 
261 	// service name and author name
262 	if (service_name != NULL || author_name != NULL || author_subname != NULL) {
263 		g_string_append(html, brtag->str);
264 		g_string_append(html, "<b>");
265 		link_html(html, service_link, service_name);
266 		if (service_name && author_name)
267 			g_string_append(html, " - ");
268 		link_html(html, author_link, author_name);
269 		if (author_subname)
270 			g_string_append(html, author_subname);
271 		g_string_append(html, "</b>");
272 	}
273 
274 	// title
275 	if (title) {
276 		g_string_append(html, brtag->str);
277 		g_string_append(html, "<b><i>");
278 		link_html(html, title_link, title);
279 		g_string_append(html, "</i></b>");
280 	}
281 
282 	// main text
283 	if (text) {
284 		g_string_append(html, brtag->str);
285 		g_string_append(html, "<i>");
286 		slack_message_to_html(html, sa, text, NULL, attachment_prefix->str);
287 		g_string_append(html, "</i>");
288 	}
289 
290 	// fields
291 	json_value *fields = json_get_prop_type(attachment, "fields", array);
292 	if (fields) {
293 		for (int i=0; i<fields->u.array.length; i++) {
294 			json_value *field = fields->u.array.values[i];
295 			char *title = json_get_prop_strptr(field, "title");
296 			char *value = json_get_prop_strptr(field, "value");
297 
298 			g_string_append_printf(html, "<br />%s<b>", attachment_prefix->str);
299 
300 			// Run the title through the conversion to html.
301 			slack_message_to_html(html, sa, title, NULL, attachment_prefix->str);
302 			g_string_append(html, "</b>: <i>");
303 
304 			// Run the value through the conversion to html.
305 			slack_message_to_html(html, sa, value, NULL, attachment_prefix->str);
306 			g_string_append(html, "</i>");
307 		}
308 	}
309 
310 	// footer
311 	if (footer) {
312 		g_string_append(html, brtag->str);
313 		g_string_append(html, footer);
314 	}
315 	if (ts) {
316 		g_string_append(html, brtag->str);
317 		g_string_append(html, ctime(&ts));
318 	}
319 
320 	g_string_free(brtag, TRUE);
321 	g_string_free(attachment_prefix, TRUE);
322 }
323 
slack_file_to_html(GString * html,SlackAccount * sa,json_value * file)324 static void slack_file_to_html(GString *html, SlackAccount *sa, json_value *file) {
325 	char *title = json_get_prop_strptr(file, "title");
326 	char *url = json_get_prop_strptr(file, "url_private");
327 	if (!url)
328 		url = json_get_prop_strptr(file, "permalink");
329 
330 	g_string_append_printf(html, "<br/>%s<a href=\"%s\">%s</a>",
331 		purple_account_get_string(sa->account, "attachment_prefix", "▎ "),
332 		url ?: "",
333 		title ?: "file");
334 }
335 
slack_json_to_html(GString * html,SlackAccount * sa,json_value * message,PurpleMessageFlags * flags)336 void slack_json_to_html(GString *html, SlackAccount *sa, json_value *message, PurpleMessageFlags *flags) {
337 	const char *subtype = json_get_prop_strptr(message, "subtype");
338 	int i;
339 
340 	if (flags && json_get_prop_boolean(message, "hidden", FALSE))
341 		*flags |= PURPLE_MESSAGE_INVISIBLE;
342 
343 	if (!g_strcmp0(subtype, "me_message"))
344 		g_string_append(html, "/me ");
345 	else if (subtype && flags)
346 		*flags |= PURPLE_MESSAGE_SYSTEM;
347 
348 	json_value *thread = json_get_prop(message, "thread_ts");
349 	if (thread) {
350 		time_t tt = slack_parse_time(thread);
351 		g_string_append(html, purple_time_format(localtime(&tt)));
352 		g_string_append(html, "⤷  ");
353 	}
354 
355 	slack_message_to_html(html, sa, json_get_prop_strptr(message, "text"), flags, NULL);
356 
357 	json_value *files = json_get_prop_type(message, "files", array);
358 	if (files)
359 		for (i=0; i < files->u.array.length; i++)
360 			slack_file_to_html(html, sa, files->u.array.values[i]);
361 
362 	// If there are attachements, show them.
363 	json_value *attachments = json_get_prop_type(message, "attachments", array);
364 	if (attachments)
365 		for (i=0; i < attachments->u.array.length; i++)
366 			slack_attachment_to_html(html, sa, attachments->u.array.values[i]);
367 }
368 
slack_handle_message(SlackAccount * sa,SlackObject * obj,json_value * json,PurpleMessageFlags flags)369 void slack_handle_message(SlackAccount *sa, SlackObject *obj, json_value *json, PurpleMessageFlags flags) {
370 	if (!obj) {
371 		purple_debug_warning("slack", "Message to unknown channel %s\n", json_get_prop_strptr(json, "channel"));
372 		return;
373 	}
374 	const char *subtype     = json_get_prop_strptr(json, "subtype");
375 	json_value *ts          = json_get_prop(json, "ts");
376 	time_t mt = slack_parse_time(ts);
377 	json_value *message     = json;
378 	GString *html = g_string_new(NULL);
379 
380 	if (!g_strcmp0(subtype, "message_changed")) {
381 		message = json_get_prop(json, "message");
382 		json_value *old_message = json_get_prop(json, "previous_message");
383 		/* this may consist only of added attachments, no changed text */
384 		gboolean changed = g_strcmp0(json_get_prop_strptr(message, "text"), json_get_prop_strptr(old_message, "text"));
385 		g_string_append(html, "<font color=\"#717274\"><i>[edit]</i></font> ");
386 		slack_json_to_html(html, sa, message, &flags);
387 		if (old_message && changed) {
388 			g_string_append(html, "<br>(Old message: ");
389 			slack_json_to_html(html, sa, old_message, NULL);
390 			g_string_append(html, ")");
391 		}
392 	}
393 	else if (!g_strcmp0(subtype, "message_deleted")) {
394 		message = json_get_prop(json, "previous_message");
395 		g_string_append(html, "(<font color=\"#717274\"><i>Deleted message</i></font>");
396 		if (message) {
397 			g_string_append(html, ": ");
398 			slack_json_to_html(html, sa, message, &flags);
399 		}
400 		g_string_append(html, ")");
401 	}
402 	else
403 		slack_json_to_html(html, sa, message, &flags);
404 
405 	if (!html->len) {
406 		/* if after all of that we still have no message, just dump it */
407 		g_string_free(html, TRUE);
408 		purple_debug_info("slack", "Ignoring unparsed message\n");
409 		return;
410 	}
411 
412 	const char *user_id = json_get_prop_strptr(message, "user");
413 	SlackUser *user = NULL;
414 	if (slack_object_id_is(sa->self->object.id, user_id)) {
415 		user = sa->self;
416 #if PURPLE_VERSION_CHECK(2,12,0)
417 		flags |= PURPLE_MESSAGE_REMOTE_SEND;
418 #else
419 		flags |= 0x10000;
420 #endif
421 		flags |= PURPLE_MESSAGE_SEND;
422 		flags &= ~PURPLE_MESSAGE_RECV;
423 	}
424 	/* for bots providing different display name */
425 	const char *username = json_get_prop_strptr(message, "username");
426 	if (username)
427 		flags &= ~PURPLE_MESSAGE_SYSTEM;
428 
429 	PurpleConversation *conv = NULL;
430 	if (SLACK_IS_CHANNEL(obj)) {
431 		SlackChannel *chan = (SlackChannel*)obj;
432 		/* Channel */
433 		if (!chan->cid) {
434 			if (!purple_account_get_bool(sa->account, "open_chat", FALSE)) {
435 				g_string_free(html, TRUE);
436 				return;
437 			}
438 			slack_chat_open(sa, chan);
439 		}
440 
441 		if (!user)
442 			user = (SlackUser*)slack_object_hash_table_lookup(sa->users, user_id);
443 
444 		PurpleConvChat *chat = slack_channel_get_conversation(sa, chan);
445 		if (chat) {
446 			conv = purple_conv_chat_get_conversation(chat);
447 			if (!subtype);
448 			else if (!strcmp(subtype, "channel_topic") ||
449 					!strcmp(subtype, "group_topic"))
450 				purple_conv_chat_set_topic(chat, user ? user->object.name : user_id, json_get_prop_strptr(json, "topic"));
451 		}
452 
453 		serv_got_chat_in(sa->gc, chan->cid, user ? user->object.name : user_id ?: username ?: "", flags, html->str, mt);
454 	} else if (SLACK_IS_USER(obj)) {
455 		SlackUser *im = (SlackUser*)obj;
456 		/* IM */
457 		conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_IM, im->object.name, sa->account);
458 		if (slack_object_id_is(im->object.id, user_id))
459 			serv_got_im(sa->gc, im->object.name, html->str, flags, mt);
460 		else {
461 			if (!conv)
462 				conv = purple_conversation_new(PURPLE_CONV_TYPE_IM, sa->account, im->object.name);
463 			if (!user)
464 				/* is this necessary? shouldn't be anyone else in here */
465 				user = (SlackUser*)slack_object_hash_table_lookup(sa->users, user_id);
466 			purple_conversation_write(conv, user ? user->object.name : user_id ?: username, html->str, flags, mt);
467 		}
468 	}
469 
470 	g_string_free(html, TRUE);
471 
472 	/* update most recent ts for later marking */
473 	const char *tss = json_get_strptr(ts);
474 	if (slack_ts_cmp(tss, obj->last_mesg) > 0) {
475 		g_free(obj->last_mesg);
476 		obj->last_mesg = g_strdup(tss);
477 	}
478 }
479 
handle_message(SlackAccount * sa,gpointer data,SlackObject * obj)480 static void handle_message(SlackAccount *sa, gpointer data, SlackObject *obj) {
481 	json_value *json = data;
482 	slack_handle_message(sa, obj, json, PURPLE_MESSAGE_RECV);
483 	json_value_free(json);
484 }
485 
slack_message(SlackAccount * sa,json_value * json)486 gboolean slack_message(SlackAccount *sa, json_value *json) {
487 	slack_conversation_retrieve(sa, json_get_prop_strptr(json, "channel"), handle_message, json);
488 	return TRUE;
489 }
490 
491 typedef struct {
492 	PurpleConvChat *chat;
493 	gchar *name;
494 } SlackChatBuddy;
495 
slack_unset_typing_cb(SlackChatBuddy * chatbuddy)496 static gboolean slack_unset_typing_cb(SlackChatBuddy *chatbuddy) {
497 	PurpleConvChatBuddy *cb = purple_conv_chat_cb_find(chatbuddy->chat, chatbuddy->name);
498 	if (cb) {
499 		purple_conv_chat_user_set_flags(chatbuddy->chat, chatbuddy->name, cb->flags & ~PURPLE_CBFLAGS_TYPING);
500 	}
501 
502 	g_free(chatbuddy->name);
503 	chatbuddy->name = NULL;
504 	return FALSE;
505 }
506 
slack_user_typing(SlackAccount * sa,json_value * json)507 void slack_user_typing(SlackAccount *sa, json_value *json) {
508 	const char *user_id    = json_get_prop_strptr(json, "user");
509 	const char *channel_id = json_get_prop_strptr(json, "channel");
510 
511 	SlackUser *user = (SlackUser*)slack_object_hash_table_lookup(sa->users, user_id);
512 	SlackChannel *chan;
513 	if (user && slack_object_id_is(user->im, channel_id)) {
514 		/* IM */
515 		serv_got_typing(sa->gc, user->object.name, 4, PURPLE_TYPING);
516 	} else if (user && (chan = (SlackChannel*)slack_object_hash_table_lookup(sa->channels, channel_id))) {
517 		/* Channel */
518 		PurpleConvChat *chat = slack_channel_get_conversation(sa, chan);
519 		PurpleConvChatBuddy *cb = chat ? purple_conv_chat_cb_find(chat, user->object.name) : NULL;
520 		if (cb) {
521 			purple_conv_chat_user_set_flags(chat, user->object.name, cb->flags | PURPLE_CBFLAGS_TYPING);
522 
523 			guint timeout = GPOINTER_TO_UINT(g_dataset_get_data(user, "typing_timeout"));
524 			SlackChatBuddy *chatbuddy = g_dataset_get_data(user, "chatbuddy");
525 			if (timeout) {
526 				purple_timeout_remove(timeout);
527 				if (chatbuddy) {
528 					g_free(chatbuddy->name);
529 					g_free(chatbuddy);
530 				}
531 			}
532 			chatbuddy = g_new0(SlackChatBuddy, 1);
533 			chatbuddy->chat = chat;
534 			chatbuddy->name = g_strdup(user->object.name);
535 			timeout = purple_timeout_add_seconds(4, (GSourceFunc)slack_unset_typing_cb, chatbuddy);
536 
537 			g_dataset_set_data(user, "typing_timeout", GUINT_TO_POINTER(timeout));
538 			g_dataset_set_data(user, "chatbuddy", chatbuddy);
539 		}
540 	} else {
541 		purple_debug_warning("slack", "Unhandled typing: %s@%s\n", user_id, channel_id);
542 	}
543 }
544 
slack_send_typing(PurpleConnection * gc,const char * who,PurpleTypingState state)545 unsigned int slack_send_typing(PurpleConnection *gc, const char *who, PurpleTypingState state) {
546 	SlackAccount *sa = gc->proto_data;
547 
548 	if (state != PURPLE_TYPING)
549 		return 0;
550 
551 	SlackUser *user = g_hash_table_lookup(sa->user_names, who);
552 	if (!user || !*user->im)
553 		return 0;
554 
555 	GString *channel = append_json_string(g_string_new(NULL), user->im);
556 	slack_rtm_send(sa, NULL, NULL, "typing", "channel", channel->str, NULL);
557 	g_string_free(channel, TRUE);
558 
559 	return 3;
560 }
561