add "bti:" to the beginning of all debug messages
[gregoa/bti.git] / bti.c
1 /*
2  * Copyright (C) 2008 Greg Kroah-Hartman <greg@kroah.com>
3  * Copyright (C) 2009 Bart Trojanowski <bart@jukie.net>
4  *
5  * This program is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License as published by the
7  * Free Software Foundation version 2 of the License.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17  */
18
19 #include <stdio.h>
20 #include <stdlib.h>
21 #include <stddef.h>
22 #include <string.h>
23 #include <getopt.h>
24 #include <errno.h>
25 #include <ctype.h>
26 #include <fcntl.h>
27 #include <unistd.h>
28 #include <time.h>
29 #include <sys/stat.h>
30 #include <sys/types.h>
31 #include <sys/wait.h>
32 #include <curl/curl.h>
33 #include <readline/readline.h>
34 #include <libxml/xmlmemory.h>
35 #include <libxml/parser.h>
36 #include <libxml/tree.h>
37 #include <pcre.h>
38 #include "bti_version.h"
39
40
41 #define zalloc(size)    calloc(size, 1)
42
43 #define dbg(format, arg...)                                             \
44         do {                                                            \
45                 if (debug)                                              \
46                         fprintf(stdout, "bti: %s: " format , __func__ , \
47                                 ## arg);                                \
48         } while (0)
49
50
51 static int debug;
52
53 enum host {
54         HOST_TWITTER = 0,
55         HOST_IDENTICA = 1,
56 };
57
58 enum action {
59         ACTION_UPDATE = 0,
60         ACTION_FRIENDS = 1,
61         ACTION_USER = 2,
62         ACTION_REPLIES = 4,
63         ACTION_PUBLIC = 8,
64         ACTION_UNKNOWN = 16
65 };
66
67 struct session {
68         char *password;
69         char *account;
70         char *tweet;
71         char *proxy;
72         char *time;
73         char *homedir;
74         char *logfile;
75         char *user;
76         int bash;
77         int shrink_urls;
78         int dry_run;
79         int page;
80         enum host host;
81         enum action action;
82 };
83
84 struct bti_curl_buffer {
85         char *data;
86         enum action action;
87         int length;
88 };
89
90 static void display_help(void)
91 {
92         fprintf(stdout, "bti - send tweet to twitter or identi.ca\n");
93         fprintf(stdout, "Version: " BTI_VERSION "\n");
94         fprintf(stdout, "Usage:\n");
95         fprintf(stdout, "  bti [options]\n");
96         fprintf(stdout, "options are:\n");
97         fprintf(stdout, "  --account accountname\n");
98         fprintf(stdout, "  --password password\n");
99         fprintf(stdout, "  --action action\n");
100         fprintf(stdout, "    ('update', 'friends', 'public', 'replies' "
101                 "or 'user')\n");
102         fprintf(stdout, "  --user screenname\n");
103         fprintf(stdout, "  --proxy PROXY:PORT\n");
104         fprintf(stdout, "  --host HOST\n");
105         fprintf(stdout, "  --logfile logfile\n");
106         fprintf(stdout, "  --shrink-urls\n");
107         fprintf(stdout, "  --page PAGENUMBER\n");
108         fprintf(stdout, "  --bash\n");
109         fprintf(stdout, "  --debug\n");
110         fprintf(stdout, "  --version\n");
111         fprintf(stdout, "  --help\n");
112 }
113
114 static void display_version(void)
115 {
116         fprintf(stdout, "bti - version %s\n", BTI_VERSION);
117 }
118
119 static struct session *session_alloc(void)
120 {
121         struct session *session;
122
123         session = zalloc(sizeof(*session));
124         if (!session)
125                 return NULL;
126         return session;
127 }
128
129 static void session_free(struct session *session)
130 {
131         if (!session)
132                 return;
133         free(session->password);
134         free(session->account);
135         free(session->tweet);
136         free(session->proxy);
137         free(session->time);
138         free(session->homedir);
139         free(session->user);
140         free(session);
141 }
142
143 static struct bti_curl_buffer *bti_curl_buffer_alloc(enum action action)
144 {
145         struct bti_curl_buffer *buffer;
146
147         buffer = zalloc(sizeof(*buffer));
148         if (!buffer)
149                 return NULL;
150
151         /* start out with a data buffer of 1 byte to
152          * make the buffer fill logic simpler */
153         buffer->data = zalloc(1);
154         if (!buffer->data) {
155                 free(buffer);
156                 return NULL;
157         }
158         buffer->length = 0;
159         buffer->action = action;
160         return buffer;
161 }
162
163 static void bti_curl_buffer_free(struct bti_curl_buffer *buffer)
164 {
165         if (!buffer)
166                 return;
167         free(buffer->data);
168         free(buffer);
169 }
170
171 static const char *twitter_user_url    = "http://twitter.com/statuses/user_timeline/";
172 static const char *twitter_update_url  = "https://twitter.com/statuses/update.xml";
173 static const char *twitter_public_url  = "http://twitter.com/statuses/public_timeline.xml";
174 static const char *twitter_friends_url = "https://twitter.com/statuses/friends_timeline.xml";
175 static const char *twitter_replies_url = "http://twitter.com/statuses/replies.xml";
176
177 static const char *identica_user_url    = "http://identi.ca/api/statuses/user_timeline/";
178 static const char *identica_update_url  = "http://identi.ca/api/statuses/update.xml";
179 static const char *identica_public_url  = "http://identi.ca/api/statuses/public_timeline.xml";
180 static const char *identica_friends_url = "http://identi.ca/api/statuses/friends_timeline.xml";
181 static const char *identica_replies_url = "http://identi.ca/api/statuses/replies.xml";
182
183 static CURL *curl_init(void)
184 {
185         CURL *curl;
186
187         curl = curl_easy_init();
188         if (!curl) {
189                 fprintf(stderr, "Can not init CURL!\n");
190                 return NULL;
191         }
192         /* some ssl sanity checks on the connection we are making */
193         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
194         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
195         return curl;
196 }
197
198 void parse_statuses(xmlDocPtr doc, xmlNodePtr current)
199 {
200         xmlChar *text = NULL;
201         xmlChar *user = NULL;
202         xmlNodePtr userinfo;
203
204         current = current->xmlChildrenNode;
205         while (current != NULL) {
206                 if (current->type == XML_ELEMENT_NODE) {
207                         if (!xmlStrcmp(current->name, (const xmlChar *)"text"))
208                                 text = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
209                         if (!xmlStrcmp(current->name, (const xmlChar *)"user")) {
210                                 userinfo = current->xmlChildrenNode;
211                                 while (userinfo != NULL) {
212                                         if ((!xmlStrcmp(userinfo->name, (const xmlChar *)"screen_name"))) {
213                                                 if (user)
214                                                         xmlFree(user);
215                                                 user = xmlNodeListGetString(doc, userinfo->xmlChildrenNode, 1);
216                                         }
217                                         userinfo = userinfo->next;
218                                 }
219                         }
220                         if (user && text) {
221                                 printf("[%s] %s\n", user, text);
222                                 xmlFree(user);
223                                 xmlFree(text);
224                                 user = NULL;
225                                 text = NULL;
226                         }
227                 }
228                 current = current->next;
229         }
230
231         return;
232 }
233
234 static void parse_timeline(char *document)
235 {
236         xmlDocPtr doc;
237         xmlNodePtr current;
238
239         doc = xmlReadMemory(document, strlen(document), "timeline.xml",
240                             NULL, XML_PARSE_NOERROR);
241         if (doc == NULL)
242                 return;
243
244         current = xmlDocGetRootElement(doc);
245         if (current == NULL) {
246                 fprintf(stderr, "empty document\n");
247                 xmlFreeDoc(doc);
248                 return;
249         }
250
251         if (xmlStrcmp(current->name, (const xmlChar *) "statuses")) {
252                 fprintf(stderr, "unexpected document type\n");
253                 xmlFreeDoc(doc);
254                 return;
255         }
256
257         current = current->xmlChildrenNode;
258         while (current != NULL) {
259                 if ((!xmlStrcmp(current->name, (const xmlChar *)"status")))
260                         parse_statuses(doc, current);
261                 current = current->next;
262         }
263         xmlFreeDoc(doc);
264
265         return;
266 }
267
268 size_t curl_callback(void *buffer, size_t size, size_t nmemb, void *userp)
269 {
270         struct bti_curl_buffer *curl_buf = userp;
271         size_t buffer_size = size * nmemb;
272         char *temp;
273
274         if ((!buffer) || (!buffer_size) || (!curl_buf))
275                 return -EINVAL;
276
277         /* add to the data we already have */
278         temp = zalloc(curl_buf->length + buffer_size + 1);
279         if (!temp)
280                 return -ENOMEM;
281
282         memcpy(temp, curl_buf->data, curl_buf->length);
283         free(curl_buf->data);
284         curl_buf->data = temp;
285         memcpy(&curl_buf->data[curl_buf->length], (char *)buffer, buffer_size);
286         curl_buf->length += buffer_size;
287         if (curl_buf->action)
288                 parse_timeline(curl_buf->data);
289
290         dbg("%s\n", curl_buf->data);
291
292         return buffer_size;
293 }
294
295 static int send_request(struct session *session)
296 {
297         char user_password[500];
298         char data[500];
299         /* is there usernames longer than 22 chars? */
300         char user_url[70];
301         struct bti_curl_buffer *curl_buf;
302         CURL *curl = NULL;
303         CURLcode res;
304         struct curl_httppost *formpost = NULL;
305         struct curl_httppost *lastptr = NULL;
306         struct curl_slist *slist = NULL;
307
308         if (!session)
309                 return -EINVAL;
310
311         curl_buf = bti_curl_buffer_alloc(session->action);
312         if (!curl_buf)
313                 return -ENOMEM;
314
315         curl = curl_init();
316         if (!curl)
317                 return -EINVAL;
318
319         switch (session->action) {
320         case ACTION_UPDATE:
321                 snprintf(user_password, sizeof(user_password), "%s:%s",
322                          session->account, session->password);
323                 snprintf(data, sizeof(data), "status=\"%s\"", session->tweet);
324                 curl_formadd(&formpost, &lastptr,
325                              CURLFORM_COPYNAME, "status",
326                              CURLFORM_COPYCONTENTS, session->tweet,
327                              CURLFORM_END);
328
329                 curl_formadd(&formpost, &lastptr,
330                              CURLFORM_COPYNAME, "source",
331                              CURLFORM_COPYCONTENTS, "bti",
332                              CURLFORM_END);
333
334                 curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
335                 slist = curl_slist_append(slist, "Expect:");
336                 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
337                 switch (session->host) {
338                 case HOST_TWITTER:
339                         curl_easy_setopt(curl, CURLOPT_URL,
340                                          twitter_update_url);
341                         break;
342                 case HOST_IDENTICA:
343                         curl_easy_setopt(curl, CURLOPT_URL,
344                                          identica_update_url);
345                         break;
346                 }
347                 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
348
349                 break;
350         case ACTION_FRIENDS:
351                 snprintf(user_password, sizeof(user_password), "%s:%s",
352                          session->account, session->password);
353                 switch (session->host) {
354                 case HOST_TWITTER:
355                         sprintf(user_url, "%s?page=%d", twitter_friends_url, session->page);
356                         curl_easy_setopt(curl, CURLOPT_URL, user_url);
357                         break;
358                 case HOST_IDENTICA:
359                         sprintf(user_url, "%s?page=%d", identica_friends_url, session->page);
360                         curl_easy_setopt(curl, CURLOPT_URL, user_url);
361                         break;
362                 }
363                 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
364
365                 break;
366         case ACTION_USER:
367                 switch (session->host) {
368                 case HOST_TWITTER:
369                         sprintf(user_url, "%s%s.xml?page=%d", twitter_user_url, session->user, session->page);
370                         curl_easy_setopt(curl, CURLOPT_URL, user_url);
371                         break;
372                 case HOST_IDENTICA:
373                         sprintf(user_url, "%s%s.xml?page=%d", identica_user_url, session->user, session->page);
374                         curl_easy_setopt(curl, CURLOPT_URL, user_url);
375                         break;
376                 }
377
378                 break;
379         case ACTION_REPLIES:
380                 snprintf(user_password, sizeof(user_password), "%s:%s",
381                          session->account, session->password);
382                 switch (session->host) {
383                 case HOST_TWITTER:
384                         sprintf(user_url, "%s?page=%d", twitter_replies_url, session->page);
385                         curl_easy_setopt(curl, CURLOPT_URL, user_url);
386                         break;
387                 case HOST_IDENTICA:
388                         sprintf(user_url, "%s?page=%d", identica_replies_url, session->page);
389                         curl_easy_setopt(curl, CURLOPT_URL, user_url);
390                         break;
391                 }
392                 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
393
394                 break;
395         case ACTION_PUBLIC:
396                 switch (session->host) {
397                 case HOST_TWITTER:
398                         sprintf(user_url, "%s?page=%d", twitter_public_url, session->page);
399                         curl_easy_setopt(curl, CURLOPT_URL, user_url);
400                         break;
401                 case HOST_IDENTICA:
402                         sprintf(user_url, "%s?page=%d", identica_public_url, session->page);
403                         curl_easy_setopt(curl, CURLOPT_URL, user_url);
404                         break;
405                 }
406
407                 break;
408         default:
409                 break;
410         }
411
412         if (session->proxy)
413                 curl_easy_setopt(curl, CURLOPT_PROXY, session->proxy);
414
415         if (debug)
416                 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
417
418         dbg("user_password = %s\n", user_password);
419         dbg("data = %s\n", data);
420         dbg("proxy = %s\n", session->proxy);
421
422         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_callback);
423         curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_buf);
424         if (!session->dry_run) {
425                 res = curl_easy_perform(curl);
426                 if (res && !session->bash) {
427                         fprintf(stderr, "error(%d) trying to perform "
428                                 "operation\n", res);
429                         return -EINVAL;
430                 }
431         }
432
433         curl_easy_cleanup(curl);
434         if (session->action == ACTION_UPDATE)
435                 curl_formfree(formpost);
436         bti_curl_buffer_free(curl_buf);
437         return 0;
438 }
439
440 static void parse_configfile(struct session *session)
441 {
442         FILE *config_file;
443         char *line = NULL;
444         size_t len = 0;
445         char *account = NULL;
446         char *password = NULL;
447         char *host = NULL;
448         char *proxy = NULL;
449         char *logfile = NULL;
450         char *action = NULL;
451         char *user = NULL;
452         char *file;
453         int shrink_urls = 0;
454
455         /* config file is ~/.bti  */
456         file = alloca(strlen(session->homedir) + 7);
457
458         sprintf(file, "%s/.bti", session->homedir);
459
460         config_file = fopen(file, "r");
461
462         /* No error if file does not exist or is unreadable.  */
463         if (config_file == NULL)
464                 return;
465
466         do {
467                 ssize_t n = getline(&line, &len, config_file);
468                 if (n < 0)
469                         break;
470                 if (line[n - 1] == '\n')
471                         line[n - 1] = '\0';
472                 /* Parse file.  Format is the usual value pairs:
473                    account=name
474                    passwort=value
475                    # is a comment character
476                 */
477                 *strchrnul(line, '#') = '\0';
478                 char *c = line;
479                 while (isspace(*c))
480                         c++;
481                 /* Ignore blank lines.  */
482                 if (c[0] == '\0')
483                         continue;
484
485                 if (!strncasecmp(c, "account", 7) && (c[7] == '=')) {
486                         c += 8;
487                         if (c[0] != '\0')
488                                 account = strdup(c);
489                 } else if (!strncasecmp(c, "password", 8) &&
490                            (c[8] == '=')) {
491                         c += 9;
492                         if (c[0] != '\0')
493                                 password = strdup(c);
494                 } else if (!strncasecmp(c, "host", 4) &&
495                            (c[4] == '=')) {
496                         c += 5;
497                         if (c[0] != '\0')
498                                 host = strdup(c);
499                 } else if (!strncasecmp(c, "proxy", 5) &&
500                            (c[5] == '=')) {
501                         c += 6;
502                         if (c[0] != '\0')
503                                 proxy = strdup(c);
504                 } else if (!strncasecmp(c, "logfile", 7) &&
505                            (c[7] == '=')) {
506                         c += 8;
507                         if (c[0] != '\0')
508                                 logfile = strdup(c);
509                 } else if (!strncasecmp(c, "action", 6) &&
510                            (c[6] == '=')) {
511                         c += 7;
512                         if (c[0] != '\0')
513                                 action = strdup(c);
514                 } else if (!strncasecmp(c, "user", 4) &&
515                                 (c[4] == '=')) {
516                         c += 5;
517                         if (c[0] != '\0')
518                                 user = strdup(c);
519                 } else if (!strncasecmp(c, "shrink-urls", 11) &&
520                                 (c[11] == '=')) {
521                         c += 12;
522                         if (!strncasecmp(c, "true", 4) ||
523                                         !strncasecmp(c, "yes", 3))
524                                 shrink_urls = 1;
525                 }
526         } while (!feof(config_file));
527
528         if (password)
529                 session->password = password;
530         if (account)
531                 session->account = account;
532         if (host) {
533                 if (strcasecmp(host, "twitter") == 0)
534                         session->host = HOST_TWITTER;
535                 if (strcasecmp(host, "identica") == 0)
536                         session->host = HOST_IDENTICA;
537                 free(host);
538         }
539         if (proxy) {
540                 if (session->proxy)
541                         free(session->proxy);
542                 session->proxy = proxy;
543         }
544         if (logfile)
545                 session->logfile = logfile;
546         if (action) {
547                 if (strcasecmp(action, "update") == 0)
548                         session->action = ACTION_UPDATE;
549                 else if (strcasecmp(action, "friends") == 0)
550                         session->action = ACTION_FRIENDS;
551                 else if (strcasecmp(action, "user") == 0)
552                         session->action = ACTION_USER;
553                 else if (strcasecmp(action, "replies") == 0)
554                         session->action = ACTION_REPLIES;
555                 else if (strcasecmp(action, "public") == 0)
556                         session->action = ACTION_PUBLIC;
557                 else
558                         session->action = ACTION_UNKNOWN;
559                 free(action);
560         }
561         if (user)
562                 session->user = user;
563         session->shrink_urls = shrink_urls;
564
565         /* Free buffer and close file.  */
566         free(line);
567         fclose(config_file);
568 }
569
570 static void log_session(struct session *session, int retval)
571 {
572         FILE *log_file;
573         char *filename;
574         char *host;
575
576         /* Only log something if we have a log file set */
577         if (!session->logfile)
578                 return;
579
580         filename = alloca(strlen(session->homedir) +
581                           strlen(session->logfile) + 3);
582
583         sprintf(filename, "%s/%s", session->homedir, session->logfile);
584
585         log_file = fopen(filename, "a+");
586         if (log_file == NULL)
587                 return;
588         switch (session->host) {
589         case HOST_TWITTER:
590                 host = "twitter";
591                 break;
592         case HOST_IDENTICA:
593                 host = "identi.ca";
594                 break;
595         default:
596                 host = "unknown";
597                 break;
598         }
599
600         switch (session->action) {
601         case ACTION_UPDATE:
602                 if (retval)
603                         fprintf(log_file, "%s: host=%s tweet failed\n",
604                                 session->time, host);
605                 else
606                         fprintf(log_file, "%s: host=%s tweet=%s\n",
607                                 session->time, host, session->tweet);
608                 break;
609         case ACTION_FRIENDS:
610                 fprintf(log_file, "%s: host=%s retrieving friends timeline\n",
611                         session->time, host);
612                 break;
613         case ACTION_USER:
614                 fprintf(log_file, "%s: host=%s retrieving %s's timeline\n",
615                         session->time, host, session->user);
616                 break;
617         case ACTION_REPLIES:
618                 fprintf(log_file, "%s: host=%s retrieving replies\n",
619                         session->time, host);
620                 break;
621         case ACTION_PUBLIC:
622                 fprintf(log_file, "%s: host=%s retrieving public timeline\n",
623                         session->time, host);
624                 break;
625         default:
626                 break;
627         }
628
629         fclose(log_file);
630 }
631
632 static char *get_string_from_stdin(void)
633 {
634         char *temp;
635         char *string;
636
637         string = zalloc(1000);
638         if (!string)
639                 return NULL;
640
641         if (!fgets(string, 999, stdin))
642                 return NULL;
643         temp = strchr(string, '\n');
644         *temp = '\0';
645         return string;
646 }
647
648 static int find_urls(const char *tweet, int **pranges)
649 {
650         /*
651          * magic obtained from
652          * http://www.geekpedia.com/KB65_How-to-validate-an-URL-using-RegEx-in-Csharp.html
653          */
654         static const char *re_magic =
655                 "(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)/{1,3}"
656                 "[0-9a-zA-Z;/~?:@&=+$\\.\\-_'()%]+)"
657                 "(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?";
658         pcre *re;
659         const char *errptr;
660         int erroffset;
661         int ovector[10] = {0,};
662         const size_t ovsize = sizeof(ovector)/sizeof(*ovector);
663         int startoffset, tweetlen;
664         int i, rc;
665         int rbound = 10;
666         int rcount = 0;
667         int *ranges = malloc(sizeof(int) * rbound);
668
669         re = pcre_compile(re_magic,
670                         PCRE_NO_AUTO_CAPTURE,
671                         &errptr, &erroffset, NULL);
672         if (!re) {
673                 fprintf(stderr, "pcre_compile @%u: %s\n", erroffset, errptr);
674                 exit(1);
675         }
676
677         tweetlen = strlen(tweet);
678         for (startoffset = 0; startoffset < tweetlen; ) {
679
680                 rc = pcre_exec(re, NULL, tweet, strlen(tweet), startoffset, 0,
681                                 ovector, ovsize);
682                 if (rc == PCRE_ERROR_NOMATCH)
683                         break;
684
685                 if (rc < 0) {
686                         fprintf(stderr, "pcre_exec @%u: %s\n",
687                                 erroffset, errptr);
688                         exit(1);
689                 }
690
691                 for (i = 0; i < rc; i += 2) {
692                         if ((rcount+2) == rbound) {
693                                 rbound *= 2;
694                                 ranges = realloc(ranges, sizeof(int) * rbound);
695                         }
696
697                         ranges[rcount++] = ovector[i];
698                         ranges[rcount++] = ovector[i+1];
699                 }
700
701                 startoffset = ovector[1];
702         }
703
704         pcre_free(re);
705
706         *pranges = ranges;
707         return rcount;
708 }
709
710 /**
711  * bidirectional popen() call
712  *
713  * @param rwepipe - int array of size three
714  * @param exe - program to run
715  * @param argv - argument list
716  * @return pid or -1 on error
717  *
718  * The caller passes in an array of three integers (rwepipe), on successful
719  * execution it can then write to element 0 (stdin of exe), and read from
720  * element 1 (stdout) and 2 (stderr).
721  */
722 static int popenRWE(int *rwepipe, const char *exe, const char *const argv[])
723 {
724         int in[2];
725         int out[2];
726         int err[2];
727         int pid;
728         int rc;
729
730         rc = pipe(in);
731         if (rc < 0)
732                 goto error_in;
733
734         rc = pipe(out);
735         if (rc < 0)
736                 goto error_out;
737
738         rc = pipe(err);
739         if (rc < 0)
740                 goto error_err;
741
742         pid = fork();
743         if (pid > 0) {
744                 /* parent */
745                 close(in[0]);
746                 close(out[1]);
747                 close(err[1]);
748                 rwepipe[0] = in[1];
749                 rwepipe[1] = out[0];
750                 rwepipe[2] = err[0];
751                 return pid;
752         } else if (pid == 0) {
753                 /* child */
754                 close(in[1]);
755                 close(out[0]);
756                 close(err[0]);
757                 close(0);
758                 rc = dup(in[0]);
759                 close(1);
760                 rc = dup(out[1]);
761                 close(2);
762                 rc = dup(err[1]);
763
764                 execvp(exe, (char **)argv);
765                 exit(1);
766         } else
767                 goto error_fork;
768
769         return pid;
770
771 error_fork:
772         close(err[0]);
773         close(err[1]);
774 error_err:
775         close(out[0]);
776         close(out[1]);
777 error_out:
778         close(in[0]);
779         close(in[1]);
780 error_in:
781         return -1;
782 }
783
784 static int pcloseRWE(int pid, int *rwepipe)
785 {
786         int rc, status;
787         close(rwepipe[0]);
788         close(rwepipe[1]);
789         close(rwepipe[2]);
790         rc = waitpid(pid, &status, 0);
791         return status;
792 }
793
794 static char *shrink_one_url(int *rwepipe, char *big)
795 {
796         int biglen = strlen(big);
797         char *small;
798         int smalllen;
799         int rc;
800
801         rc = dprintf(rwepipe[0], "%s\n", big);
802         if (rc < 0)
803                 return big;
804
805         smalllen = biglen + 128;
806         small = malloc(smalllen);
807         if (!small)
808                 return big;
809
810         rc = read(rwepipe[1], small, smalllen);
811         if (rc < 0 || rc > biglen)
812                 goto error_free_small;
813
814         if (strncmp(small, "http://", 7))
815                 goto error_free_small;
816
817         smalllen = rc;
818         while (smalllen && isspace(small[smalllen-1]))
819                         small[--smalllen] = 0;
820
821         free(big);
822         return small;
823
824 error_free_small:
825         free(small);
826         return big;
827 }
828
829 static char *shrink_urls(char *text)
830 {
831         int *ranges;
832         int rcount;
833         int i;
834         int inofs = 0;
835         int outofs = 0;
836         const char *const shrink_args[] = {
837                 "bti-shrink-urls",
838                 NULL
839         };
840         int shrink_pid;
841         int shrink_pipe[3];
842         int inlen = strlen(text);
843
844         dbg("before len=%u\n", inlen);
845
846         shrink_pid = popenRWE(shrink_pipe, shrink_args[0], shrink_args);
847         if (shrink_pid < 0)
848                 return text;
849
850         rcount = find_urls(text, &ranges);
851         if (!rcount)
852                 return text;
853
854         for (i = 0; i < rcount; i += 2) {
855                 int url_start = ranges[i];
856                 int url_end = ranges[i+1];
857                 int long_url_len = url_end - url_start;
858                 char *url = strndup(text + url_start, long_url_len);
859                 int short_url_len;
860                 int not_url_len = url_start - inofs;
861
862                 dbg("long  url[%u]: %s\n", long_url_len, url);
863                 url = shrink_one_url(shrink_pipe, url);
864                 short_url_len = url ? strlen(url) : 0;
865                 dbg("short url[%u]: %s\n", short_url_len, url);
866
867                 if (!url || short_url_len >= long_url_len) {
868                         /* The short url ended up being too long
869                          * or unavailable */
870                         if (inofs) {
871                                 strncpy(text + outofs, text + inofs,
872                                                 not_url_len + long_url_len);
873                         }
874                         inofs += not_url_len + long_url_len;
875                         outofs += not_url_len + long_url_len;
876
877                 } else {
878                         /* copy the unmodified block */
879                         strncpy(text + outofs, text + inofs, not_url_len);
880                         inofs += not_url_len;
881                         outofs += not_url_len;
882
883                         /* copy the new url */
884                         strncpy(text + outofs, url, short_url_len);
885                         inofs += long_url_len;
886                         outofs += short_url_len;
887                 }
888
889                 free(url);
890         }
891
892         /* copy the last block after the last match */
893         if (inofs) {
894                 int tail = inlen - inofs;
895                 if (tail) {
896                         strncpy(text + outofs, text + inofs, tail);
897                         outofs += tail;
898                 }
899         }
900
901         free(ranges);
902
903         (void)pcloseRWE(shrink_pid, shrink_pipe);
904
905         text[outofs] = 0;
906         dbg("after len=%u\n", outofs);
907         return text;
908 }
909
910 int main(int argc, char *argv[], char *envp[])
911 {
912         static const struct option options[] = {
913                 { "debug", 0, NULL, 'd' },
914                 { "account", 1, NULL, 'a' },
915                 { "password", 1, NULL, 'p' },
916                 { "host", 1, NULL, 'H' },
917                 { "proxy", 1, NULL, 'P' },
918                 { "action", 1, NULL, 'A' },
919                 { "user", 1, NULL, 'u' },
920                 { "logfile", 1, NULL, 'L' },
921                 { "shrink-urls", 0, NULL, 's' },
922                 { "help", 0, NULL, 'h' },
923                 { "bash", 0, NULL, 'b' },
924                 { "dry-run", 0, NULL, 'n' },
925                 { "page", 1, NULL, 'g' },
926                 { "version", 0, NULL, 'v' },
927                 { }
928         };
929         struct session *session;
930         pid_t child;
931         char *tweet;
932         int retval = 0;
933         int option;
934         char *http_proxy;
935         time_t t;
936         int page_nr;
937
938         debug = 0;
939         rl_bind_key('\t', rl_insert);
940
941         session = session_alloc();
942         if (!session) {
943                 fprintf(stderr, "no more memory...\n");
944                 return -1;
945         }
946
947         /* get the current time so that we can log it later */
948         time(&t);
949         session->time = strdup(ctime(&t));
950         session->time[strlen(session->time)-1] = 0x00;
951
952         session->homedir = strdup(getenv("HOME"));
953
954         curl_global_init(CURL_GLOBAL_ALL);
955
956         /* Set environment variables first, before reading command line options
957          * or config file values. */
958         http_proxy = getenv("http_proxy");
959         if (http_proxy) {
960                 if (session->proxy)
961                         free(session->proxy);
962                 session->proxy = strdup(http_proxy);
963                 dbg("http_proxy = %s\n", session->proxy);
964         }
965
966         parse_configfile(session);
967
968         while (1) {
969                 option = getopt_long_only(argc, argv, "dqe:p:P:H:a:A:u:hg:",
970                                           options, NULL);
971                 if (option == -1)
972                         break;
973                 switch (option) {
974                 case 'd':
975                         debug = 1;
976                         break;
977                 case 'a':
978                         if (session->account)
979                                 free(session->account);
980                         session->account = strdup(optarg);
981                         dbg("account = %s\n", session->account);
982                         break;
983                 case 'g':
984                         page_nr = atoi(optarg);
985                         dbg("page = %d\n", page_nr);
986                         session->page = page_nr;
987                         break;
988                 case 'p':
989                         if (session->password)
990                                 free(session->password);
991                         session->password = strdup(optarg);
992                         dbg("password = %s\n", session->password);
993                         break;
994                 case 'P':
995                         if (session->proxy)
996                                 free(session->proxy);
997                         session->proxy = strdup(optarg);
998                         dbg("proxy = %s\n", session->proxy);
999                         break;
1000                 case 'A':
1001                         if (strcasecmp(optarg, "update") == 0)
1002                                 session->action = ACTION_UPDATE;
1003                         else if (strcasecmp(optarg, "friends") == 0)
1004                                 session->action = ACTION_FRIENDS;
1005                         else if (strcasecmp(optarg, "user") == 0)
1006                                 session->action = ACTION_USER;
1007                         else if (strcasecmp(optarg, "replies") == 0)
1008                                 session->action = ACTION_REPLIES;
1009                         else if (strcasecmp(optarg, "public") == 0)
1010                                 session->action = ACTION_PUBLIC;
1011                         else
1012                                 session->action = ACTION_UNKNOWN;
1013                         dbg("action = %d\n", session->action);
1014                         break;
1015                 case 'u':
1016                         if (session->user)
1017                                 free(session->user);
1018                         session->user = strdup(optarg);
1019                         dbg("user = %s\n", session->user);
1020                         break;
1021                 case 'L':
1022                         if (session->logfile)
1023                                 free(session->logfile);
1024                         session->logfile = strdup(optarg);
1025                         dbg("logfile = %s\n", session->logfile);
1026                         break;
1027                 case 's':
1028                         session->shrink_urls = 1;
1029                         break;
1030                 case 'H':
1031                         if (strcasecmp(optarg, "twitter") == 0)
1032                                 session->host = HOST_TWITTER;
1033                         if (strcasecmp(optarg, "identica") == 0)
1034                                 session->host = HOST_IDENTICA;
1035                         dbg("host = %d\n", session->host);
1036                         break;
1037                 case 'b':
1038                         session->bash = 1;
1039                         break;
1040                 case 'h':
1041                         display_help();
1042                         goto exit;
1043                 case 'n':
1044                         session->dry_run = 1;
1045                         break;
1046                 case 'v':
1047                         display_version();
1048                         goto exit;
1049                 default:
1050                         display_help();
1051                         goto exit;
1052                 }
1053         }
1054
1055         if (session->action == ACTION_UNKNOWN) {
1056                 fprintf(stderr, "Unknown action, valid actions are:\n");
1057                 fprintf(stderr, "'update', 'friends', 'public', "
1058                         "'replies' or 'user'.\n");
1059                 goto exit;
1060         }
1061
1062         if (!session->account) {
1063                 fprintf(stdout, "Enter twitter account: ");
1064                 session->account = readline(NULL);
1065         }
1066
1067         if (!session->password) {
1068                 fprintf(stdout, "Enter twitter password: ");
1069                 session->password = readline(NULL);
1070         }
1071
1072         if (session->action == ACTION_UPDATE) {
1073                 if (session->bash)
1074                         tweet = get_string_from_stdin();
1075                 else
1076                         tweet = readline("tweet: ");
1077                 if (!tweet || strlen(tweet) == 0) {
1078                         dbg("no tweet?\n");
1079                         return -1;
1080                 }
1081
1082                 if (session->shrink_urls)
1083                         tweet = shrink_urls(tweet);
1084
1085                 session->tweet = zalloc(strlen(tweet) + 10);
1086                 if (session->bash)
1087                         sprintf(session->tweet, "$ %s", tweet);
1088                 else
1089                         sprintf(session->tweet, "%s", tweet);
1090
1091                 free(tweet);
1092                 dbg("tweet = %s\n", session->tweet);
1093         }
1094
1095         if (!session->user)
1096                 session->user = strdup(session->account);
1097
1098         if (session->page == 0)
1099                 session->page = 1;
1100         dbg("account = %s\n", session->account);
1101         dbg("password = %s\n", session->password);
1102         dbg("host = %d\n", session->host);
1103         dbg("action = %d\n", session->action);
1104
1105         /* fork ourself so that the main shell can get on
1106          * with it's life as we try to connect and handle everything
1107          */
1108         if (session->bash) {
1109                 child = fork();
1110                 if (child) {
1111                         dbg("child is %d\n", child);
1112                         exit(0);
1113                 }
1114         }
1115
1116         retval = send_request(session);
1117         if (retval && !session->bash)
1118                 fprintf(stderr, "operation failed\n");
1119
1120         log_session(session, retval);
1121 exit:
1122         session_free(session);
1123         return retval;;
1124 }