releasing version 012-1
[debian/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 "bti_version.h"
32
33
34 #define zalloc(size)    calloc(size, 1)
35
36 #define dbg(format, arg...)                                             \
37         do {                                                            \
38                 if (debug)                                              \
39                         printf("%s: " format , __func__ , ## arg );     \
40         } while (0)
41
42
43 static int debug = 0;
44
45 enum host {
46         HOST_TWITTER = 0,
47         HOST_IDENTICA = 1,
48 };
49
50 struct session {
51         char *password;
52         char *account;
53         char *tweet;
54         char *proxy;
55         char *time;
56         char *homedir;
57         char *logfile;
58         int bash;
59         enum host host;
60 };
61
62 struct bti_curl_buffer {
63         char *data;
64         int length;
65 };
66
67 static void display_help(void)
68 {
69         fprintf(stdout, "bti - send tweet to twitter\n");
70         fprintf(stdout, "Version: " BTI_VERSION "\n");
71         fprintf(stdout, "Usage:\n");
72         fprintf(stdout, "  bti [options]\n");
73         fprintf(stdout, "options are:\n");
74         fprintf(stdout, "  --account accountname\n");
75         fprintf(stdout, "  --password password\n");
76         fprintf(stdout, "  --proxy PROXY:PORT\n");
77         fprintf(stdout, "  --host HOST\n");
78         fprintf(stdout, "  --logfile logfile\n");
79         fprintf(stdout, "  --bash\n");
80         fprintf(stdout, "  --debug\n");
81         fprintf(stdout, "  --version\n");
82         fprintf(stdout, "  --help\n");
83 }
84
85 static void display_version(void)
86 {
87         fprintf(stdout, "bti - version %s\n", BTI_VERSION);
88 }
89
90 static char *get_string_from_stdin(void)
91 {
92         static char *string = (char *)NULL;
93         if (string) {
94                 free(string);
95                 string = (char *)NULL;
96         }
97
98         string = readline("tweet: ");
99
100         return string;
101 }
102
103 static struct session *session_alloc(void)
104 {
105         struct session *session;
106
107         session = zalloc(sizeof(*session));
108         if (!session)
109                 return NULL;
110         return session;
111 }
112
113 static void session_free(struct session *session)
114 {
115         if (!session)
116                 return;
117         free(session->password);
118         free(session->account);
119         free(session->tweet);
120         free(session->proxy);
121         free(session->time);
122         free(session->homedir);
123         free(session);
124 }
125
126 static struct bti_curl_buffer *bti_curl_buffer_alloc(void)
127 {
128         struct bti_curl_buffer *buffer;
129
130         buffer = zalloc(sizeof(*buffer));
131         if (!buffer)
132                 return NULL;
133
134         /* start out with a data buffer of 1 byte to
135          * make the buffer fill logic simpler */
136         buffer->data = zalloc(1);
137         if (!buffer->data) {
138                 free(buffer);
139                 return NULL;
140         }
141         buffer->length = 0;
142         return buffer;
143 }
144
145 static void bti_curl_buffer_free(struct bti_curl_buffer *buffer)
146 {
147         if (!buffer)
148                 return;
149         free(buffer->data);
150         free(buffer);
151 }
152
153 static const char *twitter_url = "https://twitter.com/statuses/update.xml";
154 static const char *identica_url = "http://identi.ca/api/statuses/update.xml";
155
156 static CURL *curl_init(void)
157 {
158         CURL *curl;
159
160         curl = curl_easy_init();
161         if (!curl) {
162                 fprintf(stderr, "Can not init CURL!\n");
163                 return NULL;
164         }
165         /* some ssl sanity checks on the connection we are making */
166         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
167         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
168         return curl;
169 }
170
171 size_t curl_callback(void *buffer, size_t size, size_t nmemb, void *userp)
172 {
173         struct bti_curl_buffer *curl_buf = userp;
174         size_t buffer_size = size * nmemb;
175         char *temp;
176
177         if ((!buffer) || (!buffer_size) || (!curl_buf))
178                 return -EINVAL;
179
180         /* add to the data we already have */
181         temp = zalloc(curl_buf->length + buffer_size + 1);
182         if (!temp)
183                 return -ENOMEM;
184
185         memcpy(temp, curl_buf->data, curl_buf->length);
186         free(curl_buf->data);
187         curl_buf->data = temp;
188         memcpy(&curl_buf->data[curl_buf->length], (char *)buffer, buffer_size);
189         curl_buf->length += buffer_size;
190
191         dbg("%s\n", curl_buf->data);
192
193         return buffer_size;
194 }
195
196 static int send_tweet(struct session *session)
197 {
198         char user_password[500];
199         char data[500];
200         struct bti_curl_buffer *curl_buf;
201         CURL *curl = NULL;
202         CURLcode res;
203         struct curl_httppost *formpost = NULL;
204         struct curl_httppost *lastptr = NULL;
205         struct curl_slist *slist = NULL;
206
207         if (!session)
208                 return -EINVAL;
209
210         curl_buf = bti_curl_buffer_alloc();
211         if (!curl_buf)
212                 return -ENOMEM;
213
214         snprintf(user_password, sizeof(user_password), "%s:%s",
215                  session->account, session->password);
216         snprintf(data, sizeof(data), "status=\"%s\"", session->tweet);
217
218         curl = curl_init();
219         if (!curl)
220                 return -EINVAL;
221
222         curl_formadd(&formpost, &lastptr,
223                      CURLFORM_COPYNAME, "status",
224                      CURLFORM_COPYCONTENTS, session->tweet,
225                      CURLFORM_END);
226
227         curl_formadd(&formpost, &lastptr,
228                      CURLFORM_COPYNAME, "source",
229                      CURLFORM_COPYCONTENTS, "bti",
230                      CURLFORM_END);
231
232         curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
233
234         switch (session->host) {
235         case HOST_TWITTER:
236                 curl_easy_setopt(curl, CURLOPT_URL, twitter_url);
237                 /*
238                  * twitter doesn't like the "Expect: 100-continue" header
239                  * anymore, so turn it off.
240                  */
241                 slist = curl_slist_append(slist, "Expect:");
242                 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist);
243                 break;
244         case HOST_IDENTICA:
245                 curl_easy_setopt(curl, CURLOPT_URL, identica_url);
246                 break;
247         }
248
249         if (session->proxy)
250                 curl_easy_setopt(curl, CURLOPT_PROXY, session->proxy);
251
252         if (debug)
253                 curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
254         curl_easy_setopt(curl, CURLOPT_USERPWD, user_password);
255
256         dbg("user_password = %s\n", user_password);
257         dbg("data = %s\n", data);
258         dbg("proxy = %s\n", session->proxy);
259
260         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_callback);
261         curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_buf);
262         res = curl_easy_perform(curl);
263         if (res && !session->bash) {
264                 fprintf(stderr, "error(%d) trying to send tweet\n", res);
265                 return -EINVAL;
266         }
267
268         curl_easy_cleanup(curl);
269         curl_formfree(formpost);
270         bti_curl_buffer_free(curl_buf);
271         return 0;
272 }
273
274 static void parse_configfile(struct session *session)
275 {
276         FILE *config_file;
277         char *line = NULL;
278         size_t len = 0;
279         char *account = NULL;
280         char *password = NULL;
281         char *host = NULL;
282         char *proxy = NULL;
283         char *logfile = NULL;
284         char *file;
285
286         /* config file is ~/.bti  */
287         file = alloca(strlen(session->homedir) + 7);
288
289         sprintf(file, "%s/.bti", session->homedir);
290
291         config_file = fopen(file, "r");
292
293         /* No error if file does not exist or is unreadable.  */
294         if (config_file == NULL)
295                 return;
296
297         do {
298                 ssize_t n = getline(&line, &len, config_file);
299                 if (n < 0)
300                         break;
301                 if (line[n - 1] == '\n')
302                         line[n - 1] = '\0';
303                 /* Parse file.  Format is the usual value pairs:
304                    account=name
305                    passwort=value
306                    # is a comment character
307                 */
308                 *strchrnul(line, '#') = '\0';
309                 char *c = line;
310                 while (isspace(*c))
311                         c++;
312                 /* Ignore blank lines.  */
313                 if (c[0] == '\0')
314                         continue;
315
316                 if (!strncasecmp(c, "account", 7) && (c[7] == '=')) {
317                         c += 8;
318                         if (c[0] != '\0')
319                                 account = strdup(c);
320                 } else if (!strncasecmp(c, "password", 8) &&
321                            (c[8] == '=')) {
322                         c += 9;
323                         if (c[0] != '\0')
324                                 password = strdup(c);
325                 } else if (!strncasecmp(c, "host", 4) &&
326                            (c[4] == '=')) {
327                         c += 5;
328                         if (c[0] != '\0')
329                                 host = strdup(c);
330                 } else if (!strncasecmp(c, "proxy", 5) &&
331                            (c[5] == '=')) {
332                         c += 6;
333                         if (c[0] != '\0')
334                                 proxy = strdup(c);
335                 } else if (!strncasecmp(c, "logfile", 7) &&
336                            (c[7] == '=')) {
337                         c += 8;
338                         if (c[0] != '\0')
339                                 logfile = strdup(c);
340                 }
341         } while (!feof(config_file));
342
343         if (password)
344                 session->password = password;
345         if (account)
346                 session->account = account;
347         if (host) {
348                 if (strcasecmp(host, "twitter") == 0)
349                         session->host = HOST_TWITTER;
350                 if (strcasecmp(host, "identica") == 0)
351                         session->host = HOST_IDENTICA;
352                 free(host);
353         }
354         if (proxy) {
355                 if (session->proxy)
356                         free(session->proxy);
357                 session->proxy = proxy;
358         }
359         if (logfile)
360                 session->logfile = logfile;
361
362         /* Free buffer and close file.  */
363         free(line);
364         fclose(config_file);
365 }
366
367 static void log_session(struct session *session, int retval)
368 {
369         FILE *log_file;
370         char *filename;
371         char *host;
372
373         /* Only log something if we have a log file set */
374         if (!session->logfile)
375                 return;
376
377         filename = alloca(strlen(session->homedir) +
378                           strlen(session->logfile) + 3);
379
380         sprintf(filename, "%s/%s", session->homedir, session->logfile);
381
382         log_file = fopen(filename, "a+");
383         if (log_file == NULL)
384                 return;
385         switch (session->host) {
386         case HOST_TWITTER:
387                 host = "twitter";
388                 break;
389         case HOST_IDENTICA:
390                 host = "identi.ca";
391                 break;
392         default:
393                 host = "unknown";
394                 break;
395         }
396
397         if (retval)
398                 fprintf(log_file, "%s: host=%s tweet failed\n", session->time, host);
399         else
400                 fprintf(log_file, "%s: host=%s tweet=%s\n", session->time, host, session->tweet);
401
402         fclose(log_file);
403 }
404
405 int main(int argc, char *argv[], char *envp[])
406 {
407         static const struct option options[] = {
408                 { "debug", 0, NULL, 'd' },
409                 { "account", 1, NULL, 'a' },
410                 { "password", 1, NULL, 'p' },
411                 { "host", 1, NULL, 'H' },
412                 { "proxy", 1, NULL, 'P' },
413                 { "logfile", 1, NULL, 'L' },
414                 { "help", 0, NULL, 'h' },
415                 { "bash", 0, NULL, 'b' },
416                 { "version", 0, NULL, 'v' },
417                 { }
418         };
419         struct session *session;
420         pid_t child;
421         char *tweet;
422         int retval = 0;
423         int option;
424         char *http_proxy;
425         time_t t;
426 #if 0
427         char *pwd = getenv("PWD");
428         char *dir;
429 #endif
430
431         rl_bind_key('\t', rl_insert);
432         session = session_alloc();
433         if (!session) {
434                 fprintf(stderr, "no more memory...\n");
435                 return -1;
436         }
437
438         /* get the current time so that we can log it later */
439         time(&t);
440         session->time = strdup(ctime(&t));
441         session->time[strlen(session->time)-1] = 0x00;
442
443         session->homedir = strdup(getenv("HOME"));
444
445         curl_global_init(CURL_GLOBAL_ALL);
446
447         /* Set environment variables first, before reading command line options
448          * or config file values. */
449         http_proxy = getenv("http_proxy");
450         if (http_proxy) {
451                 if (session->proxy)
452                         free(session->proxy);
453                 session->proxy = strdup(http_proxy);
454                 dbg("http_proxy = %s\n", session->proxy);
455         }
456
457         parse_configfile(session);
458
459         while (1) {
460                 option = getopt_long_only(argc, argv, "dqe:p:P:H:a:h",
461                                           options, NULL);
462                 if (option == -1)
463                         break;
464                 switch (option) {
465                 case 'd':
466                         debug = 1;
467                         break;
468                 case 'a':
469                         if (session->account)
470                                 free(session->account);
471                         session->account = strdup(optarg);
472                         dbg("account = %s\n", session->account);
473                         break;
474                 case 'p':
475                         if (session->password)
476                                 free(session->password);
477                         session->password = strdup(optarg);
478                         dbg("password = %s\n", session->password);
479                         break;
480                 case 'P':
481                         if (session->proxy)
482                                 free(session->proxy);
483                         session->proxy = strdup(optarg);
484                         dbg("proxy = %s\n", session->proxy);
485                         break;
486                 case 'L':
487                         if (session->logfile)
488                                 free(session->logfile);
489                         session->logfile = strdup(optarg);
490                         dbg("logfile = %s\n", session->logfile);
491                         break;
492                 case 'H':
493                         if (strcasecmp(optarg, "twitter") == 0)
494                                 session->host = HOST_TWITTER;
495                         if (strcasecmp(optarg, "identica") == 0)
496                                 session->host = HOST_IDENTICA;
497                         dbg("host = %d\n", session->host);
498                         break;
499                 case 'b':
500                         session->bash= 1;
501                         break;
502                 case 'h':
503                         display_help();
504                         goto exit;
505                 case 'v':
506                         display_version();
507                         goto exit;
508                 default:
509                         display_help();
510                         goto exit;
511                 }
512         }
513
514         if (!session->account) {
515                 fprintf(stdout, "Enter twitter account: ");
516                 session->account = get_string_from_stdin();
517         }
518
519         if (!session->password) {
520                 fprintf(stdout, "Enter twitter password: ");
521                 session->password = get_string_from_stdin();
522         }
523 #if 0
524         /* get the current working directory basename */
525         if (strcmp(pwd, home) == 0)
526                 dir = "~";
527         else {
528                 dir = strrchr(pwd, '/');
529                 if (dir)
530                         dir++;
531                 else
532                         dir = "?";
533         }
534 #endif
535         tweet = get_string_from_stdin();
536         if (!tweet || strlen(tweet) == 0) {
537                 dbg("no tweet?\n");
538                 return -1;
539         }
540
541 //      session->tweet = zalloc(strlen(tweet) + strlen(dir) + 10);
542         session->tweet = zalloc(strlen(tweet) + 10);
543
544         /* if --bash is specified, add the "PWD $ " to
545          * the start of the tweet. */
546         if (session->bash)
547 //              sprintf(session->tweet, "%s $ %s", dir, tweet);
548                 sprintf(session->tweet, "$ %s", tweet);
549         else
550                 sprintf(session->tweet, "%s", tweet);
551         free(tweet);
552
553         dbg("account = %s\n", session->account);
554         dbg("password = %s\n", session->password);
555         dbg("tweet = %s\n", session->tweet);
556         dbg("host = %d\n", session->host);
557
558         /* fork ourself so that the main shell can get on
559          * with it's life as we try to connect and handle everything
560          */
561         if (session->bash) {
562                 child = fork();
563                         if (child) {
564                         dbg("child is %d\n", child);
565                         exit(0);
566                 }
567         }
568
569         retval = send_tweet(session);
570         if (retval && !session->bash)
571                 fprintf(stderr, "tweet failed\n");
572
573         log_session(session, retval);
574 exit:
575         session_free(session);
576         return retval;;
577 }