aboutsummaryrefslogtreecommitdiffstats
path: root/protocols
diff options
context:
space:
mode:
Diffstat (limited to 'protocols')
-rw-r--r--protocols/account.c92
-rw-r--r--protocols/jabber/jabber.c2
-rw-r--r--protocols/jabber/jabber.h17
-rw-r--r--protocols/jabber/sasl.c260
-rw-r--r--protocols/purple/purple.c22
-rw-r--r--protocols/twitter/twitter.c57
-rw-r--r--protocols/twitter/twitter.h10
-rw-r--r--protocols/twitter/twitter_lib.c131
-rw-r--r--protocols/twitter/twitter_lib.h1
9 files changed, 555 insertions, 37 deletions
diff --git a/protocols/account.c b/protocols/account.c
index e25e40c7..aa14b0c3 100644
--- a/protocols/account.c
+++ b/protocols/account.c
@@ -74,6 +74,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);
/* Hardcode some more clever tag guesses. */
strcpy(tag, prpl->name);
@@ -122,6 +124,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;
@@ -163,14 +195,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)) {
@@ -291,9 +337,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. */
@@ -317,6 +397,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 11a90ff4..2fa19c0f 100644
--- a/protocols/jabber/jabber.c
+++ b/protocols/jabber/jabber.c
@@ -377,6 +377,8 @@ 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);
jabber_connections = g_slist_remove(jabber_connections, ic);
diff --git a/protocols/jabber/jabber.h b/protocols/jabber/jabber.h
index d76ee08f..3445e4d7 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,13 @@ struct jabber_data {
char *me; /* bare jid */
char *internal_jid;
+ struct {
+ jabber_challenge_t type;
+ int scram_algo;
+ char *cnonce;
+ char *server_signature;
+ } challenge;
+
const struct oauth2_service *oauth2_service;
char *oauth2_access_token;
diff --git a/protocols/jabber/sasl.c b/protocols/jabber/sasl.c
index 7778af1f..10fbe270 100644
--- a/protocols/jabber/sasl.c
+++ b/protocols/jabber/sasl.c
@@ -22,11 +22,13 @@
\***************************************************************************/
#include <ctype.h>
+#include <gcrypt.h>
#include "jabber.h"
#include "base64.h"
#include "oauth2.h"
#include "oauth.h"
+#include "sha1.h"
const struct oauth2_service oauth2_service_google =
{
@@ -38,13 +40,27 @@ const struct oauth2_service oauth2_service_google =
"6C-Zgf7Tr7gEQTPlBhMUgo7R",
};
+static int is_ascii(const char *str)
+{
+ if (!str) {
+ return 0;
+ }
+
+ while (*str) {
+ if (*str++ & 0x80)
+ return 0;
+ }
+
+ return 1;
+}
+
xt_status sasl_pkt_mechanisms(struct xt_node *node, gpointer data)
{
struct im_connection *ic = 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;
+ int sup_plain = 0, sup_digest = 0, sup_gtalk = 0, sup_anonymous = 0, sup_scram = 0;
int want_oauth = FALSE, want_hipchat = FALSE, want_anonymous = FALSE;
GString *mechs;
@@ -71,24 +87,33 @@ 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;
- }
-
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, "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 (sup_scram && (!is_ascii(jd->username) || !is_ascii(ic->acc->pass))) {
+ imcb_log(ic, "Username/password contains non-ascii characters, SCRAM authentication disabled");
+ sup_scram = 0;
+ }
+
+ if (!want_oauth && !want_anonymous && !sup_plain && !sup_digest && !sup_scram) {
if (sup_gtalk) {
imcb_error(ic, "This server requires OAuth "
"(supported schemes:%s)", mechs->str);
@@ -139,11 +164,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) {
+ GString *gs;
+ int len;
+ unsigned char cnonce_bin[30];
+ char *cnonce;
+
+ 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;
+ }
+
+ gs = g_string_sized_new(128);
+
+ random_bytes(cnonce_bin, sizeof(cnonce_bin));
+ cnonce = base64_encode(cnonce_bin, sizeof(cnonce_bin));
+
+ jd->challenge.cnonce = cnonce;
+
+ /* XXX: Saslprep username */
+ g_string_append_printf(gs, "n,,n=%s,r=%s", jd->username, cnonce);
+
+ len = gs->len;
+ s = g_string_free(gs, FALSE);
+
+ reply->text = base64_encode((unsigned char *)s , len);
+ reply->text_len = strlen(reply->text);
+ 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/>. */
@@ -266,7 +327,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;
@@ -385,6 +446,175 @@ 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,
+ *client_final_noproof = NULL, *auth_message = NULL, *client_final = NULL;
+ unsigned char *salt = NULL;
+ size_t salt_len, iter_count, i;
+
+ int algo = jd->challenge.scram_algo;
+ 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;
+ }
+
+ // XXX: Normalize password with saslprep
+
+ salt_len = base64_decode(salt64, &salt);
+
+ if (gcry_kdf_derive(ic->acc->pass, strlen(ic->acc->pass),
+ GCRY_KDF_PBKDF2, algo,
+ salt, salt_len, iter_count,
+ sizeof(salted_password), salted_password) != 0) {
+ imcb_error(ic, "gcrypt went booboo :(");
+ 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));
+
+ client_first_bare = g_strdup_printf("n=%s,r=%s", jd->username, jd->challenge.cnonce);
+ client_final_noproof = g_strdup_printf("c=biws,r=%s", 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(random);
+ g_free(salt64);
+ g_free(salt);
+ g_free(iter_str);
+ g_free(encoded_proof);
+ 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;
+ 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;
@@ -402,6 +632,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/purple/purple.c b/protocols/purple/purple.c
index bee45607..42963fc5 100644
--- a/protocols/purple/purple.c
+++ b/protocols/purple/purple.c
@@ -278,6 +278,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);
@@ -380,6 +392,8 @@ static void purple_logout(struct im_connection *ic)
imcb_chat_free(ic->groupchats->data);
}
+ purple_save_password(ic->acc, pd->account);
+
purple_account_set_enabled(pd->account, "BitlBee", FALSE);
purple_connections = g_slist_remove(purple_connections, ic);
purple_accounts_remove(pd->account);
@@ -1659,8 +1673,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);
}
register_protocol(ret);
@@ -1668,12 +1682,12 @@ 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";
ret->data = prot->info->id;
register_protocol(ret);
- }
+ }*/
}
g_string_append(help, "\n\nFor used protocols, more information about available "
diff --git a/protocols/twitter/twitter.c b/protocols/twitter/twitter.c
index b2039171..9017d3b3 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);
@@ -518,6 +525,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 ||
@@ -572,6 +596,12 @@ static void twitter_init(account_t * acc)
s = set_add(&acc->set, "strip_newlines", "false", set_eval_bool, acc);
+ s = set_add(&acc->set, "long_ids", "false", set_eval_bool, 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, "_last_tweet", "0", NULL, acc);
s->flags |= SET_HIDDEN | SET_NOSAVE;
@@ -649,8 +679,13 @@ 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->long_ids = set_getbool(&ic->acc->set, "long_ids");
+ td->log_length = (td->long_ids) ? TWITTER_LONG_LOG_LENGTH : TWITTER_SHORT_LOG_LENGTH;
+
+ 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) {
@@ -894,7 +929,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)) {
@@ -985,7 +1020,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;
@@ -1024,7 +1060,18 @@ 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;
diff --git a/protocols/twitter/twitter.h b/protocols/twitter/twitter.h
index 86c88262..61afa4bb 100644
--- a/protocols/twitter/twitter.h
+++ b/protocols/twitter/twitter.h
@@ -68,7 +68,9 @@ struct twitter_data {
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 +86,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;
+
+ gboolean long_ids;
+ int log_length;
};
#define TWITTER_FILTER_UPDATE_WAIT 3000
@@ -99,7 +106,8 @@ struct twitter_user_data {
time_t last_time;
};
-#define TWITTER_LOG_LENGTH 256
+#define TWITTER_SHORT_LOG_LENGTH 256
+#define TWITTER_LONG_LOG_LENGTH (256 * 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 77f487ae..bffd61aa 100644
--- a/protocols/twitter/twitter_lib.c
+++ b/protocols/twitter/twitter_lib.c
@@ -790,7 +790,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;
@@ -808,26 +808,52 @@ 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);
+ if (td->long_ids) {
+ return g_strdup_printf("\002[\002%04x->%04x\002]\002 %s%s",
+ txs->from_filter ? td->filter_log_id : td->log_id,
+ reply_to, prefix, txs->text);
+ } else {
+ return g_strdup_printf("\002[\002%02x->%02x\002]\002 %s%s",
+ txs->from_filter ? td->filter_log_id : td->log_id,
+ reply_to, prefix, txs->text);
+ }
} else {
- return g_strdup_printf("\002[\002%02x\002]\002 %s%s",
- td->log_id, prefix, txs->text);
+ if (td->long_ids) {
+ return g_strdup_printf("\002[\002%04x\002]\002 %s%s",
+ txs->from_filter ? td->filter_log_id : td->log_id,
+ prefix, txs->text);
+ } else {
+ return g_strdup_printf("\002[\002%02x\002]\002 %s%s",
+ txs->from_filter ? td->filter_log_id : td->log_id,
+ prefix, txs->text);
+ }
}
} else {
if (*prefix) {
@@ -1009,8 +1035,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);
@@ -1090,10 +1131,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;
}
@@ -1185,6 +1228,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;
}
@@ -1240,6 +1284,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;
}
@@ -1791,3 +1836,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..d3b6ae9e 100644
--- a/protocols/twitter/twitter_lib.h
+++ b/protocols/twitter/twitter_lib.h
@@ -105,6 +105,7 @@ 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