source: protocols/twitter/twitter_lib.c @ d6aa6dd

Last change on this file since d6aa6dd was d6aa6dd, checked in by Wilmer van der Gaast <wilmer@…>, at 2010-06-24T00:43:15Z

Load the whole Twitter contact list at login time if mode=chat/many,
instead of adding contacts as they post tweets. Also in mode=chat, populate
the channel *before* adding the user to it, avoiding a flood of joins.

  • Property mode set to 100644
File size: 19.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/* For strptime(): */
25#define _XOPEN_SOURCE
26
27#include "twitter_http.h"
28#include "twitter.h"
29#include "bitlbee.h"
30#include "url.h"
31#include "misc.h"
32#include "base64.h"
33#include "xmltree.h"
34#include "twitter_lib.h"
35#include <ctype.h>
36#include <errno.h>
37
38#define TXL_STATUS 1
39#define TXL_USER 2
40#define TXL_ID 3
41
42struct twitter_xml_list {
43        int type;
44        int next_cursor;
45        GSList *list;
46        gpointer data;
47};
48
49struct twitter_xml_user {
50        char *name;
51        char *screen_name;
52};
53
54struct twitter_xml_status {
55        time_t created_at;
56        char *text;
57        struct twitter_xml_user *user;
58        guint64 id;
59};
60
61static void twitter_groupchat_init(struct im_connection *ic);
62
63/**
64 * Frees a twitter_xml_user struct.
65 */
66static void txu_free(struct twitter_xml_user *txu)
67{
68        g_free(txu->name);
69        g_free(txu->screen_name);
70        g_free(txu);
71}
72
73
74/**
75 * Frees a twitter_xml_status struct.
76 */
77static void txs_free(struct twitter_xml_status *txs)
78{
79        g_free(txs->text);
80        txu_free(txs->user);
81        g_free(txs);
82}
83
84/**
85 * Free a twitter_xml_list struct.
86 * type is the type of list the struct holds.
87 */
88static void txl_free(struct twitter_xml_list *txl)
89{
90        GSList *l;
91        for ( l = txl->list; l ; l = g_slist_next(l) )
92                if (txl->type == TXL_STATUS)
93                        txs_free((struct twitter_xml_status *)l->data);
94                else if (txl->type == TXL_ID)
95                        g_free(l->data);
96        g_slist_free(txl->list);
97}
98
99/**
100 * Add a buddy if it is not allready added, set the status to logged in.
101 */
102static void twitter_add_buddy(struct im_connection *ic, char *name, const char *fullname)
103{
104        struct twitter_data *td = ic->proto_data;
105
106        // Check if the buddy is allready in the buddy list.
107        if (!imcb_find_buddy( ic, name ))
108        {
109                char *mode = set_getstr(&ic->acc->set, "mode");
110               
111                // The buddy is not in the list, add the buddy and set the status to logged in.
112                imcb_add_buddy( ic, name, NULL );
113                imcb_rename_buddy( ic, name, fullname );
114                if (g_strcasecmp(mode, "chat") == 0)
115                        imcb_chat_add_buddy( td->home_timeline_gc, name );
116                else if (g_strcasecmp(mode, "many") == 0)
117                        imcb_buddy_status( ic, name, OPT_LOGGED_IN, NULL, NULL );
118        }
119}
120
121/* Warning: May return a malloc()ed value, which will be free()d on the next
122   call. Only for short-term use. */
123static char *twitter_parse_error(struct http_request *req)
124{
125        static char *ret = NULL;
126        struct xt_parser *xp = NULL;
127        struct xt_node *node;
128       
129        g_free(ret);
130        ret = NULL;
131       
132        if (req->body_size > 0)
133        {
134                xp = xt_new(NULL, NULL);
135                xt_feed(xp, req->reply_body, req->body_size);
136               
137                if ((node = xt_find_node(xp->root, "hash")) &&
138                    (node = xt_find_node(node->children, "error")) &&
139                    node->text_len > 0)
140                {
141                        ret = g_strdup_printf("%s (%s)", req->status_string, node->text);
142                        xt_free(xp);
143                        return ret;
144                }
145               
146                xt_free(xp);
147        }
148       
149        return req->status_string;
150}
151
152static void twitter_http_get_friends_ids(struct http_request *req);
153
154/**
155 * Get the friends ids.
156 */
157void twitter_get_friends_ids(struct im_connection *ic, int next_cursor)
158{
159        // Primitive, but hey! It works...     
160        char* args[2];
161        args[0] = "cursor";
162        args[1] = g_strdup_printf ("%d", next_cursor);
163        twitter_http(ic, TWITTER_FRIENDS_IDS_URL, twitter_http_get_friends_ids, ic, 0, args, 2);
164
165        g_free(args[1]);
166}
167
168/**
169 * Function to help fill a list.
170 */
171static xt_status twitter_xt_next_cursor( struct xt_node *node, struct twitter_xml_list *txl )
172{
173        // Do something with the cursor.
174        txl->next_cursor = node->text != NULL ? atoi(node->text) : -1;
175
176        return XT_HANDLED;
177}
178
179/**
180 * Fill a list of ids.
181 */
182static xt_status twitter_xt_get_friends_id_list( struct xt_node *node, struct twitter_xml_list *txl )
183{
184        struct xt_node *child;
185       
186        // Set the list type.
187        txl->type = TXL_ID;
188
189        // The root <statuses> node should hold the list of statuses <status>
190        // Walk over the nodes children.
191        for( child = node->children ; child ; child = child->next )
192        {
193                if ( g_strcasecmp( "id", child->name ) == 0)
194                {
195                        // Add the item to the list.
196                        txl->list = g_slist_append (txl->list, g_memdup( child->text, child->text_len + 1 ));
197                }
198                else if ( g_strcasecmp( "next_cursor", child->name ) == 0)
199                {
200                        twitter_xt_next_cursor(child, txl);
201                }
202        }
203
204        return XT_HANDLED;
205}
206
207/**
208 * Callback for getting the friends ids.
209 */
210static void twitter_http_get_friends_ids(struct http_request *req)
211{
212        struct im_connection *ic;
213        struct xt_parser *parser;
214        struct twitter_xml_list *txl;
215        struct twitter_data *td;
216
217        ic = req->data;
218
219        // Check if the connection is still active.
220        if( !g_slist_find( twitter_connections, ic ) )
221                return;
222       
223        td = ic->proto_data;
224
225        // Check if the HTTP request went well.
226        if (req->status_code != 200) {
227                // It didn't go well, output the error and return.
228                if (++td->http_fails >= 5)
229                        imcb_error(ic, "Could not retrieve friends: %s", twitter_parse_error(req));
230               
231                return;
232        } else {
233                td->http_fails = 0;
234        }
235
236        txl = g_new0(struct twitter_xml_list, 1);
237
238        // Parse the data.
239        parser = xt_new( NULL, txl );
240        xt_feed( parser, req->reply_body, req->body_size );
241        twitter_xt_get_friends_id_list(parser->root, txl);
242        xt_free( parser );
243
244        if (txl->next_cursor)
245                twitter_get_friends_ids(ic, txl->next_cursor);
246
247        txl_free(txl);
248        g_free(txl);
249}
250
251/**
252 * Function to fill a twitter_xml_user struct.
253 * It sets:
254 *  - the name and
255 *  - the screen_name.
256 */
257static xt_status twitter_xt_get_user( struct xt_node *node, struct twitter_xml_user *txu )
258{
259        struct xt_node *child;
260
261        // Walk over the nodes children.
262        for( child = node->children ; child ; child = child->next )
263        {
264                if ( g_strcasecmp( "name", child->name ) == 0)
265                {
266                        txu->name = g_memdup( child->text, child->text_len + 1 );
267                }
268                else if (g_strcasecmp( "screen_name", child->name ) == 0)
269                {
270                        txu->screen_name = g_memdup( child->text, child->text_len + 1 );
271                }
272        }
273        return XT_HANDLED;
274}
275
276/**
277 * Function to fill a twitter_xml_list struct.
278 * It sets:
279 *  - all <user>s from the <users> element.
280 */
281static xt_status twitter_xt_get_users( struct xt_node *node, struct twitter_xml_list *txl )
282{
283        struct twitter_xml_user *txu;
284        struct xt_node *child;
285
286        // Set the type of the list.
287        txl->type = TXL_USER;
288
289        // The root <users> node should hold the list of users <user>
290        // Walk over the nodes children.
291        for( child = node->children ; child ; child = child->next )
292        {
293                if ( g_strcasecmp( "user", child->name ) == 0)
294                {
295                        txu = g_new0(struct twitter_xml_user, 1);
296                        twitter_xt_get_user(child, txu);
297                        // Put the item in the front of the list.
298                        txl->list = g_slist_prepend (txl->list, txu);
299                }
300        }
301
302        return XT_HANDLED;
303}
304
305/**
306 * Function to fill a twitter_xml_list struct.
307 * It calls twitter_xt_get_users to get the <user>s from a <users> element.
308 * It sets:
309 *  - the next_cursor.
310 */
311static xt_status twitter_xt_get_user_list( struct xt_node *node, struct twitter_xml_list *txl )
312{
313        struct xt_node *child;
314
315        // Set the type of the list.
316        txl->type = TXL_USER;
317
318        // The root <user_list> node should hold a users <users> element
319        // Walk over the nodes children.
320        for( child = node->children ; child ; child = child->next )
321        {
322                if ( g_strcasecmp( "users", child->name ) == 0)
323                {
324                        twitter_xt_get_users(child, txl);
325                }
326                else if ( g_strcasecmp( "next_cursor", child->name ) == 0)
327                {
328                        twitter_xt_next_cursor(child, txl);
329                }
330        }
331
332        return XT_HANDLED;
333}
334
335
336/**
337 * Function to fill a twitter_xml_status struct.
338 * It sets:
339 *  - the status text and
340 *  - the created_at timestamp and
341 *  - the status id and
342 *  - the user in a twitter_xml_user struct.
343 */
344static xt_status twitter_xt_get_status( struct xt_node *node, struct twitter_xml_status *txs )
345{
346        struct xt_node *child;
347
348        // Walk over the nodes children.
349        for( child = node->children ; child ; child = child->next )
350        {
351                if ( g_strcasecmp( "text", child->name ) == 0)
352                {
353                        txs->text = g_memdup( child->text, child->text_len + 1 );
354                }
355                else if (g_strcasecmp( "created_at", child->name ) == 0)
356                {
357                        struct tm parsed;
358                       
359                        /* Very sensitive to changes to the formatting of
360                           this field. :-( Also assumes the timezone used
361                           is UTC since C time handling functions suck. */
362                        if( strptime( child->text, "%a %b %d %H:%M:%S %z %Y", &parsed ) != NULL )
363                                txs->created_at = mktime_utc( &parsed );
364                }
365                else if (g_strcasecmp( "user", child->name ) == 0)
366                {
367                        txs->user = g_new0(struct twitter_xml_user, 1);
368                        twitter_xt_get_user( child, txs->user );
369                }
370                else if (g_strcasecmp( "id", child->name ) == 0)
371                {
372                        txs->id = g_ascii_strtoull (child->text, NULL, 10);
373                }
374        }
375        return XT_HANDLED;
376}
377
378/**
379 * Function to fill a twitter_xml_list struct.
380 * It sets:
381 *  - all <status>es within the <status> element and
382 *  - the next_cursor.
383 */
384static xt_status twitter_xt_get_status_list( struct xt_node *node, struct twitter_xml_list *txl )
385{
386        struct twitter_xml_status *txs;
387        struct xt_node *child;
388
389        // Set the type of the list.
390        txl->type = TXL_STATUS;
391
392        // The root <statuses> node should hold the list of statuses <status>
393        // Walk over the nodes children.
394        for( child = node->children ; child ; child = child->next )
395        {
396                if ( g_strcasecmp( "status", child->name ) == 0)
397                {
398                        txs = g_new0(struct twitter_xml_status, 1);
399                        twitter_xt_get_status(child, txs);
400                        // Put the item in the front of the list.
401                        txl->list = g_slist_prepend (txl->list, txs);
402                }
403                else if ( g_strcasecmp( "next_cursor", child->name ) == 0)
404                {
405                        twitter_xt_next_cursor(child, txl);
406                }
407        }
408
409        return XT_HANDLED;
410}
411
412static void twitter_http_get_home_timeline(struct http_request *req);
413
414/**
415 * Get the timeline.
416 */
417void twitter_get_home_timeline(struct im_connection *ic, int next_cursor)
418{
419        struct twitter_data *td = ic->proto_data;
420
421        char* args[4];
422        args[0] = "cursor";
423        args[1] = g_strdup_printf ("%d", next_cursor);
424        if (td->home_timeline_id) {
425                args[2] = "since_id";
426                args[3] = g_strdup_printf ("%llu", (long long unsigned int) td->home_timeline_id);
427        }
428
429        twitter_http(ic, TWITTER_HOME_TIMELINE_URL, twitter_http_get_home_timeline, ic, 0, args, td->home_timeline_id ? 4 : 2);
430
431        g_free(args[1]);
432        if (td->home_timeline_id) {
433                g_free(args[3]);
434        }
435}
436
437static void twitter_groupchat_init(struct im_connection *ic)
438{
439        char *name_hint;
440        struct groupchat *gc;
441        struct twitter_data *td = ic->proto_data;
442       
443        td->home_timeline_gc = gc = imcb_chat_new( ic, "home/timeline" );
444       
445        name_hint = g_strdup_printf( "Twitter_%s", ic->acc->user );
446        imcb_chat_name_hint( gc, name_hint );
447        g_free( name_hint );
448}
449
450/**
451 * Function that is called to see the statuses in a groupchat window.
452 */
453static void twitter_groupchat(struct im_connection *ic, GSList *list)
454{
455        struct twitter_data *td = ic->proto_data;
456        GSList *l = NULL;
457        struct twitter_xml_status *status;
458        struct groupchat *gc;
459
460        // Create a new groupchat if it does not exsist.
461        if (!td->home_timeline_gc)
462                twitter_groupchat_init(ic);
463       
464        gc = td->home_timeline_gc;
465        if (!gc->joined)
466                imcb_chat_add_buddy( gc, ic->acc->user );
467
468        for ( l = list; l ; l = g_slist_next(l) )
469        {
470                status = l->data;
471                twitter_add_buddy(ic, status->user->screen_name, status->user->name);
472               
473                strip_html(status->text);
474               
475                // Say it!
476                if (g_strcasecmp(td->user, status->user->screen_name) == 0)
477                        imcb_chat_log (gc, "Your Tweet: %s", status->text);
478                else
479                        imcb_chat_msg (gc, status->user->screen_name, status->text, 0, status->created_at );
480               
481                // Update the home_timeline_id to hold the highest id, so that by the next request
482                // we won't pick up the updates allready in the list.
483                td->home_timeline_id = td->home_timeline_id < status->id ? status->id : td->home_timeline_id;
484        }
485}
486
487/**
488 * Function that is called to see statuses as private messages.
489 */
490static void twitter_private_message_chat(struct im_connection *ic, GSList *list)
491{
492        struct twitter_data *td = ic->proto_data;
493        GSList *l = NULL;
494        struct twitter_xml_status *status;
495        char from[MAX_STRING];
496        gboolean mode_one;
497       
498        mode_one = g_strcasecmp( set_getstr( &ic->acc->set, "mode" ), "one" ) == 0;
499
500        if( mode_one )
501        {
502                g_snprintf( from, sizeof( from ) - 1, "twitter_%s", ic->acc->user );
503                from[MAX_STRING-1] = '\0';
504        }
505       
506        for ( l = list; l ; l = g_slist_next(l) )
507        {
508                char *text = NULL;
509               
510                status = l->data;
511               
512                strip_html( status->text );
513                if( mode_one )
514                        text = g_strdup_printf( "\002<\002%s\002>\002 %s",
515                                                status->user->screen_name, status->text );
516                else
517                        twitter_add_buddy(ic, status->user->screen_name, status->user->name);
518               
519                imcb_buddy_msg( ic,
520                                mode_one ? from : status->user->screen_name,
521                                mode_one ? text : status->text,
522                                0, status->created_at );
523               
524                // Update the home_timeline_id to hold the highest id, so that by the next request
525                // we won't pick up the updates allready in the list.
526                td->home_timeline_id = td->home_timeline_id < status->id ? status->id : td->home_timeline_id;
527               
528                g_free( text );
529        }
530}
531
532/**
533 * Callback for getting the home timeline.
534 */
535static void twitter_http_get_home_timeline(struct http_request *req)
536{
537        struct im_connection *ic = req->data;
538        struct twitter_data *td;
539        struct xt_parser *parser;
540        struct twitter_xml_list *txl;
541
542        // Check if the connection is still active.
543        if( !g_slist_find( twitter_connections, ic ) )
544                return;
545       
546        td = ic->proto_data;
547
548        // Check if the HTTP request went well.
549        if (req->status_code == 200)
550        {
551                td->http_fails = 0;
552                if (!(ic->flags & OPT_LOGGED_IN))
553                        imcb_connected(ic);
554        }
555        else if (req->status_code == 401)
556        {
557                imcb_error( ic, "Authentication failure" );
558                imc_logout( ic, FALSE );
559                return;
560        }
561        else
562        {
563                // It didn't go well, output the error and return.
564                if (++td->http_fails >= 5)
565                        imcb_error(ic, "Could not retrieve " TWITTER_HOME_TIMELINE_URL ": %s", twitter_parse_error(req));
566               
567                return;
568        }
569
570        txl = g_new0(struct twitter_xml_list, 1);
571        txl->list = NULL;
572
573        // Parse the data.
574        parser = xt_new( NULL, txl );
575        xt_feed( parser, req->reply_body, req->body_size );
576        // The root <statuses> node should hold the list of statuses <status>
577        twitter_xt_get_status_list(parser->root, txl);
578        xt_free( parser );
579
580        // See if the user wants to see the messages in a groupchat window or as private messages.
581        if (g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "chat") == 0)
582                twitter_groupchat(ic, txl->list);
583        else
584                twitter_private_message_chat(ic, txl->list);
585
586        // Free the structure. 
587        txl_free(txl);
588        g_free(txl);
589}
590
591/**
592 * Callback for getting (twitter)friends...
593 *
594 * Be afraid, be very afraid! This function will potentially add hundreds of "friends". "Who has
595 * hundreds of friends?" you wonder? You probably not, since you are reading the source of
596 * BitlBee... Get a life and meet new people!
597 */
598static void twitter_http_get_statuses_friends(struct http_request *req)
599{
600        struct im_connection *ic = req->data;
601        struct twitter_data *td;
602        struct xt_parser *parser;
603        struct twitter_xml_list *txl;
604        GSList *l = NULL;
605        struct twitter_xml_user *user;
606
607        // Check if the connection is still active.
608        if( !g_slist_find( twitter_connections, ic ) )
609                return;
610       
611        td = ic->proto_data;
612       
613        // Check if the HTTP request went well.
614        if (req->status_code == 401)
615        {
616                imcb_error( ic, "Authentication failure" );
617                imc_logout( ic, FALSE );
618                return;
619        } else if (req->status_code != 200) {
620                // It didn't go well, output the error and return.
621                imcb_error(ic, "Could not retrieve " TWITTER_SHOW_FRIENDS_URL ": %s", twitter_parse_error(req));
622                imc_logout( ic, TRUE );
623                return;
624        } else {
625                td->http_fails = 0;
626        }
627       
628        if( !td->home_timeline_gc &&
629            g_strcasecmp( set_getstr( &ic->acc->set, "mode" ), "chat" ) == 0 )
630                twitter_groupchat_init( ic );
631
632        txl = g_new0(struct twitter_xml_list, 1);
633        txl->list = NULL;
634
635        // Parse the data.
636        parser = xt_new( NULL, txl );
637        xt_feed( parser, req->reply_body, req->body_size );
638
639        // Get the user list from the parsed xml feed.
640        twitter_xt_get_user_list(parser->root, txl);
641        xt_free( parser );
642
643        // Add the users as buddies.
644        for ( l = txl->list; l ; l = g_slist_next(l) )
645        {
646                user = l->data;
647                twitter_add_buddy(ic, user->screen_name, user->name);
648        }
649
650        // if the next_cursor is set to something bigger then 0 there are more friends to gather.
651        if (txl->next_cursor > 0)
652                twitter_get_statuses_friends(ic, txl->next_cursor);
653
654        // Free the structure.
655        txl_free(txl);
656        g_free(txl);
657       
658        td->flags |= TWITTER_HAVE_FRIENDS;
659        twitter_login_finish(ic);
660}
661
662/**
663 * Get the friends.
664 */
665void twitter_get_statuses_friends(struct im_connection *ic, int next_cursor)
666{
667        char* args[2];
668        args[0] = "cursor";
669        args[1] = g_strdup_printf ("%d", next_cursor);
670
671        twitter_http(ic, TWITTER_SHOW_FRIENDS_URL, twitter_http_get_statuses_friends, ic, 0, args, 2);
672
673        g_free(args[1]);
674}
675
676/**
677 * Callback to use after sending a post request to twitter.
678 */
679static void twitter_http_post(struct http_request *req)
680{
681        struct im_connection *ic = req->data;
682
683        // Check if the connection is still active.
684        if( !g_slist_find( twitter_connections, ic ) )
685                return;
686
687        // Check if the HTTP request went well.
688        if (req->status_code != 200) {
689                // It didn't go well, output the error and return.
690                imcb_error(ic, "HTTP error: %s", twitter_parse_error(req));
691                return;
692        }
693}
694
695/**
696 * Function to POST a new status to twitter.
697 */ 
698void twitter_post_status(struct im_connection *ic, char* msg)
699{
700        char* args[2];
701        args[0] = "status";
702        args[1] = msg;
703        twitter_http(ic, TWITTER_STATUS_UPDATE_URL, twitter_http_post, ic, 1, args, 2);
704//      g_free(args[1]);
705}
706
707
708/**
709 * Function to POST a new message to twitter.
710 */
711void twitter_direct_messages_new(struct im_connection *ic, char *who, char *msg)
712{
713        char* args[4];
714        args[0] = "screen_name";
715        args[1] = who;
716        args[2] = "text";
717        args[3] = msg;
718        // Use the same callback as for twitter_post_status, since it does basically the same.
719        twitter_http(ic, TWITTER_DIRECT_MESSAGES_NEW_URL, twitter_http_post, ic, 1, args, 4);
720//      g_free(args[1]);
721//      g_free(args[3]);
722}
723
724void twitter_friendships_create_destroy(struct im_connection *ic, char *who, int create)
725{
726        char* args[2];
727        args[0] = "screen_name";
728        args[1] = who;
729        twitter_http(ic, create ? TWITTER_FRIENDSHIPS_CREATE_URL : TWITTER_FRIENDSHIPS_DESTROY_URL, twitter_http_post, ic, 1, args, 2);
730}
Note: See TracBrowser for help on using the repository browser.