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