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/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[] = "http://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[] = "http://api.twitter.com/1.1";
264 const char identica_host[] = "https://identi.ca/api/statuses";
265 const char twitter_name[] = "twitter";
266 const char identica_name[] = "identi.ca";
268 static const char twitter_request_token_uri[] = "https://twitter.com/oauth/request_token";
269 static const char twitter_access_token_uri[] = "https://twitter.com/oauth/access_token";
270 static const char twitter_authorize_uri[] = "https://twitter.com/oauth/authorize?oauth_token=";
271 static const char identica_request_token_uri[] = "https://identi.ca/api/oauth/request_token?oauth_callback=oob";
272 static const char identica_access_token_uri[] = "https://identi.ca/api/oauth/access_token";
273 static const char identica_authorize_uri[] = "https://identi.ca/api/oauth/authorize?oauth_token=";
274 static const char custom_request_token_uri[] = "/../oauth/request_token?oauth_callback=oob";
275 static const char custom_access_token_uri[] = "/../oauth/access_token";
276 static const char custom_authorize_uri[] = "/../oauth/authorize?oauth_token=";
278 static const char user_uri[] = "/user_timeline.json";
279 static const char update_uri[] = "/update.json";
280 static const char public_uri[] = "/sample.json";
281 static const char friends_uri[] = "/home_timeline.json";
282 static const char mentions_uri[] = "/mentions_timeline.json";
283 static const char replies_uri[] = "/replies.xml";
284 static const char retweet_uri[] = "/retweet/";
285 static const char group_uri[] = "/../statusnet/groups/timeline/";
286 /*static const char direct_uri[] = "/direct_messages/new.xml";*/
287 static const char direct_uri[] = "/direct_messages/new.json";
289 static const char config_default[] = "/etc/bti";
290 static const char config_xdg_default[] = ".config/bti";
291 static const char config_user_default[] = ".bti";
294 static CURL *curl_init(void)
298 curl = curl_easy_init();
300 fprintf(stderr, "Can not init CURL!\n");
303 /* some ssl sanity checks on the connection we are making */
304 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
305 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
309 static void find_config_file(struct session *session)
317 * Get the home directory so we can try to find a config file.
318 * If we have no home dir set up, look in /etc/bti
320 home = getenv("HOME");
322 /* No home dir, so take the defaults and get out of here */
323 session->homedir = strdup("");
324 session->configfile = strdup(config_default);
328 /* We have a home dir, so this might be a user */
329 session->homedir = strdup(home);
330 homedir_size = strlen(session->homedir);
333 * Try to find a config file, we do so in this order:
334 * ~/.bti old-school config file
335 * ~/.config/bti new-school config file
337 file = zalloc(homedir_size + strlen(config_user_default) + 7);
338 sprintf(file, "%s/%s", home, config_user_default);
339 if (stat(file, &s) == 0) {
340 /* Found the config file at ~/.bti */
341 session->configfile = strdup(file);
347 file = zalloc(homedir_size + strlen(config_xdg_default) + 7);
348 sprintf(file, "%s/%s", home, config_xdg_default);
349 if (stat(file, &s) == 0) {
350 /* config file is at ~/.config/bti */
351 session->configfile = strdup(file);
356 /* No idea where the config file is, so punt */
358 session->configfile = strdup("");
361 /* The final place data is sent to the screen/pty/tty */
362 static void bti_output_line(struct session *session, xmlChar *user,
363 xmlChar *id, xmlChar *created, xmlChar *text)
365 if (session->verbose)
366 printf("[%*s] {%s} (%.16s) %s\n", -session->column_output, user,
369 printf("[%*s] %s\n", -session->column_output, user, text);
372 static void parse_statuses(struct session *session,
373 xmlDocPtr doc, xmlNodePtr current)
375 xmlChar *text = NULL;
376 xmlChar *user = NULL;
377 xmlChar *created = NULL;
381 current = current->xmlChildrenNode;
382 while (current != NULL) {
383 if (current->type == XML_ELEMENT_NODE) {
384 if (!xmlStrcmp(current->name, (const xmlChar *)"created_at"))
385 created = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
386 if (!xmlStrcmp(current->name, (const xmlChar *)"text"))
387 text = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
388 if (!xmlStrcmp(current->name, (const xmlChar *)"id"))
389 id = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
390 if (!xmlStrcmp(current->name, (const xmlChar *)"user")) {
391 userinfo = current->xmlChildrenNode;
392 while (userinfo != NULL) {
393 if ((!xmlStrcmp(userinfo->name, (const xmlChar *)"screen_name"))) {
396 user = xmlNodeListGetString(doc, userinfo->xmlChildrenNode, 1);
398 userinfo = userinfo->next;
402 if (user && text && created && id) {
403 bti_output_line(session, user, id,
415 current = current->next;
421 static void parse_timeline(char *document, struct session *session)
426 doc = xmlReadMemory(document, strlen(document), "timeline.xml",
427 NULL, XML_PARSE_NOERROR);
431 current = xmlDocGetRootElement(doc);
432 if (current == NULL) {
433 fprintf(stderr, "empty document\n");
438 if (xmlStrcmp(current->name, (const xmlChar *) "statuses")) {
439 fprintf(stderr, "unexpected document type\n");
444 current = current->xmlChildrenNode;
445 while (current != NULL) {
446 if ((!xmlStrcmp(current->name, (const xmlChar *)"status")))
447 parse_statuses(session, doc, current);
448 current = current->next;
456 /* avoids the c99 option */
457 #define json_object_object_foreach_alt(obj,key,val) \
459 struct json_object *val; \
460 struct lh_entry *entry; \
461 for (entry = json_object_get_object(obj)->head; \
462 ({ if(entry && !is_error(entry)) { \
463 key = (char*)entry->k; \
464 val = (struct json_object*)entry->v; \
466 entry = entry->next )
469 /* Forward Declaration */
470 static void json_parse(json_object * jobj, int nestlevel);
472 static void print_json_value(json_object *jobj, int nestlevel)
476 type = json_object_get_type(jobj);
478 case json_type_boolean:
480 printf("value: %s\n", json_object_get_boolean(jobj)? "true": "false");
482 case json_type_double:
484 printf("value: %lf\n", json_object_get_double(jobj));
488 printf("value: %d\n", json_object_get_int(jobj));
490 case json_type_string:
492 printf("value: %s\n", json_object_get_string(jobj));
499 #define MAXKEYSTACK 20
500 char *keystack[MAXKEYSTACK];
502 static void json_parse_array(json_object *jobj, char *key, int nestlevel)
507 /* Simply get the array */
508 json_object *jarray = jobj;
510 /* Get the array if it is a key value pair */
511 jarray = json_object_object_get(jobj, key);
514 /* Get the length of the array */
515 int arraylen = json_object_array_length(jarray);
517 printf("Array Length: %d\n",arraylen);
521 for (i = 0; i < arraylen; i++) {
524 for (j=0; j < nestlevel; ++j)
526 printf("element[%d]\n",i);
529 /* Get the array element at position i */
530 jvalue = json_object_array_get_idx(jarray, i);
531 type = json_object_get_type(jvalue);
532 if (type == json_type_array) {
533 json_parse_array(jvalue, NULL, nestlevel);
534 } else if (type != json_type_object) {
536 printf("value[%d]: ", i);
537 print_json_value(jvalue,nestlevel);
540 /* printf("obj: "); */
541 keystack[nestlevel%MAXKEYSTACK]="[]";
542 json_parse(jvalue,nestlevel);
553 struct session *store_session;
561 static void json_interpret(json_object *jobj, int nestlevel)
563 if (nestlevel == 3 &&
564 strcmp(keystack[1], "errors") == 0 &&
565 strcmp(keystack[2], "[]") == 0) {
566 if (strcmp(keystack[3], "message") == 0) {
567 if (json_object_get_type(jobj) == json_type_string)
568 results.message = (char *)json_object_get_string(jobj);
570 if (strcmp(keystack[3], "code") == 0) {
571 if (json_object_get_type(jobj) == json_type_int)
572 results.code = json_object_get_int(jobj);
576 if (nestlevel >= 2 &&
577 strcmp(keystack[1],"[]") == 0) {
578 if (strcmp(keystack[2], "created_at") == 0) {
580 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
581 tweetdetail.created_at = (char *)json_object_get_string(jobj);
583 if (strcmp(keystack[2], "text") == 0) {
585 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
586 tweetdetail.text = (char *)json_object_get_string(jobj);
588 if (strcmp(keystack[2], "id") == 0) {
590 printf("%s : %s\n", keystack[2], json_object_get_string(jobj));
591 tweetdetail.id = (char *)json_object_get_string(jobj);
593 if (nestlevel >= 3 &&
594 strcmp(keystack[2], "user") == 0) {
595 if (strcmp(keystack[3], "screen_name") == 0) {
597 printf("%s->%s : %s\n", keystack[2], keystack[3], json_object_get_string(jobj));
598 tweetdetail.screen_name=(char *)json_object_get_string(jobj);
599 bti_output_line(store_session,
600 (xmlChar *)tweetdetail.screen_name,
601 (xmlChar *)tweetdetail.id,
602 (xmlChar *)tweetdetail.created_at,
603 (xmlChar *)tweetdetail.text);
609 /* Parsing the json object */
610 static void json_parse(json_object * jobj, int nestlevel)
615 fprintf(stderr,"jobj null\n");
620 json_object_object_foreach_alt(jobj, key, val) {
621 /* work around pre10 */
623 type = json_object_get_type(val);
627 for (i = 0; i < nestlevel; ++i)
630 printf("key %-34s ", key);
632 for (i = 0; i < 8 - nestlevel; ++i)
635 case json_type_boolean:
636 case json_type_double:
638 case json_type_string:
639 if (debug) print_json_value(val,nestlevel);
640 if (debug) for (i=0; i<nestlevel+1; ++i) printf(" ");
641 if (debug) printf("(");
642 if (debug) for (i=1; i<nestlevel; ++i) { printf("%s->",keystack[i]); }
643 if (debug) printf("%s)\n",key);
644 keystack[nestlevel%MAXKEYSTACK] = key;
645 json_interpret(val,nestlevel);
647 case json_type_object:
648 if (debug) printf("json_type_object\n");
649 keystack[nestlevel%MAXKEYSTACK] = key;
650 json_parse(json_object_object_get(jobj, key), nestlevel);
652 case json_type_array:
653 if (debug) printf("json_type_array, ");
654 keystack[nestlevel%MAXKEYSTACK] = key;
655 json_parse_array(jobj, key, nestlevel);
658 if (debug) printf("null\n");
661 if (debug) printf("\n");
667 static int parse_response_json(char *document, struct session *session)
669 dbg("Got this json response:\n");
670 dbg("%s\n",document);
673 results.message=NULL;
674 json_object *jobj = json_tokener_parse(document);
676 /* make global for now */
677 store_session = session;
678 if (!is_error(jobj)) {
679 /* guards against a json pre 0.10 bug */
682 if (results.code && results.message != NULL) {
684 printf("Got an error code:\n code=%d\n message=%s\n",
685 results.code, results.message);
686 fprintf(stderr, "error condition detected: %d = %s\n",
687 results.code, results.message);
693 static void parse_timeline_json(char *document, struct session *session)
695 dbg("Got this json response:\n");
696 dbg("%s\n",document);
698 results.message = NULL;
699 json_object *jobj = json_tokener_parse(document);
701 /* make global for now */
702 store_session = session;
703 if (!is_error(jobj)) {
704 /* guards against a json pre 0.10 bug */
705 if (json_object_get_type(jobj)==json_type_array) {
706 json_parse_array(jobj, NULL, 0);
711 if (results.code && results.message != NULL) {
713 printf("Got an error code:\n code=%d\n message=%s\n",
714 results.code, results.message);
715 fprintf(stderr, "error condition detected: %d = %s\n",
716 results.code, results.message);
721 static int parse_response_xml(char *document, struct session *session)
726 doc = xmlReadMemory(document, strlen(document),
727 "response.xml", NULL,
732 current = xmlDocGetRootElement(doc);
733 if (current == NULL) {
734 fprintf(stderr, "empty document\n");
739 if (xmlStrcmp(current->name, (const xmlChar *) "status")) {
740 if (xmlStrcmp(current->name, (const xmlChar *) "direct_message")) {
741 if (xmlStrcmp(current->name, (const xmlChar *) "hash")
742 && xmlStrcmp(current->name, (const xmlChar *) "errors")) {
743 fprintf(stderr, "unexpected document type\n");
748 while (current != NULL) {
749 if (current->type == XML_ELEMENT_NODE)
750 if (!xmlStrcmp(current->name, (const xmlChar *)"error")) {
751 text = xmlNodeListGetString(doc, current->xmlChildrenNode, 1);
754 if (current->children)
755 current = current->children;
757 current = current->next;
761 fprintf(stderr, "error condition detected = %s\n", text);
764 fprintf(stderr, "unknown error condition\n");
778 static size_t curl_callback(void *buffer, size_t size, size_t nmemb,
781 struct bti_curl_buffer *curl_buf = userp;
782 size_t buffer_size = size * nmemb;
785 if ((!buffer) || (!buffer_size) || (!curl_buf))
788 /* add to the data we already have */
789 temp = zalloc(curl_buf->length + buffer_size + 1);
793 memcpy(temp, curl_buf->data, curl_buf->length);
794 free(curl_buf->data);
795 curl_buf->data = temp;
796 memcpy(&curl_buf->data[curl_buf->length], (char *)buffer, buffer_size);
797 curl_buf->length += buffer_size;
798 if (curl_buf->action)
799 parse_timeline(curl_buf->data, curl_buf->session);
801 dbg("%s\n", curl_buf->data);
806 static int parse_osp_reply(const char *reply, char **token, char **secret)
811 rc = oauth_split_url_parameters(reply, &rv);
812 qsort(rv, rc, sizeof(char *), oauth_cmpstringp);
813 if (rc == 2 || rc == 4) {
814 if (!strncmp(rv[0], "oauth_token=", 11) &&
815 !strncmp(rv[1], "oauth_token_secret=", 18)) {
817 *token = strdup(&(rv[0][12]));
819 *secret = strdup(&(rv[1][19]));
823 } else if (rc == 3) {
824 if (!strncmp(rv[1], "oauth_token=", 11) &&
825 !strncmp(rv[2], "oauth_token_secret=", 18)) {
827 *token = strdup(&(rv[1][12]));
829 *secret = strdup(&(rv[2][19]));
835 dbg("token: %s\n", *token);
836 dbg("secret: %s\n", *secret);
844 static int request_access_token(struct session *session)
846 char *post_params = NULL;
847 char *request_url = NULL;
850 char *at_secret = NULL;
851 char *verifier = NULL;
858 if (session->host == HOST_TWITTER)
859 request_url = oauth_sign_url2(
860 twitter_request_token_uri, NULL,
861 OA_HMAC, NULL, session->consumer_key,
862 session->consumer_secret, NULL, NULL);
863 else if (session->host == HOST_IDENTICA)
864 request_url = oauth_sign_url2(
865 identica_request_token_uri, NULL,
866 OA_HMAC, NULL, session->consumer_key,
867 session->consumer_secret, NULL, NULL);
869 sprintf(token_uri, "%s%s",
870 session->hosturl, custom_request_token_uri);
871 request_url = oauth_sign_url2(
873 OA_HMAC, NULL, session->consumer_key,
874 session->consumer_secret, NULL, NULL);
876 reply = oauth_http_get(request_url, post_params);
887 if (parse_osp_reply(reply, &at_key, &at_secret))
893 "Please open the following link in your browser, and "
894 "allow 'bti' to access your account. Then paste "
895 "back the provided PIN in here.\n");
896 if (session->host == HOST_TWITTER) {
897 fprintf(stdout, "%s%s\nPIN: ", twitter_authorize_uri, at_key);
898 verifier = session->readline(NULL);
899 sprintf(at_uri, "%s?oauth_verifier=%s",
900 twitter_access_token_uri, verifier);
901 } else if (session->host == HOST_IDENTICA) {
902 fprintf(stdout, "%s%s\nPIN: ", identica_authorize_uri, at_key);
903 verifier = session->readline(NULL);
904 sprintf(at_uri, "%s?oauth_verifier=%s",
905 identica_access_token_uri, verifier);
907 fprintf(stdout, "%s%s%s\nPIN: ",
908 session->hosturl, custom_authorize_uri, at_key);
909 verifier = session->readline(NULL);
910 sprintf(at_uri, "%s%s?oauth_verifier=%s",
911 session->hosturl, custom_access_token_uri, verifier);
913 request_url = oauth_sign_url2(at_uri, NULL, OA_HMAC, NULL,
914 session->consumer_key,
915 session->consumer_secret,
917 reply = oauth_http_get(request_url, post_params);
922 if (parse_osp_reply(reply, &at_key, &at_secret))
928 "Please put these two lines in your bti "
929 "configuration file (%s):\n"
930 "access_token_key=%s\n"
931 "access_token_secret=%s\n",
932 session->configfile, at_key, at_secret);
937 static int send_request(struct session *session)
939 const int endpoint_size = 2000;
940 char endpoint[endpoint_size];
941 char user_password[500];
943 struct bti_curl_buffer *curl_buf;
946 struct curl_httppost *formpost = NULL;
947 struct curl_httppost *lastptr = NULL;
948 struct curl_slist *slist = NULL;
949 char *req_url = NULL;
951 char *postarg = NULL;
952 char *escaped_tweet = NULL;
958 if (!session->hosturl)
959 session->hosturl = strdup(twitter_host);
961 if (session->no_oauth || session->guest) {
962 curl_buf = bti_curl_buffer_alloc(session->action);
965 curl_buf->session = session;
969 bti_curl_buffer_free(curl_buf);
973 if (!session->hosturl)
974 session->hosturl = strdup(twitter_host);
976 switch (session->action) {
978 snprintf(user_password, sizeof(user_password), "%s:%s",
979 session->account, session->password);
980 snprintf(data, sizeof(data), "status=\"%s\"",
982 curl_formadd(&formpost, &lastptr,
983 CURLFORM_COPYNAME, "status",
984 CURLFORM_COPYCONTENTS, session->tweet,
987 curl_formadd(&formpost, &lastptr,
988 CURLFORM_COPYNAME, "source",
989 CURLFORM_COPYCONTENTS, "bti",
992 if (session->replyto)
993 curl_formadd(&formpost, &lastptr,
995 "in_reply_to_status_id",
996 CURLFORM_COPYCONTENTS,
1000 curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
1001 slist = curl_slist_append(slist, "Expect:");
1002 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
1004 snprintf(endpoint, endpoint_size, "%s%s", session->hosturl, update_uri);
1005 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1006 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
1009 case ACTION_FRIENDS:
1010 snprintf(user_password, sizeof(user_password), "%s:%s",
1011 session->account, session->password);
1012 snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,
1013 friends_uri, session->page);
1014 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1015 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
1019 snprintf(endpoint, endpoint_size, "%s%s%s.xml?page=%d", session->hosturl,
1020 user_uri, session->user, session->page);
1021 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1024 case ACTION_REPLIES:
1025 snprintf(user_password, sizeof(user_password), "%s:%s",
1026 session->account, session->password);
1027 snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,
1028 replies_uri, session->page);
1029 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1030 curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
1034 /*snprintf(endpoint, endpoint_size, "%s%s?page=%d", session->hosturl,*/
1035 snprintf(endpoint, endpoint_size, "%s%s", twitter_host_stream,
1037 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1041 snprintf(endpoint, endpoint_size, "%s%s%s.xml?page=%d",
1042 session->hosturl, group_uri, session->group,
1044 curl_easy_setopt(curl, CURLOPT_URL, endpoint);
1048 /* NOT IMPLEMENTED - twitter requires authentication anyway */
1056 curl_easy_setopt(curl, CURLOPT_PROXY, session->proxy);
1059 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
1061 dbg("user_password = %s\n", user_password);
1062 dbg("data = %s\n", data);
1063 dbg("proxy = %s\n", session->proxy);
1065 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_callback);
1066 curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_buf);
1067 if (!session->dry_run) {
1068 res = curl_easy_perform(curl);
1069 if (!session->background) {
1075 "error(%d) trying to perform operation\n",
1077 curl_easy_cleanup(curl);
1078 if (session->action == ACTION_UPDATE)
1079 curl_formfree(formpost);
1080 bti_curl_buffer_free(curl_buf);
1084 doc = xmlReadMemory(curl_buf->data,
1086 "response.xml", NULL,
1089 curl_easy_cleanup(curl);
1090 if (session->action == ACTION_UPDATE)
1091 curl_formfree(formpost);
1092 bti_curl_buffer_free(curl_buf);
1096 current = xmlDocGetRootElement(doc);
1097 if (current == NULL) {
1098 fprintf(stderr, "empty document\n");
1100 curl_easy_cleanup(curl);
1101 if (session->action == ACTION_UPDATE)
1102 curl_formfree(formpost);
1103 bti_curl_buffer_free(curl_buf);
1107 if (xmlStrcmp(current->name, (const xmlChar *)"status")) {
1108 fprintf(stderr, "unexpected document type\n");
1110 curl_easy_cleanup(curl);
1111 if (session->action == ACTION_UPDATE)
1112 curl_formfree(formpost);
1113 bti_curl_buffer_free(curl_buf);
1121 curl_easy_cleanup(curl);
1122 if (session->action == ACTION_UPDATE)
1123 curl_formfree(formpost);
1124 bti_curl_buffer_free(curl_buf);
1126 switch (session->action) {
1128 /* dont test it here, let twitter return an error that we show */
1129 if (strlen_utf8(session->tweet) > 140 + 1000 ) {
1130 printf("E: tweet is too long!\n");
1134 /* TODO: add tweet crunching function. */
1135 escaped_tweet = oauth_url_escape(session->tweet);
1136 if (session->replyto) {
1138 "%s%s?status=%s&in_reply_to_status_id=%s",
1139 session->hosturl, update_uri,
1140 escaped_tweet, session->replyto);
1142 sprintf(endpoint, "%s%s?status=%s",
1143 session->hosturl, update_uri,
1150 sprintf(endpoint, "%s%s?screen_name=%s&page=%d",
1151 session->hosturl, user_uri, session->user,
1154 case ACTION_REPLIES:
1155 sprintf(endpoint, "%s%s?page=%d", session->hosturl,
1156 mentions_uri, session->page);
1159 sprintf(endpoint, "%s%s", twitter_host_stream,
1163 sprintf(endpoint, "%s%s%s.xml?page=%d",
1164 session->hosturl, group_uri, session->group,
1167 case ACTION_FRIENDS:
1168 sprintf(endpoint, "%s%s?page=%d", session->hosturl,
1169 friends_uri, session->page);
1171 case ACTION_RETWEET:
1172 sprintf(endpoint, "%s%s%s.xml", session->hosturl,
1173 retweet_uri, session->retweet);
1177 escaped_tweet = oauth_url_escape(session->tweet);
1178 sprintf(endpoint, "%s%s?user=%s&text=%s", twitter_host_simple,
1179 direct_uri, session->user, escaped_tweet);
1186 dbg("%s\n", endpoint);
1187 if (!session->dry_run) {
1189 req_url = oauth_sign_url2(endpoint, &postarg, OA_HMAC,
1190 NULL, session->consumer_key,
1191 session->consumer_secret,
1192 session->access_token_key,
1193 session->access_token_secret);
1194 reply = oauth_http_post(req_url, postarg);
1196 req_url = oauth_sign_url2(endpoint, NULL, OA_HMAC, NULL,
1197 session->consumer_key,
1198 session->consumer_secret,
1199 session->access_token_key,
1200 session->access_token_secret);
1201 reply = oauth_http_get(req_url, postarg);
1204 dbg("req_url:%s\n", req_url);
1205 dbg("reply:%s\n", reply);
1211 fprintf(stderr, "Error retrieving from URL (%s)\n", endpoint);
1215 if (!session->dry_run) {
1216 if ((session->action != ACTION_UPDATE) &&
1217 (session->action != ACTION_RETWEET) &&
1218 (session->action != ACTION_DIRECT))
1219 parse_timeline_json(reply, session);
1221 if ((session->action == ACTION_UPDATE) ||
1222 (session->action == ACTION_DIRECT))
1223 /*return parse_response_xml(reply, session);*/
1224 return parse_response_json(reply, session);
1233 static void log_session(struct session *session, int retval)
1238 /* Only log something if we have a log file set */
1239 if (!session->logfile)
1242 filename = alloca(strlen(session->homedir) +
1243 strlen(session->logfile) + 3);
1245 sprintf(filename, "%s/%s", session->homedir, session->logfile);
1247 log_file = fopen(filename, "a+");
1248 if (log_file == NULL)
1251 switch (session->action) {
1254 fprintf(log_file, "%s: host=%s tweet failed\n",
1255 session->time, session->hostname);
1257 fprintf(log_file, "%s: host=%s tweet=%s\n",
1258 session->time, session->hostname,
1261 case ACTION_FRIENDS:
1262 fprintf(log_file, "%s: host=%s retrieving friends timeline\n",
1263 session->time, session->hostname);
1266 fprintf(log_file, "%s: host=%s retrieving %s's timeline\n",
1267 session->time, session->hostname, session->user);
1269 case ACTION_REPLIES:
1270 fprintf(log_file, "%s: host=%s retrieving replies\n",
1271 session->time, session->hostname);
1274 fprintf(log_file, "%s: host=%s retrieving public timeline\n",
1275 session->time, session->hostname);
1278 fprintf(log_file, "%s: host=%s retrieving group timeline\n",
1279 session->time, session->hostname);
1283 fprintf(log_file, "%s: host=%s tweet failed\n",
1284 session->time, session->hostname);
1286 fprintf(log_file, "%s: host=%s tweet=%s\n",
1287 session->time, session->hostname,
1297 static char *get_string_from_stdin(void)
1302 string = zalloc(1000);
1306 if (!fgets(string, 999, stdin)) {
1310 temp = strchr(string, '\n');
1316 static void read_password(char *buf, size_t len, char *host)
1325 tp.c_lflag &= (~ECHO);
1326 tcsetattr(0, TCSANOW, &tp);
1328 fprintf(stdout, "Enter password for %s: ", host);
1333 * I'd like to do something with the return value here, but really,
1336 (void)scanf("%79s", pwd);
1339 fprintf(stdout, "\n");
1341 tcsetattr(0, TCSANOW, &old);
1343 strncpy(buf, pwd, len);
1347 static int find_urls(const char *tweet, int **pranges)
1350 * magic obtained from
1351 * http://www.geekpedia.com/KB65_How-to-validate-an-URL-using-RegEx-in-Csharp.html
1353 static const char *re_magic =
1354 "(([a-zA-Z][0-9a-zA-Z+\\-\\.]*:)/{1,3}"
1355 "[0-9a-zA-Z;/~?:@&=+$\\.\\-_'()%]+)"
1356 "(#[0-9a-zA-Z;/?:@&=+$\\.\\-_!~*'()%]+)?";
1360 int ovector[10] = {0,};
1361 const size_t ovsize = sizeof(ovector)/sizeof(*ovector);
1362 int startoffset, tweetlen;
1366 int *ranges = malloc(sizeof(int) * rbound);
1368 re = pcre_compile(re_magic,
1369 PCRE_NO_AUTO_CAPTURE,
1370 &errptr, &erroffset, NULL);
1372 fprintf(stderr, "pcre_compile @%u: %s\n", erroffset, errptr);
1376 tweetlen = strlen(tweet);
1377 for (startoffset = 0; startoffset < tweetlen; ) {
1379 rc = pcre_exec(re, NULL, tweet, strlen(tweet), startoffset, 0,
1381 if (rc == PCRE_ERROR_NOMATCH)
1385 fprintf(stderr, "pcre_exec @%u: %s\n",
1390 for (i = 0; i < rc; i += 2) {
1391 if ((rcount+2) == rbound) {
1393 ranges = realloc(ranges, sizeof(int) * rbound);
1396 ranges[rcount++] = ovector[i];
1397 ranges[rcount++] = ovector[i+1];
1400 startoffset = ovector[1];
1410 * bidirectional popen() call
1412 * @param rwepipe - int array of size three
1413 * @param exe - program to run
1414 * @param argv - argument list
1415 * @return pid or -1 on error
1417 * The caller passes in an array of three integers (rwepipe), on successful
1418 * execution it can then write to element 0 (stdin of exe), and read from
1419 * element 1 (stdout) and 2 (stderr).
1421 static int popenRWE(int *rwepipe, const char *exe, const char *const argv[])
1448 rwepipe[1] = out[0];
1449 rwepipe[2] = err[0];
1451 } else if (pid == 0) {
1463 execvp(exe, (char **)argv);
1483 static int pcloseRWE(int pid, int *rwepipe)
1489 (void)waitpid(pid, &status, 0);
1493 static char *shrink_one_url(int *rwepipe, char *big)
1495 int biglen = strlen(big);
1500 rc = dprintf(rwepipe[0], "%s\n", big);
1504 smalllen = biglen + 128;
1505 small = malloc(smalllen);
1509 rc = read(rwepipe[1], small, smalllen);
1510 if (rc < 0 || rc > biglen)
1511 goto error_free_small;
1513 if (strncmp(small, "http://", 7))
1514 goto error_free_small;
1517 while (smalllen && isspace(small[smalllen-1]))
1518 small[--smalllen] = 0;
1528 static char *shrink_urls(char *text)
1535 const char *const shrink_args[] = {
1541 int inlen = strlen(text);
1543 dbg("before len=%u\n", inlen);
1545 shrink_pid = popenRWE(shrink_pipe, shrink_args[0], shrink_args);
1549 rcount = find_urls(text, &ranges);
1553 for (i = 0; i < rcount; i += 2) {
1554 int url_start = ranges[i];
1555 int url_end = ranges[i+1];
1556 int long_url_len = url_end - url_start;
1557 char *url = strndup(text + url_start, long_url_len);
1559 int not_url_len = url_start - inofs;
1561 dbg("long url[%u]: %s\n", long_url_len, url);
1562 url = shrink_one_url(shrink_pipe, url);
1563 short_url_len = url ? strlen(url) : 0;
1564 dbg("short url[%u]: %s\n", short_url_len, url);
1566 if (!url || short_url_len >= long_url_len) {
1567 /* The short url ended up being too long
1570 strncpy(text + outofs, text + inofs,
1571 not_url_len + long_url_len);
1573 inofs += not_url_len + long_url_len;
1574 outofs += not_url_len + long_url_len;
1577 /* copy the unmodified block */
1578 strncpy(text + outofs, text + inofs, not_url_len);
1579 inofs += not_url_len;
1580 outofs += not_url_len;
1582 /* copy the new url */
1583 strncpy(text + outofs, url, short_url_len);
1584 inofs += long_url_len;
1585 outofs += short_url_len;
1591 /* copy the last block after the last match */
1593 int tail = inlen - inofs;
1595 strncpy(text + outofs, text + inofs, tail);
1602 (void)pcloseRWE(shrink_pid, shrink_pipe);
1605 dbg("after len=%u\n", outofs);
1609 int main(int argc, char *argv[], char *envp[])
1611 static const struct option options[] = {
1612 { "debug", 0, NULL, 'd' },
1613 { "verbose", 0, NULL, 'V' },
1614 { "account", 1, NULL, 'a' },
1615 { "password", 1, NULL, 'p' },
1616 { "host", 1, NULL, 'H' },
1617 { "proxy", 1, NULL, 'P' },
1618 { "action", 1, NULL, 'A' },
1619 { "user", 1, NULL, 'u' },
1620 { "group", 1, NULL, 'G' },
1621 { "logfile", 1, NULL, 'L' },
1622 { "shrink-urls", 0, NULL, 's' },
1623 { "help", 0, NULL, 'h' },
1624 { "bash", 0, NULL, 'b' },
1625 { "background", 0, NULL, 'B' },
1626 { "dry-run", 0, NULL, 'n' },
1627 { "page", 1, NULL, 'g' },
1628 { "column", 1, NULL, 'o' },
1629 { "version", 0, NULL, 'v' },
1630 { "config", 1, NULL, 'c' },
1631 { "replyto", 1, NULL, 'r' },
1632 { "retweet", 1, NULL, 'w' },
1636 struct session *session;
1639 static char password[80];
1648 session = session_alloc();
1650 fprintf(stderr, "no more memory...\n");
1654 /* get the current time so that we can log it later */
1656 session->time = strdup(ctime(&t));
1657 session->time[strlen(session->time)-1] = 0x00;
1659 find_config_file(session);
1661 /* Set environment variables first, before reading command line options
1662 * or config file values. */
1663 http_proxy = getenv("http_proxy");
1666 free(session->proxy);
1667 session->proxy = strdup(http_proxy);
1668 dbg("http_proxy = %s\n", session->proxy);
1671 bti_parse_configfile(session);
1674 option = getopt_long_only(argc, argv,
1675 "dp:P:H:a:A:u:c:hg:o:G:sr:nVvw:",
1684 session->verbose = 1;
1687 if (session->account)
1688 free(session->account);
1689 session->account = strdup(optarg);
1690 dbg("account = %s\n", session->account);
1693 page_nr = atoi(optarg);
1694 dbg("page = %d\n", page_nr);
1695 session->page = page_nr;
1698 session->column_output = atoi(optarg);
1699 dbg("column_output = %d\n", session->column_output);
1702 session->replyto = strdup(optarg);
1703 dbg("in_reply_to_status_id = %s\n", session->replyto);
1706 session->retweet = strdup(optarg);
1707 dbg("Retweet ID = %s\n", session->retweet);
1710 if (session->password)
1711 free(session->password);
1712 session->password = strdup(optarg);
1713 dbg("password = %s\n", session->password);
1717 free(session->proxy);
1718 session->proxy = strdup(optarg);
1719 dbg("proxy = %s\n", session->proxy);
1722 if (strcasecmp(optarg, "update") == 0)
1723 session->action = ACTION_UPDATE;
1724 else if (strcasecmp(optarg, "friends") == 0)
1725 session->action = ACTION_FRIENDS;
1726 else if (strcasecmp(optarg, "user") == 0)
1727 session->action = ACTION_USER;
1728 else if (strcasecmp(optarg, "replies") == 0)
1729 session->action = ACTION_REPLIES;
1730 else if (strcasecmp(optarg, "public") == 0)
1731 session->action = ACTION_PUBLIC;
1732 else if (strcasecmp(optarg, "group") == 0)
1733 session->action = ACTION_GROUP;
1734 else if (strcasecmp(optarg, "retweet") == 0)
1735 session->action = ACTION_RETWEET;
1736 else if (strcasecmp(optarg, "direct") == 0)
1737 session->action = ACTION_DIRECT;
1739 session->action = ACTION_UNKNOWN;
1740 dbg("action = %d\n", session->action);
1744 free(session->user);
1745 session->user = strdup(optarg);
1746 dbg("user = %s\n", session->user);
1751 free(session->group);
1752 session->group = strdup(optarg);
1753 dbg("group = %s\n", session->group);
1756 if (session->logfile)
1757 free(session->logfile);
1758 session->logfile = strdup(optarg);
1759 dbg("logfile = %s\n", session->logfile);
1762 session->shrink_urls = 1;
1765 if (session->hosturl)
1766 free(session->hosturl);
1767 if (session->hostname)
1768 free(session->hostname);
1769 if (strcasecmp(optarg, "twitter") == 0) {
1770 session->host = HOST_TWITTER;
1771 session->hosturl = strdup(twitter_host);
1772 session->hostname = strdup(twitter_name);
1773 } else if (strcasecmp(optarg, "identica") == 0) {
1774 session->host = HOST_IDENTICA;
1775 session->hosturl = strdup(identica_host);
1776 session->hostname = strdup(identica_name);
1778 session->host = HOST_CUSTOM;
1779 session->hosturl = strdup(optarg);
1780 session->hostname = strdup(optarg);
1782 dbg("host = %d\n", session->host);
1786 /* fall-through intended */
1788 session->background = 1;
1791 if (session->configfile)
1792 free(session->configfile);
1793 session->configfile = strdup(optarg);
1794 dbg("configfile = %s\n", session->configfile);
1795 if (stat(session->configfile, &s) == -1) {
1797 "Config file '%s' is not found.\n",
1798 session->configfile);
1803 * read the config file now. Yes, this could override
1804 * previously set options from the command line, but
1805 * the user asked for it...
1807 bti_parse_configfile(session);
1813 session->dry_run = 1;
1824 session_readline_init(session);
1826 * Show the version to make it easier to determine what
1832 if (session->host == HOST_TWITTER) {
1833 if (!session->consumer_key || !session->consumer_secret) {
1834 if (session->action == ACTION_USER ||
1835 session->action == ACTION_PUBLIC) {
1837 * Some actions may still work without
1843 "Twitter no longer supports HTTP basic authentication.\n"
1844 "Both consumer key, and consumer secret are required"
1845 " for bti in order to behave as an OAuth consumer.\n");
1849 if (session->action == ACTION_GROUP) {
1850 fprintf(stderr, "Groups only work in Identi.ca.\n");
1854 if (!session->consumer_key || !session->consumer_secret)
1855 session->no_oauth = 1;
1858 if (session->no_oauth) {
1859 if (!session->account) {
1860 fprintf(stdout, "Enter account for %s: ",
1862 session->account = session->readline(NULL);
1864 if (!session->password) {
1865 read_password(password, sizeof(password),
1867 session->password = strdup(password);
1869 } else if (!session->guest) {
1870 if (!session->access_token_key ||
1871 !session->access_token_secret) {
1872 request_access_token(session);
1877 if (session->action == ACTION_UNKNOWN) {
1878 fprintf(stderr, "Unknown action, valid actions are:\n"
1879 "'update', 'friends', 'public', 'replies', 'group', 'user' or 'direct'.\n");
1883 if (session->action == ACTION_GROUP && !session->group) {
1884 fprintf(stdout, "Enter group name: ");
1885 session->group = session->readline(NULL);
1888 if (session->action == ACTION_RETWEET) {
1889 if (!session->retweet) {
1892 fprintf(stdout, "Status ID to retweet: ");
1893 rtid = get_string_from_stdin();
1894 session->retweet = zalloc(strlen(rtid) + 10);
1895 sprintf(session->retweet, "%s", rtid);
1899 if (!session->retweet || strlen(session->retweet) == 0) {
1900 dbg("no retweet?\n");
1904 dbg("retweet ID = %s\n", session->retweet);
1907 if (session->action == ACTION_UPDATE || session->action == ACTION_DIRECT) {
1908 if (session->background || !session->interactive)
1909 tweet = get_string_from_stdin();
1911 tweet = session->readline("tweet: ");
1912 if (!tweet || strlen(tweet) == 0) {
1917 if (session->shrink_urls)
1918 tweet = shrink_urls(tweet);
1920 session->tweet = zalloc(strlen(tweet) + 10);
1922 sprintf(session->tweet, "%c %s",
1923 getuid() ? '$' : '#', tweet);
1925 sprintf(session->tweet, "%s", tweet);
1928 dbg("tweet = %s\n", session->tweet);
1931 if (session->page == 0)
1933 dbg("config file = %s\n", session->configfile);
1934 dbg("host = %d\n", session->host);
1935 dbg("action = %d\n", session->action);
1937 /* fork ourself so that the main shell can get on
1938 * with it's life as we try to connect and handle everything
1940 if (session->background) {
1943 dbg("child is %d\n", child);
1948 retval = send_request(session);
1949 if (retval && !session->background)
1950 fprintf(stderr, "operation failed\n");
1952 log_session(session, retval);
1954 session_readline_cleanup(session);
1955 session_free(session);