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, "&");
54 else if (!strcmp(ent, "<"))
55 g_string_append(msg, "<");
56 else if (!strcmp(ent, ">"))
57 g_string_append(msg, ">");
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, "<");
151 g_string_append(html, b ?: s);
152 g_string_append(html, ">");
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