source: protocols/twitter/twitter_lib.c @ 73ee390

Last change on this file since 73ee390 was 73ee390, checked in by jgeboski <jgeboski@…>, at 2015-01-26T03:46:03Z

twitter: implemented filter based group chats

Filter group chats allow for the ability to read the tweets of select
users without actually following the users, and/or track keywords or
hashtags. A filter group chat can have multiple users, keywords, or
hashtags. These users, keywords, or hashtags can span multiple group
chats. This allows for rather robust filter organization.

The underlying structure for the filters is based on linked list, as
using the glib hash tables requires >= glib-2.16 for sanity. Since the
glib requirement of bitlbee is only 2.14, linked list are used in order
to prevent an overly complex implementation.

The idea for this patch was inspired by Artem Savkov's "Twitter search
channels" patch.

In order to use the filter group chats, a group chat must be added to
the twitter account. The channel room name is either follow:username,
track:keyword, and/or track:#hashtag. Multiple elements can be used by
separating each element by a semicolon.

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