source: protocols/twitter/twitter_lib.c @ adec749

Last change on this file since adec749 was 1201fcb, checked in by dequis <dx@…>, at 2015-06-08T03:42:11Z

twitter: show full url in the url command, with username

By asking the server for the username.

Storing the username somewhere would have made sense, but this command
isn't going to be used very often, so, whatever.

  • Property mode set to 100644
File size: 41.4 KB
RevLine 
[1b221e0]1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Simple module to facilitate twitter functionality.                       *
5*                                                                           *
[199fea6]6*  Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com>            *
[0e788f5]7*  Copyright 2010-2013 Wilmer van der Gaast <wilmer@gaast.net>              *
[1b221e0]8*                                                                           *
9*  This library is free software; you can redistribute it and/or            *
10*  modify it under the terms of the GNU Lesser General Public               *
11*  License as published by the Free Software Foundation, version            *
12*  2.1.                                                                     *
13*                                                                           *
14*  This library is distributed in the hope that it will be useful,          *
15*  but WITHOUT ANY WARRANTY; without even the implied warranty of           *
16*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU        *
17*  Lesser General Public License for more details.                          *
18*                                                                           *
19*  You should have received a copy of the GNU Lesser General Public License *
20*  along with this library; if not, write to the Free Software Foundation,  *
21*  Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA           *
22*                                                                           *
23****************************************************************************/
24
[08579a1]25/* For strptime(): */
[5ebff60]26#if (__sun)
[daae10f]27#else
[08579a1]28#define _XOPEN_SOURCE
[daae10f]29#endif
[08579a1]30
[1b221e0]31#include "twitter_http.h"
32#include "twitter.h"
33#include "bitlbee.h"
34#include "url.h"
35#include "misc.h"
36#include "base64.h"
37#include "twitter_lib.h"
[e08ae0c]38#include "json_util.h"
[1b221e0]39#include <ctype.h>
40#include <errno.h>
41
42#define TXL_STATUS 1
[62d2cfb]43#define TXL_USER 2
44#define TXL_ID 3
45
[1b221e0]46struct twitter_xml_list {
[62d2cfb]47        int type;
[8203da9]48        gint64 next_cursor;
[1b221e0]49        GSList *list;
50};
51
52struct twitter_xml_user {
[73ee390]53        guint64 uid;
[1b221e0]54        char *name;
55        char *screen_name;
56};
57
58struct twitter_xml_status {
[08579a1]59        time_t created_at;
[1b221e0]60        char *text;
61        struct twitter_xml_user *user;
[67f6828]62        guint64 id, rt_id; /* Usually equal, with RTs id == *original* id */
63        guint64 reply_to;
[73ee390]64        gboolean from_filter;
[1b221e0]65};
66
[62d2cfb]67/**
68 * Frees a twitter_xml_user struct.
69 */
70static void txu_free(struct twitter_xml_user *txu)
71{
[5ebff60]72        if (txu == NULL) {
[a26af5c]73                return;
[5ebff60]74        }
[2322a9f]75
[62d2cfb]76        g_free(txu->name);
77        g_free(txu->screen_name);
[2abceca]78        g_free(txu);
[62d2cfb]79}
80
81/**
82 * Frees a twitter_xml_status struct.
83 */
84static void txs_free(struct twitter_xml_status *txs)
85{
[5ebff60]86        if (txs == NULL) {
[2322a9f]87                return;
[5ebff60]88        }
[2322a9f]89
[62d2cfb]90        g_free(txs->text);
91        txu_free(txs->user);
[2abceca]92        g_free(txs);
[62d2cfb]93}
94
95/**
96 * Free a twitter_xml_list struct.
97 * type is the type of list the struct holds.
98 */
99static void txl_free(struct twitter_xml_list *txl)
100{
101        GSList *l;
[5ebff60]102
103        if (txl == NULL) {
[a26af5c]104                return;
[5ebff60]105        }
[2322a9f]106
107        for (l = txl->list; l; l = g_slist_next(l)) {
108                if (txl->type == TXL_STATUS) {
[5983eca]109                        txs_free((struct twitter_xml_status *) l->data);
[2322a9f]110                } else if (txl->type == TXL_ID) {
[62d2cfb]111                        g_free(l->data);
[2322a9f]112                } else if (txl->type == TXL_USER) {
[de923d5]113                        txu_free(l->data);
[2322a9f]114                }
115        }
116
[62d2cfb]117        g_slist_free(txl->list);
[fd65edb]118        g_free(txl);
[62d2cfb]119}
120
121/**
[2322a9f]122 * Compare status elements
123 */
124static gint twitter_compare_elements(gconstpointer a, gconstpointer b)
125{
126        struct twitter_xml_status *a_status = (struct twitter_xml_status *) a;
127        struct twitter_xml_status *b_status = (struct twitter_xml_status *) b;
128
129        if (a_status->created_at < b_status->created_at) {
130                return -1;
131        } else if (a_status->created_at > b_status->created_at) {
132                return 1;
133        } else {
134                return 0;
135        }
136}
137
138/**
139 * Add a buddy if it is not already added, set the status to logged in.
[62d2cfb]140 */
[3e69802]141static void twitter_add_buddy(struct im_connection *ic, char *name, const char *fullname)
[62d2cfb]142{
[1014cab]143        struct twitter_data *td = ic->proto_data;
144
[de923d5]145        // Check if the buddy is already in the buddy list.
[5983eca]146        if (!bee_user_by_handle(ic->bee, ic, name)) {
[62d2cfb]147                // The buddy is not in the list, add the buddy and set the status to logged in.
[5983eca]148                imcb_add_buddy(ic, name, NULL);
149                imcb_rename_buddy(ic, name, fullname);
[631ec80]150                if (td->flags & TWITTER_MODE_CHAT) {
[5c18a76]151                        /* Necessary so that nicks always get translated to the
152                           exact Twitter username. */
[5983eca]153                        imcb_buddy_nick_hint(ic, name, name);
[5ebff60]154                        if (td->timeline_gc) {
[7f557d5]155                                imcb_chat_add_buddy(td->timeline_gc, name);
[5ebff60]156                        }
157                } else if (td->flags & TWITTER_MODE_MANY) {
[5983eca]158                        imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
[5ebff60]159                }
[62d2cfb]160        }
161}
[1b221e0]162
[a7b9ec7]163/* Warning: May return a malloc()ed value, which will be free()d on the next
[de923d5]164   call. Only for short-term use. NOT THREADSAFE!  */
[6eca2eb]165char *twitter_parse_error(struct http_request *req)
[a7b9ec7]166{
167        static char *ret = NULL;
[5246133]168        json_value *root, *err;
[5983eca]169
[a7b9ec7]170        g_free(ret);
171        ret = NULL;
[5983eca]172
173        if (req->body_size > 0) {
[7a80925]174                root = json_parse(req->reply_body, req->body_size);
[5246133]175                err = json_o_get(root, "errors");
[dff0e0b]176                if (err && err->type == json_array && (err = err->u.array.values[0]) &&
[5246133]177                    err->type == json_object) {
178                        const char *msg = json_o_str(err, "message");
[5ebff60]179                        if (msg) {
[5246133]180                                ret = g_strdup_printf("%s (%s)", req->status_string, msg);
[5ebff60]181                        }
[5246133]182                }
183                json_value_free(root);
[a7b9ec7]184        }
[5983eca]185
[6eca2eb]186        return ret ? ret : req->status_string;
[a7b9ec7]187}
188
[fb351ce]189/* WATCH OUT: This function might or might not destroy your connection.
190   Sub-optimal indeed, but just be careful when this returns NULL! */
[e08ae0c]191static json_value *twitter_parse_response(struct im_connection *ic, struct http_request *req)
[2a6da96]192{
193        gboolean logging_in = !(ic->flags & OPT_LOGGED_IN);
194        gboolean periodic;
195        struct twitter_data *td = ic->proto_data;
[e08ae0c]196        json_value *ret;
[2a6da96]197        char path[64] = "", *s;
[5ebff60]198
[2a6da96]199        if ((s = strchr(req->request, ' '))) {
[5ebff60]200                path[sizeof(path) - 1] = '\0';
[2a6da96]201                strncpy(path, s + 1, sizeof(path) - 1);
[5ebff60]202                if ((s = strchr(path, '?')) || (s = strchr(path, ' '))) {
[2a6da96]203                        *s = '\0';
[5ebff60]204                }
[2a6da96]205        }
[5ebff60]206
[2a6da96]207        /* Kinda nasty. :-( Trying to suppress error messages, but only
208           for periodic (i.e. mentions/timeline) queries. */
209        periodic = strstr(path, "timeline") || strstr(path, "mentions");
[5ebff60]210
[2a6da96]211        if (req->status_code == 401 && logging_in) {
212                /* IIRC Twitter once had an outage where they were randomly
213                   throwing 401s so I'll keep treating this one as fatal
214                   only during login. */
[5246133]215                imcb_error(ic, "Authentication failure (%s)",
[5ebff60]216                           twitter_parse_error(req));
[2a6da96]217                imc_logout(ic, FALSE);
218                return NULL;
219        } else if (req->status_code != 200) {
220                // It didn't go well, output the error and return.
[5ebff60]221                if (!periodic || logging_in || ++td->http_fails >= 5) {
[96dd574]222                        twitter_log(ic, "Error: Could not retrieve %s: %s",
[5ebff60]223                                    path, twitter_parse_error(req));
224                }
225
226                if (logging_in) {
[2a6da96]227                        imc_logout(ic, TRUE);
[5ebff60]228                }
[2a6da96]229                return NULL;
230        } else {
231                td->http_fails = 0;
232        }
233
[7a80925]234        if ((ret = json_parse(req->reply_body, req->body_size)) == NULL) {
[2a6da96]235                imcb_error(ic, "Could not retrieve %s: %s",
[5ebff60]236                           path, "XML parse error");
[2a6da96]237        }
238        return ret;
239}
240
[1b221e0]241static void twitter_http_get_friends_ids(struct http_request *req);
242
243/**
244 * Get the friends ids.
245 */
[8203da9]246void twitter_get_friends_ids(struct im_connection *ic, gint64 next_cursor)
[1b221e0]247{
[85c3004]248        // Primitive, but hey! It works...
[5983eca]249        char *args[2];
[5ebff60]250
[1b221e0]251        args[0] = "cursor";
[85c3004]252        args[1] = g_strdup_printf("%" G_GINT64_FORMAT, next_cursor);
[bb5ce4d1]253        twitter_http(ic, TWITTER_FRIENDS_IDS_URL, twitter_http_get_friends_ids, ic, 0, args, 2);
[1b221e0]254
255        g_free(args[1]);
256}
257
258/**
259 * Fill a list of ids.
260 */
[9e8c945]261static gboolean twitter_xt_get_friends_id_list(json_value *node, struct twitter_xml_list *txl)
[1b221e0]262{
[e08ae0c]263        json_value *c;
264        int i;
[5983eca]265
[62d2cfb]266        // Set the list type.
267        txl->type = TXL_ID;
[1b221e0]268
[e08ae0c]269        c = json_o_get(node, "ids");
[5ebff60]270        if (!c || c->type != json_array) {
[9e8c945]271                return FALSE;
[5ebff60]272        }
[1b221e0]273
[5ebff60]274        for (i = 0; i < c->u.array.length; i++) {
275                if (c->u.array.values[i]->type != json_integer) {
[0688e99]276                        continue;
[5ebff60]277                }
278
[e08ae0c]279                txl->list = g_slist_prepend(txl->list,
[5ebff60]280                                            g_strdup_printf("%" PRIu64, c->u.array.values[i]->u.integer));
[e08ae0c]281        }
[5ebff60]282
[e08ae0c]283        c = json_o_get(node, "next_cursor");
[5ebff60]284        if (c && c->type == json_integer) {
[e08ae0c]285                txl->next_cursor = c->u.integer;
[5ebff60]286        } else {
[e08ae0c]287                txl->next_cursor = -1;
[5ebff60]288        }
289
[9e8c945]290        return TRUE;
[1b221e0]291}
292
[de923d5]293static void twitter_get_users_lookup(struct im_connection *ic);
294
[1b221e0]295/**
296 * Callback for getting the friends ids.
297 */
298static void twitter_http_get_friends_ids(struct http_request *req)
299{
300        struct im_connection *ic;
[e08ae0c]301        json_value *parsed;
[1b221e0]302        struct twitter_xml_list *txl;
[3bd4a93]303        struct twitter_data *td;
[1b221e0]304
305        ic = req->data;
306
[62d2cfb]307        // Check if the connection is still active.
[5ebff60]308        if (!g_slist_find(twitter_connections, ic)) {
[62d2cfb]309                return;
[5ebff60]310        }
[5983eca]311
[37aa317]312        td = ic->proto_data;
[62d2cfb]313
[1b221e0]314        txl = g_new0(struct twitter_xml_list, 1);
[de923d5]315        txl->list = td->follow_ids;
[1b221e0]316
317        // Parse the data.
[5ebff60]318        if (!(parsed = twitter_parse_response(ic, req))) {
[2a6da96]319                return;
[5ebff60]320        }
321
[d0752e8]322        twitter_xt_get_friends_id_list(parsed, txl);
[e08ae0c]323        json_value_free(parsed);
[1b221e0]324
[de923d5]325        td->follow_ids = txl->list;
[5ebff60]326        if (txl->next_cursor) {
[de923d5]327                /* These were just numbers. Up to 4000 in a response AFAIK so if we get here
328                   we may be using a spammer account. \o/ */
[1b221e0]329                twitter_get_friends_ids(ic, txl->next_cursor);
[5ebff60]330        } else {
[de923d5]331                /* Now to convert all those numbers into names.. */
332                twitter_get_users_lookup(ic);
[5ebff60]333        }
[de923d5]334
335        txl->list = NULL;
336        txl_free(txl);
337}
338
[9e8c945]339static gboolean twitter_xt_get_users(json_value *node, struct twitter_xml_list *txl);
[de923d5]340static void twitter_http_get_users_lookup(struct http_request *req);
341
342static void twitter_get_users_lookup(struct im_connection *ic)
343{
344        struct twitter_data *td = ic->proto_data;
345        char *args[2] = {
346                "user_id",
347                NULL,
348        };
349        GString *ids = g_string_new("");
350        int i;
[5ebff60]351
[de923d5]352        /* We can request up to 100 users at a time. */
[5ebff60]353        for (i = 0; i < 100 && td->follow_ids; i++) {
354                g_string_append_printf(ids, ",%s", (char *) td->follow_ids->data);
[de923d5]355                g_free(td->follow_ids->data);
356                td->follow_ids = g_slist_remove(td->follow_ids, td->follow_ids->data);
357        }
358        if (ids->len > 0) {
359                args[1] = ids->str + 1;
360                /* POST, because I think ids can be up to 1KB long. */
361                twitter_http(ic, TWITTER_USERS_LOOKUP_URL, twitter_http_get_users_lookup, ic, 1, args, 2);
362        } else {
363                /* We have all users. Continue with login. (Get statuses.) */
364                td->flags |= TWITTER_HAVE_FRIENDS;
365                twitter_login_finish(ic);
366        }
367        g_string_free(ids, TRUE);
368}
369
370/**
371 * Callback for getting (twitter)friends...
372 *
[5ebff60]373 * Be afraid, be very afraid! This function will potentially add hundreds of "friends". "Who has
374 * hundreds of friends?" you wonder? You probably not, since you are reading the source of
[de923d5]375 * BitlBee... Get a life and meet new people!
376 */
377static void twitter_http_get_users_lookup(struct http_request *req)
378{
379        struct im_connection *ic = req->data;
[e08ae0c]380        json_value *parsed;
[de923d5]381        struct twitter_xml_list *txl;
382        GSList *l = NULL;
383        struct twitter_xml_user *user;
384
385        // Check if the connection is still active.
[5ebff60]386        if (!g_slist_find(twitter_connections, ic)) {
[de923d5]387                return;
[5ebff60]388        }
[de923d5]389
390        txl = g_new0(struct twitter_xml_list, 1);
391        txl->list = NULL;
[1b221e0]392
[de923d5]393        // Get the user list from the parsed xml feed.
[5ebff60]394        if (!(parsed = twitter_parse_response(ic, req))) {
[2a6da96]395                return;
[5ebff60]396        }
[d0752e8]397        twitter_xt_get_users(parsed, txl);
[e08ae0c]398        json_value_free(parsed);
[de923d5]399
400        // Add the users as buddies.
401        for (l = txl->list; l; l = g_slist_next(l)) {
402                user = l->data;
403                twitter_add_buddy(ic, user->screen_name, user->name);
404        }
405
406        // Free the structure.
[62d2cfb]407        txl_free(txl);
[de923d5]408
409        twitter_get_users_lookup(ic);
[1b221e0]410}
411
[8e3b7ac]412struct twitter_xml_user *twitter_xt_get_user(const json_value *node)
413{
414        struct twitter_xml_user *txu;
[73ee390]415        json_value *jv;
[5ebff60]416
[8e3b7ac]417        txu = g_new0(struct twitter_xml_user, 1);
418        txu->name = g_strdup(json_o_str(node, "name"));
419        txu->screen_name = g_strdup(json_o_str(node, "screen_name"));
[5ebff60]420
[73ee390]421        jv = json_o_get(node, "id");
422        txu->uid = jv->u.integer;
[5ebff60]423
[8e3b7ac]424        return txu;
425}
426
[62d2cfb]427/**
428 * Function to fill a twitter_xml_list struct.
429 * It sets:
430 *  - all <user>s from the <users> element.
431 */
[9e8c945]432static gboolean twitter_xt_get_users(json_value *node, struct twitter_xml_list *txl)
[62d2cfb]433{
434        struct twitter_xml_user *txu;
[e08ae0c]435        int i;
[62d2cfb]436
437        // Set the type of the list.
438        txl->type = TXL_USER;
439
[5ebff60]440        if (!node || node->type != json_array) {
[9e8c945]441                return FALSE;
[5ebff60]442        }
[e08ae0c]443
[62d2cfb]444        // The root <users> node should hold the list of users <user>
445        // Walk over the nodes children.
[5ebff60]446        for (i = 0; i < node->u.array.length; i++) {
[8e3b7ac]447                txu = twitter_xt_get_user(node->u.array.values[i]);
[5ebff60]448                if (txu) {
[8e3b7ac]449                        txl->list = g_slist_prepend(txl->list, txu);
[5ebff60]450                }
[62d2cfb]451        }
452
[9e8c945]453        return TRUE;
[62d2cfb]454}
455
[2b02617]456#ifdef __GLIBC__
457#define TWITTER_TIME_FORMAT "%a %b %d %H:%M:%S %z %Y"
458#else
459#define TWITTER_TIME_FORMAT "%a %b %d %H:%M:%S +0000 %Y"
460#endif
[62d2cfb]461
[cd60710]462static void expand_entities(char **text, const json_value *node);
[d1356cb]463
[1b221e0]464/**
465 * Function to fill a twitter_xml_status struct.
466 * It sets:
467 *  - the status text and
468 *  - the created_at timestamp and
469 *  - the status id and
470 *  - the user in a twitter_xml_user struct.
471 */
[c9b5817]472static struct twitter_xml_status *twitter_xt_get_status(const json_value *node)
[1b221e0]473{
[c9b5817]474        struct twitter_xml_status *txs;
[cd60710]475        const json_value *rt = NULL;
[5ebff60]476
477        if (node->type != json_object) {
[9e8c945]478                return FALSE;
[5ebff60]479        }
[c9b5817]480        txs = g_new0(struct twitter_xml_status, 1);
[1b221e0]481
[5ebff60]482        JSON_O_FOREACH(node, k, v) {
[8e3b7ac]483                if (strcmp("text", k) == 0 && v->type == json_string) {
[fb351ce]484                        txs->text = g_memdup(v->u.string.ptr, v->u.string.length + 1);
[29f72b7]485                        strip_html(txs->text);
[8e3b7ac]486                } else if (strcmp("retweeted_status", k) == 0 && v->type == json_object) {
487                        rt = v;
488                } else if (strcmp("created_at", k) == 0 && v->type == json_string) {
[08579a1]489                        struct tm parsed;
[5983eca]490
[08579a1]491                        /* Very sensitive to changes to the formatting of
492                           this field. :-( Also assumes the timezone used
493                           is UTC since C time handling functions suck. */
[5ebff60]494                        if (strptime(v->u.string.ptr, TWITTER_TIME_FORMAT, &parsed) != NULL) {
[5983eca]495                                txs->created_at = mktime_utc(&parsed);
[5ebff60]496                        }
[8e3b7ac]497                } else if (strcmp("user", k) == 0 && v->type == json_object) {
498                        txs->user = twitter_xt_get_user(v);
499                } else if (strcmp("id", k) == 0 && v->type == json_integer) {
[573e274]500                        txs->rt_id = txs->id = v->u.integer;
[8e3b7ac]501                } else if (strcmp("in_reply_to_status_id", k) == 0 && v->type == json_integer) {
502                        txs->reply_to = v->u.integer;
[ce81acd]503                }
[1b221e0]504        }
[5983eca]505
[0fff0b2]506        /* If it's a (truncated) retweet, get the original. Even if the API claims it
507           wasn't truncated because it may be lying. */
508        if (rt) {
[c9b5817]509                struct twitter_xml_status *rtxs = twitter_xt_get_status(rt);
510                if (rtxs) {
511                        g_free(txs->text);
[0ca1d79]512                        txs->text = g_strdup_printf("RT @%s: %s", rtxs->user->screen_name, rtxs->text);
[c9b5817]513                        txs->id = rtxs->id;
[0ca1d79]514                        txs_free(rtxs);
[e193aeb]515                }
[cd60710]516        } else {
517                expand_entities(&txs->text, node);
[d1356cb]518        }
519
[5ebff60]520        if (txs->text && txs->user && txs->id) {
[c9b5817]521                return txs;
[5ebff60]522        }
523
[c9b5817]524        txs_free(txs);
525        return NULL;
[d1356cb]526}
527
528/**
529 * Function to fill a twitter_xml_status struct (DM variant).
530 */
[2cd8540]531static struct twitter_xml_status *twitter_xt_get_dm(const json_value *node)
[d1356cb]532{
[2cd8540]533        struct twitter_xml_status *txs;
[5ebff60]534
535        if (node->type != json_object) {
[d1356cb]536                return FALSE;
[5ebff60]537        }
[2cd8540]538        txs = g_new0(struct twitter_xml_status, 1);
[d1356cb]539
[5ebff60]540        JSON_O_FOREACH(node, k, v) {
[d1356cb]541                if (strcmp("text", k) == 0 && v->type == json_string) {
542                        txs->text = g_memdup(v->u.string.ptr, v->u.string.length + 1);
[29f72b7]543                        strip_html(txs->text);
[d1356cb]544                } else if (strcmp("created_at", k) == 0 && v->type == json_string) {
545                        struct tm parsed;
546
547                        /* Very sensitive to changes to the formatting of
548                           this field. :-( Also assumes the timezone used
549                           is UTC since C time handling functions suck. */
[5ebff60]550                        if (strptime(v->u.string.ptr, TWITTER_TIME_FORMAT, &parsed) != NULL) {
[d1356cb]551                                txs->created_at = mktime_utc(&parsed);
[5ebff60]552                        }
[d1356cb]553                } else if (strcmp("sender", k) == 0 && v->type == json_object) {
554                        txs->user = twitter_xt_get_user(v);
555                } else if (strcmp("id", k) == 0 && v->type == json_integer) {
556                        txs->id = v->u.integer;
557                }
558        }
559
[cd60710]560        expand_entities(&txs->text, node);
[d1356cb]561
[5ebff60]562        if (txs->text && txs->user && txs->id) {
[2cd8540]563                return txs;
[5ebff60]564        }
565
[2cd8540]566        txs_free(txs);
567        return NULL;
[d1356cb]568}
569
[cd60710]570static void expand_entities(char **text, const json_value *node)
[5ebff60]571{
[cd60710]572        json_value *entities, *quoted;
573        char *quote_url = NULL, *quote_text = NULL;
574
575        if (!((entities = json_o_get(node, "entities")) && entities->type == json_object))
576                return;
577        if ((quoted = json_o_get(node, "quoted_status")) && quoted->type == json_object) {
578                /* New "retweets with comments" feature. Note that this info
579                 * seems to be included in the streaming API only! Grab the
580                 * full message and try to insert it when we run into the
581                 * Tweet entity. */
582                struct twitter_xml_status *txs = twitter_xt_get_status(quoted);
583                quote_text = g_strdup_printf("@%s: %s", txs->user->screen_name, txs->text);
584                quote_url = g_strdup_printf("%s/status/%" G_GUINT64_FORMAT, txs->user->screen_name, txs->id);
585                txs_free(txs);
586        } else {
587                quoted = NULL;
588        }
589
[5ebff60]590        JSON_O_FOREACH(entities, k, v) {
[d1356cb]591                int i;
[5ebff60]592
593                if (v->type != json_array) {
[d1356cb]594                        continue;
[5ebff60]595                }
596                if (strcmp(k, "urls") != 0 && strcmp(k, "media") != 0) {
[d1356cb]597                        continue;
[5ebff60]598                }
599
600                for (i = 0; i < v->u.array.length; i++) {
[cd60710]601                        const char *format = "%s%s <%s>%s";
602
[5ebff60]603                        if (v->u.array.values[i]->type != json_object) {
[8e3b7ac]604                                continue;
[5ebff60]605                        }
606
[d1356cb]607                        const char *kort = json_o_str(v->u.array.values[i], "url");
608                        const char *disp = json_o_str(v->u.array.values[i], "display_url");
[cd60710]609                        const char *full = json_o_str(v->u.array.values[i], "expanded_url");
[d1356cb]610                        char *pos, *new;
[5ebff60]611
[cd60710]612                        if (!kort || !disp || !(pos = strstr(*text, kort))) {
[429a9b1]613                                continue;
[5ebff60]614                        }
[cd60710]615                        if (quote_url && strstr(full, quote_url)) {
616                                format = "%s<%s> [%s]%s";
617                                disp = quote_text;
618                        }
[5ebff60]619
[d1356cb]620                        *pos = '\0';
[cd60710]621                        new = g_strdup_printf(format, *text, kort,
[d1356cb]622                                              disp, pos + strlen(kort));
[5ebff60]623
[cd60710]624                        g_free(*text);
625                        *text = new;
[429a9b1]626                }
[e193aeb]627        }
[cd60710]628        g_free(quote_text);
629        g_free(quote_url);
[1b221e0]630}
631
632/**
633 * Function to fill a twitter_xml_list struct.
634 * It sets:
635 *  - all <status>es within the <status> element and
636 *  - the next_cursor.
637 */
[9e8c945]638static gboolean twitter_xt_get_status_list(struct im_connection *ic, const json_value *node,
639                                           struct twitter_xml_list *txl)
[1b221e0]640{
641        struct twitter_xml_status *txs;
[8e3b7ac]642        int i;
[1b221e0]643
[62d2cfb]644        // Set the type of the list.
645        txl->type = TXL_STATUS;
[5ebff60]646
647        if (node->type != json_array) {
[9e8c945]648                return FALSE;
[5ebff60]649        }
[62d2cfb]650
[1b221e0]651        // The root <statuses> node should hold the list of statuses <status>
652        // Walk over the nodes children.
[5ebff60]653        for (i = 0; i < node->u.array.length; i++) {
[c9b5817]654                txs = twitter_xt_get_status(node->u.array.values[i]);
[5ebff60]655                if (!txs) {
[c9b5817]656                        continue;
[5ebff60]657                }
658
[8e3b7ac]659                txl->list = g_slist_prepend(txl->list, txs);
[1b221e0]660        }
661
[9e8c945]662        return TRUE;
[1b221e0]663}
664
[c9b5817]665/* Will log messages either way. Need to keep track of IDs for stream deduping.
666   Plus, show_ids is on by default and I don't see why anyone would disable it. */
[ce81acd]667static char *twitter_msg_add_id(struct im_connection *ic,
[5ebff60]668                                struct twitter_xml_status *txs, const char *prefix)
[ce81acd]669{
670        struct twitter_data *td = ic->proto_data;
[c9b5817]671        int reply_to = -1;
672        bee_user_t *bu;
[5983eca]673
674        if (txs->reply_to) {
[ce81acd]675                int i;
[5ebff60]676                for (i = 0; i < TWITTER_LOG_LENGTH; i++) {
[5983eca]677                        if (td->log[i].id == txs->reply_to) {
[c9b5817]678                                reply_to = i;
[ce81acd]679                                break;
680                        }
[5ebff60]681                }
[ce81acd]682        }
[5983eca]683
[c9b5817]684        if (txs->user && txs->user->screen_name &&
685            (bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) {
686                struct twitter_user_data *tud = bu->data;
687
688                if (txs->id > tud->last_id) {
689                        tud->last_id = txs->id;
690                        tud->last_time = txs->created_at;
691                }
692        }
[5ebff60]693
[c9b5817]694        td->log_id = (td->log_id + 1) % TWITTER_LOG_LENGTH;
695        td->log[td->log_id].id = txs->id;
696        td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name);
[5ebff60]697
[67f6828]698        /* This is all getting hairy. :-( If we RT'ed something ourselves,
699           remember OUR id instead so undo will work. In other cases, the
700           original tweet's id should be remembered for deduplicating. */
[5ebff60]701        if (g_strcasecmp(txs->user->screen_name, td->user) == 0) {
[67f6828]702                td->log[td->log_id].id = txs->rt_id;
[c43146d]703                /* More useful than NULL. */
704                td->log[td->log_id].bu = &twitter_log_local_user;
[5ebff60]705        }
706
[0ca1d79]707        if (set_getbool(&ic->acc->set, "show_ids")) {
[5ebff60]708                if (reply_to != -1) {
[0ca1d79]709                        return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s",
710                                               td->log_id, reply_to, prefix, txs->text);
[5ebff60]711                } else {
[0ca1d79]712                        return g_strdup_printf("\002[\002%02x\002]\002 %s%s",
713                                               td->log_id, prefix, txs->text);
[5ebff60]714                }
[0ca1d79]715        } else {
[5ebff60]716                if (*prefix) {
[0ca1d79]717                        return g_strconcat(prefix, txs->text, NULL);
[5ebff60]718                } else {
[0ca1d79]719                        return NULL;
[5ebff60]720                }
[0ca1d79]721        }
[ce81acd]722}
723
[73ee390]724/**
725 * Function that is called to see the filter statuses in groupchat windows.
726 */
727static void twitter_status_show_filter(struct im_connection *ic, struct twitter_xml_status *status)
728{
729        struct twitter_data *td = ic->proto_data;
730        char *msg = twitter_msg_add_id(ic, status, "");
731        struct twitter_filter *tf;
732        GSList *f;
733        GSList *l;
734
735        for (f = td->filters; f; f = g_slist_next(f)) {
736                tf = f->data;
737
738                switch (tf->type) {
739                case TWITTER_FILTER_TYPE_FOLLOW:
[5ebff60]740                        if (status->user->uid != tf->uid) {
[73ee390]741                                continue;
[5ebff60]742                        }
[73ee390]743                        break;
744
745                case TWITTER_FILTER_TYPE_TRACK:
[5ebff60]746                        if (strcasestr(status->text, tf->text) == NULL) {
[73ee390]747                                continue;
[5ebff60]748                        }
[73ee390]749                        break;
750
751                default:
752                        continue;
753                }
754
755                for (l = tf->groupchats; l; l = g_slist_next(l)) {
756                        imcb_chat_msg(l->data, status->user->screen_name,
[5ebff60]757                                      msg ? msg : status->text, 0, 0);
[73ee390]758                }
759        }
760
761        g_free(msg);
762}
763
[62d2cfb]764/**
765 * Function that is called to see the statuses in a groupchat window.
766 */
[29f72b7]767static void twitter_status_show_chat(struct im_connection *ic, struct twitter_xml_status *status)
[62d2cfb]768{
769        struct twitter_data *td = ic->proto_data;
770        struct groupchat *gc;
[29f72b7]771        gboolean me = g_strcasecmp(td->user, status->user->screen_name) == 0;
772        char *msg;
[62d2cfb]773
774        // Create a new groupchat if it does not exsist.
[631ec80]775        gc = twitter_groupchat_init(ic);
[62d2cfb]776
[5ebff60]777        if (!me) {
[29f72b7]778                /* MUST be done before twitter_msg_add_id() to avoid #872. */
779                twitter_add_buddy(ic, status->user->screen_name, status->user->name);
[5ebff60]780        }
[29f72b7]781        msg = twitter_msg_add_id(ic, status, "");
[5ebff60]782
[29f72b7]783        // Say it!
784        if (me) {
785                imcb_chat_log(gc, "You: %s", msg ? msg : status->text);
786        } else {
787                imcb_chat_msg(gc, status->user->screen_name,
[5ebff60]788                              msg ? msg : status->text, 0, status->created_at);
[62d2cfb]789        }
[29f72b7]790
791        g_free(msg);
[62d2cfb]792}
793
794/**
795 * Function that is called to see statuses as private messages.
796 */
[29f72b7]797static void twitter_status_show_msg(struct im_connection *ic, struct twitter_xml_status *status)
[62d2cfb]798{
799        struct twitter_data *td = ic->proto_data;
[631ec80]800        char from[MAX_STRING] = "";
[29f72b7]801        char *prefix = NULL, *text = NULL;
802        gboolean me = g_strcasecmp(td->user, status->user->screen_name) == 0;
[62d2cfb]803
[631ec80]804        if (td->flags & TWITTER_MODE_ONE) {
[5983eca]805                g_snprintf(from, sizeof(from) - 1, "%s_%s", td->prefix, ic->acc->user);
806                from[MAX_STRING - 1] = '\0';
[e88fbe27]807        }
[5983eca]808
[5ebff60]809        if (td->flags & TWITTER_MODE_ONE) {
[29f72b7]810                prefix = g_strdup_printf("\002<\002%s\002>\002 ",
811                                         status->user->screen_name);
[5ebff60]812        } else if (!me) {
[29f72b7]813                twitter_add_buddy(ic, status->user->screen_name, status->user->name);
[5ebff60]814        } else {
[29f72b7]815                prefix = g_strdup("You: ");
[5ebff60]816        }
[5983eca]817
[29f72b7]818        text = twitter_msg_add_id(ic, status, prefix ? prefix : "");
[5983eca]819
[29f72b7]820        imcb_buddy_msg(ic,
821                       *from ? from : status->user->screen_name,
822                       text ? text : status->text, 0, status->created_at);
[5983eca]823
[29f72b7]824        g_free(text);
825        g_free(prefix);
826}
[5983eca]827
[29f72b7]828static void twitter_status_show(struct im_connection *ic, struct twitter_xml_status *status)
829{
830        struct twitter_data *td = ic->proto_data;
[7549d00]831        char *last_id_str;
[5ebff60]832
833        if (status->user == NULL || status->text == NULL) {
[29f72b7]834                return;
[5ebff60]835        }
836
[29f72b7]837        /* Grrrr. Would like to do this during parsing, but can't access
838           settings from there. */
[5ebff60]839        if (set_getbool(&ic->acc->set, "strip_newlines")) {
[29f72b7]840                strip_newlines(status->text);
[5ebff60]841        }
842
843        if (status->from_filter) {
[73ee390]844                twitter_status_show_filter(ic, status);
[5ebff60]845        } else if (td->flags & TWITTER_MODE_CHAT) {
[29f72b7]846                twitter_status_show_chat(ic, status);
[5ebff60]847        } else {
[29f72b7]848                twitter_status_show_msg(ic, status);
[5ebff60]849        }
[5983eca]850
[29f72b7]851        // Update the timeline_id to hold the highest id, so that by the next request
852        // we won't pick up the updates already in the list.
[573e274]853        td->timeline_id = MAX(td->timeline_id, status->rt_id);
[7549d00]854
855        last_id_str = g_strdup_printf("%" G_GUINT64_FORMAT, td->timeline_id);
[fedc8f1]856        set_setstr(&ic->acc->set, "_last_tweet", last_id_str);
[7549d00]857        g_free(last_id_str);
[62d2cfb]858}
859
[73ee390]860static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o, gboolean from_filter);
[ddc2de5]861
862static void twitter_http_stream(struct http_request *req)
863{
[dff0e0b]864        struct im_connection *ic = req->data;
[dd672e2]865        struct twitter_data *td;
[dff0e0b]866        json_value *parsed;
[62f6b45]867        int len = 0;
868        char c, *nl;
[73ee390]869        gboolean from_filter;
[5ebff60]870
871        if (!g_slist_find(twitter_connections, ic)) {
[dff0e0b]872                return;
[5ebff60]873        }
874
[e132b60]875        ic->flags |= OPT_PONGED;
[dd672e2]876        td = ic->proto_data;
[5ebff60]877
[dd672e2]878        if ((req->flags & HTTPC_EOF) || !req->reply_body) {
[5ebff60]879                if (req == td->stream) {
[73ee390]880                        td->stream = NULL;
[5ebff60]881                } else if (req == td->filter_stream) {
[73ee390]882                        td->filter_stream = NULL;
[5ebff60]883                }
[73ee390]884
[dd672e2]885                imcb_error(ic, "Stream closed (%s)", req->status_string);
[7320610]886                if (req->status_code == 401) {
887                        imcb_error(ic, "Check your system clock.");
888                }
[dd672e2]889                imc_logout(ic, TRUE);
890                return;
891        }
[5ebff60]892
[62f6b45]893        /* MUST search for CRLF, not just LF:
894           https://dev.twitter.com/docs/streaming-apis/processing#Parsing_responses */
[5ebff60]895        if (!(nl = strstr(req->reply_body, "\r\n"))) {
[ddc2de5]896                return;
[5ebff60]897        }
898
[62f6b45]899        len = nl - req->reply_body;
900        if (len > 0) {
901                c = req->reply_body[len];
902                req->reply_body[len] = '\0';
[5ebff60]903
[7a80925]904                if ((parsed = json_parse(req->reply_body, req->body_size))) {
[73ee390]905                        from_filter = (req == td->filter_stream);
906                        twitter_stream_handle_object(ic, parsed, from_filter);
[62f6b45]907                }
908                json_value_free(parsed);
909                req->reply_body[len] = c;
[dff0e0b]910        }
[5ebff60]911
[62f6b45]912        http_flush_bytes(req, len + 2);
[5ebff60]913
[dff0e0b]914        /* One notification might bring multiple events! */
[5ebff60]915        if (req->body_size > 0) {
[dff0e0b]916                twitter_http_stream(req);
[5ebff60]917        }
[ddc2de5]918}
[2322a9f]919
[5aa96fc8]920static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o);
[c9b5817]921static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs);
[5aa96fc8]922
[73ee390]923static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o, gboolean from_filter)
[dff0e0b]924{
[5aa96fc8]925        struct twitter_data *td = ic->proto_data;
[c9b5817]926        struct twitter_xml_status *txs;
[d1356cb]927        json_value *c;
[5ebff60]928
[c9b5817]929        if ((txs = twitter_xt_get_status(o))) {
[73ee390]930                txs->from_filter = from_filter;
[2dcde94]931                gboolean ret = twitter_stream_handle_status(ic, txs);
932                txs_free(txs);
933                return ret;
[d1356cb]934        } else if ((c = json_o_get(o, "direct_message")) &&
[2cd8540]935                   (txs = twitter_xt_get_dm(c))) {
[5ebff60]936                if (g_strcasecmp(txs->user->screen_name, td->user) != 0) {
[2cd8540]937                        imcb_buddy_msg(ic, txs->user->screen_name,
[5ebff60]938                                       txs->text, 0, txs->created_at);
939                }
[d1356cb]940                txs_free(txs);
941                return TRUE;
[5aa96fc8]942        } else if ((c = json_o_get(o, "event")) && c->type == json_string) {
943                twitter_stream_handle_event(ic, o);
944                return TRUE;
945        } else if ((c = json_o_get(o, "disconnect")) && c->type == json_object) {
946                /* HACK: Because we're inside an event handler, we can't just
947                   disconnect here. Instead, just change the HTTP status string
948                   into a Twitter status string. */
949                char *reason = json_o_strdup(c, "reason");
950                if (reason) {
951                        g_free(td->stream->status_string);
952                        td->stream->status_string = reason;
953                }
954                return TRUE;
[dff0e0b]955        }
956        return FALSE;
957}
958
[c9b5817]959static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs)
960{
961        struct twitter_data *td = ic->proto_data;
962        int i;
[5ebff60]963
[c9b5817]964        for (i = 0; i < TWITTER_LOG_LENGTH; i++) {
965                if (td->log[i].id == txs->id) {
[29f72b7]966                        /* Got a duplicate (RT, probably). Drop it. */
[c9b5817]967                        return TRUE;
968                }
969        }
[5ebff60]970
[9f8bb17]971        if (!(g_strcasecmp(txs->user->screen_name, td->user) == 0 ||
[2dcde94]972              set_getbool(&ic->acc->set, "fetch_mentions") ||
[c9b5817]973              bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) {
974                /* Tweet is from an unknown person and the user does not want
975                   to see @mentions, so drop it. twitter_stream_handle_event()
976                   picks up new follows so this simple filter should be safe. */
977                /* TODO: The streaming API seems to do poor @mention matching.
978                   I.e. I'm getting mentions for @WilmerSomething, not just for
979                   @Wilmer. But meh. You want spam, you get spam. */
980                return TRUE;
981        }
[5ebff60]982
[29f72b7]983        twitter_status_show(ic, txs);
[5ebff60]984
[c9b5817]985        return TRUE;
986}
987
[5aa96fc8]988static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o)
989{
990        struct twitter_data *td = ic->proto_data;
991        json_value *source = json_o_get(o, "source");
992        json_value *target = json_o_get(o, "target");
993        const char *type = json_o_str(o, "event");
[5ebff60]994
[5aa96fc8]995        if (!type || !source || source->type != json_object
[5ebff60]996            || !target || target->type != json_object) {
[5aa96fc8]997                return FALSE;
998        }
[5ebff60]999
[5aa96fc8]1000        if (strcmp(type, "follow") == 0) {
1001                struct twitter_xml_user *us = twitter_xt_get_user(source);
1002                struct twitter_xml_user *ut = twitter_xt_get_user(target);
[9f8bb17]1003                if (g_strcasecmp(us->screen_name, td->user) == 0) {
[5aa96fc8]1004                        twitter_add_buddy(ic, ut->screen_name, ut->name);
1005                }
1006                txu_free(us);
1007                txu_free(ut);
1008        }
[5ebff60]1009
[5aa96fc8]1010        return TRUE;
1011}
1012
[dff0e0b]1013gboolean twitter_open_stream(struct im_connection *ic)
1014{
1015        struct twitter_data *td = ic->proto_data;
[5ebff60]1016        char *args[2] = { "with", "followings" };
1017
[dff0e0b]1018        if ((td->stream = twitter_http(ic, TWITTER_USER_STREAM_URL,
[62f6b45]1019                                       twitter_http_stream, ic, 0, args, 2))) {
[dff0e0b]1020                /* This flag must be enabled or we'll get no data until EOF
1021                   (which err, kind of, defeats the purpose of a streaming API). */
1022                td->stream->flags |= HTTPC_STREAMING;
1023                return TRUE;
1024        }
[5ebff60]1025
[dff0e0b]1026        return FALSE;
1027}
1028
[73ee390]1029static gboolean twitter_filter_stream(struct im_connection *ic)
1030{
1031        struct twitter_data *td = ic->proto_data;
[5ebff60]1032        char *args[4] = { "follow", NULL, "track", NULL };
[73ee390]1033        GString *followstr = g_string_new("");
1034        GString *trackstr = g_string_new("");
1035        gboolean ret = FALSE;
1036        struct twitter_filter *tf;
1037        GSList *l;
1038
1039        for (l = td->filters; l; l = g_slist_next(l)) {
1040                tf = l->data;
1041
1042                switch (tf->type) {
1043                case TWITTER_FILTER_TYPE_FOLLOW:
[5ebff60]1044                        if (followstr->len > 0) {
[73ee390]1045                                g_string_append_c(followstr, ',');
[5ebff60]1046                        }
[73ee390]1047
1048                        g_string_append_printf(followstr, "%" G_GUINT64_FORMAT,
[5ebff60]1049                                               tf->uid);
[73ee390]1050                        break;
1051
1052                case TWITTER_FILTER_TYPE_TRACK:
[5ebff60]1053                        if (trackstr->len > 0) {
[73ee390]1054                                g_string_append_c(trackstr, ',');
[5ebff60]1055                        }
[73ee390]1056
1057                        g_string_append(trackstr, tf->text);
1058                        break;
1059
1060                default:
1061                        continue;
1062                }
1063        }
1064
1065        args[1] = followstr->str;
1066        args[3] = trackstr->str;
1067
[5ebff60]1068        if (td->filter_stream) {
[73ee390]1069                http_close(td->filter_stream);
[5ebff60]1070        }
[73ee390]1071
1072        if ((td->filter_stream = twitter_http(ic, TWITTER_FILTER_STREAM_URL,
1073                                              twitter_http_stream, ic, 0,
[5ebff60]1074                                              args, 4))) {
[73ee390]1075                /* This flag must be enabled or we'll get no data until EOF
1076                   (which err, kind of, defeats the purpose of a streaming API). */
1077                td->filter_stream->flags |= HTTPC_STREAMING;
1078                ret = TRUE;
1079        }
1080
1081        g_string_free(followstr, TRUE);
1082        g_string_free(trackstr, TRUE);
1083
1084        return ret;
1085}
1086
1087static void twitter_filter_users_post(struct http_request *req)
1088{
1089        struct im_connection *ic = req->data;
1090        struct twitter_data *td;
1091        struct twitter_filter *tf;
1092        GList *users = NULL;
1093        json_value *parsed;
1094        json_value *id;
1095        const char *name;
1096        GString *fstr;
1097        GSList *l;
1098        GList *u;
1099        int i;
1100
1101        // Check if the connection is still active.
[5ebff60]1102        if (!g_slist_find(twitter_connections, ic)) {
[73ee390]1103                return;
[5ebff60]1104        }
[73ee390]1105
1106        td = ic->proto_data;
1107
[5ebff60]1108        if (!(parsed = twitter_parse_response(ic, req))) {
[73ee390]1109                return;
[5ebff60]1110        }
[73ee390]1111
1112        for (l = td->filters; l; l = g_slist_next(l)) {
1113                tf = l->data;
1114
[5ebff60]1115                if (tf->type == TWITTER_FILTER_TYPE_FOLLOW) {
[73ee390]1116                        users = g_list_prepend(users, tf);
[5ebff60]1117                }
[73ee390]1118        }
1119
[5ebff60]1120        if (parsed->type != json_array) {
[73ee390]1121                goto finish;
[5ebff60]1122        }
[73ee390]1123
1124        for (i = 0; i < parsed->u.array.length; i++) {
1125                id = json_o_get(parsed->u.array.values[i], "id");
1126                name = json_o_str(parsed->u.array.values[i], "screen_name");
1127
[5ebff60]1128                if (!name || !id || id->type != json_integer) {
[73ee390]1129                        continue;
[5ebff60]1130                }
[73ee390]1131
1132                for (u = users; u; u = g_list_next(u)) {
1133                        tf = u->data;
1134
1135                        if (g_strcasecmp(tf->text, name) == 0) {
1136                                tf->uid = id->u.integer;
1137                                users = g_list_delete_link(users, u);
1138                                break;
1139                        }
1140                }
1141        }
1142
1143finish:
1144        json_value_free(parsed);
1145        twitter_filter_stream(ic);
1146
[5ebff60]1147        if (!users) {
[73ee390]1148                return;
[5ebff60]1149        }
[73ee390]1150
1151        fstr = g_string_new("");
1152
1153        for (u = users; u; u = g_list_next(u)) {
[5ebff60]1154                if (fstr->len > 0) {
[73ee390]1155                        g_string_append(fstr, ", ");
[5ebff60]1156                }
[73ee390]1157
1158                g_string_append(fstr, tf->text);
1159        }
1160
1161        imcb_error(ic, "Failed UID acquisitions: %s", fstr->str);
1162
1163        g_string_free(fstr, TRUE);
1164        g_list_free(users);
1165}
1166
1167gboolean twitter_open_filter_stream(struct im_connection *ic)
1168{
1169        struct twitter_data *td = ic->proto_data;
[5ebff60]1170        char *args[2] = { "screen_name", NULL };
[73ee390]1171        GString *ustr = g_string_new("");
1172        struct twitter_filter *tf;
1173        struct http_request *req;
1174        GSList *l;
1175
1176        for (l = td->filters; l; l = g_slist_next(l)) {
1177                tf = l->data;
1178
[5ebff60]1179                if (tf->type != TWITTER_FILTER_TYPE_FOLLOW || tf->uid != 0) {
[73ee390]1180                        continue;
[5ebff60]1181                }
[73ee390]1182
[5ebff60]1183                if (ustr->len > 0) {
[73ee390]1184                        g_string_append_c(ustr, ',');
[5ebff60]1185                }
[73ee390]1186
1187                g_string_append(ustr, tf->text);
1188        }
1189
1190        if (ustr->len == 0) {
1191                g_string_free(ustr, TRUE);
1192                return twitter_filter_stream(ic);
1193        }
1194
1195        args[1] = ustr->str;
1196        req = twitter_http(ic, TWITTER_USERS_LOOKUP_URL,
[5ebff60]1197                           twitter_filter_users_post,
1198                           ic, 0, args, 2);
[73ee390]1199
1200        g_string_free(ustr, TRUE);
1201        return req != NULL;
1202}
1203
[dff0e0b]1204static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor);
1205static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor);
1206
[2322a9f]1207/**
1208 * Get the timeline with optionally mentions
1209 */
[2f9027c]1210gboolean twitter_get_timeline(struct im_connection *ic, gint64 next_cursor)
[2322a9f]1211{
1212        struct twitter_data *td = ic->proto_data;
1213        gboolean include_mentions = set_getbool(&ic->acc->set, "fetch_mentions");
1214
1215        if (td->flags & TWITTER_DOING_TIMELINE) {
[11ec078]1216                if (++td->http_fails >= 5) {
1217                        imcb_error(ic, "Fetch timeout (%d)", td->flags);
1218                        imc_logout(ic, TRUE);
[2f9027c]1219                        return FALSE;
[11ec078]1220                }
[2322a9f]1221        }
1222
1223        td->flags |= TWITTER_DOING_TIMELINE;
1224
1225        twitter_get_home_timeline(ic, next_cursor);
1226
1227        if (include_mentions) {
1228                twitter_get_mentions(ic, next_cursor);
1229        }
[5ebff60]1230
[2f9027c]1231        return TRUE;
[2322a9f]1232}
1233
1234/**
1235 * Call this one after receiving timeline/mentions. Show to user once we have
1236 * both.
1237 */
1238void twitter_flush_timeline(struct im_connection *ic)
1239{
1240        struct twitter_data *td = ic->proto_data;
1241        gboolean include_mentions = set_getbool(&ic->acc->set, "fetch_mentions");
[b5fe39b]1242        int show_old_mentions = set_getint(&ic->acc->set, "show_old_mentions");
[2322a9f]1243        struct twitter_xml_list *home_timeline = td->home_timeline_obj;
1244        struct twitter_xml_list *mentions = td->mentions_obj;
[29f72b7]1245        guint64 last_id = 0;
[2322a9f]1246        GSList *output = NULL;
1247        GSList *l;
1248
[29f72b7]1249        imcb_connected(ic);
[5ebff60]1250
[2322a9f]1251        if (!(td->flags & TWITTER_GOT_TIMELINE)) {
1252                return;
1253        }
1254
1255        if (include_mentions && !(td->flags & TWITTER_GOT_MENTIONS)) {
1256                return;
1257        }
1258
1259        if (home_timeline && home_timeline->list) {
1260                for (l = home_timeline->list; l; l = g_slist_next(l)) {
1261                        output = g_slist_insert_sorted(output, l->data, twitter_compare_elements);
1262                }
1263        }
1264
1265        if (include_mentions && mentions && mentions->list) {
1266                for (l = mentions->list; l; l = g_slist_next(l)) {
[b5fe39b]1267                        if (show_old_mentions < 1 && output && twitter_compare_elements(l->data, output->data) < 0) {
[2322a9f]1268                                continue;
1269                        }
1270
1271                        output = g_slist_insert_sorted(output, l->data, twitter_compare_elements);
1272                }
1273        }
1274
1275        // See if the user wants to see the messages in a groupchat window or as private messages.
[29f72b7]1276        while (output) {
1277                struct twitter_xml_status *txs = output->data;
[5ebff60]1278                if (txs->id != last_id) {
[29f72b7]1279                        twitter_status_show(ic, txs);
[5ebff60]1280                }
[29f72b7]1281                last_id = txs->id;
1282                output = g_slist_remove(output, txs);
1283        }
[2322a9f]1284
[199fea6]1285        txl_free(home_timeline);
1286        txl_free(mentions);
[2322a9f]1287
1288        td->flags &= ~(TWITTER_DOING_TIMELINE | TWITTER_GOT_TIMELINE | TWITTER_GOT_MENTIONS);
[199fea6]1289        td->home_timeline_obj = td->mentions_obj = NULL;
[2322a9f]1290}
1291
[ddc2de5]1292static void twitter_http_get_home_timeline(struct http_request *req);
1293static void twitter_http_get_mentions(struct http_request *req);
1294
[2322a9f]1295/**
1296 * Get the timeline.
1297 */
[ddc2de5]1298static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor)
[2322a9f]1299{
1300        struct twitter_data *td = ic->proto_data;
1301
[199fea6]1302        txl_free(td->home_timeline_obj);
[2322a9f]1303        td->home_timeline_obj = NULL;
1304        td->flags &= ~TWITTER_GOT_TIMELINE;
1305
[429a9b1]1306        char *args[6];
[2322a9f]1307        args[0] = "cursor";
[85c3004]1308        args[1] = g_strdup_printf("%" G_GINT64_FORMAT, next_cursor);
[429a9b1]1309        args[2] = "include_entities";
1310        args[3] = "true";
[2322a9f]1311        if (td->timeline_id) {
[429a9b1]1312                args[4] = "since_id";
[7549d00]1313                args[5] = g_strdup_printf("%" G_GUINT64_FORMAT, td->timeline_id);
[2322a9f]1314        }
1315
[90fc864]1316        if (twitter_http(ic, TWITTER_HOME_TIMELINE_URL, twitter_http_get_home_timeline, ic, 0, args,
[5ebff60]1317                         td->timeline_id ? 6 : 4) == NULL) {
1318                if (++td->http_fails >= 5) {
[90fc864]1319                        imcb_error(ic, "Could not retrieve %s: %s",
1320                                   TWITTER_HOME_TIMELINE_URL, "connection failed");
[5ebff60]1321                }
[90fc864]1322                td->flags |= TWITTER_GOT_TIMELINE;
1323                twitter_flush_timeline(ic);
1324        }
[2322a9f]1325
1326        g_free(args[1]);
1327        if (td->timeline_id) {
[429a9b1]1328                g_free(args[5]);
[2322a9f]1329        }
1330}
1331
1332/**
1333 * Get mentions.
1334 */
[ddc2de5]1335static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor)
[2322a9f]1336{
1337        struct twitter_data *td = ic->proto_data;
1338
[199fea6]1339        txl_free(td->mentions_obj);
[2322a9f]1340        td->mentions_obj = NULL;
1341        td->flags &= ~TWITTER_GOT_MENTIONS;
1342
[429a9b1]1343        char *args[6];
[2322a9f]1344        args[0] = "cursor";
[85c3004]1345        args[1] = g_strdup_printf("%" G_GINT64_FORMAT, next_cursor);
[429a9b1]1346        args[2] = "include_entities";
1347        args[3] = "true";
[2322a9f]1348        if (td->timeline_id) {
[429a9b1]1349                args[4] = "since_id";
[85c3004]1350                args[5] = g_strdup_printf("%" G_GUINT64_FORMAT, td->timeline_id);
[b5fe39b]1351        } else {
1352                args[4] = "count";
1353                args[5] = g_strdup_printf("%d", set_getint(&ic->acc->set, "show_old_mentions"));
[2322a9f]1354        }
1355
[b5fe39b]1356        if (twitter_http(ic, TWITTER_MENTIONS_URL, twitter_http_get_mentions,
1357                         ic, 0, args, 6) == NULL) {
[5ebff60]1358                if (++td->http_fails >= 5) {
[90fc864]1359                        imcb_error(ic, "Could not retrieve %s: %s",
1360                                   TWITTER_MENTIONS_URL, "connection failed");
[5ebff60]1361                }
[90fc864]1362                td->flags |= TWITTER_GOT_MENTIONS;
1363                twitter_flush_timeline(ic);
1364        }
[2322a9f]1365
1366        g_free(args[1]);
[2fb1262]1367        g_free(args[5]);
[2322a9f]1368}
1369
[1b221e0]1370/**
1371 * Callback for getting the home timeline.
1372 */
1373static void twitter_http_get_home_timeline(struct http_request *req)
1374{
[62d2cfb]1375        struct im_connection *ic = req->data;
[37aa317]1376        struct twitter_data *td;
[8e3b7ac]1377        json_value *parsed;
[1b221e0]1378        struct twitter_xml_list *txl;
[62d2cfb]1379
1380        // Check if the connection is still active.
[5ebff60]1381        if (!g_slist_find(twitter_connections, ic)) {
[62d2cfb]1382                return;
[5ebff60]1383        }
[5983eca]1384
[37aa317]1385        td = ic->proto_data;
[1b221e0]1386
[2322a9f]1387        txl = g_new0(struct twitter_xml_list, 1);
1388        txl->list = NULL;
1389
1390        // The root <statuses> node should hold the list of statuses <status>
[5ebff60]1391        if (!(parsed = twitter_parse_response(ic, req))) {
[2a6da96]1392                goto end;
[5ebff60]1393        }
[d0752e8]1394        twitter_xt_get_status_list(ic, parsed, txl);
[2fb1262]1395        json_value_free(parsed);
[2322a9f]1396
1397        td->home_timeline_obj = txl;
1398
[5ebff60]1399end:
1400        if (!g_slist_find(twitter_connections, ic)) {
[fb351ce]1401                return;
[5ebff60]1402        }
[fb351ce]1403
[2322a9f]1404        td->flags |= TWITTER_GOT_TIMELINE;
1405
1406        twitter_flush_timeline(ic);
1407}
1408
1409/**
1410 * Callback for getting mentions.
1411 */
1412static void twitter_http_get_mentions(struct http_request *req)
1413{
1414        struct im_connection *ic = req->data;
1415        struct twitter_data *td;
[8e3b7ac]1416        json_value *parsed;
[2322a9f]1417        struct twitter_xml_list *txl;
1418
1419        // Check if the connection is still active.
[5ebff60]1420        if (!g_slist_find(twitter_connections, ic)) {
[1b221e0]1421                return;
[5ebff60]1422        }
[2322a9f]1423
1424        td = ic->proto_data;
1425
[1b221e0]1426        txl = g_new0(struct twitter_xml_list, 1);
1427        txl->list = NULL;
[62d2cfb]1428
[1b221e0]1429        // The root <statuses> node should hold the list of statuses <status>
[5ebff60]1430        if (!(parsed = twitter_parse_response(ic, req))) {
[2a6da96]1431                goto end;
[5ebff60]1432        }
[d0752e8]1433        twitter_xt_get_status_list(ic, parsed, txl);
[2fb1262]1434        json_value_free(parsed);
[1b221e0]1435
[2322a9f]1436        td->mentions_obj = txl;
[1b221e0]1437
[5ebff60]1438end:
1439        if (!g_slist_find(twitter_connections, ic)) {
[fb351ce]1440                return;
[5ebff60]1441        }
[fb351ce]1442
[2322a9f]1443        td->flags |= TWITTER_GOT_MENTIONS;
1444
1445        twitter_flush_timeline(ic);
[1b221e0]1446}
1447
1448/**
[de923d5]1449 * Callback to use after sending a POST request to twitter.
1450 * (Generic, used for a few kinds of queries.)
[1b221e0]1451 */
[7d53efb]1452static void twitter_http_post(struct http_request *req)
[1b221e0]1453{
1454        struct im_connection *ic = req->data;
[7b87539]1455        struct twitter_data *td;
[24132ec]1456        json_value *parsed, *id;
[1b221e0]1457
[62d2cfb]1458        // Check if the connection is still active.
[5ebff60]1459        if (!g_slist_find(twitter_connections, ic)) {
[62d2cfb]1460                return;
[5ebff60]1461        }
[62d2cfb]1462
[7b87539]1463        td = ic->proto_data;
1464        td->last_status_id = 0;
[5983eca]1465
[5ebff60]1466        if (!(parsed = twitter_parse_response(ic, req))) {
[1b221e0]1467                return;
[5ebff60]1468        }
1469
[67f6828]1470        if ((id = json_o_get(parsed, "id")) && id->type == json_integer) {
[24132ec]1471                td->last_status_id = id->u.integer;
[67f6828]1472        }
[5ebff60]1473
[c751e51]1474        json_value_free(parsed);
[5ebff60]1475
1476        if (req->flags & TWITTER_HTTP_USER_ACK) {
[b235228]1477                twitter_log(ic, "Command processed successfully");
[5ebff60]1478        }
[1b221e0]1479}
1480
1481/**
1482 * Function to POST a new status to twitter.
[5983eca]1483 */
[b890626]1484void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_to)
[1b221e0]1485{
[5983eca]1486        char *args[4] = {
[b890626]1487                "status", msg,
1488                "in_reply_to_status_id",
[85c3004]1489                g_strdup_printf("%" G_GUINT64_FORMAT, in_reply_to)
[b890626]1490        };
[5ebff60]1491
[b890626]1492        twitter_http(ic, TWITTER_STATUS_UPDATE_URL, twitter_http_post, ic, 1,
[5ebff60]1493                     args, in_reply_to ? 4 : 2);
[b890626]1494        g_free(args[3]);
[1b221e0]1495}
1496
1497
[62d2cfb]1498/**
1499 * Function to POST a new message to twitter.
1500 */
1501void twitter_direct_messages_new(struct im_connection *ic, char *who, char *msg)
1502{
[5983eca]1503        char *args[4];
[5ebff60]1504
[62d2cfb]1505        args[0] = "screen_name";
1506        args[1] = who;
1507        args[2] = "text";
1508        args[3] = msg;
1509        // Use the same callback as for twitter_post_status, since it does basically the same.
[ba3233c]1510        twitter_http(ic, TWITTER_DIRECT_MESSAGES_NEW_URL, twitter_http_post, ic, 1, args, 4);
[62d2cfb]1511}
[7d53efb]1512
1513void twitter_friendships_create_destroy(struct im_connection *ic, char *who, int create)
1514{
[5983eca]1515        char *args[2];
[5ebff60]1516
[7d53efb]1517        args[0] = "screen_name";
1518        args[1] = who;
[5983eca]1519        twitter_http(ic, create ? TWITTER_FRIENDSHIPS_CREATE_URL : TWITTER_FRIENDSHIPS_DESTROY_URL,
[5ebff60]1520                     twitter_http_post, ic, 1, args, 2);
[a26af5c]1521}
[7b87539]1522
1523void twitter_status_destroy(struct im_connection *ic, guint64 id)
1524{
1525        char *url;
[5ebff60]1526
[85c3004]1527        url = g_strdup_printf("%s%" G_GUINT64_FORMAT "%s",
1528                              TWITTER_STATUS_DESTROY_URL, id, ".json");
[b235228]1529        twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0,
1530                       TWITTER_HTTP_USER_ACK);
[7b87539]1531        g_free(url);
1532}
[b890626]1533
1534void twitter_status_retweet(struct im_connection *ic, guint64 id)
1535{
1536        char *url;
[5ebff60]1537
[85c3004]1538        url = g_strdup_printf("%s%" G_GUINT64_FORMAT "%s",
1539                              TWITTER_STATUS_RETWEET_URL, id, ".json");
[b235228]1540        twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0,
1541                       TWITTER_HTTP_USER_ACK);
[b890626]1542        g_free(url);
1543}
[d18dee42]1544
1545/**
1546 * Report a user for sending spam.
1547 */
1548void twitter_report_spam(struct im_connection *ic, char *screen_name)
1549{
1550        char *args[2] = {
1551                "screen_name",
1552                NULL,
1553        };
[5ebff60]1554
[d18dee42]1555        args[1] = screen_name;
[b235228]1556        twitter_http_f(ic, TWITTER_REPORT_SPAM_URL, twitter_http_post,
1557                       ic, 1, args, 2, TWITTER_HTTP_USER_ACK);
[d18dee42]1558}
[b61c74c]1559
1560/**
1561 * Favourite a tweet.
1562 */
1563void twitter_favourite_tweet(struct im_connection *ic, guint64 id)
1564{
[75bda8b]1565        char *args[2] = {
1566                "id",
1567                NULL,
1568        };
[5ebff60]1569
[85c3004]1570        args[1] = g_strdup_printf("%" G_GUINT64_FORMAT, id);
[75bda8b]1571        twitter_http_f(ic, TWITTER_FAVORITE_CREATE_URL, twitter_http_post,
1572                       ic, 1, args, 2, TWITTER_HTTP_USER_ACK);
1573        g_free(args[1]);
[b61c74c]1574}
[1201fcb]1575
1576static void twitter_http_status_show_url(struct http_request *req)
1577{
1578        struct im_connection *ic = req->data;
1579        json_value *parsed, *id;
1580        const char *name;
1581
1582        // Check if the connection is still active.
1583        if (!g_slist_find(twitter_connections, ic)) {
1584                return;
1585        }
1586
1587        if (!(parsed = twitter_parse_response(ic, req))) {
1588                return;
1589        }
1590
1591        /* for the parson branch:
1592        name = json_object_dotget_string(json_object(parsed), "user.screen_name");
1593        id = json_object_get_integer(json_object(parsed), "id");
1594        */
1595
1596        name = json_o_str(json_o_get(parsed, "user"), "screen_name");
1597        id = json_o_get(parsed, "id");
1598
1599        if (name && id && id->type == json_integer) {
1600                twitter_log(ic, "https://twitter.com/%s/status/%" G_GUINT64_FORMAT, name, id->u.integer);
1601        } else {
1602                twitter_log(ic, "Error: could not fetch tweet url.");
1603        }
1604
1605        json_value_free(parsed);
1606}
1607
1608void twitter_status_show_url(struct im_connection *ic, guint64 id)
1609{
1610        char *url = g_strdup_printf("%s%" G_GUINT64_FORMAT "%s", TWITTER_STATUS_SHOW_URL, id, ".json");
1611        twitter_http(ic, url, twitter_http_status_show_url, ic, 0, NULL, 0);
1612        g_free(url);
1613}
Note: See TracBrowser for help on using the repository browser.