Changeset cc6fdf8 for protocols/twitter


Ignore:
Timestamp:
2012-12-22T00:14:26Z (12 years ago)
Author:
Wilmer van der Gaast <wilmer@…>
Branches:
master
Children:
7d5afa6
Parents:
92d3044 (diff), 573e274 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merging JSON branch. It's very stable by now, and I want more testers.

Location:
protocols/twitter
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • protocols/twitter/twitter.c

    r92d3044 rcc6fdf8  
    44*  Simple module to facilitate twitter functionality.                       *
    55*                                                                           *
    6 *  Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com>                 *
     6*  Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com>            *
     7*  Copyright 2010-2012 Wilmer van der Gaast <wilmer@gaast.net>              *
    78*                                                                           *
    89*  This library is free software; you can redistribute it and/or            *
     
    2930#include "url.h"
    3031
    31 #define twitter_msg( ic, fmt... ) \
    32         do {                                            \
    33                 struct twitter_data *td = ic->proto_data;   \
    34                 if( td->timeline_gc )                       \
    35                         imcb_chat_log( td->timeline_gc, fmt );  \
    36                 else                                        \
    37                         imcb_log( ic, fmt );                    \
    38         } while( 0 );
    39 
    4032GSList *twitter_connections = NULL;
    41 
    4233/**
    4334 * Main loop function
     
    6253        struct twitter_data *td = ic->proto_data;
    6354
     55        /* Create the room now that we "logged in". */
     56        if (td->flags & TWITTER_MODE_CHAT)
     57                twitter_groupchat_init(ic);
     58
    6459        imcb_log(ic, "Getting initial statuses");
    6560
    66         // Run this once. After this queue the main loop function.
     61        // Run this once. After this queue the main loop function (or open the
     62        // stream if available).
    6763        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 =
    72             b_timeout_add(set_getint(&ic->acc->set, "fetch_interval") * 1000, twitter_main_loop, ic);
     64       
     65        if (set_getbool(&ic->acc->set, "stream")) {
     66                /* That fetch was just to get backlog, the stream will give
     67                   us the rest. \o/ */
     68                twitter_open_stream(ic);
     69               
     70                /* Stream sends keepalives (empty lines) or actual data at
     71                   least twice a minute. Disconnect if this stops. */
     72                ic->flags |= OPT_PONGS;
     73        } else {
     74                /* Not using the streaming API, so keep polling the old-
     75                   fashioned way. :-( */
     76                td->main_loop_id =
     77                    b_timeout_add(set_getint(&ic->acc->set, "fetch_interval") * 1000,
     78                                  twitter_main_loop, ic);
     79        }
     80}
     81
     82struct groupchat *twitter_groupchat_init(struct im_connection *ic)
     83{
     84        char *name_hint;
     85        struct groupchat *gc;
     86        struct twitter_data *td = ic->proto_data;
     87        GSList *l;
     88
     89        if (td->timeline_gc)
     90                return td->timeline_gc;
     91
     92        td->timeline_gc = gc = imcb_chat_new(ic, "twitter/timeline");
     93
     94        name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user);
     95        imcb_chat_name_hint(gc, name_hint);
     96        g_free(name_hint);
     97
     98        for (l = ic->bee->users; l; l = l->next) {
     99                bee_user_t *bu = l->data;
     100                if (bu->ic == ic)
     101                        imcb_chat_add_buddy(gc, bu->handle);
     102        }
     103        imcb_chat_add_buddy(gc, ic->acc->user);
     104       
     105        return gc;
    73106}
    74107
     
    83116        if (set_getbool(&ic->acc->set, "oauth") && !td->oauth_info)
    84117                twitter_oauth_start(ic);
    85         else if (g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "one") != 0 &&
    86                 !(td->flags & TWITTER_HAVE_FRIENDS)) {
     118        else if (!(td->flags & TWITTER_MODE_ONE) &&
     119                !(td->flags & TWITTER_HAVE_FRIENDS)) {
    87120                imcb_log(ic, "Getting contact list");
    88121                twitter_get_friends_ids(ic, -1);
    89                 //twitter_get_statuses_friends(ic, -1);
    90122        } else
    91123                twitter_main_loop_start(ic);
     
    187219}
    188220
    189 
    190 static char *set_eval_mode(set_t * set, char *value)
    191 {
    192         if (g_strcasecmp(value, "one") == 0 ||
    193             g_strcasecmp(value, "many") == 0 || g_strcasecmp(value, "chat") == 0)
    194                 return value;
    195         else
    196                 return NULL;
    197 }
    198 
    199221int twitter_url_len_diff(gchar *msg, unsigned int target_len)
    200222{
     
    233255                return TRUE;
    234256
    235         imcb_error(ic, "Maximum message length exceeded: %d > %d", len, max);
     257        twitter_log(ic, "Maximum message length exceeded: %d > %d", len, max);
    236258
    237259        return FALSE;
     260}
     261
     262static char *set_eval_commands(set_t * set, char *value)
     263{
     264        if (g_strcasecmp(value, "strict") == 0 )
     265                return value;
     266        else
     267                return set_eval_bool(set, value);
     268}
     269
     270static char *set_eval_mode(set_t * set, char *value)
     271{
     272        if (g_strcasecmp(value, "one") == 0 ||
     273            g_strcasecmp(value, "many") == 0 || g_strcasecmp(value, "chat") == 0)
     274                return value;
     275        else
     276                return NULL;
    238277}
    239278
     
    257296        s->flags |= ACC_SET_OFFLINE_ONLY;
    258297
    259         s = set_add(&acc->set, "commands", "true", set_eval_bool, acc);
     298        s = set_add(&acc->set, "commands", "true", set_eval_commands, acc);
    260299
    261300        s = set_add(&acc->set, "fetch_interval", "60", set_eval_int, acc);
     
    274313
    275314        s = set_add(&acc->set, "show_ids", "true", set_eval_bool, acc);
    276         s->flags |= ACC_SET_OFFLINE_ONLY;
    277315
    278316        s = set_add(&acc->set, "show_old_mentions", "20", set_eval_int, acc);
    279317
    280318        s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc);
     319       
     320        if (strcmp(acc->prpl->name, "twitter") == 0) {
     321                s = set_add(&acc->set, "stream", "true", set_eval_bool, acc);
     322                s->flags |= ACC_SET_OFFLINE_ONLY;
     323        }
    281324}
    282325
    283326/**
    284  * Login method. Since the twitter API works with seperate HTTP request we
     327 * Login method. Since the twitter API works with separate HTTP request we
    285328 * only save the user and pass to the twitter_data object.
    286329 */
     
    298341                imc_logout(ic, FALSE);
    299342                return;
     343        }
     344
     345        if (!strstr(url.host, "twitter.com") &&
     346            set_getbool(&ic->acc->set, "stream")) {
     347                imcb_error(ic, "Warning: The streaming API is only supported by Twitter, "
     348                               "and you seem to be connecting to a different service.");
    300349        }
    301350
     
    340389        imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
    341390
    342         if (set_getbool(&acc->set, "show_ids"))
    343                 td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH);
     391        td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH);
     392        td->log_id = -1;
     393       
     394        s = set_getstr(&ic->acc->set, "mode");
     395        if (g_strcasecmp(s, "one") == 0)
     396                td->flags |= TWITTER_MODE_ONE;
     397        else if (g_strcasecmp(s, "many") == 0)
     398                td->flags |= TWITTER_MODE_MANY;
     399        else
     400                td->flags |= TWITTER_MODE_CHAT;
    344401
    345402        twitter_login_finish(ic);
     
    363420
    364421        if (td) {
     422                http_close(td->stream);
    365423                oauth_info_free(td->oauth_info);
    366424                g_free(td->user);
     
    495553 *  Returns 0 if the user provides garbage.
    496554 */
    497 static guint64 twitter_message_id_from_command_arg(struct im_connection *ic, struct twitter_data *td, char *arg) {
     555static guint64 twitter_message_id_from_command_arg(struct im_connection *ic, char *arg, bee_user_t **bu_) {
     556        struct twitter_data *td = ic->proto_data;
    498557        struct twitter_user_data *tud;
    499         bee_user_t *bu;
     558        bee_user_t *bu = NULL;
    500559        guint64 id = 0;
    501         if (g_str_has_prefix(arg, "#") &&
    502                 sscanf(arg + 1, "%" G_GUINT64_FORMAT, &id) == 1) {
    503                 if (id < TWITTER_LOG_LENGTH && td->log)
     560       
     561        if (bu_)
     562                *bu_ = NULL;
     563        if (!arg || !arg[0])
     564                return 0;
     565       
     566        if (arg[0] != '#' && (bu = bee_user_by_handle(ic->bee, ic, arg))) {
     567                if ((tud = bu->data))
     568                        id = tud->last_id;
     569        } else {
     570                if (arg[0] == '#')
     571                        arg++;
     572                if (sscanf(arg, "%" G_GINT64_MODIFIER "x", &id) == 1 &&
     573                    id < TWITTER_LOG_LENGTH) {
     574                        bu = td->log[id].bu;
    504575                        id = td->log[id].id;
    505         } else if ((bu = bee_user_by_handle(ic->bee, ic, arg)) &&
    506                 (tud = bu->data) && tud->last_id)
    507                 id = tud->last_id;
    508         else if (sscanf(arg, "%" G_GUINT64_FORMAT, &id) == 1){
    509                 if (id < TWITTER_LOG_LENGTH && td->log)
    510                         id = td->log[id].id;
    511         }
     576                        /* Beware of dangling pointers! */
     577                        if (!g_slist_find(ic->bee->users, bu))
     578                                bu = NULL;
     579                } else if (sscanf(arg, "%" G_GINT64_MODIFIER "d", &id) == 1) {
     580                        /* Allow normal tweet IDs as well; not a very useful
     581                           feature but it's always been there. Just ignore
     582                           very low IDs to avoid accidents. */
     583                        if (id < 1000000)
     584                                id = 0;
     585                }
     586        }
     587        if (bu_)
     588                *bu_ = bu;
    512589        return id;
    513590}
     
    517594        struct twitter_data *td = ic->proto_data;
    518595        char *cmds, **cmd, *new = NULL;
    519         guint64 in_reply_to = 0;
     596        guint64 in_reply_to = 0, id;
     597        gboolean allow_post =
     598                g_strcasecmp(set_getstr(&ic->acc->set, "commands"), "strict") != 0;
     599        bee_user_t *bu = NULL;
    520600
    521601        cmds = g_strdup(message);
     
    523603
    524604        if (cmd[0] == NULL) {
    525                 g_free(cmds);
    526                 return;
    527         } else if (!set_getbool(&ic->acc->set, "commands")) {
    528                 /* Not supporting commands. */
     605                goto eof;
     606        } else if (!set_getbool(&ic->acc->set, "commands") && allow_post) {
     607                /* Not supporting commands if "commands" is set to true/strict. */
    529608        } else if (g_strcasecmp(cmd[0], "undo") == 0) {
    530                 guint64 id;
    531 
    532609                if (cmd[1] == NULL)
    533610                        twitter_status_destroy(ic, td->last_status_id);
    534                 else if (sscanf(cmd[1], "%" G_GUINT64_FORMAT, &id) == 1) {
    535                         if (id < TWITTER_LOG_LENGTH && td->log)
    536                                 id = td->log[id].id;
    537                        
     611                else if ((id = twitter_message_id_from_command_arg(ic, cmd[1], NULL)))
    538612                        twitter_status_destroy(ic, id);
    539                 } else
    540                         twitter_msg(ic, "Could not undo last action");
    541 
    542                 g_free(cmds);
    543                 return;
     613                else
     614                        twitter_log(ic, "Could not undo last action");
     615
     616                goto eof;
    544617        } else if (g_strcasecmp(cmd[0], "favourite") == 0 && cmd[1]) {
    545                 guint64 id;
    546                 if ((id = twitter_message_id_from_command_arg(ic, td, cmd[1]))) {
     618                if ((id = twitter_message_id_from_command_arg(ic, cmd[1], NULL))) {
    547619                        twitter_favourite_tweet(ic, id);
    548620                } else {
    549                         twitter_msg(ic, "Please provide a message ID or username.");
     621                        twitter_log(ic, "Please provide a message ID or username.");
    550622                }
    551                 g_free(cmds);
    552                 return;
     623                goto eof;
    553624        } else if (g_strcasecmp(cmd[0], "follow") == 0 && cmd[1]) {
    554625                twitter_add_buddy(ic, cmd[1], NULL);
    555                 g_free(cmds);
    556                 return;
     626                goto eof;
    557627        } else if (g_strcasecmp(cmd[0], "unfollow") == 0 && cmd[1]) {
    558628                twitter_remove_buddy(ic, cmd[1], NULL);
    559                 g_free(cmds);
    560                 return;
     629                goto eof;
    561630        } else if ((g_strcasecmp(cmd[0], "report") == 0 ||
    562631                    g_strcasecmp(cmd[0], "spam") == 0) && cmd[1]) {
    563                 char * screen_name;
    564                 guint64 id;
    565                 screen_name = cmd[1];
     632                char *screen_name;
     633               
    566634                /* Report nominally works on users but look up the user who
    567635                   posted the given ID if the user wants to do it that way */
    568                 if (g_str_has_prefix(cmd[1], "#") &&
    569                     sscanf(cmd[1] + 1, "%" G_GUINT64_FORMAT, &id) == 1) {
    570                         if (id < TWITTER_LOG_LENGTH && td->log) {
    571                                 if (g_slist_find(ic->bee->users, td->log[id].bu)) {
    572                                         screen_name = td->log[id].bu->handle;
    573                                 }
    574                         }
    575                 }
     636                twitter_message_id_from_command_arg(ic, cmd[1], &bu);
     637                if (bu)
     638                        screen_name = bu->handle;
     639                else
     640                        screen_name = cmd[1];
     641               
    576642                twitter_report_spam(ic, screen_name);
    577                 g_free(cmds);
    578                 return;
     643                goto eof;
    579644        } else if (g_strcasecmp(cmd[0], "rt") == 0 && cmd[1]) {
    580                 guint64 id = twitter_message_id_from_command_arg(ic, td, cmd[1]);
     645                id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);
    581646
    582647                td->last_status_id = 0;
     
    584649                        twitter_status_retweet(ic, id);
    585650                else
    586                         twitter_msg(ic, "User `%s' does not exist or didn't "
     651                        twitter_log(ic, "User `%s' does not exist or didn't "
    587652                                    "post any statuses recently", cmd[1]);
    588653
    589                 g_free(cmds);
    590                 return;
     654                goto eof;
    591655        } else if (g_strcasecmp(cmd[0], "reply") == 0 && cmd[1] && cmd[2]) {
    592                 struct twitter_user_data *tud;
    593                 bee_user_t *bu = NULL;
    594                 guint64 id = 0;
    595 
    596                 if (g_str_has_prefix(cmd[1], "#") &&
    597                     sscanf(cmd[1] + 1, "%" G_GUINT64_FORMAT, &id) == 1 &&
    598                     (id < TWITTER_LOG_LENGTH) && td->log) {
    599                         bu = td->log[id].bu;
    600                         if (g_slist_find(ic->bee->users, bu))
    601                                 id = td->log[id].id;
    602                         else
    603                                 bu = NULL;
    604                 } else if ((bu = bee_user_by_handle(ic->bee, ic, cmd[1])) &&
    605                     (tud = bu->data) && tud->last_id) {
    606                         id = tud->last_id;
    607                 } else if (sscanf(cmd[1], "%" G_GUINT64_FORMAT, &id) == 1 &&
    608                            (id < TWITTER_LOG_LENGTH) && td->log) {
    609                         bu = td->log[id].bu;
    610                         if (g_slist_find(ic->bee->users, bu))
    611                                 id = td->log[id].id;
    612                         else
    613                                 bu = NULL;
    614                 }
    615 
     656                id = twitter_message_id_from_command_arg(ic, cmd[1], &bu);
    616657                if (!id || !bu) {
    617                         twitter_msg(ic, "User `%s' does not exist or didn't "
     658                        twitter_log(ic, "User `%s' does not exist or didn't "
    618659                                    "post any statuses recently", cmd[1]);
    619                         g_free(cmds);
    620                         return;
     660                        goto eof;
    621661                }
    622662                message = new = g_strdup_printf("@%s %s", bu->handle, message + (cmd[2] - cmd[0]));
    623663                in_reply_to = id;
     664                allow_post = TRUE;
    624665        } else if (g_strcasecmp(cmd[0], "post") == 0) {
    625666                message += 5;
    626         }
    627 
    628         {
     667                allow_post = TRUE;
     668        }
     669
     670        if (allow_post) {
    629671                char *s;
    630                 bee_user_t *bu;
    631 
    632                 if (!twitter_length_check(ic, message)) {
    633                         g_free(new);
    634                         g_free(cmds);
    635                         return;
    636                 }
     672
     673                if (!twitter_length_check(ic, message))
     674                        goto eof;
    637675
    638676                s = cmd[0] + strlen(cmd[0]) - 1;
     
    657695                td->last_status_id = 0;
    658696                twitter_post_status(ic, message, in_reply_to);
    659                 g_free(new);
    660         }
     697        } else {
     698                twitter_log(ic, "Unknown command: %s", cmd[0]);
     699        }
     700eof:
     701        g_free(new);
    661702        g_free(cmds);
    662703}
     704
     705void twitter_log(struct im_connection *ic, char *format, ... )
     706{
     707        struct twitter_data *td = ic->proto_data;
     708        va_list params;
     709        char *text;
     710       
     711        va_start(params, format);
     712        text = g_strdup_vprintf(format, params);
     713        va_end(params);
     714       
     715        if (td->timeline_gc)
     716                imcb_chat_log(td->timeline_gc, "%s", text);
     717        else
     718                imcb_log(ic, "%s", text);
     719       
     720        g_free(text);
     721}
     722
    663723
    664724void twitter_initmodule()
  • protocols/twitter/twitter.h

    r92d3044 rcc6fdf8  
    44*  Simple module to facilitate twitter functionality.                       *
    55*                                                                           *
    6 *  Copyright 2009 Geert Mulders <g.c.w.m.mulders@gmail.com>                 *
     6*  Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com>            *
     7*  Copyright 2010-2012 Wilmer van der Gaast <wilmer@gaast.net>              *
    78*                                                                           *
    89*  This library is free software; you can redistribute it and/or            *
     
    3536typedef enum
    3637{
    37         TWITTER_HAVE_FRIENDS = 1,
     38        TWITTER_HAVE_FRIENDS   = 0x00001,
     39        TWITTER_MODE_ONE       = 0x00002,
     40        TWITTER_MODE_MANY      = 0x00004,
     41        TWITTER_MODE_CHAT      = 0x00008,
    3842        TWITTER_DOING_TIMELINE = 0x10000,
    39         TWITTER_GOT_TIMELINE = 0x20000,
    40         TWITTER_GOT_MENTIONS = 0x40000,
     43        TWITTER_GOT_TIMELINE   = 0x20000,
     44        TWITTER_GOT_MENTIONS   = 0x40000,
    4145} twitter_flags_t;
    4246
     
    5761        guint64 last_status_id; /* For undo */
    5862        gint main_loop_id;
     63        struct http_request *stream;
    5964        struct groupchat *timeline_gc;
    6065        gint http_fails;
     
    8085};
    8186
    82 #define TWITTER_LOG_LENGTH 100
     87#define TWITTER_LOG_LENGTH 256
    8388struct twitter_log_data
    8489{
     
    99104char *twitter_parse_error( struct http_request *req );
    100105
     106void twitter_log(struct im_connection *ic, char *format, ... );
     107struct groupchat *twitter_groupchat_init(struct im_connection *ic);
     108
    101109#endif //_TWITTER_H
  • protocols/twitter/twitter_http.c

    r92d3044 rcc6fdf8  
    4747 * This is actually pretty generic function... Perhaps it should move to the lib/http_client.c
    4848 */
    49 void *twitter_http(struct im_connection *ic, char *url_string, http_input_function func,
    50                    gpointer data, int is_post, char **arguments, int arguments_len)
     49struct http_request *twitter_http(struct im_connection *ic, char *url_string, http_input_function func,
     50                                  gpointer data, int is_post, char **arguments, int arguments_len)
    5151{
    5252        struct twitter_data *td = ic->proto_data;
     
    5555        void *ret;
    5656        char *url_arguments;
     57        url_t *base_url = NULL;
    5758
    5859        url_arguments = g_strdup("");
     
    6768                }
    6869        }
     70       
     71        if (strstr(url_string, "://")) {
     72                base_url = g_new0(url_t, 1);
     73                if (!url_set(base_url, url_string)) {
     74                        g_free(base_url);
     75                        return NULL;
     76                }
     77        }
     78       
    6979        // Make the request.
    7080        g_string_printf(request, "%s %s%s%s%s HTTP/1.0\r\n"
     
    7282                        "User-Agent: BitlBee " BITLBEE_VERSION " " ARCH "/" CPU "\r\n",
    7383                        is_post ? "POST" : "GET",
    74                         td->url_path, url_string,
    75                         is_post ? "" : "?", is_post ? "" : url_arguments, td->url_host);
     84                        base_url ? base_url->file : td->url_path,
     85                        base_url ? "" : url_string,
     86                        is_post ? "" : "?", is_post ? "" : url_arguments,
     87                        base_url ? base_url->host : td->url_host);
    7688
    7789        // If a pass and user are given we append them to the request.
     
    8092                char *full_url;
    8193
    82                 full_url = g_strconcat(set_getstr(&ic->acc->set, "base_url"), url_string, NULL);
     94                if (base_url)
     95                        full_url = g_strdup(url_string);
     96                else
     97                        full_url = g_strconcat(set_getstr(&ic->acc->set, "base_url"), url_string, NULL);
    8398                full_header = oauth_http_header(td->oauth_info, is_post ? "POST" : "GET",
    8499                                                full_url, url_arguments);
     
    109124        }
    110125
    111         ret = http_dorequest(td->url_host, td->url_port, td->url_ssl, request->str, func, data);
     126        if (base_url)
     127                ret = http_dorequest(base_url->host, base_url->port, base_url->proto == PROTO_HTTPS, request->str, func, data);
     128        else
     129                ret = http_dorequest(td->url_host, td->url_port, td->url_ssl, request->str, func, data);
    112130
    113131        g_free(url_arguments);
    114132        g_string_free(request, TRUE);
     133        g_free(base_url);
     134        return ret;
     135}
     136
     137struct http_request *twitter_http_f(struct im_connection *ic, char *url_string, http_input_function func,
     138                                    gpointer data, int is_post, char **arguments, int arguments_len, twitter_http_flags_t flags)
     139{
     140        struct http_request *ret = twitter_http(ic, url_string, func, data, is_post, arguments, arguments_len);
     141        if (ret)
     142                ret->flags |= flags;
    115143        return ret;
    116144}
  • protocols/twitter/twitter_http.h

    r92d3044 rcc6fdf8  
    2828#include "http_client.h"
    2929
     30typedef enum {
     31        /* With this set, twitter_http_post() will post a generic confirmation
     32           message to the user. */
     33        TWITTER_HTTP_USER_ACK = 0x1000000,
     34} twitter_http_flags_t;
     35
    3036struct oauth_info;
    3137
    32 void *twitter_http(struct im_connection *ic, char *url_string, http_input_function func,
    33                    gpointer data, int is_post, char** arguments, int arguments_len);
     38struct http_request *twitter_http(struct im_connection *ic, char *url_string, http_input_function func,
     39                                  gpointer data, int is_post, char** arguments, int arguments_len);
     40struct http_request *twitter_http_f(struct im_connection *ic, char *url_string, http_input_function func,
     41                                    gpointer data, int is_post, char** arguments, int arguments_len, twitter_http_flags_t flags);
    3442
    3543#endif //_TWITTER_HTTP_H
  • protocols/twitter/twitter_lib.c

    r92d3044 rcc6fdf8  
    3535#include "misc.h"
    3636#include "base64.h"
    37 #include "xmltree.h"
    3837#include "twitter_lib.h"
     38#include "json_util.h"
    3939#include <ctype.h>
    4040#include <errno.h>
     
    6767        char *text;
    6868        struct twitter_xml_user *user;
    69         guint64 id, reply_to;
     69        guint64 id, rt_id; /* Usually equal, with RTs id == *original* id */
     70        guint64 reply_to;
    7071};
    71 
    72 static void twitter_groupchat_init(struct im_connection *ic);
    7372
    7473/**
     
    148147        // Check if the buddy is already in the buddy list.
    149148        if (!bee_user_by_handle(ic->bee, ic, name)) {
    150                 char *mode = set_getstr(&ic->acc->set, "mode");
    151 
    152149                // The buddy is not in the list, add the buddy and set the status to logged in.
    153150                imcb_add_buddy(ic, name, NULL);
    154151                imcb_rename_buddy(ic, name, fullname);
    155                 if (g_strcasecmp(mode, "chat") == 0) {
     152                if (td->flags & TWITTER_MODE_CHAT) {
    156153                        /* Necessary so that nicks always get translated to the
    157154                           exact Twitter username. */
    158155                        imcb_buddy_nick_hint(ic, name, name);
    159                         imcb_chat_add_buddy(td->timeline_gc, name);
    160                 } else if (g_strcasecmp(mode, "many") == 0)
     156                        if (td->timeline_gc)
     157                                imcb_chat_add_buddy(td->timeline_gc, name);
     158                } else if (td->flags & TWITTER_MODE_MANY)
    161159                        imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
    162160        }
     
    168166{
    169167        static char *ret = NULL;
    170         struct xt_node *root, *node, *err;
     168        json_value *root, *err;
    171169
    172170        g_free(ret);
     
    174172
    175173        if (req->body_size > 0) {
    176                 root = xt_from_string(req->reply_body, req->body_size);
    177                
    178                 for (node = root; node; node = node->next)
    179                         if ((err = xt_find_node(node->children, "error")) && err->text_len > 0) {
    180                                 ret = g_strdup_printf("%s (%s)", req->status_string, err->text);
    181                                 break;
    182                         }
    183 
    184                 xt_free_node(root);
     174                root = json_parse(req->reply_body);
     175                err = json_o_get(root, "errors");
     176                if (err && err->type == json_array && (err = err->u.array.values[0]) &&
     177                    err->type == json_object) {
     178                        const char *msg = json_o_str(err, "message");
     179                        if (msg)
     180                                ret = g_strdup_printf("%s (%s)", req->status_string, msg);
     181                }
     182                json_value_free(root);
    185183        }
    186184
     
    188186}
    189187
    190 static struct xt_node *twitter_parse_response(struct im_connection *ic, struct http_request *req)
     188/* WATCH OUT: This function might or might not destroy your connection.
     189   Sub-optimal indeed, but just be careful when this returns NULL! */
     190static json_value *twitter_parse_response(struct im_connection *ic, struct http_request *req)
    191191{
    192192        gboolean logging_in = !(ic->flags & OPT_LOGGED_IN);
    193193        gboolean periodic;
    194194        struct twitter_data *td = ic->proto_data;
    195         struct xt_node *ret;
     195        json_value *ret;
    196196        char path[64] = "", *s;
    197197       
     
    211211                   throwing 401s so I'll keep treating this one as fatal
    212212                   only during login. */
    213                 imcb_error(ic, "Authentication failure");
     213                imcb_error(ic, "Authentication failure (%s)",
     214                               twitter_parse_error(req));
    214215                imc_logout(ic, FALSE);
    215216                return NULL;
     
    217218                // It didn't go well, output the error and return.
    218219                if (!periodic || logging_in || ++td->http_fails >= 5)
    219                         imcb_error(ic, "Could not retrieve %s: %s",
    220                                    path, twitter_parse_error(req));
     220                        twitter_log(ic, "Error: Could not retrieve %s: %s",
     221                                    path, twitter_parse_error(req));
    221222               
    222223                if (logging_in)
     
    227228        }
    228229
    229         if ((ret = xt_from_string(req->reply_body, req->body_size)) == NULL) {
     230        if ((ret = json_parse(req->reply_body)) == NULL) {
    230231                imcb_error(ic, "Could not retrieve %s: %s",
    231232                           path, "XML parse error");
     
    251252
    252253/**
    253  * Function to help fill a list.
    254  */
    255 static xt_status twitter_xt_next_cursor(struct xt_node *node, struct twitter_xml_list *txl)
    256 {
    257         char *end = NULL;
    258 
    259         if (node->text)
    260                 txl->next_cursor = g_ascii_strtoll(node->text, &end, 10);
    261         if (end == NULL)
    262                 txl->next_cursor = -1;
    263 
    264         return XT_HANDLED;
    265 }
    266 
    267 /**
    268254 * Fill a list of ids.
    269255 */
    270 static xt_status twitter_xt_get_friends_id_list(struct xt_node *node, struct twitter_xml_list *txl)
    271 {
    272         struct xt_node *child;
     256static gboolean twitter_xt_get_friends_id_list(json_value *node, struct twitter_xml_list *txl)
     257{
     258        json_value *c;
     259        int i;
    273260
    274261        // Set the list type.
    275262        txl->type = TXL_ID;
    276263
    277         // The root <statuses> node should hold the list of statuses <status>
    278         // Walk over the nodes children.
    279         for (child = node->children; child; child = child->next) {
    280                 if (g_strcasecmp("ids", child->name) == 0) {
    281                         struct xt_node *idc;
    282                         for (idc = child->children; idc; idc = idc->next)
    283                                 if (g_strcasecmp(idc->name, "id") == 0)
    284                                         txl->list = g_slist_prepend(txl->list,
    285                                                 g_memdup(idc->text, idc->text_len + 1));
    286                 } else if (g_strcasecmp("next_cursor", child->name) == 0) {
    287                         twitter_xt_next_cursor(child, txl);
    288                 }
    289         }
    290 
    291         return XT_HANDLED;
     264        c = json_o_get(node, "ids");
     265        if (!c || c->type != json_array)
     266                return FALSE;
     267
     268        for (i = 0; i < c->u.array.length; i ++) {
     269                if (c->u.array.values[i]->type != json_integer)
     270                        continue;
     271               
     272                txl->list = g_slist_prepend(txl->list,
     273                        g_strdup_printf("%lld", c->u.array.values[i]->u.integer));
     274        }
     275       
     276        c = json_o_get(node, "next_cursor");
     277        if (c && c->type == json_integer)
     278                txl->next_cursor = c->u.integer;
     279        else
     280                txl->next_cursor = -1;
     281       
     282        return TRUE;
    292283}
    293284
     
    300291{
    301292        struct im_connection *ic;
    302         struct xt_node *parsed;
     293        json_value *parsed;
    303294        struct twitter_xml_list *txl;
    304295        struct twitter_data *td;
     
    312303        td = ic->proto_data;
    313304
    314         /* Create the room now that we "logged in". */
    315         if (!td->timeline_gc && g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "chat") == 0)
    316                 twitter_groupchat_init(ic);
    317 
    318305        txl = g_new0(struct twitter_xml_list, 1);
    319306        txl->list = td->follow_ids;
     
    322309        if (!(parsed = twitter_parse_response(ic, req)))
    323310                return;
     311       
    324312        twitter_xt_get_friends_id_list(parsed, txl);
    325         xt_free_node(parsed);
     313        json_value_free(parsed);
    326314
    327315        td->follow_ids = txl->list;
     
    338326}
    339327
    340 static xt_status twitter_xt_get_users(struct xt_node *node, struct twitter_xml_list *txl);
     328static gboolean twitter_xt_get_users(json_value *node, struct twitter_xml_list *txl);
    341329static void twitter_http_get_users_lookup(struct http_request *req);
    342330
     
    379367{
    380368        struct im_connection *ic = req->data;
    381         struct xt_node *parsed;
     369        json_value *parsed;
    382370        struct twitter_xml_list *txl;
    383371        GSList *l = NULL;
     
    395383                return;
    396384        twitter_xt_get_users(parsed, txl);
    397         xt_free_node(parsed);
     385        json_value_free(parsed);
    398386
    399387        // Add the users as buddies.
     
    409397}
    410398
    411 /**
    412  * Function to fill a twitter_xml_user struct.
    413  * It sets:
    414  *  - the name and
    415  *  - the screen_name.
    416  */
    417 static xt_status twitter_xt_get_user(struct xt_node *node, struct twitter_xml_user *txu)
    418 {
    419         struct xt_node *child;
    420 
    421         // Walk over the nodes children.
    422         for (child = node->children; child; child = child->next) {
    423                 if (g_strcasecmp("name", child->name) == 0) {
    424                         txu->name = g_memdup(child->text, child->text_len + 1);
    425                 } else if (g_strcasecmp("screen_name", child->name) == 0) {
    426                         txu->screen_name = g_memdup(child->text, child->text_len + 1);
    427                 }
    428         }
    429         return XT_HANDLED;
     399struct twitter_xml_user *twitter_xt_get_user(const json_value *node)
     400{
     401        struct twitter_xml_user *txu;
     402       
     403        txu = g_new0(struct twitter_xml_user, 1);
     404        txu->name = g_strdup(json_o_str(node, "name"));
     405        txu->screen_name = g_strdup(json_o_str(node, "screen_name"));
     406       
     407        return txu;
    430408}
    431409
     
    435413 *  - all <user>s from the <users> element.
    436414 */
    437 static xt_status twitter_xt_get_users(struct xt_node *node, struct twitter_xml_list *txl)
     415static gboolean twitter_xt_get_users(json_value *node, struct twitter_xml_list *txl)
    438416{
    439417        struct twitter_xml_user *txu;
    440         struct xt_node *child;
     418        int i;
    441419
    442420        // Set the type of the list.
    443421        txl->type = TXL_USER;
    444422
     423        if (!node || node->type != json_array)
     424                return FALSE;
     425
    445426        // The root <users> node should hold the list of users <user>
    446427        // Walk over the nodes children.
    447         for (child = node->children; child; child = child->next) {
    448                 if (g_strcasecmp("user", child->name) == 0) {
    449                         txu = g_new0(struct twitter_xml_user, 1);
    450                         twitter_xt_get_user(child, txu);
    451                         // Put the item in the front of the list.
     428        for (i = 0; i < node->u.array.length; i ++) {
     429                txu = twitter_xt_get_user(node->u.array.values[i]);
     430                if (txu)
    452431                        txl->list = g_slist_prepend(txl->list, txu);
    453                 }
    454         }
    455 
    456         return XT_HANDLED;
     432        }
     433
     434        return TRUE;
    457435}
    458436
     
    462440#define TWITTER_TIME_FORMAT "%a %b %d %H:%M:%S +0000 %Y"
    463441#endif
     442
     443static char* expand_entities(char* text, const json_value *entities);
    464444
    465445/**
     
    471451 *  - the user in a twitter_xml_user struct.
    472452 */
    473 static xt_status twitter_xt_get_status(struct xt_node *node, struct twitter_xml_status *txs)
    474 {
    475         struct xt_node *child, *rt = NULL;
    476 
    477         // Walk over the nodes children.
    478         for (child = node->children; child; child = child->next) {
    479                 if (g_strcasecmp("text", child->name) == 0) {
    480                         txs->text = g_memdup(child->text, child->text_len + 1);
    481                 } else if (g_strcasecmp("retweeted_status", child->name) == 0) {
    482                         rt = child;
    483                 } else if (g_strcasecmp("created_at", child->name) == 0) {
     453static struct twitter_xml_status *twitter_xt_get_status(const json_value *node)
     454{
     455        struct twitter_xml_status *txs;
     456        const json_value *rt = NULL, *entities = NULL;
     457       
     458        if (node->type != json_object)
     459                return FALSE;
     460        txs = g_new0(struct twitter_xml_status, 1);
     461
     462        JSON_O_FOREACH (node, k, v) {
     463                if (strcmp("text", k) == 0 && v->type == json_string) {
     464                        txs->text = g_memdup(v->u.string.ptr, v->u.string.length + 1);
     465                        strip_html(txs->text);
     466                } else if (strcmp("retweeted_status", k) == 0 && v->type == json_object) {
     467                        rt = v;
     468                } else if (strcmp("created_at", k) == 0 && v->type == json_string) {
    484469                        struct tm parsed;
    485470
     
    487472                           this field. :-( Also assumes the timezone used
    488473                           is UTC since C time handling functions suck. */
    489                         if (strptime(child->text, TWITTER_TIME_FORMAT, &parsed) != NULL)
     474                        if (strptime(v->u.string.ptr, TWITTER_TIME_FORMAT, &parsed) != NULL)
    490475                                txs->created_at = mktime_utc(&parsed);
    491                 } else if (g_strcasecmp("user", child->name) == 0) {
    492                         txs->user = g_new0(struct twitter_xml_user, 1);
    493                         twitter_xt_get_user(child, txs->user);
    494                 } else if (g_strcasecmp("id", child->name) == 0) {
    495                         txs->id = g_ascii_strtoull(child->text, NULL, 10);
    496                 } else if (g_strcasecmp("in_reply_to_status_id", child->name) == 0) {
    497                         txs->reply_to = g_ascii_strtoull(child->text, NULL, 10);
     476                } else if (strcmp("user", k) == 0 && v->type == json_object) {
     477                        txs->user = twitter_xt_get_user(v);
     478                } else if (strcmp("id", k) == 0 && v->type == json_integer) {
     479                        txs->rt_id = txs->id = v->u.integer;
     480                } else if (strcmp("in_reply_to_status_id", k) == 0 && v->type == json_integer) {
     481                        txs->reply_to = v->u.integer;
     482                } else if (strcmp("entities", k) == 0 && v->type == json_object) {
     483                        entities = v;
    498484                }
    499485        }
     
    502488           wasn't truncated because it may be lying. */
    503489        if (rt) {
    504                 struct twitter_xml_status *rtxs = g_new0(struct twitter_xml_status, 1);
    505                 if (twitter_xt_get_status(rt, rtxs) != XT_HANDLED) {
     490                struct twitter_xml_status *rtxs = twitter_xt_get_status(rt);
     491                if (rtxs) {
     492                        g_free(txs->text);
     493                        txs->text = g_strdup_printf("RT @%s: %s", rtxs->user->screen_name, rtxs->text);
     494                        txs->id = rtxs->id;
    506495                        txs_free(rtxs);
    507                         return XT_HANDLED;
    508                 }
    509 
    510                 g_free(txs->text);
    511                 txs->text = g_strdup_printf("RT @%s: %s", rtxs->user->screen_name, rtxs->text);
    512                 txs_free(rtxs);
    513         } else {
    514                 struct xt_node *urls, *url;
     496                }
     497        } else if (entities) {
     498                txs->text = expand_entities(txs->text, entities);
     499        }
     500
     501        if (txs->text && txs->user && txs->id)
     502                return txs;
     503       
     504        txs_free(txs);
     505        return NULL;
     506}
     507
     508/**
     509 * Function to fill a twitter_xml_status struct (DM variant).
     510 */
     511static struct twitter_xml_status *twitter_xt_get_dm(const json_value *node)
     512{
     513        struct twitter_xml_status *txs;
     514        const json_value *entities = NULL;
     515       
     516        if (node->type != json_object)
     517                return FALSE;
     518        txs = g_new0(struct twitter_xml_status, 1);
     519
     520        JSON_O_FOREACH (node, k, v) {
     521                if (strcmp("text", k) == 0 && v->type == json_string) {
     522                        txs->text = g_memdup(v->u.string.ptr, v->u.string.length + 1);
     523                        strip_html(txs->text);
     524                } else if (strcmp("created_at", k) == 0 && v->type == json_string) {
     525                        struct tm parsed;
     526
     527                        /* Very sensitive to changes to the formatting of
     528                           this field. :-( Also assumes the timezone used
     529                           is UTC since C time handling functions suck. */
     530                        if (strptime(v->u.string.ptr, TWITTER_TIME_FORMAT, &parsed) != NULL)
     531                                txs->created_at = mktime_utc(&parsed);
     532                } else if (strcmp("sender", k) == 0 && v->type == json_object) {
     533                        txs->user = twitter_xt_get_user(v);
     534                } else if (strcmp("id", k) == 0 && v->type == json_integer) {
     535                        txs->id = v->u.integer;
     536                }
     537        }
     538
     539        if (entities) {
     540                txs->text = expand_entities(txs->text, entities);
     541        }
     542
     543        if (txs->text && txs->user && txs->id)
     544                return txs;
     545       
     546        txs_free(txs);
     547        return NULL;
     548}
     549
     550static char* expand_entities(char* text, const json_value *entities) {
     551        JSON_O_FOREACH (entities, k, v) {
     552                int i;
    515553               
    516                 urls = xt_find_path(node, "entities");
    517                 if (urls != NULL)
    518                         urls = urls->children;
    519                 for (; urls; urls = urls->next) {
    520                         if (strcmp(urls->name, "urls") != 0 && strcmp(urls->name, "media") != 0)
     554                if (v->type != json_array)
     555                        continue;
     556                if (strcmp(k, "urls") != 0 && strcmp(k, "media") != 0)
     557                        continue;
     558               
     559                for (i = 0; i < v->u.array.length; i ++) {
     560                        if (v->u.array.values[i]->type != json_object)
    521561                                continue;
    522562                       
    523                         for (url = urls ? urls->children : NULL; url; url = url->next) {
    524                                 /* "short" is a reserved word. :-P */
    525                                 struct xt_node *kort = xt_find_node(url->children, "url");
    526                                 struct xt_node *disp = xt_find_node(url->children, "display_url");
    527                                 char *pos, *new;
    528                                
    529                                 if (!kort || !kort->text || !disp || !disp->text ||
    530                                     !(pos = strstr(txs->text, kort->text)))
    531                                         continue;
    532                                
    533                                 *pos = '\0';
    534                                 new = g_strdup_printf("%s%s &lt;%s&gt;%s", txs->text, kort->text,
    535                                                       disp->text, pos + strlen(kort->text));
    536                                
    537                                 g_free(txs->text);
    538                                 txs->text = new;
    539                         }
    540                 }
    541         }
    542 
    543         return XT_HANDLED;
     563                        const char *kort = json_o_str(v->u.array.values[i], "url");
     564                        const char *disp = json_o_str(v->u.array.values[i], "display_url");
     565                        char *pos, *new;
     566                       
     567                        if (!kort || !disp || !(pos = strstr(text, kort)))
     568                                continue;
     569                       
     570                        *pos = '\0';
     571                        new = g_strdup_printf("%s%s <%s>%s", text, kort,
     572                                              disp, pos + strlen(kort));
     573                       
     574                        g_free(text);
     575                        text = new;
     576                }
     577        }
     578       
     579        return text;
    544580}
    545581
     
    550586 *  - the next_cursor.
    551587 */
    552 static xt_status twitter_xt_get_status_list(struct im_connection *ic, struct xt_node *node,
    553                                             struct twitter_xml_list *txl)
     588static gboolean twitter_xt_get_status_list(struct im_connection *ic, const json_value *node,
     589                                           struct twitter_xml_list *txl)
    554590{
    555591        struct twitter_xml_status *txs;
    556         struct xt_node *child;
    557         bee_user_t *bu;
     592        int i;
    558593
    559594        // Set the type of the list.
    560595        txl->type = TXL_STATUS;
     596       
     597        if (node->type != json_array)
     598                return FALSE;
    561599
    562600        // The root <statuses> node should hold the list of statuses <status>
    563601        // Walk over the nodes children.
    564         for (child = node->children; child; child = child->next) {
    565                 if (g_strcasecmp("status", child->name) == 0) {
    566                         txs = g_new0(struct twitter_xml_status, 1);
    567                         twitter_xt_get_status(child, txs);
    568                         // Put the item in the front of the list.
    569                         txl->list = g_slist_prepend(txl->list, txs);
    570 
    571                         if (txs->user && txs->user->screen_name &&
    572                             (bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) {
    573                                 struct twitter_user_data *tud = bu->data;
    574 
    575                                 if (txs->id > tud->last_id) {
    576                                         tud->last_id = txs->id;
    577                                         tud->last_time = txs->created_at;
    578                                 }
    579                         }
    580                 } else if (g_strcasecmp("next_cursor", child->name) == 0) {
    581                         twitter_xt_next_cursor(child, txl);
    582                 }
    583         }
    584 
    585         return XT_HANDLED;
    586 }
    587 
     602        for (i = 0; i < node->u.array.length; i ++) {
     603                txs = twitter_xt_get_status(node->u.array.values[i]);
     604                if (!txs)
     605                        continue;
     606               
     607                txl->list = g_slist_prepend(txl->list, txs);
     608        }
     609
     610        return TRUE;
     611}
     612
     613/* Will log messages either way. Need to keep track of IDs for stream deduping.
     614   Plus, show_ids is on by default and I don't see why anyone would disable it. */
    588615static char *twitter_msg_add_id(struct im_connection *ic,
    589616                                struct twitter_xml_status *txs, const char *prefix)
    590617{
    591618        struct twitter_data *td = ic->proto_data;
    592         char *ret = NULL;
    593 
    594         if (!set_getbool(&ic->acc->set, "show_ids")) {
     619        int reply_to = -1;
     620        bee_user_t *bu;
     621
     622        if (txs->reply_to) {
     623                int i;
     624                for (i = 0; i < TWITTER_LOG_LENGTH; i++)
     625                        if (td->log[i].id == txs->reply_to) {
     626                                reply_to = i;
     627                                break;
     628                        }
     629        }
     630
     631        if (txs->user && txs->user->screen_name &&
     632            (bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) {
     633                struct twitter_user_data *tud = bu->data;
     634
     635                if (txs->id > tud->last_id) {
     636                        tud->last_id = txs->id;
     637                        tud->last_time = txs->created_at;
     638                }
     639        }
     640       
     641        td->log_id = (td->log_id + 1) % TWITTER_LOG_LENGTH;
     642        td->log[td->log_id].id = txs->id;
     643        td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name);
     644       
     645        /* This is all getting hairy. :-( If we RT'ed something ourselves,
     646           remember OUR id instead so undo will work. In other cases, the
     647           original tweet's id should be remembered for deduplicating. */
     648        if (strcmp(txs->user->screen_name, td->user) == 0)
     649                td->log[td->log_id].id = txs->rt_id;
     650       
     651        if (set_getbool(&ic->acc->set, "show_ids")) {
     652                if (reply_to != -1)
     653                        return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s",
     654                                               td->log_id, reply_to, prefix, txs->text);
     655                else
     656                        return g_strdup_printf("\002[\002%02x\002]\002 %s%s",
     657                                               td->log_id, prefix, txs->text);
     658        } else {
    595659                if (*prefix)
    596660                        return g_strconcat(prefix, txs->text, NULL);
     
    598662                        return NULL;
    599663        }
    600 
    601         td->log[td->log_id].id = txs->id;
    602         td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name);
    603         if (txs->reply_to) {
    604                 int i;
    605                 for (i = 0; i < TWITTER_LOG_LENGTH; i++)
    606                         if (td->log[i].id == txs->reply_to) {
    607                                 ret = g_strdup_printf("\002[\002%02d->%02d\002]\002 %s%s",
    608                                                       td->log_id, i, prefix, txs->text);
    609                                 break;
    610                         }
    611         }
    612         if (ret == NULL)
    613                 ret = g_strdup_printf("\002[\002%02d\002]\002 %s%s", td->log_id, prefix, txs->text);
    614         td->log_id = (td->log_id + 1) % TWITTER_LOG_LENGTH;
    615 
    616         return ret;
    617 }
    618 
    619 static void twitter_groupchat_init(struct im_connection *ic)
    620 {
    621         char *name_hint;
     664}
     665
     666/**
     667 * Function that is called to see the statuses in a groupchat window.
     668 */
     669static void twitter_status_show_chat(struct im_connection *ic, struct twitter_xml_status *status)
     670{
     671        struct twitter_data *td = ic->proto_data;
    622672        struct groupchat *gc;
    623         struct twitter_data *td = ic->proto_data;
    624         GSList *l;
    625 
    626         td->timeline_gc = gc = imcb_chat_new(ic, "twitter/timeline");
    627 
    628         name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user);
    629         imcb_chat_name_hint(gc, name_hint);
    630         g_free(name_hint);
    631 
    632         for (l = ic->bee->users; l; l = l->next) {
    633                 bee_user_t *bu = l->data;
    634                 if (bu->ic == ic)
    635                         imcb_chat_add_buddy(td->timeline_gc, bu->handle);
    636         }
    637 }
    638 
    639 /**
    640  * Function that is called to see the statuses in a groupchat window.
    641  */
    642 static void twitter_groupchat(struct im_connection *ic, GSList * list)
    643 {
    644         struct twitter_data *td = ic->proto_data;
    645         GSList *l = NULL;
    646         struct twitter_xml_status *status;
    647         struct groupchat *gc;
    648         guint64 last_id = 0;
     673        gboolean me = g_strcasecmp(td->user, status->user->screen_name) == 0;
     674        char *msg;
    649675
    650676        // Create a new groupchat if it does not exsist.
    651         if (!td->timeline_gc)
    652                 twitter_groupchat_init(ic);
    653 
    654         gc = td->timeline_gc;
    655         if (!gc->joined)
    656                 imcb_chat_add_buddy(gc, ic->acc->user);
    657 
    658         for (l = list; l; l = g_slist_next(l)) {
    659                 char *msg;
    660 
    661                 status = l->data;
    662                 if (status->user == NULL || status->text == NULL || last_id == status->id)
    663                         continue;
    664 
    665                 last_id = status->id;
    666 
    667                 strip_html(status->text);
    668 
    669                 if (set_getbool(&ic->acc->set, "strip_newlines"))
    670                         strip_newlines(status->text);
    671 
    672                 msg = twitter_msg_add_id(ic, status, "");
    673 
    674                 // Say it!
    675                 if (g_strcasecmp(td->user, status->user->screen_name) == 0) {
    676                         imcb_chat_log(gc, "You: %s", msg ? msg : status->text);
    677                 } else {
    678                         twitter_add_buddy(ic, status->user->screen_name, status->user->name);
    679 
    680                         imcb_chat_msg(gc, status->user->screen_name,
    681                                       msg ? msg : status->text, 0, status->created_at);
    682                 }
    683 
    684                 g_free(msg);
    685 
    686                 // Update the timeline_id to hold the highest id, so that by the next request
    687                 // we won't pick up the updates already in the list.
    688                 td->timeline_id = MAX(td->timeline_id, status->id);
    689         }
     677        gc = twitter_groupchat_init(ic);
     678
     679        if (!me)
     680                /* MUST be done before twitter_msg_add_id() to avoid #872. */
     681                twitter_add_buddy(ic, status->user->screen_name, status->user->name);
     682        msg = twitter_msg_add_id(ic, status, "");
     683       
     684        // Say it!
     685        if (me) {
     686                imcb_chat_log(gc, "You: %s", msg ? msg : status->text);
     687        } else {
     688                imcb_chat_msg(gc, status->user->screen_name,
     689                              msg ? msg : status->text, 0, status->created_at);
     690        }
     691
     692        g_free(msg);
    690693}
    691694
     
    693696 * Function that is called to see statuses as private messages.
    694697 */
    695 static void twitter_private_message_chat(struct im_connection *ic, GSList * list)
    696 {
    697         struct twitter_data *td = ic->proto_data;
    698         GSList *l = NULL;
    699         struct twitter_xml_status *status;
    700         char from[MAX_STRING];
    701         gboolean mode_one;
    702         guint64 last_id = 0;
    703 
    704         mode_one = g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "one") == 0;
    705 
    706         if (mode_one) {
     698static void twitter_status_show_msg(struct im_connection *ic, struct twitter_xml_status *status)
     699{
     700        struct twitter_data *td = ic->proto_data;
     701        char from[MAX_STRING] = "";
     702        char *prefix = NULL, *text = NULL;
     703        gboolean me = g_strcasecmp(td->user, status->user->screen_name) == 0;
     704
     705        if (td->flags & TWITTER_MODE_ONE) {
    707706                g_snprintf(from, sizeof(from) - 1, "%s_%s", td->prefix, ic->acc->user);
    708707                from[MAX_STRING - 1] = '\0';
    709708        }
    710709
    711         for (l = list; l; l = g_slist_next(l)) {
    712                 char *prefix = NULL, *text = NULL;
    713 
    714                 status = l->data;
    715                 if (status->user == NULL || status->text == NULL || last_id == status->id)
    716                         continue;
    717 
    718                 last_id = status->id;
    719 
    720                 strip_html(status->text);
    721                 if (mode_one)
    722                         prefix = g_strdup_printf("\002<\002%s\002>\002 ",
    723                                                  status->user->screen_name);
    724                 else
    725                         twitter_add_buddy(ic, status->user->screen_name, status->user->name);
    726 
    727                 text = twitter_msg_add_id(ic, status, prefix ? prefix : "");
    728 
    729                 imcb_buddy_msg(ic,
    730                                mode_one ? from : status->user->screen_name,
    731                                text ? text : status->text, 0, status->created_at);
    732 
    733                 // Update the timeline_id to hold the highest id, so that by the next request
    734                 // we won't pick up the updates already in the list.
    735                 td->timeline_id = MAX(td->timeline_id, status->id);
    736 
    737                 g_free(text);
    738                 g_free(prefix);
    739         }
    740 }
    741 
    742 static void twitter_http_get_home_timeline(struct http_request *req);
    743 static void twitter_http_get_mentions(struct http_request *req);
     710        if (td->flags & TWITTER_MODE_ONE)
     711                prefix = g_strdup_printf("\002<\002%s\002>\002 ",
     712                                         status->user->screen_name);
     713        else if (!me)
     714                twitter_add_buddy(ic, status->user->screen_name, status->user->name);
     715        else
     716                prefix = g_strdup("You: ");
     717
     718        text = twitter_msg_add_id(ic, status, prefix ? prefix : "");
     719
     720        imcb_buddy_msg(ic,
     721                       *from ? from : status->user->screen_name,
     722                       text ? text : status->text, 0, status->created_at);
     723
     724        g_free(text);
     725        g_free(prefix);
     726}
     727
     728static void twitter_status_show(struct im_connection *ic, struct twitter_xml_status *status)
     729{
     730        struct twitter_data *td = ic->proto_data;
     731       
     732        if (status->user == NULL || status->text == NULL)
     733                return;
     734       
     735        /* Grrrr. Would like to do this during parsing, but can't access
     736           settings from there. */
     737        if (set_getbool(&ic->acc->set, "strip_newlines"))
     738                strip_newlines(status->text);
     739       
     740        if (td->flags & TWITTER_MODE_CHAT)
     741                twitter_status_show_chat(ic, status);
     742        else
     743                twitter_status_show_msg(ic, status);
     744
     745        // Update the timeline_id to hold the highest id, so that by the next request
     746        // we won't pick up the updates already in the list.
     747        td->timeline_id = MAX(td->timeline_id, status->rt_id);
     748}
     749
     750static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o);
     751
     752static void twitter_http_stream(struct http_request *req)
     753{
     754        struct im_connection *ic = req->data;
     755        struct twitter_data *td;
     756        json_value *parsed;
     757        int len = 0;
     758        char c, *nl;
     759       
     760        if (!g_slist_find(twitter_connections, ic))
     761                return;
     762       
     763        ic->flags |= OPT_PONGED;
     764        td = ic->proto_data;
     765       
     766        if ((req->flags & HTTPC_EOF) || !req->reply_body) {
     767                td->stream = NULL;
     768                imcb_error(ic, "Stream closed (%s)", req->status_string);
     769                imc_logout(ic, TRUE);
     770                return;
     771        }
     772       
     773        printf( "%d bytes in stream\n", req->body_size );
     774       
     775        /* MUST search for CRLF, not just LF:
     776           https://dev.twitter.com/docs/streaming-apis/processing#Parsing_responses */
     777        nl = strstr(req->reply_body, "\r\n");
     778       
     779        if (!nl) {
     780                printf("Incomplete data\n");
     781                return;
     782        }
     783       
     784        len = nl - req->reply_body;
     785        if (len > 0) {
     786                c = req->reply_body[len];
     787                req->reply_body[len] = '\0';
     788               
     789                printf("JSON: %s\n", req->reply_body);
     790                printf("parsed: %p\n", (parsed = json_parse(req->reply_body)));
     791                if (parsed) {
     792                        twitter_stream_handle_object(ic, parsed);
     793                }
     794                json_value_free(parsed);
     795                req->reply_body[len] = c;
     796        }
     797       
     798        http_flush_bytes(req, len + 2);
     799       
     800        /* One notification might bring multiple events! */
     801        if (req->body_size > 0)
     802                twitter_http_stream(req);
     803}
     804
     805static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o);
     806static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs);
     807
     808static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o)
     809{
     810        struct twitter_data *td = ic->proto_data;
     811        struct twitter_xml_status *txs;
     812        json_value *c;
     813       
     814        if ((txs = twitter_xt_get_status(o))) {
     815                gboolean ret = twitter_stream_handle_status(ic, txs);
     816                txs_free(txs);
     817                return ret;
     818        } else if ((c = json_o_get(o, "direct_message")) &&
     819                   (txs = twitter_xt_get_dm(c))) {
     820                if (strcmp(txs->user->screen_name, td->user) != 0)
     821                        imcb_buddy_msg(ic, txs->user->screen_name,
     822                                       txs->text, 0, txs->created_at);
     823                txs_free(txs);
     824                return TRUE;
     825        } else if ((c = json_o_get(o, "event")) && c->type == json_string) {
     826                twitter_stream_handle_event(ic, o);
     827                return TRUE;
     828        } else if ((c = json_o_get(o, "disconnect")) && c->type == json_object) {
     829                /* HACK: Because we're inside an event handler, we can't just
     830                   disconnect here. Instead, just change the HTTP status string
     831                   into a Twitter status string. */
     832                char *reason = json_o_strdup(c, "reason");
     833                if (reason) {
     834                        g_free(td->stream->status_string);
     835                        td->stream->status_string = reason;
     836                }
     837                return TRUE;
     838        }
     839        return FALSE;
     840}
     841
     842static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs)
     843{
     844        struct twitter_data *td = ic->proto_data;
     845        int i;
     846       
     847        for (i = 0; i < TWITTER_LOG_LENGTH; i++) {
     848                if (td->log[i].id == txs->id) {
     849                        /* Got a duplicate (RT, probably). Drop it. */
     850                        return TRUE;
     851                }
     852        }
     853       
     854        if (!(strcmp(txs->user->screen_name, td->user) == 0 ||
     855              set_getbool(&ic->acc->set, "fetch_mentions") ||
     856              bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) {
     857                /* Tweet is from an unknown person and the user does not want
     858                   to see @mentions, so drop it. twitter_stream_handle_event()
     859                   picks up new follows so this simple filter should be safe. */
     860                /* TODO: The streaming API seems to do poor @mention matching.
     861                   I.e. I'm getting mentions for @WilmerSomething, not just for
     862                   @Wilmer. But meh. You want spam, you get spam. */
     863                return TRUE;
     864        }
     865       
     866        twitter_status_show(ic, txs);
     867       
     868        return TRUE;
     869}
     870
     871static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o)
     872{
     873        struct twitter_data *td = ic->proto_data;
     874        json_value *source = json_o_get(o, "source");
     875        json_value *target = json_o_get(o, "target");
     876        const char *type = json_o_str(o, "event");
     877       
     878        if (!type || !source || source->type != json_object
     879                  || !target || target->type != json_object) {
     880                return FALSE;
     881        }
     882       
     883        if (strcmp(type, "follow") == 0) {
     884                struct twitter_xml_user *us = twitter_xt_get_user(source);
     885                struct twitter_xml_user *ut = twitter_xt_get_user(target);
     886                if (strcmp(us->screen_name, td->user) == 0) {
     887                        twitter_add_buddy(ic, ut->screen_name, ut->name);
     888                }
     889                txu_free(us);
     890                txu_free(ut);
     891        }
     892       
     893        return TRUE;
     894}
     895
     896gboolean twitter_open_stream(struct im_connection *ic)
     897{
     898        struct twitter_data *td = ic->proto_data;
     899        char *args[2] = {"with", "followings"};
     900       
     901        if ((td->stream = twitter_http(ic, TWITTER_USER_STREAM_URL,
     902                                       twitter_http_stream, ic, 0, args, 2))) {
     903                /* This flag must be enabled or we'll get no data until EOF
     904                   (which err, kind of, defeats the purpose of a streaming API). */
     905                td->stream->flags |= HTTPC_STREAMING;
     906                return TRUE;
     907        }
     908       
     909        return FALSE;
     910}
     911
     912static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor);
     913static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor);
    744914
    745915/**
     
    778948        struct twitter_xml_list *home_timeline = td->home_timeline_obj;
    779949        struct twitter_xml_list *mentions = td->mentions_obj;
     950        guint64 last_id = 0;
    780951        GSList *output = NULL;
    781952        GSList *l;
    782953
     954        imcb_connected(ic);
     955       
    783956        if (!(td->flags & TWITTER_GOT_TIMELINE)) {
    784957                return;
     
    804977                }
    805978        }
    806        
    807         if (!(ic->flags & OPT_LOGGED_IN))
    808                 imcb_connected(ic);
    809979
    810980        // See if the user wants to see the messages in a groupchat window or as private messages.
    811         if (g_strcasecmp(set_getstr(&ic->acc->set, "mode"), "chat") == 0)
    812                 twitter_groupchat(ic, output);
    813         else
    814                 twitter_private_message_chat(ic, output);
    815 
    816         g_slist_free(output);
     981        while (output) {
     982                struct twitter_xml_status *txs = output->data;
     983                if (txs->id != last_id)
     984                        twitter_status_show(ic, txs);
     985                last_id = txs->id;
     986                output = g_slist_remove(output, txs);
     987        }
    817988
    818989        txl_free(home_timeline);
     
    823994}
    824995
     996static void twitter_http_get_home_timeline(struct http_request *req);
     997static void twitter_http_get_mentions(struct http_request *req);
     998
    825999/**
    8261000 * Get the timeline.
    8271001 */
    828 void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor)
     1002static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor)
    8291003{
    8301004        struct twitter_data *td = ic->proto_data;
     
    8621036 * Get mentions.
    8631037 */
    864 void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor)
     1038static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor)
    8651039{
    8661040        struct twitter_data *td = ic->proto_data;
     
    8931067
    8941068        g_free(args[1]);
    895         if (td->timeline_id) {
    896                 g_free(args[5]);
    897         }
     1069        g_free(args[5]);
    8981070}
    8991071
     
    9051077        struct im_connection *ic = req->data;
    9061078        struct twitter_data *td;
    907         struct xt_node *parsed;
     1079        json_value *parsed;
    9081080        struct twitter_xml_list *txl;
    9091081
     
    9211093                goto end;
    9221094        twitter_xt_get_status_list(ic, parsed, txl);
    923         xt_free_node(parsed);
     1095        json_value_free(parsed);
    9241096
    9251097        td->home_timeline_obj = txl;
    9261098
    9271099      end:
     1100        if (!g_slist_find(twitter_connections, ic))
     1101                return;
     1102
    9281103        td->flags |= TWITTER_GOT_TIMELINE;
    9291104
     
    9381113        struct im_connection *ic = req->data;
    9391114        struct twitter_data *td;
    940         struct xt_node *parsed;
     1115        json_value *parsed;
    9411116        struct twitter_xml_list *txl;
    9421117
     
    9541129                goto end;
    9551130        twitter_xt_get_status_list(ic, parsed, txl);
    956         xt_free_node(parsed);
     1131        json_value_free(parsed);
    9571132
    9581133        td->mentions_obj = txl;
    9591134
    9601135      end:
     1136        if (!g_slist_find(twitter_connections, ic))
     1137                return;
     1138
    9611139        td->flags |= TWITTER_GOT_MENTIONS;
    9621140
     
    9721150        struct im_connection *ic = req->data;
    9731151        struct twitter_data *td;
    974         struct xt_node *parsed, *node;
     1152        json_value *parsed, *id;
    9751153
    9761154        // Check if the connection is still active.
     
    9841162                return;
    9851163       
    986         if ((node = xt_find_node(parsed, "status")) &&
    987             (node = xt_find_node(node->children, "id")) && node->text)
    988                 td->last_status_id = g_ascii_strtoull(node->text, NULL, 10);
     1164        if ((id = json_o_get(parsed, "id")) && id->type == json_integer) {
     1165                td->last_status_id = id->u.integer;
     1166        }
     1167       
     1168        json_value_free(parsed);
     1169       
     1170        if (req->flags & TWITTER_HTTP_USER_ACK)
     1171                twitter_log(ic, "Command processed successfully");
    9891172}
    9901173
     
    10321215        char *url;
    10331216        url = g_strdup_printf("%s%llu%s", TWITTER_STATUS_DESTROY_URL,
    1034                               (unsigned long long) id, ".xml");
    1035         twitter_http(ic, url, twitter_http_post, ic, 1, NULL, 0);
     1217                              (unsigned long long) id, ".json");
     1218        twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0,
     1219                       TWITTER_HTTP_USER_ACK);
    10361220        g_free(url);
    10371221}
     
    10411225        char *url;
    10421226        url = g_strdup_printf("%s%llu%s", TWITTER_STATUS_RETWEET_URL,
    1043                               (unsigned long long) id, ".xml");
    1044         twitter_http(ic, url, twitter_http_post, ic, 1, NULL, 0);
     1227                              (unsigned long long) id, ".json");
     1228        twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0,
     1229                       TWITTER_HTTP_USER_ACK);
    10451230        g_free(url);
    10461231}
     
    10561241        };
    10571242        args[1] = screen_name;
    1058         twitter_http(ic, TWITTER_REPORT_SPAM_URL, twitter_http_post,
    1059                      ic, 1, args, 2);
     1243        twitter_http_f(ic, TWITTER_REPORT_SPAM_URL, twitter_http_post,
     1244                       ic, 1, args, 2, TWITTER_HTTP_USER_ACK);
    10601245}
    10611246
     
    10671252        char *url;
    10681253        url = g_strdup_printf("%s%llu%s", TWITTER_FAVORITE_CREATE_URL,
    1069                               (unsigned long long) id, ".xml");
    1070         twitter_http(ic, url, twitter_http_post, ic, 1, NULL, 0);
     1254                              (unsigned long long) id, ".json");
     1255        twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0,
     1256                       TWITTER_HTTP_USER_ACK);
    10711257        g_free(url);
    10721258}
  • protocols/twitter/twitter_lib.h

    r92d3044 rcc6fdf8  
    2929#include "twitter_http.h"
    3030
    31 #define TWITTER_API_URL "http://api.twitter.com/1"
     31#define TWITTER_API_URL "http://api.twitter.com/1.1"
    3232#define IDENTICA_API_URL "https://identi.ca/api"
    3333
    3434/* Status URLs */
    35 #define TWITTER_STATUS_UPDATE_URL "/statuses/update.xml"
     35#define TWITTER_STATUS_UPDATE_URL "/statuses/update.json"
    3636#define TWITTER_STATUS_SHOW_URL "/statuses/show/"
    3737#define TWITTER_STATUS_DESTROY_URL "/statuses/destroy/"
     
    3939
    4040/* Timeline URLs */
    41 #define TWITTER_PUBLIC_TIMELINE_URL "/statuses/public_timeline.xml"
    42 #define TWITTER_FEATURED_USERS_URL "/statuses/featured.xml"
    43 #define TWITTER_FRIENDS_TIMELINE_URL "/statuses/friends_timeline.xml"
    44 #define TWITTER_HOME_TIMELINE_URL "/statuses/home_timeline.xml"
    45 #define TWITTER_MENTIONS_URL "/statuses/mentions.xml"
    46 #define TWITTER_USER_TIMELINE_URL "/statuses/user_timeline.xml"
     41#define TWITTER_PUBLIC_TIMELINE_URL "/statuses/public_timeline.json"
     42#define TWITTER_FEATURED_USERS_URL "/statuses/featured.json"
     43#define TWITTER_FRIENDS_TIMELINE_URL "/statuses/friends_timeline.json"
     44#define TWITTER_HOME_TIMELINE_URL "/statuses/home_timeline.json"
     45#define TWITTER_MENTIONS_URL "/statuses/mentions_timeline.json"
     46#define TWITTER_USER_TIMELINE_URL "/statuses/user_timeline.json"
    4747
    4848/* Users URLs */
    49 #define TWITTER_USERS_LOOKUP_URL "/users/lookup.xml"
     49#define TWITTER_USERS_LOOKUP_URL "/users/lookup.json"
    5050
    5151/* Direct messages URLs */
    52 #define TWITTER_DIRECT_MESSAGES_URL "/direct_messages.xml"
    53 #define TWITTER_DIRECT_MESSAGES_NEW_URL "/direct_messages/new.xml"
    54 #define TWITTER_DIRECT_MESSAGES_SENT_URL "/direct_messages/sent.xml"
     52#define TWITTER_DIRECT_MESSAGES_URL "/direct_messages.json"
     53#define TWITTER_DIRECT_MESSAGES_NEW_URL "/direct_messages/new.json"
     54#define TWITTER_DIRECT_MESSAGES_SENT_URL "/direct_messages/sent.json"
    5555#define TWITTER_DIRECT_MESSAGES_DESTROY_URL "/direct_messages/destroy/"
    5656
    5757/* Friendships URLs */
    58 #define TWITTER_FRIENDSHIPS_CREATE_URL "/friendships/create.xml"
    59 #define TWITTER_FRIENDSHIPS_DESTROY_URL "/friendships/destroy.xml"
    60 #define TWITTER_FRIENDSHIPS_SHOW_URL "/friendships/show.xml"
     58#define TWITTER_FRIENDSHIPS_CREATE_URL "/friendships/create.json"
     59#define TWITTER_FRIENDSHIPS_DESTROY_URL "/friendships/destroy.json"
     60#define TWITTER_FRIENDSHIPS_SHOW_URL "/friendships/show.json"
    6161
    6262/* Social graphs URLs */
    63 #define TWITTER_FRIENDS_IDS_URL "/friends/ids.xml"
    64 #define TWITTER_FOLLOWERS_IDS_URL "/followers/ids.xml"
     63#define TWITTER_FRIENDS_IDS_URL "/friends/ids.json"
     64#define TWITTER_FOLLOWERS_IDS_URL "/followers/ids.json"
    6565
    6666/* Account URLs */
    67 #define TWITTER_ACCOUNT_RATE_LIMIT_URL "/account/rate_limit_status.xml"
     67#define TWITTER_ACCOUNT_RATE_LIMIT_URL "/account/rate_limit_status.json"
    6868
    6969/* Favorites URLs */
    70 #define TWITTER_FAVORITES_GET_URL "/favorites.xml"
     70#define TWITTER_FAVORITES_GET_URL "/favorites.json"
    7171#define TWITTER_FAVORITE_CREATE_URL "/favorites/create/"
    7272#define TWITTER_FAVORITE_DESTROY_URL "/favorites/destroy/"
     
    7777
    7878/* Report spam */
    79 #define TWITTER_REPORT_SPAM_URL "/report_spam.xml"
     79#define TWITTER_REPORT_SPAM_URL "/report_spam.json"
    8080
     81#define TWITTER_USER_STREAM_URL "https://userstream.twitter.com/1.1/user.json"
     82
     83gboolean twitter_open_stream(struct im_connection *ic);
    8184void twitter_get_timeline(struct im_connection *ic, gint64 next_cursor);
    8285void twitter_get_friends_ids(struct im_connection *ic, gint64 next_cursor);
    83 void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor);
    84 void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor);
    8586void twitter_get_statuses_friends(struct im_connection *ic, gint64 next_cursor);
    8687
Note: See TracChangeset for help on using the changeset viewer.