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