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