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