Update OAuth instructions on README.Debian.
[debian/bti.git] / bti.c
1 /*
2  * Copyright (C) 2008-2011 Greg Kroah-Hartman <greg@kroah.com>
3  * Copyright (C) 2009 Bart Trojanowski <bart@jukie.net>
4  * Copyright (C) 2009-2010 Amir Mohammad Saied <amirsaied@gmail.com>
5  *
6  * This program is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by the
8  * Free Software Foundation version 2 of the License.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * General Public License for more details.
14  */
15
16 #define _GNU_SOURCE
17
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <stddef.h>
21 #include <string.h>
22 #include <getopt.h>
23 #include <errno.h>
24 #include <ctype.h>
25 #include <fcntl.h>
26 #include <unistd.h>
27 #include <time.h>
28 #include <sys/stat.h>
29 #include <sys/types.h>
30 #include <sys/wait.h>
31 #include <curl/curl.h>
32 #include <libxml/xmlmemory.h>
33 #include <libxml/parser.h>
34 #include <libxml/tree.h>
35 #include <json/json.h>
36 #include <pcre.h>
37 #include <termios.h>
38 #include <dlfcn.h>
39 #include <oauth.h>
40 #include "bti.h"
41
42 #define zalloc(size)    calloc(size, 1)
43
44 #define dbg(format, arg...)                                             \
45         do {                                                            \
46                 if (debug)                                              \
47                         fprintf(stdout, "bti: %s: " format , __func__ , \
48                                 ## arg);                                \
49         } while (0)
50
51
52 int debug;
53
54 static void display_help(void)
55 {
56         fprintf(stdout, "bti - send tweet to twitter or identi.ca\n"
57                 "Version: %s\n"
58                 "Usage:\n"
59                 "  bti [options]\n"
60                 "options are:\n"
61                 "  --account accountname\n"
62                 "  --password password\n"
63                 "  --action action\n"
64                 "    ('update', 'friends', 'public', 'replies', 'user', or 'direct')\n"
65                 "  --user screenname\n"
66                 "  --group groupname\n"
67                 "  --proxy PROXY:PORT\n"
68                 "  --host HOST\n"
69                 "  --logfile logfile\n"
70                 "  --config configfile\n"
71                 "  --replyto ID\n"
72                 "  --retweet ID\n"
73                 "  --shrink-urls\n"
74                 "  --page PAGENUMBER\n"
75                 "  --column COLUMNWIDTH\n"
76                 "  --bash\n"
77                 "  --background\n"
78                 "  --debug\n"
79                 "  --verbose\n"
80                 "  --dry-run\n"
81                 "  --version\n"
82                 "  --help\n", VERSION);
83 }
84
85 static int strlen_utf8(char *s)
86 {
87         int i = 0, j = 0;
88         while (s[i]) {
89                 if ((s[i] & 0xc0) != 0x80)
90                         j++;
91                 i++;
92         }
93         return j;
94 }
95
96 static void display_version(void)
97 {
98         fprintf(stdout, "bti - version %s\n", VERSION);
99 }
100
101 static char *get_string(const char *name)
102 {
103         char *temp;
104         char *string;
105
106         string = zalloc(1000);
107         if (!string)
108                 exit(1);
109         if (name != NULL)
110                 fprintf(stdout, "%s", name);
111         if (!fgets(string, 999, stdin)) {
112                 free(string);
113                 return NULL;
114         }
115         temp = strchr(string, '\n');
116         if (temp)
117                 *temp = '\0';
118         return string;
119 }
120
121 /*
122  * Try to get a handle to a readline function from a variety of different
123  * libraries.  If nothing is present on the system, then fall back to an
124  * internal one.
125  *
126  * Logic originally based off of code in the e2fsutils package in the
127  * lib/ss/get_readline.c file, which is licensed under the MIT license.
128  *
129  * This keeps us from having to relicense the bti codebase if readline
130  * ever changes its license, as there is no link-time dependency.
131  * It is a run-time thing only, and we handle any readline-like library
132  * in the same manner, making bti not be a derivative work of any
133  * other program.
134  */
135 static void session_readline_init(struct session *session)
136 {
137         /* Libraries we will try to use for readline/editline functionality */
138         const char *libpath = "libreadline.so.6:libreadline.so.5:"
139                                 "libreadline.so.4:libreadline.so:libedit.so.2:"
140                                 "libedit.so:libeditline.so.0:libeditline.so";
141         void *handle = NULL;
142         char *tmp, *cp, *next;
143         int (*bind_key)(int, void *);
144         void (*insert)(void);
145
146         /* default to internal function if we can't or won't find anything */
147         session->readline = get_string;
148         if (!isatty(0))
149                 return;
150         session->interactive = 1;
151
152         tmp = malloc(strlen(libpath)+1);
153         if (!tmp)
154                 return;
155         strcpy(tmp, libpath);
156         for (cp = tmp; cp; cp = next) {
157                 next = strchr(cp, ':');
158                 if (next)
159                         *next++ = 0;
160                 if (*cp == 0)
161                         continue;
162                 handle = dlopen(cp, RTLD_NOW);
163                 if (handle) {
164                         dbg("Using %s for readline library\n", cp);
165                         break;
166                 }
167         }
168         free(tmp);
169         if (!handle) {
170                 dbg("No readline library found.\n");
171                 return;
172         }
173
174         session->readline_handle = handle;
175         session->readline = (char *(*)(const char *))dlsym(handle, "readline");
176         if (session->readline == NULL) {
177                 /* something odd happened, default back to internal stuff */
178                 session->readline_handle = NULL;
179                 session->readline = get_string;
180                 return;
181         }
182
183         /*
184          * If we found a library, turn off filename expansion
185          * as that makes no sense from within bti.
186          */
187         bind_key = (int (*)(int, void *))dlsym(handle, "rl_bind_key");
188         insert = (void (*)(void))dlsym(handle, "rl_insert");
189         if (bind_key && insert)
190                 bind_key('\t', insert);
191 }
192
193 static void session_readline_cleanup(struct session *session)
194 {
195         if (session->readline_handle)
196                 dlclose(session->readline_handle);
197 }
198
199 static struct session *session_alloc(void)
200 {
201         struct session *session;
202
203         session = zalloc(sizeof(*session));
204         if (!session)
205                 return NULL;
206         return session;
207 }
208
209 static void session_free(struct session *session)
210 {
211         if (!session)
212                 return;
213         free(session->retweet);
214         free(session->replyto);
215         free(session->password);
216         free(session->account);
217         free(session->consumer_key);
218         free(session->consumer_secret);
219         free(session->access_token_key);
220         free(session->access_token_secret);
221         free(session->tweet);
222         free(session->proxy);
223         free(session->time);
224         free(session->homedir);
225         free(session->user);
226         free(session->group);
227         free(session->hosturl);
228         free(session->hostname);
229         free(session->configfile);
230         free(session);
231 }
232
233 static struct bti_curl_buffer *bti_curl_buffer_alloc(enum action action)
234 {
235         struct bti_curl_buffer *buffer;
236
237         buffer = zalloc(sizeof(*buffer));
238         if (!buffer)
239                 return NULL;
240
241         /* start out with a data buffer of 1 byte to
242          * make the buffer fill logic simpler */
243         buffer->data = zalloc(1);
244         if (!buffer->data) {
245                 free(buffer);
246                 return NULL;
247         }
248         buffer->length = 0;
249         buffer->action = action;
250         return buffer;
251 }
252
253 static void bti_curl_buffer_free(struct bti_curl_buffer *buffer)
254 {
255         if (!buffer)
256                 return;
257         free(buffer->data);
258         free(buffer);
259 }
260
261 const char twitter_host[]  = "http://api.twitter.com/1.1/statuses";
262 const char twitter_host_stream[]  = "https://stream.twitter.com/1.1/statuses"; /*this is not reset, and doesnt work */
263 const char twitter_host_simple[]  = "http://api.twitter.com/1.1";
264 const char identica_host[] = "https://identi.ca/api/statuses";
265 const char twitter_name[]  = "twitter";
266 const char identica_name[] = "identi.ca";
267
268 static const char twitter_request_token_uri[]  = "https://twitter.com/oauth/request_token";
269 static const char twitter_access_token_uri[]   = "https://twitter.com/oauth/access_token";
270 static const char twitter_authorize_uri[]      = "https://twitter.com/oauth/authorize?oauth_token=";
271 static const char identica_request_token_uri[] = "https://identi.ca/api/oauth/request_token?oauth_callback=oob";
272 static const char identica_access_token_uri[]  = "https://identi.ca/api/oauth/access_token";
273 static const char identica_authorize_uri[]     = "https://identi.ca/api/oauth/authorize?oauth_token=";
274 static const char custom_request_token_uri[]   = "/../oauth/request_token?oauth_callback=oob";
275 static const char custom_access_token_uri[]    = "/../oauth/access_token";
276 static const char custom_authorize_uri[]       = "/../oauth/authorize?oauth_token=";
277
278 static const char user_uri[]     = "/user_timeline.json";
279 static const char update_uri[]   = "/update.json";
280 static const char public_uri[]   = "/sample.json";
281 static const char friends_uri[]  = "/home_timeline.json";
282 static const char mentions_uri[] = "/mentions_timeline.json";
283 static const char replies_uri[]  = "/replies.xml";
284 static const char retweet_uri[]  = "/retweet/";
285 static const char group_uri[]    = "/../statusnet/groups/timeline/";
286 /*static const char direct_uri[]   = "/direct_messages/new.xml";*/
287 static const char direct_uri[]   = "/direct_messages/new.json";
288
289 static const char config_default[]      = "/etc/bti";
290 static const char config_xdg_default[] = ".config/bti";
291 static const char config_user_default[] = ".bti";
292
293
294 static CURL *curl_init(void)
295 {
296         CURL *curl;
297
298         curl = curl_easy_init();
299         if (!curl) {
300                 fprintf(stderr, "Can not init CURL!\n");
301                 return NULL;
302         }
303         /* some ssl sanity checks on the connection we are making */
304         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
305         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
306         return curl;
307 }
308
309 static void find_config_file(struct session *session)
310 {
311         struct stat s;
312         char *home;
313         char *file;
314         int homedir_size;
315
316         /*
317          * Get the home directory so we can try to find a config file.
318          * If we have no home dir set up, look in /etc/bti
319          */
320         home = getenv("HOME");
321         if (!home) {
322                 /* No home dir, so take the defaults and get out of here */
323                 session->homedir = strdup("");
324                 session->configfile = strdup(config_default);
325                 return;
326         }
327
328         /* We have a home dir, so this might be a user */
329         session->homedir = strdup(home);
330         homedir_size = strlen(session->homedir);
331
332         /*
333          * Try to find a config file, we do so in this order:
334          * ~/.bti               old-school config file
335          * ~/.config/bti        new-school config file
336          */
337         file = zalloc(homedir_size + strlen(config_user_default) + 7);
338         sprintf(file, "%s/%s", home, config_user_default);
339         if (stat(file, &s) == 0) {
340                 /* Found the config file at ~/.bti */
341                 session->configfile = strdup(file);
342                 free(file);
343                 return;
344         }
345
346         free(file);
347         file = zalloc(homedir_size + strlen(config_xdg_default) + 7);
348         sprintf(file, "%s/%s", home, config_xdg_default);
349         if (stat(file, &s) == 0) {
350                 /* config file is at ~/.config/bti */
351                 session->configfile = strdup(file);
352                 free(file);
353                 return;
354         }
355
356         /* No idea where the config file is, so punt */
357         free(file);
358         session->configfile = strdup("");
359 }
360
361 /* The final place data is sent to the screen/pty/tty */
362 static void bti_output_line(struct session *session, xmlChar *user,
363                             xmlChar *id, xmlChar *created, xmlChar *text)
364 {
365         if (session->verbose)
366                 printf("[%*s] {%s} (%.16s) %s\n", -session->column_output, user,
367                                 id, created, text);
368         else
369                 printf("[%*s] %s\n", -session->column_output, user, text);
370 }
371
372 static void parse_statuses(struct session *session,
373                            xmlDocPtr doc, xmlNodePtr current)
374 {
375         xmlChar *text = NULL;
376         xmlChar *user = NULL;
377         xmlChar *created = NULL;
378         xmlChar *id = NULL;
379         xmlNodePtr userinfo;
380
381         current = current->xmlChildrenNode;
382         while (current != NULL) {
383                 if (current->type == XML_ELEMENT_NODE) {
384                         if (!xmlStrcmp(current->name, (const xmlChar *)"created_at"))
385                                 created = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
386                         if (!xmlStrcmp(current->name, (const xmlChar *)"text"))
387                                 text = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
388                         if (!xmlStrcmp(current->name, (const xmlChar *)"id"))
389                                 id = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
390                         if (!xmlStrcmp(current->name, (const xmlChar *)"user")) {
391                                 userinfo = current->xmlChildrenNode;
392                                 while (userinfo != NULL) {
393                                         if ((!xmlStrcmp(userinfo->name, (const xmlChar *)"screen_name"))) {
394                                                 if (user)
395                                                         xmlFree(user);
396                                                 user = xmlNodeListGetString(doc, userinfo->xmlChildrenNode, 1);
397                                         }
398                                         userinfo = userinfo->next;
399                                 }
400                         }
401
402                         if (user && text && created && id) {
403                                 bti_output_line(session, user, id,
404                                                 created, text);
405                                 xmlFree(user);
406                                 xmlFree(text);
407                                 xmlFree(created);
408                                 xmlFree(id);
409                                 user = NULL;
410                                 text = NULL;
411                                 created = NULL;
412                                 id = NULL;
413                         }
414                 }
415                 current = current->next;
416         }
417
418         return;
419 }
420
421 static void parse_timeline(char *document, struct session *session)
422 {
423         xmlDocPtr doc;
424         xmlNodePtr current;
425
426         doc = xmlReadMemory(document, strlen(document), "timeline.xml",
427                             NULL, XML_PARSE_NOERROR);
428         if (doc == NULL)
429                 return;
430
431         current = xmlDocGetRootElement(doc);
432         if (current == NULL) {
433                 fprintf(stderr, "empty document\n");
434                 xmlFreeDoc(doc);
435                 return;
436         }
437
438         if (xmlStrcmp(current->name, (const xmlChar *) "statuses")) {
439                 fprintf(stderr, "unexpected document type\n");
440                 xmlFreeDoc(doc);
441                 return;
442         }
443
444         current = current->xmlChildrenNode;
445         while (current != NULL) {
446                 if ((!xmlStrcmp(current->name, (const xmlChar *)"status")))
447                         parse_statuses(session, doc, current);
448                 current = current->next;
449         }
450         xmlFreeDoc(doc);
451
452         return;
453 }
454
455
456 /* avoids the c99 option */
457 #define json_object_object_foreach_alt(obj,key,val)             \
458         char *key;                                              \
459         struct json_object *val;                                \
460         struct lh_entry *entry;                                 \
461         for (entry = json_object_get_object(obj)->head;         \
462                 ({ if(entry && !is_error(entry)) {              \
463                         key = (char*)entry->k;                  \
464                         val = (struct json_object*)entry->v;    \
465                 } ; entry; });                                  \
466                 entry = entry->next )
467
468
469 /* Forward Declaration */
470 static void json_parse(json_object * jobj, int nestlevel);
471
472 static void print_json_value(json_object *jobj, int nestlevel)
473 {
474         enum json_type type;
475
476         type = json_object_get_type(jobj);
477         switch (type) {
478         case json_type_boolean:
479                 printf("boolean   ");
480                 printf("value: %s\n", json_object_get_boolean(jobj)? "true": "false");
481                 break;
482         case json_type_double:
483                 printf("double    ");
484                 printf("value: %lf\n", json_object_get_double(jobj));
485                 break;
486         case json_type_int:
487                 printf("int       ");
488                 printf("value: %d\n", json_object_get_int(jobj));
489                 break;
490         case json_type_string:
491                 printf("string    ");
492                 printf("value: %s\n", json_object_get_string(jobj));
493                 break;
494         default:
495                 break;
496         }
497 }
498
499 #define MAXKEYSTACK 20
500 char *keystack[MAXKEYSTACK];
501
502 static void json_parse_array(json_object *jobj, char *key, int nestlevel)
503 {
504         enum json_type type;
505
506         nestlevel++;
507         /* Simply get the array */
508         json_object *jarray = jobj;
509         if (key) {
510                 /* Get the array if it is a key value pair */
511                 jarray = json_object_object_get(jobj, key);
512         }
513
514         /* Get the length of the array */
515         int arraylen = json_object_array_length(jarray);
516         if (debug)
517                 printf("Array Length: %d\n",arraylen);
518         int i;
519         json_object *jvalue;
520
521         for (i = 0; i < arraylen; i++) {
522                 if (debug) {
523                         int j;
524                         for (j=0; j < nestlevel; ++j)
525                                 printf("  ");
526                         printf("element[%d]\n",i);
527                 }
528
529                 /* Get the array element at position i */
530                 jvalue = json_object_array_get_idx(jarray, i);
531                 type = json_object_get_type(jvalue);
532                 if (type == json_type_array) {
533                         json_parse_array(jvalue, NULL, nestlevel);
534                 } else if (type != json_type_object) {
535                         if (debug) {
536                                 printf("value[%d]: ", i);
537                                 print_json_value(jvalue,nestlevel);
538                         }
539                 } else {
540                         /* printf("obj: "); */
541                         keystack[nestlevel%MAXKEYSTACK]="[]";
542                         json_parse(jvalue,nestlevel);
543                 }
544         }
545 }
546
547
548 struct results {
549         int code;
550         char *message;
551 } results;
552
553 struct session *store_session;
554 struct tweetdetail {
555         char *id;
556         char *text;
557         char *screen_name;
558         char *created_at;
559 } tweetdetail;
560
561 static void json_interpret(json_object *jobj, int nestlevel)
562 {
563         if (nestlevel == 3 &&
564             strcmp(keystack[1], "errors") == 0 &&
565             strcmp(keystack[2], "[]") == 0) {
566                 if (strcmp(keystack[3], "message") == 0) {
567                         if (json_object_get_type(jobj) == json_type_string)
568                                 results.message = (char *)json_object_get_string(jobj);
569                 }
570                 if (strcmp(keystack[3], "code") == 0) {
571                         if (json_object_get_type(jobj) == json_type_int)
572                                 results.code = json_object_get_int(jobj);
573                 }
574         }
575
576         if (nestlevel >= 2 &&
577             strcmp(keystack[1],"[]") == 0) {
578                 if (strcmp(keystack[2], "created_at") == 0) {
579                         if (debug)
580                                 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
581                         tweetdetail.created_at = (char *)json_object_get_string(jobj);
582                 }
583                 if (strcmp(keystack[2], "text") == 0) {
584                         if (debug)
585                                 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
586                         tweetdetail.text = (char *)json_object_get_string(jobj);
587                 }
588                 if (strcmp(keystack[2], "id") == 0) {
589                         if (debug)
590                                 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
591                         tweetdetail.id = (char *)json_object_get_string(jobj);
592                 }
593                 if (nestlevel >= 3 &&
594                     strcmp(keystack[2], "user") == 0) {
595                         if (strcmp(keystack[3], "screen_name") == 0) {
596                                 if (debug)
597                                         printf("%s->%s : %s\n", keystack[2], keystack[3], json_object_get_string(jobj));
598                                 tweetdetail.screen_name=(char *)json_object_get_string(jobj);
599                                 bti_output_line(store_session,
600                                                 (xmlChar *)tweetdetail.screen_name,
601                                                 (xmlChar *)tweetdetail.id,
602                                                 (xmlChar *)tweetdetail.created_at,
603                                                 (xmlChar *)tweetdetail.text);
604                                 }
605                 }
606         }
607 }
608
609 /* Parsing the json object */
610 static void json_parse(json_object * jobj, int nestlevel)
611 {
612         int i;
613
614         if (jobj==NULL) {
615                 fprintf(stderr,"jobj null\n");
616                 return;
617         }
618         nestlevel++;
619         enum json_type type;
620         json_object_object_foreach_alt(jobj, key, val) {
621                 /* work around pre10 */
622                 if (val)
623                         type = json_object_get_type(val);
624                 else
625                         type=json_type_null;
626                 if (debug)
627                         for (i = 0; i < nestlevel; ++i)
628                                 printf("  ");
629                 if (debug)
630                         printf("key %-34s ", key);
631                 if (debug)
632                         for (i = 0; i < 8 - nestlevel; ++i)
633                                 printf("  ");
634                 switch (type) {
635                 case json_type_boolean:
636                 case json_type_double:
637                 case json_type_int:
638                 case json_type_string:
639                         if (debug) print_json_value(val,nestlevel);
640                         if (debug) for (i=0; i<nestlevel+1; ++i) printf("  ");
641                         if (debug) printf("(");
642                         if (debug) for (i=1; i<nestlevel; ++i) { printf("%s->",keystack[i]); }
643                         if (debug) printf("%s)\n",key);
644                         keystack[nestlevel%MAXKEYSTACK] = key;
645                         json_interpret(val,nestlevel);
646                         break;
647                 case json_type_object:
648                         if (debug) printf("json_type_object\n");
649                         keystack[nestlevel%MAXKEYSTACK] = key;
650                         json_parse(json_object_object_get(jobj, key), nestlevel);
651                         break;
652                 case json_type_array:
653                         if (debug) printf("json_type_array, ");
654                         keystack[nestlevel%MAXKEYSTACK] = key;
655                         json_parse_array(jobj, key, nestlevel);
656                         break;
657                 case json_type_null:
658                         if (debug) printf("null\n");
659                         break;
660                 default:
661                         if (debug) printf("\n");
662                         break;
663                 }
664         }
665 }
666
667 static int parse_response_json(char *document, struct session *session)
668 {
669         dbg("Got this json response:\n");
670         dbg("%s\n",document);
671
672         results.code=0;
673         results.message=NULL;
674         json_object *jobj = json_tokener_parse(document);
675
676         /* make global for now */
677         store_session = session;
678         if (!is_error(jobj)) {
679                 /* guards against a json pre 0.10 bug */
680                 json_parse(jobj,0);
681         }
682         if (results.code && results.message != NULL) {
683                 if (debug)
684                         printf("Got an error code:\n  code=%d\n  message=%s\n",
685                                 results.code, results.message);
686                 fprintf(stderr, "error condition detected: %d = %s\n",
687                         results.code, results.message);
688                 return -EREMOTEIO;
689         }
690         return 0;
691 }
692
693 static void parse_timeline_json(char *document, struct session *session)
694 {
695         dbg("Got this json response:\n");
696         dbg("%s\n",document);
697         results.code = 0;
698         results.message = NULL;
699         json_object *jobj = json_tokener_parse(document);
700
701         /* make global for now */
702         store_session = session;
703         if (!is_error(jobj)) {
704                 /* guards against a json pre 0.10 bug */
705                 if (json_object_get_type(jobj)==json_type_array) {
706                         json_parse_array(jobj, NULL, 0);
707                 } else {
708                         json_parse(jobj,0);
709                 }
710         }
711         if (results.code && results.message != NULL) {
712                 if (debug)
713                         printf("Got an error code:\n  code=%d\n  message=%s\n",
714                                 results.code, results.message);
715                 fprintf(stderr, "error condition detected: %d = %s\n",
716                         results.code, results.message);
717         }
718 }
719
720 #ifdef OLDXML
721 static int parse_response_xml(char *document, struct session *session)
722 {
723         xmlDocPtr doc;
724         xmlNodePtr current;
725
726         doc = xmlReadMemory(document, strlen(document),
727                                 "response.xml", NULL,
728                                 XML_PARSE_NOERROR);
729         if (doc == NULL)
730                 return -EREMOTEIO;
731
732         current = xmlDocGetRootElement(doc);
733         if (current == NULL) {
734                 fprintf(stderr, "empty document\n");
735                 xmlFreeDoc(doc);
736                 return -EREMOTEIO;
737         }
738
739         if (xmlStrcmp(current->name, (const xmlChar *) "status")) {
740                 if (xmlStrcmp(current->name, (const xmlChar *) "direct_message")) {
741                         if (xmlStrcmp(current->name, (const xmlChar *) "hash")
742                                 && xmlStrcmp(current->name, (const xmlChar *) "errors")) {
743                                 fprintf(stderr, "unexpected document type\n");
744                                 xmlFreeDoc(doc);
745                                 return -EREMOTEIO;
746                         } else {
747                                 xmlChar *text=NULL;
748                                 while (current != NULL) {
749                                         if (current->type == XML_ELEMENT_NODE)
750                                                 if (!xmlStrcmp(current->name, (const xmlChar *)"error")) {
751                                                         text = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
752                                                         break;
753                                                 }
754                                         if (current->children)
755                                                 current = current->children;
756                                         else
757                                                 current = current->next;
758                                 }
759
760                                 if (text) {
761                                         fprintf(stderr, "error condition detected = %s\n", text);
762                                         xmlFree(text);
763                                 } else
764                                         fprintf(stderr, "unknown error condition\n");
765
766                                 xmlFreeDoc(doc);
767                                 return -EREMOTEIO;
768                         }
769                 }
770         }
771
772         xmlFreeDoc(doc);
773
774         return 0;
775 }
776 #endif
777
778 static size_t curl_callback(void *buffer, size_t size, size_t nmemb,
779                             void *userp)
780 {
781         struct bti_curl_buffer *curl_buf = userp;
782         size_t buffer_size = size * nmemb;
783         char *temp;
784
785         if ((!buffer) || (!buffer_size) || (!curl_buf))
786                 return -EINVAL;
787
788         /* add to the data we already have */
789         temp = zalloc(curl_buf->length + buffer_size + 1);
790         if (!temp)
791                 return -ENOMEM;
792
793         memcpy(temp, curl_buf->data, curl_buf->length);
794         free(curl_buf->data);
795         curl_buf->data = temp;
796         memcpy(&curl_buf->data[curl_buf->length], (char *)buffer, buffer_size);
797         curl_buf->length += buffer_size;
798         if (curl_buf->action)
799                 parse_timeline(curl_buf->data, curl_buf->session);
800
801         dbg("%s\n", curl_buf->data);
802
803         return buffer_size;
804 }
805
806 static int parse_osp_reply(const char *reply, char **token, char **secret)
807 {
808         int rc;
809         int retval = 1;
810         char **rv = NULL;
811         rc = oauth_split_url_parameters(reply, &rv);
812         qsort(rv, rc, sizeof(char *), oauth_cmpstringp);
813         if (rc == 2 || rc == 4) {
814                 if (!strncmp(rv[0], "oauth_token=", 11) &&
815                     !strncmp(rv[1], "oauth_token_secret=", 18)) {
816                         if (token)
817                                 *token = strdup(&(rv[0][12]));
818                         if (secret)
819                                 *secret = strdup(&(rv[1][19]));
820
821                         retval = 0;
822                 }
823         } else if (rc == 3) {
824                 if (!strncmp(rv[1], "oauth_token=", 11) &&
825                     !strncmp(rv[2], "oauth_token_secret=", 18)) {
826                         if (token)
827                                 *token = strdup(&(rv[1][12]));
828                         if (secret)
829                                 *secret = strdup(&(rv[2][19]));
830
831                         retval = 0;
832                 }
833         }
834
835         dbg("token: %s\n", *token);
836         dbg("secret: %s\n", *secret);
837
838         if (rv)
839                 free(rv);
840
841         return retval;
842 }
843
844 static int request_access_token(struct session *session)
845 {
846         char *post_params = NULL;
847         char *request_url = NULL;
848         char *reply       = NULL;
849         char *at_key      = NULL;
850         char *at_secret   = NULL;
851         char *verifier    = NULL;
852         char at_uri[90];
853         char token_uri[90];
854
855         if (!session)
856                 return -EINVAL;
857
858         if (session->host == HOST_TWITTER)
859                 request_url = oauth_sign_url2(
860                                 twitter_request_token_uri, NULL,
861                                 OA_HMAC, NULL, session->consumer_key,
862                                 session->consumer_secret, NULL, NULL);
863         else if (session->host == HOST_IDENTICA)
864                 request_url = oauth_sign_url2(
865                                 identica_request_token_uri, NULL,
866                                 OA_HMAC, NULL, session->consumer_key,
867                                 session->consumer_secret, NULL, NULL);
868         else {
869                 sprintf(token_uri, "%s%s",
870                         session->hosturl, custom_request_token_uri);
871                 request_url = oauth_sign_url2(
872                                 token_uri, NULL,
873                                 OA_HMAC, NULL, session->consumer_key,
874                                 session->consumer_secret, NULL, NULL);
875         }
876         reply = oauth_http_get(request_url, post_params);
877
878         if (request_url)
879                 free(request_url);
880
881         if (post_params)
882                 free(post_params);
883
884         if (!reply)
885                 return 1;
886
887         if (parse_osp_reply(reply, &at_key, &at_secret))
888                 return 1;
889
890         free(reply);
891
892         fprintf(stdout,
893                 "Please open the following link in your browser, and "
894                 "allow 'bti' to access your account. Then paste "
895                 "back the provided PIN in here.\n");
896         if (session->host == HOST_TWITTER) {
897                 fprintf(stdout, "%s%s\nPIN: ", twitter_authorize_uri, at_key);
898                 verifier = session->readline(NULL);
899                 sprintf(at_uri, "%s?oauth_verifier=%s",
900                         twitter_access_token_uri, verifier);
901         } else if (session->host == HOST_IDENTICA) {
902                 fprintf(stdout, "%s%s\nPIN: ", identica_authorize_uri, at_key);
903                 verifier = session->readline(NULL);
904                 sprintf(at_uri, "%s?oauth_verifier=%s",
905                         identica_access_token_uri, verifier);
906         } else {
907                 fprintf(stdout, "%s%s%s\nPIN: ",
908                         session->hosturl, custom_authorize_uri, at_key);
909                 verifier = session->readline(NULL);
910                 sprintf(at_uri, "%s%s?oauth_verifier=%s",
911                         session->hosturl, custom_access_token_uri, verifier);
912         }
913         request_url = oauth_sign_url2(at_uri, NULL, OA_HMAC, NULL,
914                                       session->consumer_key,
915                                       session->consumer_secret,
916                                       at_key, at_secret);
917         reply = oauth_http_get(request_url, post_params);
918
919         if (!reply)
920                 return 1;
921
922         if (parse_osp_reply(reply, &at_key, &at_secret))
923                 return 1;
924
925         free(reply);
926
927         fprintf(stdout,
928                 "Please put these two lines in your bti "
929                 "configuration file (%s):\n"
930                 "access_token_key=%s\n"
931                 "access_token_secret=%s\n",
932                 session->configfile, at_key, at_secret);
933
934         return 0;
935 }
936
937 static int send_request(struct session *session)
938 {
939         const int endpoint_size = 2000;
940         char endpoint[endpoint_size];
941         char user_password[500];
942         char data[500];
943         struct bti_curl_buffer *curl_buf;
944         CURL *curl = NULL;
945         CURLcode res;
946         struct curl_httppost *formpost = NULL;
947         struct curl_httppost *lastptr = NULL;
948         struct curl_slist *slist = NULL;
949         char *req_url = NULL;
950         char *reply = NULL;
951         char *postarg = NULL;
952         char *escaped_tweet = NULL;
953         int is_post = 0;
954
955         if (!session)
956                 return -EINVAL;
957
958         if (!session->hosturl)
959                 session->hosturl = strdup(twitter_host);
960
961         if (session->no_oauth || session->guest) {
962                 curl_buf = bti_curl_buffer_alloc(session->action);
963                 if (!curl_buf)
964                         return -ENOMEM;
965                 curl_buf->session = session;
966
967                 curl = curl_init();
968                 if (!curl) {
969                         bti_curl_buffer_free(curl_buf);
970                         return -EINVAL;
971                 }
972
973                 if (!session->hosturl)
974                         session->hosturl = strdup(twitter_host);
975
976                 switch (session->action) {
977                 case ACTION_UPDATE:
978                         snprintf(user_password, sizeof(user_password), "%s:%s",
979                                  session->account, session->password);
980                         snprintf(data, sizeof(data), "status=\"%s\"",
981                                  session->tweet);
982                         curl_formadd(&formpost, &lastptr,
983                                      CURLFORM_COPYNAME, "status",
984                                      CURLFORM_COPYCONTENTS, session->tweet,
985                                      CURLFORM_END);
986
987                         curl_formadd(&formpost, &lastptr,
988                                      CURLFORM_COPYNAME, "source",
989                                      CURLFORM_COPYCONTENTS, "bti",
990                                      CURLFORM_END);
991
992                         if (session->replyto)
993                                 curl_formadd(&formpost, &lastptr,
994                                              CURLFORM_COPYNAME,
995                                              "in_reply_to_status_id",
996                                              CURLFORM_COPYCONTENTS,
997                                              session->replyto,
998                                              CURLFORM_END);
999
1000                         curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
1001                         slist = curl_slist_append(slist, "Expect:");
1002                         curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
1003
1004                         snprintf(endpoint, endpoint_size, "%s%s", session->hosturl, update_uri);
1005                         curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1006                         curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
1007                         break;
1008
1009                 case ACTION_FRIENDS:
1010                         snprintf(user_password, sizeof(user_password), "%s:%s",
1011                                  session->account, session->password);
1012                         snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,
1013                                         friends_uri, session->page);
1014                         curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1015                         curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
1016                         break;
1017
1018                 case ACTION_USER:
1019                         snprintf(endpoint, endpoint_size, "%s%s%s.xml?page=%d", session->hosturl,
1020                                 user_uri, session->user, session->page);
1021                         curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1022                         break;
1023
1024                 case ACTION_REPLIES:
1025                         snprintf(user_password, sizeof(user_password), "%s:%s",
1026                                  session->account, session->password);
1027                         snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,
1028                                 replies_uri, session->page);
1029                         curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1030                         curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
1031                         break;
1032
1033                 case ACTION_PUBLIC:
1034                         /*snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,*/
1035                         snprintf(endpoint, endpoint_size, "%s%s", twitter_host_stream,
1036                                 public_uri);
1037                         curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1038                         break;
1039
1040                 case ACTION_GROUP:
1041                         snprintf(endpoint, endpoint_size, "%s%s%s.xml?page=%d",
1042                                 session->hosturl, group_uri, session->group,
1043                                 session->page);
1044                         curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1045                         break;
1046
1047                 case ACTION_DIRECT:
1048                     /* NOT IMPLEMENTED - twitter requires authentication anyway */
1049                         break;
1050
1051                 default:
1052                         break;
1053                 }
1054
1055                 if (session->proxy)
1056                         curl_easy_setopt(curl, CURLOPT_PROXY, session->proxy);
1057
1058                 if (debug)
1059                         curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
1060
1061                 dbg("user_password = %s\n", user_password);
1062                 dbg("data = %s\n", data);
1063                 dbg("proxy = %s\n", session->proxy);
1064
1065                 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_callback);
1066                 curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_buf);
1067                 if (!session->dry_run) {
1068                         res = curl_easy_perform(curl);
1069                         if (!session->background) {
1070                                 xmlDocPtr doc;
1071                                 xmlNodePtr current;
1072
1073                                 if (res) {
1074                                         fprintf(stderr,
1075                                                 "error(%d) trying to perform operation\n",
1076                                                 res);
1077                                         curl_easy_cleanup(curl);
1078                                         if (session->action == ACTION_UPDATE)
1079                                                 curl_formfree(formpost);
1080                                         bti_curl_buffer_free(curl_buf);
1081                                         return -EINVAL;
1082                                 }
1083
1084                                 doc = xmlReadMemory(curl_buf->data,
1085                                                     curl_buf->length,
1086                                                     "response.xml", NULL,
1087                                                     XML_PARSE_NOERROR);
1088                                 if (doc == NULL) {
1089                                         curl_easy_cleanup(curl);
1090                                         if (session->action == ACTION_UPDATE)
1091                                                 curl_formfree(formpost);
1092                                         bti_curl_buffer_free(curl_buf);
1093                                         return -EINVAL;
1094                                 }
1095
1096                                 current = xmlDocGetRootElement(doc);
1097                                 if (current == NULL) {
1098                                         fprintf(stderr, "empty document\n");
1099                                         xmlFreeDoc(doc);
1100                                         curl_easy_cleanup(curl);
1101                                         if (session->action == ACTION_UPDATE)
1102                                                 curl_formfree(formpost);
1103                                         bti_curl_buffer_free(curl_buf);
1104                                         return -EINVAL;
1105                                 }
1106
1107                                 if (xmlStrcmp(current->name, (const xmlChar *)"status")) {
1108                                         fprintf(stderr, "unexpected document type\n");
1109                                         xmlFreeDoc(doc);
1110                                         curl_easy_cleanup(curl);
1111                                         if (session->action == ACTION_UPDATE)
1112                                                 curl_formfree(formpost);
1113                                         bti_curl_buffer_free(curl_buf);
1114                                         return -EINVAL;
1115                                 }
1116
1117                                 xmlFreeDoc(doc);
1118                         }
1119                 }
1120
1121                 curl_easy_cleanup(curl);
1122                 if (session->action == ACTION_UPDATE)
1123                         curl_formfree(formpost);
1124                 bti_curl_buffer_free(curl_buf);
1125         } else {
1126                 switch (session->action) {
1127                 case ACTION_UPDATE:
1128                         /* dont test it here, let twitter return an error that we show */
1129                         if (strlen_utf8(session->tweet) > 140 + 1000 ) {
1130                                 printf("E: tweet is too long!\n");
1131                                 goto skip_tweet;
1132                         }
1133
1134                         /* TODO: add tweet crunching function. */
1135                         escaped_tweet = oauth_url_escape(session->tweet);
1136                         if (session->replyto) {
1137                                 sprintf(endpoint,
1138                                         "%s%s?status=%s&in_reply_to_status_id=%s",
1139                                         session->hosturl, update_uri,
1140                                         escaped_tweet, session->replyto);
1141                         } else {
1142                                 sprintf(endpoint, "%s%s?status=%s",
1143                                         session->hosturl, update_uri,
1144                                         escaped_tweet);
1145                         }
1146
1147                         is_post = 1;
1148                         break;
1149                 case ACTION_USER:
1150                         sprintf(endpoint, "%s%s?screen_name=%s&page=%d",
1151                                 session->hosturl, user_uri, session->user,
1152                                 session->page);
1153                         break;
1154                 case ACTION_REPLIES:
1155                         sprintf(endpoint, "%s%s?page=%d", session->hosturl,
1156                                 mentions_uri, session->page);
1157                         break;
1158                 case ACTION_PUBLIC:
1159                         sprintf(endpoint, "%s%s", twitter_host_stream,
1160                                 public_uri);
1161                         break;
1162                 case ACTION_GROUP:
1163                         sprintf(endpoint, "%s%s%s.xml?page=%d",
1164                                 session->hosturl, group_uri, session->group,
1165                                 session->page);
1166                         break;
1167                 case ACTION_FRIENDS:
1168                         sprintf(endpoint, "%s%s?page=%d", session->hosturl,
1169                                 friends_uri, session->page);
1170                         break;
1171                 case ACTION_RETWEET:
1172                         sprintf(endpoint, "%s%s%s.xml", session->hosturl,
1173                                 retweet_uri, session->retweet);
1174                         is_post = 1;
1175                         break;
1176                 case ACTION_DIRECT:
1177                         escaped_tweet = oauth_url_escape(session->tweet);
1178                         sprintf(endpoint, "%s%s?user=%s&text=%s", twitter_host_simple,
1179                                 direct_uri, session->user, escaped_tweet);
1180                         is_post = 1;
1181                         break;
1182                 default:
1183                         break;
1184                 }
1185
1186                 dbg("%s\n", endpoint);
1187                 if (!session->dry_run) {
1188                         if (is_post) {
1189                                 req_url = oauth_sign_url2(endpoint, &postarg, OA_HMAC,
1190                                                           NULL, session->consumer_key,
1191                                                           session->consumer_secret,
1192                                                           session->access_token_key,
1193                                                           session->access_token_secret);
1194                                 reply = oauth_http_post(req_url, postarg);
1195                         } else {
1196                                 req_url = oauth_sign_url2(endpoint, NULL, OA_HMAC, NULL,
1197                                                           session->consumer_key,
1198                                                           session->consumer_secret,
1199                                                           session->access_token_key,
1200                                                           session->access_token_secret);
1201                                 reply = oauth_http_get(req_url, postarg);
1202                         }
1203
1204                         dbg("req_url:%s\n", req_url);
1205                         dbg("reply:%s\n", reply);
1206                         if (req_url)
1207                                 free(req_url);
1208                 }
1209
1210                 if (!reply) {
1211                         fprintf(stderr, "Error retrieving from URL (%s)\n", endpoint);
1212                         return -EIO;
1213                 }
1214
1215                 if (!session->dry_run) {
1216                         if ((session->action != ACTION_UPDATE) &&
1217                                         (session->action != ACTION_RETWEET) &&
1218                                         (session->action != ACTION_DIRECT))
1219                                 parse_timeline_json(reply, session);
1220
1221                         if ((session->action == ACTION_UPDATE) ||
1222                                         (session->action == ACTION_DIRECT))
1223                                 /*return parse_response_xml(reply, session);*/
1224                                 return parse_response_json(reply, session);
1225                 }
1226
1227                 skip_tweet: ;
1228
1229         }
1230         return 0;
1231 }
1232
1233 static void log_session(struct session *session, int retval)
1234 {
1235         FILE *log_file;
1236         char *filename;
1237
1238         /* Only log something if we have a log file set */
1239         if (!session->logfile)
1240                 return;
1241
1242         filename = alloca(strlen(session->homedir) +
1243                           strlen(session->logfile) + 3);
1244
1245         sprintf(filename, "%s/%s", session->homedir, session->logfile);
1246
1247         log_file = fopen(filename, "a+");
1248         if (log_file == NULL)
1249                 return;
1250
1251         switch (session->action) {
1252         case ACTION_UPDATE:
1253                 if (retval)
1254                         fprintf(log_file, "%s: host=%s tweet failed\n",
1255                                 session->time, session->hostname);
1256                 else
1257                         fprintf(log_file, "%s: host=%s tweet=%s\n",
1258                                 session->time, session->hostname,
1259                                 session->tweet);
1260                 break;
1261         case ACTION_FRIENDS:
1262                 fprintf(log_file, "%s: host=%s retrieving friends timeline\n",
1263                         session->time, session->hostname);
1264                 break;
1265         case ACTION_USER:
1266                 fprintf(log_file, "%s: host=%s retrieving %s's timeline\n",
1267                         session->time, session->hostname, session->user);
1268                 break;
1269         case ACTION_REPLIES:
1270                 fprintf(log_file, "%s: host=%s retrieving replies\n",
1271                         session->time, session->hostname);
1272                 break;
1273         case ACTION_PUBLIC:
1274                 fprintf(log_file, "%s: host=%s retrieving public timeline\n",
1275                         session->time, session->hostname);
1276                 break;
1277         case ACTION_GROUP:
1278                 fprintf(log_file, "%s: host=%s retrieving group timeline\n",
1279                         session->time, session->hostname);
1280                 break;
1281         case ACTION_DIRECT:
1282                 if (retval)
1283                         fprintf(log_file, "%s: host=%s tweet failed\n",
1284                                 session->time, session->hostname);
1285                 else
1286                         fprintf(log_file, "%s: host=%s tweet=%s\n",
1287                                 session->time, session->hostname,
1288                                 session->tweet);
1289                 break;
1290         default:
1291                 break;
1292         }
1293
1294         fclose(log_file);
1295 }
1296
1297 static char *get_string_from_stdin(void)
1298 {
1299         char *temp;
1300         char *string;
1301
1302         string = zalloc(1000);
1303         if (!string)
1304                 return NULL;
1305
1306         if (!fgets(string, 999, stdin)) {
1307                 free(string);
1308                 return NULL;
1309         }
1310         temp = strchr(string, '\n');
1311         if (temp)
1312                 *temp = '\0';
1313         return string;
1314 }
1315
1316 static void read_password(char *buf, size_t len, char *host)
1317 {
1318         char pwd[80];
1319         struct termios old;
1320         struct termios tp;
1321
1322         tcgetattr(0, &tp);
1323         old = tp;
1324
1325         tp.c_lflag &= (~ECHO);
1326         tcsetattr(0, TCSANOW, &tp);
1327
1328         fprintf(stdout, "Enter password for %s: ", host);
1329         fflush(stdout);
1330         tcflow(0, TCOOFF);
1331
1332         /*
1333          * I'd like to do something with the return value here, but really,
1334          * what can be done?
1335          */
1336         (void)scanf("%79s", pwd);
1337
1338         tcflow(0, TCOON);
1339         fprintf(stdout, "\n");
1340
1341         tcsetattr(0, TCSANOW, &old);
1342
1343         strncpy(buf, pwd, len);
1344         buf[len-1] = '\0';
1345 }
1346
1347 static int find_urls(const char *tweet, int **pranges)
1348 {
1349         /*
1350          * magic obtained from
1351          * http://www.geekpedia.com/KB65_How-to-validate-an-URL-using-RegEx-in-Csharp.html
1352          */
1353         static const char *re_magic =
1354                 "(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)/{1,3}"
1355                 "[0-9a-zA-Z;/~?:@&=+$\\.\\-_'()%]+)"
1356                 "(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?";
1357         pcre *re;
1358         const char *errptr;
1359         int erroffset;
1360         int ovector[10] = {0,};
1361         const size_t ovsize = sizeof(ovector)/sizeof(*ovector);
1362         int startoffset, tweetlen;
1363         int i, rc;
1364         int rbound = 10;
1365         int rcount = 0;
1366         int *ranges = malloc(sizeof(int) * rbound);
1367
1368         re = pcre_compile(re_magic,
1369                         PCRE_NO_AUTO_CAPTURE,
1370                         &errptr, &erroffset, NULL);
1371         if (!re) {
1372                 fprintf(stderr, "pcre_compile @%u: %s\n", erroffset, errptr);
1373                 exit(1);
1374         }
1375
1376         tweetlen = strlen(tweet);
1377         for (startoffset = 0; startoffset < tweetlen; ) {
1378
1379                 rc = pcre_exec(re, NULL, tweet, strlen(tweet), startoffset, 0,
1380                                 ovector, ovsize);
1381                 if (rc == PCRE_ERROR_NOMATCH)
1382                         break;
1383
1384                 if (rc < 0) {
1385                         fprintf(stderr, "pcre_exec @%u: %s\n",
1386                                 erroffset, errptr);
1387                         exit(1);
1388                 }
1389
1390                 for (i = 0; i < rc; i += 2) {
1391                         if ((rcount+2) == rbound) {
1392                                 rbound *= 2;
1393                                 ranges = realloc(ranges, sizeof(int) * rbound);
1394                         }
1395
1396                         ranges[rcount++] = ovector[i];
1397                         ranges[rcount++] = ovector[i+1];
1398                 }
1399
1400                 startoffset = ovector[1];
1401         }
1402
1403         pcre_free(re);
1404
1405         *pranges = ranges;
1406         return rcount;
1407 }
1408
1409 /**
1410  * bidirectional popen() call
1411  *
1412  * @param rwepipe - int array of size three
1413  * @param exe - program to run
1414  * @param argv - argument list
1415  * @return pid or -1 on error
1416  *
1417  * The caller passes in an array of three integers (rwepipe), on successful
1418  * execution it can then write to element 0 (stdin of exe), and read from
1419  * element 1 (stdout) and 2 (stderr).
1420  */
1421 static int popenRWE(int *rwepipe, const char *exe, const char *const argv[])
1422 {
1423         int in[2];
1424         int out[2];
1425         int err[2];
1426         int pid;
1427         int rc;
1428
1429         rc = pipe(in);
1430         if (rc < 0)
1431                 goto error_in;
1432
1433         rc = pipe(out);
1434         if (rc < 0)
1435                 goto error_out;
1436
1437         rc = pipe(err);
1438         if (rc < 0)
1439                 goto error_err;
1440
1441         pid = fork();
1442         if (pid > 0) {
1443                 /* parent */
1444                 close(in[0]);
1445                 close(out[1]);
1446                 close(err[1]);
1447                 rwepipe[0] = in[1];
1448                 rwepipe[1] = out[0];
1449                 rwepipe[2] = err[0];
1450                 return pid;
1451         } else if (pid == 0) {
1452                 /* child */
1453                 close(in[1]);
1454                 close(out[0]);
1455                 close(err[0]);
1456                 close(0);
1457                 rc = dup(in[0]);
1458                 close(1);
1459                 rc = dup(out[1]);
1460                 close(2);
1461                 rc = dup(err[1]);
1462
1463                 execvp(exe, (char **)argv);
1464                 exit(1);
1465         } else
1466                 goto error_fork;
1467
1468         return pid;
1469
1470 error_fork:
1471         close(err[0]);
1472         close(err[1]);
1473 error_err:
1474         close(out[0]);
1475         close(out[1]);
1476 error_out:
1477         close(in[0]);
1478         close(in[1]);
1479 error_in:
1480         return -1;
1481 }
1482
1483 static int pcloseRWE(int pid, int *rwepipe)
1484 {
1485         int status;
1486         close(rwepipe[0]);
1487         close(rwepipe[1]);
1488         close(rwepipe[2]);
1489         (void)waitpid(pid, &status, 0);
1490         return status;
1491 }
1492
1493 static char *shrink_one_url(int *rwepipe, char *big)
1494 {
1495         int biglen = strlen(big);
1496         char *small;
1497         int smalllen;
1498         int rc;
1499
1500         rc = dprintf(rwepipe[0], "%s\n", big);
1501         if (rc < 0)
1502                 return big;
1503
1504         smalllen = biglen + 128;
1505         small = malloc(smalllen);
1506         if (!small)
1507                 return big;
1508
1509         rc = read(rwepipe[1], small, smalllen);
1510         if (rc < 0 || rc > biglen)
1511                 goto error_free_small;
1512
1513         if (strncmp(small, "http://", 7))
1514                 goto error_free_small;
1515
1516         smalllen = rc;
1517         while (smalllen && isspace(small[smalllen-1]))
1518                         small[--smalllen] = 0;
1519
1520         free(big);
1521         return small;
1522
1523 error_free_small:
1524         free(small);
1525         return big;
1526 }
1527
1528 static char *shrink_urls(char *text)
1529 {
1530         int *ranges;
1531         int rcount;
1532         int i;
1533         int inofs = 0;
1534         int outofs = 0;
1535         const char *const shrink_args[] = {
1536                 "bti-shrink-urls",
1537                 NULL
1538         };
1539         int shrink_pid;
1540         int shrink_pipe[3];
1541         int inlen = strlen(text);
1542
1543         dbg("before len=%u\n", inlen);
1544
1545         shrink_pid = popenRWE(shrink_pipe, shrink_args[0], shrink_args);
1546         if (shrink_pid < 0)
1547                 return text;
1548
1549         rcount = find_urls(text, &ranges);
1550         if (!rcount)
1551                 return text;
1552
1553         for (i = 0; i < rcount; i += 2) {
1554                 int url_start = ranges[i];
1555                 int url_end = ranges[i+1];
1556                 int long_url_len = url_end - url_start;
1557                 char *url = strndup(text + url_start, long_url_len);
1558                 int short_url_len;
1559                 int not_url_len = url_start - inofs;
1560
1561                 dbg("long  url[%u]: %s\n", long_url_len, url);
1562                 url = shrink_one_url(shrink_pipe, url);
1563                 short_url_len = url ? strlen(url) : 0;
1564                 dbg("short url[%u]: %s\n", short_url_len, url);
1565
1566                 if (!url || short_url_len >= long_url_len) {
1567                         /* The short url ended up being too long
1568                          * or unavailable */
1569                         if (inofs) {
1570                                 strncpy(text + outofs, text + inofs,
1571                                                 not_url_len + long_url_len);
1572                         }
1573                         inofs += not_url_len + long_url_len;
1574                         outofs += not_url_len + long_url_len;
1575
1576                 } else {
1577                         /* copy the unmodified block */
1578                         strncpy(text + outofs, text + inofs, not_url_len);
1579                         inofs += not_url_len;
1580                         outofs += not_url_len;
1581
1582                         /* copy the new url */
1583                         strncpy(text + outofs, url, short_url_len);
1584                         inofs += long_url_len;
1585                         outofs += short_url_len;
1586                 }
1587
1588                 free(url);
1589         }
1590
1591         /* copy the last block after the last match */
1592         if (inofs) {
1593                 int tail = inlen - inofs;
1594                 if (tail) {
1595                         strncpy(text + outofs, text + inofs, tail);
1596                         outofs += tail;
1597                 }
1598         }
1599
1600         free(ranges);
1601
1602         (void)pcloseRWE(shrink_pid, shrink_pipe);
1603
1604         text[outofs] = 0;
1605         dbg("after len=%u\n", outofs);
1606         return text;
1607 }
1608
1609 int main(int argc, char *argv[], char *envp[])
1610 {
1611         static const struct option options[] = {
1612                 { "debug", 0, NULL, 'd' },
1613                 { "verbose", 0, NULL, 'V' },
1614                 { "account", 1, NULL, 'a' },
1615                 { "password", 1, NULL, 'p' },
1616                 { "host", 1, NULL, 'H' },
1617                 { "proxy", 1, NULL, 'P' },
1618                 { "action", 1, NULL, 'A' },
1619                 { "user", 1, NULL, 'u' },
1620                 { "group", 1, NULL, 'G' },
1621                 { "logfile", 1, NULL, 'L' },
1622                 { "shrink-urls", 0, NULL, 's' },
1623                 { "help", 0, NULL, 'h' },
1624                 { "bash", 0, NULL, 'b' },
1625                 { "background", 0, NULL, 'B' },
1626                 { "dry-run", 0, NULL, 'n' },
1627                 { "page", 1, NULL, 'g' },
1628                 { "column", 1, NULL, 'o' },
1629                 { "version", 0, NULL, 'v' },
1630                 { "config", 1, NULL, 'c' },
1631                 { "replyto", 1, NULL, 'r' },
1632                 { "retweet", 1, NULL, 'w' },
1633                 { }
1634         };
1635         struct stat s;
1636         struct session *session;
1637         pid_t child;
1638         char *tweet;
1639         static char password[80];
1640         int retval = 0;
1641         int option;
1642         char *http_proxy;
1643         time_t t;
1644         int page_nr;
1645
1646         debug = 0;
1647
1648         session = session_alloc();
1649         if (!session) {
1650                 fprintf(stderr, "no more memory...\n");
1651                 return -1;
1652         }
1653
1654         /* get the current time so that we can log it later */
1655         time(&t);
1656         session->time = strdup(ctime(&t));
1657         session->time[strlen(session->time)-1] = 0x00;
1658
1659         find_config_file(session);
1660
1661         /* Set environment variables first, before reading command line options
1662          * or config file values. */
1663         http_proxy = getenv("http_proxy");
1664         if (http_proxy) {
1665                 if (session->proxy)
1666                         free(session->proxy);
1667                 session->proxy = strdup(http_proxy);
1668                 dbg("http_proxy = %s\n", session->proxy);
1669         }
1670
1671         bti_parse_configfile(session);
1672
1673         while (1) {
1674                 option = getopt_long_only(argc, argv,
1675                                           "dp:P:H:a:A:u:c:hg:o:G:sr:nVvw:",
1676                                           options, NULL);
1677                 if (option == -1)
1678                         break;
1679                 switch (option) {
1680                 case 'd':
1681                         debug = 1;
1682                         break;
1683                 case 'V':
1684                         session->verbose = 1;
1685                         break;
1686                 case 'a':
1687                         if (session->account)
1688                                 free(session->account);
1689                         session->account = strdup(optarg);
1690                         dbg("account = %s\n", session->account);
1691                         break;
1692                 case 'g':
1693                         page_nr = atoi(optarg);
1694                         dbg("page = %d\n", page_nr);
1695                         session->page = page_nr;
1696                         break;
1697                 case 'o':
1698                         session->column_output = atoi(optarg);
1699                         dbg("column_output = %d\n", session->column_output);
1700                         break;
1701                 case 'r':
1702                         session->replyto = strdup(optarg);
1703                         dbg("in_reply_to_status_id = %s\n", session->replyto);
1704                         break;
1705                 case 'w':
1706                         session->retweet = strdup(optarg);
1707                         dbg("Retweet ID = %s\n", session->retweet);
1708                         break;
1709                 case 'p':
1710                         if (session->password)
1711                                 free(session->password);
1712                         session->password = strdup(optarg);
1713                         dbg("password = %s\n", session->password);
1714                         break;
1715                 case 'P':
1716                         if (session->proxy)
1717                                 free(session->proxy);
1718                         session->proxy = strdup(optarg);
1719                         dbg("proxy = %s\n", session->proxy);
1720                         break;
1721                 case 'A':
1722                         if (strcasecmp(optarg, "update") == 0)
1723                                 session->action = ACTION_UPDATE;
1724                         else if (strcasecmp(optarg, "friends") == 0)
1725                                 session->action = ACTION_FRIENDS;
1726                         else if (strcasecmp(optarg, "user") == 0)
1727                                 session->action = ACTION_USER;
1728                         else if (strcasecmp(optarg, "replies") == 0)
1729                                 session->action = ACTION_REPLIES;
1730                         else if (strcasecmp(optarg, "public") == 0)
1731                                 session->action = ACTION_PUBLIC;
1732                         else if (strcasecmp(optarg, "group") == 0)
1733                                 session->action = ACTION_GROUP;
1734                         else if (strcasecmp(optarg, "retweet") == 0)
1735                                 session->action = ACTION_RETWEET;
1736                         else if (strcasecmp(optarg, "direct") == 0)
1737                                 session->action = ACTION_DIRECT;
1738                         else
1739                                 session->action = ACTION_UNKNOWN;
1740                         dbg("action = %d\n", session->action);
1741                         break;
1742                 case 'u':
1743                         if (session->user)
1744                                 free(session->user);
1745                         session->user = strdup(optarg);
1746                         dbg("user = %s\n", session->user);
1747                         break;
1748
1749                 case 'G':
1750                         if (session->group)
1751                                 free(session->group);
1752                         session->group = strdup(optarg);
1753                         dbg("group = %s\n", session->group);
1754                         break;
1755                 case 'L':
1756                         if (session->logfile)
1757                                 free(session->logfile);
1758                         session->logfile = strdup(optarg);
1759                         dbg("logfile = %s\n", session->logfile);
1760                         break;
1761                 case 's':
1762                         session->shrink_urls = 1;
1763                         break;
1764                 case 'H':
1765                         if (session->hosturl)
1766                                 free(session->hosturl);
1767                         if (session->hostname)
1768                                 free(session->hostname);
1769                         if (strcasecmp(optarg, "twitter") == 0) {
1770                                 session->host = HOST_TWITTER;
1771                                 session->hosturl = strdup(twitter_host);
1772                                 session->hostname = strdup(twitter_name);
1773                         } else if (strcasecmp(optarg, "identica") == 0) {
1774                                 session->host = HOST_IDENTICA;
1775                                 session->hosturl = strdup(identica_host);
1776                                 session->hostname = strdup(identica_name);
1777                         } else {
1778                                 session->host = HOST_CUSTOM;
1779                                 session->hosturl = strdup(optarg);
1780                                 session->hostname = strdup(optarg);
1781                         }
1782                         dbg("host = %d\n", session->host);
1783                         break;
1784                 case 'b':
1785                         session->bash = 1;
1786                         /* fall-through intended */
1787                 case 'B':
1788                         session->background = 1;
1789                         break;
1790                 case 'c':
1791                         if (session->configfile)
1792                                 free(session->configfile);
1793                         session->configfile = strdup(optarg);
1794                         dbg("configfile = %s\n", session->configfile);
1795                         if (stat(session->configfile, &s) == -1) {
1796                                 fprintf(stderr,
1797                                         "Config file '%s' is not found.\n",
1798                                         session->configfile);
1799                                 goto exit;
1800                         }
1801
1802                         /*
1803                          * read the config file now.  Yes, this could override
1804                          * previously set options from the command line, but
1805                          * the user asked for it...
1806                          */
1807                         bti_parse_configfile(session);
1808                         break;
1809                 case 'h':
1810                         display_help();
1811                         goto exit;
1812                 case 'n':
1813                         session->dry_run = 1;
1814                         break;
1815                 case 'v':
1816                         display_version();
1817                         goto exit;
1818                 default:
1819                         display_help();
1820                         goto exit;
1821                 }
1822         }
1823
1824         session_readline_init(session);
1825         /*
1826          * Show the version to make it easier to determine what
1827          * is going on here
1828          */
1829         if (debug)
1830                 display_version();
1831
1832         if (session->host == HOST_TWITTER) {
1833                 if (!session->consumer_key || !session->consumer_secret) {
1834                         if (session->action == ACTION_USER ||
1835                                         session->action == ACTION_PUBLIC) {
1836                                 /*
1837                                  * Some actions may still work without
1838                                  * authentication
1839                                  */
1840                                 session->guest = 1;
1841                         } else {
1842                                 fprintf(stderr,
1843                                                 "Twitter no longer supports HTTP basic authentication.\n"
1844                                                 "Both consumer key, and consumer secret are required"
1845                                                 " for bti in order to behave as an OAuth consumer.\n");
1846                                 goto exit;
1847                         }
1848                 }
1849                 if (session->action == ACTION_GROUP) {
1850                         fprintf(stderr, "Groups only work in Identi.ca.\n");
1851                         goto exit;
1852                 }
1853         } else {
1854                 if (!session->consumer_key || !session->consumer_secret)
1855                         session->no_oauth = 1;
1856         }
1857
1858         if (session->no_oauth) {
1859                 if (!session->account) {
1860                         fprintf(stdout, "Enter account for %s: ",
1861                                 session->hostname);
1862                         session->account = session->readline(NULL);
1863                 }
1864                 if (!session->password) {
1865                         read_password(password, sizeof(password),
1866                                       session->hostname);
1867                         session->password = strdup(password);
1868                 }
1869         } else if (!session->guest) {
1870                 if (!session->access_token_key ||
1871                     !session->access_token_secret) {
1872                         request_access_token(session);
1873                         goto exit;
1874                 }
1875         }
1876
1877         if (session->action == ACTION_UNKNOWN) {
1878                 fprintf(stderr, "Unknown action, valid actions are:\n"
1879                         "'update', 'friends', 'public', 'replies', 'group', 'user' or 'direct'.\n");
1880                 goto exit;
1881         }
1882
1883         if (session->action == ACTION_GROUP && !session->group) {
1884                 fprintf(stdout, "Enter group name: ");
1885                 session->group = session->readline(NULL);
1886         }
1887
1888         if (session->action == ACTION_RETWEET) {
1889                 if (!session->retweet) {
1890                         char *rtid;
1891
1892                         fprintf(stdout, "Status ID to retweet: ");
1893                         rtid = get_string_from_stdin();
1894                         session->retweet = zalloc(strlen(rtid) + 10);
1895                         sprintf(session->retweet, "%s", rtid);
1896                         free(rtid);
1897                 }
1898
1899                 if (!session->retweet || strlen(session->retweet) == 0) {
1900                         dbg("no retweet?\n");
1901                         return -1;
1902                 }
1903
1904                 dbg("retweet ID = %s\n", session->retweet);
1905         }
1906
1907         if (session->action == ACTION_UPDATE || session->action == ACTION_DIRECT) {
1908                 if (session->background || !session->interactive)
1909                         tweet = get_string_from_stdin();
1910                 else
1911                         tweet = session->readline("tweet: ");
1912                 if (!tweet || strlen(tweet) == 0) {
1913                         dbg("no tweet?\n");
1914                         return -1;
1915                 }
1916
1917                 if (session->shrink_urls)
1918                         tweet = shrink_urls(tweet);
1919
1920                 session->tweet = zalloc(strlen(tweet) + 10);
1921                 if (session->bash)
1922                         sprintf(session->tweet, "%c %s",
1923                                 getuid() ? '$' : '#', tweet);
1924                 else
1925                         sprintf(session->tweet, "%s", tweet);
1926
1927                 free(tweet);
1928                 dbg("tweet = %s\n", session->tweet);
1929         }
1930
1931         if (session->page == 0)
1932                 session->page = 1;
1933         dbg("config file = %s\n", session->configfile);
1934         dbg("host = %d\n", session->host);
1935         dbg("action = %d\n", session->action);
1936
1937         /* fork ourself so that the main shell can get on
1938          * with it's life as we try to connect and handle everything
1939          */
1940         if (session->background) {
1941                 child = fork();
1942                 if (child) {
1943                         dbg("child is %d\n", child);
1944                         exit(0);
1945                 }
1946         }
1947
1948         retval = send_request(session);
1949         if (retval && !session->background)
1950                 fprintf(stderr, "operation failed\n");
1951
1952         log_session(session, retval);
1953 exit:
1954         session_readline_cleanup(session);
1955         session_free(session);
1956         return retval;
1957 }