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