2 * Copyright (C) 2008-2011 Greg Kroah-Hartman <greg@kroah.com>
3 * Copyright (C) 2009 Bart Trojanowski <bart@jukie.net>
4 * Copyright (C) 2009-2010 Amir Mohammad Saied <amirsaied@gmail.com>
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation version 2 of the License.
10 * This program is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
29 #include <sys/types.h>
31 #include <curl/curl.h>
32 #include <libxml/xmlmemory.h>
33 #include <libxml/parser.h>
34 #include <libxml/tree.h>
35 #include <json-c/json.h>
42 #define zalloc(size) calloc(size, 1)
44 #define dbg(format, arg...) \
47 fprintf(stdout, "bti: %s: " format , __func__ , \
54 static void display_help(void)
56 fprintf(stdout, "bti - send tweet to twitter or identi.ca\n"
61 " --account accountname\n"
62 " --password password\n"
64 " ('update', 'friends', 'public', 'replies', 'user', or 'direct')\n"
65 " --user screenname\n"
66 " --group groupname\n"
67 " --proxy PROXY:PORT\n"
69 " --logfile logfile\n"
70 " --config configfile\n"
74 " --page PAGENUMBER\n"
75 " --column COLUMNWIDTH\n"
82 " --help\n", VERSION);
85 static int strlen_utf8(char *s)
89 if ((s[i] & 0xc0) != 0x80)
96 static void display_version(void)
98 fprintf(stdout, "bti - version %s\n", VERSION);
101 static char *get_string(const char *name)
106 string = zalloc(1000);
110 fprintf(stdout, "%s", name);
111 if (!fgets(string, 999, stdin)) {
115 temp = strchr(string, '\n');
122 * Try to get a handle to a readline function from a variety of different
123 * libraries. If nothing is present on the system, then fall back to an
126 * Logic originally based off of code in the e2fsutils package in the
127 * lib/ss/get_readline.c file, which is licensed under the MIT license.
129 * This keeps us from having to relicense the bti codebase if readline
130 * ever changes its license, as there is no link-time dependency.
131 * It is a run-time thing only, and we handle any readline-like library
132 * in the same manner, making bti not be a derivative work of any
135 static void session_readline_init(struct session *session)
137 /* Libraries we will try to use for readline/editline functionality */
138 const char *libpath = "libreadline.so.6:libreadline.so.5:"
139 "libreadline.so.4:libreadline.so:libedit.so.2:"
140 "libedit.so:libeditline.so.0:libeditline.so";
142 char *tmp, *cp, *next;
143 int (*bind_key)(int, void *);
144 void (*insert)(void);
146 /* default to internal function if we can't or won't find anything */
147 session->readline = get_string;
150 session->interactive = 1;
152 tmp = malloc(strlen(libpath)+1);
155 strcpy(tmp, libpath);
156 for (cp = tmp; cp; cp = next) {
157 next = strchr(cp, ':');
162 handle = dlopen(cp, RTLD_NOW);
164 dbg("Using %s for readline library\n", cp);
170 dbg("No readline library found.\n");
174 session->readline_handle = handle;
175 session->readline = (char *(*)(const char *))dlsym(handle, "readline");
176 if (session->readline == NULL) {
177 /* something odd happened, default back to internal stuff */
178 session->readline_handle = NULL;
179 session->readline = get_string;
184 * If we found a library, turn off filename expansion
185 * as that makes no sense from within bti.
187 bind_key = (int (*)(int, void *))dlsym(handle, "rl_bind_key");
188 insert = (void (*)(void))dlsym(handle, "rl_insert");
189 if (bind_key && insert)
190 bind_key('\t', insert);
193 static void session_readline_cleanup(struct session *session)
195 if (session->readline_handle)
196 dlclose(session->readline_handle);
199 static struct session *session_alloc(void)
201 struct session *session;
203 session = zalloc(sizeof(*session));
209 static void session_free(struct session *session)
213 free(session->retweet);
214 free(session->replyto);
215 free(session->password);
216 free(session->account);
217 free(session->consumer_key);
218 free(session->consumer_secret);
219 free(session->access_token_key);
220 free(session->access_token_secret);
221 free(session->tweet);
222 free(session->proxy);
224 free(session->homedir);
226 free(session->group);
227 free(session->hosturl);
228 free(session->hostname);
229 free(session->configfile);
233 static struct bti_curl_buffer *bti_curl_buffer_alloc(enum action action)
235 struct bti_curl_buffer *buffer;
237 buffer = zalloc(sizeof(*buffer));
241 /* start out with a data buffer of 1 byte to
242 * make the buffer fill logic simpler */
243 buffer->data = zalloc(1);
249 buffer->action = action;
253 static void bti_curl_buffer_free(struct bti_curl_buffer *buffer)
261 const char twitter_host[] = "https://api.twitter.com/1.1/statuses";
262 const char twitter_host_stream[] = "https://stream.twitter.com/1.1/statuses"; /*this is not reset, and doesnt work */
263 const char twitter_host_simple[] = "https://api.twitter.com/1.1";
264 const char twitter_name[] = "twitter";
266 static const char twitter_request_token_uri[] = "https://twitter.com/oauth/request_token";
267 static const char twitter_access_token_uri[] = "https://twitter.com/oauth/access_token";
268 static const char twitter_authorize_uri[] = "https://twitter.com/oauth/authorize?oauth_token=";
269 static const char custom_request_token_uri[] = "/../oauth/request_token?oauth_callback=oob";
270 static const char custom_access_token_uri[] = "/../oauth/access_token";
271 static const char custom_authorize_uri[] = "/../oauth/authorize?oauth_token=";
273 static const char user_uri[] = "/user_timeline.json";
274 static const char update_uri[] = "/update.json";
275 static const char public_uri[] = "/sample.json";
276 static const char friends_uri[] = "/home_timeline.json";
277 static const char mentions_uri[] = "/mentions_timeline.json";
278 static const char replies_uri[] = "/replies.xml";
279 static const char retweet_uri[] = "/retweet/";
280 static const char group_uri[] = "/../statusnet/groups/timeline/";
281 /*static const char direct_uri[] = "/direct_messages/new.xml";*/
282 static const char direct_uri[] = "/direct_messages/new.json";
284 static const char config_default[] = "/etc/bti";
285 static const char config_xdg_default[] = ".config/bti";
286 static const char config_user_default[] = ".bti";
289 static CURL *curl_init(void)
293 curl = curl_easy_init();
295 fprintf(stderr, "Can not init CURL!\n");
298 /* some ssl sanity checks on the connection we are making */
299 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
300 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
304 static void find_config_file(struct session *session)
312 * Get the home directory so we can try to find a config file.
313 * If we have no home dir set up, look in /etc/bti
315 home = getenv("HOME");
317 /* No home dir, so take the defaults and get out of here */
318 session->homedir = strdup("");
319 session->configfile = strdup(config_default);
323 /* We have a home dir, so this might be a user */
324 session->homedir = strdup(home);
325 homedir_size = strlen(session->homedir);
328 * Try to find a config file, we do so in this order:
329 * ~/.bti old-school config file
330 * ~/.config/bti new-school config file
332 file = zalloc(homedir_size + strlen(config_user_default) + 7);
333 sprintf(file, "%s/%s", home, config_user_default);
334 if (stat(file, &s) == 0) {
335 /* Found the config file at ~/.bti */
336 session->configfile = strdup(file);
342 file = zalloc(homedir_size + strlen(config_xdg_default) + 7);
343 sprintf(file, "%s/%s", home, config_xdg_default);
344 if (stat(file, &s) == 0) {
345 /* config file is at ~/.config/bti */
346 session->configfile = strdup(file);
351 /* No idea where the config file is, so punt */
353 session->configfile = strdup("");
356 /* The final place data is sent to the screen/pty/tty */
357 static void bti_output_line(struct session *session, xmlChar *user,
358 xmlChar *id, xmlChar *created, xmlChar *text)
360 if (session->verbose)
361 printf("[%*s] {%s} (%.16s) %s\n", -session->column_output, user,
364 printf("[%*s] %s\n", -session->column_output, user, text);
367 static void parse_statuses(struct session *session,
368 xmlDocPtr doc, xmlNodePtr current)
370 xmlChar *text = NULL;
371 xmlChar *user = NULL;
372 xmlChar *created = NULL;
376 current = current->xmlChildrenNode;
377 while (current != NULL) {
378 if (current->type == XML_ELEMENT_NODE) {
379 if (!xmlStrcmp(current->name, (const xmlChar *)"created_at"))
380 created = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
381 if (!xmlStrcmp(current->name, (const xmlChar *)"text"))
382 text = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
383 if (!xmlStrcmp(current->name, (const xmlChar *)"id"))
384 id = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
385 if (!xmlStrcmp(current->name, (const xmlChar *)"user")) {
386 userinfo = current->xmlChildrenNode;
387 while (userinfo != NULL) {
388 if ((!xmlStrcmp(userinfo->name, (const xmlChar *)"screen_name"))) {
391 user = xmlNodeListGetString(doc, userinfo->xmlChildrenNode, 1);
393 userinfo = userinfo->next;
397 if (user && text && created && id) {
398 bti_output_line(session, user, id,
410 current = current->next;
416 static void parse_timeline(char *document, struct session *session)
421 doc = xmlReadMemory(document, strlen(document), "timeline.xml",
422 NULL, XML_PARSE_NOERROR);
426 current = xmlDocGetRootElement(doc);
427 if (current == NULL) {
428 fprintf(stderr, "empty document\n");
433 if (xmlStrcmp(current->name, (const xmlChar *) "statuses")) {
434 fprintf(stderr, "unexpected document type\n");
439 current = current->xmlChildrenNode;
440 while (current != NULL) {
441 if ((!xmlStrcmp(current->name, (const xmlChar *)"status")))
442 parse_statuses(session, doc, current);
443 current = current->next;
451 /* avoids the c99 option */
452 #define json_object_object_foreach_alt(obj,key,val) \
454 struct json_object *val; \
455 struct lh_entry *entry; \
456 for (entry = json_object_get_object(obj)->head; \
457 ({ if(entry && !is_error(entry)) { \
458 key = (char*)entry->k; \
459 val = (struct json_object*)entry->v; \
461 entry = entry->next )
464 /* Forward Declaration */
465 static void json_parse(json_object * jobj, int nestlevel);
467 static void print_json_value(json_object *jobj, int nestlevel)
471 type = json_object_get_type(jobj);
473 case json_type_boolean:
475 printf("value: %s\n", json_object_get_boolean(jobj)? "true": "false");
477 case json_type_double:
479 printf("value: %lf\n", json_object_get_double(jobj));
483 printf("value: %d\n", json_object_get_int(jobj));
485 case json_type_string:
487 printf("value: %s\n", json_object_get_string(jobj));
494 #define MAXKEYSTACK 20
495 char *keystack[MAXKEYSTACK];
497 static void json_parse_array(json_object *jobj, char *key, int nestlevel)
502 /* Simply get the array */
503 json_object *jarray = jobj;
505 /* Get the array if it is a key value pair */
506 jarray = json_object_object_get(jobj, key);
509 /* Get the length of the array */
510 int arraylen = json_object_array_length(jarray);
512 printf("Array Length: %d\n",arraylen);
516 for (i = 0; i < arraylen; i++) {
519 for (j=0; j < nestlevel; ++j)
521 printf("element[%d]\n",i);
524 /* Get the array element at position i */
525 jvalue = json_object_array_get_idx(jarray, i);
526 type = json_object_get_type(jvalue);
527 if (type == json_type_array) {
528 json_parse_array(jvalue, NULL, nestlevel);
529 } else if (type != json_type_object) {
531 printf("value[%d]: ", i);
532 print_json_value(jvalue,nestlevel);
535 /* printf("obj: "); */
536 keystack[nestlevel%MAXKEYSTACK]="[]";
537 json_parse(jvalue,nestlevel);
548 struct session *store_session;
556 static void json_interpret(json_object *jobj, int nestlevel)
558 if (nestlevel == 3 &&
559 strcmp(keystack[1], "errors") == 0 &&
560 strcmp(keystack[2], "[]") == 0) {
561 if (strcmp(keystack[3], "message") == 0) {
562 if (json_object_get_type(jobj) == json_type_string)
563 results.message = (char *)json_object_get_string(jobj);
565 if (strcmp(keystack[3], "code") == 0) {
566 if (json_object_get_type(jobj) == json_type_int)
567 results.code = json_object_get_int(jobj);
571 if (nestlevel >= 2 &&
572 strcmp(keystack[1],"[]") == 0) {
573 if (strcmp(keystack[2], "created_at") == 0) {
575 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
576 tweetdetail.created_at = (char *)json_object_get_string(jobj);
578 if (strcmp(keystack[2], "text") == 0) {
580 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
581 tweetdetail.text = (char *)json_object_get_string(jobj);
583 if (strcmp(keystack[2], "id") == 0) {
585 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
586 tweetdetail.id = (char *)json_object_get_string(jobj);
588 if (nestlevel >= 3 &&
589 strcmp(keystack[2], "user") == 0) {
590 if (strcmp(keystack[3], "screen_name") == 0) {
592 printf("%s->%s : %s\n", keystack[2], keystack[3], json_object_get_string(jobj));
593 tweetdetail.screen_name=(char *)json_object_get_string(jobj);
594 bti_output_line(store_session,
595 (xmlChar *)tweetdetail.screen_name,
596 (xmlChar *)tweetdetail.id,
597 (xmlChar *)tweetdetail.created_at,
598 (xmlChar *)tweetdetail.text);
604 /* Parsing the json object */
605 static void json_parse(json_object * jobj, int nestlevel)
610 fprintf(stderr,"jobj null\n");
615 json_object_object_foreach_alt(jobj, key, val) {
616 /* work around pre10 */
618 type = json_object_get_type(val);
622 for (i = 0; i < nestlevel; ++i)
625 printf("key %-34s ", key);
627 for (i = 0; i < 8 - nestlevel; ++i)
630 case json_type_boolean:
631 case json_type_double:
633 case json_type_string:
634 if (debug) print_json_value(val,nestlevel);
635 if (debug) for (i=0; i<nestlevel+1; ++i) printf(" ");
636 if (debug) printf("(");
637 if (debug) for (i=1; i<nestlevel; ++i) { printf("%s->",keystack[i]); }
638 if (debug) printf("%s)\n",key);
639 keystack[nestlevel%MAXKEYSTACK] = key;
640 json_interpret(val,nestlevel);
642 case json_type_object:
643 if (debug) printf("json_type_object\n");
644 keystack[nestlevel%MAXKEYSTACK] = key;
645 json_parse(json_object_object_get(jobj, key), nestlevel);
647 case json_type_array:
648 if (debug) printf("json_type_array, ");
649 keystack[nestlevel%MAXKEYSTACK] = key;
650 json_parse_array(jobj, key, nestlevel);
653 if (debug) printf("null\n");
656 if (debug) printf("\n");
662 static int parse_response_json(char *document, struct session *session)
664 dbg("Got this json response:\n");
665 dbg("%s\n",document);
668 results.message=NULL;
669 json_object *jobj = json_tokener_parse(document);
671 /* make global for now */
672 store_session = session;
673 if (!is_error(jobj)) {
674 /* guards against a json pre 0.10 bug */
677 if (results.code && results.message != NULL) {
679 printf("Got an error code:\n code=%d\n message=%s\n",
680 results.code, results.message);
681 fprintf(stderr, "error condition detected: %d = %s\n",
682 results.code, results.message);
688 static void parse_timeline_json(char *document, struct session *session)
690 dbg("Got this json response:\n");
691 dbg("%s\n",document);
693 results.message = NULL;
694 json_object *jobj = json_tokener_parse(document);
696 /* make global for now */
697 store_session = session;
698 if (!is_error(jobj)) {
699 /* guards against a json pre 0.10 bug */
700 if (json_object_get_type(jobj)==json_type_array) {
701 json_parse_array(jobj, NULL, 0);
706 if (results.code && results.message != NULL) {
708 printf("Got an error code:\n code=%d\n message=%s\n",
709 results.code, results.message);
710 fprintf(stderr, "error condition detected: %d = %s\n",
711 results.code, results.message);
715 static size_t curl_callback(void *buffer, size_t size, size_t nmemb,
718 struct bti_curl_buffer *curl_buf = userp;
719 size_t buffer_size = size * nmemb;
722 if ((!buffer) || (!buffer_size) || (!curl_buf))
725 /* add to the data we already have */
726 temp = zalloc(curl_buf->length + buffer_size + 1);
730 memcpy(temp, curl_buf->data, curl_buf->length);
731 free(curl_buf->data);
732 curl_buf->data = temp;
733 memcpy(&curl_buf->data[curl_buf->length], (char *)buffer, buffer_size);
734 curl_buf->length += buffer_size;
735 if (curl_buf->action)
736 parse_timeline(curl_buf->data, curl_buf->session);
738 dbg("%s\n", curl_buf->data);
743 static int parse_osp_reply(const char *reply, char **token, char **secret)
748 rc = oauth_split_url_parameters(reply, &rv);
749 qsort(rv, rc, sizeof(char *), oauth_cmpstringp);
750 if (rc == 2 || rc == 4) {
751 if (!strncmp(rv[0], "oauth_token=", 11) &&
752 !strncmp(rv[1], "oauth_token_secret=", 18)) {
754 *token = strdup(&(rv[0][12]));
756 *secret = strdup(&(rv[1][19]));
760 } else if (rc == 3) {
761 if (!strncmp(rv[1], "oauth_token=", 11) &&
762 !strncmp(rv[2], "oauth_token_secret=", 18)) {
764 *token = strdup(&(rv[1][12]));
766 *secret = strdup(&(rv[2][19]));
772 dbg("token: %s\n", *token);
773 dbg("secret: %s\n", *secret);
781 static int request_access_token(struct session *session)
783 char *post_params = NULL;
784 char *request_url = NULL;
787 char *at_secret = NULL;
788 char *verifier = NULL;
795 if (session->host == HOST_TWITTER)
796 request_url = oauth_sign_url2(
797 twitter_request_token_uri, NULL,
798 OA_HMAC, NULL, session->consumer_key,
799 session->consumer_secret, NULL, NULL);
801 sprintf(token_uri, "%s%s",
802 session->hosturl, custom_request_token_uri);
803 request_url = oauth_sign_url2(
805 OA_HMAC, NULL, session->consumer_key,
806 session->consumer_secret, NULL, NULL);
808 reply = oauth_http_get(request_url, post_params);
819 if (parse_osp_reply(reply, &at_key, &at_secret))
825 "Please open the following link in your browser, and "
826 "allow 'bti' to access your account. Then paste "
827 "back the provided PIN in here.\n");
828 if (session->host == HOST_TWITTER) {
829 fprintf(stdout, "%s%s\nPIN: ", twitter_authorize_uri, at_key);
830 verifier = session->readline(NULL);
831 sprintf(at_uri, "%s?oauth_verifier=%s",
832 twitter_access_token_uri, verifier);
834 fprintf(stdout, "%s%s%s\nPIN: ",
835 session->hosturl, custom_authorize_uri, at_key);
836 verifier = session->readline(NULL);
837 sprintf(at_uri, "%s%s?oauth_verifier=%s",
838 session->hosturl, custom_access_token_uri, verifier);
840 request_url = oauth_sign_url2(at_uri, NULL, OA_HMAC, NULL,
841 session->consumer_key,
842 session->consumer_secret,
844 reply = oauth_http_get(request_url, post_params);
849 if (parse_osp_reply(reply, &at_key, &at_secret))
855 "Please put these two lines in your bti "
856 "configuration file (%s):\n"
857 "access_token_key=%s\n"
858 "access_token_secret=%s\n",
859 session->configfile, at_key, at_secret);
864 static int send_request(struct session *session)
866 const int endpoint_size = 2000;
867 char endpoint[endpoint_size];
868 char user_password[500];
870 struct bti_curl_buffer *curl_buf;
873 struct curl_httppost *formpost = NULL;
874 struct curl_httppost *lastptr = NULL;
875 struct curl_slist *slist = NULL;
876 char *req_url = NULL;
878 char *postarg = NULL;
879 char *escaped_tweet = NULL;
885 if (!session->hosturl)
886 session->hosturl = strdup(twitter_host);
888 if (session->no_oauth || session->guest) {
889 curl_buf = bti_curl_buffer_alloc(session->action);
892 curl_buf->session = session;
896 bti_curl_buffer_free(curl_buf);
900 if (!session->hosturl)
901 session->hosturl = strdup(twitter_host);
903 switch (session->action) {
905 snprintf(user_password, sizeof(user_password), "%s:%s",
906 session->account, session->password);
907 snprintf(data, sizeof(data), "status=\"%s\"",
909 curl_formadd(&formpost, &lastptr,
910 CURLFORM_COPYNAME, "status",
911 CURLFORM_COPYCONTENTS, session->tweet,
914 curl_formadd(&formpost, &lastptr,
915 CURLFORM_COPYNAME, "source",
916 CURLFORM_COPYCONTENTS, "bti",
919 if (session->replyto)
920 curl_formadd(&formpost, &lastptr,
922 "in_reply_to_status_id",
923 CURLFORM_COPYCONTENTS,
927 curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
928 slist = curl_slist_append(slist, "Expect:");
929 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
931 snprintf(endpoint, endpoint_size, "%s%s", session->hosturl, update_uri);
932 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
933 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
937 snprintf(user_password, sizeof(user_password), "%s:%s",
938 session->account, session->password);
939 snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,
940 friends_uri, session->page);
941 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
942 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
946 snprintf(endpoint, endpoint_size, "%s%s%s.xml?page=%d", session->hosturl,
947 user_uri, session->user, session->page);
948 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
952 snprintf(user_password, sizeof(user_password), "%s:%s",
953 session->account, session->password);
954 snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,
955 replies_uri, session->page);
956 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
957 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
961 /*snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,*/
962 snprintf(endpoint, endpoint_size, "%s%s", twitter_host_stream,
964 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
968 snprintf(endpoint, endpoint_size, "%s%s%s.xml?page=%d",
969 session->hosturl, group_uri, session->group,
971 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
975 /* NOT IMPLEMENTED - twitter requires authentication anyway */
983 curl_easy_setopt(curl, CURLOPT_PROXY, session->proxy);
986 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
988 dbg("user_password = %s\n", user_password);
989 dbg("data = %s\n", data);
990 dbg("proxy = %s\n", session->proxy);
992 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_callback);
993 curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_buf);
994 if (!session->dry_run) {
995 res = curl_easy_perform(curl);
996 if (!session->background) {
1002 "error(%d) trying to perform operation\n",
1004 curl_easy_cleanup(curl);
1005 if (session->action == ACTION_UPDATE)
1006 curl_formfree(formpost);
1007 bti_curl_buffer_free(curl_buf);
1011 doc = xmlReadMemory(curl_buf->data,
1013 "response.xml", NULL,
1016 curl_easy_cleanup(curl);
1017 if (session->action == ACTION_UPDATE)
1018 curl_formfree(formpost);
1019 bti_curl_buffer_free(curl_buf);
1023 current = xmlDocGetRootElement(doc);
1024 if (current == NULL) {
1025 fprintf(stderr, "empty document\n");
1027 curl_easy_cleanup(curl);
1028 if (session->action == ACTION_UPDATE)
1029 curl_formfree(formpost);
1030 bti_curl_buffer_free(curl_buf);
1034 if (xmlStrcmp(current->name, (const xmlChar *)"status")) {
1035 fprintf(stderr, "unexpected document type\n");
1037 curl_easy_cleanup(curl);
1038 if (session->action == ACTION_UPDATE)
1039 curl_formfree(formpost);
1040 bti_curl_buffer_free(curl_buf);
1048 curl_easy_cleanup(curl);
1049 if (session->action == ACTION_UPDATE)
1050 curl_formfree(formpost);
1051 bti_curl_buffer_free(curl_buf);
1053 switch (session->action) {
1055 /* dont test it here, let twitter return an error that we show */
1056 if (strlen_utf8(session->tweet) > 140 + 1000 ) {
1057 printf("E: tweet is too long!\n");
1061 /* TODO: add tweet crunching function. */
1062 escaped_tweet = oauth_url_escape(session->tweet);
1063 if (session->replyto) {
1065 "%s%s?status=%s&in_reply_to_status_id=%s",
1066 session->hosturl, update_uri,
1067 escaped_tweet, session->replyto);
1069 sprintf(endpoint, "%s%s?status=%s",
1070 session->hosturl, update_uri,
1077 sprintf(endpoint, "%s%s?screen_name=%s&page=%d",
1078 session->hosturl, user_uri, session->user,
1081 case ACTION_REPLIES:
1082 sprintf(endpoint, "%s%s?page=%d", session->hosturl,
1083 mentions_uri, session->page);
1086 sprintf(endpoint, "%s%s", twitter_host_stream,
1090 sprintf(endpoint, "%s%s%s.xml?page=%d",
1091 session->hosturl, group_uri, session->group,
1094 case ACTION_FRIENDS:
1095 sprintf(endpoint, "%s%s?page=%d", session->hosturl,
1096 friends_uri, session->page);
1098 case ACTION_RETWEET:
1099 sprintf(endpoint, "%s%s%s.xml", session->hosturl,
1100 retweet_uri, session->retweet);
1104 escaped_tweet = oauth_url_escape(session->tweet);
1105 sprintf(endpoint, "%s%s?user=%s&text=%s", twitter_host_simple,
1106 direct_uri, session->user, escaped_tweet);
1113 dbg("%s\n", endpoint);
1114 if (!session->dry_run) {
1116 req_url = oauth_sign_url2(endpoint, &postarg, OA_HMAC,
1117 NULL, session->consumer_key,
1118 session->consumer_secret,
1119 session->access_token_key,
1120 session->access_token_secret);
1121 reply = oauth_http_post(req_url, postarg);
1123 req_url = oauth_sign_url2(endpoint, NULL, OA_HMAC, NULL,
1124 session->consumer_key,
1125 session->consumer_secret,
1126 session->access_token_key,
1127 session->access_token_secret);
1128 reply = oauth_http_get(req_url, postarg);
1131 dbg("req_url:%s\n", req_url);
1132 dbg("reply:%s\n", reply);
1137 fprintf(stderr, "Error retrieving from URL (%s)\n", endpoint);
1141 if ((session->action != ACTION_UPDATE) &&
1142 (session->action != ACTION_RETWEET) &&
1143 (session->action != ACTION_DIRECT))
1144 parse_timeline_json(reply, session);
1146 if ((session->action == ACTION_UPDATE) ||
1147 (session->action == ACTION_DIRECT))
1148 /*return parse_response_xml(reply, session);*/
1149 return parse_response_json(reply, session);
1158 static void log_session(struct session *session, int retval)
1163 /* Only log something if we have a log file set */
1164 if (!session->logfile)
1167 filename = alloca(strlen(session->homedir) +
1168 strlen(session->logfile) + 3);
1170 sprintf(filename, "%s/%s", session->homedir, session->logfile);
1172 log_file = fopen(filename, "a+");
1173 if (log_file == NULL)
1176 switch (session->action) {
1179 fprintf(log_file, "%s: host=%s tweet failed\n",
1180 session->time, session->hostname);
1182 fprintf(log_file, "%s: host=%s tweet=%s\n",
1183 session->time, session->hostname,
1186 case ACTION_FRIENDS:
1187 fprintf(log_file, "%s: host=%s retrieving friends timeline\n",
1188 session->time, session->hostname);
1191 fprintf(log_file, "%s: host=%s retrieving %s's timeline\n",
1192 session->time, session->hostname, session->user);
1194 case ACTION_REPLIES:
1195 fprintf(log_file, "%s: host=%s retrieving replies\n",
1196 session->time, session->hostname);
1199 fprintf(log_file, "%s: host=%s retrieving public timeline\n",
1200 session->time, session->hostname);
1203 fprintf(log_file, "%s: host=%s retrieving group timeline\n",
1204 session->time, session->hostname);
1208 fprintf(log_file, "%s: host=%s tweet failed\n",
1209 session->time, session->hostname);
1211 fprintf(log_file, "%s: host=%s tweet=%s\n",
1212 session->time, session->hostname,
1222 static char *get_string_from_stdin(void)
1227 string = zalloc(1000);
1231 if (!fgets(string, 999, stdin)) {
1235 temp = strchr(string, '\n');
1241 static void read_password(char *buf, size_t len, char *host)
1250 tp.c_lflag &= (~ECHO);
1251 tcsetattr(0, TCSANOW, &tp);
1253 fprintf(stdout, "Enter password for %s: ", host);
1258 * I'd like to do something with the return value here, but really,
1261 (void)scanf("%79s", pwd);
1264 fprintf(stdout, "\n");
1266 tcsetattr(0, TCSANOW, &old);
1268 strncpy(buf, pwd, len);
1272 static int find_urls(const char *tweet, int **pranges)
1275 * magic obtained from
1276 * http://www.geekpedia.com/KB65_How-to-validate-an-URL-using-RegEx-in-Csharp.html
1278 static const char *re_magic =
1279 "(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)/{1,3}"
1280 "[0-9a-zA-Z;/~?:@&=+$\\.\\-_'()%]+)"
1281 "(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?";
1285 int ovector[10] = {0,};
1286 const size_t ovsize = sizeof(ovector)/sizeof(*ovector);
1287 int startoffset, tweetlen;
1291 int *ranges = malloc(sizeof(int) * rbound);
1293 re = pcre_compile(re_magic,
1294 PCRE_NO_AUTO_CAPTURE,
1295 &errptr, &erroffset, NULL);
1297 fprintf(stderr, "pcre_compile @%u: %s\n", erroffset, errptr);
1301 tweetlen = strlen(tweet);
1302 for (startoffset = 0; startoffset < tweetlen; ) {
1304 rc = pcre_exec(re, NULL, tweet, strlen(tweet), startoffset, 0,
1306 if (rc == PCRE_ERROR_NOMATCH)
1310 fprintf(stderr, "pcre_exec @%u: %s\n",
1315 for (i = 0; i < rc; i += 2) {
1316 if ((rcount+2) == rbound) {
1318 ranges = realloc(ranges, sizeof(int) * rbound);
1321 ranges[rcount++] = ovector[i];
1322 ranges[rcount++] = ovector[i+1];
1325 startoffset = ovector[1];
1335 * bidirectional popen() call
1337 * @param rwepipe - int array of size three
1338 * @param exe - program to run
1339 * @param argv - argument list
1340 * @return pid or -1 on error
1342 * The caller passes in an array of three integers (rwepipe), on successful
1343 * execution it can then write to element 0 (stdin of exe), and read from
1344 * element 1 (stdout) and 2 (stderr).
1346 static int popenRWE(int *rwepipe, const char *exe, const char *const argv[])
1373 rwepipe[1] = out[0];
1374 rwepipe[2] = err[0];
1376 } else if (pid == 0) {
1388 execvp(exe, (char **)argv);
1408 static int pcloseRWE(int pid, int *rwepipe)
1414 (void)waitpid(pid, &status, 0);
1418 static char *shrink_one_url(int *rwepipe, char *big)
1420 int biglen = strlen(big);
1425 rc = dprintf(rwepipe[0], "%s\n", big);
1429 smalllen = biglen + 128;
1430 small = malloc(smalllen);
1434 rc = read(rwepipe[1], small, smalllen);
1435 if (rc < 0 || rc > biglen)
1436 goto error_free_small;
1438 if (strncmp(small, "http://", 7))
1439 goto error_free_small;
1442 while (smalllen && isspace(small[smalllen-1]))
1443 small[--smalllen] = 0;
1453 static char *shrink_urls(char *text)
1460 const char *const shrink_args[] = {
1466 int inlen = strlen(text);
1468 dbg("before len=%u\n", inlen);
1470 shrink_pid = popenRWE(shrink_pipe, shrink_args[0], shrink_args);
1474 rcount = find_urls(text, &ranges);
1478 for (i = 0; i < rcount; i += 2) {
1479 int url_start = ranges[i];
1480 int url_end = ranges[i+1];
1481 int long_url_len = url_end - url_start;
1482 char *url = strndup(text + url_start, long_url_len);
1484 int not_url_len = url_start - inofs;
1486 dbg("long url[%u]: %s\n", long_url_len, url);
1487 url = shrink_one_url(shrink_pipe, url);
1488 short_url_len = url ? strlen(url) : 0;
1489 dbg("short url[%u]: %s\n", short_url_len, url);
1491 if (!url || short_url_len >= long_url_len) {
1492 /* The short url ended up being too long
1495 strncpy(text + outofs, text + inofs,
1496 not_url_len + long_url_len);
1498 inofs += not_url_len + long_url_len;
1499 outofs += not_url_len + long_url_len;
1502 /* copy the unmodified block */
1503 strncpy(text + outofs, text + inofs, not_url_len);
1504 inofs += not_url_len;
1505 outofs += not_url_len;
1507 /* copy the new url */
1508 strncpy(text + outofs, url, short_url_len);
1509 inofs += long_url_len;
1510 outofs += short_url_len;
1516 /* copy the last block after the last match */
1518 int tail = inlen - inofs;
1520 strncpy(text + outofs, text + inofs, tail);
1527 (void)pcloseRWE(shrink_pid, shrink_pipe);
1530 dbg("after len=%u\n", outofs);
1534 int main(int argc, char *argv[], char *envp[])
1536 static const struct option options[] = {
1537 { "debug", 0, NULL, 'd' },
1538 { "verbose", 0, NULL, 'V' },
1539 { "account", 1, NULL, 'a' },
1540 { "password", 1, NULL, 'p' },
1541 { "host", 1, NULL, 'H' },
1542 { "proxy", 1, NULL, 'P' },
1543 { "action", 1, NULL, 'A' },
1544 { "user", 1, NULL, 'u' },
1545 { "group", 1, NULL, 'G' },
1546 { "logfile", 1, NULL, 'L' },
1547 { "shrink-urls", 0, NULL, 's' },
1548 { "help", 0, NULL, 'h' },
1549 { "bash", 0, NULL, 'b' },
1550 { "background", 0, NULL, 'B' },
1551 { "dry-run", 0, NULL, 'n' },
1552 { "page", 1, NULL, 'g' },
1553 { "column", 1, NULL, 'o' },
1554 { "version", 0, NULL, 'v' },
1555 { "config", 1, NULL, 'c' },
1556 { "replyto", 1, NULL, 'r' },
1557 { "retweet", 1, NULL, 'w' },
1561 struct session *session;
1564 static char password[80];
1573 session = session_alloc();
1575 fprintf(stderr, "no more memory...\n");
1579 /* get the current time so that we can log it later */
1581 session->time = strdup(ctime(&t));
1582 session->time[strlen(session->time)-1] = 0x00;
1584 find_config_file(session);
1586 /* Set environment variables first, before reading command line options
1587 * or config file values. */
1588 http_proxy = getenv("http_proxy");
1591 free(session->proxy);
1592 session->proxy = strdup(http_proxy);
1593 dbg("http_proxy = %s\n", session->proxy);
1596 bti_parse_configfile(session);
1599 option = getopt_long_only(argc, argv,
1600 "dp:P:H:a:A:u:c:hg:o:G:sr:nVvw:",
1609 session->verbose = 1;
1612 if (session->account)
1613 free(session->account);
1614 session->account = strdup(optarg);
1615 dbg("account = %s\n", session->account);
1618 page_nr = atoi(optarg);
1619 dbg("page = %d\n", page_nr);
1620 session->page = page_nr;
1623 session->column_output = atoi(optarg);
1624 dbg("column_output = %d\n", session->column_output);
1627 session->replyto = strdup(optarg);
1628 dbg("in_reply_to_status_id = %s\n", session->replyto);
1631 session->retweet = strdup(optarg);
1632 dbg("Retweet ID = %s\n", session->retweet);
1635 if (session->password)
1636 free(session->password);
1637 session->password = strdup(optarg);
1638 dbg("password = %s\n", session->password);
1642 free(session->proxy);
1643 session->proxy = strdup(optarg);
1644 dbg("proxy = %s\n", session->proxy);
1647 if (strcasecmp(optarg, "update") == 0)
1648 session->action = ACTION_UPDATE;
1649 else if (strcasecmp(optarg, "friends") == 0)
1650 session->action = ACTION_FRIENDS;
1651 else if (strcasecmp(optarg, "user") == 0)
1652 session->action = ACTION_USER;
1653 else if (strcasecmp(optarg, "replies") == 0)
1654 session->action = ACTION_REPLIES;
1655 else if (strcasecmp(optarg, "public") == 0)
1656 session->action = ACTION_PUBLIC;
1657 else if (strcasecmp(optarg, "group") == 0)
1658 session->action = ACTION_GROUP;
1659 else if (strcasecmp(optarg, "retweet") == 0)
1660 session->action = ACTION_RETWEET;
1661 else if (strcasecmp(optarg, "direct") == 0)
1662 session->action = ACTION_DIRECT;
1664 session->action = ACTION_UNKNOWN;
1665 dbg("action = %d\n", session->action);
1669 free(session->user);
1670 session->user = strdup(optarg);
1671 dbg("user = %s\n", session->user);
1676 free(session->group);
1677 session->group = strdup(optarg);
1678 dbg("group = %s\n", session->group);
1681 if (session->logfile)
1682 free(session->logfile);
1683 session->logfile = strdup(optarg);
1684 dbg("logfile = %s\n", session->logfile);
1687 session->shrink_urls = 1;
1690 if (session->hosturl)
1691 free(session->hosturl);
1692 if (session->hostname)
1693 free(session->hostname);
1694 if (strcasecmp(optarg, "twitter") == 0) {
1695 session->host = HOST_TWITTER;
1696 session->hosturl = strdup(twitter_host);
1697 session->hostname = strdup(twitter_name);
1699 session->host = HOST_CUSTOM;
1700 session->hosturl = strdup(optarg);
1701 session->hostname = strdup(optarg);
1703 dbg("host = %d\n", session->host);
1707 /* fall-through intended */
1709 session->background = 1;
1712 if (session->configfile)
1713 free(session->configfile);
1714 session->configfile = strdup(optarg);
1715 dbg("configfile = %s\n", session->configfile);
1716 if (stat(session->configfile, &s) == -1) {
1718 "Config file '%s' is not found.\n",
1719 session->configfile);
1724 * read the config file now. Yes, this could override
1725 * previously set options from the command line, but
1726 * the user asked for it...
1728 bti_parse_configfile(session);
1734 session->dry_run = 1;
1745 session_readline_init(session);
1747 * Show the version to make it easier to determine what
1753 if (session->host == HOST_TWITTER) {
1754 if (!session->consumer_key || !session->consumer_secret) {
1755 if (session->action == ACTION_USER ||
1756 session->action == ACTION_PUBLIC) {
1758 * Some actions may still work without
1764 "Twitter no longer supports HTTP basic authentication.\n"
1765 "Both consumer key, and consumer secret are required"
1766 " for bti in order to behave as an OAuth consumer.\n");
1770 if (session->action == ACTION_GROUP) {
1771 fprintf(stderr, "Groups only work in Identi.ca.\n");
1775 if (!session->consumer_key || !session->consumer_secret)
1776 session->no_oauth = 1;
1779 if (session->no_oauth) {
1780 if (!session->account) {
1781 fprintf(stdout, "Enter account for %s: ",
1783 session->account = session->readline(NULL);
1785 if (!session->password) {
1786 read_password(password, sizeof(password),
1788 session->password = strdup(password);
1790 } else if (!session->guest) {
1791 if (!session->access_token_key ||
1792 !session->access_token_secret) {
1793 request_access_token(session);
1798 if (session->action == ACTION_UNKNOWN) {
1799 fprintf(stderr, "Unknown action, valid actions are:\n"
1800 "'update', 'friends', 'public', 'replies', 'group', 'user' or 'direct'.\n");
1804 if (session->action == ACTION_GROUP && !session->group) {
1805 fprintf(stdout, "Enter group name: ");
1806 session->group = session->readline(NULL);
1809 if (session->action == ACTION_RETWEET) {
1810 if (!session->retweet) {
1813 fprintf(stdout, "Status ID to retweet: ");
1814 rtid = get_string_from_stdin();
1815 session->retweet = zalloc(strlen(rtid) + 10);
1816 sprintf(session->retweet, "%s", rtid);
1820 if (!session->retweet || strlen(session->retweet) == 0) {
1821 dbg("no retweet?\n");
1825 dbg("retweet ID = %s\n", session->retweet);
1828 if (session->action == ACTION_UPDATE || session->action == ACTION_DIRECT) {
1829 if (session->background || !session->interactive)
1830 tweet = get_string_from_stdin();
1832 tweet = session->readline("tweet: ");
1833 if (!tweet || strlen(tweet) == 0) {
1838 if (session->shrink_urls)
1839 tweet = shrink_urls(tweet);
1841 session->tweet = zalloc(strlen(tweet) + 10);
1843 sprintf(session->tweet, "%c %s",
1844 getuid() ? '$' : '#', tweet);
1846 sprintf(session->tweet, "%s", tweet);
1849 dbg("tweet = %s\n", session->tweet);
1852 if (session->page == 0)
1854 dbg("config file = %s\n", session->configfile);
1855 dbg("host = %d\n", session->host);
1856 dbg("action = %d\n", session->action);
1858 /* fork ourself so that the main shell can get on
1859 * with it's life as we try to connect and handle everything
1861 if (session->background) {
1864 dbg("child is %d\n", child);
1869 retval = send_request(session);
1870 if (retval && !session->background)
1871 fprintf(stderr, "operation failed\n");
1873 log_session(session, retval);
1875 session_readline_cleanup(session);
1876 session_free(session);