1 /***************************************************************************\
2 * *
3 * BitlBee - An IRC to IM gateway *
4 * Simple module to facilitate Mastodon functionality. *
5 * *
6 * Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com> *
7 * Copyright 2010-2013 Wilmer van der Gaast <wilmer@gaast.net> *
8 * Copyright 2017-2019 Alex Schroeder <alex@gnu.org> *
9 * *
10 * This library is free software; you can redistribute it and/or *
11 * modify it under the terms of the GNU Lesser General Public *
12 * License as published by the Free Software Foundation, version *
13 * 2.1. *
14 * *
15 * This library is distributed in the hope that it will be useful, *
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
18 * Lesser General Public License for more details. *
19 * *
20 * You should have received a copy of the GNU Lesser General Public License *
21 * along with this library; if not, write to the Free Software Foundation, *
22 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA *
23 * *
24 ****************************************************************************/
25
26 /* For strptime(): */
27 #if (__sun)
28 #else
29 #define _XOPEN_SOURCE
30 #endif
31
32 #include "mastodon-http.h"
33 #include "mastodon.h"
34 #include "rot13.h"
35 #include "bitlbee.h"
36 #include "url.h"
37 #include "misc.h"
38 #include "base64.h"
39 #include "mastodon-lib.h"
40 #include "oauth2.h"
41 #include "json.h"
42 #include "json_util.h"
43 #include <assert.h>
44 #include <ctype.h>
45 #include <errno.h>
46
47 typedef enum {
48 MT_HOME,
49 MT_LOCAL,
50 MT_FEDERATED,
51 MT_HASHTAG,
52 MT_LIST,
53 } mastodon_timeline_type_t;
54
55 typedef enum {
56 ML_STATUS,
57 ML_NOTIFICATION,
58 } mastodon_list_type_t;
59
60 struct mastodon_list {
61 mastodon_list_type_t type;
62 GSList *list;
63 };
64
65 struct mastodon_account {
66 guint64 id;
67 char *display_name;
68 char *acct;
69 };
70
71 struct mastodon_status {
72 time_t created_at;
73 char *spoiler_text;
74 char *spoiler_text_case_folded; /* for filtering */
75 char *text;
76 char *content; /* same as text without CW and NSFW prefixes */
77 char *content_case_folded; /* for filtering */
78 char *url;
79 struct mastodon_account *account;
80 guint64 id;
81 mastodon_visibility_t visibility;
82 guint64 reply_to;
83 GSList *tags;
84 GSList *mentions;
85 mastodon_timeline_type_t subscription; /* This status was created by a timeline subscription */
86 gboolean is_notification; /* This status was created from a notification */
87 };
88
89 typedef enum {
90 MN_MENTION = 1,
91 MN_REBLOG,
92 MN_FAVOURITE,
93 MN_FOLLOW,
94 } mastodon_notification_type_t;
95
96 struct mastodon_notification {
97 guint64 id;
98 mastodon_notification_type_t type;
99 time_t created_at;
100 struct mastodon_account *account;
101 struct mastodon_status *status;
102 };
103
104 struct mastodon_report {
105 struct im_connection *ic;
106 guint64 account_id;
107 guint64 status_id;
108 char *comment;
109 };
110
111 typedef enum {
112 MF_HOME = 0x00001,
113 MF_NOTIFICATIONS = 0x00002,
114 MF_PUBLIC = 0x00004,
115 MF_THREAD = 0x00008,
116 } mastodon_filter_type_t;
117
118 struct mastodon_filter {
119 guint64 id;
120 char* phrase;
121 char* phrase_case_folded;
122 mastodon_filter_type_t context;
123 gboolean irreversible;
124 gboolean whole_word;
125 time_t expires_in;
126 };
127
128 struct mastodon_command {
129 struct im_connection *ic;
130 guint64 id;
131 guint64 id2;
132 gboolean extra;
133 char *str;
134 char *undo;
135 char *redo;
136 gpointer *data;
137 mastodon_command_type_t command;
138 };
139
140 /**
141 * Frees a mastodon_account struct.
142 */
ma_free(struct mastodon_account * ma)143 static void ma_free(struct mastodon_account *ma)
144 {
145 if (ma == NULL) {
146 return;
147 }
148
149 g_free(ma->display_name);
150 g_free(ma->acct);
151 g_free(ma);
152 }
153
154 /**
155 * Creates a duplicate of an account.
156 */
ma_copy(struct mastodon_account * ma0)157 static struct mastodon_account *ma_copy(struct mastodon_account *ma0)
158 {
159 if (ma0 == NULL) {
160 return NULL;
161 }
162
163 struct mastodon_account *ma = g_new0(struct mastodon_account, 1);
164 ma->id = ma0->id;
165 ma->display_name = g_strdup(ma0->display_name);
166 ma->acct = g_strdup(ma0->acct);
167 return ma;
168 }
169
170 /**
171 * Frees a mastodon_status struct.
172 */
ms_free(struct mastodon_status * ms)173 static void ms_free(struct mastodon_status *ms)
174 {
175 if (ms == NULL) {
176 return;
177 }
178
179 g_free(ms->spoiler_text);
180 g_free(ms->text);
181 g_free(ms->content);
182 g_free(ms->url);
183 ma_free(ms->account);
184 g_slist_free_full(ms->tags, g_free);
185 g_slist_free_full(ms->mentions, (GDestroyNotify) ma_free);
186 g_free(ms);
187 }
188
189 /**
190 * Frees a mastodon_notification struct.
191 */
mn_free(struct mastodon_notification * mn)192 static void mn_free(struct mastodon_notification *mn)
193 {
194 if (mn == NULL) {
195 return;
196 }
197
198 ma_free(mn->account);
199 ms_free(mn->status);
200 g_free(mn);
201 }
202
203 /**
204 * Free a mastodon_list struct.
205 * type is the type of list the struct holds.
206 */
ml_free(struct mastodon_list * ml)207 static void ml_free(struct mastodon_list *ml)
208 {
209 GSList *l;
210
211 if (ml == NULL) {
212 return;
213 }
214
215 for (l = ml->list; l; l = g_slist_next(l)) {
216 if (ml->type == ML_STATUS) {
217 ms_free((struct mastodon_status *) l->data);
218 } else if (ml->type == ML_NOTIFICATION) {
219 mn_free((struct mastodon_notification *) l->data);
220 }
221 }
222
223 g_slist_free(ml->list);
224 g_free(ml);
225 }
226
227 /**
228 * Frees a mastodon_report struct.
229 */
mr_free(struct mastodon_report * mr)230 static void mr_free(struct mastodon_report *mr)
231 {
232 if (mr == NULL) {
233 return;
234 }
235
236 g_free(mr->comment);
237 g_free(mr);
238 }
239
240 /**
241 * Frees a mastodon_filter struct.
242 */
mf_free(struct mastodon_filter * mf)243 static void mf_free(struct mastodon_filter *mf)
244 {
245 if (mf == NULL) {
246 return;
247 }
248 g_free(mf->phrase);
249 g_free(mf);
250 }
251
252 /**
253 * Frees a mastodon_command struct. Note that the groupchat c doesn't need to be freed. It is maintained elsewhere.
254 */
mc_free(struct mastodon_command * mc)255 static void mc_free(struct mastodon_command *mc)
256 {
257 if (mc == NULL) {
258 return;
259 }
260
261 g_free(mc->str);
262 g_free(mc->undo);
263 g_free(mc->redo);
264 g_free(mc);
265 }
266
267
268 /**
269 * Compare status elements
270 */
mastodon_compare_elements(gconstpointer a,gconstpointer b)271 static gint mastodon_compare_elements(gconstpointer a, gconstpointer b)
272 {
273 struct mastodon_status *a_status = (struct mastodon_status *) a;
274 struct mastodon_status *b_status = (struct mastodon_status *) b;
275
276 if (a_status->created_at < b_status->created_at) {
277 return -1;
278 } else if (a_status->created_at > b_status->created_at) {
279 return 1;
280 } else {
281 return 0;
282 }
283 }
284
285 /**
286 * Add a buddy if it is not already added, set the status to logged in.
287 */
mastodon_add_buddy(struct im_connection * ic,gint64 id,char * name,const char * fullname)288 static void mastodon_add_buddy(struct im_connection *ic, gint64 id, char *name, const char *fullname)
289 {
290 struct mastodon_data *md = ic->proto_data;
291
292 // Check if the buddy is already in the buddy list.
293 if (!bee_user_by_handle(ic->bee, ic, name)) {
294 // The buddy is not in the list, add the buddy and set the status to logged in.
295 imcb_add_buddy(ic, name, NULL);
296 imcb_rename_buddy(ic, name, fullname);
297
298 bee_user_t *bu = bee_user_by_handle(ic->bee, ic, name);
299 struct mastodon_user_data *mud = (struct mastodon_user_data*) bu->data;
300 mud->account_id = id;
301
302 if (md->flags & MASTODON_MODE_CHAT) {
303 /* Necessary so that nicks always get translated to the
304 exact Mastodon username. */
305 imcb_buddy_nick_hint(ic, name, name);
306 if (md->timeline_gc) {
307 imcb_chat_add_buddy(md->timeline_gc, name);
308 }
309 } else if (md->flags & MASTODON_MODE_MANY) {
310 imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
311 }
312 }
313 }
314
315 /* Warning: May return a malloc()ed value, which will be free()d on the next
316 call. Only for short-term use. NOT THREADSAFE! */
mastodon_parse_error(struct http_request * req)317 char *mastodon_parse_error(struct http_request *req)
318 {
319 static char *ret = NULL;
320 json_value *root, *err;
321
322 g_free(ret);
323 ret = NULL;
324
325 if (req->body_size > 0) {
326 root = json_parse(req->reply_body, req->body_size);
327 err = json_o_get(root, "error");
328 if (err && err->type == json_string && err->u.string.length) {
329 ret = g_strdup_printf("%s (%s)", req->status_string, err->u.string.ptr);
330 }
331 json_value_free(root);
332 }
333
334 return ret ? ret : req->status_string;
335 }
336
337 /* WATCH OUT: This function might or might not destroy your connection.
338 Sub-optimal indeed, but just be careful when this returns NULL! */
mastodon_parse_response(struct im_connection * ic,struct http_request * req)339 static json_value *mastodon_parse_response(struct im_connection *ic, struct http_request *req)
340 {
341 char path[64] = "", *s;
342
343 if ((s = strchr(req->request, ' '))) {
344 path[sizeof(path) - 1] = '\0';
345 strncpy(path, s + 1, sizeof(path) - 1);
346 if ((s = strchr(path, '?')) || (s = strchr(path, ' '))) {
347 *s = '\0';
348 }
349 }
350
351 if (req->status_code != 200) {
352 mastodon_log(ic, "Error: %s returned status code %s", path, mastodon_parse_error(req));
353
354 if (!(ic->flags & OPT_LOGGED_IN)) {
355 imc_logout(ic, TRUE);
356 }
357 return NULL;
358 }
359
360 json_value *ret;
361 if ((ret = json_parse(req->reply_body, req->body_size)) == NULL) {
362 imcb_error(ic, "Error: %s return data that could not be parsed as JSON", path);
363 }
364 return ret;
365 }
366
367 /**
368 * For Mastodon 2, all id attributes in the REST API responses, including attributes that end in _id, are now returned
369 * as strings instead of integers. This is because large integers cannot be encoded in JSON losslessly, and all IDs in
370 * Mastodon are now bigint (Ruby on Rails: bigint uses 64 bits, signed, guint64 is 64 bits, unsigned). We are assuming
371 * no negative ids.
372 */
mastodon_json_int64(const json_value * v)373 static guint64 mastodon_json_int64(const json_value *v)
374 {
375 guint64 id;
376 if (v->type == json_integer) {
377 return v->u.integer; // Mastodon 1
378
379 } else if (v->type == json_string &&
380 *v->u.string.ptr &&
381 parse_int64(v->u.string.ptr, 10, &id)) {
382 return id; // Mastodon 2
383 }
384 return 0;
385 }
386
387 /* These two functions are useful to debug all sorts of callbacks. */
388 static void mastodon_log_object(struct im_connection *ic, json_value *node, int prefix);
389 static void mastodon_log_array(struct im_connection *ic, json_value *node, int prefix);
390
mastodon_xt_get_user(const json_value * node)391 struct mastodon_account *mastodon_xt_get_user(const json_value *node)
392 {
393 struct mastodon_account *ma;
394 json_value *jv;
395
396 ma = g_new0(struct mastodon_account, 1);
397 ma->display_name = g_strdup(json_o_str(node, "display_name"));
398 ma->acct = g_strdup(json_o_str(node, "acct"));
399
400 if ((jv = json_o_get(node, "id")) &&
401 (ma->id = mastodon_json_int64(jv))) {
402 return ma;
403 }
404
405 ma_free(ma);
406 return NULL;
407 }
408
409 /* This is based on strip_html but in addition to what Bitlbee does, we treat p like br. */
mastodon_strip_html(char * in)410 void mastodon_strip_html(char *in)
411 {
412 char *start = in;
413 char out[strlen(in) + 1];
414 char *s = out;
415
416 memset(out, 0, sizeof(out));
417
418 while (*in) {
419 if (*in == '<') {
420 if (g_strncasecmp(in + 1, "/p>", 3) == 0) {
421 *(s++) = '\n';
422 in += 4;
423 } else {
424 *(s++) = *(in++);
425 }
426 } else {
427 *(s++) = *(in++);
428 }
429 }
430 strcpy(start, out);
431 strip_html(start);
432 }
433
mastodon_parse_visibility(char * value)434 mastodon_visibility_t mastodon_parse_visibility(char *value)
435 {
436 if (g_strcasecmp(value, "public") == 0) {
437 return MV_PUBLIC;
438 } else if (g_strcasecmp(value, "unlisted") == 0) {
439 return MV_UNLISTED;
440 } else if (g_strcasecmp(value, "private") == 0) {
441 return MV_PRIVATE;
442 } else if (g_strcasecmp(value, "direct") == 0) {
443 return MV_DIRECT;
444 } else {
445 return MV_UNKNOWN;
446 }
447 }
448
449 /**
450 * Here, we have a similar setup as for mastodon_chained_account_function. The flow is as follows: the
451 * mastodon_command_handler() calls mastodon_unknown_list_delete(). This sets up the mastodon_command (mc). It then
452 * calls mastodon_with_named_list() and passes along a callback, mastodon_http_list_delete(). It uses
453 * mastodon_chained_list() to extract the list id and store it in mc, and calls the next handler,
454 * mastodon_list_delete(). This is a mastodon_chained_command_function! It doesn't have to check whether ic is live.
455 */
456 typedef void (*mastodon_chained_command_function)(struct im_connection *ic, struct mastodon_command *mc);
457
458 /**
459 * This is the wrapper around callbacks that need to search for the list id in a list result. Note that list titles are
460 * case-sensitive.
461 */
mastodon_chained_list(struct http_request * req,mastodon_chained_command_function func)462 static void mastodon_chained_list(struct http_request *req, mastodon_chained_command_function func) {
463 struct mastodon_command *mc = req->data;
464 struct im_connection *ic = mc->ic;
465
466 if (!g_slist_find(mastodon_connections, ic)) {
467 goto finally;
468 }
469
470 json_value *parsed;
471 if (!(parsed = mastodon_parse_response(ic, req))) {
472 /* ic would have been freed in imc_logout in this situation */
473 ic = NULL;
474 goto finally;
475 }
476
477 if (parsed->type != json_array || parsed->u.array.length == 0) {
478 mastodon_log(ic, "You seem to have no lists defined. "
479 "Create one using 'list create <title>'.");
480 goto finish;
481 }
482
483 int i;
484 guint64 id = 0;
485 char *title = mc->str;
486
487 for (i = 0; i < parsed->u.array.length; i++) {
488 json_value *a = parsed->u.array.values[i];
489 json_value *it;
490 if (a->type == json_object &&
491 (it = json_o_get(a, "id")) &&
492 g_strcmp0(title, json_o_str(a, "title")) == 0) {
493 id = mastodon_json_int64(it);
494 break;
495 }
496 }
497
498 if (!id) {
499 mastodon_log(ic, "There is no list called '%s'. "
500 "Use 'list' to show existing lists.", title);
501 goto finish;
502 } else {
503 mc->id = id;
504 func(ic, mc);
505 /* If successful, we need to keep mc for one more request. */
506 json_value_free(parsed);
507 return;
508 }
509
510 finish:
511 /* We didn't find what we were looking for and need to free the parsed data. */
512 json_value_free(parsed);
513 finally:
514 /* We've encountered a problem and we need to free the mastodon_command. */
515 mc_free(mc);
516 }
517
518 /**
519 * Wrapper which sets up the first callback for functions acting on a list. For every command, the list has to be
520 * searched, first. The callback you provide must used mastodon_chained_list() to extract the list id and then call the
521 * reall callback.
522 */
mastodon_with_named_list(struct im_connection * ic,struct mastodon_command * mc,http_input_function func)523 void mastodon_with_named_list(struct im_connection *ic, struct mastodon_command *mc, http_input_function func) {
524 mastodon_http(ic, MASTODON_LIST_URL, func, mc, HTTP_GET, NULL, 0);
525 }
526
527 /**
528 * Function to fill a mastodon_status struct.
529 */
mastodon_xt_get_status(const json_value * node,struct im_connection * ic)530 static struct mastodon_status *mastodon_xt_get_status(const json_value *node, struct im_connection *ic)
531 {
532 struct mastodon_status *ms = {0};
533 const json_value *rt = NULL;
534 const json_value *text_value = NULL;
535 const json_value *spoiler_value = NULL;
536 const json_value *url_value = NULL;
537 GSList *media = NULL;
538 gboolean nsfw = FALSE;
539 gboolean use_cw1 = g_strcasecmp(set_getstr(&ic->acc->set, "hide_sensitive"), "advanced_rot13") == 0;
540
541 if (node->type != json_object) {
542 return FALSE;
543 }
544 ms = g_new0(struct mastodon_status, 1);
545
546 JSON_O_FOREACH(node, k, v) {
547 if (strcmp("content", k) == 0 && v->type == json_string && *v->u.string.ptr) {
548 text_value = v;
549 } if (strcmp("spoiler_text", k) == 0 && v->type == json_string && *v->u.string.ptr) {
550 spoiler_value = v;
551 } else if (strcmp("url", k) == 0 && v->type == json_string) {
552 url_value = v;
553 } else if (strcmp("reblog", k) == 0 && v->type == json_object) {
554 rt = v;
555 } else if (strcmp("created_at", k) == 0 && v->type == json_string) {
556 struct tm parsed;
557
558 /* Very sensitive to changes to the formatting of
559 this field. :-( Also assumes the timezone used
560 is UTC since C time handling functions suck. */
561 if (strptime(v->u.string.ptr, MASTODON_TIME_FORMAT, &parsed) != NULL) {
562 ms->created_at = mktime_utc(&parsed);
563 }
564 } else if (strcmp("visibility", k) == 0 && v->type == json_string && *v->u.string.ptr) {
565 ms->visibility = mastodon_parse_visibility(v->u.string.ptr);
566 } else if (strcmp("account", k) == 0 && v->type == json_object) {
567 ms->account = mastodon_xt_get_user(v);
568 } else if (strcmp("id", k) == 0) {
569 ms->id = mastodon_json_int64(v);
570 } else if (strcmp("in_reply_to_id", k) == 0) {
571 ms->reply_to = mastodon_json_int64(v);
572 } else if (strcmp("tags", k) == 0 && v->type == json_array) {
573 GSList *l = NULL;
574 int i;
575 for (i = 0; i < v->u.array.length; i++) {
576 json_value *tag = v->u.array.values[i];
577 if (tag->type == json_object) {
578 const char *name = json_o_str(tag, "name");
579 if (name) {
580 l = g_slist_prepend(l, g_strdup(name));
581 }
582 }
583 }
584 ms->tags = l;
585 } else if (strcmp("mentions", k) == 0 && v->type == json_array) {
586 GSList *l = NULL;
587 int i;
588 gint64 id = set_getint(&ic->acc->set, "account_id");
589 for (i = 0; i < v->u.array.length; i++) {
590 struct mastodon_account *ma = mastodon_xt_get_user(v->u.array.values[i]);
591 /* Skip the current user in mentions since we're only interested in this information for replies where
592 * we'll never want to mention ourselves. */
593 if (ma && ma->id != id) l = g_slist_prepend(l, ma);
594 }
595 ms->mentions = l;
596 } else if (strcmp("sensitive", k) == 0 && v->type == json_boolean) {
597 nsfw = v->u.boolean;
598 } else if (strcmp("media_attachments", k) == 0 && v->type == json_array) {
599 int i;
600 for (i = 0; i < v->u.array.length; i++) {
601 json_value *attachment = v->u.array.values[i];
602 if (attachment->type == json_object) {
603 // text_url is preferred because that's what the UI also copies
604 // into the message; also ignore values such as /files/original/missing.png
605 const char *url = json_o_str(attachment, "text_url");
606 if (!url || !*url || strncmp(url, "http", 4)) {
607 url = json_o_str(attachment, "url");
608 if (!url || !*url || strncmp(url, "http", 4)) {
609 url = json_o_str(attachment, "remote_url");
610 }
611 }
612 if (url && *url && strncmp(url, "http", 4) == 0) {
613 media = g_slist_prepend(media, (char *) url); // discarding const qualifier
614 }
615 }
616 }
617 }
618 }
619
620 if (rt) {
621 struct mastodon_status *rms = mastodon_xt_get_status(rt, ic);
622 if (rms) {
623 /* Alternatively, we could free ms and just use rms, but we'd have to overwrite rms->account
624 * with ms->account, change rms->text, and maybe more. */
625 ms->text = g_strdup_printf("boosted @%s: %s", rms->account->acct, rms->text);
626 ms->id = rms->id;
627
628 ms->url = rms->url; // adopt
629 rms->url = NULL;
630
631 g_slist_free_full(ms->tags, g_free);
632 ms->tags = rms->tags; // adopt
633 rms->tags = NULL;
634
635 g_slist_free_full(ms->mentions, (GDestroyNotify) ma_free);
636 ms->mentions = rms->mentions; // adopt
637 rms->mentions = NULL;
638
639 /* add original author to mentions of boost if not ourselves */
640 gint64 id = set_getint(&ic->acc->set, "account_id");
641 if (rms->account->id != id) {
642 ms->mentions = g_slist_prepend(ms->mentions, rms->account); // adopt
643 rms->account = NULL;
644 }
645
646 ms_free(rms);
647 }
648 } else if (ms->id) {
649
650 if (url_value) {
651 ms->url = g_strdup(url_value->u.string.ptr);
652 }
653
654 // build status text
655 GString *s = g_string_new(NULL);
656
657 if (spoiler_value) {
658 char *spoiler_text = g_strdup(spoiler_value->u.string.ptr);
659 mastodon_strip_html(spoiler_text);
660 g_string_append_printf(s, "[CW: %s]", spoiler_text);
661 ms->spoiler_text = spoiler_text;
662 ms->spoiler_text_case_folded = g_utf8_casefold(spoiler_text, -1);
663 if (nsfw || !use_cw1) {
664 g_string_append(s, " ");
665 }
666 }
667
668 if (nsfw) {
669 char *sensitive_flag = set_getstr(&ic->acc->set, "sensitive_flag");
670 g_string_append(s, sensitive_flag);
671 }
672
673 if (text_value) {
674 char *text = g_strdup(text_value->u.string.ptr);
675 mastodon_strip_html(text);
676 ms->content = g_strdup(text);
677 ms->content_case_folded = g_utf8_casefold(text, -1);
678 char *fmt = "%s";
679 if (spoiler_value && use_cw1) {
680 char *wrapped = NULL;
681 char **cwed = NULL;
682 rot13(text);
683 // "\001CW1 \001" = 6 bytes, there's also a nick length issue we take into account.
684 // there's also irc_format_timestamp which can add like 28 bytes or something.
685 wrapped = word_wrap(text, IRC_WORD_WRAP - 6 - MAX_NICK_LENGTH - 28);
686 g_free(text);
687 text = wrapped;
688 cwed = g_strsplit(text, "\n", -1); // easier than a regex
689 g_free(text);
690 text = g_strjoinv("\001\n\001CW1 ", cwed); // easier than a replace
691 g_strfreev(cwed);
692 fmt = "\n\001CW1 %s\001"; // add a newline at the start because that makes word wrap a lot easier (and because it matches the web UI better)
693 } else if (spoiler_value && g_strcasecmp(set_getstr(&ic->acc->set, "hide_sensitive"), "rot13") == 0) {
694 rot13(text);
695 } else if (spoiler_value && set_getbool(&ic->acc->set, "hide_sensitive")) {
696 g_free(text);
697 text = g_strdup(ms->url);
698 if (text) {
699 fmt = "[hidden: %s]";
700 } else {
701 fmt = "[hidden]";
702 }
703 }
704 g_string_append_printf(s, fmt, text);
705 g_free(text);
706 }
707
708 GSList *l = NULL;
709 for (l = media; l; l = l->next) {
710 // TODO maybe support hiding media when it's marked NSFW.
711 // (note that only media is hidden when it's marked NSFW. the text still shows.)
712 // (note that we already don't show media, since this is all text, but IRC clients might.)
713
714 char *url = l->data;
715
716 if ((text_value && strstr(text_value->u.string.ptr, url)) || strstr(s->str, url)) {
717 // skip URLs already in the text
718 continue;
719 }
720
721 if (s->len) {
722 g_string_append(s, " ");
723 }
724 g_string_append(s, url);
725 }
726
727 ms->text = g_string_free(s, FALSE); // we keep the data
728
729 }
730
731 g_slist_free(media); // elements are pointers into node and don't need to be freed
732
733 if (ms->text && ms->account && ms->id) {
734 return ms;
735 }
736
737 ms_free(ms);
738 return NULL;
739 }
740
741 /**
742 * Function to fill a mastodon_notification struct.
743 */
mastodon_xt_get_notification(const json_value * node,struct im_connection * ic)744 static struct mastodon_notification *mastodon_xt_get_notification(const json_value *node, struct im_connection *ic)
745 {
746 if (node->type != json_object) {
747 return FALSE;
748 }
749
750 struct mastodon_notification *mn = g_new0(struct mastodon_notification, 1);
751
752 JSON_O_FOREACH(node, k, v) {
753 if (strcmp("id", k) == 0) {
754 mn->id = mastodon_json_int64(v);
755 } else if (strcmp("created_at", k) == 0 && v->type == json_string) {
756 struct tm parsed;
757
758 /* Very sensitive to changes to the formatting of
759 this field. :-( Also assumes the timezone used
760 is UTC since C time handling functions suck. */
761 if (strptime(v->u.string.ptr, MASTODON_TIME_FORMAT, &parsed) != NULL) {
762 mn->created_at = mktime_utc(&parsed);
763 }
764 } else if (strcmp("account", k) == 0 && v->type == json_object) {
765 mn->account = mastodon_xt_get_user(v);
766 } else if (strcmp("status", k) == 0 && v->type == json_object) {
767 mn->status = mastodon_xt_get_status(v, ic);
768 } else if (strcmp("type", k) == 0 && v->type == json_string) {
769 if (strcmp(v->u.string.ptr, "mention") == 0) {
770 mn->type = MN_MENTION;
771 } else if (strcmp(v->u.string.ptr, "reblog") == 0) {
772 mn->type = MN_REBLOG;
773 } else if (strcmp(v->u.string.ptr, "favourite") == 0) {
774 mn->type = MN_FAVOURITE;
775 } else if (strcmp(v->u.string.ptr, "follow") == 0) {
776 mn->type = MN_FOLLOW;
777 }
778 }
779 }
780
781 if (mn->type) {
782 return mn;
783 }
784
785 mn_free(mn);
786 return NULL;
787 }
788
mastodon_xt_get_status_list(struct im_connection * ic,const json_value * node,struct mastodon_list * ml)789 static gboolean mastodon_xt_get_status_list(struct im_connection *ic, const json_value *node,
790 struct mastodon_list *ml)
791 {
792 ml->type = ML_STATUS;
793
794 if (node->type != json_array) {
795 return FALSE;
796 }
797
798 int i;
799 for (i = 0; i < node->u.array.length; i++) {
800 struct mastodon_status *ms = mastodon_xt_get_status(node->u.array.values[i], ic);
801 if (ms) {
802 /* Code that calls this will display the toots in the home timeline, i.e. the account channel. This is true
803 * right after a login and when displaying search results or a toot context. */
804 ms->subscription = MT_HOME;
805 ml->list = g_slist_prepend(ml->list, ms);
806 }
807 }
808 ml->list = g_slist_reverse(ml->list);
809 return TRUE;
810 }
811
mastodon_xt_get_notification_list(struct im_connection * ic,const json_value * node,struct mastodon_list * ml)812 static gboolean mastodon_xt_get_notification_list(struct im_connection *ic, const json_value *node,
813 struct mastodon_list *ml)
814 {
815 ml->type = ML_NOTIFICATION;
816
817 if (node->type != json_array) {
818 return FALSE;
819 }
820
821 int i;
822 for (i = 0; i < node->u.array.length; i++) {
823 struct mastodon_notification *mn = mastodon_xt_get_notification(node->u.array.values[i], ic);
824 if (mn) {
825 ml->list = g_slist_prepend(ml->list, mn);
826 }
827 }
828 ml->list = g_slist_reverse(ml->list);
829 return TRUE;
830 }
831
832 /* Will log messages either way. Need to keep track of IDs for stream deduping.
833 Plus, show_ids is on by default and I don't see why anyone would disable it. */
mastodon_msg_add_id(struct im_connection * ic,struct mastodon_status * ms,const char * prefix)834 static char *mastodon_msg_add_id(struct im_connection *ic,
835 struct mastodon_status *ms, const char *prefix)
836 {
837 struct mastodon_data *md = ic->proto_data;
838 int reply_to = -1;
839 int idx = -1;
840
841 /* See if we know this status and if we know the status this one is replying to. */
842 int i;
843 for (i = 0; i < MASTODON_LOG_LENGTH; i++) {
844 if (ms->reply_to && md->log[i].id == ms->reply_to) {
845 reply_to = i;
846 }
847 if (md->log[i].id == ms->id) {
848 idx = i;
849 }
850 if (idx != -1 && (!ms->reply_to || reply_to != -1)) {
851 break;
852 }
853 }
854
855 /* If we didn't find the status, it's new and needs an id, and we want to record who said it, and when they said
856 * it, and who they mentioned, and the spoiler they used. We need to do this in two places: the md->log, and per
857 * user in the mastodon_user_data (mud). */
858 if (idx == -1) {
859 idx = md->log_id = (md->log_id + 1) % MASTODON_LOG_LENGTH;
860 md->log[idx].id = ms->id;
861
862 md->log[idx].visibility = ms->visibility;
863 g_slist_free_full(md->log[idx].mentions, g_free);
864 md->log[idx].mentions = g_slist_copy_deep(ms->mentions, (GCopyFunc) ma_copy, NULL);
865
866 g_free(md->log[idx].spoiler_text);
867 md->log[idx].spoiler_text = g_strdup(ms->spoiler_text); // no problem if NULL
868
869 gint64 id = set_getint(&ic->acc->set, "account_id");
870 if (ms->account->id == id) {
871 /* If this is our own status, use a fake bu without data since we can't be found by handle. This
872 * will allow us to reply to our own messages, for example. */
873 md->log[idx].bu = &mastodon_log_local_user;
874 } else {
875 bee_user_t *bu = bee_user_by_handle(ic->bee, ic, ms->account->acct);
876 struct mastodon_user_data *mud = bu->data;
877
878 if (ms->id > mud->last_id) {
879 mud->visibility = ms->visibility;
880 if (ms->visibility == MV_DIRECT) {
881 /* We need to keep the timestamp for direct communications in addition to the regular timestamp so
882 * that if somebody sends us a direct message (which shows up in a query buffer) and then posts a
883 * public message (which shows up in the regular channel) and we reply in the query buffer then we
884 * want our in_reply_to to refer to the older direct message, no the newer public message (see
885 * mastodon_buddy_msg which calls mastodon_post_message using MASTODON_REPLY). At the same time, if
886 * somebody sends a public message first, followed by a direct message, and re reply in the regular
887 * channel (!) then we want our reply to still work (mastodon_handle_command calls
888 * mastodon_post_message with MASTODON_MAYBE_REPLY). */
889 mud->last_direct_id = ms->id;
890 mud->last_direct_time = ms->created_at;
891 }
892 mud->last_id = ms->id;
893 mud->last_time = ms->created_at;
894 g_slist_free_full(mud->mentions, (GDestroyNotify) ma_free);
895 mud->mentions = g_slist_copy_deep(ms->mentions, (GCopyFunc) ma_copy, NULL);
896
897 g_free(mud->spoiler_text);
898 mud->spoiler_text = g_strdup(ms->spoiler_text); // no problem if NULL
899 }
900
901 md->log[idx].bu = bu;
902 }
903
904 }
905
906 if (set_getbool(&ic->acc->set, "show_ids")) {
907 if (reply_to != -1) {
908 return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s",
909 idx, reply_to, prefix, ms->text);
910 } else {
911 return g_strdup_printf("\002[\002%02x\002]\002 %s%s",
912 idx, prefix, ms->text);
913 }
914 } else {
915 if (*prefix) {
916 return g_strconcat(prefix, ms->text, NULL);
917 } else {
918 return NULL;
919 }
920 }
921 }
922
923 /**
924 * Helper function for mastodon_status_show_chat.
925 */
mastodon_status_show_chat1(struct im_connection * ic,gboolean me,struct groupchat * c,char * msg,struct mastodon_status * ms)926 static void mastodon_status_show_chat1(struct im_connection *ic, gboolean me, struct groupchat *c, char *msg, struct mastodon_status *ms)
927 {
928 if (me) {
929 mastodon_visibility_t default_visibility = mastodon_default_visibility(ic);
930 if (ms->visibility == default_visibility) {
931 imcb_chat_log(c, "You: %s", msg ? msg : ms->text);
932 } else {
933 imcb_chat_log(c, "You, %s: %s", mastodon_visibility(ms->visibility), msg ? msg : ms->text);
934 }
935 } else {
936 imcb_chat_msg(c, ms->account->acct,
937 msg ? msg : ms->text, 0, ms->created_at);
938 }
939 }
940
941 /**
942 * Function that is called to see the statuses in a group chat. If the user created appropriate group chats (see setup
943 * in mastodon_chat_join()), then we have extra streams providing the toots for these streams. The subscription
944 * attribute gives us a basic hint of how the status wants to be sorted. Now, we also have a TIMELINE command, which
945 * allows us to simulate the result. In this case, we can't be sure that appropriate group chats exist and thus we need
946 * to put those statuses into the user timeline if they do not. */
mastodon_status_show_chat(struct im_connection * ic,struct mastodon_status * status)947 static void mastodon_status_show_chat(struct im_connection *ic, struct mastodon_status *status)
948 {
949 gint64 id = set_getint(&ic->acc->set, "account_id");
950 gboolean me = (status->account->id == id);
951
952 if (!me) {
953 /* MUST be done before mastodon_msg_add_id() to avoid #872. */
954 mastodon_add_buddy(ic, status->account->id, status->account->acct, status->account->display_name);
955 }
956
957 char *msg = mastodon_msg_add_id(ic, status, "");
958
959 gboolean seen = FALSE;
960 struct mastodon_user_data *mud;
961 struct groupchat *c;
962 bee_user_t *bu;
963 GSList *l;
964
965 switch (status->subscription) {
966
967 case MT_LIST:
968 /* Add the status to existing group chats with a topic matching any the lists this user is part of. */
969 bu = bee_user_by_handle(ic->bee, ic, status->account->acct);
970 mud = (struct mastodon_user_data*) bu->data;
971 for (l = mud->lists; l; l = l->next) {
972 char *title = l->data;
973 struct groupchat *c = bee_chat_by_title(ic->bee, ic, title);
974 if (c) {
975 mastodon_status_show_chat1(ic, me, c, msg, status);
976 seen = TRUE;
977 }
978 }
979 break;
980
981 case MT_HASHTAG:
982 /* Add the status to any other existing group chats whose title matches one of the tags, including the hash! */
983 for (l = status->tags; l; l = l->next) {
984 char *tag = l->data;
985 char *title = g_strdup_printf("#%s", tag);
986 struct groupchat *c = bee_chat_by_title(ic->bee, ic, title);
987 if (c) {
988 mastodon_status_show_chat1(ic, me, c, msg, status);
989 seen = TRUE;
990 }
991 g_free(title);
992 }
993 break;
994
995 case MT_LOCAL:
996 /* If there is an appropriate group chat, do not put it in the user timeline. */
997 c = bee_chat_by_title(ic->bee, ic, "local");
998 if (c) {
999 mastodon_status_show_chat1(ic, me, c, msg, status);
1000 seen = TRUE;
1001 }
1002 break;
1003
1004 case MT_FEDERATED:
1005 /* If there is an appropriate group chat, do not put it in the user timeline. */
1006 c = bee_chat_by_title(ic->bee, ic, "federated");
1007 if (c) {
1008 mastodon_status_show_chat1(ic, me, c, msg, status);
1009 seen = TRUE;
1010 }
1011 break;
1012
1013 case MT_HOME:
1014 /* This is the default */
1015 break;
1016 }
1017
1018 if (!seen) {
1019 c = mastodon_groupchat_init(ic);
1020 mastodon_status_show_chat1(ic, me, c, msg, status);
1021 }
1022
1023 g_free(msg);
1024 }
1025
1026 /**
1027 * Function that is called to see statuses as private messages.
1028 */
mastodon_status_show_msg(struct im_connection * ic,struct mastodon_status * ms)1029 static void mastodon_status_show_msg(struct im_connection *ic, struct mastodon_status *ms)
1030 {
1031 struct mastodon_data *md = ic->proto_data;
1032 char from[MAX_STRING] = "";
1033 char *text = NULL;
1034 gint64 id = set_getint(&ic->acc->set, "account_id");
1035 gboolean me = (ms->account->id == id);
1036 char *name = set_getstr(&ic->acc->set, "name");
1037
1038 if (md->flags & MASTODON_MODE_ONE) {
1039
1040 char *prefix = g_strdup_printf("\002<\002%s\002>\002 ", ms->account->acct);
1041 text = mastodon_msg_add_id(ic, ms, prefix); /* may return NULL */
1042 g_free(prefix);
1043
1044 g_strlcpy(from, name, sizeof(from));
1045 imcb_buddy_msg(ic, from, text ? text : ms->text, 0, ms->created_at);
1046
1047 } else if (!me) {
1048
1049 mastodon_add_buddy(ic, ms->account->id, ms->account->acct, ms->account->display_name);
1050 text = mastodon_msg_add_id(ic, ms, ""); /* may return NULL */
1051 imcb_buddy_msg(ic, *from ? from : ms->account->acct, text ? text : ms->text, 0, ms->created_at);
1052
1053 } else if (!ms->mentions) {
1054
1055 text = mastodon_msg_add_id(ic, ms, "You, direct, but without mentioning anybody: "); /* may return NULL */
1056 mastodon_log(ic, text ? text : ms->text);
1057
1058 } else {
1059
1060 text = mastodon_msg_add_id(ic, ms, "You, direct: ");
1061
1062 /* At this point we have to cheat: if this is the echo of a message we're sending, we still want this message to
1063 * show up in the query buffer where we're chatting with somebody. So even though it is "from us" we're going to
1064 * fake that it is "from the recipient". And worse: we want to do this for all the buddies if we're chatting
1065 * directly with multiple people. */
1066
1067 GSList *l;
1068 for (l = ms->mentions; l; l = g_slist_next(l)) {
1069
1070 bee_user_t *bu;
1071 struct mastodon_account *ma = (struct mastodon_account *) l->data;
1072 if ((bu = bee_user_by_handle(ic->bee, ic, ma->acct))) {
1073 mastodon_add_buddy(ic, ma->id, ma->acct, ma->display_name);
1074 imcb_buddy_msg(ic, ma->acct, text ? text : ms->text, 0, ms->created_at);
1075 }
1076 }
1077 }
1078
1079 g_free(text);
1080 }
1081
mastodon_notification_to_status(struct mastodon_notification * notification)1082 struct mastodon_status *mastodon_notification_to_status(struct mastodon_notification *notification)
1083 {
1084 struct mastodon_account *ma = notification->account;
1085 struct mastodon_status *ms = notification->status;
1086
1087 if (ma == NULL) {
1088 // Should not happen.
1089 ma = g_new0(struct mastodon_account, 1);
1090 ma->acct = g_strdup("anon");
1091 ma->display_name = g_strdup("Unknown");
1092 }
1093
1094 /* The status in the notification was written by you, it's account is your account, but now somebody else is doing
1095 * something with it. We want to avoid the extra You at the beginning, "You: [01] @foo boosted your status: bla"
1096 * should be "<foo> [01] boosted your status: bla" or "<foo> followed you". So we're creating a fake status with a
1097 * copy of the notification account. */
1098 if (ms == NULL) {
1099 /* Could be a FOLLOW notification without status. */
1100 ms = g_new0(struct mastodon_status, 1);
1101 ms->account = ma_copy(notification->account);
1102 ms->created_at = notification->created_at;
1103 /* This ensures that ms will be freed when the notification is freed. */
1104 notification->status = ms;
1105 } else {
1106 /* Adopt the account from the notification. The account will be freed when the notification frees the status. */
1107 ma_free(ms->account);
1108 ms->account = ma;
1109 notification->account = NULL;
1110 }
1111
1112 /* Make sure filters from the notification context know that this status is from a notification. */
1113 ms->is_notification = TRUE;
1114
1115 char *original = ms->text;
1116
1117 switch (notification->type) {
1118 case MN_MENTION:
1119 // this is fine
1120 original = NULL;
1121 break;
1122 case MN_REBLOG:
1123 ms->text = g_strdup_printf("boosted your status: %s", original);
1124 break;
1125 case MN_FAVOURITE:
1126 ms->text = g_strdup_printf("favourited your status: %s", original);
1127 break;
1128 case MN_FOLLOW:
1129 ms->text = g_strdup_printf("[%s] followed you", ma->display_name);
1130 break;
1131 }
1132
1133 g_free(original);
1134
1135 return ms;
1136 }
1137
1138 /**
1139 * Test whether a filter applies to the text.
1140 */
mastodon_filter_matches_it(char * text,struct mastodon_filter * mf)1141 gboolean mastodon_filter_matches_it(char *text, struct mastodon_filter *mf)
1142 {
1143 if (!text) return FALSE;
1144
1145 if (!mf->whole_word) {
1146 return strstr(text, mf->phrase_case_folded) != NULL;
1147 } else {
1148 /* Find the character at the beginning of the phrase and the character at the end of the phrase. */
1149 int len = strlen(mf->phrase_case_folded);
1150 gunichar p1 = g_utf8_get_char(mf->phrase_case_folded);
1151 gunichar p2 = g_utf8_get_char(g_utf8_prev_char(mf->phrase_case_folded + len));
1152 gboolean p1_is_alnum = g_unichar_isalnum(p1);
1153 gboolean p2_is_alnum = g_unichar_isalnum(p2);
1154
1155 /* Start searching from the beginning. When we continue searching because a match is not at word boundaries,
1156 just skip a single character because matches can overlap. */
1157 gchar *s = text;
1158 while ((s = strstr (s, mf->phrase_case_folded))) {
1159
1160 /* At the beginning of the text counts as a word boundary. If the beginning of the phrase is not
1161 * alphanumeric, we don't care about word boundaries. */
1162 if (s != text && p1_is_alnum) {
1163 /* Find the character before the match. This could potentially be the first character. */
1164 gunichar c = g_utf8_get_char(g_utf8_prev_char(s));
1165 /* If this is also an alphanumeric character, then this is not a word boundary. */
1166 if (g_unichar_isalnum(c)) {
1167 s = g_utf8_next_char(s);
1168 continue;
1169 }
1170 }
1171
1172 /* If the beginning of the phrase is not alphanumeric, we don't care about word boundaries. */
1173 if (p2_is_alnum) {
1174 /* Find the character after the match. This could potentially be the zero byte. */
1175 gunichar c = g_utf8_get_char(g_utf8_prev_char(s) + len);
1176 /* If this is the end of the string, or an alphanumeric character, then this is not a word boundary. */
1177 if (!c || g_unichar_isalnum(c)) {
1178 s = g_utf8_next_char(s);
1179 continue;
1180 }
1181 }
1182
1183 /* Otherwise we're golden. */
1184 return TRUE;
1185 }
1186 return FALSE;
1187 }
1188 }
1189
1190 /**
1191 * Test whether a filter applies to the status.
1192 */
mastodon_filter_matches(struct mastodon_status * ms,struct mastodon_filter * mf)1193 gboolean mastodon_filter_matches(struct mastodon_status *ms, struct mastodon_filter *mf)
1194 {
1195 if (!ms || !mf || !mf->phrase_case_folded)
1196 return FALSE;
1197 return (mastodon_filter_matches_it(ms->content_case_folded, mf) ||
1198 mastodon_filter_matches_it(ms->spoiler_text_case_folded, mf));
1199 }
1200
1201 /**
1202 * Show the status to the user.
1203 */
mastodon_status_show(struct im_connection * ic,struct mastodon_status * ms)1204 static void mastodon_status_show(struct im_connection *ic, struct mastodon_status *ms)
1205 {
1206 struct mastodon_data *md = ic->proto_data;
1207
1208 if (ms->account == NULL || ms->text == NULL) {
1209 return;
1210 }
1211
1212 /* Must check all the filters. */
1213 GSList *l;
1214 for (l = md->filters; l; l = g_slist_next(l)) {
1215 struct mastodon_filter *mf = (struct mastodon_filter *) l->data;
1216 /* MF_HOME filter applies to the home timeline, MF_PUBLIC applies to the local and federated public timelines,
1217 * MF_NOTIFICATION applies to any notifications received. */
1218 if (((mf->context & MF_HOME && ms->subscription == MT_HOME) ||
1219 (mf->context & MF_PUBLIC && (ms->subscription == MT_LOCAL || ms->subscription == MT_FEDERATED)) ||
1220 (mf->context & MF_NOTIFICATIONS && ms->is_notification) ||
1221 mf->context & MF_THREAD) &&
1222 mastodon_filter_matches(ms, mf)) {
1223 /* Do not show. */
1224 return;
1225 }
1226 }
1227
1228 /* Deduplicating only affects the previous status shown. Thus, if we got mentioned in a toot by a user that we're
1229 * following, chances are that both events will arrive in sequence. In this case, the second one will be skipped.
1230 * This will also work when flushing timelines after connecting: notification and status update should be close to
1231 * each other. This will fail if the stream is really busy. Critically, it won't suppress statuses from later
1232 * context and timeline requests. */
1233 if (ms->id == md->seen_id) {
1234 return;
1235 } else {
1236 md->seen_id = ms->id;
1237 }
1238
1239 /* Grrrr. Would like to do this during parsing, but can't access settings from there. */
1240 if (set_getbool(&ic->acc->set, "strip_newlines")) {
1241 strip_newlines(ms->text);
1242 }
1243
1244 /* By default, everything except direct messages goes into a channel. */
1245 if (md->flags & MASTODON_MODE_CHAT &&
1246 ms->visibility != MV_DIRECT) {
1247 mastodon_status_show_chat(ic, ms);
1248 } else {
1249 mastodon_status_show_msg(ic, ms);
1250 }
1251 }
1252
mastodon_notification_show(struct im_connection * ic,struct mastodon_notification * notification)1253 static void mastodon_notification_show(struct im_connection *ic, struct mastodon_notification *notification)
1254 {
1255 gboolean show = TRUE;
1256
1257 switch (notification->type) {
1258 case MN_MENTION:
1259 show = !set_getbool(&ic->acc->set, "hide_mentions");
1260 break;
1261 case MN_REBLOG:
1262 show = !set_getbool(&ic->acc->set, "hide_boosts");
1263 break;
1264 case MN_FAVOURITE:
1265 show = !set_getbool(&ic->acc->set, "hide_favourites");
1266 break;
1267 case MN_FOLLOW:
1268 show = !set_getbool(&ic->acc->set, "hide_follows");
1269 break;
1270 }
1271
1272 if (show)
1273 mastodon_status_show(ic, mastodon_notification_to_status(notification));
1274 }
1275
1276 /**
1277 * Add exactly one notification to the timeline.
1278 */
mastodon_stream_handle_notification(struct im_connection * ic,json_value * parsed,mastodon_timeline_type_t subscription)1279 static void mastodon_stream_handle_notification(struct im_connection *ic, json_value *parsed, mastodon_timeline_type_t subscription)
1280 {
1281 struct mastodon_notification *mn = mastodon_xt_get_notification(parsed, ic);
1282 if (mn) {
1283 /* A follow notification has no status and thus cannot be assigned a subsription (see mastodon_timeline_type_t).
1284 * But if there is a status associated with the notification, we know where it came from. */
1285 if (mn->status)
1286 mn->status->subscription = subscription;
1287 mastodon_notification_show(ic, mn);
1288 mn_free(mn);
1289 }
1290 }
1291
1292 /**
1293 * Add exactly one status to the timeline.
1294 */
mastodon_stream_handle_update(struct im_connection * ic,json_value * parsed,mastodon_timeline_type_t subscription)1295 static void mastodon_stream_handle_update(struct im_connection *ic, json_value *parsed, mastodon_timeline_type_t subscription)
1296 {
1297 struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic);
1298 if (ms) {
1299 ms->subscription = subscription;
1300 mastodon_status_show(ic, ms);
1301 ms_free(ms);
1302 }
1303 }
1304
1305 /* Let the user know if a status they have recently seen was deleted. If we can't find the deleted status in our list of
1306 * recently seen statuses, ignore the event. */
mastodon_stream_handle_delete(struct im_connection * ic,json_value * parsed)1307 static void mastodon_stream_handle_delete(struct im_connection *ic, json_value *parsed)
1308 {
1309 struct mastodon_data *md = ic->proto_data;
1310 guint64 id = mastodon_json_int64(parsed);
1311 if (id) {
1312 int i;
1313 for (i = 0; i < MASTODON_LOG_LENGTH; i++) {
1314 if (md->log[i].id == id) {
1315 mastodon_log(ic, "Status %02x was deleted.", i);
1316 md->log[i].id = 0; // prevent future references
1317 return;
1318 }
1319 }
1320 } else {
1321 mastodon_log(ic, "Error parsing a deletion event.");
1322 }
1323 }
1324
mastodon_stream_handle_event(struct im_connection * ic,mastodon_evt_flags_t evt_type,json_value * parsed,mastodon_timeline_type_t subscription)1325 static void mastodon_stream_handle_event(struct im_connection *ic, mastodon_evt_flags_t evt_type,
1326 json_value *parsed, mastodon_timeline_type_t subscription)
1327 {
1328 if (evt_type == MASTODON_EVT_UPDATE) {
1329 mastodon_stream_handle_update(ic, parsed, subscription);
1330 } else if (evt_type == MASTODON_EVT_NOTIFICATION) {
1331 mastodon_stream_handle_notification(ic, parsed, subscription);
1332 } else if (evt_type == MASTODON_EVT_DELETE) {
1333 mastodon_stream_handle_delete(ic, parsed);
1334 } else {
1335 mastodon_log(ic, "Ignoring event type %d", evt_type);
1336 }
1337 }
1338
1339 /**
1340 * When streaming, we also want to tag the events appropriately. This only affects updates, for now.
1341 */
mastodon_http_stream(struct http_request * req,mastodon_timeline_type_t subscription)1342 static void mastodon_http_stream(struct http_request *req, mastodon_timeline_type_t subscription)
1343 {
1344 struct im_connection *ic = req->data;
1345 struct mastodon_data *md = ic->proto_data;
1346 int len = 0;
1347 char *nl;
1348
1349 if (!g_slist_find(mastodon_connections, ic)) {
1350 return;
1351 }
1352
1353 if ((req->flags & HTTPC_EOF) || !req->reply_body) {
1354 md->streams = g_slist_remove (md->streams, req);
1355 imcb_error(ic, "Stream closed (%s)", req->status_string);
1356 imc_logout(ic, TRUE);
1357 return;
1358 }
1359
1360 /* It doesn't matter which stream sent us something. */
1361 ic->flags |= OPT_PONGED;
1362
1363 /*
1364 https://github.com/tootsuite/documentation/blob/master/Using-the-API/Streaming-API.md
1365 https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format
1366 */
1367
1368 if (req->reply_body[0] == ':' &&
1369 (nl = strchr(req->reply_body, '\n'))) {
1370 // found a comment such as the heartbeat ":thump\n"
1371 len = nl - req->reply_body + 1;
1372 goto end;
1373 } else if (!(nl = strstr(req->reply_body, "\n\n"))) {
1374 // wait until we have a complete event
1375 return;
1376 }
1377
1378 // include the two newlines at the end
1379 len = nl - req->reply_body + 2;
1380
1381 if (len > 0) {
1382 char *p;
1383 mastodon_evt_flags_t evt_type = MASTODON_EVT_UNKNOWN;
1384
1385 // assuming space after colon
1386 if (strncmp(req->reply_body, "event: ", 7) == 0) {
1387 p = req->reply_body + 7;
1388 if (strncmp(p, "update\n", 7) == 0) {
1389 evt_type = MASTODON_EVT_UPDATE;
1390 p += 7;
1391 } else if (strncmp(p, "notification\n", 13) == 0) {
1392 evt_type = MASTODON_EVT_NOTIFICATION;
1393 p += 13;
1394 } else if (strncmp(p, "delete\n", 7) == 0) {
1395 evt_type = MASTODON_EVT_DELETE;
1396 p += 7;
1397 }
1398 }
1399
1400 if (evt_type != MASTODON_EVT_UNKNOWN) {
1401
1402 GString *data = g_string_new("");
1403 char* q;
1404
1405 while (strncmp(p, "data: ", 6) == 0) {
1406 p += 6;
1407 q = strchr(p, '\n');
1408 p[q-p] = '\0';
1409 g_string_append(data, p);
1410 p = q + 1;
1411 }
1412
1413 json_value *parsed;
1414 if ((parsed = json_parse(data->str, data->len))) {
1415 mastodon_stream_handle_event(ic, evt_type, parsed, subscription);
1416 json_value_free(parsed);
1417 }
1418
1419 g_string_free(data, TRUE);
1420 }
1421 }
1422
1423 end:
1424 http_flush_bytes(req, len);
1425
1426 /* We might have multiple events */
1427 if (req->body_size > 0) {
1428 mastodon_http_stream(req, subscription);
1429 }
1430 }
1431
mastodon_http_stream_user(struct http_request * req)1432 static void mastodon_http_stream_user(struct http_request *req)
1433 {
1434 mastodon_http_stream(req, MT_HOME);
1435 }
1436
mastodon_http_stream_hashtag(struct http_request * req)1437 static void mastodon_http_stream_hashtag(struct http_request *req)
1438 {
1439 mastodon_http_stream(req, MT_HASHTAG);
1440 }
1441
mastodon_http_stream_local(struct http_request * req)1442 static void mastodon_http_stream_local(struct http_request *req)
1443 {
1444 mastodon_http_stream(req, MT_LOCAL);
1445 }
1446
mastodon_http_stream_federated(struct http_request * req)1447 static void mastodon_http_stream_federated(struct http_request *req)
1448 {
1449 mastodon_http_stream(req, MT_FEDERATED);
1450 }
1451
mastodon_http_stream_list(struct http_request * req)1452 static void mastodon_http_stream_list(struct http_request *req)
1453 {
1454 mastodon_http_stream(req, MT_LIST);
1455 }
1456
1457 /**
1458 * Make sure a request continues stream instead of closing.
1459 */
mastodon_stream(struct im_connection * ic,struct http_request * req)1460 void mastodon_stream(struct im_connection *ic, struct http_request *req)
1461 {
1462 struct mastodon_data *md = ic->proto_data;
1463 if (req) {
1464 req->flags |= HTTPC_STREAMING;
1465 md->streams = g_slist_prepend(md->streams, req);
1466 }
1467 }
1468
1469 /**
1470 * Open the user (home) timeline.
1471 */
mastodon_open_user_stream(struct im_connection * ic)1472 void mastodon_open_user_stream(struct im_connection *ic)
1473 {
1474 struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_USER_URL,
1475 mastodon_http_stream_user, ic, HTTP_GET, NULL, 0);
1476 mastodon_stream(ic, req);
1477 }
1478
1479 /**
1480 * Open a stream for a hashtag timeline and return the request.
1481 */
mastodon_open_hashtag_stream(struct im_connection * ic,char * hashtag)1482 struct http_request *mastodon_open_hashtag_stream(struct im_connection *ic, char *hashtag)
1483 {
1484 char *args[2] = {
1485 "tag", hashtag,
1486 };
1487
1488 struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_HASHTAG_URL,
1489 mastodon_http_stream_hashtag, ic, HTTP_GET, args, 2);
1490 mastodon_stream(ic, req);
1491 return req;
1492 }
1493
1494 /**
1495 * Part two of the first callback: now we have mc->id. Now we're good to go.
1496 */
mastodon_list_stream(struct im_connection * ic,struct mastodon_command * mc)1497 void mastodon_list_stream(struct im_connection *ic, struct mastodon_command *mc) {
1498 char *args[2] = {
1499 "list", g_strdup_printf("%" G_GINT64_FORMAT, mc->id),
1500 };
1501
1502 struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_LIST_URL,
1503 mastodon_http_stream_list, ic, HTTP_GET, args, 2);
1504 mastodon_stream(ic, req);
1505 /* We cannot return req here because this is a callback (as we had to figure out the list id before getting here).
1506 * This is why we must rely on the groupchat being part of mastodon_command (mc). */
1507 struct groupchat *c = (struct groupchat *) mc->data;
1508 c->data = req;
1509 }
1510
1511 /**
1512 * First callback to show the stream for a list. We need to parse the lists and find the one we're looking for, then
1513 * make our next request with the list id.
1514 */
mastodon_http_list_stream(struct http_request * req)1515 static void mastodon_http_list_stream(struct http_request *req)
1516 {
1517 mastodon_chained_list(req, mastodon_list_stream);
1518 }
1519
mastodon_open_unknown_list_stream(struct im_connection * ic,struct groupchat * c,char * title)1520 void mastodon_open_unknown_list_stream(struct im_connection *ic, struct groupchat *c, char *title)
1521 {
1522 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
1523 mc->ic = ic;
1524 mc->data = (gpointer *) c;
1525 mc->str = g_strdup(title);
1526 mastodon_with_named_list(ic, mc, mastodon_http_list_stream);
1527 }
1528
1529 /**
1530 * Open a stream for the local timeline and return the request.
1531 */
mastodon_open_local_stream(struct im_connection * ic)1532 struct http_request *mastodon_open_local_stream(struct im_connection *ic)
1533 {
1534 struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_LOCAL_URL,
1535 mastodon_http_stream_local, ic, HTTP_GET, NULL, 0);
1536 mastodon_stream(ic, req);
1537 return req;
1538 }
1539
1540 /**
1541 * Open a stream for the federated timeline and return the request.
1542 */
mastodon_open_federated_stream(struct im_connection * ic)1543 struct http_request *mastodon_open_federated_stream(struct im_connection *ic)
1544 {
1545 struct http_request *req = mastodon_http(ic, MASTODON_STREAMING_FEDERATED_URL,
1546 mastodon_http_stream_federated, ic, HTTP_GET, NULL, 0);
1547 mastodon_stream(ic, req);
1548 return req;
1549 }
1550
1551 /**
1552 * Look for the Link header and remember the URL to the next page of results. This will be used by the "more" command.
1553 */
mastodon_handle_header(struct http_request * req,mastodon_more_t more_type)1554 static void mastodon_handle_header(struct http_request *req, mastodon_more_t more_type)
1555 {
1556 struct im_connection *ic = req->data;
1557
1558 // remember the URL to fetch more if there is a header saying that there is more (URL in angled brackets)
1559 char *header = NULL;
1560 if ((header = get_rfc822_header(req->reply_headers, "Link", 0))) {
1561 char *url = NULL;
1562 gboolean next = FALSE;
1563 int i;
1564 for (i = 0; header[i]; i++) {
1565 if (header[i] == '<') {
1566 url = header + i + 1;
1567 } else if (url && header[i] == '>') {
1568 header[i] = 0;
1569 if (strncmp(header + i + 1, "; rel=\"next\"", 12) == 0) {
1570 next = TRUE;
1571 break;
1572 } else {
1573 url = NULL;
1574 }
1575 }
1576 }
1577
1578 struct mastodon_data *md = ic->proto_data;
1579 g_free(md->next_url); md->next_url = NULL;
1580 if (next) md->next_url = g_strdup(url);
1581 md->more_type = more_type;
1582
1583 g_free(header);
1584 }
1585 }
1586
1587 /**
1588 * Handle a request whose response contains nothing but statuses. Note that we expect req->data to be an im_connection,
1589 * not a mastodon_command (or NULL).
1590 */
mastodon_http_timeline(struct http_request * req,mastodon_timeline_type_t subscription)1591 static void mastodon_http_timeline(struct http_request *req, mastodon_timeline_type_t subscription)
1592 {
1593 struct im_connection *ic = req->data;
1594 if (!g_slist_find(mastodon_connections, ic)) {
1595 return;
1596 }
1597
1598 json_value *parsed;
1599 if (!(parsed = mastodon_parse_response(ic, req))) {
1600 /* ic would have been freed in imc_logout in this situation */
1601 ic = NULL;
1602 return;
1603 }
1604
1605 if (parsed->type != json_array || parsed->u.array.length == 0) {
1606 mastodon_log(ic, "No statuses found in this timeline.");
1607 goto finish;
1608 }
1609
1610 mastodon_handle_header(req, MASTODON_MORE_STATUSES);
1611
1612 // Show in reverse order!
1613 int i;
1614 for (i = parsed->u.array.length - 1; i >= 0 ; i--) {
1615 json_value *node = parsed->u.array.values[i];
1616 struct mastodon_status *ms = mastodon_xt_get_status(node, ic);
1617 if (ms) {
1618 ms->subscription = subscription;
1619 mastodon_status_show(ic, ms);
1620 ms_free(ms);
1621 }
1622 }
1623 finish:
1624 json_value_free(parsed);
1625 }
1626
mastodon_http_hashtag_timeline(struct http_request * req)1627 static void mastodon_http_hashtag_timeline(struct http_request *req)
1628 {
1629 mastodon_http_timeline(req, MT_HASHTAG);
1630 }
1631
mastodon_hashtag_timeline(struct im_connection * ic,char * hashtag)1632 void mastodon_hashtag_timeline(struct im_connection *ic, char *hashtag)
1633 {
1634 char *url = g_strdup_printf(MASTODON_HASHTAG_TIMELINE_URL, hashtag);
1635 mastodon_http(ic, url, mastodon_http_hashtag_timeline, ic, HTTP_GET, NULL, 0);
1636 g_free(url);
1637 }
1638
mastodon_http_home_timeline(struct http_request * req)1639 static void mastodon_http_home_timeline(struct http_request *req)
1640 {
1641 mastodon_http_timeline(req, MT_HOME);
1642 }
1643
mastodon_home_timeline(struct im_connection * ic)1644 void mastodon_home_timeline(struct im_connection *ic)
1645 {
1646 mastodon_http(ic, MASTODON_HOME_TIMELINE_URL, mastodon_http_home_timeline, ic, HTTP_GET, NULL, 0);
1647 }
1648
mastodon_http_local_timeline(struct http_request * req)1649 static void mastodon_http_local_timeline(struct http_request *req)
1650 {
1651 mastodon_http_timeline(req, MT_LOCAL);
1652 }
1653
mastodon_local_timeline(struct im_connection * ic)1654 void mastodon_local_timeline(struct im_connection *ic)
1655 {
1656 char *args[2] = {
1657 "local", "1",
1658 };
1659
1660 mastodon_http(ic, MASTODON_PUBLIC_TIMELINE_URL, mastodon_http_local_timeline, ic, HTTP_GET, args, 2);
1661 }
1662
mastodon_http_federated_timeline(struct http_request * req)1663 static void mastodon_http_federated_timeline(struct http_request *req)
1664 {
1665 mastodon_http_timeline(req, MT_FEDERATED);
1666 }
1667
mastodon_federated_timeline(struct im_connection * ic)1668 void mastodon_federated_timeline(struct im_connection *ic)
1669 {
1670 mastodon_http(ic, MASTODON_PUBLIC_TIMELINE_URL, mastodon_http_federated_timeline, ic, HTTP_GET, NULL, 0);
1671 }
1672
1673
1674 /**
1675 * Second callback to show the timeline for a list. We finally got a list of statuses.
1676 */
mastodon_http_list_timeline2(struct http_request * req)1677 static void mastodon_http_list_timeline2(struct http_request *req)
1678 {
1679 /* We have used a mastodon_command (mc) all this time, but now it's time to forget about it and use an im_connection
1680 * (ic) instead. */
1681 struct mastodon_command *mc = req->data;
1682 struct im_connection *ic = mc->ic;
1683 req->data = ic;
1684 mc_free(mc);
1685 mastodon_http_timeline(req, MT_LIST);
1686 }
1687
1688 /**
1689 * Part two of the first callback to show the timeline for a list. In mc->id we have our list id.
1690 */
mastodon_list_timeline(struct im_connection * ic,struct mastodon_command * mc)1691 void mastodon_list_timeline(struct im_connection *ic, struct mastodon_command *mc) {
1692 char *url = g_strdup_printf(MASTODON_LIST_TIMELINE_URL, mc->id);
1693 mastodon_http(ic, url, mastodon_http_list_timeline2, mc, HTTP_GET, NULL, 0);
1694 g_free(url);
1695 }
1696
1697 /**
1698 * First callback to show the timeline for a list. We need to parse the lists and find the one we're looking for, then
1699 * make our next request with the list id.
1700 */
mastodon_http_list_timeline(struct http_request * req)1701 static void mastodon_http_list_timeline(struct http_request *req)
1702 {
1703 mastodon_chained_list(req, mastodon_list_timeline);
1704 }
1705
1706 /**
1707 * Timeline for a named list. This requires two callbacks: the first to find the list id, the second one to do the
1708 * actual work.
1709 */
mastodon_unknown_list_timeline(struct im_connection * ic,char * title)1710 void mastodon_unknown_list_timeline(struct im_connection *ic, char *title)
1711 {
1712 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
1713 mc->ic = ic;
1714 mc->str = g_strdup(title);
1715 mastodon_with_named_list(ic, mc, mastodon_http_list_timeline);
1716 }
1717
1718 /**
1719 * Call this one after receiving timeline/notifications. Show to user
1720 * once we have both.
1721 */
mastodon_flush_timeline(struct im_connection * ic)1722 void mastodon_flush_timeline(struct im_connection *ic)
1723 {
1724 struct mastodon_data *md = ic->proto_data;
1725 struct mastodon_list *home_timeline;
1726 struct mastodon_list *notifications;
1727 GSList *output = NULL;
1728 GSList *l;
1729
1730 if (md == NULL) {
1731 return;
1732 }
1733
1734 imcb_connected(ic);
1735
1736 /* Wait until we have all the data we need. */
1737 if (!(md->flags & MASTODON_GOT_TIMELINE) ||
1738 !(md->flags & MASTODON_GOT_NOTIFICATIONS) ||
1739 !(md->flags & MASTODON_GOT_FILTERS)) {
1740 return;
1741 }
1742
1743 home_timeline = md->home_timeline_obj;
1744 notifications = md->notifications_obj;
1745
1746 if (home_timeline && home_timeline->list) {
1747 for (l = home_timeline->list; l; l = g_slist_next(l)) {
1748 output = g_slist_insert_sorted(output, l->data, mastodon_compare_elements);
1749 }
1750 }
1751
1752 if (notifications && notifications->list) {
1753 for (l = notifications->list; l; l = g_slist_next(l)) {
1754 // Skip notifications older than the earliest entry in the timeline.
1755 struct mastodon_status *ms = mastodon_notification_to_status((struct mastodon_notification *) l->data);
1756 if (output && mastodon_compare_elements(ms, output->data) < 0) {
1757 continue;
1758 }
1759
1760 output = g_slist_insert_sorted(output, ms, mastodon_compare_elements);
1761 }
1762 }
1763
1764 while (output) {
1765 struct mastodon_status *ms = output->data;
1766 mastodon_status_show(ic, ms);
1767 output = g_slist_remove(output, ms);
1768 }
1769
1770 ml_free(home_timeline);
1771 ml_free(notifications);
1772 g_slist_free(output);
1773
1774 md->flags &= ~(MASTODON_GOT_TIMELINE | MASTODON_GOT_NOTIFICATIONS | MASTODON_GOT_FILTERS);
1775 md->home_timeline_obj = md->notifications_obj = NULL;
1776 }
1777
1778 /**
1779 * Callback for getting the home timeline. This runs in parallel to
1780 * getting the notifications.
1781 */
mastodon_http_get_home_timeline(struct http_request * req)1782 static void mastodon_http_get_home_timeline(struct http_request *req)
1783 {
1784 struct im_connection *ic = req->data;
1785 if (!g_slist_find(mastodon_connections, ic)) {
1786 return;
1787 }
1788
1789 struct mastodon_data *md = ic->proto_data;
1790
1791 json_value *parsed;
1792 if (!(parsed = mastodon_parse_response(ic, req))) {
1793 /* ic would have been freed in imc_logout in this situation */
1794 ic = NULL;
1795 return;
1796 }
1797
1798 struct mastodon_list *ml = g_new0(struct mastodon_list, 1);
1799
1800 mastodon_xt_get_status_list(ic, parsed, ml);
1801 json_value_free(parsed);
1802
1803 md->home_timeline_obj = ml;
1804 md->flags |= MASTODON_GOT_TIMELINE;
1805 mastodon_flush_timeline(ic);
1806 }
1807
1808 /**
1809 * Callback for getting the notifications. This runs in parallel to
1810 * getting the home timeline.
1811 */
mastodon_http_get_notifications(struct http_request * req)1812 static void mastodon_http_get_notifications(struct http_request *req)
1813 {
1814 struct im_connection *ic = req->data;
1815 if (!g_slist_find(mastodon_connections, ic)) {
1816 return;
1817 }
1818
1819 struct mastodon_data *md = ic->proto_data;
1820
1821 json_value *parsed;
1822 if (!(parsed = mastodon_parse_response(ic, req))) {
1823 /* ic would have been freed in imc_logout in this situation */
1824 ic = NULL;
1825 return;
1826 }
1827
1828 struct mastodon_list *ml = g_new0(struct mastodon_list, 1);
1829
1830 mastodon_xt_get_notification_list(ic, parsed, ml);
1831 json_value_free(parsed);
1832
1833 md->notifications_obj = ml;
1834 md->flags |= MASTODON_GOT_NOTIFICATIONS;
1835 mastodon_flush_timeline(ic);
1836 }
1837
1838 /**
1839 * See mastodon_initial_timeline.
1840 */
mastodon_get_home_timeline(struct im_connection * ic)1841 static void mastodon_get_home_timeline(struct im_connection *ic)
1842 {
1843 struct mastodon_data *md = ic->proto_data;
1844
1845 ml_free(md->home_timeline_obj);
1846 md->home_timeline_obj = NULL;
1847 md->flags &= ~MASTODON_GOT_TIMELINE;
1848
1849 mastodon_http(ic, MASTODON_HOME_TIMELINE_URL, mastodon_http_get_home_timeline, ic, HTTP_GET, NULL, 0);
1850 }
1851
1852 /**
1853 * See mastodon_initial_timeline.
1854 */
mastodon_get_notifications(struct im_connection * ic)1855 static void mastodon_get_notifications(struct im_connection *ic)
1856 {
1857 struct mastodon_data *md = ic->proto_data;
1858
1859 ml_free(md->notifications_obj);
1860 md->notifications_obj = NULL;
1861 md->flags &= ~MASTODON_GOT_NOTIFICATIONS;
1862
1863 mastodon_http(ic, MASTODON_NOTIFICATIONS_URL, mastodon_http_get_notifications, ic, HTTP_GET, NULL, 0);
1864 }
1865
1866 static void mastodon_get_filters(struct im_connection *ic);
1867
1868 /**
1869 * Get the initial timeline. This consists of three things: the home timeline, notifications, and filters. During normal
1870 * use, the timeline and the notifications are provided via the Streaming API. However, when we connect to an instance
1871 * we want to load the home timeline and notifications and sort them in a meaningful way. We use flags:
1872 * MASTODON_GOT_TIMELINE to indicate that we now have home timeline, MASTODON_GOT_NOTIFICATIONS to indicate that we now
1873 * have notifications, and MASTODON_GOT_FILTERS to indicate that we now have filters . All callbacks will attempt to
1874 * flush the initial timeline, but this will only succeed if all three flags are set.
1875 */
mastodon_initial_timeline(struct im_connection * ic)1876 void mastodon_initial_timeline(struct im_connection *ic)
1877 {
1878 imcb_log(ic, "Getting home timeline");
1879 mastodon_get_home_timeline(ic);
1880 mastodon_get_notifications(ic);
1881 mastodon_get_filters(ic);
1882 return;
1883 }
1884
1885 /**
1886 * Callback for getting notifications manually.
1887 */
mastodon_http_notifications(struct http_request * req)1888 static void mastodon_http_notifications(struct http_request *req)
1889 {
1890 struct im_connection *ic = req->data;
1891 if (!g_slist_find(mastodon_connections, ic)) {
1892 return;
1893 }
1894
1895 json_value *parsed;
1896 if (!(parsed = mastodon_parse_response(ic, req))) {
1897 /* ic would have been freed in imc_logout in this situation */
1898 ic = NULL;
1899 return;
1900 }
1901
1902 if (parsed->type != json_array || parsed->u.array.length == 0) {
1903 mastodon_log(ic, "No notifications found.");
1904 goto finish;
1905 }
1906
1907 mastodon_handle_header(req, MASTODON_MORE_NOTIFICATIONS);
1908
1909 // Show in reverse order!
1910 int i;
1911 for (i = parsed->u.array.length - 1; i >= 0 ; i--) {
1912 json_value *node = parsed->u.array.values[i];
1913 struct mastodon_notification *mn = mastodon_xt_get_notification(node, ic);
1914 if (mn) {
1915 mastodon_notification_show(ic, mn);
1916 mn_free(mn);
1917 }
1918 }
1919 finish:
1920 json_value_free(parsed);
1921 }
1922
1923 /**
1924 * Notifications are usually shown by the Streaming API, and when showing the initial timeline after connecting. In
1925 * order to allow manual review (and going through past notifications using the more command, we need yet another way to
1926 * get notifications.
1927 */
mastodon_notifications(struct im_connection * ic)1928 void mastodon_notifications(struct im_connection *ic)
1929 {
1930 mastodon_http(ic, MASTODON_NOTIFICATIONS_URL, mastodon_http_notifications, ic, HTTP_GET, NULL, 0);
1931 }
1932
mastodon_default_visibility(struct im_connection * ic)1933 mastodon_visibility_t mastodon_default_visibility(struct im_connection *ic)
1934 {
1935 return mastodon_parse_visibility(set_getstr(&ic->acc->set, "visibility"));
1936 }
1937
mastodon_visibility(mastodon_visibility_t visibility)1938 char *mastodon_visibility(mastodon_visibility_t visibility)
1939 {
1940 switch (visibility) {
1941 case MV_UNKNOWN:
1942 case MV_PUBLIC:
1943 return "public";
1944 case MV_UNLISTED:
1945 return "unlisted";
1946 case MV_PRIVATE:
1947 return "private";
1948 case MV_DIRECT:
1949 return "direct";
1950 }
1951 g_assert(FALSE); // should not happen
1952 return NULL;
1953 }
1954
1955 /**
1956 * Generic callback to use after sending a POST request to mastodon when the reply doesn't have any information we need.
1957 * All we care about are errors. If got here, there was no error. If you want to tell the user that everything went
1958 * fine, call mastodon_http_callback_and_ack instead. This command also stores some information for later use.
1959 */
mastodon_http_callback(struct http_request * req)1960 static void mastodon_http_callback(struct http_request *req)
1961 {
1962 struct mastodon_command *mc = req->data;
1963 struct im_connection *ic = mc->ic;
1964 if (!g_slist_find(mastodon_connections, ic)) {
1965 return;
1966 }
1967
1968 json_value *parsed;
1969 if (!(parsed = mastodon_parse_response(ic, req))) {
1970 /* ic would have been freed in imc_logout in this situation */
1971 ic = NULL;
1972 return;
1973 }
1974
1975 /* Store stuff in the undo/redo stack. */
1976 struct mastodon_data *md = ic->proto_data;
1977 md->last_id = 0;
1978
1979 struct mastodon_status *ms;
1980
1981 switch (mc->command) {
1982 case MC_UNKNOWN:
1983 break;
1984 case MC_POST:
1985 ms = mastodon_xt_get_status(parsed, ic);
1986 gint64 id = set_getint(&ic->acc->set, "account_id");
1987 if (ms && ms->id && ms->account->id == id) {
1988 /* we posted this status */
1989 md->last_id = ms->id;
1990 md->last_visibility = ms->visibility;
1991 g_free(md->last_spoiler_text);
1992 md->last_spoiler_text = ms->spoiler_text; // adopt
1993 ms->spoiler_text = NULL;
1994 g_slist_free_full(md->mentions, (GDestroyNotify) ma_free);
1995 md->mentions = ms->mentions; // adopt
1996 ms->mentions = NULL;
1997
1998 if(md->undo_type == MASTODON_NEW) {
1999
2000 GString *todo = g_string_new (NULL);
2001 char *undo = g_strdup_printf("delete %" G_GUINT64_FORMAT, ms->id);
2002
2003 /* At this point redoing the reply no longer has the reference to the toot we are replying to (which
2004 * only works by looking it up in the mastodon_user_data (mud) or the md->log). That is why we need
2005 * to add spoiler_text and visibility to the todo item on our redo list. */
2006
2007 if (ms->spoiler_text) {
2008 g_string_append_printf(todo, "cw %s" FS, ms->spoiler_text);
2009 } else {
2010 g_string_append(todo, "cw" FS);
2011 }
2012
2013 if (mastodon_default_visibility(ic) != ms->visibility) {
2014 g_string_append_printf(todo, "visibility %s" FS, mastodon_visibility(ms->visibility));
2015 } else {
2016 g_string_append(todo, "visibility" FS);
2017 }
2018
2019 if (ms->reply_to) {
2020 g_string_append_printf(todo, "reply %" G_GUINT64_FORMAT " ", ms->reply_to);
2021 } else {
2022 g_string_append(todo, "post ");
2023 }
2024 g_string_append(todo, ms->content);
2025
2026 mastodon_do(ic, todo->str, undo);
2027
2028 g_string_free(todo, FALSE); /* data is kept by mastodon_do! */
2029
2030 } else {
2031 char *s = g_strdup_printf("delete %" G_GUINT64_FORMAT, ms->id);
2032 mastodon_do_update(ic, s);
2033 g_free(s);
2034 }
2035 }
2036 break;
2037 case MC_FOLLOW:
2038 case MC_UNFOLLOW:
2039 case MC_BLOCK:
2040 case MC_UNBLOCK:
2041 case MC_FAVOURITE:
2042 case MC_UNFAVOURITE:
2043 case MC_PIN:
2044 case MC_UNPIN:
2045 case MC_ACCOUNT_MUTE:
2046 case MC_ACCOUNT_UNMUTE:
2047 case MC_STATUS_MUTE:
2048 case MC_STATUS_UNMUTE:
2049 case MC_BOOST:
2050 case MC_UNBOOST:
2051 case MC_LIST_CREATE:
2052 case MC_LIST_DELETE:
2053 case MC_LIST_ADD_ACCOUNT:
2054 case MC_LIST_REMOVE_ACCOUNT:
2055 case MC_FILTER_CREATE:
2056 case MC_FILTER_DELETE:
2057 case MC_DELETE:
2058 md->last_id = 0;
2059 mastodon_do(ic, mc->redo, mc->undo);
2060 // adopting these strings: do not free them at the end
2061 mc->redo = mc->undo = 0;
2062 break;
2063 }
2064 mc_free(mc);
2065 json_value_free(parsed);
2066 }
2067
2068 /**
2069 * Call the generic callback function and print an acknowledgement for the user.
2070 * Commands should use mastodon_post instead.
2071 */
mastodon_http_callback_and_ack(struct http_request * req)2072 static void mastodon_http_callback_and_ack(struct http_request *req)
2073 {
2074 struct mastodon_command *mc = req->data;
2075 struct im_connection *ic = mc->ic;
2076 mastodon_http_callback(req); // this frees mc
2077
2078 if (req->status_code == 200) {
2079 mastodon_log(ic, "Command processed successfully");
2080 }
2081 }
2082
2083 /**
2084 * Return a static string n spaces long. No deallocation needed.
2085 */
indent(int n)2086 static char *indent(int n)
2087 {
2088 char *spaces = " ";
2089 int len = 10;
2090 return n > len ? spaces : spaces + len - n;
2091 }
2092
2093 /**
2094 * Return a static yes or no string. No deallocation needed.
2095 */
yes_or_no(int bool)2096 static char *yes_or_no(int bool)
2097 {
2098 return bool ? "yes" : "no";
2099 }
2100
2101 /**
2102 * Log a JSON array out to the channel. When you call it, use a
2103 * prefix of 0. Recursive calls will then indent nested objects.
2104 */
mastodon_log_array(struct im_connection * ic,json_value * node,int prefix)2105 static void mastodon_log_array(struct im_connection *ic, json_value *node, int prefix)
2106 {
2107 int i;
2108 for (i = 0; i < node->u.array.length; i++) {
2109 json_value *v = node->u.array.values[i];
2110 char *s;
2111 switch (v->type) {
2112 case json_object:
2113 if (v->u.object.values == 0) {
2114 mastodon_log(ic, "%s{}", indent(prefix));
2115 break;
2116 }
2117 mastodon_log(ic, "%s{", indent(prefix));
2118 mastodon_log_object (ic, v, prefix + 1);
2119 mastodon_log(ic, "%s}", indent(prefix));
2120 break;
2121 case json_array:
2122 if (v->u.array.length == 0) {
2123 mastodon_log(ic, "%s[]", indent(prefix));
2124 break;
2125 }
2126 mastodon_log(ic, "%s[", indent(prefix));
2127 int i;
2128 for (i = 0; i < v->u.array.length; i++) {
2129 mastodon_log_object (ic, node->u.array.values[i], prefix + 1);
2130 }
2131 mastodon_log(ic, "%s]", indent(prefix));
2132 break;
2133 case json_string:
2134 s = g_strdup(v->u.string.ptr);
2135 mastodon_strip_html(s);
2136 mastodon_log(ic, "%s%s", indent(prefix), s);
2137 g_free(s);
2138 break;
2139 case json_double:
2140 mastodon_log(ic, "%s%f", indent(prefix), v->u.dbl);
2141 break;
2142 case json_integer:
2143 mastodon_log(ic, "%s%d", indent(prefix), v->u.boolean);
2144 break;
2145 case json_boolean:
2146 mastodon_log(ic, "%s%s: %s", indent(prefix), yes_or_no(v->u.boolean));
2147 break;
2148 case json_null:
2149 mastodon_log(ic, "%snull", indent(prefix));
2150 break;
2151 case json_none:
2152 mastodon_log(ic, "%snone", indent(prefix));
2153 break;
2154 }
2155 }
2156 }
2157
2158 /**
2159 * Log a JSON object out to the channel. When you call it, use a
2160 * prefix of 0. Recursive calls will then indent nested objects.
2161 */
mastodon_log_object(struct im_connection * ic,json_value * node,int prefix)2162 static void mastodon_log_object(struct im_connection *ic, json_value *node, int prefix)
2163 {
2164 char *s;
2165 JSON_O_FOREACH(node, k, v) {
2166 switch (v->type) {
2167 case json_object:
2168 if (v->u.object.values == 0) {
2169 mastodon_log(ic, "%s%s: {}", indent(prefix), k);
2170 break;
2171 }
2172 mastodon_log(ic, "%s%s: {", indent(prefix), k);
2173 mastodon_log_object (ic, v, prefix + 1);
2174 mastodon_log(ic, "%s}", indent(prefix));
2175 break;
2176 case json_array:
2177 if (v->u.array.length == 0) {
2178 mastodon_log(ic, "%s%s: []", indent(prefix), k);
2179 break;
2180 }
2181 mastodon_log(ic, "%s%s: [", indent(prefix), k);
2182 mastodon_log_array(ic, v, prefix + 1);
2183 mastodon_log(ic, "%s]", indent(prefix));
2184 break;
2185 case json_string:
2186 s = g_strdup(v->u.string.ptr);
2187 mastodon_strip_html(s);
2188 mastodon_log(ic, "%s%s: %s", indent(prefix), k, s);
2189 g_free(s);
2190 break;
2191 case json_double:
2192 mastodon_log(ic, "%s%s: %f", indent(prefix), k, v->u.dbl);
2193 break;
2194 case json_integer:
2195 mastodon_log(ic, "%s%s: %d", indent(prefix), k, v->u.boolean);
2196 break;
2197 case json_boolean:
2198 mastodon_log(ic, "%s%s: %s", indent(prefix), k, yes_or_no(v->u.boolean));
2199 break;
2200 case json_null:
2201 mastodon_log(ic, "%s%s: null", indent(prefix), k);
2202 break;
2203 case json_none:
2204 mastodon_log(ic, "%s%s: unknown type", indent(prefix), k);
2205 break;
2206 }
2207 }
2208 }
2209
2210 /**
2211 * Generic callback which simply logs the JSON response to the
2212 * channel.
2213 */
mastodon_http_log_all(struct http_request * req)2214 static void mastodon_http_log_all(struct http_request *req)
2215 {
2216 struct im_connection *ic = req->data;
2217 if (!g_slist_find(mastodon_connections, ic)) {
2218 return;
2219 }
2220
2221 json_value *parsed;
2222 if (!(parsed = mastodon_parse_response(ic, req))) {
2223 /* ic would have been freed in imc_logout in this situation */
2224 ic = NULL;
2225 return;
2226 }
2227
2228 if (parsed->type == json_object) {
2229 mastodon_log_object(ic, parsed, 0);
2230 } else if (parsed->type == json_array) {
2231 mastodon_log_array(ic, parsed, 0);
2232 } else {
2233 mastodon_log(ic, "Sadly, the response to this request is not a JSON object or array.");
2234 }
2235
2236 json_value_free(parsed);
2237 }
2238
2239 /**
2240 * Function to POST a new status to mastodon.
2241 */
mastodon_post_status(struct im_connection * ic,char * msg,guint64 in_reply_to,mastodon_visibility_t visibility,char * spoiler_text)2242 void mastodon_post_status(struct im_connection *ic, char *msg, guint64 in_reply_to, mastodon_visibility_t visibility, char *spoiler_text)
2243 {
2244 char *args[8] = {
2245 "status", msg,
2246 "visibility", mastodon_visibility(visibility),
2247 "spoiler_text", spoiler_text,
2248 "in_reply_to_id", g_strdup_printf("%" G_GUINT64_FORMAT, in_reply_to)
2249 };
2250 int count = 8;
2251
2252 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
2253 mc->ic = ic;
2254
2255 mc->command = MC_POST;
2256
2257 if (!in_reply_to) {
2258 count -= 2;
2259 }
2260 if (!spoiler_text) {
2261 count -= 2;
2262 if (in_reply_to) {
2263 args[4] = args[6];
2264 args[5] = args[7]; // we have 2 pointers to the in_reply_to_id string now,
2265 }
2266 }
2267 // No need to acknowledge the processing of a post: we will get notified.
2268 mastodon_http(ic, MASTODON_STATUS_POST_URL, mastodon_http_callback, mc, HTTP_POST,
2269 args, count);
2270
2271 g_free(args[7]); // but we only free one of them!
2272 }
2273
2274 /**
2275 * Generic POST request taking a numeric ID. The format string must contain one placeholder for the ID, like
2276 * "/accounts/%" G_GINT64_FORMAT "/mute".
2277 */
mastodon_post(struct im_connection * ic,char * format,mastodon_command_type_t command,guint64 id)2278 void mastodon_post(struct im_connection *ic, char *format, mastodon_command_type_t command, guint64 id)
2279 {
2280 struct mastodon_data *md = ic->proto_data;
2281 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
2282 mc->ic = ic;
2283
2284 if (md->undo_type == MASTODON_NEW) {
2285 mc->command = command;
2286
2287 switch (command) {
2288 case MC_UNKNOWN:
2289 case MC_POST:
2290 case MC_DELETE:
2291 case MC_LIST_CREATE:
2292 case MC_LIST_DELETE:
2293 case MC_LIST_ADD_ACCOUNT:
2294 case MC_LIST_REMOVE_ACCOUNT:
2295 case MC_FILTER_CREATE:
2296 case MC_FILTER_DELETE:
2297 /* These commands should not be calling mastodon_post. Instead, call mastodon_post_status or whatever else
2298 * is required. */
2299 break;
2300 case MC_FOLLOW:
2301 mc->redo = g_strdup_printf("follow %" G_GUINT64_FORMAT, id);
2302 mc->undo = g_strdup_printf("unfollow %" G_GUINT64_FORMAT, id);
2303 break;
2304 case MC_UNFOLLOW:
2305 mc->redo = g_strdup_printf("unfollow %" G_GUINT64_FORMAT, id);
2306 mc->undo = g_strdup_printf("follow %" G_GUINT64_FORMAT, id);
2307 break;
2308 case MC_BLOCK:
2309 mc->redo = g_strdup_printf("block %" G_GUINT64_FORMAT, id);
2310 mc->undo = g_strdup_printf("unblock %" G_GUINT64_FORMAT, id);
2311 break;
2312 case MC_UNBLOCK:
2313 mc->redo = g_strdup_printf("unblock %" G_GUINT64_FORMAT, id);
2314 mc->undo = g_strdup_printf("block %" G_GUINT64_FORMAT, id);
2315 break;
2316 case MC_FAVOURITE:
2317 mc->redo = g_strdup_printf("favourite %" G_GUINT64_FORMAT, id);
2318 mc->undo = g_strdup_printf("unfavourite %" G_GUINT64_FORMAT, id);
2319 break;
2320 case MC_UNFAVOURITE:
2321 mc->redo = g_strdup_printf("unfavourite %" G_GUINT64_FORMAT, id);
2322 mc->undo = g_strdup_printf("favourite %" G_GUINT64_FORMAT, id);
2323 break;
2324 case MC_PIN:
2325 mc->redo = g_strdup_printf("pin %" G_GUINT64_FORMAT, id);
2326 mc->undo = g_strdup_printf("unpin %" G_GUINT64_FORMAT, id);
2327 break;
2328 case MC_UNPIN:
2329 mc->redo = g_strdup_printf("unpin %" G_GUINT64_FORMAT, id);
2330 mc->undo = g_strdup_printf("pin %" G_GUINT64_FORMAT, id);
2331 break;
2332 case MC_ACCOUNT_MUTE:
2333 mc->redo = g_strdup_printf("mute user %" G_GUINT64_FORMAT, id);
2334 mc->undo = g_strdup_printf("unmute user %" G_GUINT64_FORMAT, id);
2335 break;
2336 case MC_ACCOUNT_UNMUTE:
2337 mc->redo = g_strdup_printf("unmute user %" G_GUINT64_FORMAT, id);
2338 mc->undo = g_strdup_printf("mute user %" G_GUINT64_FORMAT, id);
2339 break;
2340 case MC_STATUS_MUTE:
2341 mc->redo = g_strdup_printf("mute %" G_GUINT64_FORMAT, id);
2342 mc->undo = g_strdup_printf("unmute %" G_GUINT64_FORMAT, id);
2343 break;
2344 case MC_STATUS_UNMUTE:
2345 mc->redo = g_strdup_printf("unmute %" G_GUINT64_FORMAT, id);
2346 mc->undo = g_strdup_printf("mute %" G_GUINT64_FORMAT, id);
2347 break;
2348 case MC_BOOST:
2349 mc->redo = g_strdup_printf("boost %" G_GUINT64_FORMAT, id);
2350 mc->undo = g_strdup_printf("unboost %" G_GUINT64_FORMAT, id);
2351 break;
2352 case MC_UNBOOST:
2353 mc->redo = g_strdup_printf("unboost %" G_GUINT64_FORMAT, id);
2354 mc->undo = g_strdup_printf("boost %" G_GUINT64_FORMAT, id);
2355 break;
2356 }
2357 }
2358
2359 char *url = g_strdup_printf(format, id);
2360 mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_POST, NULL, 0);
2361 g_free(url);
2362 }
2363
mastodon_http_status_delete(struct http_request * req)2364 void mastodon_http_status_delete(struct http_request *req)
2365 {
2366 struct mastodon_command *mc = req->data;
2367 struct im_connection *ic = mc->ic;
2368 if (!g_slist_find(mastodon_connections, ic)) {
2369 return;
2370 }
2371
2372 json_value *parsed;
2373 if (!(parsed = mastodon_parse_response(ic, req))) {
2374 /* ic would have been freed in imc_logout in this situation */
2375 ic = NULL;
2376 return;
2377 }
2378
2379 /* Maintain undo/redo list. */
2380 struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic);
2381 struct mastodon_data *md = ic->proto_data;
2382 gint64 id = set_getint(&ic->acc->set, "account_id");
2383 if (ms && ms->id && ms->account->id == id) {
2384 /* we deleted our own status */
2385 md->last_id = ms->id;
2386
2387 mc->redo = g_strdup_printf("delete %" G_GUINT64_FORMAT, ms->id);
2388 GString *todo = g_string_new (NULL);
2389
2390 if (ms->spoiler_text) {
2391 g_string_append_printf(todo, "cw %s" FS, ms->spoiler_text);
2392 } else {
2393 g_string_append(todo, "cw" FS);
2394 }
2395
2396 if (mastodon_default_visibility(ic) != ms->visibility) {
2397 g_string_append_printf(todo, "visibility %s" FS, mastodon_visibility(ms->visibility));
2398 } else {
2399 g_string_append(todo, "visibility" FS);
2400 }
2401
2402 if (ms->reply_to) {
2403 g_string_append_printf(todo, "reply %" G_GUINT64_FORMAT " ", ms->reply_to);
2404 } else {
2405 g_string_append(todo, "post ");
2406 }
2407 g_string_append(todo, ms->content);
2408
2409 mc->undo = todo->str;
2410 g_string_free(todo, FALSE); /* data is kept by mc! */
2411 }
2412
2413 char *url = g_strdup_printf(MASTODON_STATUS_URL, mc->id);
2414 // No need to acknowledge the processing of the delete: we will get notified.
2415 mastodon_http(ic, url, mastodon_http_callback, mc, HTTP_DELETE, NULL, 0);
2416 g_free(url);
2417 }
2418
2419 /**
2420 * Helper for all functions that need to act on a status before they
2421 * can do anything else. Provide a function to use as a callback. This
2422 * callback will get the status back and will need to call
2423 * mastodon_xt_get_status and do something with it.
2424 */
mastodon_with_status(struct mastodon_command * mc,guint64 id,http_input_function func)2425 void mastodon_with_status(struct mastodon_command *mc, guint64 id, http_input_function func)
2426 {
2427 char *url = g_strdup_printf(MASTODON_STATUS_URL, id);
2428 mastodon_http(mc->ic, url, func, mc, HTTP_GET, NULL, 0);
2429 g_free(url);
2430 }
2431
2432 /**
2433 * Delete a status. In order to ensure that we can undo and redo this,
2434 * fetch the status to be deleted before actually deleting it.
2435 */
mastodon_status_delete(struct im_connection * ic,guint64 id)2436 void mastodon_status_delete(struct im_connection *ic, guint64 id)
2437 {
2438 struct mastodon_data *md = ic->proto_data;
2439 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
2440 mc->ic = ic;
2441
2442 if (md->undo_type == MASTODON_NEW) {
2443 mc->command = MC_DELETE;
2444 mc->id = id;
2445 mastodon_with_status(mc, id, mastodon_http_status_delete);
2446 } else {
2447 // Shortcut
2448 char *url = g_strdup_printf(MASTODON_STATUS_URL, id);
2449 // No need to acknowledge the processing of the delete: we will get notified.
2450 mastodon_http(ic, url, mastodon_http_callback, mc, HTTP_DELETE, NULL, 0);
2451 g_free(url);
2452 }
2453 }
2454
2455 /**
2456 * Callback for reporting a user for sending spam.
2457 */
mastodon_http_report(struct http_request * req)2458 void mastodon_http_report(struct http_request *req)
2459 {
2460 struct mastodon_report *mr = req->data;
2461 struct im_connection *ic = mr->ic;
2462 if (!g_slist_find(mastodon_connections, ic)) {
2463 goto finally;
2464 }
2465
2466 json_value *parsed;
2467 if (!(parsed = mastodon_parse_response(ic, req))) {
2468 /* ic would have been freed in imc_logout in this situation */
2469 ic = NULL;
2470 goto finally;
2471 }
2472
2473 struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic);
2474 if (ms) {
2475 mr->account_id = ms->account->id;
2476 ms_free(ms);
2477 } else {
2478 mastodon_log(ic, "Error: could not fetch toot to report.");
2479 goto finish;
2480 }
2481
2482 char *args[6] = {
2483 "account_id", g_strdup_printf("%" G_GUINT64_FORMAT, mr->account_id),
2484 "status_ids", g_strdup_printf("%" G_GUINT64_FORMAT, mr->status_id), // API allows an array, here
2485 "comment", mr->comment,
2486 };
2487
2488 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
2489 mc->ic = ic;
2490 mastodon_http(ic, MASTODON_REPORT_URL, mastodon_http_callback_and_ack, mc, HTTP_POST, args, 6);
2491
2492 g_free(args[1]);
2493 g_free(args[3]);
2494 finish:
2495 ms_free(ms);
2496 json_value_free(parsed);
2497 finally:
2498 // The report structure was created by mastodon_report and has
2499 // to be freed under all circumstances.
2500 mr_free(mr);
2501 }
2502
2503 /**
2504 * Report a user. Since all we have is the id of the offending status,
2505 * we need to retrieve the status, first.
2506 */
mastodon_report(struct im_connection * ic,guint64 id,char * comment)2507 void mastodon_report(struct im_connection *ic, guint64 id, char *comment)
2508 {
2509 char *url = g_strdup_printf(MASTODON_STATUS_URL, id);
2510 struct mastodon_report *mr = g_new0(struct mastodon_report, 1);
2511
2512 mr->ic = ic;
2513 mr->status_id = id;
2514 mr->comment = g_strdup(comment);
2515
2516 mastodon_http(ic, url, mastodon_http_report, mr, HTTP_POST, NULL, 0);
2517 g_free(url);
2518 }
2519
2520 /**
2521 * Callback for search.
2522 */
mastodon_http_search(struct http_request * req)2523 void mastodon_http_search(struct http_request *req)
2524 {
2525 struct im_connection *ic = req->data;
2526 if (!g_slist_find(mastodon_connections, ic)) {
2527 return;
2528 }
2529
2530 json_value *parsed;
2531 if (!(parsed = mastodon_parse_response(ic, req))) {
2532 /* ic would have been freed in imc_logout in this situation */
2533 ic = NULL;
2534 return;
2535 }
2536
2537 json_value *v;
2538 gboolean found = FALSE;
2539
2540 /* hashtags */
2541 if ((v = json_o_get(parsed, "hashtags")) &&
2542 (v->type == json_array) &&
2543 (v->u.array.length > 0)) {
2544 found = TRUE;
2545 int i;
2546 for (i = 0; i < v->u.array.length; i++) {
2547 json_value *s;
2548 s = v->u.array.values[i];
2549 if (s->type == json_string) {
2550 mastodon_log(ic, "#%s", s->u.string.ptr);
2551 }
2552 }
2553 }
2554
2555 /* accounts */
2556 if ((v = json_o_get(parsed, "accounts")) &&
2557 (v->type == json_array) &&
2558 (v->u.array.length > 0)) {
2559 found = TRUE;
2560 int i;
2561 for (i = 0; i < v->u.array.length; i++) {
2562 json_value *a;
2563 a = v->u.array.values[i];
2564 if (a->type == json_object) {
2565 mastodon_log(ic, "@%s %s",
2566 json_o_str(a, "acct"),
2567 json_o_str(a, "display_name"));
2568 }
2569 }
2570 }
2571
2572 /* statuses */
2573 if ((v = json_o_get(parsed, "statuses")) &&
2574 (v->type == json_array) &&
2575 (v->u.array.length > 0)) {
2576 found = TRUE;
2577 struct mastodon_list *ml = g_new0(struct mastodon_list, 1);
2578 mastodon_xt_get_status_list(ic, v, ml);
2579 GSList *l;
2580 for (l = ml->list; l; l = g_slist_next(l)) {
2581 struct mastodon_status *s = (struct mastodon_status *) l->data;
2582 mastodon_status_show_chat(ic, s);
2583 }
2584 ml_free(ml);
2585 }
2586
2587 json_value_free(parsed);
2588
2589 if (!found) {
2590 mastodon_log(ic, "Search returned no results on this instance");
2591 }
2592 }
2593
2594 /**
2595 * Search for a status URL, account, or hashtag.
2596 */
mastodon_search(struct im_connection * ic,char * what)2597 void mastodon_search(struct im_connection *ic, char *what)
2598 {
2599 char *args[4] = {
2600 "q", what,
2601 "resolve", "1",
2602 };
2603
2604 mastodon_http(ic, MASTODON_SEARCH_URL, mastodon_http_search, ic, HTTP_GET, args, 4);
2605 }
2606
2607 /**
2608 * Show information about the instance.
2609 */
mastodon_instance(struct im_connection * ic)2610 void mastodon_instance(struct im_connection *ic)
2611 {
2612 mastodon_http(ic, MASTODON_INSTANCE_URL, mastodon_http_log_all, ic, HTTP_GET, NULL, 0);
2613 }
2614
2615 /**
2616 * Show information about an account.
2617 */
mastodon_account(struct im_connection * ic,guint64 id)2618 void mastodon_account(struct im_connection *ic, guint64 id)
2619 {
2620 char *url = g_strdup_printf(MASTODON_ACCOUNT_URL, id);
2621 mastodon_http(ic, url, mastodon_http_log_all, ic, HTTP_GET, NULL, 0);
2622 g_free(url);
2623 }
2624
2625 /**
2626 * Helper for all functions that need to search for an account before
2627 * they can do anything else. Provide a function to use as a callback.
2628 * This callback will get the account search result back and will need
2629 * to call mastodon_xt_get_user and do something with it.
2630 */
mastodon_with_search_account(struct im_connection * ic,char * who,http_input_function func)2631 void mastodon_with_search_account(struct im_connection *ic, char *who, http_input_function func)
2632 {
2633 char *args[2] = {
2634 "q", who,
2635 };
2636
2637 mastodon_http(ic, MASTODON_ACCOUNT_SEARCH_URL, func, ic, HTTP_GET, args, 2);
2638 }
2639
2640 /**
2641 * Show debug information for an account.
2642 */
mastodon_search_account(struct im_connection * ic,char * who)2643 void mastodon_search_account(struct im_connection *ic, char *who)
2644 {
2645 mastodon_with_search_account(ic, who, mastodon_http_log_all);
2646 }
2647
2648 /**
2649 * Show debug information for the relationship with an account.
2650 */
mastodon_relationship(struct im_connection * ic,guint64 id)2651 void mastodon_relationship(struct im_connection *ic, guint64 id)
2652 {
2653 char *args[2] = {
2654 "id", g_strdup_printf("%" G_GUINT64_FORMAT, id),
2655 };
2656
2657 mastodon_http(ic, MASTODON_ACCOUNT_RELATIONSHIP_URL, mastodon_http_log_all, ic, HTTP_GET, args, 2);
2658 g_free(args[1]);
2659 }
2660
2661 /**
2662 * Callback to print debug information about a relationship.
2663 */
mastodon_http_search_relationship(struct http_request * req)2664 static void mastodon_http_search_relationship(struct http_request *req)
2665 {
2666 struct im_connection *ic = req->data;
2667 if (!g_slist_find(mastodon_connections, ic)) {
2668 return;
2669 }
2670
2671 json_value *parsed;
2672 if (!(parsed = mastodon_parse_response(ic, req))) {
2673 /* ic would have been freed in imc_logout in this situation */
2674 ic = NULL;
2675 return;
2676 }
2677
2678 struct mastodon_account *ma = mastodon_xt_get_user(parsed);
2679
2680 if (!ma) {
2681 mastodon_log(ic, "Couldn't find a matching account.");
2682 goto finish;
2683 }
2684
2685 char *args[2] = {
2686 "id", g_strdup_printf("%" G_GUINT64_FORMAT, ma->id),
2687 };
2688
2689 mastodon_http(ic, MASTODON_ACCOUNT_RELATIONSHIP_URL, mastodon_http_log_all, ic, HTTP_GET, args, 2);
2690
2691 g_free(args[1]);
2692 finish:
2693 ma_free(ma);
2694 json_value_free(parsed);
2695 }
2696
2697 /**
2698 * Search for an account and and show debug information for the
2699 * relationship with the first account found.
2700 */
mastodon_search_relationship(struct im_connection * ic,char * who)2701 void mastodon_search_relationship(struct im_connection *ic, char *who)
2702 {
2703 mastodon_with_search_account(ic, who, mastodon_http_search_relationship);
2704 }
2705
2706 /**
2707 * Show debug information for a status.
2708 */
mastodon_status(struct im_connection * ic,guint64 id)2709 void mastodon_status(struct im_connection *ic, guint64 id)
2710 {
2711 char *url = g_strdup_printf(MASTODON_STATUS_URL, id);
2712 mastodon_http(ic, url, mastodon_http_log_all, ic, HTTP_GET, NULL, 0);
2713 g_free(url);
2714 }
2715
2716 /**
2717 * Allow the user to make a raw request.
2718 */
mastodon_raw(struct im_connection * ic,char * method,char * url,char ** arguments,int arguments_len)2719 void mastodon_raw(struct im_connection *ic, char *method, char *url, char **arguments, int arguments_len) {
2720 http_method_t m = HTTP_GET;
2721 if (g_ascii_strcasecmp(method, "get") == 0) {
2722 m = HTTP_GET;
2723 } else if (g_ascii_strcasecmp(method, "put") == 0) {
2724 m = HTTP_PUT;
2725 } else if (g_ascii_strcasecmp(method, "post") == 0) {
2726 m = HTTP_POST;
2727 } else if (g_ascii_strcasecmp(method, "delete") == 0) {
2728 m = HTTP_DELETE;
2729 }
2730 mastodon_http(ic, url, mastodon_http_log_all, ic, m, arguments, arguments_len);
2731 }
2732
2733 /**
2734 * Callback for showing the URL of a status.
2735 */
mastodon_http_status_show_url(struct http_request * req)2736 static void mastodon_http_status_show_url(struct http_request *req)
2737 {
2738 struct im_connection *ic = req->data;
2739 if (!g_slist_find(mastodon_connections, ic)) {
2740 return;
2741 }
2742
2743 json_value *parsed;
2744 if (!(parsed = mastodon_parse_response(ic, req))) {
2745 /* ic would have been freed in imc_logout in this situation */
2746 ic = NULL;
2747 return;
2748 }
2749
2750 struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic);
2751 if (ms) {
2752 mastodon_log(ic, ms->url);
2753 ms_free(ms);
2754 } else {
2755 mastodon_log(ic, "Error: could not fetch toot url.");
2756 }
2757
2758 json_value_free(parsed);
2759 }
2760
2761 /**
2762 * Show the URL for a status.
2763 */
mastodon_status_show_url(struct im_connection * ic,guint64 id)2764 void mastodon_status_show_url(struct im_connection *ic, guint64 id)
2765 {
2766 char *url = g_strdup_printf(MASTODON_STATUS_URL, id);
2767 mastodon_http(ic, url, mastodon_http_status_show_url, ic, HTTP_GET, NULL, 0);
2768 g_free(url);
2769 }
2770
2771 /**
2772 * Append a the acct attribute to a gstring user_data, separated with a space, if necessary. This is to be used with
2773 * g_list_foreach(). The prefix "@" is added in front of every element.
2774 */
mastodon_account_append(struct mastodon_account * ma,GString * user_data)2775 static void mastodon_account_append(struct mastodon_account *ma, GString *user_data)
2776 {
2777 if (user_data->len > 0) {
2778 g_string_append(user_data, " ");
2779 }
2780 g_string_append(user_data, "@");
2781 g_string_append(user_data, ma->acct);
2782 }
2783
2784 /**
2785 * Join all the acct attributes of a list of accounts, space-separated. Be sure to free the returned GString with
2786 * g_string_free(). If there is no initial element for the list, use NULL for the second argument. The prefix "@" is
2787 * added in front of every element. It is added to the initial element, too! This is used to generated a list of
2788 * accounts to mention in a toot.
2789 */
mastodon_account_join(GSList * l,gchar * init)2790 GString *mastodon_account_join(GSList *l, gchar *init)
2791 {
2792 if (!l && !init) return NULL;
2793 GString *s = g_string_new(NULL);
2794 if (init) {
2795 g_string_append(s, "@");
2796 g_string_append(s, init);
2797 }
2798 g_slist_foreach(l, (GFunc) mastodon_account_append, s);
2799 return s;
2800 }
2801
2802 /**
2803 * Show the list of mentions for a status in our log data.
2804 */
mastodon_show_mentions(struct im_connection * ic,GSList * l)2805 void mastodon_show_mentions(struct im_connection *ic, GSList *l)
2806 {
2807 if (l) {
2808 GString *s = mastodon_account_join(l, NULL);
2809 mastodon_log(ic, "Mentioned: %s", s->str);
2810 g_string_free(s, TRUE);
2811 } else {
2812 mastodon_log(ic, "Nobody was mentioned in this toot");
2813 }
2814 }
2815
2816 /**
2817 * Callback for showing the mentions of a status.
2818 */
mastodon_http_status_show_mentions(struct http_request * req)2819 static void mastodon_http_status_show_mentions(struct http_request *req)
2820 {
2821 struct im_connection *ic = req->data;
2822 if (!g_slist_find(mastodon_connections, ic)) {
2823 return;
2824 }
2825
2826 json_value *parsed;
2827 if (!(parsed = mastodon_parse_response(ic, req))) {
2828 /* ic would have been freed in imc_logout in this situation */
2829 ic = NULL;
2830 return;
2831 }
2832
2833 struct mastodon_status *ms = mastodon_xt_get_status(parsed, ic);
2834 if (ms) {
2835 mastodon_show_mentions(ic, ms->mentions);
2836 ms_free(ms);
2837 } else {
2838 mastodon_log(ic, "Error: could not fetch toot url.");
2839 }
2840
2841 json_value_free(parsed);
2842 }
2843
2844 /**
2845 * Show the mentions for a status.
2846 */
mastodon_status_show_mentions(struct im_connection * ic,guint64 id)2847 void mastodon_status_show_mentions(struct im_connection *ic, guint64 id)
2848 {
2849 char *url = g_strdup_printf(MASTODON_STATUS_URL, id);
2850 mastodon_http(ic, url, mastodon_http_status_show_mentions, ic, HTTP_GET, NULL, 0);
2851 g_free(url);
2852 }
2853
2854 /**
2855 * Attempt to flush the context data. This is called by the two
2856 * callbacks for the context request because we need to wait for two
2857 * responses: the original status details, and the context itself.
2858 */
mastodon_flush_context(struct im_connection * ic)2859 void mastodon_flush_context(struct im_connection *ic)
2860 {
2861 struct mastodon_data *md = ic->proto_data;
2862
2863 if (!(md->flags & MASTODON_GOT_STATUS) ||
2864 !(md->flags & MASTODON_GOT_CONTEXT)) {
2865 return;
2866 }
2867
2868 struct mastodon_status *ms = md->status_obj;
2869 struct mastodon_list *bl = md->context_before_obj;
2870 struct mastodon_list *al = md->context_after_obj;
2871 GSList *l;
2872
2873 for (l = bl->list; l; l = g_slist_next(l)) {
2874 struct mastodon_status *s = (struct mastodon_status *) l->data;
2875 mastodon_status_show_chat(ic, s);
2876 }
2877
2878 mastodon_status_show_chat(ic, ms);
2879
2880 for (l = al->list; l; l = g_slist_next(l)) {
2881 struct mastodon_status *s = (struct mastodon_status *) l->data;
2882 mastodon_status_show_chat(ic, s);
2883 }
2884
2885 ml_free(al);
2886 ml_free(bl);
2887 ms_free(ms);
2888
2889 md->flags &= ~(MASTODON_GOT_STATUS | MASTODON_GOT_CONTEXT);
2890 md->status_obj = md->context_before_obj = md->context_after_obj = NULL;
2891 }
2892
2893 /**
2894 * Callback for the context of a status. Store it in our mastodon data
2895 * structure and attempt to flush it.
2896 */
mastodon_http_context(struct http_request * req)2897 void mastodon_http_context(struct http_request *req)
2898 {
2899 struct im_connection *ic = req->data;
2900 if (!g_slist_find(mastodon_connections, ic)) {
2901 return;
2902 }
2903
2904 struct mastodon_data *md = ic->proto_data;
2905
2906 json_value *parsed;
2907 if (!(parsed = mastodon_parse_response(ic, req))) {
2908 /* ic would have been freed in imc_logout in this situation */
2909 ic = NULL;
2910 goto end;
2911 }
2912
2913 if (parsed->type != json_object) {
2914 goto finished;
2915 }
2916
2917 struct mastodon_list *bl = g_new0(struct mastodon_list, 1);
2918 struct mastodon_list *al = g_new0(struct mastodon_list, 1);
2919
2920 json_value *before = json_o_get(parsed, "ancestors");
2921 json_value *after = json_o_get(parsed, "descendants");
2922
2923 if (before->type == json_array &&
2924 mastodon_xt_get_status_list(ic, before, bl)) {
2925 md->context_before_obj = bl;
2926 }
2927
2928 if (after->type == json_array &&
2929 mastodon_xt_get_status_list(ic, after, al)) {
2930 md->context_after_obj = al;
2931 }
2932 finished:
2933 json_value_free(parsed);
2934 end:
2935 if (ic) {
2936 md->flags |= MASTODON_GOT_CONTEXT;
2937 mastodon_flush_context(ic);
2938 }
2939 }
2940
2941 /**
2942 * Callback for the original status as part of a context request.
2943 * Store it in our mastodon data structure and attempt to flush it.
2944 */
mastodon_http_context_status(struct http_request * req)2945 void mastodon_http_context_status(struct http_request *req)
2946 {
2947 struct im_connection *ic = req->data;
2948 if (!g_slist_find(mastodon_connections, ic)) {
2949 return;
2950 }
2951
2952 struct mastodon_data *md = ic->proto_data;
2953
2954 json_value *parsed;
2955 if (!(parsed = mastodon_parse_response(ic, req))) {
2956 /* ic would have been freed in imc_logout in this situation */
2957 ic = NULL;
2958 goto end;
2959 }
2960
2961 md->status_obj = mastodon_xt_get_status(parsed, ic);
2962
2963 json_value_free(parsed);
2964 end:
2965 if (ic) {
2966 md->flags |= MASTODON_GOT_STATUS;
2967 mastodon_flush_context(ic);
2968 }
2969 }
2970
2971 /**
2972 * Search for a status and its context. The problem is that the
2973 * context doesn't include the status we're interested in. That's why
2974 * we must make two requests and wait until we get the response to
2975 * both.
2976 */
mastodon_context(struct im_connection * ic,guint64 id)2977 void mastodon_context(struct im_connection *ic, guint64 id)
2978 {
2979 struct mastodon_data *md = ic->proto_data;
2980
2981 ms_free(md->status_obj);
2982 ml_free(md->context_before_obj);
2983 ml_free(md->context_after_obj);
2984
2985 md->status_obj = md->context_before_obj = md->context_after_obj = NULL;
2986
2987 md->flags &= ~(MASTODON_GOT_STATUS | MASTODON_GOT_CONTEXT);
2988
2989 char *url = g_strdup_printf(MASTODON_STATUS_CONTEXT_URL, id);
2990 mastodon_http(ic, url, mastodon_http_context, ic, HTTP_GET, NULL, 0);
2991 g_free(url);
2992
2993 url = g_strdup_printf(MASTODON_STATUS_URL, id);
2994 mastodon_http(ic, url, mastodon_http_context_status, ic, HTTP_GET, NULL, 0);
2995 g_free(url);
2996 }
2997
2998
2999 /**
3000 * The callback functions for mastodon_http_statuses_chain() should look like this, e.g. mastodon_account_statuses or
3001 * mastodon_account_pinned_statuses. The way this works: If you know the id of an account, call the function directly:
3002 * mastodon_account_statuses(). The callback for this is mastodon_http_statuses() which uses mastodon_http_timeline() to
3003 * display the statuses. If you don't know the id of an account by you know the "who" of the account, call a function
3004 * like mastodon_unknown_account_statuses(). It calls mastodon_with_search_account() and the callback for this is
3005 * mastodon_http_unknown_account_statuses(). The http_request it gets has the account(s) you need. Call
3006 * mastodon_chained_account() which parses the request and determines the actual account id. Finally, it calls the last
3007 * callback, which mastodon_account_statuses(), with the account id. And we already know how that works! Phew!!
3008 *
3009 */
3010 typedef void (*mastodon_chained_account_function)(struct im_connection *ic, guint64 id);
3011
mastodon_chained_account(struct http_request * req,mastodon_chained_account_function func)3012 void mastodon_chained_account(struct http_request *req, mastodon_chained_account_function func)
3013 {
3014 struct im_connection *ic = req->data;
3015 if (!g_slist_find(mastodon_connections, ic)) {
3016 return;
3017 }
3018
3019 json_value *parsed;
3020 if (!(parsed = mastodon_parse_response(ic, req))) {
3021 /* ic would have been freed in imc_logout in this situation */
3022 ic = NULL;
3023 return;
3024 }
3025
3026 if (parsed->type != json_array || parsed->u.array.length == 0) {
3027 mastodon_log(ic, "Couldn't find a matching account.");
3028 goto finish;
3029 }
3030
3031 // Just use the first one, let's hope these are sorted appropriately!
3032 struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[0]);
3033
3034 if (ma) {
3035 func(ic, ma->id);
3036 } else {
3037 mastodon_log(ic, "Couldn't find a matching account.");
3038 }
3039
3040 ma_free(ma);
3041 finish:
3042 json_value_free(parsed);
3043 }
3044
3045 /**
3046 * Callback for a reponse containing one or more statuses which are to be shown, usually the result of looking at the
3047 * statuses of an account.
3048 */
mastodon_http_statuses(struct http_request * req)3049 void mastodon_http_statuses(struct http_request *req)
3050 {
3051 mastodon_http_timeline(req, MT_HOME);
3052 }
3053
3054 /**
3055 * Given a command that showed a bunch of statuses, which will have used mastodon_http_timeline as a callback, and which
3056 * will therefore have set md->next_url, we can use now use this URL to request more statuses.
3057 */
mastodon_more(struct im_connection * ic)3058 void mastodon_more(struct im_connection *ic)
3059 {
3060 struct mastodon_data *md = ic->proto_data;
3061
3062 if (!md->next_url) {
3063 mastodon_log(ic, "Next URL is not set. This shouldn't happen, as they say!?");
3064 return;
3065 }
3066
3067 char *url = g_strdup(md->next_url);
3068 char *s = NULL;
3069 int len = 0;
3070 int i;
3071
3072 for (i = 0; url[i]; i++) {
3073 if (url[i] == '?') {
3074 url[i] = 0;
3075 s = url + i + 1;
3076 len = 1;
3077 } else if (s && url[i] == '&') {
3078 url[i] = '='; // for later splitting
3079 len++;
3080 }
3081 }
3082
3083 gchar **args = NULL;
3084
3085 if (s) {
3086 args = g_strsplit (s, "=", -1);
3087 }
3088
3089 switch(md->more_type) {
3090 case MASTODON_MORE_STATUSES:
3091 mastodon_http(ic, url, mastodon_http_statuses, ic, HTTP_GET, args, len);
3092 break;
3093 case MASTODON_MORE_NOTIFICATIONS:
3094 mastodon_http(ic, url, mastodon_http_notifications, ic, HTTP_GET, args, len);
3095 break;
3096 }
3097
3098 g_strfreev(args);
3099 g_free(url);
3100 }
3101
3102 /**
3103 * Show the timeline of a user.
3104 */
mastodon_account_statuses(struct im_connection * ic,guint64 id)3105 void mastodon_account_statuses(struct im_connection *ic, guint64 id)
3106 {
3107 char *url = g_strdup_printf(MASTODON_ACCOUNT_STATUSES_URL, id);
3108 mastodon_http(ic, url, mastodon_http_statuses, ic, HTTP_GET, NULL, 0);
3109 g_free(url);
3110 }
3111
3112 /**
3113 * Show the pinned statuses of a user.
3114 */
mastodon_account_pinned_statuses(struct im_connection * ic,guint64 id)3115 void mastodon_account_pinned_statuses(struct im_connection *ic, guint64 id)
3116 {
3117 char *args[2] = {
3118 "pinned", "1",
3119 };
3120
3121 char *url = g_strdup_printf(MASTODON_ACCOUNT_STATUSES_URL, id);
3122 mastodon_http(ic, url, mastodon_http_statuses, ic, HTTP_GET, args, 2);
3123 g_free(url);
3124 }
3125
3126 /**
3127 * Callback to display the timeline for a unknown user. We got the account data back and now we just take the first user
3128 * and display their timeline.
3129 */
mastodon_http_unknown_account_statuses(struct http_request * req)3130 void mastodon_http_unknown_account_statuses(struct http_request *req)
3131 {
3132 mastodon_chained_account(req, mastodon_account_statuses);
3133 }
3134
3135 /**
3136 * Show the timeline of an unknown user. Thus, we first have to search for them.
3137 */
mastodon_unknown_account_statuses(struct im_connection * ic,char * who)3138 void mastodon_unknown_account_statuses(struct im_connection *ic, char *who)
3139 {
3140 mastodon_with_search_account(ic, who, mastodon_http_unknown_account_statuses);
3141 }
3142
3143 /**
3144 * Callback to display the timeline for a unknown user. We got the account data back and now we just take the first user
3145 * and display their timeline.
3146 */
mastodon_http_unknown_account_pinned_statuses(struct http_request * req)3147 void mastodon_http_unknown_account_pinned_statuses(struct http_request *req)
3148 {
3149 mastodon_chained_account(req, mastodon_account_pinned_statuses);
3150 }
3151
3152 /**
3153 * Show the timeline of an unknown user. Thus, we first have to search for them.
3154 */
mastodon_unknown_account_pinned_statuses(struct im_connection * ic,char * who)3155 void mastodon_unknown_account_pinned_statuses(struct im_connection *ic, char *who)
3156 {
3157 mastodon_with_search_account(ic, who, mastodon_http_unknown_account_pinned_statuses);
3158 }
3159
3160 /**
3161 * Callback for the user bio.
3162 */
mastodon_http_account_bio(struct http_request * req)3163 void mastodon_http_account_bio(struct http_request *req)
3164 {
3165 struct im_connection *ic = req->data;
3166 if (!g_slist_find(mastodon_connections, ic)) {
3167 return;
3168 }
3169
3170 json_value *parsed;
3171 if (!(parsed = mastodon_parse_response(ic, req))) {
3172 /* ic would have been freed in imc_logout in this situation */
3173 ic = NULL;
3174 return;
3175 }
3176
3177 const char *display_name = json_o_str(parsed, "display_name");
3178 char *note = g_strdup(json_o_str(parsed, "note"));
3179 mastodon_strip_html(note); // modified in place
3180
3181 mastodon_log(ic, "Bio for %s: %s", display_name, note);
3182
3183 g_free(note);
3184 json_value_free(parsed);
3185 }
3186
3187 /**
3188 * Show a user bio.
3189 */
mastodon_account_bio(struct im_connection * ic,guint64 id)3190 void mastodon_account_bio(struct im_connection *ic, guint64 id)
3191 {
3192 char *url = g_strdup_printf(MASTODON_ACCOUNT_URL, id);
3193 mastodon_http(ic, url, mastodon_http_account_bio, ic, HTTP_GET, NULL, 0);
3194 g_free(url);
3195 }
3196 /**
3197 * Callback to display the timeline for a unknown user. We got the account data back and now we just take the first user
3198 * and show their bio.
3199 */
mastodon_http_unknown_account_bio(struct http_request * req)3200 void mastodon_http_unknown_account_bio(struct http_request *req)
3201 {
3202 mastodon_chained_account(req, mastodon_account_bio);
3203 }
3204
3205 /**
3206 * Show the bio of an unknown user. Thus, we first have to search for them.
3207 */
mastodon_unknown_account_bio(struct im_connection * ic,char * who)3208 void mastodon_unknown_account_bio(struct im_connection *ic, char *who)
3209 {
3210 mastodon_with_search_account(ic, who, mastodon_http_unknown_account_bio);
3211 }
3212
3213 /**
3214 * Call back for step 3 of mastodon_follow: adding the buddy.
3215 */
mastodon_http_follow3(struct http_request * req)3216 static void mastodon_http_follow3(struct http_request *req)
3217 {
3218 struct im_connection *ic = req->data;
3219 if (!g_slist_find(mastodon_connections, ic)) {
3220 return;
3221 }
3222
3223 json_value *parsed;
3224 if (!(parsed = mastodon_parse_response(ic, req))) {
3225 /* ic would have been freed in imc_logout in this situation */
3226 ic = NULL;
3227 return;
3228 }
3229
3230 struct mastodon_account *ma = mastodon_xt_get_user(parsed);
3231
3232 if (ma) {
3233 mastodon_add_buddy(ic, ma->id, ma->acct, ma->display_name);
3234 mastodon_log(ic, "You are now following %s.", ma->acct);
3235 } else {
3236 mastodon_log(ic, "Couldn't find a matching account.");
3237 }
3238
3239 ma_free(ma);
3240 json_value_free(parsed);
3241 }
3242
3243 /**
3244 * Call back for step 2 of mastodon_follow: actually following.
3245 */
mastodon_http_follow2(struct http_request * req)3246 static void mastodon_http_follow2(struct http_request *req)
3247 {
3248 struct im_connection *ic = req->data;
3249 if (!g_slist_find(mastodon_connections, ic)) {
3250 return;
3251 }
3252
3253 json_value *parsed;
3254 if (!(parsed = mastodon_parse_response(ic, req))) {
3255 /* ic would have been freed in imc_logout in this situation */
3256 ic = NULL;
3257 return;
3258 }
3259
3260 json_value *it;
3261 if ((it = json_o_get(parsed, "domain_blocking")) && it->type == json_boolean && it->u.boolean) {
3262 mastodon_log(ic, "This user's domain is being blocked by your instance.");
3263 }
3264
3265 if ((it = json_o_get(parsed, "blocking")) && it->type == json_boolean && it->u.boolean) {
3266 mastodon_log(ic, "You need to unblock this user.");
3267 }
3268
3269 if ((it = json_o_get(parsed, "muting")) && it->type == json_boolean && it->u.boolean) {
3270 mastodon_log(ic, "You might want to unmute this user.");
3271 }
3272
3273 if ((it = json_o_get(parsed, "muting")) && it->type == json_boolean && it->u.boolean) {
3274 mastodon_log(ic, "You might want to unmute this user.");
3275 }
3276
3277 if ((it = json_o_get(parsed, "requested")) && it->type == json_boolean && it->u.boolean) {
3278 mastodon_log(ic, "You have requested to follow this user.");
3279 }
3280
3281 if ((it = json_o_get(parsed, "followed_by")) && it->type == json_boolean && it->u.boolean) {
3282 mastodon_log(ic, "Nice, this user is already following you.");
3283 }
3284
3285 if ((it = json_o_get(parsed, "following")) && it->type == json_boolean && it->u.boolean) {
3286 guint64 id;
3287 if ((it = json_o_get(parsed, "id")) &&
3288 (id = mastodon_json_int64(it))) {
3289 char *url = g_strdup_printf(MASTODON_ACCOUNT_URL, id);
3290 mastodon_http(ic, url, mastodon_http_follow3, ic, HTTP_GET, NULL, 0);
3291 g_free(url);
3292 } else {
3293 mastodon_log(ic, "I can't believe it: this relation has no id. I can't add them!");
3294 }
3295 }
3296
3297 json_value_free(parsed);
3298 }
3299
3300 /**
3301 * Call back for step 1 of mastodon_follow: searching for the account to follow.
3302 */
mastodon_http_follow1(struct http_request * req)3303 static void mastodon_http_follow1(struct http_request *req)
3304 {
3305 struct im_connection *ic = req->data;
3306 if (!g_slist_find(mastodon_connections, ic)) {
3307 return;
3308 }
3309
3310 json_value *parsed;
3311 if (!(parsed = mastodon_parse_response(ic, req))) {
3312 /* ic would have been freed in imc_logout in this situation */
3313 ic = NULL;
3314 return;
3315 }
3316
3317 if (parsed->type != json_array || parsed->u.array.length == 0) {
3318 mastodon_log(ic, "Couldn't find a matching account.");
3319 goto finish;
3320 }
3321
3322 // Just use the first one, let's hope these are sorted appropriately!
3323 struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[0]);
3324
3325 if (ma) {
3326 char *url = g_strdup_printf(MASTODON_ACCOUNT_FOLLOW_URL, ma->id);
3327 mastodon_http(ic, url, mastodon_http_follow2, ic, HTTP_POST, NULL, 0);
3328 g_free(url);
3329 ma_free(ma);
3330 } else {
3331 mastodon_log(ic, "Couldn't find a matching account.");
3332 }
3333 finish:
3334 json_value_free(parsed);
3335 }
3336
3337 /**
3338 * Function to follow an unknown user. First we need to search for it,
3339 * though.
3340 */
mastodon_follow(struct im_connection * ic,char * who)3341 void mastodon_follow(struct im_connection *ic, char *who)
3342 {
3343 mastodon_with_search_account(ic, who, mastodon_http_follow1);
3344 }
3345
3346 /**
3347 * Callback for adding the buddies you are following.
3348 */
mastodon_http_following(struct http_request * req)3349 static void mastodon_http_following(struct http_request *req)
3350 {
3351 struct im_connection *ic = req->data;
3352 if (!g_slist_find(mastodon_connections, ic)) {
3353 return;
3354 }
3355
3356 json_value *parsed;
3357 if (!(parsed = mastodon_parse_response(ic, req))) {
3358 /* ic would have been freed in imc_logout in this situation */
3359 ic = NULL;
3360 return;
3361 }
3362
3363 if (parsed->type != json_array || parsed->u.array.length == 0) {
3364 goto finish;
3365 }
3366
3367 int i;
3368 for (i = 0; i < parsed->u.array.length; i++) {
3369
3370 struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[i]);
3371
3372 if (ma) {
3373 mastodon_add_buddy(ic, ma->id, ma->acct, ma->display_name);
3374 }
3375
3376 ma_free(ma);
3377 }
3378
3379 finish:
3380 json_value_free(parsed);
3381 gboolean done = TRUE;
3382
3383 // try to fetch more if there is a header saying that there is
3384 // more (URL in angled brackets)
3385 char *header = NULL;
3386 if ((header = get_rfc822_header(req->reply_headers, "Link", 0))) {
3387
3388 char *url = NULL;
3389 char *s = NULL;
3390 int len = 0;
3391 int i;
3392
3393 for (i = 0; header[i]; i++) {
3394 if (header[i] == '<') {
3395 url = header + i + 1;
3396 } else if (header[i] == '?') {
3397 header[i] = 0; // end url
3398 s = header + i + 1;
3399 len = 1;
3400 } else if (s && header[i] == '&') {
3401 header[i] = '='; // for later splitting
3402 len++;
3403 } else if (url && header[i] == '>') {
3404 header[i] = 0;
3405 if (strncmp(header + i + 1, "; rel=\"next\"", 12) == 0) {
3406 break;
3407 } else {
3408 url = NULL;
3409 s = NULL;
3410 len = 0;
3411 }
3412 }
3413 }
3414
3415 if (url) {
3416 gchar **args = NULL;
3417
3418 if (s) {
3419 args = g_strsplit (s, "=", -1);
3420 }
3421
3422 mastodon_http(ic, url, mastodon_http_following, ic, HTTP_GET, args, len);
3423 done = FALSE;
3424
3425 g_strfreev(args);
3426 }
3427
3428 g_free(header);
3429 }
3430
3431 if (done) {
3432 /* Now that we have reached the end of the list, everybody has mastodon_user_data set, at last: imcb_add_buddy →
3433 bee_user_new → ic->acc->prpl->buddy_data_add → mastodon_buddy_data_add. Now we're ready to (re)load lists. */
3434 mastodon_list_reload(ic, TRUE);
3435
3436 struct mastodon_data *md = ic->proto_data;
3437 md->flags |= MASTODON_HAVE_FRIENDS;
3438 }
3439 }
3440
3441 /**
3442 * Add the buddies the current account is following.
3443 */
mastodon_following(struct im_connection * ic)3444 void mastodon_following(struct im_connection *ic)
3445 {
3446 gint64 id = set_getint(&ic->acc->set, "account_id");
3447
3448 if (!id) {
3449 return;
3450 }
3451
3452 char *url = g_strdup_printf(MASTODON_ACCOUNT_FOLLOWING_URL, id);
3453 mastodon_http(ic, url, mastodon_http_following, ic, HTTP_GET, NULL, 0);
3454 g_free(url);
3455 }
3456
3457 /**
3458 * Callback for the list of lists.
3459 */
mastodon_http_lists(struct http_request * req)3460 void mastodon_http_lists(struct http_request *req)
3461 {
3462 struct im_connection *ic = req->data;
3463 if (!g_slist_find(mastodon_connections, ic)) {
3464 return;
3465 }
3466
3467 json_value *parsed;
3468 if (!(parsed = mastodon_parse_response(ic, req))) {
3469 /* ic would have been freed in imc_logout in this situation */
3470 ic = NULL;
3471 return;
3472 }
3473
3474 if (parsed->type != json_array || parsed->u.array.length == 0) {
3475 mastodon_log(ic, "Use 'list create <name>' to create a list.");
3476 goto finish;
3477 }
3478
3479 int i;
3480 GString *s = g_string_new(g_strdup_printf("Lists: "));
3481 gboolean first = TRUE;
3482 for (i = 0; i < parsed->u.array.length; i++) {
3483 json_value *a = parsed->u.array.values[i];
3484 if (a->type == json_object) {
3485 if (first) {
3486 first = FALSE;
3487 } else {
3488 g_string_append(s, "; ");
3489 }
3490 g_string_append(s, json_o_str(a, "title"));
3491 }
3492 }
3493 mastodon_log(ic, s->str);
3494 g_string_free(s, TRUE);
3495 finish:
3496 json_value_free(parsed);
3497 }
3498
3499 /**
3500 * Retrieving lists. Returns at most 50 Lists without pagination.
3501 */
mastodon_lists(struct im_connection * ic)3502 void mastodon_lists(struct im_connection *ic) {
3503 mastodon_http(ic, MASTODON_LIST_URL, mastodon_http_lists, ic, HTTP_GET, NULL, 0);
3504 }
3505
3506 /**
3507 * Create a list.
3508 */
mastodon_list_create(struct im_connection * ic,char * title)3509 void mastodon_list_create(struct im_connection *ic, char *title) {
3510 struct mastodon_data *md = ic->proto_data;
3511
3512 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
3513 mc->ic = ic;
3514
3515 if (md->undo_type == MASTODON_NEW) {
3516 mc->command = MC_LIST_CREATE;
3517 mc->redo = g_strdup_printf("list create %s", title);
3518 mc->undo = g_strdup_printf("list delete %s", title);
3519 }
3520
3521 char *args[2] = {
3522 "title", title,
3523 };
3524
3525 mastodon_http(ic, MASTODON_LIST_URL, mastodon_http_callback_and_ack, mc, HTTP_POST, args, 2);
3526 }
3527
3528 /**
3529 * Second callback for the list of accounts.
3530 */
mastodon_http_list_accounts2(struct http_request * req)3531 void mastodon_http_list_accounts2(struct http_request *req) {
3532 struct mastodon_command *mc = req->data;
3533 struct im_connection *ic = mc->ic;
3534
3535 if (!g_slist_find(mastodon_connections, ic)) {
3536 goto finally;
3537 }
3538
3539 json_value *parsed;
3540 if (!(parsed = mastodon_parse_response(ic, req))) {
3541 /* ic would have been freed in imc_logout in this situation */
3542 ic = NULL;
3543 goto finally;
3544 }
3545
3546 if (parsed->type != json_array || parsed->u.array.length == 0) {
3547 mastodon_log(ic, "There are no members in this list. Your options:\n"
3548 "Delete it using 'list delete %s'\n"
3549 "Add members using 'list add <nick> to %s'",
3550 mc->str, mc->str);
3551 goto finish;
3552 }
3553
3554 int i;
3555 GString *m = g_string_new("Members:");
3556
3557 for (i = 0; i < parsed->u.array.length; i++) {
3558
3559 struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[i]);
3560
3561 if (ma) {
3562 g_string_append(m, " ");
3563 bee_user_t *bu = bee_user_by_handle(ic->bee, ic, ma->acct);
3564 if (bu) {
3565 irc_user_t *iu = bu->ui_data;
3566 g_string_append(m, iu->nick);
3567 } else {
3568 g_string_append(m, "@");
3569 g_string_append(m, ma->acct);
3570 }
3571 ma_free(ma);
3572 }
3573 }
3574 mastodon_log(ic, m->str);
3575 g_string_free(m, TRUE);
3576
3577 finish:
3578 /* We need to free the parsed data. */
3579 json_value_free(parsed);
3580 finally:
3581 /* We've encountered a problem and we need to free the mastodon_command. */
3582 mc_free(mc);
3583 }
3584
3585
3586 /**
3587 * Part two of the first callback: now we have mc->id. Call the URL which will give us the accounts themselves. The API
3588 * documentation says: If you specify limit=0 in the query, all accounts will be returned without pagination.
3589 */
mastodon_list_accounts(struct im_connection * ic,struct mastodon_command * mc)3590 void mastodon_list_accounts(struct im_connection *ic, struct mastodon_command *mc) {
3591 char *args[2] = { "limit", "0", };
3592 char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, mc->id);
3593 mastodon_http(ic, url, mastodon_http_list_accounts2, mc, HTTP_GET, args, 2);
3594 g_free(url);
3595 }
3596
3597 /**
3598 * First callback for listing the accounts in a list. First, get the list id from the data we received, then call the
3599 * next function.
3600 */
mastodon_http_list_accounts(struct http_request * req)3601 void mastodon_http_list_accounts(struct http_request *req) {
3602 mastodon_chained_list(req, mastodon_list_accounts);
3603 }
3604
3605 /**
3606 * Show accounts in a list.
3607 */
mastodon_unknown_list_accounts(struct im_connection * ic,char * title)3608 void mastodon_unknown_list_accounts(struct im_connection *ic, char *title) {
3609 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
3610 mc->ic = ic;
3611 mc->str = g_strdup(title);
3612 mastodon_with_named_list(ic, mc, mastodon_http_list_accounts);
3613 }
3614
3615 /**
3616 * Second callback for list delete. We get back the accounts in the list we are about to delete. We need these to
3617 * prepare the undo command. Undo is serious business. Then we delete the list.
3618 */
mastodon_http_list_delete2(struct http_request * req)3619 void mastodon_http_list_delete2(struct http_request *req) {
3620 struct mastodon_command *mc = req->data;
3621 struct im_connection *ic = mc->ic;
3622 struct mastodon_data *md = ic->proto_data;
3623
3624 if (!g_slist_find(mastodon_connections, ic)) {
3625 goto finish;
3626 }
3627
3628 json_value *parsed;
3629 if (!(parsed = mastodon_parse_response(ic, req))) {
3630 /* ic would have been freed in imc_logout in this situation */
3631 ic = NULL;
3632 goto finish;
3633 }
3634
3635 if (parsed->type != json_array || parsed->u.array.length == 0) {
3636 mastodon_log(ic, "There are no members in this list. Cool!");
3637 } else if (md->undo_type == MASTODON_NEW) {
3638 int i;
3639 char *title = mc->str;
3640 GString *undo = g_string_new(mc->undo);
3641
3642 for (i = 0; i < parsed->u.array.length; i++) {
3643
3644 struct mastodon_account *ma = mastodon_xt_get_user(parsed->u.array.values[i]);
3645
3646 if (ma) {
3647 g_string_append(undo, FS);
3648 g_string_append_printf(undo, "list add %" G_GINT64_FORMAT " to %s", ma->id, title);
3649 }
3650 ma_free(ma);
3651 }
3652
3653 g_free(mc->undo); mc->undo = undo->str; // adopt
3654 g_string_free(undo, FALSE);
3655 }
3656
3657 char *url = g_strdup_printf(MASTODON_LIST_DATA_URL, mc->id);
3658 mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_DELETE, NULL, 0);
3659 g_free(url);
3660 json_value_free(parsed);
3661 return;
3662 finish:
3663 /* We have encountered a problem an need to free mc. If we don't run into a problem, mc is passed on to the next
3664 * request. But not here. */
3665 mc_free(mc);
3666 }
3667
3668 /**
3669 * This is part of the first callback. We have the list id in mc->id! If this is a new command, we want to find all the
3670 * accounts in the list in order to prepare the undo command. Undo is serious business. If this command is not new, in
3671 * other words, this is a redo command, then we can skip right ahead and go for the list delete.
3672 */
mastodon_list_delete(struct im_connection * ic,struct mastodon_command * mc)3673 void mastodon_list_delete(struct im_connection *ic, struct mastodon_command *mc) {
3674 struct mastodon_data *md = ic->proto_data;
3675
3676 if (md->undo_type == MASTODON_NEW) {
3677 /* Make sure we get all the accounts for undo. The API documentation says: If you specify limit=0 in the query,
3678 * all accounts will be returned without pagination. */
3679 char *args[2] = { "limit", "0", };
3680 char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, mc->id);
3681 mastodon_http(ic, url, mastodon_http_list_delete2, mc, HTTP_GET, args, 2);
3682 g_free(url);
3683 } else {
3684 /* This is a short cut! */
3685 char *url = g_strdup_printf(MASTODON_LIST_DATA_URL, mc->id);
3686 mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_DELETE, NULL, 0);
3687 g_free(url);
3688 }
3689 }
3690
3691 /**
3692 * Callback for list delete. We get back the lists we know about and need to find the list to delete. Once we have it,
3693 * we use another callback to get the list of all its members in order to prepare the undo command. Undo is serious
3694 * business.
3695 */
mastodon_http_list_delete(struct http_request * req)3696 void mastodon_http_list_delete(struct http_request *req) {
3697 mastodon_chained_list(req, mastodon_list_delete);
3698 }
3699
3700 /**
3701 * Delete a list by title.
3702 */
mastodon_unknown_list_delete(struct im_connection * ic,char * title)3703 void mastodon_unknown_list_delete(struct im_connection *ic, char *title) {
3704 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
3705 mc->ic = ic;
3706 mc->str = g_strdup(title);
3707 struct mastodon_data *md = ic->proto_data;
3708 if (md->undo_type == MASTODON_NEW) {
3709 mc->command = MC_LIST_DELETE;
3710 mc->redo = g_strdup_printf("list delete %s", title);
3711 mc->undo = g_strdup_printf("list create %s", title);
3712 }
3713 mastodon_with_named_list(ic, mc, mastodon_http_list_delete);
3714 }
3715
3716 /**
3717 * Part two of the first callback: now we have mc->id. Call the URL which will give us the accounts themselves. The API
3718 * documentation says: If you specify limit=0 in the query, all accounts will be returned without pagination.
3719 */
mastodon_list_add_account(struct im_connection * ic,struct mastodon_command * mc)3720 void mastodon_list_add_account(struct im_connection *ic, struct mastodon_command *mc) {
3721 char *args[2] = {
3722 "account_ids[]", g_strdup_printf("%" G_GUINT64_FORMAT, mc->id2),
3723 };
3724 char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, mc->id);
3725 mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_POST, args, 2);
3726 g_free(args[1]);
3727 g_free(url);
3728 }
3729
3730 /**
3731 * First callback for adding an account to a list. First, get the list id from the data we received, then call the next
3732 * function.
3733 */
mastodon_http_list_add_account(struct http_request * req)3734 void mastodon_http_list_add_account(struct http_request *req) {
3735 mastodon_chained_list(req, mastodon_list_add_account);
3736 }
3737
3738 /**
3739 * Add one or more accounts to a list.
3740 */
mastodon_unknown_list_add_account(struct im_connection * ic,guint64 id,char * title)3741 void mastodon_unknown_list_add_account(struct im_connection *ic, guint64 id, char *title) {
3742 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
3743 mc->ic = ic;
3744 mc->id2 = id;
3745 mc->str = g_strdup(title);
3746 struct mastodon_data *md = ic->proto_data;
3747 if (md->undo_type == MASTODON_NEW) {
3748 mc->command = MC_LIST_ADD_ACCOUNT;
3749 mc->redo = g_strdup_printf("list add %" G_GINT64_FORMAT " to %s", id, title);
3750 mc->undo = g_strdup_printf("list remove %" G_GINT64_FORMAT " from %s", id, title);
3751 }
3752 mastodon_with_named_list(ic, mc, mastodon_http_list_add_account);
3753 }
3754
3755 /**
3756 * Part two of the first callback: now we have mc->id. Call the URL which will give us the accounts themselves. The API
3757 * documentation says: If you specify limit=0 in the query, all accounts will be returned without pagination.
3758 */
mastodon_list_remove_account(struct im_connection * ic,struct mastodon_command * mc)3759 void mastodon_list_remove_account(struct im_connection *ic, struct mastodon_command *mc) {
3760 char *args[2] = {
3761 "account_ids[]", g_strdup_printf("%" G_GUINT64_FORMAT, mc->id2),
3762 };
3763 char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, mc->id);
3764 mastodon_http(ic, url, mastodon_http_callback_and_ack, mc, HTTP_DELETE, args, 2);
3765 g_free(args[1]);
3766 g_free(url);
3767 }
3768
3769 /**
3770 * First callback for removing an accounts from a list. First, get the list id from the data we received, then call the
3771 * next function.
3772 */
mastodon_http_list_remove_account(struct http_request * req)3773 void mastodon_http_list_remove_account(struct http_request *req) {
3774 mastodon_chained_list(req, mastodon_list_remove_account);
3775 }
3776
3777 /**
3778 * Remove one or more accounts from a list.
3779 */
mastodon_unknown_list_remove_account(struct im_connection * ic,guint64 id,char * title)3780 void mastodon_unknown_list_remove_account(struct im_connection *ic, guint64 id, char *title) {
3781 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
3782 mc->ic = ic;
3783 mc->id2 = id;
3784 mc->str = g_strdup(title);
3785 struct mastodon_data *md = ic->proto_data;
3786 if (md->undo_type == MASTODON_NEW) {
3787 mc->command = MC_LIST_REMOVE_ACCOUNT;
3788 mc->redo = g_strdup_printf("list remove %" G_GINT64_FORMAT " from %s", id, title);
3789 mc->undo = g_strdup_printf("list add %" G_GINT64_FORMAT " to %s", id, title);
3790 }
3791 mastodon_with_named_list(ic, mc, mastodon_http_list_remove_account);
3792 }
3793
3794 /**
3795 * Second callback to reload all the lists. We are getting all the accounts for one of the lists, here.
3796 * The mastodon_command (mc) has id (id of the list), str (title of the list), and optionally extra.
3797 */
mastodon_http_list_reload2(struct http_request * req)3798 static void mastodon_http_list_reload2(struct http_request *req) {
3799 struct mastodon_command *mc = req->data;
3800 struct im_connection *ic = mc->ic;
3801
3802 if (!g_slist_find(mastodon_connections, ic)) {
3803 goto finally;
3804 }
3805
3806 json_value *parsed;
3807 if (!(parsed = mastodon_parse_response(ic, req))) {
3808 /* ic would have been freed in imc_logout in this situation */
3809 ic = NULL;
3810 goto finally;
3811 }
3812
3813 if (parsed->type != json_array || parsed->u.array.length == 0) {
3814 goto finish;
3815 }
3816
3817 int i;
3818 for (i = 0; i < parsed->u.array.length; i++) {
3819
3820 struct mastodon_account *ma;
3821 bee_user_t *bu;
3822 struct mastodon_user_data *mud;
3823 if ((ma = mastodon_xt_get_user(parsed->u.array.values[i])) &&
3824 (bu = bee_user_by_handle(ic->bee, ic, ma->acct)) &&
3825 (mud = (struct mastodon_user_data*) bu->data)) {
3826 mud->lists = g_slist_prepend(mud->lists, g_strdup(mc->str));
3827 ma_free(ma);
3828 }
3829 }
3830
3831 mastodon_log(ic, "Membership of %s list reloaded", mc->str);
3832
3833 finish:
3834 json_value_free(parsed);
3835
3836 if (mc->extra) {
3837 /* Keep using the mc and don't free it! */
3838 mastodon_list_timeline(ic, mc);
3839 return;
3840 }
3841
3842 finally:
3843 /* We've encountered a problem and we need to free the mastodon_command. */
3844 mc_free(mc);
3845 }
3846
3847 /**
3848 * First callback to reload all the lists. We are getting all the lists, here. For each one, get the members in the
3849 * list. Our mastodon_command (mc) might have the extra attribute set.
3850 */
mastodon_http_list_reload(struct http_request * req)3851 static void mastodon_http_list_reload(struct http_request *req) {
3852 struct mastodon_command *mc = req->data;
3853 struct im_connection *ic = mc->ic;
3854
3855 if (!g_slist_find(mastodon_connections, ic)) {
3856 goto finally;
3857 }
3858
3859 json_value *parsed;
3860 if (!(parsed = mastodon_parse_response(ic, req))) {
3861 /* ic would have been freed in imc_logout in this situation */
3862 ic = NULL;
3863 goto finally;
3864 }
3865
3866 if (parsed->type != json_array || parsed->u.array.length == 0) {
3867 goto finish;
3868 }
3869
3870 /* Clear existing list membership. */
3871 GSList *l;
3872 for (l = ic->bee->users; l; l = l->next) {
3873 bee_user_t *bu = l->data;
3874 struct mastodon_user_data *mud = (struct mastodon_user_data*) bu->data;
3875 if (mud) {
3876 g_slist_free_full(mud->lists, g_free); mud->lists = NULL;
3877 }
3878 }
3879
3880 int i;
3881 guint64 id = 0;
3882
3883 /* Get members for every list defined. */
3884 for (i = 0; i < parsed->u.array.length; i++) {
3885 json_value *a = parsed->u.array.values[i];
3886 json_value *it;
3887 const char *title;
3888 if (a->type == json_object &&
3889 (it = json_o_get(a, "id")) &&
3890 (id = mastodon_json_int64(it)) &&
3891 (title = json_o_str(a, "title"))) {
3892
3893 struct mastodon_command *mc2 = g_new0(struct mastodon_command, 1);
3894 mc2->ic = ic;
3895 mc2->id = id;
3896 mc2->str = g_strdup(title);
3897 mc2->extra = mc->extra;
3898
3899 char *url = g_strdup_printf(MASTODON_LIST_ACCOUNTS_URL, id);
3900 mastodon_http(ic, url, mastodon_http_list_reload2, mc2, HTTP_GET, NULL, 0);
3901 g_free(url);
3902 }
3903 }
3904
3905 finish:
3906 json_value_free(parsed);
3907 finally:
3908 /* We've encountered a problem and we need to free the mastodon_command. */
3909 mc_free(mc);
3910 }
3911
3912 /**
3913 * Reload the memberships of all the lists. We need this for mastodon_status_show_chat(). The populate parameter says
3914 * whether we should issue a timeline request for every list we have a group chat for, at the end.
3915 */
mastodon_list_reload(struct im_connection * ic,gboolean populate)3916 void mastodon_list_reload(struct im_connection *ic, gboolean populate) {
3917 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
3918 mc->ic = ic;
3919 mc->extra = populate;
3920 mastodon_http(ic, MASTODON_LIST_URL, mastodon_http_list_reload, mc, HTTP_GET, NULL, 0);
3921 }
3922
3923 /**
3924 * Free the md->filter list so that it can be done from mastodon_logout().
3925 */
mastodon_filters_destroy(struct mastodon_data * md)3926 void mastodon_filters_destroy(struct mastodon_data *md) {
3927 GSList *l;
3928 for (l = md->filters; l; l = g_slist_next(l)) {
3929 mf_free((struct mastodon_filter *) l->data);
3930 }
3931 g_slist_free(md->filters);
3932 md->filters = NULL;
3933 }
3934
3935 /**
3936 * Parse the context attribute of a Mastodon filter.
3937 */
mastodon_parse_context(json_value * parsed)3938 mastodon_filter_type_t mastodon_parse_context(json_value *parsed)
3939 {
3940 mastodon_filter_type_t context = 0;
3941
3942 int i;
3943 for (i = 0; i < parsed->u.array.length; i++) {
3944 json_value *s = parsed->u.array.values[i];
3945 if (s->type == json_string) {
3946 if (g_ascii_strcasecmp(s->u.string.ptr, "home") == 0)
3947 context |= MF_HOME;
3948 if (g_ascii_strcasecmp(s->u.string.ptr, "notifications") == 0)
3949 context |= MF_NOTIFICATIONS;
3950 if (g_ascii_strcasecmp(s->u.string.ptr, "public") == 0)
3951 context |= MF_PUBLIC;
3952 if (g_ascii_strcasecmp(s->u.string.ptr, "thread") == 0)
3953 context |= MF_THREAD;
3954 }
3955 }
3956 return context;
3957 }
3958
3959 /**
3960 * Parse a filter.
3961 */
mastodon_parse_filter(json_value * parsed)3962 struct mastodon_filter *mastodon_parse_filter (json_value *parsed)
3963 {
3964 json_value *it;
3965 guint64 id = 0;
3966 const char *phrase;
3967 if (parsed && parsed->type == json_object &&
3968 (it = json_o_get(parsed, "id")) &&
3969 (id = mastodon_json_int64(it)) &&
3970 (phrase = json_o_str(parsed, "phrase"))) {
3971
3972 struct mastodon_filter *mf = g_new0(struct mastodon_filter, 1);
3973 mf->id = id;
3974 mf->phrase = g_strdup(phrase);
3975 mf->phrase_case_folded = g_utf8_casefold(phrase, -1);
3976
3977 if ((it = json_o_get(parsed, "context")) && it->type == json_array)
3978 mf->context = mastodon_parse_context(it);
3979 if ((it = json_o_get(parsed, "irreversible")) && it->type == json_boolean)
3980 mf->irreversible = it->u.boolean;
3981 if ((it = json_o_get(parsed, "whole_word")) && it->type == json_boolean)
3982 mf->whole_word = it->u.boolean;
3983
3984 struct tm time;
3985 if ((it = json_o_get(parsed, "expires_in")) && it->type == json_string &&
3986 strptime(it->u.string.ptr, MASTODON_TIME_FORMAT, &time) != NULL)
3987 mf->expires_in = mktime_utc(&time);
3988
3989 return mf;
3990 }
3991 return NULL;
3992 }
3993
3994 /**
3995 * Callback for loading filters. We need to do this when connecting to the instance, and we want to do it when
3996 * displaying the filters.
3997 */
mastodon_http_filters_load(struct http_request * req)3998 void mastodon_http_filters_load (struct http_request *req)
3999 {
4000 struct im_connection *ic = req->data;
4001 struct mastodon_data *md = ic->proto_data;
4002
4003 if (!g_slist_find(mastodon_connections, ic)) {
4004 return;
4005 }
4006
4007 if (req->status_code != 200) {
4008 mastodon_log(ic, "Filters did not load. This requires Mastodon v2.4.3 or newer. See 'info instance' for more about your instance.");
4009 // Don't log off: no ic = NULL!
4010 return;
4011 }
4012
4013 json_value *parsed;
4014 if (!(parsed = mastodon_parse_response(ic, req))) {
4015 /* ic would have been freed in imc_logout in this situation */
4016 ic = NULL;
4017 return;
4018 }
4019
4020 if (parsed->type != json_array || parsed->u.array.length == 0) {
4021 goto finish;
4022 }
4023
4024 mastodon_filters_destroy(md);
4025
4026 int i;
4027
4028 for (i = 0; i < parsed->u.array.length; i++) {
4029 json_value *it = parsed->u.array.values[i];
4030 struct mastodon_filter *mf = mastodon_parse_filter(it);
4031 if (mf)
4032 md->filters = g_slist_prepend(md->filters, mf);
4033 }
4034
4035 finish:
4036 json_value_free(parsed);
4037 }
4038
4039 /**
4040 * Callback for reloading and displaying filters.
4041 */
mastodon_http_filters(struct http_request * req)4042 void mastodon_http_filters (struct http_request *req)
4043 {
4044 struct im_connection *ic = req->data;
4045 struct mastodon_data *md = ic->proto_data;
4046
4047 mastodon_http_filters_load(req);
4048
4049 if (!md->filters) {
4050 mastodon_log(ic, "No filters. Use 'filter create'.");
4051 return;
4052 }
4053
4054 GSList *l;
4055 int i = 1;
4056 for (l = md->filters; l; l = g_slist_next(l)) {
4057 struct mastodon_filter *mf = (struct mastodon_filter *) l->data;
4058
4059 GString *p = g_string_new(NULL);
4060 int mask = MF_HOME|MF_PUBLIC|MF_NOTIFICATIONS|MF_THREAD;
4061 if ((mf->context & mask) == mask) {
4062 g_string_append(p, " everywhere");
4063 } else {
4064 if (mf->context & MF_HOME) { g_string_append(p, " home"); }
4065 if (mf->context & MF_PUBLIC) { g_string_append(p, " public"); }
4066 if (mf->context & MF_NOTIFICATIONS) { g_string_append(p, " notifications"); }
4067 if (mf->context & MF_THREAD) { g_string_append(p, " thread"); }
4068 }
4069 if (mf->irreversible) { g_string_append(p, ", server side"); }
4070 if (mf->whole_word) { g_string_append(p, ", whole word"); }
4071 mastodon_log(ic, "%2d. %s (properties:%s)", i++, mf->phrase, p->str);
4072 g_string_free(p, TRUE);
4073 }
4074 }
4075
4076 /**
4077 * Load and display the filters from the instance.
4078 */
mastodon_filters(struct im_connection * ic)4079 void mastodon_filters(struct im_connection *ic)
4080 {
4081 mastodon_http(ic, MASTODON_FILTER_URL, mastodon_http_filters, ic, HTTP_GET, NULL, 0);
4082 }
4083
4084 /**
4085 * Callback for mastodon_get_filters.
4086 */
mastodon_http_get_filters(struct http_request * req)4087 void mastodon_http_get_filters (struct http_request *req)
4088 {
4089 struct im_connection *ic = req->data;
4090 if (!g_slist_find(mastodon_connections, ic)) {
4091 return;
4092 }
4093
4094 mastodon_http_filters_load(req);
4095
4096 struct mastodon_data *md = ic->proto_data;
4097 md->flags |= MASTODON_GOT_FILTERS;
4098 mastodon_flush_timeline(ic);
4099 }
4100
4101 /**
4102 * See mastodon_initial_timeline.
4103 */
mastodon_get_filters(struct im_connection * ic)4104 static void mastodon_get_filters(struct im_connection *ic)
4105 {
4106 struct mastodon_data *md = ic->proto_data;
4107
4108 md->flags &= ~MASTODON_GOT_FILTERS;
4109
4110 mastodon_http(ic, MASTODON_FILTER_URL, mastodon_http_get_filters, ic, HTTP_GET, NULL, 0);
4111 }
4112
4113 /**
4114 * Callback for filter creation. We need to get the number of the filter created and use that for the undo command.
4115 */
mastodon_http_filter_create(struct http_request * req)4116 void mastodon_http_filter_create(struct http_request *req)
4117 {
4118 struct mastodon_command *mc = req->data;
4119 struct im_connection *ic = mc->ic;
4120
4121 if (!g_slist_find(mastodon_connections, ic)) {
4122 return;
4123 }
4124
4125 json_value *parsed;
4126 if (!(parsed = mastodon_parse_response(ic, req))) {
4127 /* ic would have been freed in imc_logout in this situation */
4128 ic = NULL;
4129 return;
4130 }
4131
4132 struct mastodon_filter *mf = mastodon_parse_filter(parsed);
4133 if (mf) {
4134 struct mastodon_data *md = ic->proto_data;
4135 md->filters = g_slist_prepend(md->filters, mf);
4136 mastodon_log(ic, "Filter created");
4137 /* Maintain undo/redo list. */
4138 mc->undo = g_strdup_printf("filter delete %" G_GUINT64_FORMAT, mf->id);
4139 if(md->undo_type == MASTODON_NEW) {
4140 mastodon_do(ic, mc->redo, mc->undo);
4141 } else {
4142 mastodon_do_update(ic, mc->undo);
4143 }
4144 }
4145 }
4146
4147 /**
4148 * Create a new filter.
4149 */
mastodon_filter_create(struct im_connection * ic,char * str)4150 void mastodon_filter_create(struct im_connection *ic, char *str)
4151 {
4152 struct mastodon_data *md = ic->proto_data;
4153
4154 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
4155 mc->ic = ic;
4156
4157 if (md->undo_type == MASTODON_NEW) {
4158 mc->command = MC_FILTER_CREATE;
4159 mc->redo = g_strdup_printf("filter create %s", str);
4160 }
4161
4162 /* FIXME: add timeout */
4163 char *args[14] = {
4164 "phrase", str,
4165 "context[]", "home",
4166 "context[]", "notifications",
4167 "context[]", "public",
4168 "context[]", "thread",
4169 "irreversible", "true",
4170 "whole_words", "1",
4171 };
4172 mastodon_http(ic, MASTODON_FILTER_URL, mastodon_http_filter_create, mc, HTTP_POST, args, 14);
4173 }
4174
4175 /**
4176 * Callback for filter deletion.
4177 */
mastodon_http_filter_delete(struct http_request * req)4178 void mastodon_http_filter_delete(struct http_request *req)
4179 {
4180 struct mastodon_command *mc = req->data;
4181 struct im_connection *ic = mc->ic;
4182
4183 if (!g_slist_find(mastodon_connections, ic)) {
4184 return;
4185 }
4186
4187 if (req->status_code == 200) {
4188 struct mastodon_data *md = ic->proto_data;
4189 struct mastodon_filter *mf = (struct mastodon_filter *) mc->data;
4190 md->filters = g_slist_remove(md->filters, mf);
4191 mastodon_http_callback_and_ack(req);
4192 }
4193 }
4194
4195 /**
4196 * Delete a filter based on the item number when listing them.
4197 */
mastodon_filter_delete(struct im_connection * ic,char * arg)4198 void mastodon_filter_delete(struct im_connection *ic, char *arg)
4199 {
4200 guint64 id;
4201 if (!parse_int64(arg, 10, &id)) {
4202 mastodon_log(ic, "You must refer to a filter number. Use 'filter' to list them.");
4203 return;
4204 }
4205
4206 struct mastodon_data *md = ic->proto_data;
4207 /* filters are listed starting at 1 */
4208 struct mastodon_filter *mf = (struct mastodon_filter *) g_slist_nth_data(md->filters, id - 1);
4209
4210 if (!mf) {
4211 GSList *l;
4212 gboolean found = FALSE;
4213 for (l = md->filters; l; l = g_slist_next(l)) {
4214 mf = (struct mastodon_filter *) l->data;
4215 if (mf->id == id) {
4216 found = TRUE;
4217 break;
4218 }
4219 }
4220 if (!found) {
4221 mastodon_log(ic, "This filter is unkown. Use 'filter' to list them.");
4222 return;
4223 }
4224 }
4225
4226 struct mastodon_command *mc = g_new0(struct mastodon_command, 1);
4227 mc->ic = ic;
4228 mc->data = (gpointer *) mf;
4229 if (md->undo_type == MASTODON_NEW) {
4230 mc->command = MC_FILTER_DELETE;
4231 /* FIXME: more parameters */
4232 mc->redo = g_strdup_printf("filter delete %" G_GUINT64_FORMAT, mf->id);
4233 mc->undo = g_strdup_printf("filter create %s", mf->phrase);
4234 }
4235
4236 char *url = g_strdup_printf(MASTODON_FILTER_DATA_URL, mf->id);
4237 mastodon_http(ic, url, mastodon_http_filter_delete, mc, HTTP_DELETE, NULL, 0);
4238 g_free(url);
4239 }
4240
4241 /**
4242 * Callback for getting your own account. This saves the account_id.
4243 * Once we have that, we are ready to figure out who our followers are.
4244 */
mastodon_http_verify_credentials(struct http_request * req)4245 static void mastodon_http_verify_credentials(struct http_request *req)
4246 {
4247 struct im_connection *ic = req->data;
4248 if (!g_slist_find(mastodon_connections, ic)) {
4249 return;
4250 }
4251
4252 json_value *parsed;
4253 if ((parsed = mastodon_parse_response(ic, req))) {
4254 json_value *it;
4255 guint64 id;
4256 if ((it = json_o_get(parsed, "id")) &&
4257 (id = mastodon_json_int64(it))) {
4258 set_setint(&ic->acc->set, "account_id", id);
4259 }
4260 json_value_free(parsed);
4261
4262 mastodon_following(ic);
4263 }
4264 }
4265
4266 /**
4267 * Get the account of the current user.
4268 */
mastodon_verify_credentials(struct im_connection * ic)4269 void mastodon_verify_credentials(struct im_connection *ic)
4270 {
4271 imcb_log(ic, "Verifying credentials");
4272 mastodon_http(ic, MASTODON_VERIFY_CREDENTIALS_URL, mastodon_http_verify_credentials, ic, HTTP_GET, NULL, 0);
4273 }
4274
4275 /**
4276 * Callback for registering a new application.
4277 */
mastodon_http_register_app(struct http_request * req)4278 static void mastodon_http_register_app(struct http_request *req)
4279 {
4280 struct im_connection *ic = req->data;
4281 if (!g_slist_find(mastodon_connections, ic)) {
4282 return;
4283 }
4284
4285 mastodon_log(ic, "Parsing application registration response");
4286
4287 json_value *parsed;
4288 if ((parsed = mastodon_parse_response(ic, req))) {
4289
4290 set_setint(&ic->acc->set, "app_id", json_o_get(parsed, "id")->u.integer);
4291
4292 char *key = json_o_strdup(parsed, "client_id");
4293 char *secret = json_o_strdup(parsed, "client_secret");
4294
4295 json_value_free(parsed);
4296
4297 // save for future sessions
4298 set_setstr(&ic->acc->set, "consumer_key", key);
4299 set_setstr(&ic->acc->set, "consumer_secret", secret);
4300
4301 // and set for the current session, and connect
4302 struct mastodon_data *md = ic->proto_data;
4303 struct oauth2_service *os = md->oauth2_service;
4304 os->consumer_key = key;
4305 os->consumer_secret = secret;
4306
4307 oauth2_init(ic);
4308 }
4309 }
4310
4311 /**
4312 * Function to register a new application (Bitlbee) for the server.
4313 */
mastodon_register_app(struct im_connection * ic)4314 void mastodon_register_app(struct im_connection *ic)
4315 {
4316 char *args[8] = {
4317 "client_name", "bitlbee",
4318 "redirect_uris", "urn:ietf:wg:oauth:2.0:oob",
4319 "scopes", "read write follow",
4320 "website", "https://www.bitlbee.org/"
4321 };
4322
4323 mastodon_http(ic, MASTODON_REGISTER_APP_URL, mastodon_http_register_app, ic, HTTP_POST, args, 8);
4324 }
4325