libxml2 libs and cflags
[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;
179         xmlChar *user;
180         xmlNodePtr userinfo;
181         current = current->xmlChildrenNode;
182         while (current != NULL) {
183                 if (current->type == XML_ELEMENT_NODE) {
184                         if (!xmlStrcmp(current->name, (const xmlChar *)"text")) {
185                                 text = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
186                                 printf("%s", text);
187                                 xmlFree(text);
188                         }
189                         if (!xmlStrcmp(current->name, (const xmlChar *)"user")) {
190                                 userinfo = current->xmlChildrenNode;
191                                 while (userinfo != NULL) {
192                                         if ((!xmlStrcmp(userinfo->name, (const xmlChar *)"screen_name"))) {
193                                                 user = xmlNodeListGetString(doc, userinfo->xmlChildrenNode, 1);
194                                                 printf(" [%s]\n", user);
195                                                 xmlFree(user);
196                                         }
197
198                                         userinfo = userinfo->next;
199                                 }
200                         }
201                 }
202
203                 current = current->next;
204         }
205
206         return;
207 }
208
209 static void parse_timeline(char *document)
210 {
211         xmlDocPtr doc;
212         xmlNodePtr current;
213         doc = xmlReadMemory(document, strlen(document), "timeline.xml", NULL, XML_PARSE_NOERROR);
214
215         if (doc == NULL)
216                 return;
217
218         current = xmlDocGetRootElement(doc);
219         if (current == NULL) {
220                 fprintf(stderr, "empty document\n");
221                 xmlFreeDoc(doc);
222                 return;
223         }
224
225         if (xmlStrcmp(current->name, (const xmlChar *) "statuses")) {
226                 fprintf(stderr, "unexpected document type\n");
227                 xmlFreeDoc(doc);
228                 return;
229         }
230
231         current = current->xmlChildrenNode;
232         while (current != NULL) {
233                 if ((!xmlStrcmp(current->name, (const xmlChar *)"status")))
234                         parse_statuses(doc, current);
235                 current = current->next;
236         }
237         xmlFreeDoc(doc);
238
239         return;
240 }
241
242 size_t curl_callback(void *buffer, size_t size, size_t nmemb, void *userp)
243 {
244         struct bti_curl_buffer *curl_buf = userp;
245         size_t buffer_size = size * nmemb;
246         char *temp;
247
248         if ((!buffer) || (!buffer_size) || (!curl_buf))
249                 return -EINVAL;
250
251         /* add to the data we already have */
252         temp = zalloc(curl_buf->length + buffer_size + 1);
253         if (!temp)
254                 return -ENOMEM;
255
256         memcpy(temp, curl_buf->data, curl_buf->length);
257         free(curl_buf->data);
258         curl_buf->data = temp;
259         memcpy(&curl_buf->data[curl_buf->length], (char *)buffer, buffer_size);
260         curl_buf->length += buffer_size;
261         if ((curl_buf->action == ACTION_FRIENDS) ||
262                 (curl_buf->action == ACTION_PUBLIC))
263                 parse_timeline(curl_buf->data);
264
265         dbg("%s\n", curl_buf->data);
266
267         return buffer_size;
268 }
269
270 static int send_request(struct session *session)
271 {
272         char user_password[500];
273         char data[500];
274         struct bti_curl_buffer *curl_buf;
275         CURL *curl = NULL;
276         CURLcode res;
277         struct curl_httppost *formpost = NULL;
278         struct curl_httppost *lastptr = NULL;
279         struct curl_slist *slist = NULL;
280
281         if (!session)
282                 return -EINVAL;
283
284         curl_buf = bti_curl_buffer_alloc(session->action);
285         if (!curl_buf)
286                 return -ENOMEM;
287
288         curl = curl_init();
289         if (!curl)
290                 return -EINVAL;
291
292         switch (session->action) {
293         case ACTION_UPDATE:
294                 snprintf(user_password, sizeof(user_password), "%s:%s",
295                          session->account, session->password);
296                 snprintf(data, sizeof(data), "status=\"%s\"", session->tweet);
297                 curl_formadd(&formpost, &lastptr,
298                              CURLFORM_COPYNAME, "status",
299                              CURLFORM_COPYCONTENTS, session->tweet,
300                              CURLFORM_END);
301
302                 curl_formadd(&formpost, &lastptr,
303                              CURLFORM_COPYNAME, "source",
304                              CURLFORM_COPYCONTENTS, "bti",
305                              CURLFORM_END);
306
307                 curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
308                 slist = curl_slist_append(slist, "Expect:");
309                 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
310                 switch (session->host) {
311                 case HOST_TWITTER:
312                         curl_easy_setopt(curl, CURLOPT_URL, twitter_update_url);
313                         break;
314                 case HOST_IDENTICA:
315                         curl_easy_setopt(curl, CURLOPT_URL, identica_update_url);
316                         break;
317                 }
318                 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
319
320                 break;
321         case ACTION_FRIENDS:
322                 snprintf(user_password, sizeof(user_password), "%s:%s",
323                          session->account, session->password);
324                 switch (session->host) {
325                 case HOST_TWITTER:
326                         curl_easy_setopt(curl, CURLOPT_URL, twitter_friends_url);
327                         break;
328                 case HOST_IDENTICA:
329                         curl_easy_setopt(curl, CURLOPT_URL, identica_friends_url);
330                         break;
331                 }
332                 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
333
334                 break;
335         case ACTION_PUBLIC:
336                 switch (session->host) {
337                 case HOST_TWITTER:
338                         curl_easy_setopt(curl, CURLOPT_URL, twitter_public_url);
339                         break;
340                 case HOST_IDENTICA:
341                         curl_easy_setopt(curl, CURLOPT_URL, identica_public_url);
342                         break;
343                 }
344
345                 break;
346         }
347
348         if (session->proxy)
349                 curl_easy_setopt(curl, CURLOPT_PROXY, session->proxy);
350
351         if (debug)
352                 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
353
354         dbg("user_password = %s\n", user_password);
355         dbg("data = %s\n", data);
356         dbg("proxy = %s\n", session->proxy);
357
358         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_callback);
359         curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_buf);
360         res = curl_easy_perform(curl);
361         if (res && !session->bash) {
362                 fprintf(stderr, "error(%d) trying to perform operation\n", res);
363                 return -EINVAL;
364         }
365
366         curl_easy_cleanup(curl);
367         if (session->action == ACTION_UPDATE)
368                 curl_formfree(formpost);
369         bti_curl_buffer_free(curl_buf);
370         return 0;
371 }
372
373 static void parse_configfile(struct session *session)
374 {
375         FILE *config_file;
376         char *line = NULL;
377         size_t len = 0;
378         char *account = NULL;
379         char *password = NULL;
380         char *host = NULL;
381         char *proxy = NULL;
382         char *logfile = NULL;
383         char *action = NULL;
384         char *file;
385
386         /* config file is ~/.bti  */
387         file = alloca(strlen(session->homedir) + 7);
388
389         sprintf(file, "%s/.bti", session->homedir);
390
391         config_file = fopen(file, "r");
392
393         /* No error if file does not exist or is unreadable.  */
394         if (config_file == NULL)
395                 return;
396
397         do {
398                 ssize_t n = getline(&line, &len, config_file);
399                 if (n < 0)
400                         break;
401                 if (line[n - 1] == '\n')
402                         line[n - 1] = '\0';
403                 /* Parse file.  Format is the usual value pairs:
404                    account=name
405                    passwort=value
406                    # is a comment character
407                 */
408                 *strchrnul(line, '#') = '\0';
409                 char *c = line;
410                 while (isspace(*c))
411                         c++;
412                 /* Ignore blank lines.  */
413                 if (c[0] == '\0')
414                         continue;
415
416                 if (!strncasecmp(c, "account", 7) && (c[7] == '=')) {
417                         c += 8;
418                         if (c[0] != '\0')
419                                 account = strdup(c);
420                 } else if (!strncasecmp(c, "password", 8) &&
421                            (c[8] == '=')) {
422                         c += 9;
423                         if (c[0] != '\0')
424                                 password = strdup(c);
425                 } else if (!strncasecmp(c, "host", 4) &&
426                            (c[4] == '=')) {
427                         c += 5;
428                         if (c[0] != '\0')
429                                 host = strdup(c);
430                 } else if (!strncasecmp(c, "proxy", 5) &&
431                            (c[5] == '=')) {
432                         c += 6;
433                         if (c[0] != '\0')
434                                 proxy = strdup(c);
435                 } else if (!strncasecmp(c, "logfile", 7) &&
436                            (c[7] == '=')) {
437                         c += 8;
438                         if (c[0] != '\0')
439                                 logfile = strdup(c);
440                 } else if (!strncasecmp(c, "action", 6) &&
441                            (c[6] == '=')) {
442                         c += 7;
443                         if (c[0] != '\0')
444                                 action = strdup(c);
445                 }
446         } while (!feof(config_file));
447
448         if (password)
449                 session->password = password;
450         if (account)
451                 session->account = account;
452         if (host) {
453                 if (strcasecmp(host, "twitter") == 0)
454                         session->host = HOST_TWITTER;
455                 if (strcasecmp(host, "identica") == 0)
456                         session->host = HOST_IDENTICA;
457                 free(host);
458         }
459         if (proxy) {
460                 if (session->proxy)
461                         free(session->proxy);
462                 session->proxy = proxy;
463         }
464         if (logfile)
465                 session->logfile = logfile;
466         if (action) {
467                 if (strcasecmp(action, "update") == 0)
468                         session->action = ACTION_UPDATE;
469                 if (strcasecmp(action, "friends") == 0)
470                         session->action = ACTION_FRIENDS;
471                 if (strcasecmp(action, "public") == 0)
472                         session->action = ACTION_PUBLIC;
473                 free(action);
474         }
475
476         /* Free buffer and close file.  */
477         free(line);
478         fclose(config_file);
479 }
480
481 static void log_session(struct session *session, int retval)
482 {
483         FILE *log_file;
484         char *filename;
485         char *host;
486
487         /* Only log something if we have a log file set */
488         if (!session->logfile)
489                 return;
490
491         filename = alloca(strlen(session->homedir) +
492                           strlen(session->logfile) + 3);
493
494         sprintf(filename, "%s/%s", session->homedir, session->logfile);
495
496         log_file = fopen(filename, "a+");
497         if (log_file == NULL)
498                 return;
499         switch (session->host) {
500         case HOST_TWITTER:
501                 host = "twitter";
502                 break;
503         case HOST_IDENTICA:
504                 host = "identi.ca";
505                 break;
506         default:
507                 host = "unknown";
508                 break;
509         }
510
511         if (session->action == ACTION_UPDATE) {
512                 if (retval)
513                         fprintf(log_file, "%s: host=%s tweet failed\n",
514                                 session->time, host);
515                 else
516                         fprintf(log_file, "%s: host=%s tweet=%s\n",
517                                 session->time, host, session->tweet);
518         } else if (session->action == ACTION_FRIENDS) {
519                 fprintf(log_file, "%s: host=%s retrieving friends timeline\n",
520                         session->time, host);
521         } else if (session->action == ACTION_PUBLIC) {
522                 fprintf(log_file, "%s: host=%s retrieving public timeline\n",
523                         session->time, host);
524         }
525
526         fclose(log_file);
527 }
528
529 int main(int argc, char *argv[], char *envp[])
530 {
531         static const struct option options[] = {
532                 { "debug", 0, NULL, 'd' },
533                 { "account", 1, NULL, 'a' },
534                 { "password", 1, NULL, 'p' },
535                 { "host", 1, NULL, 'H' },
536                 { "proxy", 1, NULL, 'P' },
537                 { "action", 1, NULL, 'A' },
538                 { "logfile", 1, NULL, 'L' },
539                 { "help", 0, NULL, 'h' },
540                 { "bash", 0, NULL, 'b' },
541                 { "version", 0, NULL, 'v' },
542                 { }
543         };
544         struct session *session;
545         pid_t child;
546         char *tweet;
547         int retval = 0;
548         int option;
549         char *http_proxy;
550         time_t t;
551
552         debug = 0;
553         rl_bind_key('\t', rl_insert);
554
555         session = session_alloc();
556         if (!session) {
557                 fprintf(stderr, "no more memory...\n");
558                 return -1;
559         }
560
561         /* get the current time so that we can log it later */
562         time(&t);
563         session->time = strdup(ctime(&t));
564         session->time[strlen(session->time)-1] = 0x00;
565
566         session->homedir = strdup(getenv("HOME"));
567
568         curl_global_init(CURL_GLOBAL_ALL);
569
570         /* Set environment variables first, before reading command line options
571          * or config file values. */
572         http_proxy = getenv("http_proxy");
573         if (http_proxy) {
574                 if (session->proxy)
575                         free(session->proxy);
576                 session->proxy = strdup(http_proxy);
577                 dbg("http_proxy = %s\n", session->proxy);
578         }
579
580         parse_configfile(session);
581
582         while (1) {
583                 option = getopt_long_only(argc, argv, "dqe:p:P:H:a:A:h",
584                                           options, NULL);
585                 if (option == -1)
586                         break;
587                 switch (option) {
588                 case 'd':
589                         debug = 1;
590                         break;
591                 case 'a':
592                         if (session->account)
593                                 free(session->account);
594                         session->account = strdup(optarg);
595                         dbg("account = %s\n", session->account);
596                         break;
597                 case 'p':
598                         if (session->password)
599                                 free(session->password);
600                         session->password = strdup(optarg);
601                         dbg("password = %s\n", session->password);
602                         break;
603                 case 'P':
604                         if (session->proxy)
605                                 free(session->proxy);
606                         session->proxy = strdup(optarg);
607                         dbg("proxy = %s\n", session->proxy);
608                         break;
609                 case 'A':
610                         if (strcasecmp(optarg, "update") == 0)
611                                 session->action = ACTION_UPDATE;
612                         if (strcasecmp(optarg, "friends") == 0)
613                                 session->action = ACTION_FRIENDS;
614                         if (strcasecmp(optarg, "public") == 0)
615                                 session->action = ACTION_PUBLIC;
616                         dbg("action = %d\n", session->action);
617                         break;
618                 case 'L':
619                         if (session->logfile)
620                                 free(session->logfile);
621                         session->logfile = strdup(optarg);
622                         dbg("logfile = %s\n", session->logfile);
623                         break;
624                 case 'H':
625                         if (strcasecmp(optarg, "twitter") == 0)
626                                 session->host = HOST_TWITTER;
627                         if (strcasecmp(optarg, "identica") == 0)
628                                 session->host = HOST_IDENTICA;
629                         dbg("host = %d\n", session->host);
630                         break;
631                 case 'b':
632                         session->bash = 1;
633                         break;
634                 case 'h':
635                         display_help();
636                         goto exit;
637                 case 'v':
638                         display_version();
639                         goto exit;
640                 default:
641                         display_help();
642                         goto exit;
643                 }
644         }
645
646         if (!session->account) {
647                 fprintf(stdout, "Enter twitter account: ");
648                 session->account = readline(NULL);
649         }
650
651         if (!session->password) {
652                 fprintf(stdout, "Enter twitter password: ");
653                 session->password = readline(NULL);
654         }
655
656         if (session->action == ACTION_UPDATE) {
657                 if (session->bash)
658                         tweet = readline(NULL);
659                 else
660                         tweet = readline("tweet: ");
661                 if (!tweet || strlen(tweet) == 0) {
662                         dbg("no tweet?\n");
663                         return -1;
664                 }
665
666                 session->tweet = zalloc(strlen(tweet) + 10);
667                 if (session->bash)
668                         sprintf(session->tweet, "$ %s", tweet);
669                 else
670                         sprintf(session->tweet, "%s", tweet);
671
672                 free(tweet);
673                 dbg("tweet = %s\n", session->tweet);
674         }
675
676         dbg("account = %s\n", session->account);
677         dbg("password = %s\n", session->password);
678         dbg("host = %d\n", session->host);
679         dbg("action = %d\n", session->action);
680
681         /* fork ourself so that the main shell can get on
682          * with it's life as we try to connect and handle everything
683          */
684         if (session->bash) {
685                 child = fork();
686                 if (child) {
687                         dbg("child is %d\n", child);
688                         exit(0);
689                 }
690         }
691
692         retval = send_request(session);
693         if (retval && !session->bash)
694                 fprintf(stderr, "operation failed\n");
695
696         log_session(session, retval);
697 exit:
698         session_free(session);
699         return retval;;
700 }