source: protocols/twitter/twitter_lib.c @ 15c3a6b

Last change on this file since 15c3a6b was ed83279, checked in by Wilmer van der Gaast <wilmer@…>, at 2015-06-17T23:58:02Z

Update Twitter "url" command change to work with Parson.

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