source: protocols/twitter/twitter.c @ 5f1e78d

Last change on this file since 5f1e78d was 5f1e78d, checked in by Wilmer van der Gaast <wilmer@…>, at 2011-08-17T21:58:21Z

To reduce ambiguity, allow using a # to specify Tweet ID's when doing retweets.
Patch from Jasper Spaans.

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