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