source: protocols/twitter/twitter.c @ 27ad50b

Last change on this file since 27ad50b was 27ad50b, checked in by Wilmer van der Gaast <wilmer@…>, at 2011-06-09T08:19:32Z

Dirty workaround: Don't download the contact list for now as Twitter
deprecated the API call BitlBee uses for that. The contact list will be
updated as tweets come in. Real fix will come in later, at least this lets
everyone log in again.

  • Property mode set to 100644
File size: 16.9 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       
[27ad50b]288        td->flags |= TWITTER_HAVE_FRIENDS;
[1b221e0]289        td->user = acc->user;
[bb5ce4d1]290        if( strstr( acc->pass, "oauth_token=" ) )
[ce617f0]291                td->oauth_info = oauth_from_string( acc->pass, get_oauth_service( ic ) );
[e88fbe27]292       
[ffcdf13]293        sprintf( name, "%s_%s", td->prefix, acc->user );
[e88fbe27]294        imcb_add_buddy( ic, name, NULL );
295        imcb_buddy_status( ic, name, OPT_LOGGED_IN, NULL, NULL );
[713d611]296       
[ce81acd]297        if( set_getbool( &acc->set, "show_ids" ) )
298                td->log = g_new0( struct twitter_log_data, TWITTER_LOG_LENGTH );
299       
[d6aa6dd]300        imcb_log( ic, "Connecting" );
301       
302        twitter_login_finish( ic );
[1b221e0]303}
304
305/**
306 * Logout method. Just free the twitter_data.
307 */
308static void twitter_logout( struct im_connection *ic )
309{
310        struct twitter_data *td = ic->proto_data;
311       
312        // Set the status to logged out.
[4ffd757]313        ic->flags &= ~ OPT_LOGGED_IN;
[1b221e0]314
[2abceca]315        // Remove the main_loop function from the function queue.
316        b_event_remove(td->main_loop_id);
317
[37d84b3]318        if(td->home_timeline_gc)
319                imcb_chat_free(td->home_timeline_gc);
[1014cab]320
[1b221e0]321        if( td )
322        {
[18dbb20]323                oauth_info_free( td->oauth_info );
[ffcdf13]324                g_free( td->prefix );
[04a927c]325                g_free( td->url_host );
326                g_free( td->url_path );
[508c340]327                g_free( td->pass );
[ce81acd]328                g_free( td->log );
[1b221e0]329                g_free( td );
330        }
[62d2cfb]331
332        twitter_connections = g_slist_remove( twitter_connections, ic );
[1b221e0]333}
334
[7b87539]335static void twitter_handle_command( struct im_connection *ic, char *message );
336
[1b221e0]337/**
338 *
339 */
340static int twitter_buddy_msg( struct im_connection *ic, char *who, char *message, int away )
341{
[c42e8b9]342        struct twitter_data *td = ic->proto_data;
[ffcdf13]343        int plen = strlen( td->prefix );
[c42e8b9]344       
[ffcdf13]345        if (g_strncasecmp(who, td->prefix, plen) == 0 && who[plen] == '_' &&
346            g_strcasecmp(who + plen + 1, ic->acc->user) == 0)
[c42e8b9]347        {
[c2ecadc]348                if( set_getbool( &ic->acc->set, "oauth" ) &&
349                    td->oauth_info && td->oauth_info->token == NULL )
[18dbb20]350                {
[64f8c425]351                        char pin[strlen(message)+1], *s;
352                       
353                        strcpy( pin, message );
354                        for( s = pin + sizeof( pin ) - 2; s > pin && isspace( *s ); s -- )
355                                *s = '\0';
356                        for( s = pin; *s && isspace( *s ); s ++ ) {}
357                       
358                        if( !oauth_access_token( s, td->oauth_info ) )
[c2ecadc]359                        {
360                                imcb_error( ic, "OAuth error: %s", "Failed to send access token request" );
361                                imc_logout( ic, TRUE );
362                                return FALSE;
363                        }
[18dbb20]364                }
[7b87539]365                else
366                        twitter_handle_command(ic, message);
[c42e8b9]367        }
[e88fbe27]368        else
[c42e8b9]369        {
[e88fbe27]370                twitter_direct_messages_new(ic, who, message);
[c42e8b9]371        }
[1b221e0]372        return( 0 );
373}
374
375/**
376 *
377 */
378static void twitter_set_my_name( struct im_connection *ic, char *info )
379{
380}
381
382static void twitter_get_info(struct im_connection *ic, char *who) 
383{
384}
385
386static void twitter_add_buddy( struct im_connection *ic, char *who, char *group )
387{
[7d53efb]388        twitter_friendships_create_destroy(ic, who, 1);
[1b221e0]389}
390
391static void twitter_remove_buddy( struct im_connection *ic, char *who, char *group )
392{
[7d53efb]393        twitter_friendships_create_destroy(ic, who, 0);
[1b221e0]394}
395
396static void twitter_chat_msg( struct groupchat *c, char *message, int flags )
397{
[7b87539]398        if( c && message )
399                twitter_handle_command( c->ic, message );
[1b221e0]400}
401
402static void twitter_chat_invite( struct groupchat *c, char *who, char *message )
403{
404}
405
406static void twitter_chat_leave( struct groupchat *c )
407{
[16592d8]408        struct twitter_data *td = c->ic->proto_data;
409       
410        if( c != td->home_timeline_gc )
411                return; /* WTF? */
412       
413        /* If the user leaves the channel: Fine. Rejoin him/her once new
414           tweets come in. */
415        imcb_chat_free(td->home_timeline_gc);
416        td->home_timeline_gc = NULL;
[1b221e0]417}
418
419static void twitter_keepalive( struct im_connection *ic )
420{
421}
422
423static void twitter_add_permit( struct im_connection *ic, char *who )
424{
425}
426
427static void twitter_rem_permit( struct im_connection *ic, char *who )
428{
429}
430
431static void twitter_add_deny( struct im_connection *ic, char *who )
432{
433}
434
435static void twitter_rem_deny( struct im_connection *ic, char *who )
436{
437}
438
439//static char *twitter_set_display_name( set_t *set, char *value )
440//{
441//      return value;
442//}
[7b87539]443
[203a2d2]444static void twitter_buddy_data_add( struct bee_user *bu )
445{
446        bu->data = g_new0( struct twitter_user_data, 1 );
447}
448
449static void twitter_buddy_data_free( struct bee_user *bu )
450{
451        g_free( bu->data );
452}
453
[7b87539]454static void twitter_handle_command( struct im_connection *ic, char *message )
455{
456        struct twitter_data *td = ic->proto_data;
[15bc063]457        char *cmds, **cmd, *new = NULL;
458        guint64 in_reply_to = 0;
[7b87539]459       
460        cmds = g_strdup( message );
461        cmd = split_command_parts( cmds );
462       
463        if( cmd[0] == NULL )
464        {
465                g_free( cmds );
466                return;
467        }
[203a2d2]468        else if( !set_getbool( &ic->acc->set, "commands" ) )
[7b87539]469        {
470                /* Not supporting commands. */
471        }
472        else if( g_strcasecmp( cmd[0], "undo" ) == 0 )
473        {
474                guint64 id;
475               
476                if( cmd[1] )
[b890626]477                        id = g_ascii_strtoull( cmd[1], NULL, 10 );
[7b87539]478                else
479                        id = td->last_status_id;
480               
481                /* TODO: User feedback. */
482                if( id )
483                        twitter_status_destroy( ic, id );
[665c24f]484                else
485                        twitter_msg( ic, "Could not undo last action" );
[7b87539]486               
487                g_free( cmds );
488                return;
489        }
[b890626]490        else if( g_strcasecmp( cmd[0], "follow" ) == 0 && cmd[1] )
491        {
492                twitter_add_buddy( ic, cmd[1], NULL );
493                g_free( cmds );
494                return;
495        }
496        else if( g_strcasecmp( cmd[0], "unfollow" ) == 0 && cmd[1] )
497        {
498                twitter_remove_buddy( ic, cmd[1], NULL );
499                g_free( cmds );
500                return;
501        }
502        else if( g_strcasecmp( cmd[0], "rt" ) == 0 && cmd[1] )
503        {
504                struct twitter_user_data *tud;
505                bee_user_t *bu;
506                guint64 id;
507               
508                if( ( bu = bee_user_by_handle( ic->bee, ic, cmd[1] ) ) &&
509                    ( tud = bu->data ) && tud->last_id )
510                        id = tud->last_id;
511                else
[4f50ea5]512                {
[b890626]513                        id = g_ascii_strtoull( cmd[1], NULL, 10 );
[6220254]514                        if( id < TWITTER_LOG_LENGTH && td->log )
[4f50ea5]515                                id = td->log[id].id;
516                }
[b890626]517               
518                td->last_status_id = 0;
519                if( id )
520                        twitter_status_retweet( ic, id );
[665c24f]521                else
522                        twitter_msg( ic, "User `%s' does not exist or didn't "
523                                         "post any statuses recently", cmd[1] );
[b890626]524               
525                g_free( cmds );
526                return;
527        }
[15bc063]528        else if( g_strcasecmp( cmd[0], "reply" ) == 0 && cmd[1] && cmd[2] )
529        {
530                struct twitter_user_data *tud;
531                bee_user_t *bu = NULL;
532                guint64 id = 0;
533               
534                if( ( bu = bee_user_by_handle( ic->bee, ic, cmd[1] ) ) &&
535                    ( tud = bu->data ) && tud->last_id )
536                {
537                        id = tud->last_id;
538                }
539                else if( ( id = g_ascii_strtoull( cmd[1], NULL, 10 ) ) &&
[6220254]540                         ( id < TWITTER_LOG_LENGTH ) && td->log )
[15bc063]541                {
542                        bu = td->log[id].bu;
543                        if( g_slist_find( ic->bee->users, bu ) )
544                                id = td->log[id].id;
545                        else
546                                bu = NULL;
547                }
548                if( !id || !bu )
549                {
550                        twitter_msg( ic, "User `%s' does not exist or didn't "
551                                         "post any statuses recently", cmd[1] );
552                        return;
553                }
554                message = new = g_strdup_printf( "@%s %s", bu->handle,
555                                                 message + ( cmd[2] - cmd[0] ) );
556                in_reply_to = id;
557        }
[7b87539]558        else if( g_strcasecmp( cmd[0], "post" ) == 0 )
559        {
560                message += 5;
561        }
562       
563        {
[15bc063]564                char *s;
[7b87539]565                bee_user_t *bu;
566               
567                if( !twitter_length_check( ic, message ) )
568                {
[15bc063]569                        g_free( new );
[7b87539]570                        g_free( cmds );
571                        return;
572                }
573               
574                s = cmd[0] + strlen( cmd[0] ) - 1;
[15bc063]575                if( !new && s > cmd[0] && ( *s == ':' || *s == ',' ) )
[7b87539]576                {
577                        *s = '\0';
578                       
579                        if( ( bu = bee_user_by_handle( ic->bee, ic, cmd[0] ) ) )
580                        {
[b890626]581                                struct twitter_user_data *tud = bu->data;
582                               
[7b87539]583                                new = g_strdup_printf( "@%s %s", bu->handle,
584                                                       message + ( s - cmd[0] ) + 2 );
585                                message = new;
[b890626]586                               
587                                if( time( NULL ) < tud->last_time +
588                                    set_getint( &ic->acc->set, "auto_reply_timeout" ) )
589                                        in_reply_to = tud->last_id;
[7b87539]590                        }
591                }
592               
[b890626]593                /* If the user runs undo between this request and its response
594                   this would delete the second-last Tweet. Prevent that. */
595                td->last_status_id = 0;
596                twitter_post_status( ic, message, in_reply_to );
[7b87539]597                g_free( new );
598        }
599        g_free( cmds );
600}
[1b221e0]601
602void twitter_initmodule()
603{
604        struct prpl *ret = g_new0(struct prpl, 1);
605       
[1dd3470]606        ret->options = OPT_NOOTR;
[1b221e0]607        ret->name = "twitter";
608        ret->login = twitter_login;
609        ret->init = twitter_init;
610        ret->logout = twitter_logout;
611        ret->buddy_msg = twitter_buddy_msg;
612        ret->get_info = twitter_get_info;
613        ret->set_my_name = twitter_set_my_name;
614        ret->add_buddy = twitter_add_buddy;
615        ret->remove_buddy = twitter_remove_buddy;
616        ret->chat_msg = twitter_chat_msg;
617        ret->chat_invite = twitter_chat_invite;
618        ret->chat_leave = twitter_chat_leave;
619        ret->keepalive = twitter_keepalive;
620        ret->add_permit = twitter_add_permit;
621        ret->rem_permit = twitter_rem_permit;
622        ret->add_deny = twitter_add_deny;
623        ret->rem_deny = twitter_rem_deny;
[203a2d2]624        ret->buddy_data_add = twitter_buddy_data_add;
625        ret->buddy_data_free = twitter_buddy_data_free;
[1b221e0]626        ret->handle_cmp = g_strcasecmp;
[ffcdf13]627       
628        register_protocol(ret);
[1b221e0]629
[ffcdf13]630        /* And an identi.ca variant: */
631        ret = g_memdup(ret, sizeof(struct prpl));
632        ret->name = "identica";
[1b221e0]633        register_protocol(ret);
634}
Note: See TracBrowser for help on using the repository browser.