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