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