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 #include "bitlbee.h"
27 #include "account.h"
28 #include "nogaim.h"
29 #include "oauth.h"
30 #include "oauth2.h"
31 #include "mastodon.h"
32 #include "mastodon-http.h"
33 #include "mastodon-lib.h"
34 #include "rot13.h"
35 #include "url.h"
36 #include "help.h"
37 #include <stdbool.h>
38 
39 #define HELPFILE_NAME "mastodon-help.txt"
40 
mastodon_help_init()41 static void mastodon_help_init()
42 {
43   /* Figure out where our help file is by looking at the global helpfile. */
44   gchar *dir = g_path_get_dirname (global.helpfile);
45   if (strcmp(dir, ".") == 0) {
46     log_message(LOGLVL_WARNING, "Error finding the directory of helpfile %s.", global.helpfile);
47     g_free(dir);
48     return;
49   }
50   gchar *df = g_strjoin("/", dir, HELPFILE_NAME, NULL);
51   g_free(dir);
52 
53   /* Load help from our own help file. */
54   help_t *dh;
55   help_init(&dh, df);
56   if(dh == NULL) {
57     log_message(LOGLVL_WARNING, "Error opening helpfile: %s.", df);
58     g_free(df);
59     return;
60   }
61   g_free(df);
62 
63   /* Link the last entry of global.help with first entry of our help. */
64   help_t *h, *l = NULL;
65   for (h = global.help; h; h = h->next) {
66     l = h;
67   }
68   if (l) {
69     l->next = dh;
70   } else {
71     /* No global help but ours? */
72     global.help = dh;
73   }
74 }
75 
76 #ifdef BITLBEE_ABI_VERSION_CODE
init_plugin_info(void)77 struct plugin_info *init_plugin_info(void)
78 {
79 	/* Run ./configure to change these. */
80 	static struct plugin_info info = {
81 		BITLBEE_ABI_VERSION_CODE,
82 		PACKAGE_NAME,
83 		PACKAGE_VERSION,
84 		"Bitlbee plugin for Mastodon <https://joinmastodon.org/>",
85 		"Alex Schroeder <alex@gnu.org>",
86 		"https://alexschroeder.ch/cgit/bitlbee-mastodon/about/"
87 	};
88 
89 	return &info;
90 }
91 #endif
92 
93 GSList *mastodon_connections = NULL;
94 
mastodon_groupchat_init(struct im_connection * ic)95 struct groupchat *mastodon_groupchat_init(struct im_connection *ic)
96 {
97 	struct groupchat *gc;
98 	struct mastodon_data *md = ic->proto_data;
99 	GSList *l;
100 
101 	if (md->timeline_gc) {
102 		return md->timeline_gc;
103 	}
104 
105 	md->timeline_gc = gc = imcb_chat_new(ic, "mastodon/timeline");
106 	imcb_chat_name_hint(gc, md->name);
107 
108 	for (l = ic->bee->users; l; l = l->next) {
109 		bee_user_t *bu = l->data;
110 		if (bu->ic == ic) {
111 			imcb_chat_add_buddy(gc, bu->handle);
112 		}
113 	}
114 	imcb_chat_add_buddy(gc, ic->acc->user);
115 
116 	return gc;
117 }
118 
119 /**
120  * Free the oauth2_service struct.
121  */
os_free(struct oauth2_service * os)122 static void os_free(struct oauth2_service *os) {
123 
124 	if (os == NULL) {
125 		return;
126 	}
127 
128 	g_free(os->auth_url);
129 	g_free(os->token_url);
130 	g_free(os);
131 }
132 
133 /**
134  * Create a new oauth2_service struct. If we haven never connected to
135  * the server, we'll be missing our key and secret.
136  */
get_oauth2_service(struct im_connection * ic)137 static struct oauth2_service *get_oauth2_service(struct im_connection *ic)
138 {
139 	struct mastodon_data *md = ic->proto_data;
140 
141 	struct oauth2_service *os = g_new0(struct oauth2_service, 1);
142 	os->auth_url = g_strconcat("https://", md->url_host, "/oauth/authorize", NULL);
143 	os->token_url = g_strconcat("https://", md->url_host, "/oauth/token", NULL);
144 	os->redirect_url = "urn:ietf:wg:oauth:2.0:oob";
145 	os->scope = MASTODON_SCOPE;
146 
147 	// possibly empty strings if the client is not registered
148 	os->consumer_key = set_getstr(&ic->acc->set, "consumer_key");
149 	os->consumer_secret = set_getstr(&ic->acc->set, "consumer_secret");
150 
151 	return os;
152 }
153 
154 /**
155  * Check message length by comparing it to the appropriate setting.
156  * Note this issue: "Count all URLs in text as 23 characters flat, do
157  * not count domain part of usernames."
158  * https://github.com/tootsuite/mastodon/pull/4427
159  **/
mastodon_length_check(struct im_connection * ic,gchar * msg,char * cw)160 static gboolean mastodon_length_check(struct im_connection *ic, gchar *msg, char *cw)
161 {
162 	int len = g_utf8_strlen(msg, -1);
163 	if (len == 0) {
164 		mastodon_log(ic, "This message is empty.");
165 		return FALSE;
166 	}
167 
168 	if (cw != NULL) {
169 		len += g_utf8_strlen(cw, -1);
170 	}
171 
172 	int max = set_getint(&ic->acc->set, "message_length");
173 	if (max == 0) {
174 		return TRUE;
175 	}
176 
177 	GRegex *regex = g_regex_new (MASTODON_URL_REGEX, 0, 0, NULL);
178 	GMatchInfo *match_info;
179 
180 	g_regex_match (regex, msg, 0, &match_info);
181 	while (g_match_info_matches (match_info))
182 	{
183 	    gchar *url = g_match_info_fetch (match_info, 0);
184 	    len = len - g_utf8_strlen(url, -1) + 23;
185 	    g_free (url);
186 	    g_match_info_next (match_info, NULL);
187 	}
188 	g_regex_unref (regex);
189 
190 	regex = g_regex_new (MASTODON_MENTION_REGEX, 0, 0, NULL);
191 	g_regex_match (regex, msg, 0, &match_info);
192 	while (g_match_info_matches (match_info))
193 	{
194 	    gchar *mention = g_match_info_fetch (match_info, 0);
195 	    gchar *nick = g_match_info_fetch (match_info, 2);
196 	    len = len - g_utf8_strlen(mention, -1) + g_utf8_strlen(nick, -1);
197 	    g_free (mention);
198 	    g_free (nick);
199 	    g_match_info_next (match_info, NULL);
200 	}
201 	g_regex_unref (regex);
202 
203 	g_match_info_free (match_info);
204 
205 	if (len <= max) {
206 		return TRUE;
207 	}
208 
209 	mastodon_log(ic, "Maximum message length exceeded: %d > %d", len, max);
210 
211 	return FALSE;
212 }
213 
set_eval_commands(set_t * set,char * value)214 static char *set_eval_commands(set_t * set, char *value)
215 {
216 	if (g_ascii_strcasecmp(value, "strict") == 0) {
217 		return value;
218 	} else {
219 		return set_eval_bool(set, value);
220 	}
221 }
222 
set_eval_mode(set_t * set,char * value)223 static char *set_eval_mode(set_t * set, char *value)
224 {
225 	if (g_ascii_strcasecmp(value, "one") == 0 ||
226 	    g_ascii_strcasecmp(value, "many") == 0 || g_ascii_strcasecmp(value, "chat") == 0) {
227 		return value;
228 	} else {
229 		return NULL;
230 	}
231 }
232 
set_eval_hide_sensitive(set_t * set,char * value)233 static char *set_eval_hide_sensitive(set_t * set, char *value)
234 {
235 	if (g_ascii_strcasecmp(value, "rot13") == 0 ||
236 	    g_ascii_strcasecmp(value, "advanced_rot13") == 0) {
237 		return value;
238 	} else {
239 		return set_eval_bool(set, value);
240 	}
241 }
242 
set_eval_visibility(set_t * set,char * value)243 static char *set_eval_visibility(set_t * set, char *value)
244 {
245 	if (g_ascii_strcasecmp(value, "public") == 0
246 	    || g_ascii_strcasecmp(value, "unlisted") == 0
247 	    || g_ascii_strcasecmp(value, "private") == 0) {
248 		return value;
249 	} else {
250 		return "public";
251 	}
252 }
253 
mastodon_init(account_t * acc)254 static void mastodon_init(account_t * acc)
255 {
256 	set_t *s;
257 
258 	char* handle = acc -> user,
259 	    * new_user_name;
260 	bool change_user_name = false;
261 
262 	if (*handle == '@') {
263 		change_user_name = true;
264 		new_user_name = ++ handle;
265 	} else new_user_name = acc -> user;
266 
267 	size_t handle_sz = strlen(handle);
268 	char const* base_url;
269 
270 	while (*handle != '@') {
271 		if (*handle == 0) {
272 			/* the user has entered an invalid handle - the smart thing
273 			 * to do here would be to fail, but bitlbee doesn't provide
274 			 * a way for us to indicate that an account add command has
275 			 * failed, so we glue a common instance name to the account
276 			 * and hope for the best */
277 			base_url = MASTODON_DEFAULT_INSTANCE;
278 			goto no_instance_in_username;
279 		}
280 		handle++;
281 	}
282 
283 	*handle = 0; /* delete the server component from the handle */
284 	change_user_name = true;
285 	size_t endpoint_sz = (handle - (acc -> user));
286 	handle_sz -= endpoint_sz + 1;
287 
288 	/* construct a server url */ {
289 		char const* instance = handle + 1;
290 		char* endpoint = alloca( /* using alloca instead of VLAs to
291 									avoid thorny scope problems */
292 			endpoint_sz +
293 			sizeof "https://" +
294 			1 /* trailing nul */
295 		);
296 
297 		char* eptr = endpoint;
298 		eptr = g_stpcpy(eptr, "https://");
299 		eptr = g_stpcpy(eptr, instance);
300 
301 		base_url = endpoint;
302 	}
303 
304 no_instance_in_username:
305 	if (change_user_name) {
306 		char saved_str [handle_sz + 1]; g_stpcpy(saved_str, new_user_name);
307  		/* i promise i can explain.
308  		 * i haven't dug too deeply into what causes this bug, because
309  		 * it's 5am and i've gotten no sleep tonight, but for some
310  		 * ungodly reason - due to a bug in either glib or the bitlbee
311  		 * set structure - passing a substring of the set's existing
312  		 * value appears to cause memory corruption of some kind (in
313  		 * this instance, deleting the first character of the username.)
314  		 * temporarily duplicating the string and setting it from the
315  		 * duplicate seems to fix the problem. it's an atrocious hack,
316  		 * and if you're reading this, i beg you to do what i did not
317  		 * have the strength to, and figure out why on god's green
318  		 * earth it happened. */
319 
320  		set_setstr(&acc -> set, "username", saved_str);
321 	}
322 
323 	s = set_add(&acc->set, "auto_reply_timeout", "10800", set_eval_int, acc);
324 
325 	s = set_add(&acc->set, "base_url", base_url, NULL, acc);
326 	s->flags |= ACC_SET_OFFLINE_ONLY;
327 
328 	s = set_add(&acc->set, "commands", "true", set_eval_commands, acc);
329 
330 	s = set_add(&acc->set, "message_length", "500", set_eval_int, acc);
331 
332 	s = set_add(&acc->set, "mode", "chat", set_eval_mode, acc);
333 	s->flags |= ACC_SET_OFFLINE_ONLY;
334 
335 	s = set_add(&acc->set, "name", "", NULL, acc);
336 	s->flags |= ACC_SET_OFFLINE_ONLY;
337 
338 	s = set_add(&acc->set, "show_ids", "true", set_eval_bool, acc);
339 
340 	s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc);
341 
342 	s = set_add(&acc->set, "hide_sensitive", "false", set_eval_hide_sensitive, acc);
343 	s = set_add(&acc->set, "sensitive_flag", "*NSFW* ", NULL, acc);
344 
345 	s = set_add(&acc->set, "visibility", "public", set_eval_visibility, acc);
346 
347 	s = set_add(&acc->set, "hide_boosts", "false", set_eval_bool, acc);
348 	s = set_add(&acc->set, "hide_favourites", "false", set_eval_bool, acc);
349 	s = set_add(&acc->set, "hide_mentions", "false", set_eval_bool, acc);
350 	s = set_add(&acc->set, "hide_follows", "false", set_eval_bool, acc);
351 
352 	s = set_add(&acc->set, "app_id", "0", set_eval_int, acc);
353 	s->flags |= SET_HIDDEN;
354 
355 	s = set_add(&acc->set, "account_id", "0", set_eval_int, acc);
356 	s->flags |= SET_HIDDEN;
357 
358 	s = set_add(&acc->set, "consumer_key", "", NULL, acc);
359 	s->flags |= SET_HIDDEN;
360 
361 	s = set_add(&acc->set, "consumer_secret", "", NULL, acc);
362 	s->flags |= SET_HIDDEN;
363 
364 	mastodon_help_init();
365 }
366 
367 /**
368  * Set the name of the Mastodon channel, either based on a preference, or based on hostname and account name.
369  */
mastodon_set_name(struct im_connection * ic)370 static void mastodon_set_name(struct im_connection *ic)
371 {
372 	struct mastodon_data *md = ic->proto_data;
373 	char *name = set_getstr(&ic->acc->set, "name");
374 	if (name[0]) {
375 		md->name = g_strdup(name);
376 	} else {
377 		md->name = g_strdup_printf("%s_%s", md->url_host, ic->acc->user);
378 	}
379 }
380 
381 
382 /**
383  * Connect to Mastodon server, using the data we saved in the account.
384  */
mastodon_connect(struct im_connection * ic)385 static void mastodon_connect(struct im_connection *ic)
386 {
387 	struct mastodon_data *md = ic->proto_data;
388 	url_t url;
389 	char *s;
390 
391 	imcb_log(ic, "Connecting");
392 
393 	if (!url_set(&url, set_getstr(&ic->acc->set, "base_url")) ||
394 	    url.proto != PROTO_HTTPS) {
395 		imcb_error(ic, "Incorrect API base URL: %s", set_getstr(&ic->acc->set, "base_url"));
396 		imc_logout(ic, FALSE);
397 		return;
398 	}
399 
400 	md->url_ssl = url.proto == PROTO_HTTPS; // always
401 	md->url_port = url.port;
402 	md->url_host = g_strdup(url.host);
403 
404 	mastodon_set_name(ic);
405 	imcb_add_buddy(ic, md->name, NULL);
406 	imcb_buddy_status(ic, md->name, OPT_LOGGED_IN, NULL, NULL);
407 
408 	md->log = g_new0(struct mastodon_log_data, MASTODON_LOG_LENGTH);
409 	md->log_id = -1;
410 
411 	s = set_getstr(&ic->acc->set, "mode");
412 	if (g_ascii_strcasecmp(s, "one") == 0) {
413 		md->flags |= MASTODON_MODE_ONE;
414 	} else if (g_ascii_strcasecmp(s, "many") == 0) {
415 		md->flags |= MASTODON_MODE_MANY;
416 	} else {
417 		md->flags |= MASTODON_MODE_CHAT;
418 	}
419 
420 	if (!(md->flags & MASTODON_MODE_ONE) &&
421 	    !(md->flags & MASTODON_HAVE_FRIENDS)) {
422 		// find our account_id and store it, eventually
423 		mastodon_verify_credentials(ic);
424 	}
425 
426 	/* Create the room. */
427 	if (md->flags & MASTODON_MODE_CHAT) {
428 		mastodon_groupchat_init(ic);
429 	}
430 
431 	mastodon_initial_timeline(ic);
432 	mastodon_open_user_stream(ic);
433 	ic->flags |= OPT_PONGS;
434 }
435 
436 /**
437  * Initiate OAuth dialog with user. A reply to the MASTODON_OAUTH_HANDLE is handled by mastodon_buddy_msg.
438  */
oauth2_init(struct im_connection * ic)439 void oauth2_init(struct im_connection *ic)
440 {
441 	struct mastodon_data *md = ic->proto_data;
442 
443 	imcb_log(ic, "Starting OAuth authentication");
444 
445 	/* Temporary contact, just used to receive the OAuth response. */
446 	imcb_add_buddy(ic, MASTODON_OAUTH_HANDLE, NULL);
447 
448 	char *url = oauth2_url(md->oauth2_service);
449 	char *msg = g_strdup_printf("Open this URL in your browser to authenticate: %s", url);
450 	imcb_buddy_msg(ic, MASTODON_OAUTH_HANDLE, msg, 0, 0);
451 
452 	g_free(msg);
453 	g_free(url);
454 
455 	imcb_buddy_msg(ic, MASTODON_OAUTH_HANDLE, "Respond to this message with the returned "
456 	               "authorization token.", 0, 0);
457 
458 	ic->flags |= OPT_SLOW_LOGIN;
459 }
460 
461 int oauth2_refresh(struct im_connection *ic, const char *refresh_token);
462 
mastodon_login(account_t * acc)463 static void mastodon_login(account_t * acc)
464 {
465 	struct im_connection *ic = imcb_new(acc);
466 	struct mastodon_data *md = g_new0(struct mastodon_data, 1);
467 	url_t url;
468 
469 	imcb_log(ic, "Login");
470 
471 	mastodon_connections = g_slist_append(mastodon_connections, ic);
472 	ic->proto_data = md;
473 	md->user = g_strdup(acc->user);
474 
475 	if (!url_set(&url, set_getstr(&ic->acc->set, "base_url"))) {
476 		imcb_error(ic, "Cannot parse API base URL: %s", set_getstr(&ic->acc->set, "base_url"));
477 		imc_logout(ic, FALSE);
478 		return;
479 	}
480 	if (url.proto != PROTO_HTTPS) {
481 		imcb_error(ic, "API base URL must use HTTPS: %s", set_getstr(&ic->acc->set, "base_url"));
482 		imc_logout(ic, FALSE);
483 		return;
484 	}
485 
486 	md->url_ssl = 1;
487 	md->url_port = url.port;
488 	md->url_host = g_strdup(url.host);
489  	mastodon_set_name(ic);
490 
491 	GSList *p_in = NULL;
492 	const char *tok;
493 
494 	md->oauth2_service = get_oauth2_service(ic);
495 
496 	oauth_params_parse(&p_in, ic->acc->pass);
497 
498 	/* If we did not have these stored, register the app and try
499 	 * again. We'll call oauth2_init from the callback in order to
500 	 * connect, eventually. */
501 	if (!md->oauth2_service->consumer_key || !md->oauth2_service->consumer_secret ||
502 	    strlen(md->oauth2_service->consumer_key) == 0 || strlen(md->oauth2_service->consumer_secret) == 0) {
503 		mastodon_register_app(ic);
504 	}
505         /* If we have a refresh token, in which case any access token
506 	   we *might* have has probably expired already anyway.
507 	   Refresh and connect. */
508 	else if ((tok = oauth_params_get(&p_in, "refresh_token"))) {
509 		oauth2_refresh(ic, tok);
510 	}
511 	/* If we don't have a refresh token, let's hope the access
512 	   token is still usable. */
513 	else if ((tok = oauth_params_get(&p_in, "access_token"))) {
514 		md->oauth2_access_token = g_strdup(tok);
515 		mastodon_connect(ic);
516 	}
517 	/* If we don't have any, start the OAuth process now. */
518 	else {
519 		oauth2_init(ic);
520 	}
521 	/* All of the above will end up calling mastodon_connect(). */
522 
523 	oauth_params_free(&p_in);
524 }
525 
526 /**
527  * Logout method. Just free the mastodon_data.
528  */
mastodon_logout(struct im_connection * ic)529 static void mastodon_logout(struct im_connection *ic)
530 {
531 	struct mastodon_data *md = ic->proto_data;
532 
533 	// Set the status to logged out.
534 	ic->flags &= ~OPT_LOGGED_IN;
535 
536 	if (md) {
537 		if (md->timeline_gc) {
538 			imcb_chat_free(md->timeline_gc);
539 		}
540 
541 		GSList *l;
542 		for (l = md->streams; l; l = l->next) {
543 			struct http_request *req = l->data;
544 			http_close(req);
545 		}
546 
547 		g_slist_free(md->streams); md->streams = NULL;
548 
549 		if (md->log) {
550 			/* When mastodon_connect hasn't been called, yet, such as when imc_logout is being called from
551 			 * mastodon_login, the log hasn not yet been initialised. */
552 			int i;
553 			for (i = 0; i < MASTODON_LOG_LENGTH; i++) {
554 				g_slist_free_full(md->log[i].mentions, g_free); md->log[i].mentions = NULL;
555 				g_free(md->log[i].spoiler_text);
556 			}
557 			g_free(md->log); md->log = NULL;
558 		}
559 
560 		mastodon_filters_destroy(md);
561 
562 		g_slist_free_full(md->mentions, g_free); md->mentions = NULL;
563 		g_free(md->last_spoiler_text); md->last_spoiler_text = NULL;
564 		g_free(md->spoiler_text); md->spoiler_text = NULL;
565 
566 		os_free(md->oauth2_service); md->oauth2_service = NULL;
567 		g_free(md->user); md->user = NULL;
568 		g_free(md->name); md->name = NULL;
569 		g_free(md->next_url); md->next_url = NULL;
570 		g_free(md->url_host); md->url_host = NULL;
571 		g_free(md);
572 		ic->proto_data = NULL;
573 	}
574 
575 	mastodon_connections = g_slist_remove(mastodon_connections, ic);
576 }
577 
578 /**
579  * When the user replies to the MASTODON_OAUTH_HANDLE with a refresh token we request the access token and this is where
580  * we get it. Save both in our settings and proceed to mastodon_connect.
581  */
oauth2_got_token(gpointer data,const char * access_token,const char * refresh_token,const char * error)582 void oauth2_got_token(gpointer data, const char *access_token, const char *refresh_token, const char *error)
583 {
584 	struct im_connection *ic = data;
585 	struct mastodon_data *md;
586 	GSList *auth = NULL;
587 
588 	if (g_slist_find(mastodon_connections, ic) == NULL) {
589 		return;
590 	}
591 
592 	md = ic->proto_data;
593 
594 	if (access_token == NULL) {
595 		imcb_error(ic, "OAuth failure (%s)", error);
596 		imc_logout(ic, TRUE);
597 		return;
598 	}
599 
600 	oauth_params_parse(&auth, ic->acc->pass);
601 	if (refresh_token) {
602 		oauth_params_set(&auth, "refresh_token", refresh_token);
603 	}
604 	if (access_token) {
605 		oauth_params_set(&auth, "access_token", access_token);
606 	}
607 
608 	g_free(ic->acc->pass);
609 	ic->acc->pass = oauth_params_string(auth);
610 	oauth_params_free(&auth);
611 
612 	g_free(md->oauth2_access_token);
613 	md->oauth2_access_token = g_strdup(access_token);
614 
615 	mastodon_connect(ic);
616 }
617 
oauth2_remove_contact(gpointer data,gint fd,b_input_condition cond)618 static gboolean oauth2_remove_contact(gpointer data, gint fd, b_input_condition cond)
619 {
620 	struct im_connection *ic = data;
621 
622 	if (g_slist_find(mastodon_connections, ic)) {
623 		imcb_remove_buddy(ic, MASTODON_OAUTH_HANDLE, NULL);
624 	}
625 	return FALSE;
626 }
627 
628 /**
629  * Get the refresh token from the user via a reply to MASTODON_OAUTH_HANDLE in mastodon_buddy_msg.
630  * Then get the access token Using the refresh token. The access token is then handled by oauth2_got_token.
631  */
oauth2_get_refresh_token(struct im_connection * ic,const char * msg)632 int oauth2_get_refresh_token(struct im_connection *ic, const char *msg)
633 {
634 	struct mastodon_data *md = ic->proto_data;
635 	char *code;
636 	int ret;
637 
638 	imcb_log(ic, "Requesting OAuth access token");
639 
640 	/* Don't do it here because the caller may get confused if the contact
641 	   we're currently sending a message to is deleted. */
642 	b_timeout_add(1, oauth2_remove_contact, ic);
643 
644 	code = g_strdup(msg);
645 	g_strstrip(code);
646 	ret = oauth2_access_token(md->oauth2_service, OAUTH2_AUTH_CODE,
647 	                          code, oauth2_got_token, ic);
648 
649 	g_free(code);
650 	return ret;
651 }
652 
oauth2_refresh(struct im_connection * ic,const char * refresh_token)653 int oauth2_refresh(struct im_connection *ic, const char *refresh_token)
654 {
655 	struct mastodon_data *md = ic->proto_data;
656 
657 	return oauth2_access_token(md->oauth2_service, OAUTH2_AUTH_REFRESH,
658 	                           refresh_token, oauth2_got_token, ic);
659 }
660 
661 /**
662  * Post a message. Make sure we get all the meta data for the status right.
663  */
mastodon_post_message(struct im_connection * ic,char * message,guint64 in_reply_to,char * who,mastodon_message_t type,GSList * mentions,mastodon_visibility_t visibility,char * spoiler_text)664 static void mastodon_post_message(struct im_connection *ic, char *message, guint64 in_reply_to,
665 				  char *who, mastodon_message_t type, GSList *mentions, mastodon_visibility_t visibility,
666 				  char *spoiler_text)
667 {
668 	struct mastodon_data *md = ic->proto_data;
669 	char *text = NULL;
670 	GString *m = NULL;
671 	int wlen;
672 	char *s;
673 
674 	switch (type) {
675 	case MASTODON_DIRECT:
676 		visibility = MV_DIRECT;
677 		// fall through
678 	case MASTODON_REPLY:
679 		/* Mentioning OP and other mentions is the traditional thing to do. Note that who can be NULL if we're
680 		   redoing a command like "redo 1234567 foo" where we didn't get any user info from the status id. */
681 		if (!who) break;
682 		if (g_ascii_strcasecmp(who, md->user) == 0) {
683 			/* if replying to ourselves, we still want to mention others, if any */
684 			m = mastodon_account_join(mentions, NULL);
685 		} else {
686 			/* if replying to others, mention them, too */
687 			m = mastodon_account_join(mentions, who);
688 		}
689 		if (m) {
690 			text = g_strdup_printf("%s %s", m->str, message);
691 			g_string_free(m, TRUE);
692 		}
693 		/* Note that visibility and spoiler_text have already been set, no need to do anything else. */
694 		break;
695 	case MASTODON_NEW_MESSAGE:
696 		visibility = md->visibility;
697 		/* Note that at the end we will use the default visibility if this is NULL. */
698 		break;
699 	case MASTODON_MAYBE_REPLY:
700 		{
701 			g_assert(visibility == MV_UNKNOWN);
702 			wlen = strlen(who); // length of the first word
703 
704 			// If the message starts with "nick:" or "nick,"
705 			if (who && wlen && strncmp(who, message, wlen) == 0 &&
706 			    (s = message + wlen - 1) && (*s == ':' || *s == ',')) {
707 
708 				// Trim punctuation from who.
709 				who[wlen - 1] = '\0';
710 
711 				// Determine what we are replying to.
712 				bee_user_t *bu;
713 				if ((bu = bee_user_by_handle(ic->bee, ic, who))) {
714 					struct mastodon_user_data *mud = bu->data;
715 
716 					if (time(NULL) < mud->last_time + set_getint(&ic->acc->set, "auto_reply_timeout")) {
717 						// this is a reply
718 						in_reply_to = mud->last_id;
719 						// We're always replying to at least one person. bu->handle is fully qualified unlike who
720 						m = mastodon_account_join(mud->mentions, bu->handle);
721 						visibility = mud->visibility;
722 						spoiler_text = mud->spoiler_text;
723 					} else {
724 						// this is a new message but we still need to prefix the @ and use bu->handle instead of who
725 						m = g_string_new("@");
726 						g_string_append(m, bu->handle);
727 					}
728 
729 					// use +wlen+1 to remove "nick: " (note the space) from message
730 					text = g_strdup_printf("%s %s", m->str, message + wlen + 1);
731 					g_string_free(m, TRUE);
732 
733 				} else if (g_ascii_strcasecmp(who, md->user) == 0) {
734 					/* Compare case-insensitively because this is user input. */
735 
736 					/* Same as a above but replying to myself and therefore using mastodon_data
737 					   (md). We don't set this data to NULL because we might want to send multiple
738 					   replies to ourselves. We want this to work on a slow instance, so the user
739 					   can send multiple replies without having to wait for replies to come back and
740 					   set these values again via mastodon_http_callback. */
741 					in_reply_to = md->last_id;
742 					visibility = md->last_visibility;
743 					spoiler_text = g_strdup(md->last_spoiler_text);
744 					if (md->mentions) {
745 						m = mastodon_account_join(md->mentions, NULL);
746 						mastodon_log(ic, "Mentions %s", m->str);
747 						text = g_strdup_printf("%s %s", m->str, message + wlen + 1);
748 						g_string_free(m, TRUE);
749 					} else {
750 						// use +wlen+1 to remove "nick: " (note the space) from message
751 						message += wlen + 1;
752 					}
753 				}
754 			}
755 		}
756 		break;
757 	}
758 
759 	if (!mastodon_length_check(ic, text ? text : message,
760 			     md->spoiler_text ? md->spoiler_text : spoiler_text)) {
761 		goto finish;
762 	}
763 
764 	/* If we explicitly set a visibility for the next toot, use that. Otherwise, use the visibility as determined above,
765 	 * but make sure that a higher default visibility takes precedence: higher means more private. See
766 	 * mastodon_visibility_t. */
767 	if (md->visibility != MV_UNKNOWN) {
768 		visibility = md->visibility;
769 	} else {
770 		mastodon_visibility_t default_visibility = mastodon_default_visibility(ic);
771 		if (default_visibility > visibility) visibility = default_visibility;
772 	}
773 
774 	/* md->spoiler_text set by the CW command and md->visibility set by the VISIBILITY command take precedence and get
775 	 * removed after posting. */
776 	mastodon_post_status(ic, text ? text : message, in_reply_to, visibility,
777 			     md->spoiler_text ? md->spoiler_text : spoiler_text);
778 	g_free(md->spoiler_text); md->spoiler_text = NULL;
779 	md->visibility = MV_UNKNOWN;
780 
781 finish:
782 	g_free(text);
783 	g_free(spoiler_text);
784 }
785 
786 static void mastodon_handle_command(struct im_connection *ic, char *message, mastodon_undo_t undo_type);
787 
788 /**
789  * Send a direct message. If this buddy is the magic mastodon oauth handle, then treat the message as the refresh token.
790  * If this buddy is me, then treat the message as a command. Everything else is a message to a buddy in a query.
791  */
mastodon_buddy_msg(struct im_connection * ic,char * who,char * message,int away)792 static int mastodon_buddy_msg(struct im_connection *ic, char *who, char *message, int away)
793 {
794 	struct mastodon_data *md = ic->proto_data;
795 
796 	/* OAuth message to "mastodon_oauth" */
797 	if (g_ascii_strcasecmp(who, MASTODON_OAUTH_HANDLE) == 0 &&
798 	    !(md->flags & OPT_LOGGED_IN)) {
799 
800 		if (oauth2_get_refresh_token(ic, message)) {
801 			return 1;
802 		} else {
803 			imcb_error(ic, "OAuth failure");
804 			imc_logout(ic, TRUE);
805 			return 0;
806 		}
807 	}
808 
809 	if (g_ascii_strcasecmp(who, md->name) == 0) {
810 		/* Message to ourselves */
811 		mastodon_handle_command(ic, message, MASTODON_NEW);
812 	} else {
813 		/* Determine who and to what post id we are replying to */
814 		guint64 in_reply_to = 0;
815 		bee_user_t *bu;
816 		if ((bu = bee_user_by_handle(ic->bee, ic, who))) {
817 			struct mastodon_user_data *mud = bu->data;
818 			if (time(NULL) < mud->last_direct_time + set_getint(&ic->acc->set, "auto_reply_timeout")) {
819 				/* this is a reply */
820 				in_reply_to = mud->last_direct_id;
821 			}
822 		}
823 		mastodon_post_message(ic, message, in_reply_to, who, MASTODON_REPLY, NULL, MV_DIRECT, NULL);
824 	}
825 	return 0;
826 }
827 
828 static void mastodon_user(struct im_connection *ic, char *who);
829 
mastodon_get_info(struct im_connection * ic,char * who)830 static void mastodon_get_info(struct im_connection *ic, char *who)
831 {
832 	struct mastodon_data *md = ic->proto_data;
833 	struct irc_channel *ch = md->timeline_gc->ui_data;
834 
835 	imcb_log(ic, "Sending output to %s", ch->name);
836 	if (g_ascii_strcasecmp(who, md->name) == 0) {
837 		mastodon_instance(ic);
838 	} else {
839 		mastodon_user(ic, who);
840 	}
841 }
842 
mastodon_chat_msg(struct groupchat * c,char * message,int flags)843 static void mastodon_chat_msg(struct groupchat *c, char *message, int flags)
844 {
845 	if (c && message) {
846 		mastodon_handle_command(c->ic, message, MASTODON_NEW);
847 	}
848 }
849 
850 /**
851  * Joining a group chat means showing the appropriate timeline and start streaming it.
852  */
mastodon_chat_join(struct im_connection * ic,const char * room,const char * nick,const char * password,set_t ** sets)853 static struct groupchat *mastodon_chat_join(struct im_connection *ic,
854                                            const char *room, const char *nick,
855                                            const char *password, set_t **sets)
856 {
857 	char *topic = g_strdup(room);
858 	struct groupchat *c = imcb_chat_new(ic, topic);
859 	imcb_chat_topic(c, NULL, topic, 0);
860 	imcb_chat_add_buddy(c, ic->acc->user);
861 	struct http_request *req = NULL;
862 	if (strcmp(topic, "local") == 0) {
863 		mastodon_local_timeline(ic);
864 		req = mastodon_open_local_stream(ic);
865 	} else if (strcmp(topic, "federated") == 0) {
866 		mastodon_federated_timeline(ic);
867 		req = mastodon_open_federated_stream(ic);
868 	} else if (topic[0] == '#') {
869 		mastodon_hashtag_timeline(ic, topic + 1);
870 		req = mastodon_open_hashtag_stream(ic, topic + 1);
871 	} else {
872 		/* After the initial login we cannot be sure that an initial list timeline will work because the lists are not
873 		   loaded, yet. That's why mastodon_following() will end up reloading the lists with the extra parameter which
874 		   will load these timelines. If we're creating this channel at a later point, however, this should be possible.
875 		   One way to determine if we're "at a later point" is by looking at MASTODON_HAVE_FRIENDS. It's actually not
876 		   quite correct: at this point we have the lists but not the list members, but it should be good enough as
877 		   we're only interested in later chat joining, not auto_join. */
878 		struct mastodon_data *md = ic->proto_data;
879 		if (md->flags & MASTODON_HAVE_FRIENDS) {
880 			mastodon_unknown_list_timeline(ic, topic);
881 		}
882 		/* We need to identify the list we're going to stream but we don't get a request on the return from
883 		   mastodon_open_unknown_list_stream(). Instead, we pass the channel along and when we have the list, the
884 		   request will be set accordingly. */
885 		mastodon_open_unknown_list_stream(ic, c, topic);
886 	}
887 	g_free(topic);
888 	c->data = req;
889 	return c;
890 }
891 
892 /**
893  * If the user leaves the main channel: Fine. Rejoin him/her once new toots come in. But what if the user leaves a
894  * channel that is connected to a stream? In this case we need to find the appropriate stream and close it, too.
895  */
mastodon_chat_leave(struct groupchat * c)896 static void mastodon_chat_leave(struct groupchat *c)
897 {
898 	GSList *l;
899 	struct mastodon_data *md = c->ic->proto_data;
900 
901 	if (c == md->timeline_gc) {
902 		md->timeline_gc = NULL;
903 	} else {
904 		struct http_request *stream = c->data;
905 		for (l = md->streams; l; l = l->next) {
906 			struct http_request *req = l->data;
907 			if (stream == req) {
908 				md->streams = g_slist_remove(md->streams, req);
909 				http_close(req);
910 				break;
911 			}
912 		}
913 	}
914 
915 	imcb_chat_free(c);
916 }
917 
mastodon_add_permit(struct im_connection * ic,char * who)918 static void mastodon_add_permit(struct im_connection *ic, char *who)
919 {
920 }
921 
mastodon_rem_permit(struct im_connection * ic,char * who)922 static void mastodon_rem_permit(struct im_connection *ic, char *who)
923 {
924 }
925 
mastodon_buddy_data_add(bee_user_t * bu)926 static void mastodon_buddy_data_add(bee_user_t *bu)
927 {
928 	bu->data = g_new0(struct mastodon_user_data, 1);
929 }
930 
mastodon_buddy_data_free(bee_user_t * bu)931 static void mastodon_buddy_data_free(bee_user_t *bu)
932 {
933 	struct mastodon_user_data *mud = (struct mastodon_user_data*) bu->data;
934 	g_slist_free_full(mud->lists, g_free); mud->lists = NULL;
935 	g_slist_free_full(mud->mentions, g_free); mud->mentions = NULL;
936 	g_free(mud->spoiler_text); mud->spoiler_text = NULL;
937 	g_free(bu->data);
938 }
939 
940 bee_user_t mastodon_log_local_user;
941 
942 /**
943  * Find a user account based on their nick name.
944  */
mastodon_user_by_nick(struct im_connection * ic,char * nick)945 static bee_user_t *mastodon_user_by_nick(struct im_connection *ic, char *nick)
946 {
947 	GSList *l;
948 	for (l = ic->bee->users; l; l = l->next) {
949 		bee_user_t *bu = l->data;
950 		irc_user_t *iu = bu->ui_data;
951 		if (g_ascii_strcasecmp(iu->nick, nick) == 0) {
952 			/* Compare case-insentively because this is user input. */
953 			return bu;
954 		}
955 	}
956 	return NULL;
957 }
958 
959 /**
960  * Convert the given bitlbee toot ID or bitlbee username into a
961  * mastodon status ID and returns it. If provided with a pointer to a
962  * bee_user_t, fills that as well. Provide NULL if you don't need it.
963  * The same is true for mentions, visibility and spoiler text.
964  *
965  * Returns 0 if the user provides garbage.
966  */
mastodon_message_id_from_command_arg(struct im_connection * ic,char * arg,bee_user_t ** bu_,GSList ** mentions_,mastodon_visibility_t * visibility_,char ** spoiler_text_)967 static guint64 mastodon_message_id_from_command_arg(struct im_connection *ic, char *arg, bee_user_t **bu_,
968 						    GSList **mentions_, mastodon_visibility_t *visibility_,
969 						    char **spoiler_text_)
970 {
971 	struct mastodon_data *md = ic->proto_data;
972 	struct mastodon_user_data *mud;
973 	bee_user_t *bu = NULL;
974 	guint64 id = 0;
975 
976 	if (bu_) {
977 		*bu_ = NULL;
978 	}
979 	if (!arg || !arg[0]) {
980 		return 0;
981 	}
982 
983 	if (arg[0] != '#' && (bu = mastodon_user_by_nick(ic, arg))) {
984 		if ((mud = bu->data)) {
985 			id = mud->last_id;
986 			if (mentions_) *mentions_ = mud->mentions;
987 			if (visibility_) *visibility_ = mud->visibility;
988 			if (spoiler_text_) *spoiler_text_ = mud->spoiler_text;
989 		}
990 	} else {
991 		if (arg[0] == '#') {
992 			arg++;
993 		}
994 		if (parse_int64(arg, 16, &id) && id < MASTODON_LOG_LENGTH) {
995 			if (mentions_) *mentions_ = md->log[id].mentions;
996 			if (visibility_) *visibility_ = md->log[id].visibility;
997 			if (spoiler_text_) *spoiler_text_ = md->log[id].spoiler_text;
998 			bu = md->log[id].bu;
999 			id = md->log[id].id;
1000 		} else if (parse_int64(arg, 10, &id)) {
1001 			/* Allow normal toot IDs as well. Required do undo posts, for example. */
1002 		} else {
1003 			/* Reset id if id was a valid hex number but >= MASTODON_LOG_LENGTH. */
1004 			id = 0;
1005 		}
1006 	}
1007 	if (bu_) {
1008 		if (bu == &mastodon_log_local_user) {
1009 			/* HACK alert. There's no bee_user object for the local
1010 			 * user so just fake one for the few cmds that need it. */
1011 			mastodon_log_local_user.handle = md->user;
1012 		} else {
1013 			/* Beware of dangling pointers! */
1014 			if (!g_slist_find(ic->bee->users, bu)) {
1015 				bu = NULL;
1016 			}
1017 		}
1018 		*bu_ = bu;
1019 	}
1020 	return id;
1021 }
1022 
mastodon_no_id_warning(struct im_connection * ic,char * what)1023 static void mastodon_no_id_warning(struct im_connection *ic, char *what)
1024 {
1025 	mastodon_log(ic, "User or status '%s' is unknown.", what);
1026 }
1027 
mastodon_unknown_user_warning(struct im_connection * ic,char * who)1028 static void mastodon_unknown_user_warning(struct im_connection *ic, char *who)
1029 {
1030 	mastodon_log(ic, "User '%s' is unknown.", who);
1031 }
1032 
1033 /**
1034  * Get the message id given a nick or a status id. If possible, also set a number of other variables by reference.
1035  */
mastodon_message_id_or_warn_and_more(struct im_connection * ic,char * what,bee_user_t ** bu,GSList ** mentions,mastodon_visibility_t * visibility,char ** spoiler_text)1036 static guint64 mastodon_message_id_or_warn_and_more(struct im_connection *ic, char *what, bee_user_t **bu,
1037 					   GSList **mentions, mastodon_visibility_t *visibility, char **spoiler_text)
1038 {
1039 	guint64 id = mastodon_message_id_from_command_arg(ic, what, bu, mentions, visibility, spoiler_text);
1040 	if (!id) {
1041 		mastodon_no_id_warning(ic, what);
1042 	}
1043 	return id;
1044 }
1045 
1046 /**
1047  * Simple interface to mastodon_message_id_or_warn_and_more. Get the message id given a nick or a status id.
1048  */
mastodon_message_id_or_warn(struct im_connection * ic,char * what)1049 static guint64 mastodon_message_id_or_warn(struct im_connection *ic, char *what)
1050 {
1051 	return mastodon_message_id_or_warn_and_more(ic, what, NULL, NULL, NULL, NULL);
1052 }
1053 
mastodon_account_id(bee_user_t * bu)1054 static guint64 mastodon_account_id(bee_user_t *bu) {
1055 	struct mastodon_user_data *mud;
1056 	if (bu != NULL && (mud = bu->data)) {
1057 		return mud->account_id;
1058 	}
1059 	return 0;
1060 }
1061 
mastodon_user_id_or_warn(struct im_connection * ic,char * who)1062 static guint64 mastodon_user_id_or_warn(struct im_connection *ic, char *who)
1063 {
1064 	bee_user_t *bu;
1065 	guint64 id;
1066 	if ((bu = mastodon_user_by_nick(ic, who)) &&
1067 	    (id = mastodon_account_id(bu))) {
1068 		return id;
1069 	} else if (parse_int64(who, 10, &id)) {
1070 		return id;
1071 	}
1072 	mastodon_unknown_user_warning(ic, who);
1073 	return 0;
1074 }
1075 
mastodon_user(struct im_connection * ic,char * who)1076 static void mastodon_user(struct im_connection *ic, char *who)
1077 {
1078 	bee_user_t *bu;
1079 	guint64 id;
1080 	if ((bu = mastodon_user_by_nick(ic, who)) &&
1081 	    (id = mastodon_account_id(bu))) {
1082 		mastodon_account(ic, id);
1083 	} else {
1084 		mastodon_search_account(ic, who);
1085 	}
1086 }
1087 
mastodon_relation_to_user(struct im_connection * ic,char * who)1088 static void mastodon_relation_to_user(struct im_connection *ic, char *who)
1089 {
1090 	bee_user_t *bu;
1091 	guint64 id;
1092 	if ((bu = mastodon_user_by_nick(ic, who)) &&
1093 	    (id = mastodon_account_id(bu))) {
1094 		mastodon_relationship(ic, id);
1095 	} else {
1096 		mastodon_search_relationship(ic, who);
1097 	}
1098 }
1099 
mastodon_add_buddy(struct im_connection * ic,char * who,char * group)1100 static void mastodon_add_buddy(struct im_connection *ic, char *who, char *group)
1101 {
1102 	bee_user_t *bu;
1103 	guint64 id;
1104 	if ((bu = mastodon_user_by_nick(ic, who)) &&
1105 	    (id = mastodon_account_id(bu))) {
1106 		// If the nick is already in the channel (when we just
1107 		// unfollowed them, for example), we're taking a
1108 		// shortcut. No fancy looking at the relationship and
1109 		// all that. The nick is already here, after all.
1110 		mastodon_post(ic, MASTODON_ACCOUNT_FOLLOW_URL, MC_FOLLOW, id);
1111 	} else if (parse_int64(who, 10, &id)) {
1112 		// If we provided a numerical id, then that will also
1113 		// work. This is used by redo/undo.
1114 		mastodon_post(ic, MASTODON_ACCOUNT_FOLLOW_URL, MC_FOLLOW, id);
1115 	} else {
1116 		// Alternatively, we're looking for an unknown user.
1117 		// They must be searched, followed, and added to the
1118 		// channel. It's going to take more requests.
1119 		mastodon_follow(ic, who);
1120 	}
1121 }
1122 
mastodon_remove_buddy(struct im_connection * ic,char * who,char * group)1123 static void mastodon_remove_buddy(struct im_connection *ic, char *who, char *group)
1124 {
1125 	guint64 id;
1126 	if ((id = mastodon_user_id_or_warn(ic, who))) {
1127 		mastodon_post(ic, MASTODON_ACCOUNT_UNFOLLOW_URL, MC_UNFOLLOW, id);
1128 	}
1129 }
1130 
mastodon_add_deny(struct im_connection * ic,char * who)1131 static void mastodon_add_deny(struct im_connection *ic, char *who)
1132 {
1133 	guint64 id;
1134 	if ((id = mastodon_user_id_or_warn(ic, who))) {
1135 		mastodon_post(ic, MASTODON_ACCOUNT_BLOCK_URL, MC_BLOCK, id);
1136 	}
1137 }
1138 
mastodon_rem_deny(struct im_connection * ic,char * who)1139 static void mastodon_rem_deny(struct im_connection *ic, char *who)
1140 {
1141 	guint64 id;
1142 	if ((id = mastodon_user_id_or_warn(ic, who))) {
1143 		mastodon_post(ic, MASTODON_ACCOUNT_UNBLOCK_URL, MC_UNBLOCK, id);
1144 	}
1145 }
1146 
1147 /**
1148  * Add a command and a way to undo it to the undo stack. Remember that
1149  * only the callback knows whether a command succeeded or not, and
1150  * what the id of a newly posted status is, and all that. Thus,
1151  * there's a delay that we need to take into account.
1152  *
1153  * The stack is organized as follows if we just did D:
1154  *           0 1 2 3 4 5 6 7 8 9
1155  *   undo = [a b c d e f g h i j]
1156  *   redo = [A B C D E F G H I J]
1157  *   first_undo = 3
1158  *   current_undo = 3
1159  * If we do X:
1160  *   undo = [a b c d x f g h i j]
1161  *   redo = [A B C D X F G H I J]
1162  *   first_undo = 4
1163  *   current_undo = 4
1164  * If we undo it, send x and:
1165  *   undo = [a b c d x f g h i j]
1166  *   redo = [A B C D X F G H I J]
1167  *   first_undo = 4
1168  *   current_undo = 3
1169  * If we redo, send X and increase current_undo.
1170  * If we undo instead, send d and decrease current_undo again:
1171  *   undo = [a b c d x f g h i j]
1172  *   redo = [A B C D X F G H I J]
1173  *   first_undo = 4
1174  *   current_undo = 2
1175  * If we do Y with current_undo different from first_undo, null the tail:
1176  *  undo = [a b c y 0 f g h i j]
1177  *  redo = [A B C Y 0 F G H I J]
1178  *  first_undo = 3
1179  *  current_undo = 3
1180  */
mastodon_do(struct im_connection * ic,char * redo,char * undo)1181 void mastodon_do(struct im_connection *ic, char *redo, char *undo) {
1182 	struct mastodon_data *md = ic->proto_data;
1183 	int i = (md->current_undo + 1) % MASTODON_MAX_UNDO;
1184 
1185 	g_free(md->redo[i]);
1186 	g_free(md->undo[i]);
1187 	md->redo[i] = redo;
1188 	md->undo[i] = undo;
1189 
1190 	if (md->current_undo == md->first_undo) {
1191 		md->current_undo = md->first_undo = i;
1192 	} else {
1193 		md->current_undo = i;
1194 		int end = (md->first_undo + 1) % MASTODON_MAX_UNDO;
1195 		for (i = (md->current_undo + 1) % MASTODON_MAX_UNDO; i != end; i = (i + 1) % MASTODON_MAX_UNDO) {
1196 			g_free(md->redo[i]);
1197 			g_free(md->undo[i]);
1198 			md->redo[i] = NULL;
1199 			md->undo[i] = NULL;
1200 		}
1201 
1202 		md->first_undo = md->current_undo;
1203 	}
1204 }
1205 
1206 /**
1207  * Undo the last command.
1208  */
mastodon_undo(struct im_connection * ic)1209 void mastodon_undo(struct im_connection *ic) {
1210 	struct mastodon_data *md = ic->proto_data;
1211 	char *cmd = md->undo[md->current_undo];
1212 
1213 	if (!cmd) {
1214 		mastodon_log(ic, "There is nothing to undo.");
1215 		return;
1216 	}
1217 
1218 	gchar **cmds = g_strsplit (cmd, FS, -1);
1219 
1220 	int i;
1221 	for (i = 0; cmds[i]; i++) {
1222 		mastodon_handle_command(ic, cmds[i], MASTODON_UNDO);
1223 	}
1224 
1225 	g_strfreev(cmds);
1226 
1227 	// beware of negatives and modulo
1228 	md->current_undo = (md->current_undo + MASTODON_MAX_UNDO - 1) % MASTODON_MAX_UNDO;
1229 }
1230 
1231 /**
1232  * Redo the last command. Multiple commands can be executed as one using the ASCII Field Separator (FS).
1233  */
mastodon_redo(struct im_connection * ic)1234 void mastodon_redo(struct im_connection *ic) {
1235 	struct mastodon_data *md = ic->proto_data;
1236 
1237 	if (md->current_undo == md->first_undo) {
1238 		mastodon_log(ic, "There is nothing to redo.");
1239 		return;
1240 	}
1241 
1242 	md->current_undo = (md->current_undo + 1) % MASTODON_MAX_UNDO;
1243 	char *cmd = md->redo[md->current_undo];
1244 
1245 	gchar **cmds = g_strsplit (cmd, FS, -1);
1246 
1247 	int i;
1248 	for (i = 0; cmds[i]; i++) {
1249 		mastodon_handle_command(ic, cmds[i], MASTODON_REDO);
1250 	}
1251 
1252 	g_strfreev(cmds);
1253 }
1254 
1255 /**
1256  * Update the current command in the stack. This is necessary when
1257  * executing commands which change references that we saved. For
1258  * example: every delete statement refers to an id. Whenever a post
1259  * happens because of redo, the delete command in the undo stack has
1260  * to be replaced. Whenever a post happens because of undo, the delete
1261  * command in the redo stack has to be replaced.
1262  *
1263  * We make our own copies of 'to'.
1264  */
mastodon_do_update(struct im_connection * ic,char * to)1265 void mastodon_do_update(struct im_connection *ic, char *to)
1266 {
1267 	struct mastodon_data *md = ic->proto_data;
1268 	char *from = NULL;
1269 	int i;
1270 
1271 	switch (md->undo_type) {
1272 	case MASTODON_NEW:
1273 		// should not happen
1274 		return;
1275 	case MASTODON_UNDO:
1276 		// after post due to undo of a delete statement, the
1277 		// old delete statement is in the next redo element
1278 		i = (md->current_undo + 1) % MASTODON_MAX_UNDO;
1279 		from = g_strdup(md->redo[i]);
1280 		break;
1281 	case MASTODON_REDO:
1282 		// after post due to redo of a post statement, the
1283 		// old delete statement is in the undo element
1284 		i = md->current_undo;
1285 		from = g_strdup(md->undo[i]);
1286 		break;
1287 	}
1288 
1289 	/* After a post and a delete of that post, there are at least
1290 	 * two cells where the old reference can be hiding (undo of
1291 	 * the post and redo of the delete). Brute force! */
1292 	for (i = 0; i < MASTODON_MAX_UNDO; i++) {
1293 		if (md->undo[i] && strcmp(from, md->undo[i]) == 0) {
1294 			g_free(md->undo[i]);
1295 			md->undo[i] = g_strdup(to);
1296 			break;
1297 		}
1298 	}
1299 	for (i = 0; i < MASTODON_MAX_UNDO; i++) {
1300 		if (md->redo[i] && strcmp(from, md->redo[i]) == 0) {
1301 			g_free(md->redo[i]);
1302 			md->redo[i] = g_strdup(to);
1303 			break;
1304 		}
1305 	}
1306 
1307 	g_free(from);
1308 }
1309 
1310 /**
1311  * Show the current history. The history shows the redo
1312  * commands.
1313  */
mastodon_history(struct im_connection * ic,gboolean undo_history)1314 void mastodon_history(struct im_connection *ic, gboolean undo_history) {
1315 	struct mastodon_data *md = ic->proto_data;
1316 	int i;
1317 	for (i = 0; i < MASTODON_MAX_UNDO; i++) {
1318 		// start with the last
1319 		int n = (md->first_undo + i + 1) % MASTODON_MAX_UNDO;
1320 		char *cmd = undo_history ? md->undo[n] : md->redo[n];
1321 
1322 		if (cmd) {
1323 			gchar **cmds = g_strsplit (cmd, FS, -1);
1324 
1325 			int j;
1326 			for (j = 0; cmds[j]; j++) {
1327 				if (n == md->current_undo) {
1328 					mastodon_log(ic, "%02d > %s", MASTODON_MAX_UNDO - i, cmds[j]);
1329 				} else {
1330 					mastodon_log(ic, "%02d %s", MASTODON_MAX_UNDO - i, cmds[j]);
1331 				}
1332 			}
1333 
1334 			g_strfreev(cmds);
1335 		}
1336 	}
1337 }
1338 
1339 /**
1340  * Commands we understand. Changes should be documented in
1341  * doc/mastodon-help.txt and on https://wiki.bitlbee.org/HowtoMastodon
1342  */
mastodon_handle_command(struct im_connection * ic,char * message,mastodon_undo_t undo_type)1343 static void mastodon_handle_command(struct im_connection *ic, char *message, mastodon_undo_t undo_type)
1344 {
1345 	struct mastodon_data *md = ic->proto_data;
1346 	gboolean allow_post = g_ascii_strcasecmp(set_getstr(&ic->acc->set, "commands"), "strict") != 0;
1347 	bee_user_t *bu = NULL;
1348 	guint64 id;
1349 
1350 	md->undo_type = undo_type;
1351 
1352 	char *cmds = g_strdup(message);
1353 	char **cmd = split_command_parts(cmds, 2);
1354 
1355 	if (cmd[0] == NULL) {
1356 		/* Nothing to do */
1357 	} else if (!set_getbool(&ic->acc->set, "commands") && allow_post) {
1358 		/* Not supporting commands if "commands" is set to true/strict. */
1359 	} else if (g_ascii_strcasecmp(cmd[0], "help") == 0) {
1360 		/* For unsupported undo and redo commands. */
1361 		mastodon_log(ic, "Please use help mastodon in the control channel, &bitlbee.");
1362 	} else if (g_ascii_strcasecmp(cmd[0], "info") == 0) {
1363 		if (!cmd[1]) {
1364 			mastodon_log(ic, "Usage:\n"
1365 				     "- info instance\n"
1366 				     "- info [id|screenname]\n"
1367 				     "- info user [nick|account]\n"
1368 				     "- info relation [nick|account]\n"
1369 				     "- info [get|put|post|delete] url [args]");
1370 		} else if (g_ascii_strcasecmp(cmd[1], "instance") == 0) {
1371 			mastodon_instance(ic);
1372 		} else if (g_ascii_strcasecmp(cmd[1], "user") == 0) {
1373 			if (cmd[2]) {
1374 				mastodon_user(ic, cmd[2]);
1375 			} else {
1376 				mastodon_log(ic, "User info about whom?");
1377 			}
1378 		} else if (g_ascii_strcasecmp(cmd[1], "relation") == 0) {
1379 			if (cmd[2]) {
1380 				mastodon_relation_to_user(ic, cmd[2]);
1381 			} else {
1382 				mastodon_log(ic, "Relation with whom?");
1383 			}
1384 		} else if ((id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1385 			mastodon_status(ic, id);
1386 		}
1387 	} else if (g_ascii_strcasecmp(cmd[0], "api") == 0) {
1388 		if (!cmd[1] || !cmd[2]) {
1389 			mastodon_log(ic, "Usage: api [get|put|post|delete] endpoint params...\n"
1390 						 "Example: api post /lists/12/accounts account_ids[] 321");
1391 		} else if ((g_ascii_strcasecmp(cmd[1], "get") == 0 ||
1392 					g_ascii_strcasecmp(cmd[1], "put") == 0 ||
1393 					g_ascii_strcasecmp(cmd[1], "post") == 0 ||
1394 					g_ascii_strcasecmp(cmd[1], "delete") == 0) && cmd[2]) {
1395 			char *s = strstr(cmd[2], " ");
1396 			if (s) {
1397 				*s = '\0';
1398 				char **args = g_strsplit(s+1, " ", 0);
1399 				/* find length of null-terminated vector */
1400 				int i = 0;
1401 				for (; args[i]; i++);
1402 				if (i % 2) {
1403 					mastodon_log(ic, "Wrong number of arguments. Did you forget the URL?");
1404 				} else {
1405 					mastodon_raw(ic, cmd[1], cmd[2], args, i);
1406 				}
1407 				g_strfreev(args);
1408 			} else {
1409 				mastodon_raw(ic, cmd[1], cmd[2], NULL, 0);
1410 			}
1411 		} else {
1412 			mastodon_log(ic, "Usage: 'api [get|put|post|delete] url [name value]*");
1413 		}
1414 	} else if (g_ascii_strcasecmp(cmd[0], "undo") == 0) {
1415 		if (cmd[1] == NULL) {
1416 			mastodon_undo(ic);
1417 		} else {
1418 			// because it used to take an argument
1419 			mastodon_log(ic, "Undo takes no arguments.");
1420 		}
1421 	} else if (g_ascii_strcasecmp(cmd[0], "redo") == 0) {
1422 		if (cmd[1] == NULL) {
1423 			mastodon_redo(ic);
1424 		} else {
1425 			mastodon_log(ic, "Redo takes no arguments.");
1426 		}
1427 	} else if (g_ascii_strcasecmp(cmd[0], "his") == 0 ||
1428 		   g_ascii_strcasecmp(cmd[0], "history") == 0) {
1429 		if (cmd[1] && g_ascii_strcasecmp(cmd[1], "undo") == 0) {
1430 			mastodon_history(ic, TRUE);
1431 		} else if (cmd[1] == NULL) {
1432 			mastodon_history(ic, FALSE);
1433 		} else {
1434 			mastodon_log(ic, "History only takes the optional undo argument.");
1435 		}
1436 	} else if (g_ascii_strcasecmp(cmd[0], "del") == 0 ||
1437 		   g_ascii_strcasecmp(cmd[0], "delete") == 0) {
1438 		if (cmd[1] == NULL && md->last_id) {
1439 			mastodon_status_delete(ic, md->last_id);
1440 		} else if (cmd[1] && (id = mastodon_message_id_from_command_arg(ic, cmd[1], NULL, NULL, NULL, NULL))) {
1441 			mastodon_status_delete(ic, id);
1442 		} else {
1443 			mastodon_log(ic, "Could not delete the last post.");
1444 		}
1445 	} else if ((g_ascii_strcasecmp(cmd[0], "favourite") == 0 ||
1446 	            g_ascii_strcasecmp(cmd[0], "favorite") == 0 ||
1447 	            g_ascii_strcasecmp(cmd[0], "fav") == 0 ||
1448 	            g_ascii_strcasecmp(cmd[0], "like") == 0)) {
1449 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1450 			mastodon_post(ic, MASTODON_STATUS_FAVOURITE_URL, MC_FAVOURITE, id);
1451 		} else {
1452 			mastodon_log(ic, "Huh? Please provide a log number or nick.");
1453 		}
1454 	} else if ((g_ascii_strcasecmp(cmd[0], "unfavourite") == 0 ||
1455 	            g_ascii_strcasecmp(cmd[0], "unfavorite") == 0 ||
1456 	            g_ascii_strcasecmp(cmd[0], "unfav") == 0 ||
1457 	            g_ascii_strcasecmp(cmd[0], "unlike") == 0 ||
1458 	            g_ascii_strcasecmp(cmd[0], "dislike") == 0)) {
1459 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1460 			mastodon_post(ic, MASTODON_STATUS_UNFAVOURITE_URL, MC_UNFAVOURITE, id);
1461 		} else {
1462 			mastodon_log(ic, "What? Please provide a log number or nick.");
1463 		}
1464 	} else if (g_ascii_strcasecmp(cmd[0], "pin") == 0) {
1465 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1466 			mastodon_post(ic, MASTODON_STATUS_PIN_URL, MC_PIN, id);
1467 		} else {
1468 			mastodon_log(ic, "Sorry, what? Please provide a log number or nick.");
1469 		}
1470 	} else if (g_ascii_strcasecmp(cmd[0], "unpin") == 0) {
1471 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1472 			mastodon_post(ic, MASTODON_STATUS_UNPIN_URL, MC_UNPIN, id);
1473 		} else {
1474 			mastodon_log(ic, "No can do! I need a a log number or nick.");
1475 		}
1476 	} else if (g_ascii_strcasecmp(cmd[0], "follow") == 0) {
1477 		if (cmd[1]) {
1478 			mastodon_add_buddy(ic, cmd[1], NULL);
1479 		} else {
1480 			mastodon_log(ic, "I'm confused! Follow whom?");
1481 		}
1482 	} else if (g_ascii_strcasecmp(cmd[0], "unfollow") == 0) {
1483 		if (cmd[1]) {
1484 			mastodon_remove_buddy(ic, cmd[1], NULL);
1485 		} else {
1486 			mastodon_log(ic, "Unfollow whom?");
1487 		}
1488 	} else if (g_ascii_strcasecmp(cmd[0], "block") == 0) {
1489 		if (cmd[1]) {
1490 			mastodon_add_deny(ic, cmd[1]);
1491 		} else {
1492 			mastodon_log(ic, "Whom should I block?");
1493 		}
1494 	} else if (g_ascii_strcasecmp(cmd[0], "unblock") == 0 ||
1495 			   g_ascii_strcasecmp(cmd[0], "allow") == 0) {
1496 		if (cmd[1]) {
1497 			mastodon_rem_deny(ic, cmd[1]);
1498 		} else {
1499 			mastodon_log(ic, "Unblock who?");
1500 		}
1501 	} else if (g_ascii_strcasecmp(cmd[0], "mute") == 0 &&
1502 			   g_ascii_strcasecmp(cmd[1], "user") == 0) {
1503 		if (cmd[2] && (id = mastodon_user_id_or_warn(ic, cmd[2]))) {
1504 			mastodon_post(ic, MASTODON_ACCOUNT_MUTE_URL, MC_ACCOUNT_MUTE, id);
1505 		} else {
1506 			mastodon_log(ic, "Mute user? I also need a nick!");
1507 		}
1508 	} else if (g_ascii_strcasecmp(cmd[0], "unmute") == 0 &&
1509 			   g_ascii_strcasecmp(cmd[1], "user") == 0) {
1510 		if (cmd[2] && (id = mastodon_user_id_or_warn(ic, cmd[2]))) {
1511 			mastodon_post(ic, MASTODON_ACCOUNT_UNMUTE_URL, MC_ACCOUNT_UNMUTE, id);
1512 		} else {
1513 			mastodon_log(ic, "Sure, unmute a user. But who is it? Give me a nick!");
1514 		}
1515 	} else if (g_ascii_strcasecmp(cmd[0], "mute") == 0) {
1516 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1517 			mastodon_post(ic, MASTODON_STATUS_MUTE_URL, MC_STATUS_MUTE, id);
1518 		} else {
1519 			mastodon_log(ic, "Muting? Please provide a log number or nick!");
1520 		}
1521 	} else if (g_ascii_strcasecmp(cmd[0], "unmute") == 0) {
1522 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1523 			mastodon_post(ic, MASTODON_STATUS_UNMUTE_URL, MC_STATUS_UNMUTE, id);
1524 		} else {
1525 			mastodon_log(ic, "OK, I'll unmute something. But what? I need a log number or nick.");
1526 		}
1527 	} else if (g_ascii_strcasecmp(cmd[0], "boost") == 0) {
1528 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1529 			mastodon_post(ic, MASTODON_STATUS_BOOST_URL, MC_BOOST, id);
1530 		} else {
1531 			mastodon_log(ic, "Failed to boost! Please provide a log number or nick.");
1532 		}
1533 	} else if (g_ascii_strcasecmp(cmd[0], "unboost") == 0) {
1534 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1535 			mastodon_post(ic, MASTODON_STATUS_UNBOOST_URL, MC_UNBOOST, id);
1536 		} else {
1537 			mastodon_log(ic, "Argh, #fail! Please provide a log number or nick.");
1538 		}
1539 	} else if (g_ascii_strcasecmp(cmd[0], "url") == 0) {
1540 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1541 			mastodon_status_show_url(ic, id);
1542 		} else {
1543 			mastodon_log(ic, "This is confusing. Do you have a log number or nick?");
1544 		}
1545 	} else if ((g_ascii_strcasecmp(cmd[0], "whois") == 0 ||
1546 		    g_ascii_strcasecmp(cmd[0], "who") == 0)) {
1547 		if (!cmd[1]) {
1548 			mastodon_log(ic, "The IRC command /names should give you a list.");
1549 		} else if ((bu = mastodon_user_by_nick(ic, cmd[1]))) {
1550 			mastodon_log(ic, "%s [%s]", bu->handle, bu->fullname);
1551 		} else if ((parse_int64(cmd[1], 16, &id) && id < MASTODON_LOG_LENGTH)) {
1552 			mastodon_show_mentions(ic, md->log[id].mentions);
1553 		} else if ((parse_int64(cmd[1], 10, &id))) {
1554 			mastodon_status_show_mentions(ic, id);
1555 		} else if (g_ascii_strcasecmp(cmd[1], md->user) == 0) {
1556 			mastodon_log(ic, "This is you!");
1557 		} else {
1558 			mastodon_unknown_user_warning(ic, cmd[1]);
1559 		}
1560 	} else if (g_ascii_strcasecmp(cmd[0], "report") == 0 ||
1561 			   g_ascii_strcasecmp(cmd[0], "spam") == 0) {
1562 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1563 			if (!cmd[2] || strlen(cmd[2]) == 0) {
1564 				mastodon_log(ic, "You must provide a comment with your report.");
1565 			} else {
1566 				mastodon_report(ic, id, cmd[2]);
1567 			}
1568 		} else {
1569 			mastodon_log(ic, "I need a log number or nick, and a comment!");
1570 		}
1571 	} else if (g_ascii_strcasecmp(cmd[0], "search") == 0) {
1572 		if (cmd[1]) {
1573 			mastodon_search(ic, cmd[1]);
1574 		} else {
1575 			mastodon_log(ic, "Sure, but what?");
1576 		}
1577 	} else if (g_ascii_strcasecmp(cmd[0], "context") == 0) {
1578 		if (cmd[1] && (id = mastodon_message_id_or_warn(ic, cmd[1]))) {
1579 			mastodon_context(ic, id);
1580 		} else {
1581 			mastodon_log(ic, "Context of what, though? Please provide a log number or nick.");
1582 		}
1583 	} else if (g_ascii_strcasecmp(cmd[0], "timeline") == 0) {
1584 		if (!cmd[1] || strcmp(cmd[1], "home") == 0) {
1585 			mastodon_home_timeline(ic);
1586 		} else if ((bu = mastodon_user_by_nick(ic, cmd[1])) &&
1587 				   (id = mastodon_account_id(bu))) {
1588 			mastodon_account_statuses(ic, id);
1589 		} else if (*cmd[1] == '#') {
1590 			mastodon_hashtag_timeline(ic, cmd[1] + 1);
1591 		} else if (*cmd[1] == '@') {
1592 			mastodon_unknown_account_statuses(ic, cmd[1] + 1);
1593 		} else if (strcmp(cmd[1], "local") == 0) {
1594 			mastodon_local_timeline(ic);
1595 		} else if (strcmp(cmd[1], "federated") == 0) {
1596 			mastodon_federated_timeline(ic);
1597 		} else {
1598 			mastodon_unknown_list_timeline(ic, message + 9); // "timeline %s"
1599 		}
1600 	} else if (g_ascii_strcasecmp(cmd[0], "notifications") == 0) {
1601 		if (cmd[1] == NULL) {
1602 			mastodon_notifications(ic);
1603 		} else {
1604 			mastodon_log(ic, "Notifications takes no arguments.");
1605 		}
1606 	} else if (g_ascii_strcasecmp(cmd[0], "pinned") == 0) {
1607 		if (!cmd[1]) {
1608 			mastodon_log(ic, "Pin the void? I need a nick or an account.");
1609 		} else if ((bu = mastodon_user_by_nick(ic, cmd[1])) &&
1610 				   (id = mastodon_account_id(bu))) {
1611 			mastodon_account_pinned_statuses(ic, id);
1612 		} else {
1613 			mastodon_unknown_account_pinned_statuses(ic, cmd[1]);
1614 		}
1615 	} else if (g_ascii_strcasecmp(cmd[0], "bio") == 0) {
1616 		if (!cmd[1]) {
1617 			mastodon_log(ic, "Bio what? Please provide a nick or an account.");
1618 		} else if ((bu = mastodon_user_by_nick(ic, cmd[1])) &&
1619 				   (id = mastodon_account_id(bu))) {
1620 			mastodon_account_bio(ic, id);
1621 		} else {
1622 			mastodon_unknown_account_bio(ic, cmd[1]);
1623 		}
1624 	} else if (g_ascii_strcasecmp(cmd[0], "more") == 0) {
1625 		if (cmd[1]) {
1626 			mastodon_log(ic, "More takes no arguments.");
1627 		} else if (md->next_url) {
1628 			mastodon_more(ic);
1629 		} else {
1630 			mastodon_log(ic, "More of what? Use the timeline command, first.");
1631 		}
1632 	} else if (g_ascii_strcasecmp(cmd[0], "list") == 0) {
1633 		if (!cmd[1]) {
1634 			mastodon_lists(ic);
1635 		} else if (g_ascii_strcasecmp(cmd[1], "create") == 0) {
1636 			if (!cmd[2]) {
1637 				mastodon_log(ic, "You forgot the title of the new list!");
1638 			} else {
1639 				mastodon_list_create(ic, message + 12); // "list create %s"
1640 			}
1641 		} else if (g_ascii_strcasecmp(cmd[1], "reload") == 0) {
1642 			if (cmd[2]) {
1643 				mastodon_log(ic, "List reloading takes no argument");
1644 			} else {
1645 				mastodon_list_reload(ic, FALSE);
1646 			}
1647 		} else if (g_ascii_strcasecmp(cmd[1], "delete") == 0) {
1648 			if (!cmd[2]) {
1649 				mastodon_log(ic, "Which list should be deleted? Use list to find out.");
1650 			} else {
1651 				mastodon_unknown_list_delete(ic, message + 12); // "list delete %s"
1652 			}
1653 		} else if (g_ascii_strcasecmp(cmd[1], "add") == 0) {
1654 			char **args = g_strsplit(cmd[2], " to ", 2);
1655 			if (args[0] && args[1] && (id = mastodon_user_id_or_warn(ic, args[0]))) {
1656 				mastodon_unknown_list_add_account(ic, id, args[1]);
1657 			} else {
1658 				mastodon_log(ic, "I am confused. Please use list add <nick> to <list>.");
1659 			}
1660 			g_strfreev(args);
1661 		} else if (g_ascii_strcasecmp(cmd[1], "remove") == 0) {
1662 			char **args = g_strsplit(cmd[2], " from ", 2);
1663 			if (args[0] && args[1] && (id = mastodon_user_id_or_warn(ic, args[0]))) {
1664 				mastodon_unknown_list_remove_account(ic, id, args[1]);
1665 			} else {
1666 				mastodon_log(ic, "I need to what to do! Use list remove <nick> from <list>.");
1667 			}
1668 			g_strfreev(args);
1669 		} else {
1670 			mastodon_unknown_list_accounts(ic, message + 5); // "list %s"
1671 		}
1672 	} else if (g_ascii_strcasecmp(cmd[0], "filter") == 0) {
1673 		if (!cmd[1]) {
1674 			mastodon_filters(ic);
1675 		} else if (g_ascii_strcasecmp(cmd[1], "create") == 0) {
1676 			if (!cmd[2]) {
1677 				mastodon_log(ic, "What do you want to filter?");
1678 			} else {
1679 				mastodon_filter_create(ic, message + 14); // "filter create %s"
1680 			}
1681 		} else if (g_ascii_strcasecmp(cmd[1], "delete") == 0) {
1682 			if (!cmd[2]) {
1683 				mastodon_log(ic, "Which filter should be deleted? Use filter to find out.");
1684 			} else {
1685 				mastodon_filter_delete(ic, cmd[2]);
1686 			}
1687 		} else {
1688 			mastodon_log(ic, "I only understand the filter subcommands create and delete.");
1689 		}
1690 	} else if (g_ascii_strcasecmp(cmd[0], "reply") == 0) {
1691 		if (!cmd[1] || !cmd[2]) {
1692 			mastodon_log(ic, "Sorry, what? Please provide a log number or nick, and your reply.");
1693 		} else {
1694 			/* These three variables will be set, if we find the toot we are replying to in our log or in the
1695 			 * mastodon_user_data (mud). If we are replying to a fixed id, then we'll get an id and the three variables
1696 			 * remain untouched, so handle them with care. */
1697 			GSList *mentions = NULL;
1698 			char *spoiler_text = NULL;
1699 			mastodon_visibility_t visibility = MV_UNKNOWN;
1700 			if ((id = mastodon_message_id_or_warn_and_more(ic, cmd[1], &bu, &mentions, &visibility, &spoiler_text))) {
1701 				mastodon_visibility_t default_visibility = mastodon_default_visibility(ic);
1702 				if (default_visibility > visibility) visibility = default_visibility;
1703 				char *who = bu ? bu->handle : NULL;
1704 				mastodon_post_message(ic, cmd[2], id, who, MASTODON_REPLY, mentions, visibility, spoiler_text);
1705 			} else {
1706 				mastodon_log(ic, "Sorry, I can't figure out what you're reply to!");
1707 			}
1708 		}
1709 	} else if (g_ascii_strcasecmp(cmd[0], "cw") == 0) {
1710 		g_free(md->spoiler_text);
1711 		if (cmd[1] == NULL) {
1712 			md->spoiler_text = NULL;
1713 			mastodon_log(ic, "Next post will get no content warning");
1714 		} else {
1715 			md->spoiler_text = g_strdup(message + 3);
1716 			mastodon_log(ic, "Next post will get content warning '%s'", md->spoiler_text);
1717 		}
1718 	} else if ((g_ascii_strcasecmp(cmd[0], "visibility") == 0 ||
1719 				g_ascii_strcasecmp(cmd[0], "vis") == 0)) {
1720 		if (cmd[1] == NULL) {
1721 			md->visibility = mastodon_default_visibility(ic);
1722 		} else {
1723 			md->visibility = mastodon_parse_visibility(cmd[1]);
1724 		}
1725 		mastodon_log(ic, "Next post is %s",
1726 					 mastodon_visibility(md->visibility));
1727 	} else if (g_ascii_strcasecmp(cmd[0], "post") == 0) {
1728 		if (cmd[1] == NULL) {
1729 			mastodon_log(ic, "What should we post?");
1730 		} else {
1731 			mastodon_post_message(ic, message + 5, 0, cmd[1], MASTODON_NEW_MESSAGE, NULL, MV_UNKNOWN, NULL);
1732 		}
1733 	} else if (g_ascii_strcasecmp(cmd[0], "public") == 0 ||
1734 			   g_ascii_strcasecmp(cmd[0], "unlisted") == 0 ||
1735 			   g_ascii_strcasecmp(cmd[0], "private") == 0 ||
1736 			   g_ascii_strcasecmp(cmd[0], "direct") == 0) {
1737 		mastodon_log(ic, "Please use the visibility command instead");
1738 	} else if (allow_post) {
1739 		mastodon_post_message(ic, message, 0, cmd[0], MASTODON_MAYBE_REPLY, NULL, MV_UNKNOWN, NULL);
1740 	} else {
1741 		mastodon_log(ic, "Unknown command: %s", cmd[0]);
1742 	}
1743 
1744 	g_free(cmds);
1745 }
1746 
mastodon_log(struct im_connection * ic,char * format,...)1747 void mastodon_log(struct im_connection *ic, char *format, ...)
1748 {
1749 	struct mastodon_data *md = ic->proto_data;
1750 	va_list params;
1751 	char *text;
1752 
1753 	va_start(params, format);
1754 	text = g_strdup_vprintf(format, params);
1755 	va_end(params);
1756 
1757 	if (md->timeline_gc) {
1758 		imcb_chat_log(md->timeline_gc, "%s", text);
1759 	} else {
1760 		imcb_log(ic, "%s", text);
1761 	}
1762 
1763 	g_free(text);
1764 }
1765 
init_plugin(void)1766 G_MODULE_EXPORT void init_plugin(void)
1767 {
1768 	struct prpl *ret = g_new0(struct prpl, 1);
1769 
1770 	ret->options = PRPL_OPT_NOOTR | PRPL_OPT_NO_PASSWORD;
1771 	ret->name = "mastodon";
1772 	ret->login = mastodon_login;
1773 	ret->init = mastodon_init;
1774 	ret->logout = mastodon_logout;
1775 	ret->buddy_msg = mastodon_buddy_msg;
1776 	ret->get_info = mastodon_get_info;
1777 	ret->add_buddy = mastodon_add_buddy;
1778 	ret->remove_buddy = mastodon_remove_buddy;
1779 	ret->chat_msg = mastodon_chat_msg;
1780 	ret->chat_join = mastodon_chat_join;
1781 	ret->chat_leave = mastodon_chat_leave;
1782 	ret->add_permit = mastodon_add_permit;
1783 	ret->rem_permit = mastodon_rem_permit;
1784 	ret->add_deny = mastodon_add_deny;
1785 	ret->rem_deny = mastodon_rem_deny;
1786 	ret->buddy_data_add = mastodon_buddy_data_add;
1787 	ret->buddy_data_free = mastodon_buddy_data_free;
1788 	ret->handle_cmp = g_ascii_strcasecmp;
1789 
1790 	register_protocol(ret);
1791 }
1792