diff options
Diffstat (limited to 'protocols')
| -rw-r--r-- | protocols/account.c | 92 | ||||
| -rw-r--r-- | protocols/jabber/jabber.c | 6 | ||||
| -rw-r--r-- | protocols/jabber/jabber.h | 18 | ||||
| -rw-r--r-- | protocols/jabber/sasl.c | 263 | ||||
| -rw-r--r-- | protocols/nogaim.c | 24 | ||||
| -rw-r--r-- | protocols/purple/purple.c | 24 | ||||
| -rw-r--r-- | protocols/twitter/twitter.c | 113 | ||||
| -rw-r--r-- | protocols/twitter/twitter.h | 13 | ||||
| -rw-r--r-- | protocols/twitter/twitter_lib.c | 244 | ||||
| -rw-r--r-- | protocols/twitter/twitter_lib.h | 10 | 
10 files changed, 713 insertions, 94 deletions
diff --git a/protocols/account.c b/protocols/account.c index b5ff4e88..80b1d020 100644 --- a/protocols/account.c +++ b/protocols/account.c @@ -77,6 +77,8 @@ account_t *account_add(bee_t *bee, struct prpl *prpl, char *user, char *pass)  	s = set_add(&a->set, "username", NULL, set_eval_account, a);  	s->flags |= SET_NOSAVE | ACC_SET_OFFLINE_ONLY | ACC_SET_LOCKABLE;  	set_setstr(&a->set, "username", user); +	set_add(&a->set, "offline_user_quits", "true", set_eval_bool, a); +	set_add(&a->set, "offline_is_away", "false", set_eval_bool, a);  	if (prpl == &protocol_missing) {  		s = set_add(&a->set, "server", NULL, set_eval_account, a); @@ -130,6 +132,36 @@ account_t *account_add(bee_t *bee, struct prpl *prpl, char *user, char *pass)  	return a;  } +void account_update_channel_set(irc_channel_t *ic, char *old_tag, char *new_tag) +{ +	gboolean found = FALSE; +	char **account, **accounts; +	char *saccount = set_getstr(&ic->set, "account"); + +	if (saccount == NULL || *saccount == '\0') { +		return; +	} + +	accounts = g_strsplit(saccount, ",", 0); +	for (account = accounts; *account; account++) { +		if (g_strcasecmp(*account, old_tag) == 0) { +			found = TRUE; +			break; +		} +	} + +	if (found) { +		g_free(*account); +		*account = g_strdup(new_tag); + +		saccount = g_strjoinv(",", accounts); +		set_setstr(&ic->set, "account", saccount); +		g_free(saccount); +	} + +	g_strfreev(accounts); +} +  char *set_eval_account(set_t *set, char *value)  {  	account_t *acc = set->data; @@ -171,14 +203,28 @@ char *set_eval_account(set_t *set, char *value)  		return NULL;    /* password shouldn't be visible in plaintext! */  	} else if (strcmp(set->key, "tag") == 0) {  		account_t *oa; +		irc_t *irc; +		GSList *l; +		char *old;  		/* Enforce uniqueness. */  		if ((oa = account_by_tag(acc->bee, value)) && oa != acc) {  			return SET_INVALID;  		} -		g_free(acc->tag); +		old = acc->tag;  		acc->tag = g_strdup(value); + +		irc = acc->bee->ui_data; +		for (l = irc->channels; l; l = l->next) { +			irc_channel_t *ic = l->data; + +			if (g_strcasecmp(set_getstr(&ic->set, "type"), "control") == 0) { +				account_update_channel_set(ic, old, value); +			} +		} + +		g_free(old);  		return value;  	} else if (strcmp(set->key, "auto_connect") == 0) {  		if (!is_bool(value)) { @@ -299,9 +345,43 @@ account_t *account_by_tag(bee_t *bee, const char *tag)  	return NULL;  } +void account_remove_from_channel_set(irc_channel_t *ic, char *tag) +{ +	gboolean found = FALSE; +	char **account, **accounts; +	char *saccount = set_getstr(&ic->set, "account"); + +	if (saccount == NULL || *saccount == '\0') { +		return; +	} + +	accounts = g_strsplit(saccount, ",", 0); +	for (account = accounts; *account; account++) { +		if (g_strcasecmp(*account, tag) == 0) { +			found = TRUE; +			break; +		} +	} + +	if (found) { +		g_free(*account); + +		do { +			*account = *(account + 1); +		} while (*(++account) != NULL); + +		saccount = g_strjoinv(",", accounts); +		set_setstr(&ic->set, "account", saccount); +	} + +	g_strfreev(accounts); +} +  void account_del(bee_t *bee, account_t *acc)  {  	account_t *a, *l = NULL; +	GSList *accl; +	irc_t *irc;  	if (acc->ic) {  		/* Caller should have checked, accounts still in use can't be deleted. */ @@ -325,6 +405,16 @@ void account_del(bee_t *bee, account_t *acc)  			}  			*/ +			/* Remove from channel set account */ +			irc = acc->bee->ui_data; +			for (accl = irc->channels; accl; accl = accl->next) { +				irc_channel_t *ic = accl->data; + +				if (g_strcasecmp(set_getstr(&ic->set, "type"), "control") == 0) { +					account_remove_from_channel_set(ic, acc->tag); +				} +			} +  			while (a->set) {  				set_del(&a->set, a->set->key);  			} diff --git a/protocols/jabber/jabber.c b/protocols/jabber/jabber.c index f31abc50..73bc77ea 100644 --- a/protocols/jabber/jabber.c +++ b/protocols/jabber/jabber.c @@ -116,6 +116,9 @@ static void jabber_init(account_t *acc)  	s = set_add(&acc->set, "carbons", "true", set_eval_bool, acc);  	s->flags |= ACC_SET_OFFLINE_ONLY; +	s = set_add(&acc->set, "disable_scram", "false", set_eval_bool, acc); +	s->flags |= SET_HIDDEN_DEFAULT; +  	acc->flags |= ACC_FLAG_AWAY_MESSAGE | ACC_FLAG_STATUS_MESSAGE |  	              ACC_FLAG_HANDLE_DOMAINS;  } @@ -379,6 +382,9 @@ static void jabber_logout(struct im_connection *ic)  	g_free(jd->muc_host);  	g_free(jd->username);  	g_free(jd->me); +	g_free(jd->challenge.cnonce); +	g_free(jd->challenge.server_signature); +	g_free(jd->challenge.cb_header);  	g_free(jd);  	jabber_connections = g_slist_remove(jabber_connections, ic); diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h index e1087315..5100b7a9 100644 --- a/protocols/jabber/jabber.h +++ b/protocols/jabber/jabber.h @@ -77,6 +77,16 @@ typedef enum {  	JCFLAG_ALWAYS_USE_NICKS = 2,  } jabber_chat_flags_t; +typedef enum { +	JCHALLENGE_DIGEST_MD5, +	JCHALLENGE_SCRAM +} jabber_challenge_t; + +typedef enum { +	JSCRAM_SHA1   = 0x0001, +	JSCRAM_SHA256 = 0x0002 +} jabber_scram_t; +  struct jabber_data {  	struct im_connection *ic; @@ -94,6 +104,14 @@ struct jabber_data {  	char *me;               /* bare jid */  	char *internal_jid; +	struct { +		jabber_challenge_t type; +		int scram_algo; +		char *cnonce; +		char *server_signature; +		char *cb_header; +	} challenge; +  	const struct oauth2_service *oauth2_service;  	char *oauth2_access_token; diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c index 77a2cee0..d4556811 100644 --- a/protocols/jabber/sasl.c +++ b/protocols/jabber/sasl.c @@ -22,11 +22,15 @@  \***************************************************************************/  #include <ctype.h> +#include <gcrypt.h> +#include <gnutls/gnutls.h> +#include <stringprep.h>  #include "jabber.h"  #include "base64.h"  #include "oauth2.h"  #include "oauth.h" +#include "sha1.h"  const struct oauth2_service oauth2_service_google =  { @@ -47,7 +51,7 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)  	struct jabber_data *jd = ic->proto_data;  	struct xt_node *c, *reply;  	char *s; -	int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_hipchat_oauth = 0; +	int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_scram = 0, sup_hipchat_oauth = 0;  	int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE;  	GString *mechs; @@ -74,26 +78,30 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)  	mechs = g_string_new("");  	c = node->children;  	while ((c = xt_find_node(c, "mechanism"))) { -		if (c->text && g_strcasecmp(c->text, "PLAIN") == 0) { -			sup_plain = 1; -		} else if (c->text && g_strcasecmp(c->text, "DIGEST-MD5") == 0) { -			sup_digest = 1; -		} else if (c->text && g_strcasecmp(c->text, "ANONYMOUS") == 0) { -			sup_anonymous = 1; -		} else if (c->text && g_strcasecmp(c->text, "X-OAUTH2") == 0) { -			sup_gtalk = 1; -		} else if (c->text && g_strcasecmp(c->text, "X-HIPCHAT-OAUTH2") == 0) { -			sup_hipchat_oauth = 1; -		} -  		if (c->text) { +			if (g_strcasecmp(c->text, "PLAIN") == 0) { +				sup_plain = 1; +			} else if (g_strcasecmp(c->text, "DIGEST-MD5") == 0) { +				sup_digest = 1; +			} else if (g_strcasecmp(c->text, "ANONYMOUS") == 0) { +				sup_anonymous = 1; +			} else if (g_strcasecmp(c->text, "X-OAUTH2") == 0) { +				sup_gtalk = 1; +			} else if (g_strcasecmp(c->text, "X-HIPCHAT-OAUTH2") == 0) { +				sup_hipchat_oauth = 1; +			} else if (g_strcasecmp(c->text, "SCRAM-SHA-1") == 0) { +				sup_scram |= JSCRAM_SHA1; +			} else if (g_strcasecmp(c->text, "SCRAM-SHA-256") == 0) { +				sup_scram |= JSCRAM_SHA256; +			} +  			g_string_append_printf(mechs, " %s", c->text);  		}  		c = c->next;  	} -	if (!want_oauth && !sup_plain && !sup_digest) { +	if (!want_oauth && !want_anonymous && !sup_plain && !sup_digest && !sup_scram) {  		if (sup_gtalk || sup_hipchat_oauth) {  			imcb_error(ic, "This server requires OAuth "  			           "(supported schemes:%s)", mechs->str); @@ -158,11 +166,47 @@ xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)  		imc_logout(ic, FALSE);  		xt_free_node(reply);  		return XT_ABORT; +	} else if (sup_scram && !set_getbool(&ic->acc->set, "disable_scram")) { +		int rc; +		unsigned char cnonce_bin[30]; +		char *puser = NULL; + +		if (sup_scram & JSCRAM_SHA256) { +			jd->challenge.type = JCHALLENGE_SCRAM; +			jd->challenge.scram_algo = GCRY_MD_SHA256; +			xt_add_attr(reply, "mechanism", "SCRAM-SHA-256"); +		} else if (sup_scram & JSCRAM_SHA1) { +			jd->challenge.type = JCHALLENGE_SCRAM; +			jd->challenge.scram_algo = GCRY_MD_SHA1; +			xt_add_attr(reply, "mechanism", "SCRAM-SHA-1"); +		} else { +			imcb_error(ic, "Unknown scram method"); /* Just in case, but we should not get here */ +			return XT_ABORT; +		} + +		rc = stringprep_profile(jd->username, &puser, "SASLprep", 0); +		if (rc != STRINGPREP_OK) { +			imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); +			return XT_ABORT; +		} + +		random_bytes(cnonce_bin, sizeof(cnonce_bin)); +		jd->challenge.cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin)); + +		jd->challenge.cb_header = g_strdup("n,,"); + +		s = g_strdup_printf("%sn=%s,r=%s", jd->challenge.cb_header, puser, jd->challenge.cnonce); + +		reply->text = base64_encode((unsigned char *)s , strlen(s)); +		reply->text_len = strlen(reply->text); +		g_free(puser); +		g_free(s);  	} else if (sup_digest && !(jd->ssl && sup_plain)) {  		/* Only try DIGEST-MD5 if there's no SSL/TLS or if PLAIN isn't supported.  		 * Which in practice means "don't bother with DIGEST-MD5 most of the time".  		 * It's weak, pointless over TLS, and often breaks with some servers (hi openfire) */ +		jd->challenge.type = JCHALLENGE_DIGEST_MD5;  		xt_add_attr(reply, "mechanism", "DIGEST-MD5");  		/* The rest will be done later, when we receive a <challenge/>. */ @@ -281,7 +325,7 @@ char *sasl_get_part(char *data, char *field)  	}  } -xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data) +static xt_status sasl_pkt_challenge_digest_md5(struct xt_node *node, gpointer data)  {  	struct im_connection *ic = data;  	struct jabber_data *jd = ic->proto_data; @@ -400,6 +444,191 @@ silent_error:  	return ret;  } +static void hmac(int algo, unsigned char *key, size_t key_len, unsigned char *buf, size_t buf_len, unsigned char *out) +{ +	gcry_md_hd_t digest; +	unsigned char *tmp; + +	gcry_md_open(&digest, algo, GCRY_MD_FLAG_HMAC); +	gcry_md_setkey(digest, key, key_len); +	gcry_md_write(digest, buf, buf_len); + +	tmp = gcry_md_read(digest, 0); +	memcpy(out, tmp, gcry_md_get_algo_dlen(algo)); + +	gcry_md_close(digest); +} + +static xt_status sasl_pkt_challenge_scram(struct xt_node *node, gpointer data) +{ +	struct im_connection *ic = data; +	struct jabber_data *jd = ic->proto_data; +	struct xt_node *reply_pkt = NULL; +	xt_status ret = XT_ABORT; +	char *random = NULL, *salt64 = NULL, *iter_str = NULL, *encoded_proof = NULL, +	     *reply = NULL, *client_first_bare = NULL, *server_first = NULL, *cb_header64 = NULL, +	     *client_final_noproof = NULL, *auth_message = NULL, *client_final = NULL, *puser = NULL, *ppass = NULL; + +	unsigned char *salt = NULL; +	size_t salt_len, iter_count, i; + +	int algo = jd->challenge.scram_algo, rc; +	size_t md_len = gcry_md_get_algo_dlen(algo); + +	unsigned char client_key[md_len]; +	unsigned char client_proof[md_len]; +	unsigned char client_signature[md_len]; +	unsigned char salted_password[md_len]; +	unsigned char server_key[md_len]; +	unsigned char server_signature[md_len]; +	unsigned char stored_key[md_len]; + +	if (node->text_len == 0) { +		goto error; +	} + +	server_first = frombase64(node->text); + +	random = sasl_get_part(server_first, "r"); +	salt64 = sasl_get_part(server_first, "s"); +	iter_str = sasl_get_part(server_first, "i"); +	iter_count = atoi(iter_str); + +	if (strncmp(random, jd->challenge.cnonce, strlen(jd->challenge.cnonce)) != 0) { +		imcb_error(ic, "Server nonce doesn't start with client nonce"); +		goto error; +	} + +	rc = stringprep_profile(jd->username, &puser, "SASLprep", 0); +	if (rc != STRINGPREP_OK) { +		imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); +		goto error; +	} +	rc = stringprep_profile(ic->acc->pass, &ppass, "SASLprep", 0); +	if (rc != STRINGPREP_OK) { +		imcb_error(ic, "SASLprep failed: %s", stringprep_strerror(rc)); +		goto error; +	} + +	salt_len = base64_decode(salt64, &salt); + +	if (gcry_kdf_derive(ppass, strlen(ppass), +			GCRY_KDF_PBKDF2, algo, +			salt, salt_len, iter_count, +			sizeof(salted_password), salted_password) != 0) { +		imcb_error(ic, "PBKDF failed"); +		goto error; +	} + +	hmac(algo, salted_password, sizeof(salted_password), (unsigned char *)"Client Key", 10, client_key); + +	gcry_md_hash_buffer(algo, stored_key, client_key, sizeof(client_key)); + +	cb_header64 = tobase64(jd->challenge.cb_header); +	client_first_bare = g_strdup_printf("n=%s,r=%s", puser, jd->challenge.cnonce); +	client_final_noproof = g_strdup_printf("c=%s,r=%s", cb_header64, random); + +	auth_message = g_strdup_printf("%s,%s,%s", client_first_bare, server_first, client_final_noproof); + +	hmac(algo, stored_key, sizeof(stored_key), (unsigned char *)auth_message, strlen(auth_message), client_signature); + +	for (i = 0; i < sizeof(client_key); i++) { +		client_proof[i] = client_key[i] ^ client_signature[i]; +	} + +	encoded_proof = base64_encode(client_proof, sizeof(client_proof)); + +	hmac(algo, salted_password, sizeof(salted_password), (unsigned char *)"Server Key", 10, server_key); +	hmac(algo, server_key, sizeof(server_key), (unsigned char *)auth_message, strlen(auth_message), server_signature); + +	jd->challenge.server_signature = base64_encode(server_signature, sizeof(server_signature)); + +	client_final = g_strdup_printf("%s,p=%s", client_final_noproof, encoded_proof); +	reply = tobase64(client_final); +	reply_pkt = xt_new_node("response", reply, NULL); +	xt_add_attr(reply_pkt, "xmlns", XMLNS_SASL); + +	if (!jabber_write_packet(ic, reply_pkt)) { +		goto silent_error; +	} + +	ret = XT_HANDLED; +	goto silent_error; + +error: +	imcb_error(ic, "Incorrect SASL challenge received"); +	imc_logout(ic, FALSE); + +silent_error: +	g_free(puser); +	g_free(ppass); +	g_free(random); +	g_free(salt64); +	g_free(salt); +	g_free(iter_str); +	g_free(encoded_proof); +	g_free(cb_header64); +	g_free(client_first_bare); +	g_free(client_final_noproof); +	g_free(client_final); +	g_free(auth_message); +	g_free(reply); +	g_free(jd->challenge.cnonce); +	jd->challenge.cnonce = NULL; +	g_free(jd->challenge.cb_header); +	jd->challenge.cb_header = NULL; +	xt_free_node(reply_pkt); +	return ret; +} + +xt_status sasl_pkt_challenge(struct xt_node *node, gpointer data) +{ +	struct im_connection *ic = data; +	struct jabber_data *jd = ic->proto_data; + +	switch (jd->challenge.type) { +	case JCHALLENGE_DIGEST_MD5: +		return sasl_pkt_challenge_digest_md5(node, data); + +	case JCHALLENGE_SCRAM: +		return sasl_pkt_challenge_scram(node, data); + +	default: +		imcb_error(ic, "Invalid challenge type"); +		return XT_ABORT; +	} +} + +static xt_status sasl_pkt_validate_scram_success(struct xt_node *node, gpointer data) +{ +	struct im_connection *ic = data; +	struct jabber_data *jd = ic->proto_data; +	char *v, *dec; +	xt_status ret = XT_HANDLED; + +	if (node->text_len == 0) { +		imcb_error(ic, "Server didn't send signature"); +		imc_logout(ic, FALSE); +		ret = XT_ABORT; +		goto error; +	} + +	dec = frombase64(node->text); +	v = sasl_get_part(dec, "v"); + +	if (g_strcmp0(v, jd->challenge.server_signature) != 0) { +		imcb_error(ic, "Invalid server signature"); +		imc_logout(ic, FALSE); +		ret = XT_ABORT; +		goto error; +	} + +error: +	g_free(jd->challenge.server_signature); +	jd->challenge.server_signature = NULL; +	return ret; +} +  xt_status sasl_pkt_result(struct xt_node *node, gpointer data)  {  	struct im_connection *ic = data; @@ -417,6 +646,10 @@ xt_status sasl_pkt_result(struct xt_node *node, gpointer data)  		imcb_log(ic, "Authentication finished");  		jd->flags |= JFLAG_AUTHENTICATED | JFLAG_STREAM_RESTART; +		if (jd->challenge.type == JCHALLENGE_SCRAM) { +			return sasl_pkt_validate_scram_success(node, data); +		} +  		if (jd->flags & JFLAG_HIPCHAT) {  			return hipchat_handle_success(ic, node);  		} diff --git a/protocols/nogaim.c b/protocols/nogaim.c index 58f74f8b..2937bdcc 100644 --- a/protocols/nogaim.c +++ b/protocols/nogaim.c @@ -349,18 +349,22 @@ static void serv_got_crap(struct im_connection *ic, char *format, ...)  		strip_html(text);  	} -	/* Try to find a different connection on the same protocol. */ -	for (a = ic->bee->accounts; a; a = a->next) { -		if (a->prpl == ic->acc->prpl && a->ic != ic) { -			break; -		} -	} - -	/* If we found one, include the screenname in the message. */ -	if (a) { +	if (set_getbool(&ic->bee->set, "only_log_tags")) {  		ic->bee->ui->log(ic->bee, ic->acc->tag, text);  	} else { -		ic->bee->ui->log(ic->bee, ic->acc->prpl->name, text); +		/* Try to find a different connection on the same protocol. */ +		for (a = ic->bee->accounts; a; a = a->next) { +			if (a->prpl == ic->acc->prpl && a->ic != ic) { +				break; +			} +		} + +		/* If we found one, include the screenname in the message. */ +		if (a) { +			ic->bee->ui->log(ic->bee, ic->acc->tag, text); +		} else { +			ic->bee->ui->log(ic->bee, ic->acc->prpl->name, text); +		}  	}  	g_free(text); diff --git a/protocols/purple/purple.c b/protocols/purple/purple.c index 0b74c8d8..64faadfb 100644 --- a/protocols/purple/purple.c +++ b/protocols/purple/purple.c @@ -328,6 +328,18 @@ static void purple_init(account_t *acc)  	purple_accounts_remove(pa);  } +static void purple_save_password(account_t *acc, PurpleAccount *pa) +{ +	char *old_password, *new_password; + +	old_password = set_getstr(&acc->set, "password"); +	new_password = g_strdup(purple_account_get_password(pa)); +	if (!old_password || !*old_password || g_strcmp0(old_password, new_password) != 0) { +		set_setstr(&acc->set, "password", new_password); +	} +	g_free(new_password); +} +  static void purple_sync_settings(account_t *acc, PurpleAccount *pa)  {  	PurplePlugin *prpl = purple_plugins_find_with_id(pa->protocol_id); @@ -424,6 +436,8 @@ static void purple_logout(struct im_connection *ic)  		imcb_chat_free(ic->groupchats->data);  	} +	purple_save_password(ic->acc, pd->account); +  	if (pd->filetransfers) {  		purple_transfer_cancel_all(ic);  	} @@ -1900,8 +1914,8 @@ void purple_initmodule()  		ret = g_memdup(&funcs, sizeof(funcs));  		ret->name = ret->data = prot->info->id; -		if (strncmp(ret->name, "prpl-", 5) == 0) { -			ret->name += 5; +		if (strncmp(ret->name, "prpl-", 5) != 0) { +			ret->name = g_strdup_printf("prpl-%s", ret->name);  		}  		if (pi->options & OPT_PROTO_NO_PASSWORD) { @@ -1918,13 +1932,13 @@ void purple_initmodule()  		/* libpurple doesn't define a protocol called OSCAR, but we  		   need it to be compatible with normal BitlBee. */ -		if (g_strcasecmp(prot->info->id, "prpl-aim") == 0) { +		/*if (g_strcasecmp(prot->info->id, "prpl-aim") == 0) {  			ret = g_memdup(&funcs, sizeof(funcs));  			ret->name = "oscar"; -			/* purple_get_account_prpl_id() determines the actual protocol ID (icq/aim) */ +			/ * purple_get_account_prpl_id() determines the actual protocol ID (icq/aim) * /  			ret->data = NULL;  			register_protocol(ret); -		} +		}*/  		info = g_new0(struct plugin_info, 1);  		info->abiver = BITLBEE_ABI_VERSION_CODE; diff --git a/protocols/twitter/twitter.c b/protocols/twitter/twitter.c index 2b432a19..b0ab98c4 100644 --- a/protocols/twitter/twitter.c +++ b/protocols/twitter/twitter.c @@ -304,7 +304,7 @@ static void twitter_main_loop_start(struct im_connection *ic)  struct groupchat *twitter_groupchat_init(struct im_connection *ic)  { -	char *name_hint; +	char *name_hint, *tmp;  	struct groupchat *gc;  	struct twitter_data *td = ic->proto_data;  	GSList *l; @@ -315,7 +315,14 @@ struct groupchat *twitter_groupchat_init(struct im_connection *ic)  	td->timeline_gc = gc = imcb_chat_new(ic, "twitter/timeline"); -	name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user); +	tmp = set_getstr(&ic->acc->set, "channel_name"); +	 +	if (tmp == NULL || strlen(tmp) == 0) { +		name_hint = g_strdup_printf("%s_%s", td->prefix, ic->acc->user); +	} else { +		name_hint = g_strdup(tmp); +	} +  	imcb_chat_name_hint(gc, name_hint);  	g_free(name_hint); @@ -343,8 +350,10 @@ void twitter_login_finish(struct im_connection *ic)  	} else if (!(td->flags & TWITTER_MODE_ONE) &&  	           !(td->flags & TWITTER_HAVE_FRIENDS)) {  		imcb_log(ic, "Getting contact list"); +  		twitter_get_friends_ids(ic, -1);  		twitter_get_mutes_ids(ic, -1); +		twitter_get_blocks_ids(ic, -1);  		twitter_get_noretweets_ids(ic, -1);  	} else {  		twitter_main_loop_start(ic); @@ -518,6 +527,23 @@ static char *set_eval_commands(set_t * set, char *value)  	}  } +static char *set_eval_channel_name(set_t * set, char *value) +{ +	size_t len; + +	if (value == NULL) { +		return NULL; +	} + +	len = strlen(value); + +	if (len < MAX_NICK_LENGTH && len > 0) { +		return value; +	} else { +		return NULL; +	} +} +  static char *set_eval_mode(set_t * set, char *value)  {  	if (g_strcasecmp(value, "one") == 0 || @@ -528,6 +554,15 @@ static char *set_eval_mode(set_t * set, char *value)  	}  } +static char *set_eval_id_length(set_t * set, char *value) +{ +	int len = atoi(value); +	if (len >= 1 || len <= 4) +		return value; + +	return SET_INVALID; +} +  static void twitter_init(account_t * acc)  {  	set_t *s; @@ -572,6 +607,14 @@ static void twitter_init(account_t * acc)  	s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc); +	s = set_add(&acc->set, "id_length", "2", set_eval_id_length, acc); +	s->flags |= ACC_SET_OFFLINE_ONLY; + +	s = set_add(&acc->set, "channel_name", NULL, set_eval_channel_name, acc); +	s->flags |= ACC_SET_OFFLINE_ONLY; + +	s = set_add(&acc->set, "autofill_reply", "true", set_eval_bool, acc); +  	s = set_add(&acc->set, "_last_tweet", "0", NULL, acc);  	s->flags |= SET_HIDDEN | SET_NOSAVE; @@ -595,6 +638,7 @@ static void twitter_login(account_t * acc)  	char name[strlen(acc->user) + 9];  	url_t url;  	char *s; +	size_t i;  	if (!url_set(&url, set_getstr(&ic->acc->set, "base_url")) ||  	    (url.proto != PROTO_HTTP && url.proto != PROTO_HTTPS)) { @@ -652,8 +696,17 @@ static void twitter_login(account_t * acc)  	imcb_add_buddy(ic, name, NULL);  	imcb_buddy_status(ic, name, OPT_LOGGED_IN, NULL, NULL); -	td->log = g_new0(struct twitter_log_data, TWITTER_LOG_LENGTH); +	td->id_length = set_getint(&ic->acc->set, "id_length"); +	td->log_length = 0; +	for (i = 0; i < td->id_length; i++) { +		td->log_length = (td->log_length << 4) + 0x0F; +	} +	td->log_length += 1; + +	td->log = g_new0(struct twitter_log_data, td->log_length); +	td->filter_log = g_new0(struct twitter_log_data, td->log_length);  	td->log_id = -1; +	td->filter_log_id = -1;  	s = set_getstr(&ic->acc->set, "mode");  	if (g_strcasecmp(s, "one") == 0) { @@ -664,6 +717,10 @@ static void twitter_login(account_t * acc)  		td->flags |= TWITTER_MODE_CHAT;  	} +	td->mutes_ids = g_hash_table_new_full(g_str_hash, (GEqualFunc)strcmp, (GDestroyNotify)g_free, NULL); +	td->blocks_ids = g_hash_table_new_full(g_str_hash, (GEqualFunc)strcmp, (GDestroyNotify)g_free, NULL); +	td->noretweets_ids = g_hash_table_new_full(g_str_hash, (GEqualFunc)strcmp, (GDestroyNotify)g_free, NULL); +  	twitter_login_finish(ic);  } @@ -689,11 +746,9 @@ static void twitter_logout(struct im_connection *ic)  			b_event_remove(td->filter_update_id);  		} -		g_slist_foreach(td->mutes_ids, (GFunc) g_free, NULL); -		g_slist_free(td->mutes_ids); - -		g_slist_foreach(td->noretweets_ids, (GFunc) g_free, NULL); -		g_slist_free(td->noretweets_ids); +		g_hash_table_unref(td->mutes_ids); +		g_hash_table_unref(td->blocks_ids); +		g_hash_table_unref(td->noretweets_ids);  		http_close(td->stream);  		twitter_filter_remove_all(ic); @@ -897,7 +952,7 @@ static guint64 twitter_message_id_from_command_arg(struct im_connection *ic, cha  		if (arg[0] == '#') {  			arg++;  		} -		if (parse_int64(arg, 16, &id) && id < TWITTER_LOG_LENGTH) { +		if (parse_int64(arg, 16, &id) && id < td->log_length) {  			bu = td->log[id].bu;  			id = td->log[id].id;  		} else if (parse_int64(arg, 10, &id)) { @@ -988,7 +1043,8 @@ static void twitter_handle_command(struct im_connection *ic, char *message)  		twitter_report_spam(ic, screen_name);  		goto eof; -	} else if (g_strcasecmp(cmd[0], "rt") == 0 && cmd[1]) { +	} else if ((g_strcasecmp(cmd[0], "rt") == 0 || +		    g_strcasecmp(cmd[0], "retweet") == 0) && cmd[1]) {  		id = twitter_message_id_from_command_arg(ic, cmd[1], NULL);  		td->last_status_id = 0; @@ -1007,7 +1063,11 @@ static void twitter_handle_command(struct im_connection *ic, char *message)  			            "post any statuses recently", cmd[1]);  			goto eof;  		} -		message = new = g_strdup_printf("@%s %s", bu->handle, cmd[2]); +		if (!set_getbool(&ic->acc->set, "autofill_reply")) { +			message = new = g_strdup_printf("@%s %s", bu->handle, cmd[2]); +		} else { +			message = cmd[2]; +		}  		in_reply_to = id;  		allow_post = TRUE;  	} else if (g_strcasecmp(cmd[0], "rawreply") == 0 && cmd[1] && cmd[2]) { @@ -1027,12 +1087,43 @@ static void twitter_handle_command(struct im_connection *ic, char *message)  			twitter_status_show_url(ic, id);  		}  		goto eof; +	} else if (g_strcasecmp(cmd[0], "quote") == 0 && cmd[1]) { +		id = twitter_message_id_from_command_arg(ic, cmd[1], NULL); +		td->last_status_id = 0; +		if (id) { +			twitter_status_quote_post(ic, id, cmd[2]); +		} else { +			twitter_log(ic, "User '%s' does not exist or didn't " +				    "post any statuses recently", cmd[1]); +		} + +		goto eof;  	} else if (g_strcasecmp(cmd[0], "post") == 0) {  		message += 5;  		allow_post = TRUE; +	} else if (g_strcasecmp(cmd[0], "dm") == 0 && cmd[1] && cmd[2]) { +		if ((bu = bee_user_by_handle(ic->bee, ic, cmd[1]))) { +			twitter_direct_messages_new(ic, bu->handle, cmd[2]); +		} else { +			twitter_direct_messages_new(ic, cmd[1], cmd[2]); +		} +		goto eof; +	} else if (g_strcasecmp(cmd[0], "rtoff") == 0 && cmd[1]) { +		twitter_retweet_enable_disable(ic, cmd[1], 0); +		goto eof; +	} else if (g_strcasecmp(cmd[0], "rton") == 0 && cmd[1]) { +		twitter_retweet_enable_disable(ic, cmd[1], 1); +		goto eof; +	} else if (g_strcasecmp(cmd[0], "block") == 0 && cmd[1]) { +		twitter_block_create_destroy(ic, cmd[1], 1); +		goto eof; +	} else if (g_strcasecmp(cmd[0], "unblock") == 0 && cmd[1]) { +		twitter_block_create_destroy(ic, cmd[1], 0); +		goto eof;  	} +  	if (allow_post) {  		char *s; diff --git a/protocols/twitter/twitter.h b/protocols/twitter/twitter.h index 86c88262..bf47d4fc 100644 --- a/protocols/twitter/twitter.h +++ b/protocols/twitter/twitter.h @@ -60,15 +60,18 @@ struct twitter_data {  	guint64 timeline_id;  	GSList *follow_ids; -	GSList *mutes_ids; -	GSList *noretweets_ids;  	GSList *filters; +	GHashTable *mutes_ids; +	GHashTable *blocks_ids; +	GHashTable *noretweets_ids;  	guint64 last_status_id; /* For undo */  	gint main_loop_id;  	gint filter_update_id;  	struct http_request *stream; +	time_t stream_opentime;  	struct http_request *filter_stream; +	time_t filter_stream_opentime;  	struct groupchat *timeline_gc;  	gint http_fails;  	twitter_flags_t flags; @@ -84,6 +87,11 @@ struct twitter_data {  	/* set show_ids */  	struct twitter_log_data *log;  	int log_id; +	struct twitter_log_data *filter_log; +	int filter_log_id; + +	int id_length; +	int log_length;  };  #define TWITTER_FILTER_UPDATE_WAIT 3000 @@ -99,7 +107,6 @@ struct twitter_user_data {  	time_t last_time;  }; -#define TWITTER_LOG_LENGTH 256  struct twitter_log_data {  	guint64 id;  	/* DANGER: bu can be a dead pointer. Check it first. diff --git a/protocols/twitter/twitter_lib.c b/protocols/twitter/twitter_lib.c index 761d74f7..a046f35e 100644 --- a/protocols/twitter/twitter_lib.c +++ b/protocols/twitter/twitter_lib.c @@ -240,6 +240,7 @@ static json_value *twitter_parse_response(struct im_connection *ic, struct http_  static void twitter_http_get_friends_ids(struct http_request *req);  static void twitter_http_get_mutes_ids(struct http_request *req); +static void twitter_http_get_blocks_ids(struct http_request *req);  static void twitter_http_get_noretweets_ids(struct http_request *req);  /** @@ -272,6 +273,20 @@ void twitter_get_mutes_ids(struct im_connection *ic, gint64 next_cursor)  }  /** + * Get the blocked users ids. + */ +void twitter_get_blocks_ids(struct im_connection *ic, gint64 next_cursor) +{ +	char *args[2]; + +	args[0] = "cursor"; +	args[1] = g_strdup_printf("%" G_GINT64_FORMAT, next_cursor); +	twitter_http(ic, TWITTER_BLOCKS_IDS_URL, twitter_http_get_blocks_ids, ic, 0, args, 2); + +	g_free(args[1]); +} + +/**   * Get the ids for users from whom we should ignore retweets.   */  void twitter_get_noretweets_ids(struct im_connection *ic, gint64 next_cursor) @@ -367,14 +382,15 @@ static void twitter_http_get_friends_ids(struct http_request *req)  }  /** - * Callback for getting the mutes ids. + * Callback for getting the blocks and mutes ids.   */ -static void twitter_http_get_mutes_ids(struct http_request *req) +static void twitter_http_get_mutes_blocks_ids(struct http_request *req, int blocks)  {  	struct im_connection *ic = req->data;  	json_value *parsed;  	struct twitter_xml_list *txl;  	struct twitter_data *td; +	GSList *elem;  	// Check if the connection is stil active  	if (!g_slist_find(twitter_connections, ic)) { @@ -394,24 +410,45 @@ static void twitter_http_get_mutes_ids(struct http_request *req)  	}  	txl = g_new0(struct twitter_xml_list, 1); -	txl->list = td->mutes_ids;  	/* mute ids API response is similar enough to friends response  	   to reuse this method */  	twitter_xt_get_friends_id_list(parsed, txl);  	json_value_free(parsed); -	td->mutes_ids = txl->list; +	for (elem = txl->list; elem; elem = elem->next) { +		g_hash_table_add(blocks ? td->blocks_ids : td->mutes_ids, elem->data); +	} +  	if (txl->next_cursor) {  		/* Recurse while there are still more pages */ -		twitter_get_mutes_ids(ic, txl->next_cursor); +		if (blocks) { +			twitter_get_blocks_ids(ic, txl->next_cursor); +		} else { +			twitter_get_mutes_ids(ic, txl->next_cursor); +		}  	} -	txl->list = NULL;  	txl_free(txl);  }  /** + * Callback for getting the mutes ids. + */ +static void twitter_http_get_mutes_ids(struct http_request *req) +{ +	twitter_http_get_mutes_blocks_ids(req, 0); +} + +/** + * Callback for  getting the blocks ids. + */ +static void twitter_http_get_blocks_ids(struct http_request *req) +{ +	twitter_http_get_mutes_blocks_ids(req, 1); +} + +/**   * Callback for getting the no-retweets ids.   */  static void twitter_http_get_noretweets_ids(struct http_request *req) @@ -439,7 +476,6 @@ static void twitter_http_get_noretweets_ids(struct http_request *req)  	}  	txl = g_new0(struct twitter_xml_list, 1); -	txl->list = td->noretweets_ids;  	// Process the retweet ids  	txl->type = TXL_ID; @@ -450,15 +486,12 @@ static void twitter_http_get_noretweets_ids(struct http_request *req)  			if (c->type != json_integer) {  				continue;  			} -			txl->list = g_slist_prepend(txl->list, -			                            g_strdup_printf("%"PRIu64, c->u.integer)); +			g_hash_table_add(td->noretweets_ids, g_strdup_printf("%"PRIu64, c->u.integer));  		}  	}  	json_value_free(parsed); -	td->noretweets_ids = txl->list; -	txl->list = NULL;  	txl_free(txl);  } @@ -816,7 +849,7 @@ static char *twitter_msg_add_id(struct im_connection *ic,  	if (txs->reply_to) {  		int i; -		for (i = 0; i < TWITTER_LOG_LENGTH; i++) { +		for (i = 0; i < td->log_length; i++) {  			if (td->log[i].id == txs->reply_to) {  				reply_to = i;  				break; @@ -834,26 +867,39 @@ static char *twitter_msg_add_id(struct im_connection *ic,  		}  	} -	td->log_id = (td->log_id + 1) % TWITTER_LOG_LENGTH; -	td->log[td->log_id].id = txs->id; -	td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name); +	if (txs->from_filter) { +		td->filter_log_id = (td->filter_log_id + 1) % td->log_length; +		td->filter_log[td->filter_log_id].id = txs->id; +		td->filter_log[td->filter_log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name); +	} else { +		td->log_id = (td->log_id + 1) % td->log_length; +		td->log[td->log_id].id = txs->id; +		td->log[td->log_id].bu = bee_user_by_handle(ic->bee, ic, txs->user->screen_name); +	}  	/* This is all getting hairy. :-( If we RT'ed something ourselves,  	   remember OUR id instead so undo will work. In other cases, the  	   original tweet's id should be remembered for deduplicating. */  	if (g_strcasecmp(txs->user->screen_name, td->user) == 0) { -		td->log[td->log_id].id = txs->rt_id; -		/* More useful than NULL. */ -		td->log[td->log_id].bu = &twitter_log_local_user; +		if (txs->from_filter) { +			td->filter_log[td->filter_log_id].id = txs->rt_id; +			/* More useful than NULL. */ +			td->filter_log[td->filter_log_id].bu = &twitter_log_local_user; +		} else { +			td->log[td->log_id].id = txs->rt_id; +			/* More useful than NULL. */ +			td->log[td->log_id].bu = &twitter_log_local_user; +		}  	}  	if (set_getbool(&ic->acc->set, "show_ids")) {  		if (reply_to != -1) { -			return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s", -			                       td->log_id, reply_to, prefix, txs->text); +			return g_strdup_printf("\002[\002%0*x->%0*x\002]\002 %s%s", +			                       td->id_length, td->log_id, td->id_length, +					       reply_to, prefix, txs->text);  		} else { -			return g_strdup_printf("\002[\002%02x\002]\002 %s%s", -			                       td->log_id, prefix, txs->text); +			return g_strdup_printf("\002[\002%0*x\002]\002 %s%s", +			                       td->id_length, td->log_id, prefix, txs->text);  		}  	} else {  		if (*prefix) { @@ -981,11 +1027,12 @@ static void twitter_status_show(struct im_connection *ic, struct twitter_xml_sta  	/* Check this is not a tweet that should be muted */  	uid_str = g_strdup_printf("%" PRIu64, status->user->uid); -	if (g_slist_find_custom(td->mutes_ids, uid_str, (GCompareFunc)strcmp)) { +	if (g_hash_table_lookup(td->mutes_ids, uid_str) || +			g_hash_table_lookup(td->blocks_ids, uid_str)) {  		g_free(uid_str);  		return;  	} -	if (status->id != status->rt_id && g_slist_find_custom(td->noretweets_ids, uid_str, (GCompareFunc)strcmp)) { +	if (status->id != status->rt_id && g_hash_table_lookup(td->noretweets_ids, uid_str)) {  		g_free(uid_str);  		return;  	} @@ -1034,8 +1081,23 @@ static void twitter_http_stream(struct http_request *req)  	if ((req->flags & HTTPC_EOF) || !req->reply_body) {  		if (req == td->stream) {  			td->stream = NULL; + +			if (req->status_code == 200 && +			    td->stream_opentime + 3 < time(NULL)) { +				debug("Reconnecting to twitter stream."); +				twitter_open_stream(ic); +				twitter_get_timeline(ic, -1); +				return; +			}  		} else if (req == td->filter_stream) {  			td->filter_stream = NULL; + +			if (req->status_code == 200 && +			    td->filter_stream_opentime + 3 < time(NULL)) { +				debug("Reconnecting to twitter filter stream."); +				twitter_open_filter_stream(ic); +				return; +			}  		}  		imcb_error(ic, "Stream closed (%s)", req->status_string); @@ -1094,8 +1156,12 @@ static gboolean twitter_stream_handle_object(struct im_connection *ic, json_valu  	} else if ((c = json_o_get(o, "direct_message")) &&  	           (txs = twitter_xt_get_dm(c))) {  		if (g_strcasecmp(txs->user->screen_name, td->user) != 0) { -			imcb_buddy_msg(ic, txs->user->screen_name, -			               txs->text, 0, txs->created_at); +			char *uid_str = g_strdup_printf("%" PRIu64, txs->user->uid); +			if (!g_hash_table_lookup(td->blocks_ids, uid_str)) { +				imcb_buddy_msg(ic, txs->user->screen_name, +					       txs->text, 0, txs->created_at); +			} +			g_free(uid_str);  		}  		txs_free(txs);  		return TRUE; @@ -1119,10 +1185,12 @@ static gboolean twitter_stream_handle_object(struct im_connection *ic, json_valu  static gboolean twitter_stream_handle_status(struct im_connection *ic, struct twitter_xml_status *txs)  {  	struct twitter_data *td = ic->proto_data; +	struct twitter_log_data *tl;  	int i; -	for (i = 0; i < TWITTER_LOG_LENGTH; i++) { -		if (td->log[i].id == txs->id) { +	tl = txs->from_filter ? td->filter_log : td->log; +	for (i = 0; i < td->log_length; i++) { +		if (tl[i].id == txs->id) {  			/* Got a duplicate (RT, probably). Drop it. */  			return TRUE;  		} @@ -1165,33 +1233,25 @@ static gboolean twitter_stream_handle_event(struct im_connection *ic, json_value  		if (g_strcasecmp(us->screen_name, td->user) == 0) {  			twitter_add_buddy(ic, ut->screen_name, ut->name);  		} -	} else if (strcmp(type, "mute") == 0) { -		GSList *found; +	} else if (strcmp(type, "mute") == 0 || strcmp(type, "block") == 0) {  		char *uid_str; +		int mute = strcmp(type, "mute") == 0;  		ut = twitter_xt_get_user(target);  		uid_str = g_strdup_printf("%" PRIu64, ut->uid); -		if (!(found = g_slist_find_custom(td->mutes_ids, uid_str, -		                                  (GCompareFunc)strcmp))) { -			td->mutes_ids = g_slist_prepend(td->mutes_ids, uid_str); -		} -		twitter_log(ic, "Muted user %s", ut->screen_name); +		g_hash_table_add(mute ? td->mutes_ids : td->blocks_ids, uid_str); +		twitter_log(ic, "%s user %s", mute ? "Muted" : "Blocked", ut->screen_name);  		if (getenv("BITLBEE_DEBUG")) {  			fprintf(stderr, "New mute: %s %"PRIu64"\n",  			        ut->screen_name, ut->uid);  		} -	} else if (strcmp(type, "unmute") == 0) { -		GSList *found; +	} else if (strcmp(type, "unmute") == 0 || strcmp(type, "unblock") == 0) {  		char *uid_str; +		int unmute = strcmp(type, "unmute") == 0;  		ut = twitter_xt_get_user(target);  		uid_str = g_strdup_printf("%" PRIu64, ut->uid); -		if ((found = g_slist_find_custom(td->mutes_ids, uid_str, -		                                (GCompareFunc)strcmp))) { -			char *found_str = found->data; -			td->mutes_ids = g_slist_delete_link(td->mutes_ids, found); -			g_free(found_str); -		} +		g_hash_table_remove(unmute ? td->mutes_ids : td->blocks_ids, uid_str);  		g_free(uid_str); -		twitter_log(ic, "Unmuted user %s", ut->screen_name); +		twitter_log(ic, "%s user %s", unmute ? "Unmuted" : "Blocked", ut->screen_name);  		if (getenv("BITLBEE_DEBUG")) {  			fprintf(stderr, "New unmute: %s %"PRIu64"\n",  			        ut->screen_name, ut->uid); @@ -1214,6 +1274,7 @@ gboolean twitter_open_stream(struct im_connection *ic)  		/* 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->stream->flags |= HTTPC_STREAMING; +		td->stream_opentime = time(NULL);  		return TRUE;  	} @@ -1269,6 +1330,7 @@ static gboolean twitter_filter_stream(struct im_connection *ic)  		/* 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; +		td->filter_stream_opentime = time(NULL);  		ret = TRUE;  	} @@ -1683,10 +1745,12 @@ static void twitter_http_post(struct http_request *req)   */  void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_to)  { -	char *args[4] = { +	char *args[6] = {  		"status", msg,  		"in_reply_to_status_id", -		g_strdup_printf("%" G_GUINT64_FORMAT, in_reply_to) +		g_strdup_printf("%" G_GUINT64_FORMAT, in_reply_to), +		"auto_populate_reply_metadata", +		"true",  	};  	if (set_getbool(&ic->acc->set, "in_korea") && !in_reply_to) { @@ -1697,7 +1761,7 @@ void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_t  	}  	twitter_http(ic, TWITTER_STATUS_UPDATE_URL, twitter_http_post, ic, 1, -	             args, in_reply_to ? 4 : 2); +	             args, in_reply_to ? (set_getbool(&ic->acc->set, "autofill_reply") ? 6 : 4) : 2);  	g_free(args[3]);  } @@ -1740,6 +1804,32 @@ void twitter_mute_create_destroy(struct im_connection *ic, char *who, int create  		     twitter_http_post, ic, 1, args, 2);  } +/** + * Block or unblock a user + */ +void twitter_block_create_destroy(struct im_connection *ic, char *who, int create) +{ +	char *args[2]; + +	args[0] = "screen_name"; +	args[1] = who; +	twitter_http(ic, create ? TWITTER_BLOCKS_CREATE_URL : TWITTER_BLOCKS_DESTROY_URL, +			twitter_http_post, ic, 1, args, 2); +} + +/** + * Enable or disable retweets for user + */ +void twitter_retweet_enable_disable(struct im_connection *ic, char *who, int enable) +{ +	char *args[4]; +	args[0] = "screen_name"; +	args[1] = who; +	args[2] = "retweets"; +	args[3] = enable ? "true" : "false"; +	twitter_http(ic, TWITTER_FRIENDSHIPS_UPDATE_URL, twitter_http_post, ic, 1, args, 4); +} +  void twitter_status_destroy(struct im_connection *ic, guint64 id)  {  	char *url; @@ -1831,3 +1921,63 @@ void twitter_status_show_url(struct im_connection *ic, guint64 id)  	twitter_http(ic, url, twitter_http_status_show_url, ic, 0, NULL, 0);  	g_free(url);  } + +struct twitter_http_msg { +	struct im_connection *ic; +	char *message; +}; + +static void twitter_http_status_quote_post(struct http_request *req) +{ +	struct twitter_http_msg *thm = req->data; +	struct im_connection *ic = thm->ic; +	struct twitter_data *td = ic->proto_data; +	char *message; +	const char *name; +	json_value *parsed, *id; + +	if (!g_slist_find(twitter_connections, ic)) { +		goto eof; +	} + +	if (!(parsed = twitter_parse_response(ic, req))) { +		goto eof; +	} + +	name = json_o_str(json_o_get(parsed, "user"), "screen_name"); +	id = json_o_get(parsed, "id"); + +	if (name && id && id->type == json_integer) { +		message = g_strdup_printf("%s https://twitter.com/%s/status/%" G_GUINT64_FORMAT, thm->message, name, id->u.integer); + +		/*if (!twitter_length_check(ic, message)) { +			goto eof; +		}*/ + +		td->last_status_id = 0; +		twitter_post_status(ic, message, 0); +	} else { +		twitter_log(ic, "Error: could not fetch url for quoted tweet."); +	} + +	json_value_free(parsed); + +eof: +	g_free(thm->message); +	g_free(thm); +} + +void twitter_status_quote_post(struct im_connection *ic, guint64 id, char *message) +{ +	struct twitter_http_msg *thm; +	char *url; + +	thm = g_new0(struct twitter_http_msg, 1); + +	thm->ic = ic; +	thm->message = g_strdup(message); + +	url = g_strdup_printf("%s%" G_GUINT64_FORMAT "%s", TWITTER_STATUS_SHOW_URL, id, ".json"); +	twitter_http(ic, url, twitter_http_status_quote_post, thm, 0, NULL, 0); +	g_free(url); +} diff --git a/protocols/twitter/twitter_lib.h b/protocols/twitter/twitter_lib.h index 6833d23d..88396a1e 100644 --- a/protocols/twitter/twitter_lib.h +++ b/protocols/twitter/twitter_lib.h @@ -58,12 +58,14 @@  #define TWITTER_FRIENDSHIPS_CREATE_URL "/friendships/create.json"  #define TWITTER_FRIENDSHIPS_DESTROY_URL "/friendships/destroy.json"  #define TWITTER_FRIENDSHIPS_SHOW_URL "/friendships/show.json" +#define TWITTER_FRIENDSHIPS_UPDATE_URL "/friendships/update.json"  /* Social graphs URLs */  #define TWITTER_FRIENDS_IDS_URL "/friends/ids.json"  #define TWITTER_FOLLOWERS_IDS_URL "/followers/ids.json"  #define TWITTER_MUTES_IDS_URL "/mutes/users/ids.json"  #define TWITTER_NORETWEETS_IDS_URL "/friendships/no_retweets/ids.json" +#define TWITTER_BLOCKS_IDS_URL "/blocks/ids.json"  /* Account URLs */  #define TWITTER_ACCOUNT_RATE_LIMIT_URL "/account/rate_limit_status.json" @@ -74,8 +76,8 @@  #define TWITTER_FAVORITE_DESTROY_URL "/favorites/destroy.json"  /* Block URLs */ -#define TWITTER_BLOCKS_CREATE_URL "/blocks/create/" -#define TWITTER_BLOCKS_DESTROY_URL "/blocks/destroy/" +#define TWITTER_BLOCKS_CREATE_URL "/blocks/create.json" +#define TWITTER_BLOCKS_DESTROY_URL "/blocks/destroy.json"  /* Mute URLs */  #define TWITTER_MUTES_CREATE_URL "/mutes/users/create.json" @@ -93,6 +95,7 @@ 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);  void twitter_get_mutes_ids(struct im_connection *ic, gint64 next_cursor); +void twitter_get_blocks_ids(struct im_connection *ic, gint64 next_cursor);  void twitter_get_noretweets_ids(struct im_connection *ic, gint64 next_cursor);  void twitter_get_statuses_friends(struct im_connection *ic, gint64 next_cursor); @@ -100,11 +103,14 @@ void twitter_post_status(struct im_connection *ic, char *msg, guint64 in_reply_t  void twitter_direct_messages_new(struct im_connection *ic, char *who, char *message);  void twitter_friendships_create_destroy(struct im_connection *ic, char *who, int create);  void twitter_mute_create_destroy(struct im_connection *ic, char *who, int create); +void twitter_block_create_destroy(struct im_connection *ic, char *who, int create); +void twitter_retweet_enable_disable(struct im_connection *ic, char *who, int enable);  void twitter_status_destroy(struct im_connection *ic, guint64 id);  void twitter_status_retweet(struct im_connection *ic, guint64 id);  void twitter_report_spam(struct im_connection *ic, char *screen_name);  void twitter_favourite_tweet(struct im_connection *ic, guint64 id);  void twitter_status_show_url(struct im_connection *ic, guint64 id); +void twitter_status_quote_post(struct im_connection *ic, guint64 id, char *message);  #endif //_TWITTER_LIB_H  | 
