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