Index: protocols/twitter/twitter.c
===================================================================
--- protocols/twitter/twitter.c	(revision eb4ad8d31f7ed4210f24beb8753ecce594b0beef)
+++ protocols/twitter/twitter.c	(revision 73ee390abec21c2b724175d4a5c9cfc27aa85bcf)
@@ -31,4 +31,213 @@
 
 GSList *twitter_connections = NULL;
+
+static int twitter_filter_cmp(struct twitter_filter *tf1,
+                              struct twitter_filter *tf2)
+{
+	int i1 = 0;
+	int i2 = 0;
+	int i;
+
+	static const twitter_filter_type_t types[] = {
+		/* Order of the types */
+		TWITTER_FILTER_TYPE_FOLLOW,
+		TWITTER_FILTER_TYPE_TRACK
+	};
+
+	for (i = 0; i < G_N_ELEMENTS(types); i++) {
+		if (types[i] == tf1->type) {
+			i1 = i + 1;
+			break;
+		}
+	}
+
+	for (i = 0; i < G_N_ELEMENTS(types); i++) {
+		if (types[i] == tf2->type) {
+			i2 = i + 1;
+			break;
+		}
+	}
+
+	if (i1 != i2) {
+		/* With different types, return their difference */
+		return i1 - i2;
+	}
+
+	/* With the same type, return the text comparison */
+	return g_strcasecmp(tf1->text, tf2->text);
+}
+
+static gboolean twitter_filter_update(gpointer data, gint fd,
+                                      b_input_condition cond)
+{
+	struct im_connection *ic = data;
+	struct twitter_data *td = ic->proto_data;
+
+	if (td->filters) {
+		twitter_open_filter_stream(ic);
+	} else if (td->filter_stream) {
+		http_close(td->filter_stream);
+		td->filter_stream = NULL;
+	}
+
+	td->filter_update_id = 0;
+	return FALSE;
+}
+
+static struct twitter_filter *twitter_filter_get(struct groupchat *c,
+                                                 twitter_filter_type_t type,
+                                                 const char *text)
+{
+	struct twitter_data *td = c->ic->proto_data;
+	struct twitter_filter *tf = NULL;
+	struct twitter_filter tfc = {type, (char*) text};
+	GSList *l;
+
+	for (l = td->filters; l; l = g_slist_next(l)) {
+		tf = l->data;
+
+		if (twitter_filter_cmp(tf, &tfc) == 0)
+			break;
+
+		tf = NULL;
+	}
+
+	if (!tf) {
+		tf = g_new0(struct twitter_filter, 1);
+		tf->type = type;
+		tf->text = g_strdup(text);
+		td->filters = g_slist_prepend(td->filters, tf);
+	}
+
+	if (!g_slist_find(tf->groupchats, c))
+		tf->groupchats = g_slist_prepend(tf->groupchats, c);
+
+	if (td->filter_update_id > 0)
+		b_event_remove(td->filter_update_id);
+
+	/* Wait for other possible filter changes to avoid request spam */
+	td->filter_update_id = b_timeout_add(TWITTER_FILTER_UPDATE_WAIT,
+					     twitter_filter_update, c->ic);
+	return tf;
+}
+
+static void twitter_filter_free(struct twitter_filter *tf)
+{
+	g_slist_free(tf->groupchats);
+	g_free(tf->text);
+	g_free(tf);
+}
+
+static void twitter_filter_remove(struct groupchat *c)
+{
+	struct twitter_data *td = c->ic->proto_data;
+	struct twitter_filter *tf;
+	GSList *l = td->filters;
+	GSList *p;
+
+	while (l != NULL) {
+		tf = l->data;
+		tf->groupchats = g_slist_remove(tf->groupchats, c);
+
+		p = l;
+		l = g_slist_next(l);
+
+		if (!tf->groupchats) {
+			twitter_filter_free(tf);
+			td->filters = g_slist_delete_link(td->filters, p);
+		}
+	}
+
+	if (td->filter_update_id > 0)
+		b_event_remove(td->filter_update_id);
+
+	/* Wait for other possible filter changes to avoid request spam */
+	td->filter_update_id = b_timeout_add(TWITTER_FILTER_UPDATE_WAIT,
+					     twitter_filter_update, c->ic);}
+
+static void twitter_filter_remove_all(struct im_connection *ic)
+{
+	struct twitter_data *td = ic->proto_data;
+	GSList *chats = NULL;
+	struct twitter_filter *tf;
+	GSList *l = td->filters;
+	GSList *p;
+
+	while (l != NULL) {
+		tf = l->data;
+
+		/* Build up a list of groupchats to be freed */
+		for (p = tf->groupchats; p; p = g_slist_next(p)) {
+			if (!g_slist_find(chats, p->data))
+				chats = g_slist_prepend(chats, p->data);
+		}
+
+		p = l;
+		l = g_slist_next(l);
+		twitter_filter_free(p->data);
+		td->filters = g_slist_delete_link(td->filters, p);
+	}
+
+	l = chats;
+
+	while (l != NULL) {
+		p = l;
+		l = g_slist_next(l);
+
+		/* Freed each remaining groupchat */
+		imcb_chat_free(p->data);
+		chats = g_slist_delete_link(chats, p);
+	}
+
+	if (td->filter_stream) {
+		http_close(td->filter_stream);
+		td->filter_stream = NULL;
+	}
+}
+
+static GSList *twitter_filter_parse(struct groupchat *c, const char *text)
+{
+	char **fs = g_strsplit(text, ";", 0);
+	GSList *ret = NULL;
+	struct twitter_filter *tf;
+	char **f;
+	char *v;
+	int i;
+	int t;
+
+	static const twitter_filter_type_t types[] = {
+		TWITTER_FILTER_TYPE_FOLLOW,
+		TWITTER_FILTER_TYPE_TRACK
+	};
+
+	static const char *typestrs[] = {
+		"follow",
+		"track"
+	};
+
+	for (f = fs; *f; f++) {
+		if ((v = strchr(*f, ':')) == NULL)
+			continue;
+
+		*(v++) = 0;
+
+		for (t = -1, i = 0; i < G_N_ELEMENTS(types); i++) {
+			if (g_strcasecmp(typestrs[i], *f) == 0) {
+				t = i;
+				break;
+			}
+		}
+
+		if (t < 0 || strlen(v) == 0)
+			continue;
+
+		tf = twitter_filter_get(c, types[t], v);
+		ret = g_slist_prepend(ret, tf);
+	}
+
+	g_strfreev(fs);
+	return ret;
+}
+
 /**
  * Main loop function
@@ -436,5 +645,9 @@
 
 	if (td) {
+		if (td->filter_update_id > 0)
+			b_event_remove(td->filter_update_id);
+
 		http_close(td->stream);
+		twitter_filter_remove_all(ic);
 		oauth_info_free(td->oauth_info);
 		g_free(td->user);
@@ -509,10 +722,55 @@
 }
 
+static struct groupchat *twitter_chat_join(struct im_connection *ic,
+                                           const char *room, const char *nick,
+                                           const char *password, set_t **sets)
+{
+	struct groupchat *c = imcb_chat_new(ic, room);
+	GSList *fs = twitter_filter_parse(c, room);
+	GString *topic = g_string_new("");
+	struct twitter_filter *tf;
+	GSList *l;
+
+	fs = g_slist_sort(fs, (GCompareFunc) twitter_filter_cmp);
+
+	for (l = fs; l; l = g_slist_next(l)) {
+		tf = l->data;
+
+		if (topic->len > 0)
+			g_string_append(topic, ", ");
+
+		if (tf->type == TWITTER_FILTER_TYPE_FOLLOW)
+			g_string_append_c(topic, '@');
+
+		g_string_append(topic, tf->text);
+	}
+
+	if (topic->len > 0)
+		g_string_prepend(topic, "Twitter Filter: ");
+
+	imcb_chat_topic(c, NULL, topic->str, 0);
+	imcb_chat_add_buddy(c, ic->acc->user);
+
+	if (topic->len == 0) {
+		imcb_error(ic, "Failed to handle any filters");
+		imcb_chat_free(c);
+		c = NULL;
+	}
+
+	g_string_free(topic, TRUE);
+	g_slist_free(fs);
+
+	return c;
+}
+
 static void twitter_chat_leave(struct groupchat *c)
 {
 	struct twitter_data *td = c->ic->proto_data;
 
-	if (c != td->timeline_gc)
-		return;		/* WTF? */
+	if (c != td->timeline_gc) {
+		twitter_filter_remove(c);
+		imcb_chat_free(c);
+		return;
+	}
 
 	/* If the user leaves the channel: Fine. Rejoin him/her once new
@@ -748,4 +1006,5 @@
 	ret->chat_msg = twitter_chat_msg;
 	ret->chat_invite = twitter_chat_invite;
+	ret->chat_join = twitter_chat_join;
 	ret->chat_leave = twitter_chat_leave;
 	ret->keepalive = twitter_keepalive;
Index: protocols/twitter/twitter.h
===================================================================
--- protocols/twitter/twitter.h	(revision eb4ad8d31f7ed4210f24beb8753ecce594b0beef)
+++ protocols/twitter/twitter.h	(revision 73ee390abec21c2b724175d4a5c9cfc27aa85bcf)
@@ -45,4 +45,10 @@
 } twitter_flags_t;
 
+typedef enum
+{
+	TWITTER_FILTER_TYPE_FOLLOW = 0,
+	TWITTER_FILTER_TYPE_TRACK
+} twitter_filter_type_t;
+
 struct twitter_log_data;
 
@@ -58,8 +64,11 @@
 
 	GSList *follow_ids;
+	GSList *filters;
 	
 	guint64 last_status_id; /* For undo */
 	gint main_loop_id;
