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