source: protocols/twitter/twitter.c @ d717020

Last change on this file since d717020 was 3f808ca, checked in by Wilmer van der Gaast <wilmer@…>, at 2011-12-11T16:38:02Z

Support HTTP/1.1 redirect status codes and use HTTPS for OAuth setup. This
is required for identi.ca and really should be done for Twitter as well.
Twitter OAuth is still broken though, it seems to disagree about signatures.

  • Property mode set to 100644
File size: 17.7 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Simple module to facilitate twitter functionality.                       *
5*                                                                           *
6*  Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com>                 *
7*                                                                           *
8*  This library is free software; you can redistribute it and/or            *
9*  modify it under the terms of the GNU Lesser General Public               *
10*  License as published by the Free Software Foundation, version            *
11*  2.1.                                                                     *
12*                                                                           *
13*  This library is distributed in the hope that it will be useful,          *
14*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
15*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        *
16*  Lesser General Public License for more details.                          *
17*                                                                           *
18*  You should have received a copy of the GNU Lesser General Public License *
19*  along with this library; if not, write to the Free Software Foundation,  *
20*  Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA           *
21*                                                                           *
22****************************************************************************/
23
24#include "nogaim.h"
25#include "oauth.h"
26#include "twitter.h"
27#include "twitter_http.h"
28#include "twitter_lib.h"
29#include "url.h"
30
31#define twitter_msg( ic, fmt... ) \
32        do {                                            \
33                struct twitter_data *td = ic->proto_data;   \
34                if( td->timeline_gc )                       \
35                        imcb_chat_log( td->timeline_gc, fmt );  \
36                else                                        \
37                        imcb_log( ic, fmt );                    \
38        } while( 0 );
39
40GSList *twitter_connections = NULL;
41
42/**
43 * Main loop function
44 */
45gboolean twitter_main_loop(gpointer data, gint fd, b_input_condition cond)
46{
47        struct im_connection *ic = data;
48
49        // Check if we are still logged in...
50        if (!g_slist_find(twitter_connections, ic))
51                return 0;
52
53        // Do stuff..
54        twitter_get_timeline(ic, -1);
55
56        // If we are still logged in run this function again after timeout.
57        return (ic->flags & OPT_LOGGED_IN) == OPT_LOGGED_IN;
58}
59
60static void twitter_main_loop_start(struct im_connection *ic)
61{
62        struct twitter_data *td = ic->proto_data;
63
64        imcb_log(ic, "Getting initial statuses");
65
66        // Run this once. After this queue the main loop function.
67        twitter_main_loop(ic, -1, 0);
68
69        // Queue the main_loop
70        // Save the return value, so we can remove the timeout on logout.
71        td->main_loop_id =
72            b_timeout_add(set_getint(&ic->acc->set, "fetch_interval") * 1000, twitter_main_loop, ic);
73}
74
75static void twitter_oauth_start(struct im_connection *ic);
76
77void twitter_login_finish(struct im_connection *ic)
78{
79        struct twitter_data *td = ic->proto_data;
80
81        td->flags &= ~TWITTER_DOING_TIMELINE;
82
83        if (set_getbool(&ic->acc->set, "oauth") && !td->oauth_info)
84                twitter_oauth_start(ic);
85        else if (g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "one") != 0 &&
86                 !(td->flags & TWITTER_HAVE_FRIENDS)) {
87                imcb_log(ic, "Getting contact list");
88                twitter_get_friends_ids(ic, -1);
89                //twitter_get_statuses_friends(ic, -1);
90        } else
91                twitter_main_loop_start(ic);
92}
93
94static const struct oauth_service twitter_oauth = {
95        "https://api.twitter.com/oauth/request_token",
96        "https://api.twitter.com/oauth/access_token",
97        "https://api.twitter.com/oauth/authorize",
98        .consumer_key = "xsDNKJuNZYkZyMcu914uEA",
99        .consumer_secret = "FCxqcr0pXKzsF9ajmP57S3VQ8V6Drk4o2QYtqMcOszo",
100};
101
102static const struct oauth_service identica_oauth = {
103        "https://identi.ca/api/oauth/request_token",
104        "https://identi.ca/api/oauth/access_token",
105        "https://identi.ca/api/oauth/authorize",
106        .consumer_key = "e147ff789fcbd8a5a07963afbb43f9da",
107        .consumer_secret = "c596267f277457ec0ce1ab7bb788d828",
108};
109
110static gboolean twitter_oauth_callback(struct oauth_info *info);
111
112static const struct oauth_service *get_oauth_service(struct im_connection *ic)
113{
114        struct twitter_data *td = ic->proto_data;
115
116        if (strstr(td->url_host, "identi.ca"))
117                return &identica_oauth;
118        else
119                return &twitter_oauth;
120
121        /* Could add more services, or allow configuring your own base URL +
122           API keys. */
123}
124
125static void twitter_oauth_start(struct im_connection *ic)
126{
127        struct twitter_data *td = ic->proto_data;
128
129        imcb_log(ic, "Requesting OAuth request token");
130
131        td->oauth_info = oauth_request_token(get_oauth_service(ic), twitter_oauth_callback, ic);
132
133        /* We need help from the user to complete OAuth login, so don't time
134           out on this login. */
135        ic->flags |= OPT_SLOW_LOGIN;
136}
137
138static gboolean twitter_oauth_callback(struct oauth_info *info)
139{
140        struct im_connection *ic = info->data;
141        struct twitter_data *td;
142
143        if (!g_slist_find(twitter_connections, ic))
144                return FALSE;
145
146        td = ic->proto_data;
147        if (info->stage == OAUTH_REQUEST_TOKEN) {
148                char name[strlen(ic->acc->user) + 9], *msg;
149
150                if (info->request_token == NULL) {
151                        imcb_error(ic, "OAuth error: %s", twitter_parse_error(info->http));
152                        imc_logout(ic, TRUE);
153                        return FALSE;
154                }
155
156                sprintf(name, "%s_%s", td->prefix, ic->acc->user);
157                msg = g_strdup_printf("To finish OAuth authentication, please visit "
158                                      "%s and respond with the resulting PIN code.",
159                                      info->auth_url);
160                imcb_buddy_msg(ic, name, msg, 0, 0);
161                g_free(msg);
162        } else if (info->stage == OAUTH_ACCESS_TOKEN) {
163                if (info->token == NULL || info->token_secret == NULL) {
164                        imcb_error(ic, "OAuth error: %s", twitter_parse_error(info->http));
165                        imc_logout(ic, TRUE);
166                        return FALSE;
167                } else {
168                        const char *sn = oauth_params_get(&info->params, "screen_name");
169
170                        if (sn != NULL && ic->acc->prpl->handle_cmp(sn, ic->acc->user) != 0) {
171                                imcb_log(ic, "Warning: You logged in via OAuth as %s "
172                                         "instead of %s.", sn, ic->acc->user);
173                        }
174                        g_free(td->user);
175                        td->user = g_strdup(sn);
176                }
177
178                /* IM mods didn't do this so far and it's ugly but I should
179                   be able to get away with it... */
180                g_free(ic->acc->pass);
181                ic->acc->pass = oauth_to_string(info);
182
183                twitter_login_finish(ic);
184        }
185
186        return TRUE;
187}
188
189
190static char *set_eval_mode(set_t * set, char *value)
191{
192        if (g_strcasecmp(value, "one") == 0 ||
193            g_strcasecmp(value, "many") == 0 || g_strcasecmp(value, "chat") == 0)
194                return value;
195        else
196                return NULL;
197}
198
199static gboolean twitter_length_check(struct im_connection *ic, gchar * msg)
200{
201        int max = set_getint(&ic->acc->set, "message_length"), len;
202
203        if (max == 0 || (len = g_utf8_strlen(msg, -1)) <= max)
204                return TRUE;
205
206        imcb_error(ic, "Maximum message length exceeded: %d > %d", len, max);
207
208        return FALSE;
209}
210
211static void twitter_init(account_t * acc)
212{
213        set_t *s;
214        char *def_url;
215        char *def_oauth;
216
217        if (strcmp(acc->prpl->name, "twitter") == 0) {
218                def_url = TWITTER_API_URL;
219                def_oauth = "true";
220        } else {                /* if( strcmp( acc->prpl->name, "identica" ) == 0 ) */
221                def_url = IDENTICA_API_URL;
222                def_oauth = "false";
223        }
224
225        s = set_add(&acc->set, "auto_reply_timeout", "10800", set_eval_int, acc);
226
227        s = set_add(&acc->set, "base_url", def_url, NULL, acc);
228        s->flags |= ACC_SET_OFFLINE_ONLY;
229
230        s = set_add(&acc->set, "commands", "true", set_eval_bool, acc);
231
232        s = set_add(&acc->set, "fetch_interval", "60", set_eval_int, acc);
233        s->flags |= ACC_SET_OFFLINE_ONLY;
234
235        s = set_add(&acc->set, "fetch_mentions", "true", set_eval_bool, acc);
236
237        s = set_add(&acc->set, "message_length", "140", set_eval_int, acc);
238
239        s = set_add(&acc->set, "mode", "chat", set_eval_mode, acc);
240        s->flags |= ACC_SET_OFFLINE_ONLY;
241
242        s = set_add(&acc->set, "show_ids", "false", set_eval_bool, acc);
243        s->flags |= ACC_SET_OFFLINE_ONLY;
244
245        s = set_add(&acc->set, "show_old_mentions", "true", set_eval_bool, acc);
246
247        s = set_add(&acc->set, "oauth", def_oauth, set_eval_bool, acc);
248}
249
250/**
251 * Login method. Since the twitter API works with seperate HTTP request we
252 * only save the user and pass to the twitter_data object.
253 */
254static void twitter_login(account_t * acc)
255{
256        struct im_connection *ic = imcb_new(acc);
257        struct twitter_data *td;
258        char name[strlen(acc->user) + 9];
259        url_t url;
260        char *s;
261       
262        if (!url_set(&url, set_getstr(&ic->acc->set, "base_url")) ||
263            (url.proto != PROTO_HTTP && url.proto != PROTO_HTTPS)) {
264                imcb_error(ic, "Incorrect API base URL: %s", set_getstr(&ic->acc->set, "base_url"));
265                imc_logout(ic, FALSE);
266                return;
267        }
268
269        imcb_log(ic, "Connecting");
270
271        twitter_connections = g_slist_append(twitter_connections, ic);
272        td = g_new0(struct twitter_data, 1);
273        ic->proto_data = td;
274        td->user = g_strdup(acc->user);
275
276        td->url_ssl = url.proto == PROTO_HTTPS;
277        td->url_port = url.port;
278        td->url_host = g_strdup(url.host);
279        if (strcmp(url.file, "/") != 0)
280                td->url_path = g_strdup(url.file);
281        else {
282                td->url_path = g_strdup("");
283                if (g_str_has_suffix(url.host, "twitter.com"))
284                        /* May fire for people who turned on HTTPS. */
285                        imcb_error(ic, "Warning: Twitter requires a version number in API calls "
286                                       "now. Try resetting the base_url account setting.");
287        }
288       
289        /* Hacky string mangling: Turn identi.ca into identi.ca and api.twitter.com
290           into twitter, and try to be sensible if we get anything else. */
291        td->prefix = g_strdup(url.host);
292        if (g_str_has_suffix(td->prefix, ".com"))
293                td->prefix[strlen(url.host) - 4] = '\0';
294        if ((s = strrchr(td->prefix, '.')) && strlen(s) > 4) {
295                /* If we have at least 3 chars after the last dot, cut off the rest.
296                   (mostly a www/api prefix or sth) */
297                s = g_strdup(s + 1);
298                g_free(td->prefix);
299                td->prefix = s;
300        }
301       
302        if (strstr(acc->pass, "oauth_token="))
303                td->oauth_info = oauth_from_string(acc->pass, get_oauth_service(ic));
304
305        sprintf(name, "%s_%s", td->prefix, acc->user);
306        imcb_add_buddy(ic, name, NULL);
307        imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
308
309        if (set_getbool(&acc->set, "show_ids"))
310                td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH);
311
312        twitter_login_finish(ic);
313}
314
315/**
316 * Logout method. Just free the twitter_data.
317 */
318static void twitter_logout(struct im_connection *ic)
319{
320        struct twitter_data *td = ic->proto_data;
321
322        // Set the status to logged out.
323        ic->flags &= ~OPT_LOGGED_IN;
324
325        // Remove the main_loop function from the function queue.
326        b_event_remove(td->main_loop_id);
327
328        if (td->timeline_gc)
329                imcb_chat_free(td->timeline_gc);
330
331        if (td) {
332                oauth_info_free(td->oauth_info);
333                g_free(td->user);
334                g_free(td->prefix);
335                g_free(td->url_host);
336                g_free(td->url_path);
337                g_free(td->log);
338                g_free(td);
339        }
340
341        twitter_connections = g_slist_remove(twitter_connections, ic);
342}
343
344static void twitter_handle_command(struct im_connection *ic, char *message);
345
346/**
347 *
348 */
349static int twitter_buddy_msg(struct im_connection *ic, char *who, char *message, int away)
350{
351        struct twitter_data *td = ic->proto_data;
352        int plen = strlen(td->prefix);
353
354        if (g_strncasecmp(who, td->prefix, plen) == 0 && who[plen] == '_' &&
355            g_strcasecmp(who + plen + 1, ic->acc->user) == 0) {
356                if (set_getbool(&ic->acc->set, "oauth") &&
357                    td->oauth_info && td->oauth_info->token == NULL) {
358                        char pin[strlen(message) + 1], *s;
359
360                        strcpy(pin, message);
361                        for (s = pin + sizeof(pin) - 2; s > pin && isspace(*s); s--)
362                                *s = '\0';
363                        for (s = pin; *s && isspace(*s); s++) {
364                        }
365
366                        if (!oauth_access_token(s, td->oauth_info)) {
367                                imcb_error(ic, "OAuth error: %s",
368                                           "Failed to send access token request");
369                                imc_logout(ic, TRUE);
370                                return FALSE;
371                        }
372                } else
373                        twitter_handle_command(ic, message);
374        } else {
375                twitter_direct_messages_new(ic, who, message);
376        }
377        return (0);
378}
379
380/**
381 *
382 */
383static void twitter_set_my_name(struct im_connection *ic, char *info)
384{
385}
386
387static void twitter_get_info(struct im_connection *ic, char *who)
388{
389}
390
391static void twitter_add_buddy(struct im_connection *ic, char *who, char *group)
392{
393        twitter_friendships_create_destroy(ic, who, 1);
394}
395
396static void twitter_remove_buddy(struct im_connection *ic, char *who, char *group)
397{
398        twitter_friendships_create_destroy(ic, who, 0);
399}
400
401static void twitter_chat_msg(struct groupchat *c, char *message, int flags)
402{
403        if (c && message)
404                twitter_handle_command(c->ic, message);
405}
406
407static void twitter_chat_invite(struct groupchat *c, char *who, char *message)
408{
409}
410
411static void twitter_chat_leave(struct groupchat *c)
412{
413        struct twitter_data *td = c->ic->proto_data;
414
415        if (c != td->timeline_gc)
416                return;         /* WTF? */
417
418        /* If the user leaves the channel: Fine. Rejoin him/her once new
419           tweets come in. */
420        imcb_chat_free(td->timeline_gc);
421        td->timeline_gc = NULL;
422}
423
424static void twitter_keepalive(struct im_connection *ic)
425{
426}
427
428static void twitter_add_permit(struct im_connection *ic, char *who)
429{
430}
431
432static void twitter_rem_permit(struct im_connection *ic, char *who)
433{
434}
435
436static void twitter_add_deny(struct im_connection *ic, char *who)
437{
438}
439
440static void twitter_rem_deny(struct im_connection *ic, char *who)
441{
442}
443
444//static char *twitter_set_display_name( set_t *set, char *value )
445//{
446//      return value;
447//}
448
449static void twitter_buddy_data_add(struct bee_user *bu)
450{
451        bu->data = g_new0(struct twitter_user_data, 1);
452}
453
454static void twitter_buddy_data_free(struct bee_user *bu)
455{
456        g_free(bu->data);
457}
458
459static void twitter_handle_command(struct im_connection *ic, char *message)
460{
461        struct twitter_data *td = ic->proto_data;
462        char *cmds, **cmd, *new = NULL;
463        guint64 in_reply_to = 0;
464
465        cmds = g_strdup(message);
466        cmd = split_command_parts(cmds);
467
468        if (cmd[0] == NULL) {
469                g_free(cmds);
470                return;
471        } else if (!set_getbool(&ic->acc->set, "commands")) {
472                /* Not supporting commands. */
473        } else if (g_strcasecmp(cmd[0], "undo") == 0) {
474                guint64 id;
475
476                if (cmd[1] == NULL)
477                        twitter_status_destroy(ic, td->last_status_id);
478                else if (sscanf(cmd[1], "%" G_GUINT64_FORMAT, &id) == 1) {
479                        if (id < TWITTER_LOG_LENGTH && td->log)
480                                id = td->log[id].id;
481                       
482                        twitter_status_destroy(ic, id);
483                } else
484                        twitter_msg(ic, "Could not undo last action");
485
486                g_free(cmds);
487                return;
488        } else if (g_strcasecmp(cmd[0], "follow") == 0 && cmd[1]) {
489                twitter_add_buddy(ic, cmd[1], NULL);
490                g_free(cmds);
491                return;
492        } else if (g_strcasecmp(cmd[0], "unfollow") == 0 && cmd[1]) {
493                twitter_remove_buddy(ic, cmd[1], NULL);
494                g_free(cmds);
495                return;
496        } else if (g_strcasecmp(cmd[0], "rt") == 0 && cmd[1]) {
497                struct twitter_user_data *tud;
498                bee_user_t *bu;
499                guint64 id;
500
501                if (g_str_has_prefix(cmd[1], "#") &&
502                    sscanf(cmd[1] + 1, "%" G_GUINT64_FORMAT, &id) == 1) {
503                        if (id < TWITTER_LOG_LENGTH && td->log)
504                                id = td->log[id].id;
505                } else if ((bu = bee_user_by_handle(ic->bee, ic, cmd[1])) &&
506                    (tud = bu->data) && tud->last_id)
507                        id = tud->last_id;
508                else if (sscanf(cmd[1], "%" G_GUINT64_FORMAT, &id) == 1){
509                        if (id < TWITTER_LOG_LENGTH && td->log)
510                                id = td->log[id].id;
511                }
512
513                td->last_status_id = 0;
514                if (id)
515                        twitter_status_retweet(ic, id);
516                else
517                        twitter_msg(ic, "User `%s' does not exist or didn't "
518                                    "post any statuses recently", cmd[1]);
519
520                g_free(cmds);
521                return;
522        } else if (g_strcasecmp(cmd[0], "reply") == 0 && cmd[1] && cmd[2]) {
523                struct twitter_user_data *tud;
524                bee_user_t *bu = NULL;
525                guint64 id = 0;
526
527                if (g_str_has_prefix(cmd[1], "#") &&
528                    sscanf(cmd[1] + 1, "%" G_GUINT64_FORMAT, &id) == 1 &&
529                    (id < TWITTER_LOG_LENGTH) && td->log) {
530                        bu = td->log[id].bu;
531                        if (g_slist_find(ic->bee->users, bu))
532                                id = td->log[id].id;
533                        else
534                                bu = NULL;
535                } else if ((bu = bee_user_by_handle(ic->bee, ic, cmd[1])) &&
536                    (tud = bu->data) && tud->last_id) {
537                        id = tud->last_id;
538                } else if (sscanf(cmd[1], "%" G_GUINT64_FORMAT, &id) == 1 &&
539                           (id < TWITTER_LOG_LENGTH) && td->log) {
540                        bu = td->log[id].bu;
541                        if (g_slist_find(ic->bee->users, bu))
542                                id = td->log[id].id;
543                        else
544                                bu = NULL;
545                }
546
547                if (!id || !bu) {
548                        twitter_msg(ic, "User `%s' does not exist or didn't "
549                                    "post any statuses recently", cmd[1]);
550                        return;
551                }
552                message = new = g_strdup_printf("@%s %s", bu->handle, message + (cmd[2] - cmd[0]));
553                in_reply_to = id;
554        } else if (g_strcasecmp(cmd[0], "post") == 0) {
555                message += 5;
556        }
557
558        {
559                char *s;
560                bee_user_t *bu;
561
562                if (!twitter_length_check(ic, message)) {
563                        g_free(new);
564                        g_free(cmds);
565                        return;
566                }
567
568                s = cmd[0] + strlen(cmd[0]) - 1;
569                if (!new && s > cmd[0] && (*s == ':' || *s == ',')) {
570                        *s = '\0';
571
572                        if ((bu = bee_user_by_handle(ic->bee, ic, cmd[0]))) {
573                                struct twitter_user_data *tud = bu->data;
574
575                                new = g_strdup_printf("@%s %s", bu->handle,
576                                                      message + (s - cmd[0]) + 2);
577                                message = new;
578
579                                if (time(NULL) < tud->last_time +
580                                    set_getint(&ic->acc->set, "auto_reply_timeout"))
581                                        in_reply_to = tud->last_id;
582                        }
583                }
584
585                /* If the user runs undo between this request and its response
586                   this would delete the second-last Tweet. Prevent that. */
587                td->last_status_id = 0;
588                twitter_post_status(ic, message, in_reply_to);
589                g_free(new);
590        }
591        g_free(cmds);
592}
593
594void twitter_initmodule()
595{
596        struct prpl *ret = g_new0(struct prpl, 1);
597
598        ret->options = OPT_NOOTR;
599        ret->name = "twitter";
600        ret->login = twitter_login;
601        ret->init = twitter_init;
602        ret->logout = twitter_logout;
603        ret->buddy_msg = twitter_buddy_msg;
604        ret->get_info = twitter_get_info;
605        ret->set_my_name = twitter_set_my_name;
606        ret->add_buddy = twitter_add_buddy;
607        ret->remove_buddy = twitter_remove_buddy;
608        ret->chat_msg = twitter_chat_msg;
609        ret->chat_invite = twitter_chat_invite;
610        ret->chat_leave = twitter_chat_leave;
611        ret->keepalive = twitter_keepalive;
612        ret->add_permit = twitter_add_permit;
613        ret->rem_permit = twitter_rem_permit;
614        ret->add_deny = twitter_add_deny;
615        ret->rem_deny = twitter_rem_deny;
616        ret->buddy_data_add = twitter_buddy_data_add;
617        ret->buddy_data_free = twitter_buddy_data_free;
618        ret->handle_cmp = g_strcasecmp;
619
620        register_protocol(ret);
621
622        /* And an identi.ca variant: */
623        ret = g_memdup(ret, sizeof(struct prpl));
624        ret->name = "identica";
625        register_protocol(ret);
626}
Note: See TracBrowser for help on using the repository browser.