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