+	gint filter_update_id;
 	struct http_request *stream;
+	struct http_request *filter_stream;
 	struct groupchat *timeline_gc;
 	gint http_fails;
@@ -77,4 +86,13 @@
 	struct twitter_log_data *log;
 	int log_id;
+};
+
+#define TWITTER_FILTER_UPDATE_WAIT 3000
+struct twitter_filter
+{
+	twitter_filter_type_t type;
+	char *text;
+	guint64 uid;
+	GSList *groupchats;
 };
 
Index: protocols/twitter/twitter_lib.c
===================================================================
--- protocols/twitter/twitter_lib.c	(revision eb4ad8d31f7ed4210f24beb8753ecce594b0beef)
+++ protocols/twitter/twitter_lib.c	(revision 73ee390abec21c2b724175d4a5c9cfc27aa85bcf)
@@ -51,4 +51,5 @@
 
 struct twitter_xml_user {
+	guint64 uid;
 	char *name;
 	char *screen_name;
@@ -61,4 +62,5 @@
 	guint64 id, rt_id; /* Usually equal, with RTs id == *original* id */
 	guint64 reply_to;
+	gboolean from_filter;
 };
 
@@ -392,8 +394,12 @@
 {
 	struct twitter_xml_user *txu;
+	json_value *jv;
 	
 	txu = g_new0(struct twitter_xml_user, 1);
 	txu->name = g_strdup(json_o_str(node, "name"));
 	txu->screen_name = g_strdup(json_o_str(node, "screen_name"));
+	
+	jv = json_o_get(node, "id");
+	txu->uid = jv->u.integer;
 	
 	return txu;
@@ -657,4 +663,42 @@
 
 /**
+ * Function that is called to see the filter statuses in groupchat windows.
+ */
+static void twitter_status_show_filter(struct im_connection *ic, struct twitter_xml_status *status)
+{
+	struct twitter_data *td = ic->proto_data;
+	char *msg = twitter_msg_add_id(ic, status, "");
+	struct twitter_filter *tf;
+	GSList *f;
+	GSList *l;
+
+	for (f = td->filters; f; f = g_slist_next(f)) {
+		tf = f->data;
+
+		switch (tf->type) {
+		case TWITTER_FILTER_TYPE_FOLLOW:
+			if (status->user->uid != tf->uid)
+				continue;
+			break;
+
+		case TWITTER_FILTER_TYPE_TRACK:
+			if (strcasestr(status->text, tf->text) == NULL)
+				continue;
+			break;
+
+		default:
+			continue;
+		}
+
+		for (l = tf->groupchats; l; l = g_slist_next(l)) {
+			imcb_chat_msg(l->data, status->user->screen_name,
+				      msg ? msg : status->text, 0, 0);
+		}
+	}
+
+	g_free(msg);
+}
+
+/**
  * Function that is called to see the statuses in a groupchat window.
  */
@@ -731,5 +775,7 @@
 		strip_newlines(status->text);
 	
-	if (td->flags & TWITTER_MODE_CHAT)
+	if (status->from_filter)
+		twitter_status_show_filter(ic, status);
+	else if (td->flags & TWITTER_MODE_CHAT)
 		twitter_status_show_chat(ic, status);
 	else
@@ -745,5 +791,5 @@
 }
 
