1 /*
2 * Mattermost plugin for libpurple
3 * Copyright (C) 2016 Eion Robb
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 <stdio.h>
20 #include <stdlib.h>
21 #include <string.h>
22 #ifdef __GNUC__
23 #include <unistd.h>
24 #endif
25 #include <errno.h>
26
27 #include <glib.h>
28 #include <json-glib/json-glib.h>
29 #include "glibcompat.h"
30
31 #include <purple.h>
32
33 #include "purplecompat.h"
34 #include "image-store.h"
35 #include "image.h"
36 #include "libmattermost-json.h"
37 #include "libmattermost-markdown.h"
38 #include "libmattermost-helpers.h"
39 #include "libmattermost.h"
40
41 static gint
mm_get_next_seq(MattermostAccount * ma)42 mm_get_next_seq(MattermostAccount *ma)
43 {
44 return ma->seq++;
45 }
46
47 static void
mm_update_cookies(MattermostAccount * ma,const GList * cookie_headers)48 mm_update_cookies(MattermostAccount *ma, const GList *cookie_headers)
49 {
50 const gchar *cookie_start;
51 const gchar *cookie_end;
52 gchar *cookie_name;
53 gchar *cookie_value;
54 const GList *cur;
55
56 for (cur = cookie_headers; cur != NULL; cur = g_list_next(cur))
57 {
58 cookie_start = cur->data;
59
60 cookie_end = strchr(cookie_start, '=');
61 if (cookie_end != NULL) {
62 cookie_name = g_strndup(cookie_start, cookie_end-cookie_start);
63 cookie_start = cookie_end + 1;
64 cookie_end = strchr(cookie_start, ';');
65 if (cookie_end != NULL) {
66 cookie_value = g_strndup(cookie_start, cookie_end-cookie_start);
67 cookie_start = cookie_end;
68
69 g_hash_table_replace(ma->cookie_table, cookie_name, cookie_value);
70 }
71 }
72 }
73 }
74
75
76 static void
mm_response_callback(PurpleHttpConnection * http_conn,PurpleHttpResponse * response,gpointer user_data)77 mm_response_callback(PurpleHttpConnection *http_conn,
78 PurpleHttpResponse *response, gpointer user_data)
79 {
80 gsize body_len;
81 const gchar *body = purple_http_response_get_data(response, &body_len);
82 const gchar *error_message = purple_http_response_get_error(response);
83 const GList *headers = purple_http_response_get_headers_by_name(response, "Set-Cookie");
84
85 MattermostProxyConnection *conn = user_data;
86 JsonParser *parser = json_parser_new();
87
88 conn->ma->http_conns = g_slist_remove(conn->ma->http_conns, http_conn);
89
90 mm_update_cookies(conn->ma, headers);
91
92 if (body == NULL && error_message != NULL) {
93 //connection error - unersolvable dns name, non existing server
94 gchar *error_msg_formatted = g_strdup_printf(_("Connection error: %s."), error_message);
95 purple_connection_error(conn->ma->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, error_msg_formatted);
96 g_free(error_msg_formatted);
97 g_free(conn);
98 return;
99 }
100
101 if (body != NULL && !json_parser_load_from_data(parser, body, body_len, NULL)) {
102 //purple_debug_error("mattermost", "Error parsing response: %s\n", body);
103 if (conn->callback) {
104 JsonNode *dummy_node = json_node_new(JSON_NODE_OBJECT);
105 JsonObject *dummy_object = json_object_new();
106
107 json_node_set_object(dummy_node, dummy_object);
108 json_object_set_string_member(dummy_object, "body", body);
109 json_object_set_int_member(dummy_object, "len", body_len);
110 if (body_len >= 12 && g_str_has_prefix(body, "HTTP/1.")) {
111 json_object_set_int_member(dummy_object, "status_code", g_ascii_strtoll(body + 9, NULL, 10));
112 } else {
113 json_object_set_int_member(dummy_object, "status_code", 500);
114 }
115 g_dataset_set_data(dummy_node, "raw_body", (gpointer) body);
116
117 conn->callback(conn->ma, dummy_node, conn->user_data);
118
119 g_dataset_destroy(dummy_node);
120 json_node_free(dummy_node);
121 json_object_unref(dummy_object);
122 }
123 } else {
124 JsonNode *root = json_parser_get_root(parser);
125
126 purple_debug_misc("mattermost", "Got response: %s\n", body);
127 if (conn->callback) {
128 conn->callback(conn->ma, root, conn->user_data);
129 }
130 }
131
132 g_object_unref(parser);
133 g_free(conn);
134 }
135
136 gchar *
137 mm_build_url(MattermostAccount *ma, const gchar *url_format, ...)
138 __attribute__ ((format (printf, 2, 3)));
139
140 gchar *
mm_build_url(MattermostAccount * ma,const gchar * url_format,...)141 mm_build_url(MattermostAccount *ma, const gchar *url_format, ...)
142 {
143 GString *url = g_string_new(NULL);
144 const gchar *last_cur, *cur, *tok;
145 va_list args;
146
147 if (purple_account_get_bool(ma->account, "use-ssl", TRUE)) {
148 g_string_append(url, "https://");
149 } else {
150 g_string_append(url, "http://");
151 }
152 g_string_append(url, ma->server);
153
154 g_string_append(url, ma->api_endpoint);
155
156 va_start(args, url_format);
157 for(last_cur = cur = url_format; cur; last_cur = cur, cur = strchr(cur, '%')) {
158 g_string_append_len(url, last_cur, cur - last_cur);
159
160 if (*cur == '%') {
161 if (*(cur + 1) == 's') {
162 tok = va_arg(args, char *);
163 g_string_append_uri_escaped(url, tok, NULL, TRUE);
164 } else if (*(cur + 1) == '%') {
165 g_string_append_c(url, '%');
166 } else if (*(cur + 1) == 'd') {
167 int d = va_arg(args, int);
168 g_string_append_printf(url, "%d", d);
169 } else if (*(cur + 1) == 'c') {
170 char c = va_arg(args, int);
171 g_string_append_c(url, c);
172 } else if (strncmp((cur + 1), G_GINT64_FORMAT, sizeof(G_GINT64_FORMAT) - 1) == 0) {
173 gint64 i = va_arg(args, gint64);
174 g_string_append_printf(url, "%" G_GINT64_FORMAT, i);
175 cur += sizeof(G_GINT64_FORMAT) - 2;
176 }
177 cur += 2;
178 }
179 }
180 va_end(args);
181
182 g_string_append(url, last_cur);
183
184 return g_string_free(url, FALSE);
185 }
186
187 static gboolean
mm_check_mattermost_response(MattermostAccount * ma,JsonNode * node,gchar * errtitle,gchar * errtext,gboolean show)188 mm_check_mattermost_response(MattermostAccount *ma, JsonNode *node, gchar *errtitle, gchar *errtext, gboolean show)
189 {
190 if (json_node_get_node_type(node) == JSON_NODE_OBJECT) {
191 JsonObject *response = json_node_get_object(node);
192 if (json_object_get_int_member(response, "status_code") >= 400) {
193 if (show) {
194 purple_notify_error(ma->pc, errtitle, errtext, json_object_get_string_member(response, "message"), purple_request_cpar_from_connection(ma->pc));
195 }
196 return FALSE;
197 }
198 return TRUE;
199 }
200 if (json_node_get_node_type(node) == JSON_NODE_ARRAY) {
201 return TRUE;
202 }
203 purple_notify_error(ma->pc, _("Error"), _("Cannot parse Mattermost reply"), _("(not json object or array)"), purple_request_cpar_from_connection(ma->pc));
204 return FALSE;
205 }
206
207 static void
mm_fetch_url(MattermostAccount * ma,const gchar * url,const guint optype,const gchar * postdata,const guint postdata_size,MattermostProxyCallbackFunc callback,gpointer user_data)208 mm_fetch_url(MattermostAccount *ma, const gchar *url, const guint optype, const gchar *postdata, const guint postdata_size, MattermostProxyCallbackFunc callback, gpointer user_data)
209 {
210 PurpleAccount *account;
211 MattermostProxyConnection *conn;
212 PurpleHttpConnection *http_conn;
213
214 account = ma->account;
215 if (purple_account_is_disconnected(account)) return;
216
217 conn = g_new0(MattermostProxyConnection, 1);
218 conn->ma = ma;
219 conn->callback = callback;
220 conn->user_data = user_data;
221
222 purple_debug_info("mattermost", "Fetching url %s\n", url);
223
224
225 PurpleHttpRequest *request = purple_http_request_new(url);
226 purple_http_request_header_set(request, "Accept", "*/*");
227 purple_http_request_header_set(request, "User-Agent", MATTERMOST_USERAGENT);
228 purple_http_request_header_set(request, "X-Requested-With", "XMLHttpRequest");
229 if (ma->session_token) {
230 purple_http_request_header_set_printf(request, "Authorization", "Bearer %s", ma->session_token);
231 }
232
233 if (postdata) {
234 purple_debug_info("mattermost", "With postdata %s\n", postdata);
235
236 if (postdata[0] == '{') {
237 purple_http_request_header_set(request, "Content-Type", "application/json");
238 purple_http_request_set_contents(request, postdata, -1);
239 } else if (postdata_size > 0){
240 purple_http_request_header_set(request, "Content-Type", "application/octet-stream");
241 purple_http_request_set_contents(request, postdata, postdata_size);
242 } else {
243 purple_http_request_header_set(request, "Content-Type", "application/x-www-form-urlencoded");
244 purple_http_request_set_contents(request, postdata, -1);
245 }
246
247 }
248 switch(optype) {
249 case MATTERMOST_HTTP_DELETE: purple_http_request_set_method(request,"DELETE"); break;
250 case MATTERMOST_HTTP_PUT: purple_http_request_set_method(request,"PUT"); break;
251 case MATTERMOST_HTTP_POST: purple_http_request_set_method(request,"POST"); break;
252 default: purple_http_request_set_method(request,"GET");
253 }
254 http_conn = purple_http_request(ma->pc, request, mm_response_callback, conn);
255 purple_http_request_unref(request);
256
257 if (http_conn != NULL)
258 ma->http_conns = g_slist_prepend(ma->http_conns, http_conn);
259
260 }
261
262 const gchar *
mm_split_topic(gchar * str)263 mm_split_topic(gchar *str)
264 {
265 gchar *p = g_strstr_len(str, -1, MATTERMOST_CHAT_TOPIC_SEP);
266 if (p == NULL) return NULL;
267 *p = '\0';
268 return p + strlen(MATTERMOST_CHAT_TOPIC_SEP);
269 }
270
271 const gchar *
mm_make_topic(const gchar * header,const gchar * purpose,const gchar * old_topic)272 mm_make_topic(const gchar *header, const gchar *purpose, const gchar *old_topic)
273 {
274 //TODO: limit len !
275 const gchar *old_purpose = mm_split_topic((gchar *)old_topic);
276 const gchar *old_header = old_topic;
277
278 const gchar *topic = g_strconcat((header && *header) ? header : old_header, MATTERMOST_CHAT_TOPIC_SEP, (purpose && *purpose) ? purpose : old_purpose, NULL);
279
280 return topic;
281 }
282
283 static void
mm_send_email_cb(PurpleBuddy * buddy)284 mm_send_email_cb(PurpleBuddy *buddy)
285 {
286 PurpleBlistNode *bnode = PURPLE_BLIST_NODE(buddy);
287 const gchar *email = purple_blist_node_get_string(bnode, "email");
288 const gchar *first_name = purple_blist_node_get_string(bnode, "first_name");
289 const gchar *last_name = purple_blist_node_get_string(bnode, "last_name");
290 GString *full_email = g_string_new("mailto:");
291
292 if (first_name) {
293 g_string_append_printf(full_email, "%s ", first_name);
294 }
295 if (last_name) {
296 g_string_append_printf(full_email, "%s ", last_name);
297 }
298
299 g_string_append_printf(full_email, "<%s>", email);
300
301 gchar *uri = g_string_free(full_email, FALSE);
302 purple_notify_uri(purple_account_get_connection(purple_buddy_get_account(buddy)), uri);
303 g_free(uri);
304 }
305
306 static GList *
mm_buddy_menu(PurpleBuddy * buddy)307 mm_buddy_menu(PurpleBuddy *buddy)
308 {
309 GList *menu = NULL;
310 if (purple_blist_node_get_string(PURPLE_BLIST_NODE(buddy), "email")) {
311 PurpleMenuAction *action = purple_menu_action_new(_("Email Buddy"), PURPLE_CALLBACK(mm_send_email_cb), NULL, NULL);
312 menu = g_list_append(menu, action);
313 }
314 return menu;
315 }
316
317 static GList *
mm_blist_node_menu(PurpleBlistNode * node)318 mm_blist_node_menu(PurpleBlistNode *node)
319 {
320 if(PURPLE_BUDDY(node)) {
321 return mm_buddy_menu((PurpleBuddy *) node);
322 }
323 return NULL;
324 }
325
326 static const gchar *
mm_get_first_team_id(MattermostAccount * ma)327 mm_get_first_team_id(MattermostAccount *ma)
328 {
329 GList *team_ids = g_hash_table_get_keys(ma->teams);
330 const gchar *first_team_id = team_ids ? team_ids->data : NULL;
331
332 g_list_free(team_ids);
333
334 return first_team_id;
335 }
336
337 static void mm_get_user_prefs(MattermostAccount *ma);
338
339 PurpleGroup* mm_get_or_create_default_group();
340 static void mm_get_history_of_room(MattermostAccount *ma, MattermostChannel *channel, gint64 since);
341
342 static void mm_start_socket(MattermostAccount *ma);
343 static void mm_socket_write_json(MattermostAccount *ma, JsonObject *data);
344 static void mm_get_users_by_ids(MattermostAccount *ma, GList *ids);
345 static void mm_get_avatar(MattermostAccount *ma, PurpleBuddy *buddy);
346
347 static void mm_join_room(MattermostAccount *ma, MattermostChannel *channel);
348 static PurpleChatUserFlags mm_role_to_purple_flag(MattermostAccount *ma, const gchar *rolelist);
349
350 static void mm_get_channel_by_id(MattermostAccount *ma, const gchar *team_id, const gchar *id);
351 static void mm_mark_room_messages_read_timeout_response(MattermostAccount *ma, JsonNode *node, gpointer user_data);
352 static void mm_save_user_pref(MattermostAccount *ma, MattermostUserPref *pref);
353 static void mm_close(PurpleConnection *pc);
354 const gchar *
mm_get_alias(MattermostUser * mu)355 mm_get_alias(MattermostUser *mu)
356 {
357 gchar *nickname = NULL;
358 gchar *full_name = NULL;
359 gchar *alias = NULL;
360
361 if (mu->nickname && *mu->nickname) { nickname = g_strconcat(" (",mu->nickname,")",NULL); }
362 full_name = g_strconcat(mu->first_name ? mu->first_name : "", (mu->first_name && *mu->first_name) ? " " : "", mu->last_name, nickname, NULL);
363 alias = g_strdup((full_name && *full_name) ? full_name : (mu->email && *mu->email) ? mu->email : NULL);
364
365 g_free(nickname);
366 g_free(full_name);
367
368 return alias;
369 }
370
371 const gchar *
mm_get_chat_alias(MattermostAccount * ma,MattermostChannel * ch)372 mm_get_chat_alias(MattermostAccount *ma, MattermostChannel *ch)
373 {
374 gchar *alias = NULL;
375 gchar *type = NULL;
376
377 //FIXME: redo with some pattern matching.. this is ugly.
378 if (ch->type && purple_strequal(ch->type,MATTERMOST_CHANNEL_TYPE_STRING(MATTERMOST_CHANNEL_GROUP))) {
379 const gchar *tmpa = g_strjoinv("", g_strsplit(ch->display_name, ma->username, -1));
380 const gchar *tmpb = g_strjoinv(",", g_strsplit(tmpa,", ",-1));
381 const gchar *tmpc = g_strjoinv(",", g_strsplit(tmpb,",,",-1));
382 if (g_str_has_prefix(tmpc,",")) {
383 alias = g_strndup(tmpc+1,strlen(tmpc));
384 } else {
385 alias = g_strdup(tmpc);
386 }
387 return alias;
388 }
389
390 type = g_strconcat((ch->type && purple_strequal(ch->type,MATTERMOST_CHANNEL_TYPE_STRING(MATTERMOST_CHANNEL_PRIVATE))) ? MATTERMOST_CHANNEL_PRIVATE_VISUAL : "", NULL);
391
392 alias = g_strconcat(type, ch->display_name, MATTERMOST_CHANNEL_SEPARATOR_VISUAL, g_hash_table_lookup(ma->teams_display_names, ch->team_id), NULL);
393
394 g_free(type);
395
396 return alias;
397 }
398 static void mm_set_group_chat(MattermostAccount *ma, const gchar *team_id, const gchar *channel_name, const gchar *channel_id);
399
400 // only non-changing values are channel_id and team_id !
401 // name and display_name for teams and channels can change
402 PurpleChat *
mm_purple_blist_find_chat(MattermostAccount * ma,const gchar * channel_id)403 mm_purple_blist_find_chat(MattermostAccount *ma, const gchar *channel_id)
404 {
405 PurpleBlistNode *bnode;
406 for (bnode = purple_blist_get_root(); bnode != NULL; bnode = purple_blist_node_next(bnode, FALSE)) {
407 if (!PURPLE_IS_CHAT(bnode)) continue;
408 if (purple_chat_get_account(PURPLE_CHAT(bnode)) != ma->account) continue;
409
410 GHashTable *components = purple_chat_get_components(PURPLE_CHAT(bnode));
411
412 if (purple_strequal(g_hash_table_lookup(components, "id"), channel_id)) return PURPLE_CHAT(bnode);
413 }
414 return NULL;
415 }
416
417 void
mm_purple_blist_remove_chat(MattermostAccount * ma,const gchar * channel_id)418 mm_purple_blist_remove_chat(MattermostAccount *ma, const gchar *channel_id)
419 {
420 PurpleBlistNode *bnode;
421 for (bnode = purple_blist_get_root(); bnode != NULL; bnode = purple_blist_node_next(bnode, FALSE)) {
422 if (!PURPLE_IS_CHAT(bnode)) continue;
423 if (purple_chat_get_account(PURPLE_CHAT(bnode)) != ma->account) continue;
424
425 GHashTable *components = purple_chat_get_components(PURPLE_CHAT(bnode));
426
427 if (purple_strequal(g_hash_table_lookup(components, "id"), channel_id)) purple_blist_remove_chat(PURPLE_CHAT(bnode));
428 }
429 }
430
431 static gboolean
mm_channel_is_hidden(MattermostAccount * ma,const gchar * id)432 mm_channel_is_hidden(MattermostAccount *ma, const gchar *id)
433 {
434 GList *prefs;
435
436 for(prefs=ma->user_prefs; prefs != NULL; prefs = g_list_next(prefs)) {
437 MattermostUserPref *pref = prefs->data;
438 if(purple_strequal(pref->name,id)) {
439 if(purple_strequal(pref->category,"direct_channel_show") || purple_strequal(pref->category,"group_channel_show")) {
440 if(purple_strequal(pref->value,"false")) {
441 return TRUE;
442 }
443 }
444 }
445 }
446 return FALSE;
447 }
448
449 static gint64
mm_find_channel_approximate_view_time(MattermostAccount * ma,const gchar * id)450 mm_find_channel_approximate_view_time(MattermostAccount *ma, const gchar *id)
451 {
452 // GList *prefs;
453 gint64 now = (g_get_real_time() / 1000); // -(60*60*24*5*1000); //(- 5 days for debug, remove !)
454 // gint64 then = 0;
455
456 // if (!id) return now;
457
458 //OK, so this does not work as expected in MM server 5.0 .. lets do timekeeping ourselves.
459 // for (prefs=ma->user_prefs; prefs != NULL; prefs = g_list_next(prefs)) {
460 // MattermostUserPref *pref = prefs->data;
461 // if (purple_strequal(pref->category,"channel_approximate_view_time") && purple_strequal(pref->name,id)) {
462 // then = g_ascii_strtoll(pref->value,NULL,10);
463 // return (then ? then : now);
464 // }
465 // }
466 return now;
467 }
468
469 gboolean
470 mm_idle_updater_timeout(gpointer data);
471
472 void
473 mm_set_status(PurpleAccount *account, PurpleStatus *status);
474
475 static void
mm_add_channels_to_blist(MattermostAccount * ma,JsonNode * node,gpointer user_data)476 mm_add_channels_to_blist(MattermostAccount *ma, JsonNode *node, gpointer user_data)
477 {
478 gchar *team_id = user_data;
479 //gboolean first_team = FALSE;
480
481 //if (purple_strequal(mm_get_first_team_id(ma),team_id)) first_team = TRUE;
482 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting Mattermost channels"),TRUE)) return;
483
484 JsonArray *channels = json_node_get_array(node);
485 guint i, len = json_array_get_length(channels);
486 GList *mm_users = NULL;
487 GList *mm_channels = NULL;
488 GList *j = NULL;
489 GList *removenodes = NULL;
490 PurpleBlistNode *bnode;
491
492 // channels = buddies and chats
493 for (i = 0; i < len; i++) {
494 MattermostChannel *mm_channel = g_new0(MattermostChannel,1);
495 JsonObject *channel = json_array_get_object_element(channels, i);
496 mm_channel->id = g_strdup(json_object_get_string_member(channel, "id"));
497 mm_channel->display_name = g_strdup(json_object_get_string_member(channel, "display_name"));
498 mm_channel->type = g_strdup(json_object_get_string_member(channel, "type"));
499 mm_channel->creator_id = g_strdup(json_object_get_string_member(channel, "creator_id"));
500 mm_channel->channel_approximate_view_time = mm_find_channel_approximate_view_time(ma, mm_channel->id);
501
502 const gchar *name = json_object_get_string_member(channel, "name");
503
504 if (mm_channel->type && *(mm_channel->type) == MATTERMOST_CHANNEL_DIRECT) {
505 MattermostUser *mm_user = g_new0(MattermostUser, 1);
506 gchar **names = g_strsplit(name, "__", 2);
507 mm_user->user_id = g_strdup(purple_strequal(names[0], ma->self->user_id) ? names[1] : names[0]);
508 mm_user->room_id = g_strdup(mm_channel->id);
509 g_strfreev(names);
510
511 if (mm_channel_is_hidden(ma, mm_user->user_id)) {
512 mm_g_free_mattermost_user(mm_user);
513 } else {
514 mm_users = g_list_prepend(mm_users, mm_user);
515 }
516
517 } else {
518 // group channels do not belong to any team, avoid duplicating.
519 //if (mm_channel->type && *(mm_channel->type) == MATTERMOST_CHANNEL_GROUP && !first_team) continue;
520 //OK : this is done for each team now, but we get group channels below other in initial sort list. no dups.
521
522 mm_channel->name=g_strdup(name);
523 mm_channel->team_id = g_strdup(json_object_get_string_member(channel, "team_id")); // NULL for group channels
524 if (mm_channel_is_hidden(ma, mm_channel->id)) {
525 mm_g_free_mattermost_channel(mm_channel);
526 } else {
527 mm_channels = g_list_prepend(mm_channels, mm_channel);
528 }
529 }
530
531 }
532
533 // remove from blist unseen buddies and chats (removed MM channels)
534 for (bnode = purple_blist_get_root(); bnode != NULL; bnode = purple_blist_node_next(bnode, FALSE)) {
535 MattermostChannel *tmpchannel = g_new0(MattermostChannel,1);
536 MattermostUser *tmpuser = g_new0(MattermostUser,1);
537
538 gboolean founduser, foundchannel;
539
540 if (PURPLE_IS_CHAT(bnode) && purple_chat_get_account(PURPLE_CHAT(bnode)) == ma->account) {
541 GHashTable *components = purple_chat_get_components(PURPLE_CHAT(bnode));
542 tmpchannel->id = g_hash_table_lookup(components, "id");
543 tmpchannel->team_id = g_hash_table_lookup(components, "team_id");
544 tmpchannel->name = g_hash_table_lookup(components, "name");
545 tmpchannel->type = g_hash_table_lookup(components, "type");
546 tmpchannel->display_name = g_hash_table_lookup(components, "display_name");
547
548 if(tmpchannel->team_id == NULL || purple_strequal(tmpchannel->team_id, team_id)) {
549 GList *tmplist;foundchannel = FALSE;
550 for(tmplist=mm_channels;tmplist != NULL; tmplist=g_list_next(tmplist)){
551 MattermostChannel *tmp2channel = tmplist->data;
552 if(purple_strequal(tmp2channel->id,tmpchannel->id)) {
553 foundchannel = TRUE;
554 }
555 }
556 if (!foundchannel || mm_channel_is_hidden(ma, tmpchannel->id)) {
557 removenodes = g_list_prepend(removenodes, bnode);
558 }
559 }
560
561 } else if (PURPLE_IS_BUDDY(bnode) && purple_buddy_get_account(PURPLE_BUDDY(bnode)) == ma->account) {
562 tmpuser->room_id = g_strdup(purple_blist_node_get_string(bnode, "room_id"));
563 tmpuser->user_id = g_strdup(purple_blist_node_get_string(bnode, "user_id"));
564 tmpuser->email = g_strdup(purple_blist_node_get_string(bnode, "email"));
565 GList *tmplist;founduser = FALSE;
566 for(tmplist=mm_users;tmplist != NULL; tmplist=g_list_next(tmplist)){
567 MattermostUser *tmp2user = tmplist->data;
568 if(purple_strequal(tmp2user->user_id,tmpuser->user_id)) {
569 founduser = TRUE;
570 }
571 }
572 if (!founduser || mm_channel_is_hidden(ma, tmpuser->room_id)) {
573 removenodes = g_list_prepend(removenodes, bnode);
574 }
575 }
576 g_free(tmpchannel);
577 g_free(tmpuser);
578 }
579
580 //TODO: use mm_remove_blist_by_id here.
581 for (j = removenodes; j != NULL; j = j->next) {
582 if (PURPLE_IS_CHAT(j->data)) {
583 purple_blist_remove_chat(PURPLE_CHAT(j->data));
584 } else if (PURPLE_IS_BUDDY(j->data)) {
585 purple_blist_remove_buddy(PURPLE_BUDDY(j->data));
586 }
587 }
588 g_list_free(removenodes);
589
590 //gboolean autojoin = purple_account_get_bool(ma->account, "use-autojoin", FALSE);
591
592
593 mm_channels = g_list_sort(mm_channels, mm_compare_channels_by_display_name_int);
594 mm_channels = g_list_sort(mm_channels, mm_compare_channels_by_type_int);
595
596 for (j = mm_channels; j != NULL; j=j->next) {
597 MattermostChannel *channel = j->data;
598 mm_set_group_chat(ma, channel->team_id, channel->name, channel->id);
599
600 PurpleChat *chat = mm_purple_blist_find_chat(ma, channel->id);
601
602 if (chat == NULL) {
603 GHashTable *defaults = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
604
605 const gchar *alias;
606 g_hash_table_insert(defaults, "team_id", g_strdup(channel->team_id));
607 g_hash_table_insert(defaults, "id", g_strdup(channel->id));
608 g_hash_table_insert(defaults, "creator_id", g_strdup(channel->creator_id));
609 g_hash_table_insert(defaults, "type", g_strdup(channel->type));
610 g_hash_table_insert(defaults, "display_name", g_strdup(channel->display_name));
611
612 alias = mm_get_chat_alias(ma,channel);
613
614 if (channel->type && *(channel->type) != MATTERMOST_CHANNEL_GROUP) {
615 g_hash_table_insert(defaults, "name", g_strconcat(channel->name, MATTERMOST_CHANNEL_SEPARATOR, g_hash_table_lookup(ma->teams, channel->team_id), NULL));
616 } else {
617 g_hash_table_insert(defaults, "name", g_strdup(channel->name));
618 }
619
620 //g_hash_table_insert(defaults,"display_name",g_strdup(alias));
621
622 chat = purple_chat_new(ma->account, alias, defaults);
623 purple_blist_add_chat(chat, mm_get_or_create_default_group(), NULL);
624 purple_blist_node_set_bool(PURPLE_BLIST_NODE(chat), "gtk-autojoin", FALSE /*autojoin*/);
625 purple_blist_node_set_bool(PURPLE_BLIST_NODE(chat), "gtk-persistent", TRUE);
626
627 purple_chat_set_alias(chat, alias);
628 g_hash_table_replace(ma->group_chats, g_strdup(channel->id), g_strdup(channel->name));
629 g_hash_table_replace(ma->group_chats_rev, g_strdup(channel->name), g_strdup(channel->id));
630 g_hash_table_replace(ma->aliases,g_strdup(channel->id),g_strdup(alias));
631 if (channel->creator_id) {
632 g_hash_table_replace(ma->group_chats_creators, g_strdup(channel->id), g_strdup(channel->creator_id));
633 }
634 }
635
636 const gchar *alias;
637 alias = mm_get_chat_alias(ma,channel);
638
639 g_hash_table_replace(ma->aliases,g_strdup(channel->id),g_strdup(alias));
640
641 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel->id));
642
643 if (chatconv || purple_blist_node_get_bool(PURPLE_BLIST_NODE(chat), "gtk-autojoin")) {
644
645 PurpleChatConversation *conv = purple_serv_got_joined_chat(ma->pc, g_str_hash(channel->id), alias);
646 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "id", g_strdup(channel->id));
647 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "team_id", g_strdup(channel->team_id));
648 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "name", g_strdup(channel->name));
649 purple_conversation_present(PURPLE_CONVERSATION(conv));
650 }
651 // already called from mm_join_chat
652 if (!purple_blist_node_get_bool(PURPLE_BLIST_NODE(chat), "gtk-autojoin")) mm_get_channel_by_id(ma, channel->team_id, channel->id);
653 }
654
655 mm_get_users_by_ids(ma,mm_users);
656
657 // Is this the last team we are waiting to receive channels
658 // for? If so mark the connection as connected.
659 ma->groupchat_team_count --;
660 if (ma->groupchat_team_count == 0) {
661 purple_connection_set_state(ma->pc, PURPLE_CONNECTION_CONNECTED);
662 mm_set_status(ma->account, purple_presence_get_active_status(purple_account_get_presence(ma->account)));
663 ma->idle_timeout = g_timeout_add_seconds(270, mm_idle_updater_timeout, ma->pc);
664 }
665
666
667 }
668
669 static void
mm_get_open_channels_for_team(MattermostAccount * ma,const gchar * team_id)670 mm_get_open_channels_for_team(MattermostAccount *ma, const gchar *team_id)
671 {
672 gchar *url;
673
674 //FIXME: v4 API bug ? 'me' instead of user_id does not work here ? ...
675 url = mm_build_url(ma,"/users/%s/teams/%s/channels", ma->self->user_id, team_id);
676 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_add_channels_to_blist, g_strdup(team_id));
677 g_free(url);
678 }
679
680 gboolean mm_idle_updater_timeout(gpointer data);
681
682
683 void mm_set_status(PurpleAccount *account, PurpleStatus *status);
684
685 static gchar *mm_purple_flag_to_role(PurpleChatUserFlags flags);
686
687 static PurpleNotifyUserInfo *
mm_user_info(MattermostUser * mu)688 mm_user_info(MattermostUser *mu)
689 {
690 PurpleNotifyUserInfo *user_info = purple_notify_user_info_new();
691 purple_notify_user_info_add_pair_plaintext(user_info,_("Nickname"), mu->nickname);
692 purple_notify_user_info_add_pair_plaintext(user_info,_("First Name"), mu->first_name);
693 purple_notify_user_info_add_pair_plaintext(user_info,_("Last Name"), mu->last_name);
694 purple_notify_user_info_add_pair_plaintext(user_info,_("Email address"), mu->email);
695 purple_notify_user_info_add_pair_plaintext(user_info,_("Position"), mu->position);
696 purple_notify_user_info_add_pair_plaintext(user_info,_("Locale"), mu->locale);
697 purple_notify_user_info_add_section_break(user_info);
698 purple_notify_user_info_add_pair_plaintext(user_info,_("Username"), mu->username);
699 purple_notify_user_info_add_pair_plaintext(user_info,_("User ID"), mu->user_id);
700
701 gchar *rolelist = mm_purple_flag_to_role(mu->roles);
702 purple_notify_user_info_add_pair_plaintext(user_info,_("Roles"), rolelist);
703 g_free(rolelist);
704
705 return user_info;
706 }
707
708 static void
mm_about_server(PurpleProtocolAction * action)709 mm_about_server(PurpleProtocolAction *action)
710 {
711 PurpleConnection *pc = purple_protocol_action_get_connection(action);
712 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
713 PurpleNotifyUserInfo *user_info = purple_notify_user_info_new();
714 purple_notify_user_info_add_pair_plaintext(user_info,_("Server Version"), ma->client_config->server_version);
715 purple_notify_user_info_add_pair_plaintext(user_info,_("Site Name"), ma->client_config->site_name);
716 purple_notify_user_info_add_pair_plaintext(user_info,_("Site URL"), ma->client_config->site_url);
717 purple_notify_user_info_add_pair_plaintext(user_info,_("Support Email"), ma->client_config->support_email);
718 purple_notify_user_info_add_pair_plaintext(user_info,_("Report Problems"), ma->client_config->report_a_problem_link);
719
720 purple_notify_user_info_add_section_break(user_info);
721
722 if(ma->client_config->enable_commands) {
723 purple_notify_user_info_add_pair_plaintext(user_info,_("Slash commands"),_("enabled"));
724 } else {
725 purple_notify_user_info_add_pair_plaintext(user_info,_("Slash commands"),_("disabled"));
726 }
727
728 if(ma->client_config->public_link) {
729 purple_notify_user_info_add_pair_plaintext(user_info,_("Public file links"),_("enabled"));
730 } else {
731 purple_notify_user_info_add_pair_plaintext(user_info,_("Public file links"),_("disabled"));
732 }
733
734 purple_notify_user_info_add_section_break(user_info);
735
736 purple_notify_user_info_add_pair_plaintext(user_info,_("Build number"), ma->client_config->build_number);
737 purple_notify_user_info_add_pair_plaintext(user_info,_("Build hash"), ma->client_config->build_hash);
738 purple_notify_user_info_add_pair_plaintext(user_info,_("Build date"), ma->client_config->build_date);
739 purple_notify_user_info_add_pair_plaintext(user_info,_("Enterprise ready"), ma->client_config->enterprise_ready);
740
741 purple_notify_userinfo(ma->pc, _("Mattermost Server"), user_info, NULL, NULL);
742
743 purple_notify_user_info_destroy(user_info);
744 }
745
746 static void
mm_about_commands(PurpleProtocolAction * action)747 mm_about_commands(PurpleProtocolAction *action)
748 {
749 PurpleConnection *pc = purple_protocol_action_get_connection(action);
750 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
751 PurpleNotifyUserInfo *user_info = purple_notify_user_info_new();
752 GList *i = NULL;
753 MattermostCommand *cmd = NULL;
754 for(i=ma->commands;i;i=i->next) {
755 cmd = i->data;
756 const gchar *info = g_strconcat("/",cmd->trigger," ",
757 strlen(cmd->auto_complete_hint) ? g_strconcat(cmd->auto_complete_hint," | ",NULL) : " | ",
758 strlen(cmd->auto_complete_desc) ? g_strconcat(cmd->auto_complete_desc," ",NULL) : "",
759 ( !strlen(cmd->auto_complete_desc) && strlen(cmd->description) ) ? g_strconcat(cmd->description," ",NULL) : " ",
760 strlen(cmd->team_id) ? g_strconcat("[team only: ",g_hash_table_lookup(ma->teams, cmd->team_id),"]",NULL) : "",
761 NULL);
762
763 purple_notify_user_info_add_pair_plaintext(user_info,cmd->trigger, info);
764 }
765 purple_notify_userinfo(ma->pc, _("Mattermost Slash Commands"), user_info, NULL, NULL);
766 purple_notify_user_info_destroy(user_info);
767 }
768
769 static void
mm_about_myself(PurpleProtocolAction * action)770 mm_about_myself(PurpleProtocolAction *action)
771 {
772 PurpleConnection *pc = purple_protocol_action_get_connection(action);
773 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
774 PurpleNotifyUserInfo *user_info = mm_user_info(ma->self);
775
776 purple_notify_user_info_add_section_break(user_info);
777
778 GList *team_names = g_hash_table_get_values(ma->teams);
779 GList *team_name = NULL;
780
781 for (team_name = team_names; team_name != NULL; team_name=team_name->next) {
782 purple_notify_user_info_add_pair_plaintext(user_info,_("Team"), team_name->data);
783 }
784 g_list_free(team_names);
785
786 purple_notify_user_info_add_section_break(user_info);
787
788 GString *mention_keys = g_string_new(NULL);
789 GList *i;
790
791 for (i = ma->mention_words; i != NULL; i=i->next) {
792 g_string_append(mention_keys,i->data);
793 g_string_append(mention_keys,",");
794 }
795
796 gchar *tmp = g_string_free(mention_keys, FALSE);
797 purple_notify_user_info_add_pair_plaintext(user_info,_("Mention"), tmp);
798
799
800 purple_notify_userinfo(ma->pc, ma->self->username, user_info, NULL, NULL);
801
802 purple_notify_user_info_destroy(user_info);
803
804 g_free(tmp);
805 }
806
807 static void
mm_got_teams(MattermostAccount * ma,JsonNode * node,gpointer user_data)808 mm_got_teams(MattermostAccount *ma, JsonNode *node, gpointer user_data)
809 {
810 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting Mattermost teams"),TRUE)) { return; };
811
812 JsonArray *teams = json_node_get_array(node);
813 guint i, len = json_array_get_length(teams);
814
815 // Set the counter so we know how many teams to wait for
816 // before marking the connection as connected.
817 ma->groupchat_team_count = len;
818 for (i = 0; i < len; i++) {
819 JsonObject *team = json_array_get_object_element(teams, i);
820
821 const gchar *team_id = json_object_get_string_member(team, "id");
822 const gchar *name = json_object_get_string_member(team, "name");
823 const gchar *display_name = json_object_get_string_member(team, "display_name");
824
825 g_hash_table_replace(ma->teams, g_strdup(team_id), g_strdup(name));
826 g_hash_table_replace(ma->teams_display_names, g_strdup(team_id), g_strdup(display_name));
827
828 mm_get_commands_for_team(ma, team_id);
829 mm_get_open_channels_for_team(ma, team_id);
830 }
831 }
832
833
834 static void
mm_set_user_blist(MattermostAccount * ma,MattermostUser * mu,PurpleBuddy * buddy)835 mm_set_user_blist(MattermostAccount *ma, MattermostUser *mu, PurpleBuddy *buddy)
836 {
837 PurpleBlistNode *bnode = PURPLE_BLIST_NODE(buddy);
838
839 purple_blist_node_set_string(bnode, "nickname", mu->nickname);
840 purple_blist_node_set_string(bnode, "first_name", mu->first_name);
841 purple_blist_node_set_string(bnode, "last_name", mu->last_name);
842
843 // room_id exists only if a direct channel has been created.
844 if (mu->room_id && *mu->room_id) {
845 purple_blist_node_set_string(bnode, "room_id", mu->room_id);
846 }
847
848 purple_blist_node_set_string(bnode, "email", mu->email);
849 purple_blist_node_set_string(bnode, "locale", mu->locale);
850 purple_blist_node_set_string(bnode, "position", mu->position);
851 purple_blist_node_set_int(bnode, "roles", mu->roles);
852
853 if(purple_account_get_bool(ma->account, "use-alias", FALSE)) {
854 gchar *alias = g_strdup(mm_get_alias(mu));
855 purple_buddy_set_local_alias(buddy, alias);
856 g_free(alias);
857 }
858
859 }
860
861 static void
mm_info_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)862 mm_info_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
863 {
864 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting Mattermost user information"),TRUE)) return;
865
866 JsonObject *user = json_node_get_object(node);
867
868 PurpleBuddy *buddy = user_data;
869 MattermostUser *mu = g_new0(MattermostUser, 1);
870
871 mu->nickname = g_strdup(json_object_get_string_member(user, "nickname"));
872 mu->first_name = g_strdup(json_object_get_string_member(user, "first_name"));
873 mu->last_name = g_strdup(json_object_get_string_member(user, "last_name"));
874 mu->email = g_strdup(json_object_get_string_member(user, "email"));
875 mu->username = g_strdup(json_object_get_string_member(user, "username"));
876 mu->user_id = g_strdup(json_object_get_string_member(user, "id"));
877 mu->locale = g_strdup(json_object_get_string_member(user, "locale"));
878 mu->position = g_strdup(json_object_get_string_member(user, "position"));
879 mu->roles = mm_role_to_purple_flag(ma, json_object_get_string_member(user, "roles"));
880
881 PurpleNotifyUserInfo *user_info = mm_user_info(mu);
882
883 purple_notify_userinfo(ma->pc, purple_buddy_get_name(buddy), user_info, NULL, NULL);
884
885 purple_notify_user_info_destroy(user_info);
886
887 if (!purple_strequal(purple_buddy_get_name(buddy), ma->self->username)) {
888 mm_set_user_blist(ma, mu, buddy);
889 }
890
891 mm_g_free_mattermost_user(mu);
892 }
893
894 static void
mm_get_info(PurpleConnection * pc,const gchar * username)895 mm_get_info(PurpleConnection *pc,const gchar *username)
896 {
897 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
898 PurpleBuddy *buddy = purple_blist_find_buddy(ma->account, username);
899 gchar *url;
900
901 // Don't add BOT to buddies
902 // hope no user account/alias ends in [BOT] ...
903 if (purple_str_has_suffix(username, MATTERMOST_BOT_LABEL)) {
904 PurpleNotifyUserInfo *user_info = purple_notify_user_info_new();
905 purple_notify_user_info_add_pair_plaintext(user_info,_("BOT Name"), purple_strreplace(username, MATTERMOST_BOT_LABEL, ""));
906 gchar *info = g_strconcat(purple_account_get_bool(ma->account, "use-ssl", TRUE) ? "see https://" : "http://", ma->server, "/ -> team -> integrations", NULL); //We do not know which team is the BOT on.
907 purple_notify_user_info_add_pair_plaintext(user_info,_("Information"), info);
908 purple_notify_user_info_add_section_break(user_info);
909 purple_notify_user_info_add_pair_plaintext(user_info, NULL, _("Mattermost webhook integration"));
910 purple_notify_userinfo(ma->pc, username, user_info, NULL, NULL);
911 purple_notify_user_info_destroy(user_info);
912 g_free(info);
913 return;
914 }
915
916 if (buddy == NULL) {
917 buddy = purple_buddy_new(ma->account, username, NULL);
918 }
919
920 url = mm_build_url(ma,"/users/username/%s", username);
921 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_info_response, buddy);
922 g_free(url);
923 }
924
925 static void
mm_add_user_to_channel_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)926 mm_add_user_to_channel_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
927 {
928 MattermostChannel *channel = user_data;
929 JsonObject *obj = json_node_get_object(node);
930
931 if (json_object_get_int_member(obj, "status_code") >= 400) {
932 purple_notify_error(ma->pc, "Error", "Error joining channel", json_object_get_string_member(obj, "message"), purple_request_cpar_from_connection(ma->pc));
933 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel->id));
934 if (chatconv) purple_conv_chat_left(chatconv);
935 return;
936 }
937
938 if (mm_purple_blist_find_chat(ma, channel->id) == NULL) {
939 PurpleChat *chat = NULL;
940 GHashTable *defaults = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
941 const gchar *alias = mm_get_chat_alias(ma, channel);
942
943 g_hash_table_insert(defaults, "team_id", g_strdup(channel->team_id));
944 g_hash_table_insert(defaults, "id", g_strdup(channel->id));
945 g_hash_table_insert(defaults, "type", g_strdup(channel->type));
946 g_hash_table_insert(defaults, "creator_id", g_strdup(channel->creator_id));
947 g_hash_table_insert(defaults,"display_name",g_strdup(channel->display_name));
948
949 if (channel->type && *(channel->type) != MATTERMOST_CHANNEL_GROUP) {
950 g_hash_table_insert(defaults, "name", g_strconcat(channel->name, MATTERMOST_CHANNEL_SEPARATOR, g_hash_table_lookup(ma->teams, channel->team_id), NULL));
951 } else {
952 g_hash_table_insert(defaults, "name", g_strdup(channel->name));
953 }
954
955 chat = purple_chat_new(ma->account, alias, defaults);
956 purple_blist_add_chat(chat, mm_get_or_create_default_group(), NULL);
957
958 mm_set_group_chat(ma, channel->team_id, channel->name, channel->id);
959
960 purple_blist_node_set_bool(PURPLE_BLIST_NODE(chat), "gtk-persistent", TRUE);
961 purple_blist_node_set_bool(PURPLE_BLIST_NODE(chat), "gtk-autojoin", FALSE /*autojoin*/);
962
963 purple_chat_set_alias(chat, alias);
964
965 }
966 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel->id));
967 if (chatconv != NULL) {
968 purple_chat_conversation_set_topic(chatconv, NULL, mm_make_topic(channel->header, channel->purpose, purple_chat_conversation_get_topic(chatconv)));
969 }
970 mm_join_room(ma, channel);
971 }
972
973 static void
mm_add_user_to_channel(MattermostAccount * ma,MattermostChannel * channel)974 mm_add_user_to_channel(MattermostAccount *ma, MattermostChannel *channel)
975 {
976 const gchar *user_id;
977 JsonObject *data;
978 gchar *postdata;
979 gchar *url;
980
981 data = json_object_new();
982 user_id = ma->self->user_id;
983 json_object_set_string_member(data, "user_id", user_id);
984
985 postdata = json_object_to_string(data);
986
987 url = mm_build_url(ma,"/channels/%s/members", channel->id);
988
989 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_add_user_to_channel_response, channel);
990
991 g_free(postdata);
992 g_free(url);
993 }
994
995 static void
mm_get_channel_by_id_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)996 mm_get_channel_by_id_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
997 {
998 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting Mattermost channel information"),TRUE)) return;
999
1000 JsonObject *channel = json_node_get_object(node);
1001 const gchar *id = json_object_get_string_member(channel, "id");
1002 const gchar *name = json_object_get_string_member(channel, "name");
1003 const gchar *display_name = json_object_get_string_member(channel, "display_name");
1004 const gchar *type = json_object_get_string_member(channel, "type");
1005 const gchar *creator_id = json_object_get_string_member(channel, "creator_id");
1006 const gchar *team_id = user_data;
1007 const gchar *header = json_object_get_string_member(channel, "header");
1008 const gchar *purpose = json_object_get_string_member(channel, "purpose");
1009
1010 const gchar *alias;
1011 //gboolean autojoin = purple_account_get_bool(ma->account, "use-autojoin", FALSE);
1012
1013 if (creator_id && *creator_id) {
1014 g_hash_table_replace(ma->group_chats_creators, g_strdup(id), g_strdup(creator_id));
1015 }
1016
1017 MattermostChannel *tmpchannel = g_new0(MattermostChannel,1);
1018 tmpchannel->id = g_strdup(id);
1019 tmpchannel->display_name = g_strdup(display_name);
1020 tmpchannel->type = g_strdup(type);
1021 tmpchannel->creator_id = g_strdup(creator_id);
1022 tmpchannel->name = g_strdup(name);
1023 tmpchannel->team_id = g_strdup(team_id);
1024 tmpchannel->header = g_strdup(header);
1025 tmpchannel->purpose = g_strdup(purpose);
1026 tmpchannel->channel_approximate_view_time = mm_find_channel_approximate_view_time(ma, tmpchannel->id);
1027
1028 alias = mm_get_chat_alias(ma, tmpchannel);
1029
1030 if (mm_purple_blist_find_chat(ma, id) == NULL) {
1031 // user is trying to join a new channel
1032 mm_add_user_to_channel(ma, tmpchannel);
1033 } else {
1034 // user is already present in the channel, we just open the chat conv
1035 purple_chat_set_alias(mm_purple_blist_find_chat(ma, id),alias);
1036 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(tmpchannel->id));
1037 if (chatconv != NULL) {
1038 purple_chat_conversation_set_topic(chatconv, NULL, mm_make_topic(header, purpose, purple_chat_conversation_get_topic(chatconv)));
1039 }
1040 mm_join_room(ma, tmpchannel);
1041 }
1042 }
1043
1044
1045 static void
mm_get_channel_by_id(MattermostAccount * ma,const gchar * team_id,const gchar * id)1046 mm_get_channel_by_id(MattermostAccount *ma, const gchar *team_id, const gchar *id)
1047 {
1048 gchar *url;
1049 GList *tmpl;
1050 gboolean joined = FALSE;
1051
1052 for(tmpl=ma->joined_channels;tmpl != NULL; tmpl=g_list_next(tmpl))
1053 if (purple_strequal(tmpl->data,id)) {
1054 joined = TRUE; continue;
1055 }
1056
1057 // user list is lost when conv window is closed, we need to re-read data from MM
1058 // this is rather a workaround .. reimplement the workflow ? ...
1059 if (joined && purple_conv_chat_get_users(purple_conversations_find_chat(ma->pc, g_str_hash(id))) != NULL) {
1060 return; }
1061 if (!joined) ma->joined_channels = g_list_prepend(ma->joined_channels, g_strdup(id));
1062
1063 url = mm_build_url(ma,"/channels/%s",id);
1064 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_get_channel_by_id_response, g_strdup(team_id));
1065 g_free(url);
1066 }
1067
1068
1069 static void mm_refresh_statuses(MattermostAccount *ma, const gchar *id);
1070
1071
1072
1073 static MattermostUser *mm_user_from_json(MattermostAccount *ma, JsonObject *user);
1074
1075 static void
mm_get_users_by_ids_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)1076 mm_get_users_by_ids_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1077 {
1078
1079 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting Mattermost users list"),TRUE)) return;
1080
1081 PurpleGroup *default_group = mm_get_or_create_default_group();
1082 MattermostUser *mm_user = NULL;
1083 GList *mm_users = user_data;
1084 GList *i = NULL;
1085 JsonArray *users = json_node_get_array(node);
1086 guint j, len = json_array_get_length(users);
1087
1088 if (len == 0) return;
1089
1090 for (i=mm_users;i;i=i->next) {
1091 mm_user = i->data;
1092 for (j = 0; j < len; j++) {
1093 JsonObject *user = json_array_get_object_element(users,j);
1094 if (g_strcmp0(mm_user->user_id,json_object_get_string_member(user,"id")) == 0){
1095 mm_user->username = g_strdup(json_object_get_string_member(user, "username"));
1096 mm_user->nickname = g_strdup(json_object_get_string_member(user, "nickname"));
1097 mm_user->first_name = g_strdup(json_object_get_string_member(user, "first_name"));
1098 mm_user->last_name = g_strdup(json_object_get_string_member(user, "last_name"));
1099 mm_user->email = g_strdup(json_object_get_string_member(user, "email"));
1100 mm_user->locale = g_strdup(json_object_get_string_member(user, "locale"));
1101 mm_user->position = g_strdup(json_object_get_string_member(user, "position"));
1102 mm_user->alias = g_strdup(mm_get_alias(mm_user));
1103 mm_user->channel_approximate_view_time = mm_find_channel_approximate_view_time(ma, g_hash_table_lookup(ma->one_to_ones_rev,mm_user->username));
1104 }
1105 }
1106 }
1107
1108 mm_users = g_list_sort(mm_users, mm_compare_users_by_alias_int);
1109
1110 for (i=mm_users; i; i=i->next) {
1111
1112 MattermostUser *mm_user = i->data;
1113 PurpleBuddy *buddy = purple_blist_find_buddy(ma->account, mm_user->username);
1114 if (buddy == NULL) {
1115 buddy = purple_buddy_new(ma->account, mm_user->username, NULL);
1116 purple_blist_add_buddy(buddy, NULL, default_group, NULL);
1117 } else {
1118 MattermostChannel *tmpchannel = g_new0(MattermostChannel,1);
1119 tmpchannel->id = g_strdup(mm_user->room_id);
1120 tmpchannel->page_history = 0;
1121 mm_get_history_of_room(ma, tmpchannel, -1);
1122 //FIXME: GFREE THAT !
1123 }
1124
1125 if (mm_user->user_id && mm_user->username) {
1126 g_hash_table_replace(ma->ids_to_usernames, g_strdup(mm_user->user_id), g_strdup(mm_user->username));
1127 g_hash_table_replace(ma->usernames_to_ids, g_strdup(mm_user->username), g_strdup(mm_user->user_id));
1128 }
1129
1130 mm_set_user_blist(ma, mm_user, buddy);
1131
1132 purple_blist_node_set_string(PURPLE_BLIST_NODE(buddy), "user_id", mm_user->user_id);
1133
1134 //this is called for new buddies or on startup: set a flag to read history from server
1135 purple_blist_node_set_bool(PURPLE_BLIST_NODE(buddy), "seen", FALSE);
1136
1137 if(purple_account_get_bool(ma->account, "use-alias", FALSE)) {
1138 gchar *alias = g_strdup(mm_get_alias(mm_user));
1139 purple_buddy_set_local_alias(buddy, alias);
1140 g_free(alias);
1141 }
1142
1143 mm_get_avatar(ma,buddy);
1144 mm_refresh_statuses(ma, mm_user->user_id);
1145
1146 }
1147 g_list_free_full(user_data, mm_g_free_mattermost_user);
1148 }
1149
1150
1151
1152 static void
mm_get_users_by_ids(MattermostAccount * ma,GList * ids)1153 mm_get_users_by_ids(MattermostAccount *ma, GList *ids)
1154 {
1155 GList *i;
1156 gchar *url, *postdata;
1157 MattermostUser *mm_user;
1158
1159 if (ids == NULL) {
1160 return;
1161 }
1162
1163 JsonArray *data = json_array_new();
1164
1165 for (i = ids; i; i = i->next) {
1166 mm_user = i->data;
1167 json_array_add_string_element(data, mm_user->user_id);
1168 }
1169
1170 postdata = json_array_to_string(data);
1171
1172 url = mm_build_url(ma,"/users/ids");
1173 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_get_users_by_ids_response, ids);
1174
1175 json_array_unref(data);
1176 g_free(postdata);
1177 g_free(url);
1178 }
1179
1180 static void
mm_tooltip_text(PurpleBuddy * buddy,PurpleNotifyUserInfo * user_info,gboolean full)1181 mm_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, gboolean full)
1182 {
1183 const PurplePresence *presence = purple_buddy_get_presence(buddy);
1184
1185 if(purple_presence_is_online(presence)) {
1186 _MM_TOOLTIP_LINE_ADD(buddy,user_info,_("Status"),NULL,purple_status_get_name(purple_presence_get_active_status(presence)));
1187 }
1188
1189 _MM_TOOLTIP_LINE_ADD(buddy,user_info,_("Nickname"),"nickname",NULL);
1190 _MM_TOOLTIP_LINE_ADD(buddy,user_info,_("First Name"),"first_name",NULL);
1191 _MM_TOOLTIP_LINE_ADD(buddy,user_info,_("Last Name"),"last_name",NULL);
1192 _MM_TOOLTIP_LINE_ADD(buddy,user_info,_("Email"),"email",NULL);
1193 _MM_TOOLTIP_LINE_ADD(buddy,user_info,_("Position"),"position",NULL);
1194 _MM_TOOLTIP_LINE_ADD(buddy,user_info,_("Locale"),"locale",NULL);
1195
1196 gchar *rolelist = mm_purple_flag_to_role(purple_blist_node_get_int(PURPLE_BLIST_NODE(buddy),"roles"));
1197 purple_notify_user_info_add_pair_plaintext(user_info,_("Roles"), rolelist);
1198 g_free(rolelist);
1199 }
1200
1201 static void
mm_set_group_chat(MattermostAccount * ma,const gchar * team_id,const gchar * channel_name,const gchar * channel_id)1202 mm_set_group_chat(MattermostAccount *ma, const gchar *team_id, const gchar *channel_name, const gchar *channel_id)
1203 {
1204 g_hash_table_replace(ma->group_chats, g_strdup(channel_id), g_strdup(channel_name));
1205 g_hash_table_replace(ma->group_chats_rev, g_strdup(channel_name), g_strdup(channel_id));
1206 if (team_id) g_hash_table_replace(ma->channel_teams, g_strdup(channel_id), g_strdup(team_id));
1207 }
1208
1209 static void
mm_remove_group_chat(MattermostAccount * ma,const gchar * channel_id)1210 mm_remove_group_chat(MattermostAccount *ma, const gchar *channel_id)
1211 {
1212 if (!g_hash_table_lookup(ma->group_chats, channel_id)) return;
1213
1214 g_hash_table_remove(ma->group_chats_rev, g_hash_table_lookup(ma->group_chats, channel_id));
1215 g_hash_table_remove(ma->group_chats, channel_id);
1216 g_hash_table_remove(ma->channel_teams, channel_id);
1217 }
1218
1219 static void
mm_set_me(MattermostAccount * ma)1220 mm_set_me(MattermostAccount *ma)
1221 {
1222 if (!purple_account_get_private_alias(ma->account)) {
1223 purple_account_set_private_alias(ma->account, ma->self->username);
1224 }
1225
1226 purple_connection_set_display_name(ma->pc, ma->self->username);
1227
1228 g_hash_table_replace(ma->ids_to_usernames, g_strdup(ma->self->user_id), g_strdup(ma->self->username));
1229 g_hash_table_replace(ma->usernames_to_ids, g_strdup(ma->self->username), g_strdup(ma->self->user_id));
1230
1231 }
1232
1233 static void
mm_get_teams(MattermostAccount * ma)1234 mm_get_teams(MattermostAccount *ma)
1235 {
1236 gchar *url;
1237
1238 mm_start_socket(ma);
1239
1240 url = mm_build_url(ma,"/users/me/teams");
1241 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_got_teams, NULL);
1242
1243 g_free(url);
1244 }
1245
1246 static void
mm_save_user_pref_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)1247 mm_save_user_pref_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1248 {
1249 MattermostUserPref *pref = user_data;
1250 g_free(pref);
1251 mm_check_mattermost_response(ma,node,_("Error"),_("Error saving Mattermost user preferences"),TRUE);
1252 }
1253
1254 static void
mm_save_user_pref(MattermostAccount * ma,MattermostUserPref * pref)1255 mm_save_user_pref(MattermostAccount *ma, MattermostUserPref *pref)
1256 {
1257 JsonArray *data = json_array_new();
1258 JsonObject *pref_data = json_object_new();
1259 gchar *postdata, *url;
1260
1261 json_object_set_string_member(pref_data, "user_id", pref->user_id);
1262 json_object_set_string_member(pref_data, "category", pref->category);
1263 json_object_set_string_member(pref_data, "name", pref->name);
1264 json_object_set_string_member(pref_data, "value", pref->value);
1265
1266 json_array_add_object_element(data,pref_data);
1267 postdata = json_array_to_string(data);
1268
1269 if (purple_strequal(pref->category,"direct_channel_show") || purple_strequal(pref->category,"group_channel_show")) {
1270 url = mm_build_url(ma,"/users/me/preferences");
1271 mm_fetch_url(ma, url, MATTERMOST_HTTP_PUT, postdata, -1, mm_save_user_pref_response, pref);
1272 }
1273
1274 g_free(postdata);
1275 json_array_unref(data);
1276 }
1277
1278
1279 //static void mm_chat_leave(PurpleConnection *pc, int id);
1280 /*
1281 static void
1282 mm_remove_blist_by_id(MattermostAccount *ma, const gchar *id)
1283 {
1284 if (mm_hash_table_contains(ma->ids_to_usernames, id)) {
1285 const gchar *user_name = g_hash_table_lookup(ma->ids_to_usernames, id);
1286 PurpleBuddy *buddy = purple_blist_find_buddy(ma->account, user_name);
1287 if (buddy) {
1288 g_hash_table_remove(ma->ids_to_usernames, id);
1289 g_hash_table_remove(ma->usernames_to_ids, user_name);
1290 purple_blist_remove_buddy(buddy);
1291 }
1292 //TODO: leave imconversation ?
1293 } else {
1294 PurpleBlistNode *node;
1295 gboolean found = FALSE;
1296 for (node = purple_blist_get_root(); node != NULL && !found; node = purple_blist_node_next(node, TRUE)) {
1297 if (PURPLE_IS_CHAT(node) && purple_chat_get_account(PURPLE_CHAT(node)) == ma->account &&
1298 purple_strequal(purple_blist_node_get_string(node, "type"), MATTERMOST_CHANNEL_TYPE_STRING(MATTERMOST_CHANNEL_GROUP))) {
1299
1300 found = TRUE;
1301 }
1302 }
1303
1304 if (found && PURPLE_IS_CHAT(node)) {
1305 purple_blist_remove_chat(PURPLE_CHAT(node));
1306 mm_chat_leave(ma->pc, g_str_hash(id));
1307 }
1308 //TODO: leave chatconversation ?
1309 // 3.0 purple_chat_conversation_leave(chatconv);
1310 }
1311 }
1312 */
1313
1314 static void
mm_get_user_prefs_response(MattermostAccount * ma,JsonNode * node,gpointer userdata)1315 mm_get_user_prefs_response(MattermostAccount *ma, JsonNode *node, gpointer userdata)
1316 {
1317 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting Mattermost user preferences"),TRUE)) return;
1318
1319 JsonArray *arr = json_node_get_array(node);
1320 GList *prefs = json_array_get_elements(arr);
1321 GList *i;
1322
1323 g_list_free(ma->user_prefs);
1324
1325 for (i = prefs; i != NULL; i = i->next) {
1326
1327 JsonNode *prefnode = i->data;
1328 JsonObject *tmppref = json_node_get_object(prefnode);
1329
1330 MattermostUserPref *pref = g_new0(MattermostUserPref,1);
1331 pref->user_id = g_strdup(ma->self->user_id); // not really needed
1332 pref->category = g_strdup(json_object_get_string_member(tmppref, "category"));
1333 pref->name = g_strdup(json_object_get_string_member(tmppref, "name"));
1334 pref->value = g_strdup(json_object_get_string_member(tmppref, "value"));
1335 ma->user_prefs = g_list_prepend(ma->user_prefs,pref);
1336 }
1337 }
1338
1339
1340 static void
mm_get_user_prefs(MattermostAccount * ma)1341 mm_get_user_prefs(MattermostAccount *ma)
1342 {
1343 //TODO: preference categories (v5 server):
1344 // direct_channel_show { name:channel_id,value:bool}
1345 // group_channel_show { name:channel_id,value:bool}
1346 // tutorial_step { name:user_id,value:4 }
1347 // last { name:channel,value:channel_id }
1348 // display_settings { name:use_military_time,value:bool}
1349 // { name:name_format,value:username} (or?)
1350 // { name:selected_font,value:Open Sans} (or?)
1351 // { name:channel_display_mode,value:full} (or?}
1352 // { name:message_display,value:compact} (or?}
1353 // {.name:collapse_previews,value:bool}
1354 // channel_approximate_view_time { name:channel_id,value:unixseconds}
1355 // channel_open_time { name:channel_id,value:unixseconds}
1356
1357 gchar *url;
1358 url = mm_build_url(ma,"/users/me/preferences");
1359 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_get_user_prefs_response, NULL);
1360 g_free(url);
1361
1362 }
1363
1364
1365 static void
mm_get_commands_for_team_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)1366 mm_get_commands_for_team_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1367 {
1368 JsonArray *response = json_node_get_array(node);
1369 guint i, len = json_array_get_length(response);
1370
1371 for (i = 0; i < len; i++) {
1372 JsonObject *command = json_array_get_object_element(response, i);
1373 //printf("command is:%s\n",json_object_to_string(command));
1374 MattermostCommand *cmd = g_new0(MattermostCommand,1);
1375 cmd->trigger = g_strdup(json_object_get_string_member(command,"trigger"));
1376 cmd->team_id = g_strdup(json_object_get_string_member(command,"team_id"));
1377 cmd->display_name = g_strdup(json_object_get_string_member(command,"display_name"));
1378 cmd->description = g_strdup(json_object_get_string_member(command,"description"));
1379 cmd->auto_complete_hint = g_strdup(json_object_get_string_member(command,"auto_complete_hint"));
1380 cmd->auto_complete_desc = g_strdup(json_object_get_string_member(command,"auto_complete_desc"));
1381
1382 if (!g_list_find_custom(ma->commands,cmd,mm_compare_cmd_int)) {
1383 // we implement these commands ourselves.
1384 if (!purple_strequal(cmd->trigger,"help") &&
1385 !purple_strequal(cmd->trigger,"leave") &&
1386 !purple_strequal(cmd->trigger,"online") &&
1387 !purple_strequal(cmd->trigger,"away") &&
1388 !purple_strequal(cmd->trigger,"dnd") &&
1389 !purple_strequal(cmd->trigger,"offline") &&
1390 !purple_strequal(cmd->trigger,"logout")) {
1391 ma->commands=g_list_prepend(ma->commands,cmd);
1392 const gchar *info = g_strconcat(cmd->trigger," ",
1393 strlen(cmd->auto_complete_hint) ? g_strconcat(cmd->auto_complete_hint," | ",NULL) : " | ",
1394 strlen(cmd->auto_complete_desc) ? g_strconcat(cmd->auto_complete_desc," ",NULL) : "",
1395 ( !strlen(cmd->auto_complete_desc) && strlen(cmd->description) ) ? g_strconcat(cmd->description," ",NULL) : " ",
1396 strlen(cmd->team_id) ? g_strconcat("[team only: ",g_hash_table_lookup(ma->teams, cmd->team_id),"]",NULL) : "",
1397 NULL);
1398
1399 purple_cmd_register(cmd->trigger, "s", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM |
1400 PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
1401 MATTERMOST_PLUGIN_ID, mm_slash_command, info, NULL);
1402 } else {
1403 mm_g_free_mattermost_command(cmd);
1404 }
1405 } else {
1406 mm_g_free_mattermost_command(cmd);
1407 }
1408 }
1409 ma->commands = g_list_sort(ma->commands,mm_compare_cmd_2_int);
1410 }
1411
1412 void
mm_get_commands_for_team(MattermostAccount * ma,const gchar * team_id)1413 mm_get_commands_for_team(MattermostAccount *ma,const gchar *team_id)
1414 {
1415 gchar *url;
1416
1417 url = mm_build_url(ma,"/commands?team_id=%s",team_id);
1418
1419 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_get_commands_for_team_response, g_strdup(team_id));
1420 g_free(url);
1421 }
1422
1423 static void
mm_get_client_config_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)1424 mm_get_client_config_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1425 {
1426 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting Mattermost client configuration"),TRUE)) return;
1427
1428 JsonObject *response = json_node_get_object(node);
1429
1430 if (json_object_get_string_member(response,"EnablePublicLink"), "true") {
1431 ma->client_config->public_link = TRUE;
1432 } else {
1433 ma->client_config->public_link = FALSE;
1434 }
1435
1436 if (json_object_get_string_member(response,"EnableCommands"), "true") {
1437 ma->client_config->enable_commands = TRUE;
1438 } else {
1439 ma->client_config->enable_commands = FALSE;
1440 }
1441
1442 ma->client_config->site_name = g_strdup(json_object_get_string_member(response,"SiteName"));
1443 ma->client_config->support_email = g_strdup(json_object_get_string_member(response,"SupportEmail"));
1444 ma->client_config->server_version = g_strdup(json_object_get_string_member(response,"Version"));
1445 ma->client_config->site_url = g_strdup(json_object_get_string_member(response,"SiteURL"));
1446 ma->client_config->report_a_problem_link = g_strdup(json_object_get_string_member(response,"ReportAProblemLink"));
1447 ma->client_config->build_number = g_strdup(json_object_get_string_member(response,"BuildNumber"));
1448 ma->client_config->build_hash = g_strdup(json_object_get_string_member(response,"BuildHash"));
1449 ma->client_config->build_date = g_strdup(json_object_get_string_member(response,"BuildDate"));
1450 ma->client_config->enterprise_ready = g_strdup(json_object_get_string_member(response,"BuildEnterpriseReady"));
1451 }
1452
1453 static void
mm_get_client_config(MattermostAccount * ma)1454 mm_get_client_config(MattermostAccount *ma)
1455 {
1456 gchar *url;
1457 //NOTE: MM 5.3 still does not implement 'new' format.
1458 url = mm_build_url(ma,"/config/client?format=old");
1459
1460 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_get_client_config_response, NULL);
1461 g_free(url);
1462 }
1463
1464 static void
mm_me_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)1465 mm_me_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1466 {
1467 JsonObject *response;
1468 gboolean gitlabauth = FALSE;
1469
1470 if (node == NULL) {
1471 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, _("Invalid or expired Gitlab cookie"));
1472 return;
1473 }
1474
1475 response = json_node_get_object(node);
1476
1477 if (json_object_get_int_member(response, "status_code") >= 400) {
1478 if (purple_account_get_bool(ma->account, "use-mmauthtoken", FALSE)) {
1479 gitlabauth = TRUE;
1480 }
1481 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, g_strconcat(json_object_get_string_member(response, "message"), gitlabauth ? _("(Invalid or expired Gitlab cookie)") : "",NULL));
1482 return;
1483 }
1484
1485 mm_g_free_mattermost_user(ma->self);
1486 ma->self = g_new0(MattermostUser, 1);
1487
1488 if (!json_object_get_string_member(response, "id") || !json_object_get_string_member(response, "username")) {
1489 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, _("User ID/Name not received from server"));
1490 return;
1491 }
1492
1493 ma->self = mm_user_from_json(ma, response);
1494
1495 JsonObject *notify_props = json_object_get_object_member(response, "notify_props");
1496
1497 if (purple_strequal(json_object_get_string_member(notify_props, "all"), "true")) {
1498 ma->mention_words = g_list_prepend(ma->mention_words,"@all");
1499 }
1500
1501 if (purple_strequal(json_object_get_string_member(notify_props, "channel"), "true")) {
1502 ma->mention_words = g_list_prepend(ma->mention_words,"@channel");
1503 }
1504
1505 if (purple_strequal(json_object_get_string_member(notify_props, "first_name"), "true")) {
1506 ma->mention_words = g_list_prepend(ma->mention_words,g_strconcat("@", ma->self->first_name, NULL));
1507 ma->mention_words = g_list_prepend(ma->mention_words, ma->self->first_name);
1508 }
1509
1510 gchar **mention_keys;
1511 mention_keys = g_strsplit_set(json_object_get_string_member(notify_props, "mention_keys"), ",", -1);
1512 gint i;
1513 for (i =0 ; mention_keys[i] != NULL; i++) {
1514 const gchar *mkey = mention_keys[i];
1515 ma->mention_words=g_list_prepend(ma->mention_words, g_strdup(mkey));
1516 if (mkey[0] != '@') {
1517 ma->mention_words=g_list_prepend(ma->mention_words, g_strconcat("@", mkey, NULL));
1518 }
1519 }
1520 g_strfreev(mention_keys);
1521
1522 gchar *regex = g_strdup("");
1523
1524 GList *j;
1525 for (j = ma->mention_words; j != NULL; j=j->next) {
1526 const gchar *tmp = j->data;
1527 if (j != ma->mention_words) {
1528 regex = g_strconcat(regex, "|", tmp, NULL);
1529 } else {
1530 regex = g_strdup(tmp);
1531 }
1532 }
1533
1534 if (ma->mention_all_regex) {
1535 g_regex_unref(ma->mention_all_regex);
1536 }
1537 ma->mention_all_regex = g_regex_new(MATTERMOST_MENTION_ALL_MATCH, G_REGEX_CASELESS|G_REGEX_DOTALL|G_REGEX_OPTIMIZE, G_REGEX_MATCH_NOTEMPTY, NULL);
1538
1539 if (ma->mention_me_regex) {
1540 g_regex_unref(ma->mention_me_regex);
1541 }
1542
1543 if (!purple_strequal(regex,"")) {
1544 ma->mention_me_regex = g_regex_new(MATTERMOST_MENTION_ME_MATCH(regex), G_REGEX_CASELESS|G_REGEX_DOTALL|G_REGEX_OPTIMIZE, G_REGEX_MATCH_NOTEMPTY, NULL);
1545 } else {
1546 ma->mention_me_regex = NULL;
1547 }
1548
1549 g_free(regex);
1550
1551 //TODO: get avatar ?
1552 mm_get_user_prefs(ma);
1553 mm_get_client_config(ma);
1554 mm_set_me(ma);
1555 mm_get_teams(ma);
1556 }
1557
1558 static void
mm_get_me(MattermostAccount * ma)1559 mm_get_me(MattermostAccount *ma)
1560 {
1561 gchar *url;
1562 url = mm_build_url(ma,"/users/me");
1563
1564 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_me_response, NULL);
1565 g_free(url);
1566 }
1567
1568 static void
mm_login_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)1569 mm_login_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1570 {
1571 JsonObject *response;
1572
1573 if (node == NULL) {
1574 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, _("Bad username/password"));
1575 return;
1576 }
1577
1578 response = json_node_get_object(node);
1579
1580 if (mm_hash_table_contains(ma->cookie_table, "MMAUTHTOKEN")) {
1581 g_free(ma->session_token);
1582 ma->session_token = g_strdup(g_hash_table_lookup(ma->cookie_table, "MMAUTHTOKEN"));
1583 } else if (json_object_has_member(response, "body")) {
1584 // Uh oh, error
1585 gchar *stripped = purple_markup_strip_html(json_object_get_string_member(response, "body"));
1586 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, stripped);
1587 g_free(stripped);
1588 return;
1589 }
1590
1591 if (json_object_get_int_member(response, "status_code") >= 400) {
1592 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, json_object_get_string_member(response, "message"));
1593 return;
1594 }
1595
1596 if (!json_object_get_string_member(response, "id") || !json_object_get_string_member(response, "username")) {
1597 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, _("User ID/Name not received from server"));
1598 return;
1599 }
1600
1601 mm_get_me(ma);
1602 }
1603
1604
1605
1606 static PurpleChatUserFlags
mm_role_to_purple_flag(MattermostAccount * ma,const gchar * rolelist)1607 mm_role_to_purple_flag(MattermostAccount *ma, const gchar *rolelist)
1608 {
1609 PurpleChatUserFlags flags = PURPLE_CHAT_USER_NONE;
1610 gchar **roles = g_strsplit_set(rolelist, " ", -1);
1611 gint i;
1612
1613 for(i = 0; roles[i]; i++) {
1614 const gchar *role = roles[i];
1615
1616 // we are always channel_user
1617 if (purple_strequal(role, "channel_admin")) {
1618 flags |= PURPLE_CHAT_USER_OP;
1619 } else if (purple_strequal(role, "system_admin")) {
1620 flags |= PURPLE_CHAT_USER_FOUNDER;
1621 }
1622 }
1623
1624 g_strfreev(roles);
1625
1626 return flags;
1627 }
1628
1629 static gchar *
mm_purple_flag_to_role(PurpleChatUserFlags flags)1630 mm_purple_flag_to_role(PurpleChatUserFlags flags)
1631 {
1632 const gchar *cu_str = _("Channel User");
1633 const gchar *ca_str = _("Channel Administrator");
1634 const gchar *sa_str = _("System Administrator");
1635 gboolean ca = FALSE;
1636 gboolean sa = FALSE;
1637
1638 // we are always channel_user
1639 if (flags & PURPLE_CHAT_USER_OP) {
1640 ca = TRUE;
1641 }
1642 if (flags & PURPLE_CHAT_USER_FOUNDER) {
1643 sa = TRUE;
1644 }
1645
1646 return g_strjoin(", ", cu_str, ca ? ca_str : "", sa ? sa_str : "", NULL);
1647 }
1648
1649 static void
mm_purple_message_file_send(MattermostAccount * ma,MattermostFile * mmfile,const gchar * anchor,gboolean isimage)1650 mm_purple_message_file_send(MattermostAccount *ma, MattermostFile *mmfile, const gchar *anchor, gboolean isimage)
1651 {
1652 PurpleMessageFlags msg_flags = (purple_strequal(mmfile->mmchlink->sender, ma->self->username) ? PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_REMOTE_SEND | PURPLE_MESSAGE_DELAYED : PURPLE_MESSAGE_RECV);
1653
1654 if (isimage) msg_flags |= PURPLE_MESSAGE_IMAGES;
1655
1656 if (mm_hash_table_contains(ma->group_chats, mmfile->mmchlink->channel_id)) {
1657 purple_serv_got_chat_in(ma->pc, g_str_hash(mmfile->mmchlink->channel_id), mmfile->mmchlink->sender, msg_flags, anchor, mmfile->mmchlink->timestamp);
1658 } else {
1659 if (msg_flags == PURPLE_MESSAGE_RECV) {
1660 purple_serv_got_im(ma->pc, mmfile->mmchlink->sender, anchor, msg_flags, mmfile->mmchlink->timestamp);
1661 } else {
1662 const gchar *other_user = g_hash_table_lookup(ma->one_to_ones, mmfile->mmchlink->channel_id);
1663 // TODO null check
1664 PurpleIMConversation *imconv = purple_conversations_find_im_with_account(other_user, ma->account);
1665 PurpleMessage *pmsg = purple_message_new_outgoing(other_user, anchor, msg_flags);
1666
1667 if (imconv == NULL) {
1668 imconv = purple_im_conversation_new(ma->account, other_user);
1669 }
1670 purple_message_set_time(pmsg, mmfile->mmchlink->timestamp);
1671 purple_conversation_write_message(PURPLE_CONVERSATION(imconv), pmsg);
1672 purple_message_destroy(pmsg);
1673 }
1674 }
1675 }
1676
1677 static void
mm_process_message_image_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)1678 mm_process_message_image_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1679 {
1680 MattermostFile *mmfile = user_data;
1681 PurpleImage *image;
1682 guint image_id;
1683 gsize response_len;
1684 gpointer response_dup;
1685 const gchar *response_str;
1686 gchar *anchor;
1687
1688 JsonObject *response = json_node_get_object(node);
1689
1690 response_str = g_dataset_get_data(node, "raw_body");
1691 response_len = json_object_get_int_member(response,"len");
1692 response_dup = g_memdup(response_str, response_len);
1693
1694 image = purple_image_new_from_data(response_dup,response_len);
1695 image_id = purple_image_store_add(image);
1696
1697 if (purple_account_get_bool(ma->account,"show-full-images", FALSE)) {
1698 anchor = g_strdup_printf("<img id='%d' src='%s' />", image_id, mmfile->uri);
1699 } else {
1700 anchor = g_strdup_printf("<a href='%s'>%s <img id='%d' src='%s' /></a>", mmfile->uri, _("[view full image]"), image_id, mmfile->uri);
1701 }
1702
1703 mm_purple_message_file_send(ma, mmfile, anchor, TRUE);
1704
1705 g_free(anchor);
1706 mm_g_free_mattermost_file(mmfile);
1707 }
1708
1709 static void
mm_process_message_image(MattermostAccount * ma,MattermostFile * mmfile)1710 mm_process_message_image(MattermostAccount *ma, MattermostFile *mmfile)
1711 {
1712 gchar *url;
1713
1714 if (mmfile->has_preview_image) {
1715 url=mm_build_url(ma,"/files/%s/preview", mmfile->id);
1716 } else if (purple_account_get_bool(ma->account,"show-full-images", FALSE)) {
1717 url=mm_build_url(ma,"/files/%s" , mmfile->id);
1718 } else {
1719 url=mm_build_url(ma,"/files/%s/thumbnail" , mmfile->id);
1720 }
1721
1722 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_process_message_image_response, mmfile);
1723 g_free(url);
1724 }
1725
1726 static void
mm_file_metadata_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)1727 mm_file_metadata_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1728 {
1729
1730 JsonObject *response = json_node_get_object(node);
1731 MattermostFile *mmfile = user_data;
1732 gchar *anchor = NULL;
1733
1734 //todo: if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting Mattermost file metadata"),TRUE)) return;
1735 if (json_object_get_int_member(response, "status_code") >= 400) {
1736 anchor = g_strdup(mmfile->uri);
1737 } else {
1738 mmfile->name = g_strdup(json_object_get_string_member(response, "name"));
1739 mmfile->mime_type = g_strdup(json_object_get_string_member(response, "mime_type"));
1740 mmfile->id = g_strdup(json_object_get_string_member(response, "id"));
1741 if (purple_strequal(json_object_get_string_member(response, "has_preview_image"),"true")) {
1742 mmfile->has_preview_image = TRUE;
1743 } else {
1744 mmfile->has_preview_image = FALSE;
1745 }
1746 }
1747
1748 // do we really support any image type ? ...
1749 if (g_str_has_prefix(mmfile->mime_type,"image/") && purple_account_get_bool(ma->account, "show-images", TRUE)) {
1750 mm_process_message_image(ma,mmfile);
1751 return;
1752 }
1753
1754 //TODO: that file can have thumbnail, display it ? ...
1755 if (!mmfile->uri || !ma->client_config->public_link) {
1756 // get team_id for this channel, if possible
1757 const gchar *team_id = NULL;
1758 if (mmfile->mmchlink->channel_id) {
1759 team_id = g_hash_table_lookup(ma->channel_teams, mmfile->mmchlink->channel_id);
1760 }
1761
1762 // if there is no channel id or the lookup failed, use first team_id
1763 if (!team_id || strlen(team_id) == 0) {
1764 team_id = mm_get_first_team_id(ma);
1765 }
1766
1767 const gchar *team_name = g_hash_table_lookup(ma->teams, team_id);
1768 gchar *link_error_str = g_strconcat("[error: public links disabled on server, cannot get file: ",mmfile->name, NULL);
1769 if (team_name) {
1770 gchar *url = g_strconcat((purple_account_get_bool(ma->account, "use-ssl", TRUE)?"https://":"http://"), ma->server,"/", team_name, "/pl/", mmfile->mmchlink->post_id, NULL);
1771 anchor = g_strconcat(link_error_str, ", visit ","<a href=\"", url, "\">", url, "</a> to access the file]" , NULL);
1772 g_free(url);
1773 } else {
1774 anchor = g_strconcat(link_error_str, "]", NULL);
1775 }
1776 g_free(link_error_str);
1777 } else {
1778 if (!anchor) anchor = g_strconcat("<a href=\"", mmfile->uri, "\">", mmfile->name, "</a>", NULL);
1779 }
1780
1781 mm_purple_message_file_send(ma, mmfile, anchor, FALSE);
1782
1783 mm_g_free_mattermost_file(mmfile);
1784 g_free(anchor);
1785 }
1786
1787
1788 static void
mm_fetch_file_metadata(MattermostAccount * ma,JsonNode * node,gpointer user_data)1789 mm_fetch_file_metadata(MattermostAccount *ma, JsonNode *node, gpointer user_data)
1790 {
1791 MattermostChannelLink *mmchlink = user_data;
1792 MattermostFile *mmfile = g_new0(MattermostFile,1);
1793 mmfile->uri = g_strdup(json_object_get_string_member(json_node_get_object(node),"link"));
1794 mmfile->mmchlink = mmchlink;
1795
1796 gchar *url;
1797
1798 url = mm_build_url(ma,"/files/%s/info", mmfile->mmchlink->file_id);
1799 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_file_metadata_response, mmfile);
1800
1801 g_free(url);
1802 }
1803
1804 static void
mm_fetch_file_link_for_channel(MattermostAccount * ma,const gchar * file_id,const gchar * channel_id,const gchar * post_id,const gchar * username,gint64 timestamp)1805 mm_fetch_file_link_for_channel(MattermostAccount *ma, const gchar *file_id, const gchar *channel_id, const gchar *post_id, const gchar *username, gint64 timestamp)
1806 {
1807 MattermostChannelLink *info = g_new0(MattermostChannelLink, 1);
1808 gchar *url;
1809
1810 info->channel_id = g_strdup(channel_id);
1811 info->file_id = g_strdup(file_id);
1812 info->post_id = g_strdup(post_id);
1813 info->sender = g_strdup(username);
1814 info->timestamp = timestamp;
1815
1816 url = mm_build_url(ma,"/files/%s/link", file_id);
1817
1818 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_fetch_file_metadata, info);
1819
1820 g_free(url);
1821 }
1822
1823 static gboolean
mm_have_seen_message_id(MattermostAccount * ma,const gchar * message_id)1824 mm_have_seen_message_id(MattermostAccount *ma, const gchar *message_id)
1825 {
1826 guint message_hash = g_str_hash(message_id);
1827 gpointer message_hash_ptr = GINT_TO_POINTER(message_hash);
1828
1829 if (g_queue_find(ma->received_message_queue, message_hash_ptr)) {
1830 return TRUE;
1831 }
1832
1833 g_queue_push_head(ma->received_message_queue, message_hash_ptr);
1834 g_queue_pop_nth(ma->received_message_queue, 10);
1835
1836 return FALSE;
1837 }
1838
1839 static void mm_mark_room_messages_read(MattermostAccount *ma, const gchar *room_id);
1840
1841 static gchar *mm_process_attachment(JsonObject *attachment);
1842
1843 static gint64
mm_process_room_message(MattermostAccount * ma,JsonObject * post,JsonObject * data)1844 mm_process_room_message(MattermostAccount *ma, JsonObject *post, JsonObject *data)
1845 {
1846 const gchar *id = json_object_get_string_member(post, "id");
1847 const gchar *msg_text = json_object_get_string_member(post, "message");
1848 const gchar *channel_id = json_object_get_string_member(post, "channel_id");
1849 const gchar *msg_type = json_object_get_string_member(post, "type");
1850 const gchar *user_id = json_object_get_string_member(post, "user_id");
1851 const gchar *username = json_object_get_string_member(data, "sender_name");
1852 const gchar *channel_type = json_object_get_string_member(data, "channel_type");
1853 const gchar *type = json_object_get_string_member(post, "type");
1854 const gchar *pending_post_id = json_object_get_string_member(post, "pending_post_id");
1855 JsonObject *props = json_object_get_object_member(post, "props");
1856 const gchar *override_username = json_object_get_string_member(props, "override_username");
1857 const gchar *from_webhook = json_object_get_string_member(props, "from_webhook");
1858 gint64 update_at = json_object_get_int_member(post, "update_at");
1859 gint64 timestamp = update_at / 1000;
1860 gchar *use_username;
1861
1862 gchar *attachments = NULL;
1863
1864 // Strip '@' from username
1865 if (username && username[0] == '@') {
1866 username++;
1867 }
1868
1869 if (purple_strequal(type, "slack_attachment")) {
1870 JsonArray *attchs = json_object_get_array_member(props, "attachments");
1871 guint i, len = json_array_get_length(attchs);
1872 gchar *tmpa1, *tmpa2;
1873 for (i=0; i < len ; i++) {
1874 JsonObject *attch = json_node_get_object(json_array_get_element(attchs, i));
1875 tmpa1 = g_strdup(attachments);
1876 tmpa2 = mm_process_attachment(attch);
1877 g_free(attachments);
1878 if (tmpa1) {
1879 attachments = g_strconcat(tmpa1, tmpa2, NULL);
1880 } else {
1881 attachments = g_strdup(tmpa2);
1882 }
1883 g_free(tmpa1);
1884 g_free(tmpa2);
1885 }
1886 }
1887
1888 // ephemeral messages have update_at:0
1889 if (!timestamp) {
1890 gint64 create_at = json_object_get_int_member(post, "create_at");
1891 timestamp = create_at / 1000;
1892 update_at = create_at;
1893 }
1894
1895 PurpleMessageFlags msg_flags;
1896
1897 if (username != NULL && !mm_hash_table_contains(ma->ids_to_usernames, user_id)) {
1898 g_hash_table_replace(ma->ids_to_usernames, g_strdup(user_id), g_strdup(username));
1899 g_hash_table_replace(ma->usernames_to_ids, g_strdup(username), g_strdup(user_id));
1900 } else if (username == NULL) {
1901 username = g_hash_table_lookup(ma->ids_to_usernames, user_id);
1902 }
1903
1904 if (purple_strequal(from_webhook, "true") && override_username && *override_username) {
1905 use_username = g_strconcat(override_username, MATTERMOST_BOT_LABEL, NULL);
1906 msg_flags = PURPLE_MESSAGE_RECV; // user_id for BOT is webhook owner ID .. t own BOTS as such too !
1907 } else {
1908 use_username = g_strdup(username);
1909 msg_flags = (purple_strequal(user_id, ma->self->user_id) ? PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_REMOTE_SEND | PURPLE_MESSAGE_DELAYED : PURPLE_MESSAGE_RECV);
1910 }
1911
1912 if (use_username == NULL) {
1913 //we get here when reading history of a chat
1914 //but have not yet read user list for the channel
1915 //since this calls run in parallel
1916 //FIXME: or not ?
1917 use_username=g_strdup("[unknown]");
1918 }
1919
1920 if (!mm_hash_table_contains(ma->channel_teams, channel_id)) {
1921 const gchar *team_id = json_object_get_string_member(data, "team_id");
1922 if (team_id != NULL) {
1923 g_hash_table_replace(ma->channel_teams, g_strdup(channel_id), g_strdup(team_id));
1924 }
1925 }
1926
1927 if (g_str_has_prefix(msg_type, "system_")) {
1928 msg_flags |= PURPLE_MESSAGE_SYSTEM;
1929 }
1930
1931 if (!mm_have_seen_message_id(ma, id) || json_object_get_int_member(post, "edit_at")) {
1932 // Dont display duplicate messages (eg where the server inspects urls to give icons/header/content)
1933 // but do display edited messages
1934
1935 // check we didn't send this ourselves
1936 if (msg_flags == PURPLE_MESSAGE_RECV || !g_hash_table_remove(ma->sent_message_ids, pending_post_id)) {
1937 gchar *msg_pre = mm_markdown_to_html(ma, msg_text);
1938 gchar *msg_post = g_regex_replace(ma->mention_me_regex, msg_pre, -1, 0, MATTERMOST_MENTION_ME_REPLACE, G_REGEX_MATCH_NOTEMPTY, NULL);
1939 gchar *message = g_regex_replace(ma->mention_all_regex, msg_post, -1, 0, MATTERMOST_MENTION_ALL_REPLACE, G_REGEX_MATCH_NOTEMPTY, NULL);
1940
1941 if (!purple_strequal(msg_pre, msg_post)) {
1942 msg_flags |= PURPLE_MESSAGE_NICK;
1943 }
1944
1945 g_free(msg_pre);
1946 g_free(msg_post);
1947
1948 if (json_object_get_int_member(post, "delete_at")) {
1949 gchar *tmp = g_strconcat(_("Deleted: "), message, NULL);
1950 g_free(message);
1951 message = tmp;
1952 } else if (json_object_get_int_member(post, "edit_at")) {
1953 gchar *tmp = g_strconcat(_("Edited: "), message, NULL);
1954 g_free(message);
1955 message = tmp;
1956 }
1957
1958 if (json_object_has_member(post, "file_ids")) {
1959 JsonArray *file_ids = json_object_get_array_member(post, "file_ids");
1960 guint i, len = json_array_get_length(file_ids);
1961
1962 // pass post_id so that permalink to the post can be displayed in case of error
1963 const gchar *post_id = json_object_get_string_member(post, "id");
1964
1965 for (i = 0; i < len; i++) {
1966 const gchar *file_id = json_array_get_string_element(file_ids, i);
1967
1968 mm_fetch_file_link_for_channel(ma, file_id, channel_id, post_id, use_username, timestamp);
1969 }
1970 }
1971
1972 //FIXME JAREK: dont know the TEAM here
1973
1974 if ((channel_type != NULL && *channel_type != MATTERMOST_CHANNEL_DIRECT) || mm_hash_table_contains(ma->group_chats, channel_id)) {
1975 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel_id));
1976
1977 if (chatconv) {
1978 if (purple_strequal(msg_type, "system_header_change") || purple_strequal(msg_type, "system_purpose_change")) {
1979 const gchar *new_header = json_object_get_string_member(props, "new_header");
1980 const gchar *new_purpose = json_object_get_string_member(props, "new_purpose");
1981 const gchar *new_topic_who = json_object_get_string_member(props, "username");
1982 purple_chat_conversation_set_topic(chatconv, new_topic_who, mm_make_topic(new_header, new_purpose, purple_chat_conversation_get_topic(chatconv)));
1983 }
1984
1985 // Group chat message
1986 gchar *msg_out = g_strconcat( message ? message : " " , attachments ? attachments : NULL, NULL);
1987 gchar *alias = g_hash_table_lookup(ma->aliases,channel_id);
1988
1989 if (alias && chatconv) {
1990 purple_conversation_set_name(PURPLE_CONVERSATION(chatconv),alias);
1991 }
1992
1993 purple_serv_got_chat_in(ma->pc, g_str_hash(channel_id), use_username, msg_flags, msg_out, timestamp);
1994
1995 mm_get_channel_by_id(ma, g_hash_table_lookup(ma->channel_teams, channel_id), channel_id);
1996
1997 g_free(msg_out);
1998
1999 if (purple_conversation_has_focus(PURPLE_CONVERSATION(chatconv))) {
2000 mm_mark_room_messages_read(ma, channel_id);
2001 }
2002 } //TODO: else { ERROR } - we have received a group chat message for a chat we dont know about ?
2003 } else {
2004 if (msg_flags == PURPLE_MESSAGE_RECV) {
2005 gchar *msg_out = g_strconcat( message ? message : " " , attachments ? attachments : NULL, NULL);
2006 purple_serv_got_im(ma->pc, use_username, msg_out, msg_flags, timestamp);
2007
2008 g_free(msg_out);
2009
2010 if (channel_type && *channel_type == MATTERMOST_CHANNEL_DIRECT && !mm_hash_table_contains(ma->one_to_ones, channel_id)) {
2011 g_hash_table_replace(ma->one_to_ones, g_strdup(channel_id), g_strdup(username));
2012 g_hash_table_replace(ma->one_to_ones_rev, g_strdup(username), g_strdup(channel_id));
2013 }
2014
2015 if (purple_conversation_has_focus(PURPLE_CONVERSATION(purple_conversations_find_im_with_account(username, ma->account)))) {
2016 mm_mark_room_messages_read(ma, channel_id);
2017 }
2018
2019 } else {
2020 const gchar *other_user = g_hash_table_lookup(ma->one_to_ones, channel_id);
2021 // TODO null check
2022 PurpleIMConversation *imconv = purple_conversations_find_im_with_account(other_user, ma->account);
2023 PurpleMessage *pmsg = purple_message_new_outgoing(other_user, message, msg_flags);
2024
2025 if (imconv == NULL) {
2026 imconv = purple_im_conversation_new(ma->account, other_user);
2027 }
2028 purple_message_set_time(pmsg, timestamp);
2029 purple_conversation_write_message(PURPLE_CONVERSATION(imconv), pmsg);
2030 purple_message_destroy(pmsg);
2031 }
2032 }
2033
2034 g_free(message);
2035 }
2036
2037 }
2038
2039 g_free(use_username);
2040 g_free(attachments);
2041
2042 return update_at;
2043 }
2044
2045 static void
mm_got_hello_user_statuses(MattermostAccount * ma,JsonNode * node,gpointer user_data)2046 mm_got_hello_user_statuses(MattermostAccount *ma, JsonNode *node, gpointer user_data)
2047 {
2048
2049 JsonObject *obj = json_node_get_object(node);
2050 JsonObject *data = json_object_get_object_member(obj, "data");
2051 GList *ids = json_object_get_members(data);
2052 GList *i;
2053
2054 for (i = ids; i; i = i->next) {
2055 const gchar *user_id = i->data;
2056 const gchar *status = json_object_get_string_member(data, user_id);
2057 const gchar *username = g_hash_table_lookup(ma->ids_to_usernames, user_id);
2058
2059 if (username != NULL && status != NULL) {
2060 purple_protocol_got_user_status(ma->account, username, status, NULL);
2061 }
2062 }
2063
2064 g_list_free(ids);
2065 }
2066
2067 static void
mm_got_user_statuses_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)2068 mm_got_user_statuses_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
2069 {
2070
2071 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error getting user statuses"),TRUE)) return;
2072
2073 JsonArray *users = json_node_get_array(node);
2074 guint i, len = json_array_get_length(users);
2075
2076 for (i = 0; i < len; i++) {
2077 JsonObject *user = json_array_get_object_element(users,i);
2078 const gchar *user_id = json_object_get_string_member(user, "user_id");
2079 const gchar *status = json_object_get_string_member(user, "status");
2080 const gchar *username = g_hash_table_lookup(ma->ids_to_usernames, user_id);
2081
2082 if (username != NULL && status != NULL) {
2083 purple_protocol_got_user_status(ma->account, username, status, NULL);
2084 }
2085 }
2086 }
2087
2088 static void
mm_refresh_statuses(MattermostAccount * ma,const gchar * id)2089 mm_refresh_statuses(MattermostAccount *ma, const gchar *id)
2090 {
2091 JsonArray *user_ids;
2092 gchar *url;
2093 gchar *postdata;
2094 user_ids = json_array_new();
2095
2096 if (id != NULL) {
2097 json_array_add_string_element(user_ids, id);
2098 } else {
2099 GSList *buddies = purple_find_buddies(ma->account, NULL);
2100 GSList *buddy_it = buddies;
2101 while(buddy_it != NULL){
2102 PurpleBuddy *buddy = buddy_it->data;
2103 const gchar *buddy_name = purple_buddy_get_name(buddy);
2104 const gchar *user_id = g_hash_table_lookup(ma->usernames_to_ids, buddy_name);
2105 json_array_add_string_element(user_ids, user_id);
2106
2107 buddy_it = g_slist_next(buddy_it);
2108 }
2109 g_slist_free(buddies);
2110 }
2111 guint len = json_array_get_length(user_ids);
2112 if(len == 0){
2113 return;
2114 }
2115 postdata = json_array_to_string(user_ids);
2116
2117 url = mm_build_url(ma,"/users/status/ids");
2118 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_got_user_statuses_response, NULL);
2119 }
2120
2121
2122 static gchar *
mm_process_attachment(JsonObject * attachment)2123 mm_process_attachment(JsonObject *attachment)
2124 {
2125 //TODO: sanitze input strings !
2126 //TODO: libpurple xhtml-im parser is .. fragile .. easy to get output not htmlized ...
2127
2128 gchar *msg_top = NULL;
2129 gchar *msg_fields = NULL;
2130 gchar *message = NULL;
2131
2132 //fallback
2133 const gchar *color = json_object_get_string_member(attachment, "color");
2134 const gchar *pretext = json_object_get_string_member(attachment, "pretext");
2135 const gchar *text = json_object_get_string_member(attachment, "text");
2136 const gchar *author_name = json_object_get_string_member(attachment, "author_name");
2137 //const gchar *author_icon = json_object_get_string_member(attachment, "author_icon");
2138 const gchar *author_link = json_object_get_string_member(attachment,"author_link");
2139 const gchar *title = json_object_get_string_member(attachment, "title");
2140 const gchar *title_link = json_object_get_string_member(attachment, "title_link");
2141 // following are not implemented in MM 3.09 (yet?)
2142 const gchar *image_url = json_object_get_string_member(attachment, "image_url");
2143 //const gchar *thumb_url = json_object_get_string_member(attachment, "thumb_url");
2144 //const gchar *footer = json_object_get_string_member(attachment, "footer");
2145 //const gchar *footer_icon = json_object_get_string_member(attachment, "footer_icon");
2146 //const gint64 *ts = json_object_get_int_member(attachment, "ts");
2147
2148 JsonArray *fields = json_object_get_array_member(attachment, "fields");
2149 guint fields_len = json_array_get_length(fields);
2150
2151 GList *flds_list = NULL;
2152 guint i;
2153
2154 for (i = 0; i < fields_len; i++) {
2155 MattermostAttachmentField *fld_cont = g_new0(MattermostAttachmentField, 1);
2156 JsonObject *field = json_node_get_object(json_array_get_element(fields, i));
2157
2158 fld_cont->title = g_strdup(json_object_get_string_member(field, "title"));
2159 fld_cont->value = g_strdup(json_object_get_string_member(field, "value"));
2160 //fld_cont-> short : we cannot format in multi-column (easily..)
2161 flds_list = g_list_append(flds_list, fld_cont);
2162 }
2163
2164 //TODO: symbolic color names .. and checking
2165
2166 if (!color) {
2167 color = "#FFFFFF";
2168 }
2169
2170 msg_top = g_strconcat(
2171 MM_ATT_BREAK, MM_ATT_TEXT(pretext),
2172 MM_ATT_LINE,
2173 MM_ATT_BORDER(color), MM_ATT_AUTHOR(author_name,author_link),
2174 MM_ATT_BORDER(color), MM_ATT_TITLE(title,title_link),
2175 MM_ATT_BORDER(color), MM_ATT_TEXT(text),
2176 MM_ATT_BORDER(color), MM_ATT_IMAGE(image_url),
2177 NULL);
2178
2179 GList *j;
2180 gchar *tmpl1 = NULL;
2181 gchar *tmpl2 = NULL;
2182
2183 for (j=flds_list; j != NULL; j=j->next) {
2184 MattermostAttachmentField *af = j->data;
2185 tmpl1 = g_strdup(msg_fields);
2186 g_free(msg_fields);
2187 tmpl2 = g_strconcat(
2188 MM_ATT_BORDER(color), MM_ATT_FTITLE(af->title),
2189 MM_ATT_BORDER(color), MM_ATT_TEXT(af->value),
2190 NULL);
2191 if (tmpl1) {
2192 msg_fields = g_strconcat(tmpl1, tmpl2, NULL);
2193 } else {
2194 msg_fields = g_strdup(tmpl2);
2195 }
2196 g_free(tmpl1);
2197 g_free(tmpl2);
2198 }
2199
2200 message = g_strconcat(msg_top, msg_fields ? msg_fields : " ", MM_ATT_LINE, NULL);
2201 g_free(msg_top);
2202 g_free(msg_fields);
2203 g_list_free_full(flds_list, mm_g_free_mattermost_attachment_field);
2204
2205 return message;
2206 }
2207
2208 static void
mm_process_msg(MattermostAccount * ma,JsonNode * element_node)2209 mm_process_msg(MattermostAccount *ma, JsonNode *element_node)
2210 {
2211 //JsonObject *response = NULL;
2212 JsonObject *obj = json_node_get_object(element_node);
2213
2214 const gchar *event = json_object_get_string_member(obj, "event");
2215 const gchar *status = json_object_get_string_member(obj, "status");
2216 JsonObject *data = json_object_get_object_member(obj, "data");
2217 JsonObject *broadcast = json_object_get_object_member(obj, "broadcast");
2218 mm_get_or_create_default_group();
2219
2220 if (event == NULL) {
2221 gint seq_reply = json_object_get_int_member(obj, "seq_reply");
2222 MattermostProxyConnection *proxy = g_hash_table_lookup(ma->result_callbacks, GINT_TO_POINTER(seq_reply));
2223
2224 if (proxy != NULL) {
2225 if (proxy->callback != NULL) {
2226 proxy->callback(ma, element_node, proxy->user_data);
2227 }
2228 g_hash_table_remove(ma->result_callbacks, GINT_TO_POINTER(seq_reply));
2229 }
2230 }
2231
2232 if (purple_strequal(event, "posted") || purple_strequal(event, "post_edited") || purple_strequal(event, "ephemeral_message")) {
2233 JsonParser *post_parser = json_parser_new();
2234 const gchar *post_str = json_object_get_string_member(data, "post");
2235
2236 if (json_parser_load_from_data(post_parser, post_str, -1, NULL)) {
2237 JsonObject *post = json_node_get_object(json_parser_get_root(post_parser));
2238 const gchar *channel_id = json_object_get_string_member(post, "channel_id");
2239 const gchar *user_id = mm_data_or_broadcast_string("user_id");
2240 const gchar *team_id = json_object_get_string_member(post, "team_id");
2241
2242 // detect posts with reactions (update time is larger than edit) and ignore them
2243 if (purple_strequal(event, "post_edited") && json_object_get_int_member(post, "update_at") > json_object_get_int_member(post, "edit_at")) {
2244 // do nothing
2245 } else
2246 //type system_join_channel, channel_id is ""
2247 if (!purple_strequal(channel_id,"") && purple_strequal(ma->self->user_id, user_id)) {
2248 mm_get_channel_by_id(ma, team_id, channel_id);
2249
2250 } else if (!purple_strequal(channel_id,"") && g_hash_table_lookup(ma->group_chats, channel_id)) {
2251 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel_id));
2252 if (!chatconv) {
2253 PurpleChat *chat = mm_purple_blist_find_chat(ma,channel_id);
2254 if (chat) {
2255 GHashTable *components = purple_chat_get_components(chat);
2256 gchar *team_id = g_hash_table_lookup(components, "team_id");
2257 gchar *channel_id = g_hash_table_lookup(components, "id");
2258 gchar *type = g_hash_table_lookup(components, "type");
2259 gchar *display_name = g_hash_table_lookup(components, "display_name");
2260 const gchar *alias;
2261
2262 MattermostChannel *tmpchannel = g_new0(MattermostChannel,1);
2263 tmpchannel->id = g_strdup(channel_id);
2264 tmpchannel->team_id = g_strdup(team_id);
2265 tmpchannel->display_name = g_strdup(display_name);
2266 tmpchannel->type = g_strdup(type);
2267
2268 alias = mm_get_chat_alias(ma,tmpchannel);
2269
2270 PurpleChatConversation *conv = purple_serv_got_joined_chat(ma->pc, g_str_hash(channel_id), alias);
2271 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "id", g_strdup(channel_id));
2272 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "team_id", g_strdup(team_id));
2273 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "name", g_strdup(alias));
2274 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "display_name", g_strdup(display_name));
2275 purple_conversation_present(PURPLE_CONVERSATION(conv));
2276 }
2277 }
2278 }
2279
2280 if (!purple_strequal(channel_id,"")) {
2281 mm_process_room_message(ma, post, data);
2282 }
2283 }
2284 g_object_unref(post_parser);
2285 } else if (purple_strequal(event, "typing")) {
2286 const gchar *channel_id = mm_data_or_broadcast_string("channel_id");
2287 const gchar *user_id = mm_data_or_broadcast_string("user_id");
2288 const gchar *username = g_hash_table_lookup(ma->ids_to_usernames, user_id);
2289
2290 if (mm_hash_table_contains(ma->group_chats, channel_id)) {
2291 // This is a group conversation
2292 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel_id));
2293 if (chatconv != NULL) {
2294 PurpleChatUser *cb = purple_chat_conversation_find_user(chatconv, username);
2295 PurpleChatUserFlags cbflags;
2296
2297 if (cb == NULL) {
2298 // Getting notified about a buddy we dont know about yet
2299 //TODO add buddy
2300 return;
2301 }
2302 cbflags = purple_chat_user_get_flags(cb);
2303
2304 //if (is_typing)
2305 cbflags |= PURPLE_CHAT_USER_TYPING;
2306 //else //TODO
2307 // cbflags &= ~PURPLE_CHAT_USER_TYPING;
2308
2309 purple_chat_user_set_flags(cb, cbflags);
2310 }
2311 } else {
2312 purple_serv_got_typing(ma->pc, username, 15, PURPLE_IM_TYPING);
2313 }
2314
2315 } else if (purple_strequal(event, "status_change")) {
2316 const gchar *user_id = json_object_get_string_member(data, "user_id");
2317 const gchar *status = json_object_get_string_member(data, "status");
2318 const gchar *username = g_hash_table_lookup(ma->ids_to_usernames, user_id);
2319 if (username != NULL && status != NULL) {
2320 purple_protocol_got_user_status(ma->account, username, status, NULL);
2321 }
2322 } else if (purple_strequal(event, "user_added")) {
2323 const gchar *user_id = mm_data_or_broadcast_string("user_id");
2324 const gchar *team_id = json_object_get_string_member(data, "team_id");
2325 const gchar *channel_id = mm_data_or_broadcast_string("channel_id");
2326 const gchar *username = g_hash_table_lookup(ma->ids_to_usernames, user_id);
2327 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel_id));
2328
2329 if (chatconv != NULL) {
2330 if (!purple_chat_conversation_has_left(chatconv))
2331 //FIXME: we can end up here with username == NULL and segfault pidgin.
2332 if (username)
2333 purple_chat_conversation_add_user(chatconv, username, NULL, PURPLE_CHAT_USER_NONE, FALSE);
2334 } else if (purple_strequal(user_id, ma->self->user_id)) {
2335 mm_get_channel_by_id(ma, team_id, channel_id);
2336 }
2337
2338 } else if (purple_strequal(event, "user_removed")) {
2339 const gchar *channel_id = mm_data_or_broadcast_string("channel_id");
2340 const gchar *user_id = mm_data_or_broadcast_string("user_id");
2341
2342 const gchar *username = g_hash_table_lookup(ma->ids_to_usernames, user_id);
2343 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel_id));
2344 if (chatconv != NULL) {
2345 purple_chat_conversation_remove_user(chatconv, username, NULL);
2346 }
2347
2348 if (purple_strequal(user_id, ma->self->user_id)) {
2349 if (mm_hash_table_contains(ma->group_chats, channel_id)) {
2350 PurpleChat *chat = mm_purple_blist_find_chat(ma, channel_id);
2351 if (chat) {
2352 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel_id));
2353 if (chatconv) purple_chat_conversation_leave(chatconv);
2354 mm_remove_group_chat(ma, channel_id);
2355 //FIXME twice ? mm_remove_group_chat(ma, channel_id);
2356 purple_blist_remove_chat(chat);
2357 }
2358 }
2359 }
2360 } else if (purple_strequal(event, "preferences_changed") && purple_strequal(mm_data_or_broadcast_string("user_id"), ma->self->user_id)) {
2361 GList *users = json_array_get_elements(json_array_from_string(json_node_get_string(json_object_get_member(data, "preferences"))));
2362 GList *user = NULL;
2363 GList *mm_users = NULL;
2364 for (user = users; user != NULL; user = user->next) {
2365 JsonObject *object = json_node_get_object(user->data);
2366 const gchar *id = json_object_get_string_member(object, "name");
2367 if (purple_strequal(json_object_get_string_member(object, "category"), "direct_channel_show")) {
2368 if (purple_strequal(json_object_get_string_member(object, "value"), "false")) {
2369 if (mm_hash_table_contains(ma->ids_to_usernames, id)) {
2370 const gchar *user_name = g_hash_table_lookup(ma->ids_to_usernames, id);
2371 PurpleBuddy *buddy = purple_blist_find_buddy(ma->account, user_name);
2372 if (buddy) {
2373 // don't remove conversation if any: direct channel is not destroyed so it is reuseable.
2374 g_hash_table_remove(ma->ids_to_usernames, id);
2375 g_hash_table_remove(ma->usernames_to_ids, user_name);
2376 purple_blist_remove_buddy(buddy);
2377 }
2378 }
2379 } else {
2380 MattermostUser *mm_user = g_new0(MattermostUser,1);
2381 mm_user->user_id=g_strdup(id);
2382 mm_users = g_list_prepend(mm_users, mm_user);
2383 }
2384 }
2385 if (purple_strequal(json_object_get_string_member(object, "category"), "group_channel_show")) {
2386 if (purple_strequal(json_object_get_string_member(object, "value"), "false")) {
2387 if (mm_hash_table_contains(ma->group_chats, id)) {
2388 PurpleChat *chat = mm_purple_blist_find_chat(ma, id);
2389 if (chat) {
2390 // don't remove conversation if any: group channel is not destroyed so it is reuseable.
2391 mm_remove_group_chat(ma, id);
2392 purple_blist_remove_chat(chat);
2393 }
2394 }
2395 } else {
2396 // not efficient: one callback per channel:
2397 // but no API to do it on multiple channels at once ? ...
2398 const gchar *team_id = json_object_get_string_member(data, "team_id");
2399 mm_get_channel_by_id(ma, team_id, id);
2400 }
2401 }
2402 }
2403 mm_get_users_by_ids(ma, mm_users);
2404 g_list_free(users);
2405 } else if (purple_strequal(event, "channel_created") && purple_strequal(mm_data_or_broadcast_string("user_id"), ma->self->user_id)) {
2406 const gchar *channel_id = mm_data_or_broadcast_string("channel_id");
2407 const gchar *team_id = json_object_get_string_member(data, "team_id");
2408 mm_get_channel_by_id(ma, team_id, channel_id);
2409 } else if (purple_strequal(event, "channel_deleted")) {
2410 const gchar *channel_id = mm_data_or_broadcast_string("channel_id");
2411 if (mm_hash_table_contains(ma->group_chats, channel_id)) {
2412 PurpleChat *chat = mm_purple_blist_find_chat(ma, channel_id);
2413 if (chat) {
2414 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel_id));
2415 if (chatconv) purple_chat_conversation_leave(chatconv);
2416 mm_remove_group_chat(ma, channel_id);
2417 purple_blist_remove_chat(chat);
2418 }
2419 }
2420 // } else if (purple_strequal(event, "channel_converted")) {
2421 // //TODO: implement: remove & add to blist again (see above) or just change type ?
2422 // } else if (purple_strequal(event, "channel_updated")) {
2423 // //TODO: implement
2424 } else if (purple_strequal(event, "channel_viewed")) {
2425 //we have marked it viewed already with purple_conversation_has_focus()
2426 } else if (purple_strequal(event, "hello")) {
2427 mm_refresh_statuses(ma, NULL);
2428 } else if (purple_strequal(event, "user_updated")) {
2429 //TODO: implement reusing (partsof) mm_get_users_by_ids_response()
2430 // {"event":"user_updated","data":{"user":
2431 // { "id":"XXXXX","create_at":XXXX,"update_at":XXXX ,"delete_at":0,
2432 // "username":"aaa","auth_data":"","auth_service":"","email":"aa@aa.oo","nickname":"AA",
2433 // "first_name":"AAA","last_name":"AAA","position":"CCC","roles":"system_user",
2434 // "last_picture_update": XXXXX,"locale":"en",
2435 // "timezone":{"automaticTimezone":"","manualTimezone":"","useAutomaticTimezone":"true"}}},"broadcast":
2436 // {"omit_users":null,"user_id":"","channel_id":"","team_id":""},"seq":5}
2437 } else if (event) {
2438 // can be one of: https://api.mattermost.com/#tag/WebSocket
2439 purple_debug_info("mattermost", "unhandled event %s [%s]\n", event,json_object_to_string(obj));
2440 } else if (purple_strequal(status,"OK")) {
2441 //TODO: this can be a reply to client sending 'user_typing', 'get_statuses' or 'get_statuses_by_ids'
2442 // we dont know since would need to track seq number... so assume it was one of statuses replies...
2443 JsonNode *tmpjsonnode=json_node_new(JSON_NODE_OBJECT);
2444 json_node_set_object(tmpjsonnode,obj);
2445 mm_got_hello_user_statuses(ma, tmpjsonnode, NULL);
2446 json_node_free(tmpjsonnode);
2447 } else if (status) {
2448 //TODO: this will be FAIL status in reply to 'user_typing', 'get_statuses' or 'get_statuses_by_ids'
2449 // we dont know since would need to track seq number... so just ignore it..
2450 purple_debug_info("mattermost", "unhandled status %s [%s]\n", status,json_object_to_string(obj));
2451 } else {
2452 purple_debug_info("mattermost", "unhandled message [%s]\n", json_object_to_string(obj));
2453 }
2454 }
2455
2456
2457 #undef mm_data_or_broadcast_string
2458
2459 PurpleGroup *
mm_get_or_create_default_group()2460 mm_get_or_create_default_group()
2461 {
2462 PurpleGroup *mm_group = NULL;
2463
2464 mm_group = purple_blist_find_group(MATTERMOST_DEFAULT_BLIST_GROUP_NAME);
2465 if (!mm_group)
2466 {
2467 mm_group = purple_group_new(MATTERMOST_DEFAULT_BLIST_GROUP_NAME);
2468 purple_blist_add_group(mm_group, NULL);
2469 }
2470
2471 return mm_group;
2472 }
2473
2474 static void
mm_roomlist_got_list(MattermostAccount * ma,JsonNode * node,gpointer user_data)2475 mm_roomlist_got_list(MattermostAccount *ma, JsonNode *node, gpointer user_data)
2476 {
2477
2478 #define _MAX_COLS 33
2479
2480 MatterMostTeamRoomlist *mmtrl = user_data;
2481 PurpleRoomlist *roomlist = mmtrl->roomlist;
2482 JsonArray *channels = json_node_get_array(node);
2483 guint i, len = json_array_get_length(channels);
2484 PurpleRoomlistRoom *team_category = NULL;
2485 const gchar *team_id = mmtrl->team_id;
2486 const gchar *team_name;
2487
2488 team_name = g_strconcat(g_hash_table_lookup(ma->teams_display_names, team_id), " ", mmtrl->team_desc, NULL);
2489
2490 team_category = purple_roomlist_room_new(PURPLE_ROOMLIST_ROOMTYPE_CATEGORY, team_name, NULL);
2491 purple_roomlist_room_add_field(roomlist, team_category, team_id);
2492 purple_roomlist_room_add(roomlist, team_category);
2493
2494 for (i = 0; i < len; i++) {
2495 JsonObject *channel = json_array_get_object_element(channels, i);
2496 const gchar *room_type = json_object_get_string_member(channel, "type");
2497
2498 if (*room_type == MATTERMOST_CHANNEL_DIRECT) {
2499 continue; // these are buddies - dont show empty 'rooms' in room list
2500 }
2501
2502 const gchar *id = json_object_get_string_member(channel, "id");
2503 const gchar *display_name = json_object_get_string_member(channel, "display_name");
2504 const gchar *name = json_object_get_string_member(channel, "name");
2505 const gchar *header = json_object_get_string_member(channel, "header");
2506 const gchar *purpose = json_object_get_string_member(channel, "purpose");
2507 const gchar *team_id = json_object_get_string_member(channel, "team_id");
2508
2509 //FIXME: in v4 api team_id is NULL for group chats, that breaks the code in many places.
2510 // should be rewritten.
2511
2512 //if (team_id == NULL || strlen(team_id) == 0) {
2513 // team_id = mm_get_first_team_id(ma);
2514 //}
2515
2516 const gchar *team_name = g_hash_table_lookup(ma->teams, team_id);
2517
2518 PurpleRoomlistRoom *room;
2519 const gchar *type_str;
2520 gchar *tmp_h = strlen(header) > _MAX_COLS ? g_strdup_printf("%.*s...", _MAX_COLS-3, header) : NULL;
2521 gchar *tmp_p = strlen(purpose) > _MAX_COLS ? g_strdup_printf("%.*s...", _MAX_COLS-3, purpose) : NULL;
2522
2523 switch(*room_type) {
2524 case MATTERMOST_CHANNEL_OPEN: type_str = _("Open"); break;
2525 case MATTERMOST_CHANNEL_PRIVATE: type_str = _("Private"); break;
2526 case MATTERMOST_CHANNEL_GROUP: type_str = _("Group"); break;
2527 default: type_str = _("Unknown"); break;
2528 }
2529
2530 room = purple_roomlist_room_new(PURPLE_ROOMLIST_ROOMTYPE_ROOM, name, team_category);
2531
2532 purple_roomlist_room_add_field(roomlist, room, id);
2533 purple_roomlist_room_add_field(roomlist, room, team_id);
2534 purple_roomlist_room_add_field(roomlist, room, team_name);
2535 purple_roomlist_room_add_field(roomlist, room, name);
2536 purple_roomlist_room_add_field(roomlist, room, display_name);
2537 purple_roomlist_room_add_field(roomlist, room, type_str);
2538 purple_roomlist_room_add_field(roomlist, room, tmp_h ? tmp_h : header);
2539 purple_roomlist_room_add_field(roomlist, room, tmp_p ? tmp_p : purpose);
2540
2541 purple_roomlist_room_add(roomlist, room);
2542
2543 mm_set_group_chat(ma, team_id, name, id);//ALIAS ???
2544
2545 g_hash_table_replace(ma->channel_teams, g_strdup(id), g_strdup(team_id));
2546
2547 g_free(tmp_h);
2548 g_free(tmp_p);
2549 }
2550
2551 //Only after last team
2552 ma->roomlist_team_count--;
2553 if (ma->roomlist_team_count <= 0) {
2554 purple_roomlist_set_in_progress(roomlist, FALSE);
2555 ma->roomlist_team_count = 0;
2556 }
2557
2558 g_free(mmtrl->team_id);
2559 g_free(mmtrl->team_desc);
2560 g_free(mmtrl);
2561
2562 #undef _MAX_COLS
2563 }
2564
2565 static gchar *
mm_roomlist_serialize(PurpleRoomlistRoom * room)2566 mm_roomlist_serialize(PurpleRoomlistRoom *room) {
2567 GList *fields = purple_roomlist_room_get_fields(room);
2568
2569 const gchar *id = g_list_nth_data(fields, 0);
2570 const gchar *team_id = g_list_nth_data(fields, 1);
2571 const gchar *team_name = g_list_nth_data(fields, 2);
2572 const gchar *name = g_list_nth_data(fields, 3);
2573
2574 //TODO: add alias ?
2575 return g_strconcat(team_id, "^", id, "^", name, MATTERMOST_CHANNEL_SEPARATOR, team_name, NULL); //FIXME: need proper separator - unique !
2576 }
2577
2578 //roomlist_deserialize
2579 static GHashTable *
mm_chat_info_defaults(PurpleConnection * pc,const char * chatname)2580 mm_chat_info_defaults(PurpleConnection *pc, const char *chatname)
2581 {
2582 GHashTable *defaults = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
2583
2584 if (chatname != NULL)
2585 {
2586 gchar **chat_parts = g_strsplit_set(chatname, "^", 3); //FIXME: need proper separator - unique !
2587
2588 if (chat_parts[0]) {
2589 g_hash_table_insert(defaults, "team_id", g_strdup(chat_parts[0]));
2590 if (chat_parts[1]) {
2591 g_hash_table_insert(defaults, "id", g_strdup(chat_parts[1]));
2592 if (chat_parts[2]) {
2593 g_hash_table_insert(defaults, "name", g_strdup(chat_parts[2]));
2594 }
2595 }
2596 }
2597 //TODO: add alias ?
2598 g_strfreev(chat_parts);
2599 } else {
2600 g_hash_table_insert(defaults, "team_id", g_strdup(mm_get_first_team_id(purple_connection_get_protocol_data(pc))));
2601 }
2602
2603 return defaults;
2604 }
2605
2606 PurpleRoomlist *
mm_roomlist_get_list(PurpleConnection * pc)2607 mm_roomlist_get_list(PurpleConnection *pc)
2608 {
2609 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
2610 PurpleRoomlist *roomlist;
2611 GList *fields = NULL;
2612 PurpleRoomlistField *f;
2613 gchar *url;
2614 GList *teams, *i;
2615
2616 roomlist = purple_roomlist_new(ma->account);
2617
2618 f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("ID"), "id", TRUE);
2619 fields = g_list_append(fields, f);
2620
2621 f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("Team ID"), "team_id", TRUE);
2622 fields = g_list_append(fields, f);
2623
2624 f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("Team Name"), "team_name", TRUE);
2625 fields = g_list_append(fields, f);
2626
2627 f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("Name"), "name", TRUE);
2628 fields = g_list_append(fields, f);
2629
2630 f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("Display Name"), "display_name", FALSE);
2631 fields = g_list_append(fields, f);
2632
2633 f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("Type"), "type", FALSE);
2634 fields = g_list_append(fields, f);
2635
2636 f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("Header"), "header", FALSE);
2637 fields = g_list_append(fields, f);
2638
2639 f = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING, _("Purpose"), "purpose", FALSE);
2640 fields = g_list_append(fields, f);
2641
2642 purple_roomlist_set_fields(roomlist, fields);
2643 purple_roomlist_set_in_progress(roomlist, TRUE);
2644
2645 //Loop through teams and look for channels within each
2646 for(i = teams = g_hash_table_get_keys(ma->teams); i; i = i->next)
2647 {
2648 MatterMostTeamRoomlist *mmtrl;
2649 const gchar *team_id = i->data;
2650
2651 // Get a list of public channels the user has *not* yet joined
2652 mmtrl = g_new0(MatterMostTeamRoomlist, 1);
2653 mmtrl->team_id = g_strdup(team_id);
2654 mmtrl->team_desc = g_strdup(_(": More public channels"));
2655 mmtrl->roomlist = roomlist;
2656 url = mm_build_url(ma,"/teams/%s/channels", team_id);
2657 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_roomlist_got_list, mmtrl);
2658 g_free(url);
2659
2660 ma->roomlist_team_count++;
2661 }
2662
2663 return roomlist;
2664 }
2665
2666
2667 void
mm_set_idle(PurpleConnection * pc,int time)2668 mm_set_idle(PurpleConnection *pc, int time)
2669 {
2670 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
2671 const gchar *channel_id = "";
2672
2673 if (time < 20) {
2674 channel_id = ma->last_channel_id;
2675 }
2676
2677 mm_mark_room_messages_read(ma, channel_id);
2678 }
2679
2680 gboolean
mm_idle_updater_timeout(gpointer data)2681 mm_idle_updater_timeout(gpointer data)
2682 {
2683 PurpleConnection *pc = data;
2684 PurpleAccount *account = purple_connection_get_account(pc);
2685 PurplePresence *presence = purple_account_get_presence(account);
2686 time_t idle_time = purple_presence_get_idle_time(presence);
2687
2688 if (idle_time > 0) {
2689 idle_time -= time(NULL);
2690 }
2691
2692 mm_set_idle(pc, idle_time);
2693
2694 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
2695 mm_refresh_statuses(ma,NULL);
2696
2697 return TRUE;
2698 }
2699
2700 void
mm_set_status(PurpleAccount * account,PurpleStatus * status)2701 mm_set_status(PurpleAccount *account, PurpleStatus *status)
2702 {
2703 PurpleConnection *pc = purple_account_get_connection(account);
2704 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
2705 const char *status_id = purple_status_get_id(status);
2706 JsonObject *data;
2707 gchar *setstatus;
2708 gchar *postdata, *url;
2709
2710 // tell MM that we are going offline but do not disconnect.
2711 // will stay in MM as offline until next status change.
2712 // when posting status changes for online for ~ 30 secs
2713 // then changes back again.
2714 if (purple_strequal(status_id, "invisible")) {
2715 setstatus = g_strdup("offline");
2716 } else {
2717 setstatus = g_strdup(status_id);
2718 }
2719
2720 data = json_object_new();
2721 json_object_set_string_member(data, "status", setstatus);
2722 if(ma->self == NULL){
2723 purple_debug_error("mattermost","Mattermost Account is NULL");
2724 return;
2725 }
2726 json_object_set_string_member(data, "user_id", ma->self->user_id);
2727 postdata = json_object_to_string(data);
2728 url = mm_build_url(ma,"/users/%s/status", ma->self->user_id);
2729 mm_fetch_url(ma, url, MATTERMOST_HTTP_PUT, postdata, -1, NULL, NULL);
2730 g_free(url);
2731
2732 g_free(postdata);
2733 json_object_unref(data);
2734 g_free(setstatus);
2735 }
2736
2737 static void
mm_restart_channel(MattermostAccount * ma)2738 mm_restart_channel(MattermostAccount *ma)
2739 {
2740 purple_connection_set_state(ma->pc, PURPLE_CONNECTION_CONNECTING);
2741 mm_start_socket(ma);
2742 }
2743
2744 static void
mm_build_groups_from_blist(MattermostAccount * ma)2745 mm_build_groups_from_blist(MattermostAccount *ma)
2746 {
2747 PurpleBlistNode *node;
2748
2749 for (node = purple_blist_get_root();
2750 node != NULL;
2751 node = purple_blist_node_next(node, TRUE)) {
2752 if (PURPLE_IS_CHAT(node)) {
2753 PurpleChat *chat = PURPLE_CHAT(node);
2754
2755 if (purple_chat_get_account(chat) != ma->account) {
2756 continue;
2757 }
2758
2759 GHashTable *components = purple_chat_get_components(chat);
2760
2761 if (components != NULL) {
2762 mm_set_group_chat(ma, g_hash_table_lookup(components, "team_id") , g_hash_table_lookup(components, "name"), g_hash_table_lookup(components, "id"));
2763 } //TODO: else { ERROR }
2764
2765 } else if (PURPLE_IS_BUDDY(node)) {
2766 const gchar *room_id;
2767 const gchar *user_id;
2768 const gchar *username;
2769 PurpleBuddy *buddy = PURPLE_BUDDY(node);
2770 if (purple_buddy_get_account(buddy) != ma->account) {
2771 continue;
2772 }
2773
2774 username = purple_buddy_get_name(buddy);
2775 room_id = purple_blist_node_get_string(node, "room_id");
2776 user_id = purple_blist_node_get_string(node, "user_id");
2777 if (room_id != NULL) {
2778 g_hash_table_replace(ma->one_to_ones, g_strdup(room_id), g_strdup(username));
2779 g_hash_table_replace(ma->one_to_ones_rev, g_strdup(username), g_strdup(room_id));
2780 }
2781 if (user_id != NULL) {
2782 g_hash_table_replace(ma->ids_to_usernames, g_strdup(user_id), g_strdup(username));
2783 g_hash_table_replace(ma->usernames_to_ids, g_strdup(username), g_strdup(user_id));
2784 }
2785 }
2786 }
2787 }
2788
2789 static guint mm_conv_send_typing(PurpleConversation *conv, PurpleIMTypingState state, MattermostAccount *ma);
2790 static gulong chat_conversation_typing_signal = 0;
2791 static void mm_mark_conv_seen(PurpleConversation *conv, PurpleConversationUpdateType type);
2792 static gulong conversation_updated_signal = 0;
2793
2794 void
mm_login(PurpleAccount * account)2795 mm_login(PurpleAccount *account)
2796 {
2797 MattermostAccount *ma;
2798 PurpleConnection *pc = purple_account_get_connection(account);
2799 gchar **userparts;
2800 gchar **serverparts;
2801 const gchar *username = purple_account_get_username(account);
2802 gchar *url;
2803 PurpleConnectionFlags pc_flags;
2804 const gchar *split_string = (char[2]) {MATTERMOST_SERVER_SPLIT_CHAR, '\0'};
2805
2806 pc_flags = purple_connection_get_flags(pc);
2807 pc_flags |= PURPLE_CONNECTION_FLAG_HTML;
2808 pc_flags |= PURPLE_CONNECTION_FLAG_NO_FONTSIZE;
2809 pc_flags |= PURPLE_CONNECTION_FLAG_NO_BGCOLOR;
2810 purple_connection_set_flags(pc, pc_flags);
2811
2812 ma = g_new0(MattermostAccount, 1);
2813 purple_connection_set_protocol_data(pc, ma);
2814 ma->account = account;
2815 ma->pc = pc;
2816 ma->cookie_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2817 ma->seq = 1;
2818
2819 if (purple_account_get_string(account, "last_message_timestamp", NULL)) {
2820 const gchar *last_message_timestamp_str = purple_account_get_string(account, "last_message_timestamp", NULL);
2821 ma->last_load_last_message_timestamp = g_ascii_strtoll(last_message_timestamp_str, NULL, 10);
2822 } else {
2823 ma->last_load_last_message_timestamp = purple_account_get_int(account, "last_message_timestamp_high", 0);
2824 if (ma->last_load_last_message_timestamp != 0) {
2825 ma->last_load_last_message_timestamp = (ma->last_load_last_message_timestamp << 32) | ((guint64) purple_account_get_int(account, "last_message_timestamp_low", 0) & 0xFFFFFFFF);
2826 }
2827 }
2828 if (ma->last_load_last_message_timestamp < 0) {
2829 ma->last_load_last_message_timestamp = 0;
2830 }
2831
2832 ma->client_config = g_new0(MattermostClientConfig,1);
2833 ma->client_config->public_link = FALSE;
2834 ma->client_config->enable_commands = FALSE;
2835
2836 ma->one_to_ones = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2837 ma->one_to_ones_rev = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2838 ma->group_chats = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2839 ma->group_chats_rev = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2840 ma->aliases = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2841 ma->group_chats_creators = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2842 ma->sent_message_ids = g_hash_table_new_full(g_str_insensitive_hash, g_str_insensitive_equal, g_free, NULL);
2843 ma->result_callbacks = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
2844 ma->usernames_to_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2845 ma->ids_to_usernames = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2846 ma->teams = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2847 ma->teams_display_names = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2848 ma->channel_teams = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
2849 ma->received_message_queue = g_queue_new();
2850
2851 userparts = g_strsplit(username, split_string, 2);
2852
2853 if (userparts[0] == NULL) {
2854 purple_connection_error(pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, "No username supplied");
2855 return;
2856 }
2857 if (userparts[1] == NULL) {
2858 purple_connection_error(pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, "No server supplied (use username|server)");
2859 return;
2860 }
2861
2862 serverparts = g_strsplit(userparts[1], "/", 2);
2863 if( serverparts[0] == NULL ) {
2864 purple_connection_error(pc, PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED, "No server supplied (use username|server)");
2865 return;
2866 }
2867
2868 purple_connection_set_display_name(pc, userparts[0]);
2869 ma->username = g_strdup(userparts[0]);
2870 ma->server = g_strdup(serverparts[0]);
2871 g_strfreev(userparts);
2872 if( serverparts[1] == NULL ) {
2873 ma->api_endpoint = g_strdup(MATTERMOST_API_EP);
2874 }
2875 else {
2876 ma->api_endpoint = g_strconcat("/", serverparts[1], MATTERMOST_API_EP, NULL);
2877 }
2878 g_strfreev(serverparts);
2879
2880 purple_connection_set_state(pc, PURPLE_CONNECTION_CONNECTING);
2881
2882 //Build the initial hash tables from the current buddy list
2883 mm_build_groups_from_blist(ma);
2884
2885 //TODO check for two-factor-auth
2886 {
2887 JsonObject *data = json_object_new();
2888 gchar *postdata;
2889
2890 if (purple_account_get_bool(ma->account, "use-mmauthtoken", FALSE)) {
2891 ma->session_token = g_strdup(purple_connection_get_password(pc));
2892
2893 mm_get_me(ma);
2894
2895 } else {
2896 json_object_set_string_member(data, "login_id", ma->username);
2897 json_object_set_string_member(data, "password", purple_connection_get_password(pc));
2898 json_object_set_string_member(data, "token", ""); //TODO 2FA
2899
2900 postdata = json_object_to_string(data);
2901
2902 url = mm_build_url(ma,"/users/login");
2903 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_login_response, NULL);
2904
2905 g_free(postdata);
2906 g_free(url);
2907 }
2908 json_object_unref(data);
2909 }
2910
2911 if (!chat_conversation_typing_signal) {
2912 chat_conversation_typing_signal = purple_signal_connect(purple_conversations_get_handle(), "chat-conversation-typing", purple_connection_get_protocol(pc), PURPLE_CALLBACK(mm_conv_send_typing), NULL);
2913 }
2914 if (!conversation_updated_signal) {
2915 conversation_updated_signal = purple_signal_connect(purple_conversations_get_handle(), "conversation-updated", purple_connection_get_protocol(pc), PURPLE_CALLBACK(mm_mark_conv_seen), NULL);
2916 }
2917 }
2918
2919
2920 static void
mm_close(PurpleConnection * pc)2921 mm_close(PurpleConnection *pc)
2922 {
2923 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
2924
2925 g_return_if_fail(ma != NULL);
2926
2927 mm_set_status(ma->account, purple_presence_get_active_status(purple_account_get_presence(ma->account)));
2928
2929 if(ma->idle_timeout > 0){
2930 g_source_remove(ma->idle_timeout);
2931 }
2932 if(ma->read_messages_timeout > 0){
2933 g_source_remove(ma->read_messages_timeout);
2934 }
2935
2936 purple_proxy_connect_cancel_with_handle(pc);
2937 if (ma->websocket != NULL) purple_ssl_close(ma->websocket);
2938 if (ma->websocket_inpa) purple_input_remove(ma->websocket_inpa);
2939 if (ma->websocket_fd >= 0) close(ma->websocket_fd);
2940
2941 g_hash_table_remove_all(ma->one_to_ones);
2942 g_hash_table_unref(ma->one_to_ones);
2943 g_hash_table_remove_all(ma->one_to_ones_rev);
2944 g_hash_table_unref(ma->one_to_ones_rev);
2945 g_hash_table_remove_all(ma->group_chats);
2946 g_hash_table_unref(ma->group_chats);
2947 g_hash_table_remove_all(ma->aliases);
2948 g_hash_table_unref(ma->aliases);
2949 g_hash_table_remove_all(ma->group_chats_creators);
2950 g_hash_table_unref(ma->group_chats_creators);
2951 g_hash_table_remove_all(ma->sent_message_ids);
2952 g_hash_table_unref(ma->sent_message_ids);
2953 g_hash_table_remove_all(ma->result_callbacks);
2954 g_hash_table_unref(ma->result_callbacks);
2955 g_hash_table_remove_all(ma->usernames_to_ids);
2956 g_hash_table_unref(ma->usernames_to_ids);
2957 g_hash_table_remove_all(ma->ids_to_usernames);
2958 g_hash_table_unref(ma->ids_to_usernames);
2959 g_hash_table_remove_all(ma->teams);
2960 g_hash_table_unref(ma->teams);
2961 g_hash_table_remove_all(ma->teams_display_names);
2962 g_hash_table_unref(ma->teams_display_names);
2963 g_hash_table_remove_all(ma->channel_teams);
2964 g_hash_table_unref(ma->channel_teams);
2965 g_queue_free(ma->received_message_queue);
2966
2967 while (ma->http_conns) {
2968 purple_http_conn_cancel(ma->http_conns->data);
2969 ma->http_conns = g_slist_delete_link(ma->http_conns, ma->http_conns);
2970 }
2971
2972 while (ma->pending_writes) {
2973 json_object_unref(ma->pending_writes->data);
2974 ma->pending_writes = g_slist_delete_link(ma->pending_writes, ma->pending_writes);
2975 }
2976
2977 mm_g_free_mattermost_user(ma->self);
2978 mm_g_free_mattermost_client_config(ma->client_config);
2979
2980 g_hash_table_destroy(ma->cookie_table); ma->cookie_table = NULL;
2981 g_free(ma->last_channel_id); ma->last_channel_id = NULL;
2982 g_free(ma->current_channel_id); ma->current_channel_id = NULL;
2983 g_free(ma->username); ma->username = NULL;
2984 g_free(ma->server); ma->server = NULL;
2985 g_free(ma->api_endpoint); ma->api_endpoint = NULL;
2986 g_free(ma->frame); ma->frame = NULL;
2987 g_free(ma->session_token); ma->session_token = NULL;
2988 g_free(ma->channel); ma->channel = NULL;
2989 g_regex_unref(ma->mention_me_regex); ma->mention_me_regex = NULL;
2990 g_regex_unref(ma->mention_all_regex); ma->mention_all_regex = NULL;
2991 g_free(ma);
2992 }
2993
2994
2995 //static void mm_start_polling(MattermostAccount *ma);
2996
2997 static gboolean
mm_process_frame(MattermostAccount * ma,const gchar * frame)2998 mm_process_frame(MattermostAccount *ma, const gchar *frame)
2999 {
3000 JsonParser *parser = json_parser_new();
3001 JsonNode *root;
3002
3003 purple_debug_info("mattermost", "got frame data: %s\n", frame);
3004
3005 if (!json_parser_load_from_data(parser, frame, -1, NULL))
3006 {
3007 purple_debug_error("mattermost", "Error parsing response: %s\n", frame);
3008 return TRUE;
3009 }
3010
3011 root = json_parser_get_root(parser);
3012
3013 if (root != NULL) {
3014 mm_process_msg(ma, root);
3015 }
3016
3017 g_object_unref(parser);
3018 return TRUE;
3019 }
3020
3021 static size_t
mm_socket_read(MattermostAccount * ma,gpointer buffer,size_t len)3022 mm_socket_read(MattermostAccount *ma, gpointer buffer, size_t len)
3023 {
3024 if (ma->websocket) {
3025 return purple_ssl_read(ma->websocket, buffer, len);
3026 }
3027
3028 return read(ma->websocket_fd, buffer, len);
3029 }
3030
3031 static size_t
mm_socket_write(MattermostAccount * ma,gconstpointer buffer,size_t len)3032 mm_socket_write(MattermostAccount *ma, gconstpointer buffer, size_t len)
3033 {
3034 if (ma->websocket) {
3035 return purple_ssl_write(ma->websocket, buffer, len);
3036 }
3037
3038 return write(ma->websocket_fd, buffer, len);
3039 }
3040
3041 static guchar *
mm_websocket_mask(guchar key[4],const guchar * pload,guint64 psize)3042 mm_websocket_mask(guchar key[4], const guchar *pload, guint64 psize)
3043 {
3044 guint64 i;
3045 guchar *ret = g_new0(guchar, psize);
3046
3047 for (i = 0; i < psize; i++) {
3048 ret[i] = pload[i] ^ key[i % 4];
3049 }
3050
3051 return ret;
3052 }
3053
3054 static void
mm_socket_write_data(MattermostAccount * ma,guchar * data,gssize data_len,guchar type)3055 mm_socket_write_data(MattermostAccount *ma, guchar *data, gssize data_len, guchar type)
3056 {
3057 guchar *full_data;
3058 guint len_size = 1;
3059 guchar mkey[4] = { 0x12, 0x34, 0x56, 0x78 };
3060
3061 if (data_len == -1) {
3062 data_len = strlen((gchar *) data);
3063 }
3064
3065 if (data_len) {
3066 purple_debug_info("mattermost", "sending frame: %*s\n", (int)data_len, data);
3067 }
3068
3069 data = mm_websocket_mask(mkey, data, data_len);
3070
3071 if (data_len > 125) {
3072 if (data_len <= G_MAXUINT16) {
3073 len_size += 2;
3074 } else {
3075 len_size += 8;
3076 }
3077 }
3078 full_data = g_new0(guchar, 1 + data_len + len_size + 4);
3079
3080 if (type == 0) {
3081 type = 129;
3082 }
3083 full_data[0] = type;
3084
3085 if (data_len <= 125) {
3086 full_data[1] = data_len | 0x80;
3087 } else if (data_len <= G_MAXUINT16) {
3088 guint16 be_len = GUINT16_TO_BE(data_len);
3089 full_data[1] = 126 | 0x80;
3090 memmove(full_data + 2, &be_len, 2);
3091 } else {
3092 guint64 be_len = GUINT64_TO_BE(data_len);
3093 full_data[1] = 127 | 0x80;
3094 memmove(full_data + 2, &be_len, 8);
3095 }
3096
3097 memmove(full_data + (1 + len_size), &mkey, 4);
3098 memmove(full_data + (1 + len_size + 4), data, data_len);
3099
3100 mm_socket_write(ma, full_data, 1 + data_len + len_size + 4);
3101
3102 g_free(full_data);
3103 g_free(data);
3104 }
3105
3106 /* takes ownership of data parameter */
3107 static void
mm_socket_write_json(MattermostAccount * ma,JsonObject * data)3108 mm_socket_write_json(MattermostAccount *ma, JsonObject *data)
3109 {
3110 gchar *str;
3111
3112 if (ma->websocket == NULL && ma->websocket_fd <= 0) {
3113 if (data != NULL) {
3114 ma->pending_writes = g_slist_append(ma->pending_writes, data);
3115 }
3116 return;
3117 }
3118
3119 str = json_object_to_string(data);
3120
3121 mm_socket_write_data(ma, (guchar *)str, -1, 0);
3122
3123 g_free(str);
3124 }
3125
3126 static void
mm_socket_got_data(gpointer userdata,PurpleSslConnection * conn,PurpleInputCondition cond)3127 mm_socket_got_data(gpointer userdata, PurpleSslConnection *conn, PurpleInputCondition cond)
3128 {
3129 MattermostAccount *ma = userdata;
3130 guchar length_code;
3131 int read_len = 0;
3132 gboolean done_some_reads = FALSE;
3133
3134
3135 if (G_UNLIKELY(!ma->websocket_header_received)) {
3136 gint nlbr_count = 0;
3137 gchar nextchar;
3138
3139 while(nlbr_count < 4 && (read_len = mm_socket_read(ma, &nextchar, 1)) == 1) {
3140 if (nextchar == '\r' || nextchar == '\n') {
3141 nlbr_count++;
3142 } else {
3143 nlbr_count = 0;
3144 }
3145 }
3146
3147 if (nlbr_count == 4) {
3148 ma->websocket_header_received = TRUE;
3149 done_some_reads = TRUE;
3150
3151 /* flush stuff that we attempted to send before the websocket was ready */
3152 while (ma->pending_writes) {
3153 mm_socket_write_json(ma, ma->pending_writes->data);
3154 ma->pending_writes = g_slist_delete_link(ma->pending_writes, ma->pending_writes);
3155 }
3156 }
3157 }
3158
3159 while(ma->frame || (read_len = mm_socket_read(ma, &ma->packet_code, 1)) == 1) {
3160 if (!ma->frame) {
3161 if (ma->packet_code != 129) {
3162 if (ma->packet_code == 136) {
3163 purple_debug_error("mattermost", "websocket closed\n");
3164
3165 // Try reconnect
3166 mm_start_socket(ma);
3167
3168 return;
3169 } else if (ma->packet_code == 137) {
3170 // Ping
3171 gint ping_frame_len = 0;
3172 length_code = 0;
3173 mm_socket_read(ma, &length_code, 1);
3174 if (length_code <= 125) {
3175 ping_frame_len = length_code;
3176 } else if (length_code == 126) {
3177 guchar len_buf[2];
3178 mm_socket_read(ma, len_buf, 2);
3179 ping_frame_len = (len_buf[0] << 8) + len_buf[1];
3180 } else if (length_code == 127) {
3181 mm_socket_read(ma, &ping_frame_len, 8);
3182 ping_frame_len = GUINT64_FROM_BE(ping_frame_len);
3183 }
3184 if (ping_frame_len) {
3185 guchar *pong_data = g_new0(guchar, ping_frame_len);
3186 mm_socket_read(ma, pong_data, ping_frame_len);
3187
3188 mm_socket_write_data(ma, pong_data, ping_frame_len, 138);
3189 g_free(pong_data);
3190 } else {
3191 mm_socket_write_data(ma, (guchar *) "", 0, 138);
3192 }
3193 return;
3194 } else if (ma->packet_code == 138) {
3195 // Pong
3196 //who cares
3197 return;
3198 }
3199 purple_debug_error("mattermost", "unknown websocket error %d\n", ma->packet_code);
3200 return;
3201 }
3202
3203 length_code = 0;
3204 mm_socket_read(ma, &length_code, 1);
3205 if (length_code <= 125) {
3206 ma->frame_len = length_code;
3207 } else if (length_code == 126) {
3208 guchar len_buf[2];
3209 mm_socket_read(ma, len_buf, 2);
3210 ma->frame_len = (len_buf[0] << 8) + len_buf[1];
3211 } else if (length_code == 127) {
3212 mm_socket_read(ma, &ma->frame_len, 8);
3213 ma->frame_len = GUINT64_FROM_BE(ma->frame_len);
3214 }
3215 //purple_debug_info("mattermost", "frame_len: %" G_GUINT64_FORMAT "\n", ma->frame_len);
3216
3217 ma->frame = g_new0(gchar, ma->frame_len + 1);
3218 ma->frame_len_progress = 0;
3219 }
3220
3221 do {
3222 read_len = mm_socket_read(ma, ma->frame + ma->frame_len_progress, ma->frame_len - ma->frame_len_progress);
3223 if (read_len > 0) {
3224 ma->frame_len_progress += read_len;
3225 }
3226 } while (read_len > 0 && ma->frame_len_progress < ma->frame_len);
3227 done_some_reads = TRUE;
3228
3229 if (ma->frame_len_progress == ma->frame_len) {
3230 gboolean success = mm_process_frame(ma, ma->frame);
3231 g_free(ma->frame); ma->frame = NULL;
3232 ma->packet_code = 0;
3233 ma->frame_len = 0;
3234 ma->frames_since_reconnect++;
3235
3236 if (G_UNLIKELY((ma->websocket == NULL && ma->websocket_fd <= 0) || success == FALSE)) {
3237 return;
3238 }
3239 } else {
3240 return;
3241 }
3242 }
3243
3244 if (done_some_reads == FALSE && read_len <= 0) {
3245 if (read_len < 0 && errno == EAGAIN) {
3246 return;
3247 }
3248
3249 purple_debug_error("mattermost", "got errno %d, read_len %d from websocket thread\n", errno, read_len);
3250
3251 if (ma->frames_since_reconnect < 2) {
3252 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, _("Lost connection to server"));
3253 } else {
3254 // Try reconnect
3255 mm_start_socket(ma);
3256 }
3257 }
3258 }
3259
3260 static void
mm_socket_got_data_nonssl(gpointer userdata,gint fd,PurpleInputCondition cond)3261 mm_socket_got_data_nonssl(gpointer userdata, gint fd, PurpleInputCondition cond)
3262 {
3263 mm_socket_got_data(userdata, NULL, cond);
3264 }
3265
3266 static void
mm_socket_send_headers(MattermostAccount * ma)3267 mm_socket_send_headers(MattermostAccount *ma)
3268 {
3269 gchar *websocket_header;
3270 const gchar *websocket_key = "15XF+ptKDhYVERXoGcdHTA=="; //TODO don't be lazy
3271
3272 // websocket_header = g_strdup_printf("GET %s/users/websocket HTTP/1.1\r\n"
3273 websocket_header = g_strdup_printf("GET %s/websocket HTTP/1.0\r\n"
3274 "Host: %s\r\n"
3275 "Connection: Upgrade\r\n"
3276 "Pragma: no-cache\r\n"
3277 "Cache-Control: no-cache\r\n"
3278 "Upgrade: websocket\r\n"
3279 "Sec-WebSocket-Version: 13\r\n"
3280 "Sec-WebSocket-Key: %s\r\n"
3281 "User-Agent: " MATTERMOST_USERAGENT "\r\n"
3282 "X-Requested-With: XMLHttpRequest\r\n"
3283 "Authorization: Bearer %s\r\n"
3284 //"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n"
3285 "\r\n", ma->api_endpoint, ma->server,
3286 websocket_key, ma->session_token);
3287
3288 mm_socket_write(ma, websocket_header, strlen(websocket_header));
3289
3290 g_free(websocket_header);
3291 }
3292
3293 static void
mm_socket_connected(gpointer userdata,PurpleSslConnection * conn,PurpleInputCondition cond)3294 mm_socket_connected(gpointer userdata, PurpleSslConnection *conn, PurpleInputCondition cond)
3295 {
3296 MattermostAccount *ma = userdata;
3297
3298 ma->websocket = conn;
3299
3300 purple_ssl_input_add(ma->websocket, mm_socket_got_data, ma);
3301
3302 mm_socket_send_headers(ma);
3303 }
3304
3305 static void
mm_socket_failed(PurpleSslConnection * conn,PurpleSslErrorType errortype,gpointer userdata)3306 mm_socket_failed(PurpleSslConnection *conn, PurpleSslErrorType errortype, gpointer userdata)
3307 {
3308 MattermostAccount *ma = userdata;
3309
3310 ma->websocket = NULL;
3311 ma->websocket_header_received = FALSE;
3312
3313 if (ma->frames_since_reconnect < 1) {
3314 purple_connection_error(ma->pc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR, _("Couldn't connect to gateway"));
3315 } else {
3316 mm_restart_channel(ma);
3317 }
3318 }
3319
3320 static void
mm_socket_connected_nonssl(gpointer userdata,gint source,const gchar * error_message)3321 mm_socket_connected_nonssl(gpointer userdata, gint source, const gchar *error_message)
3322 {
3323 MattermostAccount *ma = userdata;
3324
3325 if (source < 0) {
3326 // Error when connecting
3327 mm_socket_failed(NULL, 0, ma);
3328 return;
3329 }
3330
3331 ma->websocket_fd = source;
3332 ma->websocket_inpa = purple_input_add(source, PURPLE_INPUT_READ, mm_socket_got_data_nonssl, ma);
3333
3334 mm_socket_send_headers(ma);
3335 }
3336
3337 static void
mm_start_socket(MattermostAccount * ma)3338 mm_start_socket(MattermostAccount *ma)
3339 {
3340 gchar **server_split;
3341 gint port = 443;
3342
3343 //Reset all the old stuff
3344 if (ma->websocket != NULL) {
3345 purple_ssl_close(ma->websocket);
3346 }
3347 if (ma->websocket_inpa) {
3348 purple_input_remove(ma->websocket_inpa);
3349 }
3350 if (ma->websocket_fd > 0) {
3351 close(ma->websocket_fd);
3352 }
3353
3354 if (!purple_account_get_bool(ma->account, "use-ssl", TRUE)) {
3355 port = 80;
3356 }
3357
3358 ma->websocket_fd = 0;
3359 ma->websocket_inpa = 0;
3360 ma->websocket = NULL;
3361 ma->websocket_header_received = FALSE;
3362 g_free(ma->frame); ma->frame = NULL;
3363 ma->packet_code = 0;
3364 ma->frame_len = 0;
3365 ma->frames_since_reconnect = 0;
3366
3367 server_split = g_strsplit(ma->server, ":", 2);
3368 if (server_split[1] != NULL) {
3369 port = atoi(server_split[1]);
3370 }
3371
3372 if (purple_account_get_bool(ma->account, "use-ssl", TRUE)) {
3373 ma->websocket = purple_ssl_connect(ma->account, server_split[0], port, mm_socket_connected, mm_socket_failed, ma);
3374 } else {
3375 purple_proxy_connect(ma->pc, ma->account, server_split[0], port, mm_socket_connected_nonssl, ma);
3376 }
3377
3378 g_strfreev(server_split);
3379 }
3380
3381
3382 static void
mm_chat_leave(PurpleConnection * pc,int id)3383 mm_chat_leave(PurpleConnection *pc, int id)
3384 {
3385 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
3386 const gchar *channel_id; //, *team_id;
3387 PurpleChatConversation *chatconv;
3388 gchar *url;
3389
3390 chatconv = purple_conversations_find_chat(pc, id);
3391 if (chatconv == NULL) {
3392 return;
3393 }
3394
3395 channel_id = purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
3396 if (channel_id == NULL) {
3397 channel_id = purple_conversation_get_name(PURPLE_CONVERSATION(chatconv));
3398 }
3399
3400 url = mm_build_url(ma,"/channels/%s/members/%s", channel_id, ma->self->user_id);
3401 mm_fetch_url(ma, url, MATTERMOST_HTTP_DELETE, NULL, -1, NULL, NULL);
3402
3403 g_free(url);
3404
3405 }
3406
3407 static void
mm_chat_invite(PurpleConnection * pc,int id,const char * message,const char * who)3408 mm_chat_invite(PurpleConnection *pc, int id, const char *message, const char *who)
3409 {
3410 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
3411 PurpleChatConversation *chatconv;
3412 const gchar *user_id;
3413 JsonObject *data;
3414 gchar *postdata;
3415 gchar *url;
3416 const gchar *channel_id;
3417
3418 chatconv = purple_conversations_find_chat(pc, id);
3419 if (chatconv == NULL) {
3420 return;
3421 }
3422
3423 channel_id = purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
3424 if (channel_id == NULL) {
3425 channel_id = purple_conversation_get_name(PURPLE_CONVERSATION(chatconv));
3426 }
3427
3428 user_id = g_hash_table_lookup(ma->usernames_to_ids, who);
3429 if (user_id == NULL) {
3430 //TODO search for user
3431
3432 // /users/search
3433
3434 //"term", buddy_name
3435 //"allow_inactive", TRUE
3436
3437 return;
3438 }
3439
3440 data = json_object_new();
3441 json_object_set_string_member(data, "user_id", user_id);
3442
3443 postdata = json_object_to_string(data);
3444
3445 url = mm_build_url(ma,"/channels/%s/members", channel_id);
3446
3447 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, NULL, NULL);
3448
3449 g_free(postdata);
3450 g_free(url);
3451 }
3452
3453 static GList *
mm_chat_info(PurpleConnection * pc)3454 mm_chat_info(PurpleConnection *pc)
3455 {
3456 GList *m = NULL;
3457 PurpleProtocolChatEntry *pce;
3458
3459 pce = g_new0(PurpleProtocolChatEntry, 1);
3460 pce->label = _("Name");
3461 pce->identifier = "name";
3462 pce->required = TRUE;
3463 m = g_list_append(m, pce);
3464
3465 pce = g_new0(PurpleProtocolChatEntry, 1);
3466 pce->label = _("Channel ID");
3467 pce->identifier = "id";
3468 pce->required = TRUE;
3469 m = g_list_append(m, pce);
3470
3471 pce = g_new0(PurpleProtocolChatEntry, 1);
3472 pce->label = _("Team ID");
3473 pce->identifier = "team_id";
3474 pce->required = TRUE;
3475 m = g_list_append(m, pce);
3476
3477 return m;
3478 }
3479
3480 static gchar *
mm_get_chat_name(GHashTable * data)3481 mm_get_chat_name(GHashTable *data)
3482 {
3483 gchar *temp;
3484
3485 if (data == NULL) {
3486 return NULL;
3487 }
3488
3489 temp = g_hash_table_lookup(data, "name");
3490
3491 if (temp == NULL) {
3492 temp = g_hash_table_lookup(data, "id");
3493 }
3494
3495 if (temp == NULL) {
3496 return NULL;
3497 }
3498
3499 return g_strdup(temp);
3500 }
3501
3502
3503 static void mm_get_users_of_room(MattermostAccount *ma, MattermostChannel *channel);
3504
3505 static void
mm_got_users_of_room(MattermostAccount * ma,JsonNode * node,gpointer user_data)3506 mm_got_users_of_room(MattermostAccount *ma, JsonNode *node, gpointer user_data)
3507 {
3508 MattermostChannel *channel = user_data;
3509 PurpleGroup *default_group = mm_get_or_create_default_group();
3510
3511 if (!mm_check_mattermost_response(ma,node,_("Error"),g_strconcat(_("Error getting Mattermost channel users ("),channel->display_name,")",NULL),TRUE)) {
3512 channel->page_users = MATTERMOST_MAX_PAGES;
3513 return;
3514 }
3515
3516 PurpleChatConversation *chatconv = purple_conversations_find_chat(ma->pc, g_str_hash(channel->id));
3517
3518 GList *users_list = NULL, *flags_list = NULL;
3519
3520 JsonArray *users = json_node_get_array(node);
3521 guint i, len = json_array_get_length(users);
3522
3523 for (i = 0; i < len; i++) {
3524 JsonObject *user = json_array_get_object_element(users,i);
3525 const gchar *user_id = json_object_get_string_member(user, "id");
3526 const gchar *username = json_object_get_string_member(user, "username");
3527 const gchar *roles = json_object_get_string_member(user, "roles");
3528
3529 if (!mm_hash_table_contains(ma->ids_to_usernames, user_id)) {
3530 g_hash_table_replace(ma->ids_to_usernames, g_strdup(user_id), g_strdup(username));
3531 g_hash_table_replace(ma->usernames_to_ids, g_strdup(username), g_strdup(user_id));
3532
3533 if (chatconv == NULL && mm_hash_table_contains(ma->one_to_ones, channel->id)) {
3534 //Probably a direct message, add them to the buddy list
3535 PurpleBuddy *buddy = purple_blist_find_buddy(ma->account, username);
3536 if (buddy == NULL) {
3537 buddy = purple_buddy_new(ma->account, username, NULL);
3538 purple_blist_add_buddy(buddy, NULL, default_group, NULL);
3539
3540 PurpleIMConversation *imconv = purple_conversations_find_im_with_account(username, ma->account);
3541 if (imconv == NULL) {
3542 imconv = purple_im_conversation_new(ma->account, username);
3543 }
3544 mm_add_buddy(ma->pc, buddy, NULL, NULL);
3545 }
3546 purple_blist_node_set_string(PURPLE_BLIST_NODE(buddy), "room_id", channel->id);
3547 }
3548 }
3549
3550 if (chatconv != NULL) {
3551 PurpleChatUserFlags flags = mm_role_to_purple_flag(ma, roles);
3552 if (purple_strequal(channel->creator_id, user_id)) {
3553 flags |= PURPLE_CHAT_USER_OP;
3554 }
3555 if (!purple_chat_conversation_has_user(chatconv,username)) {
3556 users_list = g_list_prepend(users_list, g_strdup(username));
3557 flags_list = g_list_prepend(flags_list, GINT_TO_POINTER(flags));
3558 }
3559 }
3560 }
3561
3562 if (chatconv != NULL) {
3563 purple_chat_conversation_add_users(chatconv, users_list, NULL, flags_list, FALSE);
3564 }
3565
3566 while (users_list != NULL) {
3567 g_free(users_list->data);
3568 users_list = g_list_delete_link(users_list, users_list);
3569 }
3570 g_list_free(users_list);
3571 g_list_free(flags_list);
3572
3573 if (len == MATTERMOST_USER_PAGE_SIZE && channel->page_users < MATTERMOST_MAX_PAGES) {
3574 channel->page_users = channel->page_users + 1;
3575 mm_get_users_of_room(ma, channel);
3576 } else {
3577 channel->page_history = 0;
3578 mm_get_history_of_room(ma, channel, -1);
3579 }
3580 }
3581
3582
3583
3584 static void
mm_get_users_of_room(MattermostAccount * ma,MattermostChannel * channel)3585 mm_get_users_of_room(MattermostAccount *ma, MattermostChannel *channel)
3586 {
3587 gchar *url;
3588
3589 if (channel->page_users == MATTERMOST_MAX_PAGES) return;
3590
3591 url = mm_build_url(ma,"/users?in_channel=%s&page=%s&per_page=%s", channel->id,g_strdup_printf("%i",channel->page_users), g_strdup_printf("%i", MATTERMOST_USER_PAGE_SIZE));
3592 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_got_users_of_room, channel);
3593 g_free(url);
3594 }
3595
3596 static gint64
mm_get_channel_approximate_view_time(MattermostAccount * ma,MattermostChannel * channel)3597 mm_get_channel_approximate_view_time(MattermostAccount *ma, MattermostChannel *channel)
3598 {
3599 gchar *tmptime = NULL;
3600
3601 PurpleChat *chat = mm_purple_blist_find_chat(ma, channel->id);
3602 if (chat) {
3603 tmptime = g_strdup(purple_blist_node_get_string(PURPLE_BLIST_NODE(chat),"channel_approximate_view_time"));
3604 } else {
3605 PurpleBuddy *buddy = purple_blist_find_buddy(ma->account,g_hash_table_lookup(ma->one_to_ones,channel->id));
3606 if (buddy) {
3607 tmptime = g_strdup(purple_blist_node_get_string(PURPLE_BLIST_NODE(buddy),"channel_approximate_view_time"));
3608 }
3609 }
3610
3611 if(!tmptime) {
3612 tmptime = g_strdup_printf("%" G_GINT64_FORMAT, (g_get_real_time() / 1000)); // now.
3613 }
3614
3615 gint64 viewtime = g_ascii_strtoll(tmptime, NULL, 10);
3616 g_free(tmptime);
3617 return viewtime;
3618 }
3619
3620 static void mm_get_history_of_room(MattermostAccount *ma, MattermostChannel *channel, gint64 since);
3621
3622 static void
mm_got_history_of_room(MattermostAccount * ma,JsonNode * node,gpointer user_data)3623 mm_got_history_of_room(MattermostAccount *ma, JsonNode *node, gpointer user_data)
3624 {
3625 MattermostChannel *channel = user_data;
3626
3627 if (!mm_check_mattermost_response(ma,node,_("Error"),g_strconcat(_("Error getting Mattermost channel history ("),channel->display_name,")",NULL),TRUE)) {
3628 channel->page_history = MATTERMOST_MAX_PAGES;
3629 return;
3630 }
3631
3632 JsonObject *obj = json_node_get_object(node);
3633 JsonObject *posts = json_object_get_object_member(obj, "posts");
3634 JsonArray *order = json_object_get_array_member(obj, "order");
3635
3636 gint i, len = json_array_get_length(order);
3637
3638 // do not show updates (such as reactions). only edits, new posts and deletes
3639 for (i = len - 1; i >= 0; i--) {
3640 const gchar *post_id = json_array_get_string_element(order, i);
3641 JsonObject *post = json_object_get_object_member(posts, post_id);
3642
3643 const gint64 since = mm_get_channel_approximate_view_time(ma, channel);
3644 if (json_object_get_int_member(post, "create_at") < since && json_object_get_int_member(post, "edit_at") < since && json_object_get_int_member(post, "delete_at") < since) {
3645 json_array_remove_element(order, i);
3646 }
3647 }
3648 len = json_array_get_length(order);
3649
3650 if (len > 0) {
3651 if (!g_hash_table_lookup(ma->one_to_ones,channel->id)) { // not one to one
3652 PurpleChatConversation *chatconv=purple_conversations_find_chat(ma->pc, g_str_hash(channel->id));
3653 if (!chatconv) {
3654 PurpleChat *chat = mm_purple_blist_find_chat(ma,channel->id);
3655 if (chat) {
3656 GHashTable *components = purple_chat_get_components(chat);
3657 gchar *team_id = g_hash_table_lookup(components, "team_id");
3658 gchar *alias = g_hash_table_lookup(ma->aliases,channel->id);
3659
3660 PurpleChatConversation *conv = purple_serv_got_joined_chat(ma->pc, g_str_hash(channel->id), alias);
3661 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "id", g_strdup(channel->id));
3662 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "team_id", g_strdup(team_id));
3663 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "name", g_strdup(alias));
3664 purple_conversation_set_data(PURPLE_CONVERSATION(conv), "display_name", g_strdup(channel->display_name));
3665 purple_conversation_present(PURPLE_CONVERSATION(conv));
3666
3667 //HERE we already went through mm_get_users_of_room but since
3668 //chat window was not open, user list is empty, need to do it again
3669 //FIXME: this should be rewritten ...we call users read twice per channel
3670 channel->page_users = 0;
3671 mm_get_users_of_room(ma, channel);
3672 return;
3673 }
3674 }
3675 }
3676 // for IMCONV pidgin opens the conversation by itself.
3677 }
3678
3679
3680 for (i = len - 1; i >= 0; i--) {
3681 const gchar *post_id = json_array_get_string_element(order, i);
3682 JsonObject *post = json_object_get_object_member(posts, post_id);
3683 mm_process_room_message(ma, post, NULL);
3684 }
3685
3686 // 'since' does follow the page size
3687 if (len == MATTERMOST_HISTORY_PAGE_SIZE && channel->page_history < MATTERMOST_MAX_PAGES) {
3688 channel->page_history = channel->page_history + 1;
3689
3690 mm_get_history_of_room(ma, channel, -1); // FIXME: that should be parametrized !
3691 } else {
3692 channel->page_history = MATTERMOST_MAX_PAGES;
3693 // history will be stored in purple log, even if channel not read now, avoid re-reading later.
3694 mm_mark_room_messages_read_timeout_response(ma, NULL, channel->id);
3695
3696 mm_g_free_mattermost_channel(channel);
3697 }
3698 // for now we could just tell user...
3699
3700 }
3701
3702 static void
mm_get_history_of_room(MattermostAccount * ma,MattermostChannel * channel,gint64 since)3703 mm_get_history_of_room(MattermostAccount *ma, MattermostChannel *channel, gint64 since)
3704 {
3705 gchar *url;
3706
3707 if (channel->page_history == MATTERMOST_MAX_PAGES) return;
3708 if (!channel->id) return;
3709
3710 if (since < 0) {
3711 since = mm_get_channel_approximate_view_time(ma, channel);
3712 }
3713
3714 url = mm_build_url(ma,"/channels/%s/posts?page=%s&per_page=%s&since=%" G_GINT64_FORMAT "", channel->id, g_strdup_printf("%i",channel->page_history), g_strdup_printf("%i", MATTERMOST_HISTORY_PAGE_SIZE), since);
3715 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_got_history_of_room, channel);
3716 g_free(url);
3717 }
3718
3719 static void
mm_join_room(MattermostAccount * ma,MattermostChannel * channel)3720 mm_join_room(MattermostAccount *ma, MattermostChannel *channel)
3721 {
3722 mm_set_group_chat(ma, channel->team_id, channel->name, channel->id);
3723 mm_get_users_of_room(ma, channel);
3724 }
3725
3726
3727 static void
mm_join_chat(PurpleConnection * pc,GHashTable * chatdata)3728 mm_join_chat(PurpleConnection *pc, GHashTable *chatdata)
3729 {
3730 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
3731 const gchar *id = g_hash_table_lookup(chatdata, "id");
3732 const gchar *name = g_hash_table_lookup(chatdata, "name");
3733 const gchar *team_id = g_hash_table_lookup(chatdata, "team_id");
3734 const gchar *type = g_hash_table_lookup(chatdata, "type");
3735 const gchar *creator_id = g_hash_table_lookup(chatdata, "creator_id");
3736
3737 guint id_hash;
3738 PurpleChatConversation *chatconv;
3739
3740 if (id == NULL && name == NULL) {
3741 //What do?
3742 return;
3743 }
3744
3745 if (id == NULL) {
3746 id = g_hash_table_lookup(ma->group_chats_rev, name);
3747 }
3748 //TODO use the api look up name info from the id
3749 if (id == NULL) {
3750 return;
3751 }
3752
3753 id_hash = g_str_hash(id);
3754 chatconv = purple_conversations_find_chat(ma->pc, id_hash);
3755
3756 if (chatconv != NULL && !purple_chat_conversation_has_left(chatconv)) {
3757 purple_conversation_present(PURPLE_CONVERSATION(chatconv));
3758 return;
3759 }
3760
3761 const gchar *alias = g_hash_table_lookup(ma->aliases,id);
3762
3763 chatconv = purple_serv_got_joined_chat(pc, id_hash, alias);//ALIAS ?
3764
3765 purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "id", g_strdup(id));
3766 purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "team_id", g_strdup(team_id));
3767 purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "name", g_strdup(name));
3768 purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "type", g_strdup(type));
3769 purple_conversation_set_data(PURPLE_CONVERSATION(chatconv), "creator_id", g_strdup(creator_id));
3770 purple_conversation_present(PURPLE_CONVERSATION(chatconv));
3771
3772 mm_get_channel_by_id(ma,team_id,id);
3773 }
3774
3775 static void
mm_mark_room_messages_read_timeout_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)3776 mm_mark_room_messages_read_timeout_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
3777 {
3778 //reply contains 'last_viewed_at_times' which does not correspond to our activity ?
3779 //also prefs chanel_approximate_view_time is not updated ?
3780 // do we miss something ? do we need to update this ourselves ?.. for now lets do our own timekeeping.
3781 gchar *channel_id = user_data;
3782 gint64 now = g_get_real_time()/1000;
3783 PurpleChat *chat = mm_purple_blist_find_chat(ma, channel_id);
3784 if (chat) {
3785 purple_blist_node_set_string(PURPLE_BLIST_NODE(chat), "channel_approximate_view_time", g_strdup_printf("%" G_GINT64_FORMAT,now));
3786 } else {
3787 const gchar *username = g_hash_table_lookup(ma->one_to_ones,channel_id);
3788 if (username) {
3789 PurpleBuddy *buddy = purple_blist_find_buddy(ma->account, username);
3790 if (buddy) {
3791 purple_blist_node_set_string(PURPLE_BLIST_NODE(buddy), "channel_approximate_view_time", g_strdup_printf("%" G_GINT64_FORMAT,now));
3792 }
3793 }
3794 }
3795 }
3796
3797 static gboolean
mm_mark_room_messages_read_timeout(gpointer userdata)3798 mm_mark_room_messages_read_timeout(gpointer userdata)
3799 {
3800 MattermostAccount *ma = userdata;
3801 JsonObject *obj;
3802 gchar *url;
3803 gchar *postdata;
3804
3805 obj = json_object_new();
3806 json_object_set_string_member(obj, "channel_id", ma->current_channel_id);
3807 json_object_set_string_member(obj, "prev_channel_id", ma->last_channel_id);
3808 postdata = json_object_to_string(obj);
3809
3810 //FIXME: this could be NULL on first call, but why later ? check!
3811 if (!ma->current_channel_id) {
3812 return FALSE;
3813 }
3814
3815 g_free(ma->last_channel_id);
3816 ma->last_channel_id = g_strdup(ma->current_channel_id);
3817
3818 url = mm_build_url(ma,"/channels/members/me/view");
3819 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_mark_room_messages_read_timeout_response, g_strdup(ma->current_channel_id));
3820
3821 g_free(postdata);
3822 g_free(url);
3823 json_object_unref(obj);
3824
3825
3826
3827 return FALSE;
3828 }
3829
3830 static void
mm_mark_room_messages_read(MattermostAccount * ma,const gchar * room_id)3831 mm_mark_room_messages_read(MattermostAccount *ma, const gchar *room_id)
3832 {
3833 g_free(ma->current_channel_id);
3834 ma->current_channel_id = g_strdup(room_id);
3835
3836 g_source_remove(ma->read_messages_timeout);
3837 ma->read_messages_timeout = g_timeout_add_seconds(1, mm_mark_room_messages_read_timeout, ma);
3838 }
3839
3840 static void
mm_mark_conv_seen(PurpleConversation * conv,PurpleConversationUpdateType type)3841 mm_mark_conv_seen(PurpleConversation *conv, PurpleConversationUpdateType type)
3842 {
3843 PurpleConnection *pc;
3844 MattermostAccount *ma;
3845 const gchar *room_id;
3846
3847 if (type != PURPLE_CONVERSATION_UPDATE_UNSEEN)
3848 return;
3849
3850 pc = purple_conversation_get_connection(conv);
3851 if (!PURPLE_CONNECTION_IS_CONNECTED(pc))
3852 return;
3853
3854 if (g_strcmp0(purple_protocol_get_id(purple_connection_get_protocol(pc)), MATTERMOST_PLUGIN_ID))
3855 return;
3856
3857 ma = purple_connection_get_protocol_data(pc);
3858
3859 room_id = purple_conversation_get_data(conv, "id");
3860
3861 if (PURPLE_IS_IM_CONVERSATION(conv)) {
3862 room_id = g_hash_table_lookup(ma->one_to_ones_rev, purple_conversation_get_name(conv));
3863 // new conversation: selecting IM in chat room people list on a non-buddy
3864 if (room_id == NULL) {
3865 // name of a new IM conv. == buddy username: better way to do it ?
3866 const gchar *username = purple_conversation_get_name(conv);
3867 PurpleBuddy *buddy = purple_blist_find_buddy(ma->account, username);
3868 if (buddy == NULL) {
3869 buddy = purple_buddy_new(ma->account, username, NULL);
3870 purple_blist_add_buddy(buddy, NULL, mm_get_or_create_default_group(), NULL);
3871 mm_add_buddy(pc, buddy, NULL, NULL);
3872 }
3873 return;
3874 }
3875 } else {
3876 //FIXME room_id = g_hash_table_lookup(ma->group_chats_rev, room_id);
3877 //TODO: if (room_id) == NULL - create new group chat
3878 }
3879
3880 g_return_if_fail(room_id != NULL);
3881
3882 mm_mark_room_messages_read(ma, room_id);
3883 }
3884
3885 static guint
mm_conv_send_typing(PurpleConversation * conv,PurpleIMTypingState state,MattermostAccount * ma)3886 mm_conv_send_typing(PurpleConversation *conv, PurpleIMTypingState state, MattermostAccount *ma)
3887 {
3888 PurpleConnection *pc;
3889 const gchar *room_id;
3890 JsonObject *data;
3891 JsonObject *data_inside;
3892
3893 if (state != PURPLE_IM_TYPING) {
3894 return 0;
3895 }
3896
3897 pc = ma ? ma->pc : purple_conversation_get_connection(conv);
3898
3899 if (!PURPLE_CONNECTION_IS_CONNECTED(pc))
3900 return 0;
3901
3902 if (g_strcmp0(purple_protocol_get_id(purple_connection_get_protocol(pc)), MATTERMOST_PLUGIN_ID))
3903 return 0;
3904
3905 if (ma == NULL) {
3906 ma = purple_connection_get_protocol_data(pc);
3907 }
3908
3909 room_id = purple_conversation_get_data(conv, "id");
3910
3911 if (PURPLE_IS_IM_CONVERSATION(conv)) {
3912 room_id = g_hash_table_lookup(ma->one_to_ones_rev, purple_conversation_get_name(conv));
3913 } else {
3914 room_id = g_hash_table_lookup(ma->group_chats_rev, room_id);
3915 }
3916
3917 g_return_val_if_fail(room_id, -1); // this can happen if we try to type in a removed chat for which conv still exists ?
3918
3919 data = json_object_new();
3920 data_inside = json_object_new();
3921
3922 json_object_set_string_member(data_inside, "channel_id", room_id);
3923 json_object_set_string_member(data_inside, "parent_id", ""); //TODO what is this? (a reply to a post ?)
3924
3925 json_object_set_string_member(data, "action", "user_typing");
3926 json_object_set_object_member(data, "data", data_inside);
3927 json_object_set_int_member(data, "seq", mm_get_next_seq(ma));
3928
3929 mm_socket_write_json(ma, data);
3930
3931 return 10;
3932 }
3933
3934 static guint
mm_send_typing(PurpleConnection * pc,const gchar * who,PurpleIMTypingState state)3935 mm_send_typing(PurpleConnection *pc, const gchar *who, PurpleIMTypingState state)
3936 {
3937 PurpleConversation *conv;
3938
3939 conv = PURPLE_CONVERSATION(purple_conversations_find_im_with_account(who, purple_connection_get_account(pc)));
3940 g_return_val_if_fail(conv, -1);
3941
3942 return mm_conv_send_typing(conv, state, NULL);
3943 }
3944
3945
3946 static gint
3947 mm_conversation_send_message(MattermostAccount *ma, const gchar *team_id, const gchar *channel_id, const gchar *message, GList *file_ids);
3948
3949 static void
mm_coversation_send_image_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)3950 mm_coversation_send_image_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
3951 {
3952 const gchar *channel_id = user_data;
3953
3954 if (!mm_check_mattermost_response(ma,node,_("Error"),_("Error uploading image file"),TRUE)) return;
3955
3956 JsonObject *response = json_node_get_object(node);
3957 JsonArray *file_infos = json_object_get_array_member(response,"file_infos");
3958 guint i, len = json_array_get_length(file_infos);
3959 for (i=0; i < len ; i++) {
3960 JsonObject *file_info = json_node_get_object(json_array_get_element(file_infos, i));
3961 const gchar *file_id = json_object_get_string_member(file_info,"id");
3962
3963 GList *file_ids = NULL;
3964 file_ids = g_list_append(file_ids,g_strdup(file_id));
3965 mm_conversation_send_message(ma, NULL, channel_id, "", file_ids);
3966 }
3967 }
3968 static void
mm_conversation_send_image(MattermostAccount * ma,const gchar * channel_id,PurpleImage * image)3969 mm_conversation_send_image(MattermostAccount *ma,const gchar *channel_id, PurpleImage *image)
3970 {
3971 gchar *url, *postdata;
3972 const gchar *filename = purple_image_get_path(image);
3973
3974 postdata = g_memdup(purple_image_get_data(image),purple_image_get_size(image));
3975
3976 url = mm_build_url(ma,"/files?channel_id=%s&filename=%s",channel_id,filename);
3977 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, purple_image_get_size(image), mm_coversation_send_image_response, g_strdup(channel_id));
3978
3979 g_free(url);
3980 g_free(postdata);
3981 }
3982
3983 //FIXME: merge two funcs below.
3984
3985 static int
mm_conversation_find_imageid(const gchar * message)3986 mm_conversation_find_imageid(const gchar *message)
3987 {
3988 const gchar *img;
3989
3990 if ((img = strstr(message, "<img ")) || (img = strstr(message, "<IMG "))) {
3991 const gchar *id;
3992 const gchar *close = strchr(img, '>');
3993
3994 if (((id = strstr(img, "ID=\"")) || (id = strstr(img, "id=\""))) && id < close) {
3995 int imgid = atoi(id + 4);
3996 return imgid;
3997 }
3998 }
3999 return 0;
4000 }
4001
4002 static void
mm_conversation_send_files(MattermostAccount * ma,const gchar * team_id,const gchar * channel_id,const gchar * message)4003 mm_conversation_send_files(MattermostAccount *ma, const gchar *team_id, const gchar *channel_id, const gchar *message)
4004 {
4005 const gchar *msgpt = message;
4006
4007 msgpt = g_strstr_len(message, strlen(message), "<img");
4008 if (!msgpt) msgpt = g_strstr_len(message, strlen(message), "<IMG");
4009 while (msgpt && strlen(msgpt)) {
4010
4011 int imgid = mm_conversation_find_imageid(msgpt);
4012
4013 PurpleImage *image = purple_image_store_get(imgid);
4014 if (image) {mm_conversation_send_image(ma, channel_id, image);}
4015
4016 msgpt = g_strstr_len(msgpt, strlen(msgpt), ">");
4017 if (msgpt != NULL) { msgpt = msgpt + 1; }
4018 }
4019 }
4020
4021 static void
mm_conversation_send_message_response(MattermostAccount * ma,JsonNode * node,gpointer user_data)4022 mm_conversation_send_message_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
4023 {
4024 JsonObject *obj = json_node_get_object(node);
4025 if (json_object_get_int_member(obj, "status_code") >= 400) {
4026 purple_notify_error(ma->pc, _("Error"), _("Error sending Message"), json_object_get_string_member(obj, "message"), purple_request_cpar_from_connection(ma->pc));
4027 }
4028 }
4029
4030 static gint
mm_conversation_send_message(MattermostAccount * ma,const gchar * team_id,const gchar * channel_id,const gchar * message,GList * file_ids)4031 mm_conversation_send_message(MattermostAccount *ma, const gchar *team_id, const gchar *channel_id, const gchar *message, GList *file_ids)
4032 {
4033 JsonObject *data = json_object_new();
4034 gchar *stripped;
4035 gchar *_id;
4036 gchar *postdata;
4037 gchar *url;
4038 JsonArray *tmparr;
4039 GList *file_id;
4040
4041 _id = g_strdup_printf("%012XFFFF", g_random_int());
4042 json_object_set_string_member(data, "pending_post_id", _id);
4043 g_hash_table_insert(ma->sent_message_ids, _id, _id);
4044
4045 json_object_set_string_member(data, "channel_id", channel_id);
4046
4047 stripped = mm_html_to_markdown(message);
4048 json_object_set_string_member(data, "message", stripped);
4049 g_free(stripped);
4050
4051 json_object_set_string_member(data, "user_id", ma->self->user_id);
4052 json_object_set_int_member(data, "create_at", 0);
4053
4054 tmparr = json_array_new();
4055
4056 if (file_ids) {
4057 for (file_id = file_ids; file_id != NULL; file_id = file_id->next) {
4058 json_array_add_string_element(tmparr,file_id->data);
4059 }
4060 json_object_set_array_member(data, "file_ids", tmparr);
4061 }
4062
4063 postdata = json_object_to_string(data);
4064
4065 //url = mm_build_url(ma,"/teams/%s/channels/%s/posts/create", team_id, channel_id);
4066 url = mm_build_url(ma,"/posts");
4067 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_conversation_send_message_response, NULL); //todo look at callback
4068
4069 if (!file_ids) mm_conversation_send_files(ma, team_id, channel_id, message);
4070
4071 json_array_unref(tmparr);
4072 g_free(postdata);
4073 g_free(url);
4074
4075 return 1;
4076 }
4077
4078 static gint
mm_chat_send(PurpleConnection * pc,gint id,PurpleMessage * msg)4079 mm_chat_send(PurpleConnection *pc, gint id,
4080 #if PURPLE_VERSION_CHECK(3, 0, 0)
4081 PurpleMessage *msg)
4082 {
4083 const gchar *message = purple_message_get_contents(msg);
4084 #else
4085 const gchar *message, PurpleMessageFlags flags)
4086 {
4087 #endif
4088 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
4089 PurpleChatConversation *chatconv = purple_conversations_find_chat(pc, id);
4090 const gchar *room_id = purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
4091 const gchar *team_id = purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "team_id");
4092 gint ret = 0;
4093
4094 // this should not happen.
4095 g_return_val_if_fail(room_id, -1);
4096 //g_return_val_if_fail(team_id, -1); // it IS NULL for group channel.
4097
4098 ret = mm_conversation_send_message(ma, team_id, room_id, mm_purple_xhtml_im_to_html_parse(ma, message), NULL);
4099
4100 if (ret > 0) {
4101 gchar *message_out = mm_markdown_to_html(ma, message);
4102 purple_serv_got_chat_in(pc, g_str_hash(room_id), ma->self->username, PURPLE_MESSAGE_SEND, message_out, time(NULL));
4103 g_free(message_out);
4104 }
4105 return ret;
4106 }
4107
4108
4109 static void
4110 mm_created_direct_message_send(MattermostAccount *ma, JsonNode *node, gpointer user_data)
4111 {
4112 PurpleMessage *msg = user_data;
4113 JsonObject *result;
4114 const gchar *who = purple_message_get_recipient(msg);
4115 const gchar *message;
4116 const gchar *room_id;
4117 PurpleBuddy *buddy;
4118
4119 if (node == NULL) {
4120 purple_conversation_present_error(who, ma->account, _("Could not create conversation"));
4121 purple_message_destroy(msg);
4122 return;
4123 }
4124 result = json_node_get_object(node);
4125
4126 if (json_object_get_int_member(result, "status_code") >= 400) {
4127 purple_notify_error(ma->pc, _("Error"), _("Error creating Mattermost Channel"), json_object_get_string_member(result, "message"), purple_request_cpar_from_connection(ma->pc));
4128 return;
4129 }
4130
4131 message = purple_message_get_contents(msg);
4132 room_id = json_object_get_string_member(result, "id");
4133 buddy = purple_blist_find_buddy(ma->account, who);
4134
4135 if (room_id != NULL && who != NULL) {
4136 g_hash_table_replace(ma->one_to_ones, g_strdup(room_id), g_strdup(who));
4137 g_hash_table_replace(ma->one_to_ones_rev, g_strdup(who), g_strdup(room_id));
4138 }
4139
4140 if (buddy != NULL) {
4141 purple_blist_node_set_string(PURPLE_BLIST_NODE(buddy), "room_id", room_id);
4142 }
4143 //API: user is MM global, still need a team_id to contact, why ? ..
4144 mm_conversation_send_message(ma, mm_get_first_team_id(ma), room_id, message, NULL);
4145 }
4146
4147 static int
4148 mm_send_im(PurpleConnection *pc,
4149 #if PURPLE_VERSION_CHECK(3, 0, 0)
4150 PurpleMessage *msg)
4151 {
4152 const gchar *who = purple_message_get_recipient(msg);
4153 const gchar *message = purple_message_get_contents(msg);
4154 #else
4155 const gchar *who, const gchar *message, PurpleMessageFlags flags)
4156 {
4157 #endif
4158
4159 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
4160 gchar *room_id = g_hash_table_lookup(ma->one_to_ones_rev, who);
4161
4162 //API: user is MM global, still need team_id to contact, why ? ..
4163 const gchar *team_id = mm_get_first_team_id(ma);
4164
4165 if (room_id == NULL) {
4166
4167 if (purple_str_has_suffix(who, MATTERMOST_BOT_LABEL)) {
4168 purple_notify_error(ma->pc, _("Error"), _("You cannot send instant message to a BOT"), _("(However you may be able to interact with it using \"/cmd command\" in a chat)"), purple_request_cpar_from_connection(ma->pc));
4169 //TODO: 'disable' im conv window ?
4170 return -1;
4171 }
4172
4173 JsonArray *data;
4174 gchar *url, *postdata;
4175 const gchar *user_id = g_hash_table_lookup(ma->usernames_to_ids, who);
4176 #if !PURPLE_VERSION_CHECK(3, 0, 0)
4177 PurpleMessage *msg = purple_message_new_outgoing(who, message, flags);
4178 #endif
4179
4180 data = json_array_new();
4181 json_array_add_string_element(data, user_id);
4182 json_array_add_string_element(data, ma->self->user_id);
4183
4184 postdata = json_array_to_string(data);
4185 url = mm_build_url(ma,"/channels/direct");
4186 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_created_direct_message_send, msg);
4187 g_free(url);
4188
4189 g_free(postdata);
4190 json_array_unref(data);
4191
4192 MattermostUserPref *pref = g_new0(MattermostUserPref, 1);
4193 pref->user_id = g_strdup(ma->self->user_id);
4194 pref->category = g_strdup("direct_channel_show");
4195 pref->name = g_strdup(user_id);
4196 pref->value = g_strdup("true");
4197
4198 mm_save_user_pref(ma, pref);
4199 // free pref in callback
4200 return 1;
4201 }
4202 return mm_conversation_send_message(ma, team_id, room_id, message, NULL);
4203 }
4204
4205
4206 static void
4207 mm_chat_set_header_purpose(PurpleConnection *pc, int id, const char *topic, const gboolean isheader)
4208 {
4209 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
4210 PurpleChatConversation *chatconv;
4211 JsonObject *data;
4212 gchar *postdata;
4213 gchar *url;
4214 const gchar *channel_id;
4215
4216 chatconv = purple_conversations_find_chat(pc, id);
4217 if (chatconv == NULL) return;
4218
4219 channel_id = purple_conversation_get_data(PURPLE_CONVERSATION(chatconv), "id");
4220
4221 data = json_object_new();
4222 json_object_set_string_member(data, "id", channel_id);
4223
4224 if (isheader) {
4225 json_object_set_string_member(data, "header", topic);
4226 } else {
4227 json_object_set_string_member(data, "purpose", topic);
4228 }
4229
4230 url = mm_build_url(ma,"/channels/%s", channel_id);
4231
4232 postdata = json_object_to_string(data);
4233
4234 mm_fetch_url(ma, url, MATTERMOST_HTTP_PUT, postdata, -1, NULL, NULL);
4235
4236 g_free(postdata);
4237 g_free(url);
4238 }
4239
4240
4241 static void
4242 mm_chat_set_topic(PurpleConnection *pc, int id, const char *topic)
4243 {
4244 mm_chat_set_header_purpose(pc, id, topic, TRUE);
4245 }
4246
4247
4248 void
4249 mm_search_results_send_im(PurpleConnection *pc, GList *row, void *user_data)
4250 {
4251 PurpleAccount *account = purple_connection_get_account(pc);
4252 const gchar *who = g_list_nth_data(row, 0);
4253 PurpleIMConversation *imconv;
4254
4255 imconv = purple_conversations_find_im_with_account(who, account);
4256 if (imconv == NULL) {
4257 imconv = purple_im_conversation_new(account, who);
4258 }
4259 purple_conversation_present(PURPLE_CONVERSATION(imconv));
4260 }
4261
4262 void
4263 mm_search_results_add_buddy(PurpleConnection *pc, GList *row, void *user_data)
4264 {
4265 PurpleAccount *account = purple_connection_get_account(pc);
4266 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
4267 gboolean usealias = FALSE;
4268
4269 MattermostUser *mm_user=g_new0(MattermostUser,1);
4270 mm_user->username = g_strdup(g_list_nth_data(row, 0));
4271 mm_user->first_name = g_strdup(g_list_nth_data(row, 1));
4272 mm_user->last_name = g_strdup(g_list_nth_data(row, 2));
4273 mm_user->nickname = g_strdup(g_list_nth_data(row, 3));
4274 mm_user->email = g_strdup(g_list_nth_data(row, 4));
4275 mm_user->alias = g_strdup(mm_get_alias(mm_user));
4276
4277 if (purple_account_get_bool(ma->account, "use-alias", FALSE)) {
4278 usealias = TRUE;
4279 }
4280
4281 if (!purple_blist_find_buddy(account, mm_user->username)) {
4282 purple_blist_request_add_buddy(account, mm_user->username, MATTERMOST_DEFAULT_BLIST_GROUP_NAME, usealias ? mm_user->alias : NULL); //NO room_id
4283 //TODO: get info here to fill in all buddy data ?
4284 }
4285 mm_g_free_mattermost_user(mm_user);
4286 }
4287
4288 static void
4289 mm_got_add_buddy_search(MattermostAccount *ma, JsonNode *node, gpointer user_data)
4290 {
4291 gchar *search_term = user_data;
4292 GList *users, *i;
4293 PurpleNotifySearchResults *results;
4294 PurpleNotifySearchColumn *column;
4295
4296 // api docs says this should be an object response, but the api returns an array (v3?)
4297 if (json_node_get_node_type(node) == JSON_NODE_OBJECT) {
4298 JsonObject *obj = json_node_get_object(node);
4299 if (json_object_has_member(obj, "status_code")) {
4300 purple_notify_error(ma->pc, _("Search Error"), _("There was an error searching for the user"), json_object_get_string_member(obj, "message"), purple_request_cpar_from_connection(ma->pc));
4301 return;
4302 }
4303
4304 users = json_object_get_values(obj);
4305 } else {
4306 JsonArray *arr = json_node_get_array(node);
4307
4308 users = json_array_get_elements(arr);
4309 }
4310
4311 if (users == NULL) {
4312 gchar *primary_text = g_strdup_printf(_("Your search for the user \"%s\" returned no results"), search_term);
4313 purple_notify_warning(ma->pc, _("No users found"), primary_text, "", purple_request_cpar_from_connection(ma->pc));
4314 g_free(primary_text);
4315
4316 g_free(search_term);
4317 return;
4318 }
4319
4320 results = purple_notify_searchresults_new();
4321 if (results == NULL)
4322 {
4323 g_list_free(users);
4324 // This UI can't show search results
4325 return;
4326 }
4327
4328 /* columns: username, First Name, Last Name, Nickname, Email */
4329 column = purple_notify_searchresults_column_new(_("Username"));
4330 purple_notify_searchresults_column_add(results, column);
4331 column = purple_notify_searchresults_column_new(_("First Name"));
4332 purple_notify_searchresults_column_add(results, column);
4333 column = purple_notify_searchresults_column_new(_("Last Name"));
4334 purple_notify_searchresults_column_add(results, column);
4335 column = purple_notify_searchresults_column_new(_("Nickname"));
4336 purple_notify_searchresults_column_add(results, column);
4337 column = purple_notify_searchresults_column_new(_("Email"));
4338 purple_notify_searchresults_column_add(results, column);
4339
4340 purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_ADD, mm_search_results_add_buddy);
4341 //purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_INFO, mm_search_results_get_info);
4342 purple_notify_searchresults_button_add(results, PURPLE_NOTIFY_BUTTON_IM, mm_search_results_send_im);
4343
4344 for (i = users; i; i = i->next) {
4345 JsonNode *usernode = i->data;
4346 JsonObject *user = json_node_get_object(usernode);
4347 const gchar *username = json_object_get_string_member(user, "username");
4348
4349 GList *row = NULL;
4350
4351 row = g_list_append(row, g_strdup(username));
4352 row = g_list_append(row, g_strdup(json_object_get_string_member(user, "first_name")));
4353 row = g_list_append(row, g_strdup(json_object_get_string_member(user, "last_name")));
4354 row = g_list_append(row, g_strdup(json_object_get_string_member(user, "nickname")));
4355 row = g_list_append(row, g_strdup(json_object_get_string_member(user, "email")));
4356
4357 purple_notify_searchresults_row_add(results, row);
4358
4359 if (!mm_hash_table_contains(ma->usernames_to_ids, username)) {
4360 const gchar *id = json_object_get_string_member(user, "id");
4361 g_hash_table_replace(ma->ids_to_usernames, g_strdup(id), g_strdup(username));
4362 g_hash_table_replace(ma->usernames_to_ids, g_strdup(username), g_strdup(id));
4363 }
4364 }
4365
4366 purple_notify_searchresults(ma->pc, NULL, search_term, NULL, results, NULL, NULL);
4367
4368 g_list_free(users);
4369 g_free(search_term);
4370 }
4371
4372
4373 void
4374 mm_search_users_text(MattermostAccount *ma, const gchar *text)
4375 {
4376 JsonObject *obj = json_object_new();
4377 gchar *url;
4378 gchar *postdata;
4379
4380 json_object_set_string_member(obj, "term", text);
4381 json_object_set_boolean_member(obj, "allow_inactive", TRUE);
4382
4383 postdata = json_object_to_string(obj);
4384
4385 url = mm_build_url(ma,"/users/search");
4386 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_got_add_buddy_search, g_strdup(text));
4387 g_free(url);
4388
4389 g_free(postdata);
4390 json_object_unref(obj);
4391 }
4392
4393 void
4394 mm_search_users(PurpleProtocolAction *action)
4395 {
4396 PurpleConnection *pc = purple_protocol_action_get_connection(action);
4397 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
4398
4399 purple_request_input(pc, _("Search for users..."),
4400 _("Search for users..."),
4401 NULL,
4402 NULL, FALSE, FALSE, NULL,
4403 _("_Search"), G_CALLBACK(mm_search_users_text),
4404 _("_Cancel"), NULL,
4405 purple_request_cpar_from_connection(pc),
4406 ma);
4407
4408 }
4409
4410
4411 void
4412 mm_roomlist_show(PurpleProtocolAction *action)
4413 {
4414 PurpleConnection *pc = purple_protocol_action_get_connection(action);
4415 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
4416 purple_roomlist_show_with_account(ma->account);
4417 }
4418
4419
4420 static MattermostUser *
4421 mm_user_from_json(MattermostAccount *ma, JsonObject *user)
4422 {
4423 MattermostUser *mu = g_new0(MattermostUser, 1);
4424
4425 mu->user_id = g_strdup(json_object_get_string_member(user, "id"));
4426 mu->username = g_strdup(json_object_get_string_member(user, "username"));
4427 mu->first_name = g_strdup(json_object_get_string_member(user, "first_name"));
4428 mu->last_name = g_strdup(json_object_get_string_member(user, "last_name"));
4429 mu->nickname = g_strdup(json_object_get_string_member(user, "nickname"));
4430 mu->email = g_strdup(json_object_get_string_member(user, "email"));
4431 mu->position = g_strdup(json_object_get_string_member(user, "position"));
4432 mu->locale = g_strdup(json_object_get_string_member(user, "locale"));
4433 mu->alias = g_strdup(mm_get_alias(mu));
4434 mu->roles = mm_role_to_purple_flag(ma, json_object_get_string_member(user, "roles"));
4435
4436 return mu;
4437 }
4438
4439 //TODO: integrate with mm_get_users_by_ids() ?
4440 static void
4441 mm_got_add_buddy_user(MattermostAccount *ma, JsonNode *node, gpointer user_data)
4442 {
4443 JsonObject *user = json_node_get_object(node);
4444 PurpleBuddy *buddy = user_data;
4445
4446 if (json_object_has_member(user, "status_code")) {
4447 // There was an error in the response, which generally means the buddy is invalid somehow
4448 const gchar *buddy_name = purple_buddy_get_name(buddy);
4449 PurpleIMConversation *imconv = purple_conversations_find_im_with_account(buddy_name, ma->account);
4450
4451 if (imconv != NULL) {
4452 PurpleConversation *conv = PURPLE_CONVERSATION(imconv);
4453 purple_conversation_write_system_message(conv, _("Cannot sent message, invalid buddy"), PURPLE_MESSAGE_ERROR);
4454 } else {
4455 purple_notify_error(ma->pc, _("Add Buddy Error"), _("There was an error searching for the user"), json_object_get_string_member(user, "message"), purple_request_cpar_from_connection(ma->pc));
4456 }
4457
4458 // bad user, delete
4459 purple_blist_remove_buddy(buddy);
4460 return;
4461 }
4462
4463 MattermostUser *mm_user = mm_user_from_json(ma, user);
4464
4465 g_hash_table_replace(ma->ids_to_usernames, g_strdup(mm_user->user_id), g_strdup(mm_user->username));
4466 g_hash_table_replace(ma->usernames_to_ids, g_strdup(mm_user->username), g_strdup(mm_user->user_id));
4467
4468 mm_add_buddy(ma->pc, buddy, NULL, NULL);
4469
4470 if (purple_account_get_bool(ma->account,"use-alias", FALSE)) {
4471 purple_buddy_set_local_alias(buddy, mm_user->alias);
4472 }
4473
4474 mm_g_free_mattermost_user(mm_user);
4475 }
4476
4477
4478 static void
4479 mm_got_avatar(MattermostAccount *ma, JsonNode *node, gpointer user_data)
4480 {
4481 if (node != NULL) {
4482 JsonObject *response = json_node_get_object(node);
4483 const gchar *buddy_name = user_data;
4484 const gchar *response_str;
4485 gsize response_len;
4486 gpointer response_dup;
4487
4488 response_str = g_dataset_get_data(node, "raw_body");
4489 response_len = json_object_get_int_member(response, "len");
4490 response_dup = g_memdup(response_str, response_len);
4491
4492 if(purple_blist_find_buddy(ma->account, buddy_name)) {
4493 purple_buddy_icons_set_for_user(ma->account, buddy_name, response_dup, response_len, NULL);
4494 }
4495 }
4496 }
4497
4498
4499 static void
4500 mm_get_avatar(MattermostAccount *ma, PurpleBuddy *buddy)
4501 {
4502 gchar *url = mm_build_url(ma,"/users/%s/image", purple_blist_node_get_string(PURPLE_BLIST_NODE(buddy), "user_id"));
4503 const gchar *buddy_name = g_strdup(purple_buddy_get_name(buddy));
4504 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_got_avatar, (gpointer) buddy_name);
4505 g_free(url);
4506 }
4507
4508
4509 static void
4510 mm_fake_group_buddy(PurpleConnection *pc, const char *who, const char *old_group, const char *new_group)
4511 {
4512 // Do nothing to stop the remove+add behaviour
4513 }
4514
4515
4516 static void
4517 mm_fake_group_rename(PurpleConnection *pc, const char *old_name, PurpleGroup *group, GList *moved_buddies)
4518 {
4519 // Do nothing to stop the remove+add behaviour
4520 }
4521
4522
4523 static void
4524 mm_remove_buddy(PurpleConnection *pc, PurpleBuddy *buddy, PurpleGroup *group)
4525 {
4526 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
4527 MattermostUserPref *pref = g_new0(MattermostUserPref,1);
4528 pref->user_id = g_strdup(ma->self->user_id);
4529 pref->category = g_strdup("direct_channel_show");
4530 pref->name = g_strdup(purple_blist_node_get_string(PURPLE_BLIST_NODE(buddy), "user_id"));
4531 pref->value = g_strdup("false");
4532 mm_save_user_pref(ma, pref);
4533 // free pref in callback
4534 }
4535
4536 static void
4537 mm_create_direct_channel_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
4538 {
4539 const gchar *user_id = user_data;
4540 JsonObject *response = json_node_get_object(node);
4541 const gchar *room_id;
4542
4543 if (json_object_get_int_member(response, "status_code") >= 400) {
4544 purple_notify_error(ma->pc, _("Error"), _("Error creating Mattermost Channel"), json_object_get_string_member(response, "message"), purple_request_cpar_from_connection(ma->pc));
4545 return;
4546 }
4547
4548 room_id = json_object_get_string_member(response, "id");
4549
4550 if (room_id == NULL) {
4551 return;
4552 }
4553
4554 PurpleBlistNode *bnode;
4555 gboolean found = FALSE;
4556 for (bnode = purple_blist_get_root(); bnode != NULL && !found; bnode = purple_blist_node_next(bnode, TRUE)) {
4557 if (!PURPLE_IS_BUDDY(bnode)) { continue; }
4558 if (purple_strequal(purple_blist_node_get_string(bnode, "user_id"), user_id)) {
4559 purple_blist_node_set_string(bnode, "room_id", room_id);
4560 found = TRUE;
4561 }
4562 }
4563 }
4564
4565 static void
4566 mm_create_direct_channel(MattermostAccount *ma, PurpleBuddy *buddy)
4567 {
4568 gchar *url, *postdata;
4569 const gchar *user_id;
4570 JsonArray *data;
4571
4572 if (purple_blist_node_get_string(PURPLE_BLIST_NODE(buddy), "room_id")) {
4573 return;
4574 }
4575
4576 data = json_array_new();
4577
4578 user_id = purple_blist_node_get_string(PURPLE_BLIST_NODE(buddy), "user_id");
4579 json_array_add_string_element(data, user_id);
4580 json_array_add_string_element(data, ma->self->user_id);
4581
4582 postdata = json_array_to_string(data);
4583 url = mm_build_url(ma,"/channels/direct");
4584
4585 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_create_direct_channel_response, g_strdup(user_id));
4586
4587 g_free(url);
4588 json_array_unref(data);
4589 }
4590
4591
4592 void
4593 mm_add_buddy(PurpleConnection *pc, PurpleBuddy *buddy, PurpleGroup *group, const char *message)
4594 {
4595 MattermostAccount *ma = purple_connection_get_protocol_data(pc);
4596 const gchar *buddy_name = purple_buddy_get_name(buddy);
4597 const gchar *user_id = g_hash_table_lookup(ma->usernames_to_ids, buddy_name);
4598
4599 if (purple_strequal(user_id,ma->self->user_id)) {
4600 purple_blist_remove_buddy(buddy);
4601 return;
4602 }
4603
4604 if (purple_str_has_suffix(buddy_name, MATTERMOST_BOT_LABEL)) {
4605 purple_blist_remove_buddy(buddy);
4606 return;
4607 }
4608
4609 if (user_id == NULL) {
4610 gchar *url;
4611
4612 //Search for user
4613 // if they've entered what we think is a username, sanitise it
4614 if (!strchr(buddy_name, ' ') && !strchr(buddy_name, '@')) {
4615 url = mm_build_url(ma,"/users/username/%s", buddy_name);
4616 mm_fetch_url(ma, url, MATTERMOST_HTTP_GET, NULL, -1, mm_got_add_buddy_user, buddy);
4617 g_free(url);
4618 } else {
4619 // Doesn't look like a username, do a search
4620 mm_search_users_text(ma, buddy_name);
4621 purple_blist_remove_buddy(buddy);
4622 }
4623
4624 return;
4625 }
4626
4627 purple_blist_node_set_string(PURPLE_BLIST_NODE(buddy), "user_id", user_id);
4628
4629 mm_get_avatar(ma,buddy);
4630
4631 mm_create_direct_channel(ma, buddy);
4632
4633 MattermostUserPref *pref = g_new0(MattermostUserPref,1);
4634 pref->user_id = g_strdup(ma->self->user_id);
4635 pref->category = g_strdup("direct_channel_show");
4636 pref->name = g_strdup(user_id);
4637 pref->value = g_strdup("true");
4638 mm_save_user_pref(ma,pref);
4639 // free pref in callback
4640
4641 mm_refresh_statuses(ma, user_id);
4642 }
4643
4644 static const char *
4645 mm_list_icon(PurpleAccount *account, PurpleBuddy *buddy)
4646 {
4647 return "mattermost";
4648 }
4649
4650 static GList *
4651 mm_status_types(PurpleAccount *account)
4652 {
4653 GList *types = NULL;
4654 PurpleStatusType *status;
4655
4656 status = purple_status_type_new_full(PURPLE_STATUS_AVAILABLE, "online", "Online", TRUE, TRUE, FALSE);
4657 types = g_list_append(types, status);
4658
4659 status = purple_status_type_new_full(PURPLE_STATUS_AWAY, "away", "Away", TRUE, TRUE, FALSE);
4660 types = g_list_append(types, status);
4661
4662 status = purple_status_type_new_full(PURPLE_STATUS_OFFLINE, "offline", "Offline", TRUE, TRUE, FALSE);
4663 types = g_list_append(types, status);
4664
4665 status = purple_status_type_new_full(PURPLE_STATUS_UNAVAILABLE, "dnd", "Busy", TRUE, TRUE, FALSE);
4666 types = g_list_append(types, status);
4667
4668 status = purple_status_type_new_full(PURPLE_STATUS_INVISIBLE, "invisible", "Invisible", TRUE, TRUE, FALSE);
4669 types = g_list_append(types, status);
4670
4671 return types;
4672 }
4673
4674 static GHashTable *
4675 mm_get_account_text_table(PurpleAccount *unused)
4676 {
4677 GHashTable *table;
4678
4679 table = g_hash_table_new(g_str_hash, g_str_equal);
4680
4681 g_hash_table_insert(table, "login_label", (gpointer)_("Email or AD/LDAP Username..."));
4682
4683 return table;
4684 }
4685
4686 static GList *
4687 mm_add_account_options(GList *account_options)
4688 {
4689 PurpleAccountOption *option;
4690
4691 option = purple_account_option_bool_new(N_("Use SSL/HTTPS"), "use-ssl", TRUE);
4692 account_options = g_list_append(account_options, option);
4693
4694 option = purple_account_option_bool_new(N_("Password is Gitlab cookie"), "use-mmauthtoken", FALSE);
4695 account_options = g_list_append(account_options, option);
4696
4697 option = purple_account_option_bool_new(N_("Interpret (subset of) markdown"), "use-markdown", TRUE);
4698 account_options = g_list_append(account_options, option);
4699
4700 option = purple_account_option_bool_new(N_("Auto generate buddies aliases"), "use-alias", FALSE);
4701 account_options = g_list_append(account_options, option);
4702
4703 option = purple_account_option_bool_new(N_("Show images in messages"), "show-images", TRUE);
4704 account_options = g_list_append(account_options, option);
4705
4706 //FIXME: this one shall depend on above one !
4707 option = purple_account_option_bool_new(N_("Show full images in messages"), "show-full-images", FALSE);
4708 account_options = g_list_append(account_options, option);
4709
4710 return account_options;
4711 }
4712
4713 static PurpleCmdRet
4714 mm_cmd_leave(PurpleConversation *conv, const gchar *cmd, gchar **args, gchar **error, gpointer data)
4715 {
4716 PurpleConnection *pc = NULL;
4717 int id = -1;
4718
4719 pc = purple_conversation_get_connection(conv);
4720 id = purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv));
4721
4722 if (pc == NULL || id == -1)
4723 return PURPLE_CMD_RET_FAILED;
4724
4725 mm_chat_leave(pc, id);
4726
4727 return PURPLE_CMD_RET_OK;
4728 }
4729
4730 static void
4731 mm_slash_command_response(MattermostAccount *ma, JsonNode *node, gpointer user_data)
4732 {
4733 gchar *cmd = user_data;
4734 mm_check_mattermost_response(ma,node,_("Error"),g_strconcat(_("Error executing Mattermost Slash Command (/"),cmd,")",NULL),TRUE);
4735 }
4736
4737
4738 PurpleCmdRet
4739 mm_slash_command(PurpleConversation *conv, const gchar *cmd, gchar **args, gchar **error, gpointer userdata)
4740 {
4741 PurpleConnection *pc = NULL;
4742 MattermostAccount *ma = NULL;
4743 const gchar *channel_id = NULL;
4744 JsonObject *data;
4745 gchar *postdata;
4746 gchar *url;
4747 gchar *params_str, *command;
4748
4749 pc = purple_conversation_get_connection(conv);
4750 if (pc == NULL) {
4751 return PURPLE_CMD_RET_FAILED;
4752 }
4753 ma = purple_connection_get_protocol_data(pc);
4754 if (ma == NULL) {
4755 return PURPLE_CMD_RET_FAILED;
4756 }
4757
4758 channel_id = purple_conversation_get_data(conv, "id");
4759 if (channel_id == NULL) {
4760 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4761 channel_id = g_hash_table_lookup(ma->one_to_ones_rev, purple_conversation_get_name(conv));
4762 } else {
4763 channel_id = purple_conversation_get_name(conv);
4764 if (g_hash_table_lookup(ma->group_chats_rev, channel_id)) {
4765 // Convert friendly name into id
4766 channel_id = g_hash_table_lookup(ma->group_chats_rev, channel_id);
4767 }
4768 }
4769 }
4770 if (channel_id == NULL) {
4771 return PURPLE_CMD_RET_FAILED;
4772 }
4773
4774 if (PURPLE_IS_IM_CONVERSATION(conv)) {
4775 purple_notify_error(pc, _("Error"), _("Not implemented."),
4776 _("Slash commands not implemented (yet) for private channels."), purple_request_cpar_from_connection(pc));
4777 return PURPLE_CMD_RET_FAILED;
4778 }
4779
4780 if (!ma->client_config->enable_commands) {
4781 purple_notify_error(pc, _("Error"), _("Custom Slash Commands are disabled on Mattermost server"),
4782 _("(See: https://docs.mattermost.com/administration/config-settings.html#integrations)"), purple_request_cpar_from_connection(pc));
4783 return PURPLE_CMD_RET_FAILED;
4784 }
4785
4786 params_str = g_strjoinv(" ", args);
4787 command = g_strconcat("/", cmd, " ", params_str ? params_str : "", NULL);
4788
4789 g_free(params_str);
4790
4791 data = json_object_new();
4792 json_object_set_string_member(data, "command", command);
4793 json_object_set_string_member(data, "channel_id", channel_id);
4794 postdata = json_object_to_string(data);
4795
4796 url = mm_build_url(ma,"/commands/execute");
4797 mm_fetch_url(ma, url, MATTERMOST_HTTP_POST, postdata, -1, mm_slash_command_response, g_strdup(cmd));
4798 g_free(url);
4799
4800 g_free(postdata);
4801 json_object_unref(data);
4802
4803 return PURPLE_CMD_RET_OK;
4804 }
4805
4806 static GList *
4807 mm_actions(
4808 #if !PURPLE_VERSION_CHECK(3, 0, 0)
4809 PurplePlugin *plugin, gpointer context
4810 #else
4811 PurpleConnection *pc
4812 #endif
4813 )
4814 {
4815 GList *m = NULL;
4816 PurpleProtocolAction *act;
4817
4818 act = purple_protocol_action_new(_("Search for Users..."), mm_search_users);
4819 m = g_list_append(m, act);
4820
4821 act = purple_protocol_action_new(_("Room List"), mm_roomlist_show);
4822 m = g_list_append(m, act);
4823
4824 act = purple_protocol_action_new(_("About Myself"), mm_about_myself);
4825 m = g_list_append(m, act);
4826
4827 act = purple_protocol_action_new(_("Server Info"), mm_about_server);
4828 m = g_list_append(m, act);
4829
4830 act = purple_protocol_action_new(_("Slash Commands"), mm_about_commands);
4831 m = g_list_append(m, act);
4832
4833 return m;
4834 }
4835
4836 static gboolean
4837 plugin_load(PurplePlugin *plugin, GError **error)
4838 {
4839
4840 mm_purple_xhtml_im_html_init();
4841
4842 // we do not want the server to initiate channel leave, we do it ourselves.
4843 purple_cmd_register("leave", "", PURPLE_CMD_P_PLUGIN, PURPLE_CMD_FLAG_CHAT |
4844 PURPLE_CMD_FLAG_PROTOCOL_ONLY | PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS,
4845 MATTERMOST_PLUGIN_ID, mm_cmd_leave,
4846 _("leave: Leave the channel"), NULL);
4847
4848 return TRUE;
4849
4850 }
4851
4852 static gboolean
4853 plugin_unload(PurplePlugin *plugin, GError **error)
4854 {
4855 purple_signals_disconnect_by_handle(plugin);
4856
4857 return TRUE;
4858 }
4859
4860 // Purple2 Plugin Load Functions
4861 #if !PURPLE_VERSION_CHECK(3, 0, 0)
4862
4863 // Normally set in core.c in purple3
4864 void _purple_socket_init(void);
4865 void _purple_socket_uninit(void);
4866
4867 static gboolean
4868 libpurple2_plugin_load(PurplePlugin *plugin)
4869 {
4870 _purple_socket_init();
4871 purple_http_init();
4872
4873 return plugin_load(plugin, NULL);
4874 }
4875
4876 static gboolean
4877 libpurple2_plugin_unload(PurplePlugin *plugin)
4878 {
4879 _purple_socket_uninit();
4880 purple_http_uninit();
4881
4882 return plugin_unload(plugin, NULL);
4883 }
4884
4885 static void
4886 plugin_init(PurplePlugin *plugin)
4887 {
4888 // PurpleAccountOption *option;
4889 // PurplePluginInfo *info = plugin->info;
4890 // PurplePluginProtocolInfo *prpl_info = info->extra_info;
4891 //purple_signal_connect(purple_get_core(), "uri-handler", plugin, PURPLE_CALLBACK(mm_uri_handler), NULL);
4892
4893 PurpleAccountUserSplit *split;
4894 PurplePluginInfo *info;
4895 PurplePluginProtocolInfo *prpl_info = g_new0(PurplePluginProtocolInfo, 1);
4896
4897 split = purple_account_user_split_new(_("Server"), MATTERMOST_DEFAULT_SERVER, MATTERMOST_SERVER_SPLIT_CHAR);
4898 prpl_info->user_splits = g_list_append(prpl_info->user_splits, split);
4899
4900 info = plugin->info;
4901 if (info == NULL) {
4902 plugin->info = info = g_new0(PurplePluginInfo, 1);
4903 }
4904 info->actions = mm_actions;
4905 info->extra_info = prpl_info;
4906 #if PURPLE_MINOR_VERSION >= 5
4907 prpl_info->struct_size = sizeof(PurplePluginProtocolInfo);
4908 #endif
4909 #if PURPLE_MINOR_VERSION >= 8
4910 //prpl_info->add_buddy_with_invite = mm_add_buddy;
4911 #endif
4912
4913 prpl_info->options = OPT_PROTO_CHAT_TOPIC | OPT_PROTO_SLASH_COMMANDS_NATIVE | OPT_PROTO_IM_IMAGE;
4914 prpl_info->protocol_options = mm_add_account_options(prpl_info->protocol_options);
4915 prpl_info->icon_spec.format = "png,gif,jpeg";
4916 prpl_info->icon_spec.min_width = 0;
4917 prpl_info->icon_spec.min_height = 0;
4918 prpl_info->icon_spec.max_width = 96;
4919 prpl_info->icon_spec.max_height = 96;
4920 prpl_info->icon_spec.max_filesize = 0;
4921 prpl_info->icon_spec.scale_rules = PURPLE_ICON_SCALE_DISPLAY;
4922
4923 prpl_info->get_account_text_table = mm_get_account_text_table;
4924 prpl_info->list_icon = mm_list_icon;
4925 prpl_info->set_status = mm_set_status;
4926 prpl_info->set_idle = mm_set_idle;
4927 prpl_info->status_types = mm_status_types;
4928 prpl_info->chat_info = mm_chat_info;
4929 prpl_info->chat_info_defaults = mm_chat_info_defaults;
4930 prpl_info->login = mm_login;
4931 prpl_info->close = mm_close;
4932 prpl_info->send_im = mm_send_im;
4933 prpl_info->send_typing = mm_send_typing;
4934 prpl_info->join_chat = mm_join_chat;
4935 prpl_info->get_chat_name = mm_get_chat_name;
4936 prpl_info->chat_invite = mm_chat_invite;
4937 prpl_info->chat_send = mm_chat_send;
4938 prpl_info->set_chat_topic = mm_chat_set_topic;
4939 prpl_info->add_buddy = mm_add_buddy_no_message;
4940 prpl_info->remove_buddy = mm_remove_buddy;
4941 prpl_info->group_buddy = mm_fake_group_buddy;
4942 prpl_info->rename_group = mm_fake_group_rename;
4943 prpl_info->blist_node_menu = mm_blist_node_menu;
4944 prpl_info->get_info = mm_get_info;
4945 prpl_info->tooltip_text = mm_tooltip_text;
4946
4947 prpl_info->roomlist_get_list = mm_roomlist_get_list;
4948 prpl_info->roomlist_room_serialize = mm_roomlist_serialize;
4949 }
4950
4951
4952 static PurplePluginInfo info = {
4953 PURPLE_PLUGIN_MAGIC,
4954 /* PURPLE_MAJOR_VERSION,
4955 PURPLE_MINOR_VERSION,
4956 */
4957 2, 1,
4958 PURPLE_PLUGIN_PROTOCOL, // type
4959 NULL, // ui_requirement
4960 0, // flags
4961 NULL, // dependencies
4962 PURPLE_PRIORITY_DEFAULT, // priority
4963 MATTERMOST_PLUGIN_ID, // id
4964 "Mattermost", // name
4965 MATTERMOST_PLUGIN_VERSION, // version
4966 N_("Mattermost Protocol Plugin."), // summary
4967 N_("Adds Mattermost protocol support to libpurple."), // description
4968 "Eion Robb <eion@robbmob.com>", // author
4969 MATTERMOST_PLUGIN_WEBSITE, // homepage
4970 libpurple2_plugin_load, // load
4971 libpurple2_plugin_unload, // unload
4972 NULL, // destroy
4973 NULL, // ui_info
4974 NULL, // extra_info
4975 NULL, // prefs_info
4976 NULL, // actions
4977 NULL, // padding
4978 NULL,
4979 NULL,
4980 NULL
4981 };
4982
4983 PURPLE_INIT_PLUGIN(mattermost, plugin_init, info);
4984
4985 #else
4986 //Purple 3 plugin load functions
4987
4988 static void
4989 mm_protocol_init(PurpleProtocol *prpl_info)
4990 {
4991 PurpleProtocol *info = prpl_info;
4992 PurpleAccountUserSplit *split;
4993
4994 info->id = MATTERMOST_PLUGIN_ID;
4995 info->name = "Mattermost";
4996 info->options = OPT_PROTO_CHAT_TOPIC | OPT_PROTO_SLASH_COMMANDS_NATIVE | OPT_PROTO_IM_IMAGE;
4997 info->account_options = mm_add_account_options(info->account_options);
4998
4999 split = purple_account_user_split_new(_("Server"), MATTERMOST_DEFAULT_SERVER, MATTERMOST_SERVER_SPLIT_CHAR);
5000 info->user_splits = g_list_append(info->user_splits, split);
5001 }
5002
5003 static void
5004 mm_protocol_class_init(PurpleProtocolClass *prpl_info)
5005 {
5006 prpl_info->login = mm_login;
5007 prpl_info->close = mm_close;
5008 prpl_info->status_types = mm_status_types;
5009 prpl_info->list_icon = mm_list_icon;
5010 }
5011
5012
5013 static void
5014 mm_protocol_im_iface_init(PurpleProtocolIMIface *prpl_info)
5015 {
5016 prpl_info->send = mm_send_im;
5017 prpl_info->send_typing = mm_send_typing;
5018 }
5019
5020 static void
5021 mm_protocol_chat_iface_init(PurpleProtocolChatIface *prpl_info)
5022 {
5023 prpl_info->send = mm_chat_send;
5024 prpl_info->info = mm_chat_info;
5025 prpl_info->info_defaults = mm_chat_info_defaults;
5026 prpl_info->join = mm_join_chat;
5027 prpl_info->get_name = mm_get_chat_name;
5028 prpl_info->invite = mm_chat_invite;
5029 prpl_info->set_topic = mm_chat_set_topic;
5030 }
5031
5032
5033 static void
5034 mm_protocol_server_iface_init(PurpleProtocolServerIface *prpl_info)
5035 {
5036 prpl_info->add_buddy = mm_add_buddy;
5037 prpl_info->remove_buddy = mm_remove_buddy;
5038 prpl_info->group_buddy = mm_fake_group_buddy;
5039 prpl_info->rename_group = mm_fake_group_rename;
5040 prpl_info->set_status = mm_set_status;
5041 prpl_info->set_idle = mm_set_idle;
5042 prpl_info->get_info = mm_get_info;
5043 }
5044
5045
5046 static void
5047 mm_protocol_client_iface_init(PurpleProtocolClientIface *prpl_info)
5048 {
5049 prpl_info->get_actions = mm_actions;
5050 prpl_info->get_account_text_table = mm_get_account_text_table;
5051 prpl_info->blist_node_menu = mm_blist_node_menu;
5052 prpl_info->tooltip_text = mm_tooltip_text;
5053
5054 }
5055
5056
5057 static void
5058 mm_protocol_roomlist_iface_init(PurpleProtocolRoomlistIface *prpl_info)
5059 {
5060 prpl_info->get_list = mm_roomlist_get_list;
5061 prpl_info->room_serialize = mm_roomlist_serialize;
5062 }
5063
5064
5065 static PurpleProtocol *mm_protocol;
5066
5067 PURPLE_DEFINE_TYPE_EXTENDED(
5068 MattermostProtocol, mm_protocol, PURPLE_TYPE_PROTOCOL, 0,
5069
5070 PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_IM_IFACE,
5071 mm_protocol_im_iface_init)
5072
5073 PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_CHAT_IFACE,
5074 mm_protocol_chat_iface_init)
5075
5076 PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_SERVER_IFACE,
5077 mm_protocol_server_iface_init)
5078
5079 PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_CLIENT_IFACE,
5080 mm_protocol_client_iface_init)
5081
5082 PURPLE_IMPLEMENT_INTERFACE_STATIC(PURPLE_TYPE_PROTOCOL_ROOMLIST_IFACE,
5083 mm_protocol_roomlist_iface_init)
5084
5085 );
5086
5087 static gboolean
5088 libpurple3_plugin_load(PurplePlugin *plugin, GError **error)
5089 {
5090 mm_protocol_register_type(plugin);
5091 mm_protocol = purple_protocols_add(MATTERMOST_TYPE_PROTOCOL, error);
5092 if (!mm_protocol)
5093 return FALSE;
5094
5095 return plugin_load(plugin, error);
5096 }
5097
5098 static gboolean
5099 libpurple3_plugin_unload(PurplePlugin *plugin, GError **error)
5100 {
5101 if (!plugin_unload(plugin, error))
5102 return FALSE;
5103
5104 if (!purple_protocols_remove(mm_protocol, error))
5105 return FALSE;
5106
5107 return TRUE;
5108 }
5109
5110 static PurplePluginInfo *
5111 plugin_query(GError **error)
5112 {
5113 return purple_plugin_info_new(
5114 "id", MATTERMOST_PLUGIN_ID,
5115 "name", "Mattermost",
5116 "version", MATTERMOST_PLUGIN_VERSION,
5117 "category", N_("Protocol"),
5118 "summary", N_("Mattermost Protocol Plugin."),
5119 "description", N_("Adds Mattermost protocol support to libpurple."),
5120 "website", MATTERMOST_PLUGIN_WEBSITE,
5121 "abi-version", PURPLE_ABI_VERSION,
5122 "flags", PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
5123 PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD,
5124 NULL
5125 );
5126 }
5127
5128 PURPLE_PLUGIN_INIT(mattermost, plugin_query, libpurple3_plugin_load, libpurple3_plugin_unload);
5129
5130 #endif
5131