source: protocols/twitter/twitter.c @ 5155570

Last change on this file since 5155570 was 6220254, checked in by Wilmer van der Gaast <wilmer@…>, at 2011-03-31T10:28:17Z

Check td->log everywhere before using it..

  • 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                {
153                        imcb_error( ic, "OAuth error: %s", info->http->status_string );
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                {
169                        imcb_error( ic, "OAuth error: %s", info->http->status_string );
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.