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