-static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o);
+static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o, gboolean from_filter);
 
 static void twitter_http_stream(struct http_request *req)
@@ -754,4 +800,5 @@
 	int len = 0;
 	char c, *nl;
+	gboolean from_filter;
 	
 	if (!g_slist_find(twitter_connections, ic))
@@ -762,5 +809,9 @@
 	
 	if ((req->flags & HTTPC_EOF) || !req->reply_body) {
-		td->stream = NULL;
+		if (req == td->stream)
+			td->stream = NULL;
+		else if (req == td->filter_stream)
+			td->filter_stream = NULL;
+
 		imcb_error(ic, "Stream closed (%s)", req->status_string);
 		imc_logout(ic, TRUE);
@@ -779,5 +830,6 @@
 		
 		if ((parsed = json_parse(req->reply_body, req->body_size))) {
-			twitter_stream_handle_object(ic, parsed);
+			from_filter = (req == td->filter_stream);
+			twitter_stream_handle_object(ic, parsed, from_filter);
 		}
 		json_value_free(parsed);
@@ -795,5 +847,5 @@
 static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs);
 
-static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o)
+static gboolean twitter_stream_handle_object(struct im_connection *ic, json_value *o, gboolean from_filter)
 {
 	struct twitter_data *td = ic->proto_data;
@@ -802,4 +854,5 @@
 	
 	if ((txs = twitter_xt_get_status(o))) {
+		txs->from_filter = from_filter;
 		gboolean ret = twitter_stream_handle_status(ic, txs);
 		txs_free(txs);
@@ -899,4 +952,167 @@
 }
 
+static gboolean twitter_filter_stream(struct im_connection *ic)
+{
+	struct twitter_data *td = ic->proto_data;
+	char *args[4] = {"follow", NULL, "track", NULL};
+	GString *followstr = g_string_new("");
+	GString *trackstr = g_string_new("");
+	gboolean ret = FALSE;
+	struct twitter_filter *tf;
+	GSList *l;
+
+	for (l = td->filters; l; l = g_slist_next(l)) {
+		tf = l->data;
+
+		switch (tf->type) {
+		case TWITTER_FILTER_TYPE_FOLLOW:
+			if (followstr->len > 0)
+				g_string_append_c(followstr, ',');
+
+			g_string_append_printf(followstr, "%" G_GUINT64_FORMAT,
+					       tf->uid);
+			break;
+
+		case TWITTER_FILTER_TYPE_TRACK:
+			if (trackstr->len > 0)
+				g_string_append_c(trackstr, ',');
+
+			g_string_append(trackstr, tf->text);
+			break;
+
+		default:
+			continue;
+		}
+	}
+
+	args[1] = followstr->str;
+	args[3] = trackstr->str;
+
+	if (td->filter_stream)
+		http_close(td->filter_stream);
+
+	if ((td->filter_stream = twitter_http(ic, TWITTER_FILTER_STREAM_URL,
+	                                      twitter_http_stream, ic, 0,
+					      args, 4))) {
+		/* This flag must be enabled or we'll get no data until EOF
+		   (which err, kind of, defeats the purpose of a streaming API). */
+		td->filter_stream->flags |= HTTPC_STREAMING;
+		ret = TRUE;
+	}
+
+	g_string_free(followstr, TRUE);
+	g_string_free(trackstr, TRUE);
+
+	return ret;
+}
+
+static void twitter_filter_users_post(struct http_request *req)
+{
+	struct im_connection *ic = req->data;
+	struct twitter_data *td;
+	struct twitter_filter *tf;
+	GList *users = NULL;
+	json_value *parsed;
+	json_value *id;
+	const char *name;
+	GString *fstr;
+	GSList *l;
+	GList *u;
+	int i;
+
+	// Check if the connection is still active.
+	if (!g_slist_find(twitter_connections, ic))
+		return;
+
+	td = ic->proto_data;
+
+	if (!(parsed = twitter_parse_response(ic, req)))
+		return;
+
+	for (l = td->filters; l; l = g_slist_next(l)) {
+		tf = l->data;
+
+		if (tf->type == TWITTER_FILTER_TYPE_FOLLOW)
+			users = g_list_prepend(users, tf);
+	}
+
+	if (parsed->type != json_array)
+		goto finish;
+
+	for (i = 0; i < parsed->u.array.length; i++) {
+		id = json_o_get(parsed->u.array.values[i], "id");
+		name = json_o_str(parsed->u.array.values[i], "screen_name");
+
+		if (!name || !id || id->type != json_integer)
+			continue;
+
+		for (u = users; u; u = g_list_next(u)) {
+			tf = u->data;
+
+			if (g_strcasecmp(tf->text, name) == 0) {
+				tf->uid = id->u.integer;
+				users = g_list_delete_link(users, u);
+				break;
+			}
+		}
+	}
+
+finish:
+	json_value_free(parsed);
+	twitter_filter_stream(ic);
+
+	if (!users)
+		return;
+
+	fstr = g_string_new("");
+
+	for (u = users; u; u = g_list_next(u)) {
+		if (fstr->len > 0)
+			g_string_append(fstr, ", ");
+
+		g_string_append(fstr, tf->text);
+	}
+
+	imcb_error(ic, "Failed UID acquisitions: %s", fstr->str);
+
+	g_string_free(fstr, TRUE);
+	g_list_free(users);
+}
+
+gboolean twitter_open_filter_stream(struct im_connection *ic)
+{
+	struct twitter_data *td = ic->proto_data;
+	char *args[2] = {"screen_name", NULL};
+	GString *ustr = g_string_new("");
+	struct twitter_filter *tf;
+	struct http_request *req;
+	GSList *l;
+
+	for (l = td->filters; l; l = g_slist_next(l)) {
+		tf = l->data;
+
+		if (tf->type != TWITTER_FILTER_TYPE_FOLLOW || tf->uid != 0)
+			continue;
+
+		if (ustr->len > 0)
+			g_string_append_c(ustr, ',');
+
+		g_string_append(ustr, tf->text);
+	}
+
+	if (ustr->len == 0) {
+		g_string_free(ustr, TRUE);
+		return twitter_filter_stream(ic);
+	}
+
+	args[1] = ustr->str;
+	req = twitter_http(ic, TWITTER_USERS_LOOKUP_URL,
+			   twitter_filter_users_post,
+			   ic, 0, args, 2);
+
+	g_string_free(ustr, TRUE);
+	return req != NULL;
+}
+
 static void twitter_get_home_timeline(struct im_connection *ic, gint64 next_cursor);
 static void twitter_get_mentions(struct im_connection *ic, gint64 next_cursor);
Index: protocols/twitter/twitter_lib.h
===================================================================
--- protocols/twitter/twitter_lib.h	(revision eb4ad8d31f7ed4210f24beb8753ecce594b0beef)
+++ protocols/twitter/twitter_lib.h	(revision 73ee390abec21c2b724175d4a5c9cfc27aa85bcf)
@@ -79,7 +79,10 @@
 #define TWITTER_REPORT_SPAM_URL "/users/report_spam.json"
 
+/* Stream URLs */
 #define TWITTER_USER_STREAM_URL "https://userstream.twitter.com/1.1/user.json"
+#define TWITTER_FILTER_STREAM_URL "https://stream.twitter.com/1.1/statuses/filter.json"
 
 gboolean twitter_open_stream(struct im_connection *ic);
+gboolean twitter_open_filter_stream(struct im_connection *ic);
 gboolean twitter_get_timeline(struct im_connection *ic, gint64 next_cursor);
 void twitter_get_friends_ids(struct im_connection *ic, gint64 next_cursor);
