source: protocols/twitter/twitter_lib.c @ 2363cca

Last change on this file since 2363cca was 0e788f5, checked in by Wilmer van der Gaast <wilmer@…>, at 2013-02-21T19:15:59Z

I'm still bored on a long flight. Wrote a script to automatically update
my copyright mentions since some were getting pretty stale. Left files not
touched since before 2012 alone so that this change doesn't touch almost
EVERY source file.

  • Property mode set to 100644
File size: 34.3 KB
Line 
1/***************************************************************************\
2*                                                                           *
3*  BitlBee - An IRC to IM gateway                                           *
4*  Simple module to facilitate twitter functionality.                       *
5*                                                                           *
6*  Copyright 2009-2010 Geert Mulders <g.c.w.m.mulders@gmail.com>            *
7*  Copyright 2010-2013 Wilmer van der Gaast <wilmer@gaast.net>              *
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
25/* For strptime(): */
26#if(__sun)
27#else
28#define _XOPEN_SOURCE
29#endif
30
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"
38#include "json_util.h"
39#include <ctype.h>
40#include <errno.h>
41
42/* GLib < 2.12.0 doesn't have g_ascii_strtoll(), work around using system strtoll(). */
43/* GLib < 2.12.4 can be buggy: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=488013 */
44#if !GLIB_CHECK_VERSION(2,12,5)
45#include <stdlib.h>
46#include <limits.h>
47#define g_ascii_strtoll strtoll
48#endif
49
50#define TXL_STATUS 1
51#define TXL_USER 2
52#define TXL_ID 3
53
54struct twitter_xml_list {
55        int type;
56        gint64 next_cursor;
57        GSList *list;
58};
59
60struct twitter_xml_user {
61        char *name;
62        char *screen_name;
63};
64
65struct twitter_xml_status {
66        time_t created_at;
67        char *text;
68        struct twitter_xml_user *user;
69        guint64 id, rt_id; /* Usually equal, with RTs id == *original* id */
70        guint64 reply_to;
71};
72
73/**
74 * Frees a twitter_xml_user struct.
75 */
76static void txu_free(struct twitter_xml_user *txu)
77{
78        if (txu == NULL)
79                return;
80
81        g_free(txu->name);
82        g_free(txu->screen_name);
83        g_free(txu);
84}
85
86/**
87 * Frees a twitter_xml_status struct.
88 */
89static void txs_free(struct twitter_xml_status *txs)
90{
91        if (txs == NULL)
92                return;
93
94        g_free(txs->text);
95        txu_free(txs->user);
96        g_free(txs);
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;
106        if (txl == NULL)
107                return;
108
109        for (l = txl->list; l; l = g_slist_next(l)) {
110                if (txl->type == TXL_STATUS) {
111                        txs_free((struct twitter_xml_status *) l->data);
112                } else if (txl->type == TXL_ID) {
113                        g_free(l->data);
114                } else if (txl->type == TXL_USER) {
115                        txu_free(l->data);
116                }
117        }
118
119        g_slist_free(txl->list);
120        g_free(txl);
121}
122
123/**
124 * Compare status elements
125 */
126static gint twitter_compare_elements(gconstpointer a, gconstpointer b)
127{
128        struct twitter_xml_status *a_status = (struct twitter_xml_status *) a;
129        struct twitter_xml_status *b_status = (struct twitter_xml_status *) b;
130
131        if (a_status->created_at < b_status->created_at) {
132                return -1;
133        } else if (a_status->created_at > b_status->created_at) {
134                return 1;
135        } else {
136                return 0;
137        }
138}
139
140/**
141 * Add a buddy if it is not already added, set the status to logged in.
142 */
143static void twitter_add_buddy(struct im_connection *ic, char *name, const char *fullname)
144{
145        struct twitter_data *td = ic->proto_data;
146
147        // Check if the buddy is already in the buddy list.
148        if (!bee_user_by_handle(ic->bee, ic, name)) {
149                // The buddy is not in the list, add the buddy and set the status to logged in.
150                imcb_add_buddy(ic, name, NULL);
151                imcb_rename_buddy(ic, name, fullname);
152                if (td->flags & TWITTER_MODE_CHAT) {
153                        /* Necessary so that nicks always get translated to the
154                           exact Twitter username. */
155                        imcb_buddy_nick_hint(ic, name, name);
156                        if (td->timeline_gc)
157                                imcb_chat_add_buddy(td->timeline_gc, name);
158                } else if (td->flags & TWITTER_MODE_MANY)
159                        imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL);
160        }
161}
162
163/* Warning: May return a malloc()ed value, which will be free()d on the next
164   call. Only for short-term use. NOT THREADSAFE!  */
165char *twitter_parse_error(struct http_request *req)
166{
167        static char *ret = NULL;
168        json_value *root, *err;
169
170        g_free(ret);
171        ret = NULL;
172
173        if (req->body_size > 0) {
174                root = json_parse(req->reply_body);
175                err = json_o_get(root, "errors");
176                if (err && err->type == json_array && (err = err->u.array.values[0]) &&
177                    err->type == json_object) {
178                        const char *msg = json_o_str(err, "message");
179                        if (msg)
180                                ret = g_strdup_printf("%s (%s)", req->status_string, msg);
181                }
182                json_value_free(root);
183        }
184
185        return ret ? ret : req->status_string;
186}
187
188/* WATCH OUT: This function might or might not destroy your connection.
189   Sub-optimal indeed, but just be careful when this returns NULL! */
190static json_value *twitter_parse_response(struct im_connection *ic, struct http_request *req)
191{
192        gboolean logging_in = !(ic->flags & OPT_LOGGED_IN);
193        gboolean periodic;
194        struct twitter_data *td = ic->proto_data;
195        json_value *ret;
196        char path[64] = "", *s;
197       
198        if ((s = strchr(req->request, ' '))) {
199                path[sizeof(path)-1] = '\0';
200                strncpy(path, s + 1, sizeof(path) - 1);
201                if ((s = strchr(path, '?')) || (s = strchr(path, ' ')))
202                        *s = '\0';
203        }
204       
205        /* Kinda nasty. :-( Trying to suppress error messages, but only
206           for periodic (i.e. mentions/timeline) queries. */
207        periodic = strstr(path, "timeline") || strstr(path, "mentions");
208       
209        if (req->status_code == 401 && logging_in) {
210                /* IIRC Twitter once had an outage where they were randomly
211                   throwing 401s so I'll keep treating this one as fatal
212                   only during login. */
213                imcb_error(ic, "Authentication failure (%s)",
214                               twitter_parse_error(req));
215                imc_logout(ic, FALSE);
216                return NULL;
217        } else if (req->status_code != 200) {
218                // It didn't go well, output the error and return.
219                if (!periodic || logging_in || ++td->http_fails >= 5)
220                        twitter_log(ic, "Error: Could not retrieve %s: %s",
221                                    path, twitter_parse_error(req));
222               
223                if (logging_in)
224                        imc_logout(ic, TRUE);
225                return NULL;
226        } else {
227                td->http_fails = 0;
228        }
229
230        if ((ret = json_parse(req->reply_body)) == NULL) {
231                imcb_error(ic, "Could not retrieve %s: %s",
232                           path, "XML parse error");
233        }
234        return ret;
235}
236
237static void twitter_http_get_friends_ids(struct http_request *req);
238
239/**
240 * Get the friends ids.
241 */
242void twitter_get_friends_ids(struct im_connection *ic, gint64 next_cursor)
243{
244        // Primitive, but hey! It works...     
245        char *args[2];
246        args[0] = "cursor";
247        args[1] = g_strdup_printf("%lld", (long long) next_cursor);
248        twitter_http(ic, TWITTER_FRIENDS_IDS_URL, twitter_http_get_friends_ids, ic, 0, args, 2);
249
250        g_free(args[1]);
251}
252
253/**
254 * Fill a list of ids.
255 */
256static gboolean twitter_xt_get_friends_id_list(json_value *node, struct twitter_xml_list *txl)
257{
258        json_value *c;
259        int i;
260
261        // Set the list type.
262        txl->type = TXL_ID;
263
264        c = json_o_get(node, "ids");
265        if (!c || c->type != json_array)
266                return FALSE;
267
268        for (i = 0; i < c->u.array.length; i ++) {
269                if (c->u.array.values[i]->type != json_integer)
270                        continue;
271               
272                txl->list = g_slist_prepend(txl->list,
273                        g_strdup_printf("%lld", c->u.array.values[i]->u.integer));
274        }
275       
276        c = json_o_get(node, "next_cursor");
277        if (c && c->type == json_integer)
278                txl->next_cursor = c->u.integer;
279        else
280                txl->next_cursor = -1;
281       
282        return TRUE;
283}
284
285static void twitter_get_users_lookup(struct im_connection *ic);
286
287/**
288 * Callback for getting the friends ids.
289 */
290static void twitter_http_get_friends_ids(struct http_request *req)
291{
292        struct im_connection *ic;
293        json_value *parsed;
294        struct twitter_xml_list *txl;
295        struct twitter_data *td;
296
297        ic = req->data;
298
299        // Check if the connection is still active.
300        if (!g_slist_find(twitter_connections, ic))
301                return;
302
303        td = ic->proto_data;
304
305        txl = g_new0(struct twitter_xml_list, 1);
306        txl->list = td->follow_ids;
307
308        // Parse the data.
309        if (!(parsed = twitter_parse_response(ic, req)))
310                return;
311       
312        twitter_xt_get_friends_id_list(parsed, txl);
313        json_value_free(parsed);
314
315        td->follow_ids = txl->list;
316        if (txl->next_cursor)
317                /* These were just numbers. Up to 4000 in a response AFAIK so if we get here
318                   we may be using a spammer account. \o/ */
319                twitter_get_friends_ids(ic, txl->next_cursor);
320        else
321                /* Now to convert all those numbers into names.. */
322                twitter_get_users_lookup(ic);
323
324        txl->list = NULL;
325        txl_free(txl);
326}
327
328static gboolean twitter_xt_get_users(json_value *node, struct twitter_xml_list *txl);
329static void twitter_http_get_users_lookup(struct http_request *req);
330
331static void twitter_get_users_lookup(struct im_connection *ic)
332{
333        struct twitter_data *td = ic->proto_data;
334        char *args[2] = {
335                "user_id",
336                NULL,
337        };
338        GString *ids = g_string_new("");
339        int i;
340       
341        /* We can request up to 100 users at a time. */
342        for (i = 0; i < 100 && td->follow_ids; i ++) {
343                g_string_append_printf(ids, ",%s", (char*) td->follow_ids->data);
344                g_free(td->follow_ids->data);
345                td->follow_ids = g_slist_remove(td->follow_ids, td->follow_ids->data);
346        }
347        if (ids->len > 0) {
348                args[1] = ids->str + 1;
349                /* POST, because I think ids can be up to 1KB long. */
350                twitter_http(ic, TWITTER_USERS_LOOKUP_URL, twitter_http_get_users_lookup, ic, 1, args, 2);
351        } else {
352                /* We have all users. Continue with login. (Get statuses.) */
353                td->flags |= TWITTER_HAVE_FRIENDS;
354                twitter_login_finish(ic);
355        }
356        g_string_free(ids, TRUE);
357}
358
359/**
360 * Callback for getting (twitter)friends...
361 *
362 * Be afraid, be very afraid! This function will potentially add hundreds of "friends". "Who has
363 * hundreds of friends?" you wonder? You probably not, since you are reading the source of
364 * BitlBee... Get a life and meet new people!
365 */
366static void twitter_http_get_users_lookup(struct http_request *req)
367{
368        struct im_connection *ic = req->data;
369        json_value *parsed;
370        struct twitter_xml_list *txl;
371        GSList *l = NULL;
372        struct twitter_xml_user *user;
373
374        // Check if the connection is still active.
375        if (!g_slist_find(twitter_connections, ic))
376                return;
377
378        txl = g_new0(struct twitter_xml_list, 1);
379        txl->list = NULL;
380
381        // Get the user list from the parsed xml feed.
382        if (!(parsed = twitter_parse_response(ic, req)))
383                return;
384        twitter_xt_get_users(parsed, txl);
385        json_value_free(parsed);
386
387        // Add the users as buddies.
388        for (l = txl->list; l; l = g_slist_next(l)) {
389                user = l->data;
390                twitter_add_buddy(ic, user->screen_name, user->name);
391        }
392
393        // Free the structure.
394        txl_free(txl);
395
396        twitter_get_users_lookup(ic);
397}
398
399struct twitter_xml_user *twitter_xt_get_user(const json_value *node)
400{
401        struct twitter_xml_user *txu;
402       
403        txu = g_new0(struct twitter_xml_user, 1);
404        txu->name = g_strdup(json_o_str(node, "name"));
405        txu->screen_name = g_strdup(json_o_str(node, "screen_name"));
406       
407        return txu;
408}
409
410/**
411 * Function to fill a twitter_xml_list struct.
412 * It sets:
413 *  - all <user>s from the <users> element.
414 */
415static gboolean twitter_xt_get_users(json_value *node, struct twitter_xml_list *txl)
416{
417        struct twitter_xml_user *txu;
418        int i;
419
420        // Set the type of the list.
421        txl->type = TXL_USER;
422
423        if (!node || node->type != json_array)
424                return FALSE;
425
426        // The root <users> node should hold the list of users <user>
427        // Walk over the nodes children.
428        for (i = 0; i < node->u.array.length; i ++) {
429                txu = twitter_xt_get_user(node->u.array.values[i]);
430                if (txu)
431                        txl->list = g_slist_prepend(txl->list, txu);
432        }
433
434        return TRUE;
435}
436
437#ifdef __GLIBC__
438#define TWITTER_TIME_FORMAT "%a %b %d %H:%M:%S %z %Y"
439#else
440#define TWITTER_TIME_FORMAT "%a %b %d %H:%M:%S +0000 %Y"
441#endif
442
443static char* expand_entities(char* text, const json_value *entities);
444
445/**
446 * Function to fill a twitter_xml_status struct.
447 * It sets:
448 *  - the status text and
449 *  - the created_at timestamp and
450 *  - the status id and
451 *  - the user in a twitter_xml_user struct.
452 */
453static struct twitter_xml_status *twitter_xt_get_status(const json_value *node)
454{
455        struct twitter_xml_status *txs;
456        const json_value *rt = NULL, *entities = NULL;
457       
458        if (node->type != json_object)
459                return FALSE;
460        txs = g_new0(struct twitter_xml_status, 1);
461
462        JSON_O_FOREACH (node, k, v) {
463                if (strcmp("text", k) == 0 && v->type == json_string) {
464                        txs->text = g_memdup(v->u.string.ptr, v->u.string.length + 1);
465                        strip_html(txs->text);
466                } else if (strcmp("retweeted_status", k) == 0 && v->type == json_object) {
467                        rt = v;
468                } else if (strcmp("created_at", k) == 0 && v->type == json_string) {
469                        struct tm parsed;
470
471                        /* Very sensitive to changes to the formatting of
472                           this field. :-( Also assumes the timezone used
473                           is UTC since C time handling functions suck. */
474                        if (strptime(v->u.string.ptr, TWITTER_TIME_FORMAT, &parsed) != NULL)
475                                txs->created_at = mktime_utc(&parsed);
476                } else if (strcmp("user", k) == 0 && v->type == json_object) {
477                        txs->user = twitter_xt_get_user(v);
478                } else if (strcmp("id", k) == 0 && v->type == json_integer) {
479                        txs->rt_id = txs->id = v->u.integer;
480                } else if (strcmp("in_reply_to_status_id", k) == 0 && v->type == json_integer) {
481                        txs->reply_to = v->u.integer;
482                } else if (strcmp("entities", k) == 0 && v->type == json_object) {
483                        entities = v;
484                }
485        }
486
487        /* If it's a (truncated) retweet, get the original. Even if the API claims it
488           wasn't truncated because it may be lying. */
489        if (rt) {
490                struct twitter_xml_status *rtxs = twitter_xt_get_status(rt);
491                if (rtxs) {
492                        g_free(txs->text);
493                        txs->text = g_strdup_printf("RT @%s: %s", rtxs->user->screen_name, rtxs->text);
494                        txs->id = rtxs->id;
495                        txs_free(rtxs);
496                }
497        } else if (entities) {
498                txs->text = expand_entities(txs->text, entities);
499        }
500
501        if (txs->text && txs->user && txs->id)
502                return txs;
503       
504        txs_free(txs);
505        return NULL;
506}
507
508/**
509 * Function to fill a twitter_xml_status struct (DM variant).
510 */
511static struct twitter_xml_status *twitter_xt_get_dm(const json_value *node)
512{
513        struct twitter_xml_status *txs;
514        const json_value *entities = NULL;
515       
516        if (node->type != json_object)
517                return FALSE;
518        txs = g_new0(struct twitter_xml_status, 1);
519
520        JSON_O_FOREACH (node, k, v) {
521                if (strcmp("text", k) == 0 && v->type == json_string) {
522                        txs->text = g_memdup(v->u.string.ptr, v->u.string.length + 1);
523                        strip_html(txs->text);
524                } else if (strcmp("created_at", k) == 0 && v->type == json_string) {
525                        struct tm parsed;
526
527                        /* Very sensitive to changes to the formatting of
528                           this field. :-( Also assumes the timezone used
529                           is UTC since C time handling functions suck. */
530                        if (strptime(v->u.string.ptr, TWITTER_TIME_FORMAT, &parsed) != NULL)
531                                txs->created_at = mktime_utc(&parsed);
532                } else if (strcmp("sender", k) == 0 && v->type == json_object) {
533                        txs->user = twitter_xt_get_user(v);
534                } else if (strcmp("id", k) == 0 && v->type == json_integer) {
535                        txs->id = v->u.integer;
536                }
537        }
538
539        if (entities) {
540                txs->text = expand_entities(txs->text, entities);
541        }
542
543        if (txs->text && txs->user && txs->id)
544                return txs;
545       
546        txs_free(txs);
547        return NULL;
548}
549
550static char* expand_entities(char* text, const json_value *entities) {
551        JSON_O_FOREACH (entities, k, v) {
552                int i;
553               
554                if (v->type != json_array)
555                        continue;
556                if (strcmp(k, "urls") != 0 && strcmp(k, "media") != 0)
557                        continue;
558               
559                for (i = 0; i < v->u.array.length; i ++) {
560                        if (v->u.array.values[i]->type != json_object)
561                                continue;
562                       
563                        const char *kort = json_o_str(v->u.array.values[i], "url");
564                        const char *disp = json_o_str(v->u.array.values[i], "display_url");
565                        char *pos, *new;
566                       
567                        if (!kort || !disp || !(pos = strstr(text, kort)))
568                                continue;
569                       
570                        *pos = '\0';
571                        new = g_strdup_printf("%s%s <%s>%s", text, kort,
572                                              disp, pos + strlen(kort));
573                       
574                        g_free(text);
575                        text = new;
576                }
577        }
578       
579        return text;
580}
581
582/**
583 * Function to fill a twitter_xml_list struct.
584 * It sets:
585 *  - all <status>es within the <status> element and
586 *  - the next_cursor.
587 */
588static gboolean twitter_xt_get_status_list(struct im_connection *ic, const json_value *node,
589                                           struct twitter_xml_list *txl)
590{
591        struct twitter_xml_status *txs;
592        int i;
593
594        // Set the type of the list.
595        txl->type = TXL_STATUS;
596       
597        if (node->type != json_array)
598                return FALSE;
599
600        // The root <statuses> node should hold the list of statuses <status>
601        // Walk over the nodes children.
602        for (i = 0; i < node->u.array.length; i ++) {
603                txs = twitter_xt_get_status(node->u.array.values[i]);
604                if (!txs)
605                        continue;
606               
607                txl->list = g_slist_prepend(txl->list, txs);
608        }
609
610        return TRUE;
611}
612
613/* Will log messages either way. Need to keep track of IDs for stream deduping.
614   Plus, show_ids is on by default and I don't see why anyone would disable it. */
615static char *twitter_msg_add_id(struct im_connection *ic,
616                                struct twitter_xml_status *txs, const char *prefix)
617{
618        struct twitter_data *td = ic->proto_data;
619        int reply_to = -1;
620        bee_user_t *bu;
621
622        if (txs->reply_to) {
623                int i;
624                for (i = 0; i < TWITTER_LOG_LENGTH; i++)
625                        if (td->log[i].id == txs->reply_to) {
626                                reply_to = i;
627                                break;
628                        }
629        }
630
631        if (txs->user && txs->user->screen_name &&
632            (bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) {
633                struct twitter_user_data *tud = bu->data;
634
635                if (txs->id > tud->last_id) {
636                        tud->last_id = txs->id;
637                        tud->last_time = txs->created_at;
638                }
639        }
640       
641        td->log_id = (td->log_id + 1) % TWITTER_LOG_LENGTH;
642        td->log[td->log_id].id = txs->id;
643        td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name);
644       
645        /* This is all getting hairy. :-( If we RT'ed something ourselves,
646           remember OUR id instead so undo will work. In other cases, the
647           original tweet's id should be remembered for deduplicating. */
648        if (strcmp(txs->user->screen_name, td->user) == 0)
649                td->log[td->log_id].id = txs->rt_id;
650       
651        if (set_getbool(&ic->acc->set, "show_ids")) {
652                if (reply_to != -1)
653                        return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s",
654                                               td->log_id, reply_to, prefix, txs->text);
655                else
656                        return g_strdup_printf("\002[\002%02x\002]\002 %s%s",
657                                               td->log_id, prefix, txs->text);
658        } else {
659                if (*prefix)
660                        return g_strconcat(prefix, txs->text, NULL);
661                else
662                        return NULL;
663        }
664}
665
666/**
667 * Function that is called to see the statuses in a groupchat window.
668 */
669static void twitter_status_show_chat(struct im_connection *ic, struct twitter_xml_status *status)
670{
671        struct twitter_data *td = ic->proto_data;
672        struct groupchat *gc;
673        gboolean me = g_strcasecmp(td->user, status->user->screen_name) == 0;
674        char *msg;
675
676        // Create a new groupchat if it does not exsist.
677        gc = twitter_groupchat_init(ic);
678
679        if (!me)
680                /* MUST be done before twitter_msg_add_id() to avoid #872. */
681                twitter_add_buddy(ic, status->user->screen_name, status->user->name);
682        msg = twitter_msg_add_id(ic, status, "");
683       
684        // Say it!
685        if (me) {
686                imcb_chat_log(gc, "You: %s", msg ? msg : status->text);
687        } else {
688                imcb_chat_msg(gc, status->user->screen_name,
689                              msg ? msg : status->text, 0, status->created_at);
690        }
691
692        g_free(msg);
693}
694
695/**
696 * Function that is called to see statuses as private messages.
697 */
698static void twitter_status_show_msg(struct im_connection *ic, struct twitter_xml_status *status)
699{
700        struct twitter_data *td = ic->proto_data;
701        char from[MAX_STRING] = "";
702        char *prefix = NULL, *text = NULL;
703        gboolean me = g_strcasecmp(td->user, status->user->screen_name) == 0;
704
705        if (td->flags & TWITTER_MODE_ONE) {
706                g_snprintf(from, sizeof(from) - 1, "%s_%s", td->prefix, ic->acc->user);
707                from[MAX_STRING - 1] = '\0';
708        }
709
710        if (td->flags & TWITTER_MODE_ONE)
711                prefix = g_strdup_printf("\002<\002%s\002>\002 ",
712                                         status->user->screen_name);
713        else if (!me)
714                twitter_add_buddy(ic, status->user->screen_name, status->user->name);
715        else
716                prefix = g_strdup("You: ");
717
718        text = twitter_msg_add_id(ic, status, prefix ? prefix : "");
719
720        imcb_buddy_msg(ic,
721                       *from ? from : status->user->screen_name,
722                       text ? text : status->text, 0, status->created_at);
723
724        g_free(text);
725        g_free(prefix);
726}
727
728static void twitter_status_show(struct im_connection *ic, struct twitter_xml_status *status)
729{
730        struct twitter_data *td = ic->proto_data;
731       
732        if (status->user == NULL || status->text == NULL)
733                return;
734       
735        /* Grrrr. Would like to do this during parsing, but can't access
736           settings from there. */
737        if (set_getbool(&ic->acc->set, "strip_newlines"))
738                strip_newlines(status->text);
739       
740        if (td->flags & TWITTER_MODE_CHAT)
741                twitter_status_show_chat(ic, status);
742        else
743                twitter_status_show_msg(ic, status);
744
745        // Update the timeline_id to hold the highest id, so that by the next request
746        // we won't pick up the updates already in the list.
747        td->timeline_id = MAX(td->timeline_id, status->rt_id);
748}
749
750static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o);
751
752static void twitter_http_stream(struct http_request *req)
753{
754        struct im_connection *ic = req->data;
755        struct twitter_data *td;
756        json_value *parsed;
757        int len = 0;
758        char c, *nl;
759       
760        if (!g_slist_find(twitter_connections, ic))
761                return;
762       
763        ic->flags |= OPT_PONGED;
764        td = ic->proto_data;
765       
766        if ((req->flags & HTTPC_EOF) || !req->reply_body) {
767                td->stream = NULL;
768                imcb_error(ic, "Stream closed (%s)", req->status_string);
769                imc_logout(ic, TRUE);
770                return;
771        }
772       
773        /* MUST search for CRLF, not just LF:
774           https://dev.twitter.com/docs/streaming-apis/processing#Parsing_responses */
775        if (!(nl = strstr(req->reply_body, "\r\n")))
776                return;
777       
778        len = nl - req->reply_body;
779        if (len > 0) {
780                c = req->reply_body[len];
781                req->reply_body[len] = '\0';
782               
783                if ((parsed = json_parse(req->reply_body))) {
784                        twitter_stream_handle_object(ic, parsed);
785                }
786                json_value_free(parsed);
787                req->reply_body[len] = c;
788        }
789       
790        http_flush_bytes(req, len + 2);
791       
792        /* One notification might bring multiple events! */
793        if (req->body_size > 0)
794                twitter_http_stream(req);
795}
796
797static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o);
798static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs);
799
800static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o)
801{
802        struct twitter_data *td = ic->proto_data;
803        struct twitter_xml_status *txs;
804        json_value *c;
805       
806        if ((txs = twitter_xt_get_status(o))) {
807                gboolean ret = twitter_stream_handle_status(ic, txs);
808                txs_free(txs);
809                return ret;
810        } else if ((c = json_o_get(o, "direct_message")) &&
811                   (txs = twitter_xt_get_dm(c))) {
812                if (strcmp(txs->user->screen_name, td->user) != 0)
813                        imcb_buddy_msg(ic, txs->user->screen_name,
814                                       txs->text, 0, txs->created_at);
815                txs_free(txs);
816                return TRUE;
817        } else if ((c = json_o_get(o, "event")) && c->type == json_string) {
818                twitter_stream_handle_event(ic, o);
819                return TRUE;
820        } else if ((c = json_o_get(o, "disconnect")) && c->type == json_object) {
821                /* HACK: Because we're inside an event handler, we can't just
822                   disconnect here. Instead, just change the HTTP status string
823                   into a Twitter status string. */
824                char *reason = json_o_strdup(c, "reason");
825                if (reason) {
826                        g_free(td->stream->status_string);
827                        td->stream->status_string = reason;
828                }
829                return TRUE;
830        }
831        return FALSE;
832}
833
834static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs)
835{
836        struct twitter_data *td = ic->proto_data;
837        int i;
838       
839        for (i = 0; i < TWITTER_LOG_LENGTH; i++) {
840                if (td->log[i].id == txs->id) {
841                        /* Got a duplicate (RT, probably). Drop it. */
842                        return TRUE;
843                }
844        }
845       
846        if (!(strcmp(txs->user->screen_name, td->user) == 0 ||
847              set_getbool(&ic->acc->set, "fetch_mentions") ||
848              bee_user_by_handle(ic->bee, ic, txs->user->screen_name))) {
849                /* Tweet is from an unknown person and the user does not want
850                   to see @mentions, so drop it. twitter_stream_handle_event()
851                   picks up new follows so this simple filter should be safe. */
852                /* TODO: The streaming API seems to do poor @mention matching.
853                   I.e. I'm getting mentions for @WilmerSomething, not just for
854                   @Wilmer. But meh. You want spam, you get spam. */
855                return TRUE;
856        }
857       
858        twitter_status_show(ic, txs);
859       
860        return TRUE;
861}
862
863static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value *o)
864{
865        struct twitter_data *td = ic->proto_data;
866        json_value *source = json_o_get(o, "source");
867        json_value *target = json_o_get(o, "target");
868        const char *type = json_o_str(o, "event");
869       
870        if (!type || !source || source->type != json_object
871                  || !target || target->type != json_object) {
872                return FALSE;
873        }
874       
875        if (strcmp(type, "follow") == 0) {
876                struct twitter_xml_user *us = twitter_xt_get_user(source);
877                struct twitter_xml_user *ut = twitter_xt_get_user(target);
878                if (strcmp(us->screen_name, td->user) == 0) {
879                        twitter_add_buddy(ic, ut->screen_name, ut->name);
880                }
881                txu_free(us);
882                txu_free(ut);
883        }
884       
885        return TRUE;
886}
887
888gboolean twitter_open_stream(struct im_connection *ic)
889{
890        struct twitter_data *td = ic->proto_data;
891        char *args[2] = {"with", "followings"};
892       
893        if ((td->stream = twitter_http(ic, TWITTER_USER_STREAM_URL,
894                                       twitter_http_stream, ic, 0, args, 2))) {
895                /* This flag must be enabled or we'll get no data until EOF
896                   (which err, kind of, defeats the purpose of a streaming API). */
897                td->stream->flags |= HTTPC_STREAMING;
898                return TRUE;
899        }
900       
901        return FALSE;
902}
903
904static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor);
905static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor);
906
907/**
908 * Get the timeline with optionally mentions
909 */
910void twitter_get_timeline(struct im_connection *ic, gint64 next_cursor)
911{
912        struct twitter_data *td = ic->proto_data;
913        gboolean include_mentions = set_getbool(&ic->acc->set, "fetch_mentions");
914
915        if (td->flags & TWITTER_DOING_TIMELINE) {
916                if (++td->http_fails >= 5) {
917                        imcb_error(ic, "Fetch timeout (%d)", td->flags);
918                        imc_logout(ic, TRUE);
919                }
920        }
921
922        td->flags |= TWITTER_DOING_TIMELINE;
923
924        twitter_get_home_timeline(ic, next_cursor);
925
926        if (include_mentions) {
927                twitter_get_mentions(ic, next_cursor);
928        }
929}
930
931/**
932 * Call this one after receiving timeline/mentions. Show to user once we have
933 * both.
934 */
935void twitter_flush_timeline(struct im_connection *ic)
936{
937        struct twitter_data *td = ic->proto_data;
938        gboolean include_mentions = set_getbool(&ic->acc->set, "fetch_mentions");
939        int show_old_mentions = set_getint(&ic->acc->set, "show_old_mentions");
940        struct twitter_xml_list *home_timeline = td->home_timeline_obj;
941        struct twitter_xml_list *mentions = td->mentions_obj;
942        guint64 last_id = 0;
943        GSList *output = NULL;
944        GSList *l;
945
946        imcb_connected(ic);
947       
948        if (!(td->flags & TWITTER_GOT_TIMELINE)) {
949                return;
950        }
951
952        if (include_mentions && !(td->flags & TWITTER_GOT_MENTIONS)) {
953                return;
954        }
955
956        if (home_timeline && home_timeline->list) {
957                for (l = home_timeline->list; l; l = g_slist_next(l)) {
958                        output = g_slist_insert_sorted(output, l->data, twitter_compare_elements);
959                }
960        }
961
962        if (include_mentions && mentions && mentions->list) {
963                for (l = mentions->list; l; l = g_slist_next(l)) {
964                        if (show_old_mentions < 1 && output && twitter_compare_elements(l->data, output->data) < 0) {
965                                continue;
966                        }
967
968                        output = g_slist_insert_sorted(output, l->data, twitter_compare_elements);
969                }
970        }
971
972        // See if the user wants to see the messages in a groupchat window or as private messages.
973        while (output) {
974                struct twitter_xml_status *txs = output->data;
975                if (txs->id != last_id)
976                        twitter_status_show(ic, txs);
977                last_id = txs->id;
978                output = g_slist_remove(output, txs);
979        }
980
981        txl_free(home_timeline);
982        txl_free(mentions);
983
984        td->flags &= ~(TWITTER_DOING_TIMELINE | TWITTER_GOT_TIMELINE | TWITTER_GOT_MENTIONS);
985        td->home_timeline_obj = td->mentions_obj = NULL;
986}
987
988static void twitter_http_get_home_timeline(struct http_request *req);
989static void twitter_http_get_mentions(struct http_request *req);
990
991/**
992 * Get the timeline.
993 */
994static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor)
995{
996        struct twitter_data *td = ic->proto_data;
997
998        txl_free(td->home_timeline_obj);
999        td->home_timeline_obj = NULL;
1000        td->flags &= ~TWITTER_GOT_TIMELINE;
1001
1002        char *args[6];
1003        args[0] = "cursor";
1004        args[1] = g_strdup_printf("%lld", (long long) next_cursor);
1005        args[2] = "include_entities";
1006        args[3] = "true";
1007        if (td->timeline_id) {
1008                args[4] = "since_id";
1009                args[5] = g_strdup_printf("%llu", (long long unsigned int) td->timeline_id);
1010        }
1011
1012        if (twitter_http(ic, TWITTER_HOME_TIMELINE_URL, twitter_http_get_home_timeline, ic, 0, args,
1013                     td->timeline_id ? 6 : 4) == NULL) {
1014                if (++td->http_fails >= 5)
1015                        imcb_error(ic, "Could not retrieve %s: %s",
1016                                   TWITTER_HOME_TIMELINE_URL, "connection failed");
1017                td->flags |= TWITTER_GOT_TIMELINE;
1018                twitter_flush_timeline(ic);
1019        }
1020
1021        g_free(args[1]);
1022        if (td->timeline_id) {
1023                g_free(args[5]);
1024        }
1025}
1026
1027/**
1028 * Get mentions.
1029 */
1030static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor)
1031{
1032        struct twitter_data *td = ic->proto_data;
1033
1034        txl_free(td->mentions_obj);
1035        td->mentions_obj = NULL;
1036        td->flags &= ~TWITTER_GOT_MENTIONS;
1037
1038        char *args[6];
1039        args[0] = "cursor";
1040        args[1] = g_strdup_printf("%lld", (long long) next_cursor);
1041        args[2] = "include_entities";
1042        args[3] = "true";
1043        if (td->timeline_id) {
1044                args[4] = "since_id";
1045                args[5] = g_strdup_printf("%llu", (long long unsigned int) td->timeline_id);
1046        } else {
1047                args[4] = "count";
1048                args[5] = g_strdup_printf("%d", set_getint(&ic->acc->set, "show_old_mentions"));
1049        }
1050
1051        if (twitter_http(ic, TWITTER_MENTIONS_URL, twitter_http_get_mentions,
1052                         ic, 0, args, 6) == NULL) {
1053                if (++td->http_fails >= 5)
1054                        imcb_error(ic, "Could not retrieve %s: %s",
1055                                   TWITTER_MENTIONS_URL, "connection failed");
1056                td->flags |= TWITTER_GOT_MENTIONS;
1057                twitter_flush_timeline(ic);
1058        }
1059
1060        g_free(args[1]);
1061        g_free(args[5]);
1062}
1063
1064/**
1065 * Callback for getting the home timeline.
1066 */
1067static void twitter_http_get_home_timeline(struct http_request *req)
1068{
1069        struct im_connection *ic = req->data;
1070        struct twitter_data *td;
1071        json_value *parsed;
1072        struct twitter_xml_list *txl;
1073
1074        // Check if the connection is still active.
1075        if (!g_slist_find(twitter_connections, ic))
1076                return;
1077
1078        td = ic->proto_data;
1079
1080        txl = g_new0(struct twitter_xml_list, 1);
1081        txl->list = NULL;
1082
1083        // The root <statuses> node should hold the list of statuses <status>
1084        if (!(parsed = twitter_parse_response(ic, req)))
1085                goto end;
1086        twitter_xt_get_status_list(ic, parsed, txl);
1087        json_value_free(parsed);
1088
1089        td->home_timeline_obj = txl;
1090
1091      end:
1092        if (!g_slist_find(twitter_connections, ic))
1093                return;
1094
1095        td->flags |= TWITTER_GOT_TIMELINE;
1096
1097        twitter_flush_timeline(ic);
1098}
1099
1100/**
1101 * Callback for getting mentions.
1102 */
1103static void twitter_http_get_mentions(struct http_request *req)
1104{
1105        struct im_connection *ic = req->data;
1106        struct twitter_data *td;
1107        json_value *parsed;
1108        struct twitter_xml_list *txl;
1109
1110        // Check if the connection is still active.
1111        if (!g_slist_find(twitter_connections, ic))
1112                return;
1113
1114        td = ic->proto_data;
1115
1116        txl = g_new0(struct twitter_xml_list, 1);
1117        txl->list = NULL;
1118
1119        // The root <statuses> node should hold the list of statuses <status>
1120        if (!(parsed = twitter_parse_response(ic, req)))
1121                goto end;
1122        twitter_xt_get_status_list(ic, parsed, txl);
1123        json_value_free(parsed);
1124
1125        td->mentions_obj = txl;
1126
1127      end:
1128        if (!g_slist_find(twitter_connections, ic))
1129                return;
1130
1131        td->flags |= TWITTER_GOT_MENTIONS;
1132
1133        twitter_flush_timeline(ic);
1134}
1135
1136/**
1137 * Callback to use after sending a POST request to twitter.
1138 * (Generic, used for a few kinds of queries.)
1139 */
1140static void twitter_http_post(struct http_request *req)
1141{
1142        struct im_connection *ic = req->data;
1143        struct twitter_data *td;
1144        json_value *parsed, *id;
1145
1146        // Check if the connection is still active.
1147        if (!g_slist_find(twitter_connections, ic))
1148                return;
1149
1150        td = ic->proto_data;
1151        td->last_status_id = 0;
1152
1153        if (!(parsed = twitter_parse_response(ic, req)))
1154                return;
1155       
1156        if ((id = json_o_get(parsed, "id")) && id->type == json_integer) {
1157                td->last_status_id = id->u.integer;
1158        }
1159       
1160        json_value_free(parsed);
1161       
1162        if (req->flags & TWITTER_HTTP_USER_ACK)
1163                twitter_log(ic, "Command processed successfully");
1164}
1165
1166/**
1167 * Function to POST a new status to twitter.
1168 */
1169void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_to)
1170{
1171        char *args[4] = {
1172                "status", msg,
1173                "in_reply_to_status_id",
1174                g_strdup_printf("%llu", (unsigned long long) in_reply_to)
1175        };
1176        twitter_http(ic, TWITTER_STATUS_UPDATE_URL, twitter_http_post, ic, 1,
1177                     args, in_reply_to ? 4 : 2);
1178        g_free(args[3]);
1179}
1180
1181
1182/**
1183 * Function to POST a new message to twitter.
1184 */
1185void twitter_direct_messages_new(struct im_connection *ic, char *who, char *msg)
1186{
1187        char *args[4];
1188        args[0] = "screen_name";
1189        args[1] = who;
1190        args[2] = "text";
1191        args[3] = msg;
1192        // Use the same callback as for twitter_post_status, since it does basically the same.
1193        twitter_http(ic, TWITTER_DIRECT_MESSAGES_NEW_URL, twitter_http_post, ic, 1, args, 4);
1194}
1195
1196void twitter_friendships_create_destroy(struct im_connection *ic, char *who, int create)
1197{
1198        char *args[2];
1199        args[0] = "screen_name";
1200        args[1] = who;
1201        twitter_http(ic, create ? TWITTER_FRIENDSHIPS_CREATE_URL : TWITTER_FRIENDSHIPS_DESTROY_URL,
1202                     twitter_http_post, ic, 1, args, 2);
1203}
1204
1205void twitter_status_destroy(struct im_connection *ic, guint64 id)
1206{
1207        char *url;
1208        url = g_strdup_printf("%s%llu%s", TWITTER_STATUS_DESTROY_URL,
1209                              (unsigned long long) id, ".json");
1210        twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0,
1211                       TWITTER_HTTP_USER_ACK);
1212        g_free(url);
1213}
1214
1215void twitter_status_retweet(struct im_connection *ic, guint64 id)
1216{
1217        char *url;
1218        url = g_strdup_printf("%s%llu%s", TWITTER_STATUS_RETWEET_URL,
1219                              (unsigned long long) id, ".json");
1220        twitter_http_f(ic, url, twitter_http_post, ic, 1, NULL, 0,
1221                       TWITTER_HTTP_USER_ACK);
1222        g_free(url);
1223}
1224
1225/**
1226 * Report a user for sending spam.
1227 */
1228void twitter_report_spam(struct im_connection *ic, char *screen_name)
1229{
1230        char *args[2] = {
1231                "screen_name",
1232                NULL,
1233        };
1234        args[1] = screen_name;
1235        twitter_http_f(ic, TWITTER_REPORT_SPAM_URL, twitter_http_post,
1236                       ic, 1, args, 2, TWITTER_HTTP_USER_ACK);
1237}
1238
1239/**
1240 * Favourite a tweet.
1241 */
1242void twitter_favourite_tweet(struct im_connection *ic, guint64 id)
1243{
1244        char *args[2] = {
1245                "id",
1246                NULL,
1247        };
1248        args[1] = g_strdup_printf("%llu", (unsigned long long) id);
1249        twitter_http_f(ic, TWITTER_FAVORITE_CREATE_URL, twitter_http_post,
1250                       ic, 1, args, 2, TWITTER_HTTP_USER_ACK);
1251        g_free(args[1]);
1252}
Note: See TracBrowser for help on using the repository browser.