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