source: protocols/twitter/twitter.c @ 2370ec2

Last change on this file since 2370ec2 was 6eca2eb, checked in by Wilmer van der Gaast <wilmer@…>, at 2011-04-18T14:14:08Z

Try to show better Twitter error messages. Sadly this doesn't always work
since Twitter can't seem to make up their mind on the formatting of their
error responses, sometimes using XML and sometimes plain text.

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