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