Change the formatting of --action output
[gregoa/bti.git] / bti.c
1 /*
2  * Copyright (C) 2008 Greg Kroah-Hartman <greg@kroah.com>
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU General Public License as published by the
6  * Free Software Foundation version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16  */
17
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <stddef.h>
21 #include <string.h>
22 #include <getopt.h>
23 #include <errno.h>
24 #include <ctype.h>
25 #include <fcntl.h>
26 #include <unistd.h>
27 #include <time.h>
28 #include <sys/stat.h>
29 #include <curl/curl.h>
30 #include <readline/readline.h>
31 #include <libxml/xmlmemory.h>
32 #include <libxml/parser.h>
33 #include <libxml/tree.h>
34 #include "bti_version.h"
35
36
37 #define zalloc(size)    calloc(size, 1)
38
39 #define dbg(format, arg...)                                             \
40         do {                                                            \
41                 if (debug)                                              \
42                         printf("%s: " format , __func__ , ## arg);      \
43         } while (0)
44
45
46 static int debug;
47
48 enum host {
49         HOST_TWITTER = 0,
50         HOST_IDENTICA = 1,
51 };
52
53 enum action {
54      ACTION_UPDATE = 0,
55      ACTION_PUBLIC = 1,
56      ACTION_FRIENDS = 2,
57 };
58
59 struct session {
60         char *password;
61         char *account;
62         char *tweet;
63         char *proxy;
64         char *time;
65         char *homedir;
66         char *logfile;
67         int bash;
68         enum host host;
69         enum action action;
70 };
71
72 struct bti_curl_buffer {
73         char *data;
74         enum action action;
75         int length;
76 };
77
78 static void display_help(void)
79 {
80         fprintf(stdout, "bti - send tweet to twitter\n");
81         fprintf(stdout, "Version: " BTI_VERSION "\n");
82         fprintf(stdout, "Usage:\n");
83         fprintf(stdout, "  bti [options]\n");
84         fprintf(stdout, "options are:\n");
85         fprintf(stdout, "  --account accountname\n");
86         fprintf(stdout, "  --password password\n");
87         fprintf(stdout, "  --proxy PROXY:PORT\n");
88         fprintf(stdout, "  --host HOST\n");
89         fprintf(stdout, "  --logfile logfile\n");
90         fprintf(stdout, "  --bash\n");
91         fprintf(stdout, "  --action action\n");
92         fprintf(stdout, "  --debug\n");
93         fprintf(stdout, "  --version\n");
94         fprintf(stdout, "  --help\n");
95 }
96
97 static void display_version(void)
98 {
99         fprintf(stdout, "bti - version %s\n", BTI_VERSION);
100 }
101
102 static struct session *session_alloc(void)
103 {
104         struct session *session;
105
106         session = zalloc(sizeof(*session));
107         if (!session)
108                 return NULL;
109         return session;
110 }
111
112 static void session_free(struct session *session)
113 {
114         if (!session)
115                 return;
116         free(session->password);
117         free(session->account);
118         free(session->tweet);
119         free(session->proxy);
120         free(session->time);
121         free(session->homedir);
122         free(session);
123 }
124
125 static struct bti_curl_buffer *bti_curl_buffer_alloc(enum action action)
126 {
127         struct bti_curl_buffer *buffer;
128
129         buffer = zalloc(sizeof(*buffer));
130         if (!buffer)
131                 return NULL;
132
133         /* start out with a data buffer of 1 byte to
134          * make the buffer fill logic simpler */
135         buffer->data = zalloc(1);
136         if (!buffer->data) {
137                 free(buffer);
138                 return NULL;
139         }
140         buffer->length = 0;
141         buffer->action = action;
142         return buffer;
143 }
144
145 static void bti_curl_buffer_free(struct bti_curl_buffer *buffer)
146 {
147         if (!buffer)
148                 return;
149         free(buffer->data);
150         free(buffer);
151 }
152
153 static const char *twitter_update_url  = "https://twitter.com/statuses/update.xml";
154 static const char *twitter_public_url  = "http://twitter.com/statuses/public_timeline.xml";
155 static const char *twitter_friends_url = "https://twitter.com/statuses/friends_timeline.xml";
156
157 static const char *identica_update_url  = "http://identi.ca/api/statuses/update.xml";
158 static const char *identica_public_url  = "http://identi.ca/api/statuses/public_timeline.xml";
159 static const char *identica_friends_url = "http://identi.ca/api/statuses/friends_timeline.xml";
160
161 static CURL *curl_init(void)
162 {
163         CURL *curl;
164
165         curl = curl_easy_init();
166         if (!curl) {
167                 fprintf(stderr, "Can not init CURL!\n");
168                 return NULL;
169         }
170         /* some ssl sanity checks on the connection we are making */
171         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
172         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
173         return curl;
174 }
175
176 void parse_statuses(xmlDocPtr doc, xmlNodePtr current)
177 {
178         xmlChar *text = NULL;
179         xmlChar *user = NULL;
180         xmlNodePtr userinfo;
181
182         current = current->xmlChildrenNode;
183         while (current != NULL) {
184                 if (current->type == XML_ELEMENT_NODE) {
185                         if (!xmlStrcmp(current->name, (const xmlChar *)"text"))
186                                 text = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
187                         if (!xmlStrcmp(current->name, (const xmlChar *)"user")) {
188                                 userinfo = current->xmlChildrenNode;
189                                 while (userinfo != NULL) {
190                                         if ((!xmlStrcmp(userinfo->name, (const xmlChar *)"screen_name"))) {
191                                                 if (user)
192                                                         xmlFree(user);
193                                                 user = xmlNodeListGetString(doc, userinfo->xmlChildrenNode, 1);
194                                         }
195                                         userinfo = userinfo->next;
196                                 }
197                         }
198                         if (user && text) {
199                                 printf("[%s] %s\n", user, text);
200                                 xmlFree(user);
201                                 xmlFree(text);
202                                 user = NULL;
203                                 text = NULL;
204                         }
205                 }
206                 current = current->next;
207         }
208
209         return;
210 }
211
212 static void parse_timeline(char *document)
213 {
214         xmlDocPtr doc;
215         xmlNodePtr current;
216         doc = xmlReadMemory(document, strlen(document), "timeline.xml", NULL, XML_PARSE_NOERROR);
217
218         if (doc == NULL)
219                 return;
220
221         current = xmlDocGetRootElement(doc);
222         if (current == NULL) {
223                 fprintf(stderr, "empty document\n");
224                 xmlFreeDoc(doc);
225                 return;
226         }
227
228         if (xmlStrcmp(current->name, (const xmlChar *) "statuses")) {
229                 fprintf(stderr, "unexpected document type\n");
230                 xmlFreeDoc(doc);
231                 return;
232         }
233
234         current = current->xmlChildrenNode;
235         while (current != NULL) {
236                 if ((!xmlStrcmp(current->name, (const xmlChar *)"status")))
237                         parse_statuses(doc, current);
238                 current = current->next;
239         }
240         xmlFreeDoc(doc);
241
242         return;
243 }
244
245 size_t curl_callback(void *buffer, size_t size, size_t nmemb, void *userp)
246 {
247         struct bti_curl_buffer *curl_buf = userp;
248         size_t buffer_size = size * nmemb;
249         char *temp;
250
251         if ((!buffer) || (!buffer_size) || (!curl_buf))
252                 return -EINVAL;
253
254         /* add to the data we already have */
255         temp = zalloc(curl_buf->length + buffer_size + 1);
256         if (!temp)
257                 return -ENOMEM;
258
259         memcpy(temp, curl_buf->data, curl_buf->length);
260         free(curl_buf->data);
261         curl_buf->data = temp;
262         memcpy(&curl_buf->data[curl_buf->length], (char *)buffer, buffer_size);
263         curl_buf->length += buffer_size;
264         if ((curl_buf->action == ACTION_FRIENDS) ||
265                 (curl_buf->action == ACTION_PUBLIC))
266                 parse_timeline(curl_buf->data);
267
268         dbg("%s\n", curl_buf->data);
269
270         return buffer_size;
271 }
272
273 static int send_request(struct session *session)
274 {
275         char user_password[500];
276         char data[500];
277         struct bti_curl_buffer *curl_buf;
278         CURL *curl = NULL;
279         CURLcode res;
280         struct curl_httppost *formpost = NULL;
281         struct curl_httppost *lastptr = NULL;
282         struct curl_slist *slist = NULL;
283
284         if (!session)
285                 return -EINVAL;
286
287         curl_buf = bti_curl_buffer_alloc(session->action);
288         if (!curl_buf)
289                 return -ENOMEM;
290
291         curl = curl_init();
292         if (!curl)
293                 return -EINVAL;
294
295         switch (session->action) {
296         case ACTION_UPDATE:
297                 snprintf(user_password, sizeof(user_password), "%s:%s",
298                          session->account, session->password);
299                 snprintf(data, sizeof(data), "status=\"%s\"", session->tweet);
300                 curl_formadd(&formpost, &lastptr,
301                              CURLFORM_COPYNAME, "status",
302                              CURLFORM_COPYCONTENTS, session->tweet,
303                              CURLFORM_END);
304
305                 curl_formadd(&formpost, &lastptr,
306                              CURLFORM_COPYNAME, "source",
307                              CURLFORM_COPYCONTENTS, "bti",
308                              CURLFORM_END);
309
310                 curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
311                 slist = curl_slist_append(slist, "Expect:");
312                 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
313                 switch (session->host) {
314                 case HOST_TWITTER:
315                         curl_easy_setopt(curl, CURLOPT_URL, twitter_update_url);
316                         break;
317                 case HOST_IDENTICA:
318                         curl_easy_setopt(curl, CURLOPT_URL, identica_update_url);
319                         break;
320                 }
321                 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
322
323                 break;
324         case ACTION_FRIENDS:
325                 snprintf(user_password, sizeof(user_password), "%s:%s",
326                          session->account, session->password);
327                 switch (session->host) {
328                 case HOST_TWITTER:
329                         curl_easy_setopt(curl, CURLOPT_URL, twitter_friends_url);
330                         break;
331                 case HOST_IDENTICA:
332                         curl_easy_setopt(curl, CURLOPT_URL, identica_friends_url);
333                         break;
334                 }
335                 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
336
337                 break;
338         case ACTION_PUBLIC:
339                 switch (session->host) {
340                 case HOST_TWITTER:
341                         curl_easy_setopt(curl, CURLOPT_URL, twitter_public_url);
342                         break;
343                 case HOST_IDENTICA:
344                         curl_easy_setopt(curl, CURLOPT_URL, identica_public_url);
345                         break;
346                 }
347
348                 break;
349         }
350
351         if (session->proxy)
352                 curl_easy_setopt(curl, CURLOPT_PROXY, session->proxy);
353
354         if (debug)
355                 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
356
357         dbg("user_password = %s\n", user_password);
358         dbg("data = %s\n", data);
359         dbg("proxy = %s\n", session->proxy);
360
361         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_callback);
362         curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_buf);
363         res = curl_easy_perform(curl);
364         if (res && !session->bash) {
365                 fprintf(stderr, "error(%d) trying to perform operation\n", res);
366                 return -EINVAL;
367         }
368
369         curl_easy_cleanup(curl);
370         if (session->action == ACTION_UPDATE)
371                 curl_formfree(formpost);
372         bti_curl_buffer_free(curl_buf);
373         return 0;
374 }
375
376 static void parse_configfile(struct session *session)
377 {
378         FILE *config_file;
379         char *line = NULL;
380         size_t len = 0;
381         char *account = NULL;
382         char *password = NULL;
383         char *host = NULL;
384         char *proxy = NULL;
385         char *logfile = NULL;
386         char *action = NULL;
387         char *file;
388
389         /* config file is ~/.bti  */
390         file = alloca(strlen(session->homedir) + 7);
391
392         sprintf(file, "%s/.bti", session->homedir);
393
394         config_file = fopen(file, "r");
395
396         /* No error if file does not exist or is unreadable.  */
397         if (config_file == NULL)
398                 return;
399
400         do {
401                 ssize_t n = getline(&line, &len, config_file);
402                 if (n < 0)
403                         break;
404                 if (line[n - 1] == '\n')
405                         line[n - 1] = '\0';
406                 /* Parse file.  Format is the usual value pairs:
407                    account=name
408                    passwort=value
409                    # is a comment character
410                 */
411                 *strchrnul(line, '#') = '\0';
412                 char *c = line;
413                 while (isspace(*c))
414                         c++;
415                 /* Ignore blank lines.  */
416                 if (c[0] == '\0')
417                         continue;
418
419                 if (!strncasecmp(c, "account", 7) && (c[7] == '=')) {
420                         c += 8;
421                         if (c[0] != '\0')
422                                 account = strdup(c);
423                 } else if (!strncasecmp(c, "password", 8) &&
424                            (c[8] == '=')) {
425                         c += 9;
426                         if (c[0] != '\0')
427                                 password = strdup(c);
428                 } else if (!strncasecmp(c, "host", 4) &&
429                            (c[4] == '=')) {
430                         c += 5;
431                         if (c[0] != '\0')
432                                 host = strdup(c);
433                 } else if (!strncasecmp(c, "proxy", 5) &&
434                            (c[5] == '=')) {
435                         c += 6;
436                         if (c[0] != '\0')
437                                 proxy = strdup(c);
438                 } else if (!strncasecmp(c, "logfile", 7) &&
439                            (c[7] == '=')) {
440                         c += 8;
441                         if (c[0] != '\0')
442                                 logfile = strdup(c);
443                 } else if (!strncasecmp(c, "action", 6) &&
444                            (c[6] == '=')) {
445                         c += 7;
446                         if (c[0] != '\0')
447                                 action = strdup(c);
448                 }
449         } while (!feof(config_file));
450
451         if (password)
452                 session->password = password;
453         if (account)
454                 session->account = account;
455         if (host) {
456                 if (strcasecmp(host, "twitter") == 0)
457                         session->host = HOST_TWITTER;
458                 if (strcasecmp(host, "identica") == 0)
459                         session->host = HOST_IDENTICA;
460                 free(host);
461         }
462         if (proxy) {
463                 if (session->proxy)
464                         free(session->proxy);
465                 session->proxy = proxy;
466         }
467         if (logfile)
468                 session->logfile = logfile;
469         if (action) {
470                 if (strcasecmp(action, "update") == 0)
471                         session->action = ACTION_UPDATE;
472                 if (strcasecmp(action, "friends") == 0)
473                         session->action = ACTION_FRIENDS;
474                 if (strcasecmp(action, "public") == 0)
475                         session->action = ACTION_PUBLIC;
476                 free(action);
477         }
478
479         /* Free buffer and close file.  */
480         free(line);
481         fclose(config_file);
482 }
483
484 static void log_session(struct session *session, int retval)
485 {
486         FILE *log_file;
487         char *filename;
488         char *host;
489
490         /* Only log something if we have a log file set */
491         if (!session->logfile)
492                 return;
493
494         filename = alloca(strlen(session->homedir) +
495                           strlen(session->logfile) + 3);
496
497         sprintf(filename, "%s/%s", session->homedir, session->logfile);
498
499         log_file = fopen(filename, "a+");
500         if (log_file == NULL)
501                 return;
502         switch (session->host) {
503         case HOST_TWITTER:
504                 host = "twitter";
505                 break;
506         case HOST_IDENTICA:
507                 host = "identi.ca";
508                 break;
509         default:
510                 host = "unknown";
511                 break;
512         }
513
514         if (session->action == ACTION_UPDATE) {
515                 if (retval)
516                         fprintf(log_file, "%s: host=%s tweet failed\n",
517                                 session->time, host);
518                 else
519                         fprintf(log_file, "%s: host=%s tweet=%s\n",
520                                 session->time, host, session->tweet);
521         } else if (session->action == ACTION_FRIENDS) {
522                 fprintf(log_file, "%s: host=%s retrieving friends timeline\n",
523                         session->time, host);
524         } else if (session->action == ACTION_PUBLIC) {
525                 fprintf(log_file, "%s: host=%s retrieving public timeline\n",
526                         session->time, host);
527         }
528
529         fclose(log_file);
530 }
531
532 int main(int argc, char *argv[], char *envp[])
533 {
534         static const struct option options[] = {
535                 { "debug", 0, NULL, 'd' },
536                 { "account", 1, NULL, 'a' },
537                 { "password", 1, NULL, 'p' },
538                 { "host", 1, NULL, 'H' },
539                 { "proxy", 1, NULL, 'P' },
540                 { "action", 1, NULL, 'A' },
541                 { "logfile", 1, NULL, 'L' },
542                 { "help", 0, NULL, 'h' },
543                 { "bash", 0, NULL, 'b' },
544                 { "version", 0, NULL, 'v' },
545                 { }
546         };
547         struct session *session;
548         pid_t child;
549         char *tweet;
550         int retval = 0;
551         int option;
552         char *http_proxy;
553         time_t t;
554
555         debug = 0;
556         rl_bind_key('\t', rl_insert);
557
558         session = session_alloc();
559         if (!session) {
560                 fprintf(stderr, "no more memory...\n");
561                 return -1;
562         }
563
564         /* get the current time so that we can log it later */
565         time(&t);
566         session->time = strdup(ctime(&t));
567         session->time[strlen(session->time)-1] = 0x00;
568
569         session->homedir = strdup(getenv("HOME"));
570
571         curl_global_init(CURL_GLOBAL_ALL);
572
573         /* Set environment variables first, before reading command line options
574          * or config file values. */
575         http_proxy = getenv("http_proxy");
576         if (http_proxy) {
577                 if (session->proxy)
578                         free(session->proxy);
579                 session->proxy = strdup(http_proxy);
580                 dbg("http_proxy = %s\n", session->proxy);
581         }
582
583         parse_configfile(session);
584
585         while (1) {
586                 option = getopt_long_only(argc, argv, "dqe:p:P:H:a:A:h",
587                                           options, NULL);
588                 if (option == -1)
589                         break;
590                 switch (option) {
591                 case 'd':
592                         debug = 1;
593                         break;
594                 case 'a':
595                         if (session->account)
596                                 free(session->account);
597                         session->account = strdup(optarg);
598                         dbg("account = %s\n", session->account);
599                         break;
600                 case 'p':
601                         if (session->password)
602                                 free(session->password);
603                         session->password = strdup(optarg);
604                         dbg("password = %s\n", session->password);
605                         break;
606                 case 'P':
607                         if (session->proxy)
608                                 free(session->proxy);
609                         session->proxy = strdup(optarg);
610                         dbg("proxy = %s\n", session->proxy);
611                         break;
612                 case 'A':
613                         if (strcasecmp(optarg, "update") == 0)
614                                 session->action = ACTION_UPDATE;
615                         if (strcasecmp(optarg, "friends") == 0)
616                                 session->action = ACTION_FRIENDS;
617                         if (strcasecmp(optarg, "public") == 0)
618                                 session->action = ACTION_PUBLIC;
619                         dbg("action = %d\n", session->action);
620                         break;
621                 case 'L':
622                         if (session->logfile)
623                                 free(session->logfile);
624                         session->logfile = strdup(optarg);
625                         dbg("logfile = %s\n", session->logfile);
626                         break;
627                 case 'H':
628                         if (strcasecmp(optarg, "twitter") == 0)
629                                 session->host = HOST_TWITTER;
630                         if (strcasecmp(optarg, "identica") == 0)
631                                 session->host = HOST_IDENTICA;
632                         dbg("host = %d\n", session->host);
633                         break;
634                 case 'b':
635                         session->bash = 1;
636                         break;
637                 case 'h':
638                         display_help();
639                         goto exit;
640                 case 'v':
641                         display_version();
642                         goto exit;
643                 default:
644                         display_help();
645                         goto exit;
646                 }
647         }
648
649         if (!session->account) {
650                 fprintf(stdout, "Enter twitter account: ");
651                 session->account = readline(NULL);
652         }
653
654         if (!session->password) {
655                 fprintf(stdout, "Enter twitter password: ");
656                 session->password = readline(NULL);
657         }
658
659         if (session->action == ACTION_UPDATE) {
660                 if (session->bash)
661                         tweet = readline(NULL);
662                 else
663                         tweet = readline("tweet: ");
664                 if (!tweet || strlen(tweet) == 0) {
665                         dbg("no tweet?\n");
666                         return -1;
667                 }
668
669                 session->tweet = zalloc(strlen(tweet) + 10);
670                 if (session->bash)
671                         sprintf(session->tweet, "$ %s", tweet);
672                 else
673                         sprintf(session->tweet, "%s", tweet);
674
675                 free(tweet);
676                 dbg("tweet = %s\n", session->tweet);
677         }
678
679         dbg("account = %s\n", session->account);
680         dbg("password = %s\n", session->password);
681         dbg("host = %d\n", session->host);
682         dbg("action = %d\n", session->action);
683
684         /* fork ourself so that the main shell can get on
685          * with it's life as we try to connect and handle everything
686          */
687         if (session->bash) {
688                 child = fork();
689                 if (child) {
690                         dbg("child is %d\n", child);
691                         exit(0);
692                 }
693         }
694
695         retval = send_request(session);
696         if (retval && !session->bash)
697                 fprintf(stderr, "operation failed\n");
698
699         log_session(session, retval);
700 exit:
701         session_free(session);
702         return retval;;
703 }