diff options
Diffstat (limited to 'protocols')
-rw-r--r-- | protocols/account.c | 92 | ||||
-rw-r--r-- | protocols/bee.h | 10 | ||||
-rw-r--r-- | protocols/bee_chat.c | 5 | ||||
-rw-r--r-- | protocols/jabber/jabber.c | 6 | ||||
-rw-r--r-- | protocols/jabber/jabber.h | 18 | ||||
-rw-r--r-- | protocols/jabber/sasl.c | 259 | ||||
-rw-r--r-- | protocols/nogaim.h | 8 | ||||
-rw-r--r-- | protocols/purple/bpurple.h | 7 | ||||
-rw-r--r-- | protocols/purple/purple.c | 162 | ||||
-rw-r--r-- | protocols/twitter/twitter.c | 47 | ||||
-rw-r--r-- | protocols/twitter/twitter.h | 4 | ||||
-rw-r--r-- | protocols/twitter/twitter_lib.c | 105 | ||||
-rw-r--r-- | protocols/twitter/twitter_lib.h | 1 |
13 files changed, 696 insertions, 28 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/bee.h b/protocols/bee.h index d22e4d85..8e665ac8 100644 --- a/protocols/bee.h +++ b/protocols/bee.h @@ -83,6 +83,14 @@ typedef struct bee_user { void *data; /* Can be used by the IM module. */ } bee_user_t; +typedef struct bee_chat_info { + char *title; + char *topic; + + /* If less than zero, the user count is ignored when displaying */ + int userc; +} bee_chat_info_t; + /* This one's mostly used so save space and make it easier (cheaper) to compare groups of contacts, etc. */ typedef struct bee_group { @@ -184,4 +192,6 @@ G_MODULE_EXPORT int bee_chat_msg(bee_t *bee, struct groupchat *c, const char *ms G_MODULE_EXPORT struct groupchat *bee_chat_by_title(bee_t *bee, struct im_connection *ic, const char *title); G_MODULE_EXPORT void imcb_chat_invite(struct im_connection *ic, const char *name, const char *who, const char *msg); +G_MODULE_EXPORT void bee_chat_list_finish(struct im_connection *ic); + #endif /* __BEE_H__ */ diff --git a/protocols/bee_chat.c b/protocols/bee_chat.c index 2fcb0396..76ed7f85 100644 --- a/protocols/bee_chat.c +++ b/protocols/bee_chat.c @@ -273,3 +273,8 @@ void imcb_chat_invite(struct im_connection *ic, const char *name, const char *wh ic->bee->ui->chat_invite(ic->bee, bu, name, msg); } } + +void bee_chat_list_finish(struct im_connection *ic) +{ + cmd_chat_list_finish(ic); +} diff --git a/protocols/jabber/jabber.c b/protocols/jabber/jabber.c index 11a90ff4..fcd90598 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; } @@ -377,6 +380,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 d76ee08f..c97c5b71 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 7778af1f..f0381ee8 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 = { @@ -44,7 +48,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; + 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 +75,28 @@ 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 (!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 +147,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/>. */ @@ -266,7 +310,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 +429,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; @@ -402,6 +631,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.h b/protocols/nogaim.h index e5569313..b2ae2cae 100644 --- a/protocols/nogaim.h +++ b/protocols/nogaim.h @@ -95,6 +95,7 @@ struct im_connection { bee_t *bee; GSList *groupchats; + GSList *chatlist; }; struct groupchat { @@ -262,6 +263,13 @@ struct prpl { /* If null, equivalent to handle_cmp( ic->acc->user, who ) */ gboolean (* handle_is_self) (struct im_connection *, const char *who); + /* This sets/updates the im_connection->chatlist field with a + * bee_chat_info_t GSList. This function should ensure the + * bee_chat_list_finish() function gets called at some point + * after the chat list is completely updated. + */ + void (*chat_list) (struct im_connection *, const char *server); + /* Some placeholders so eventually older plugins may cooperate with newer BitlBees. */ void *resv1; void *resv2; diff --git a/protocols/purple/bpurple.h b/protocols/purple/bpurple.h index 39677b86..c7b3a59c 100644 --- a/protocols/purple/bpurple.h +++ b/protocols/purple/bpurple.h @@ -14,4 +14,11 @@ struct purple_data guint next_request_id; }; +struct purple_roomlist_data +{ + GSList *chats; + gint topic; + gint userc; +}; + #endif /* !BPURPLE_H */ diff --git a/protocols/purple/purple.c b/protocols/purple/purple.c index db553407..114fec82 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); @@ -353,6 +365,21 @@ static void purple_login(account_t *acc) } } +static void purple_chatlist_free(struct im_connection *ic) +{ + bee_chat_info_t *ci; + GSList *l = ic->chatlist; + + while (l) { + ci = l->data; + l = g_slist_delete_link(l, l); + + g_free(ci->title); + g_free(ci->topic); + g_free(ci); + } +} + static void purple_logout(struct im_connection *ic) { struct purple_data *pd = ic->proto_data; @@ -365,9 +392,12 @@ 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); + purple_chatlist_free(ic); g_hash_table_destroy(pd->input_requests); g_free(pd); } @@ -731,6 +761,18 @@ struct groupchat *purple_chat_join(struct im_connection *ic, const char *room, c return imcb_chat_new(ic, room); } +void purple_chat_list(struct im_connection *ic, const char *server) +{ + PurpleRoomlist *list; + struct purple_data *pd = ic->proto_data; + + list = purple_roomlist_get_list(pd->account->gc); + + if (list == NULL) { + imcb_log(ic, "Room listing unsupported by this purple plugin"); + } +} + void purple_transfer_request(struct im_connection *ic, file_transfer_t *ft, char *handle); static void purple_ui_init(); @@ -1266,6 +1308,116 @@ static PurplePrivacyUiOps bee_privacy_uiops = prplcb_privacy_deny_removed, /* deny_removed */ }; +static void prplcb_roomlist_create(PurpleRoomlist *list) +{ + struct purple_roomlist_data *rld; + + list->ui_data = rld = g_new0(struct purple_roomlist_data, 1); + rld->topic = -1; + rld->userc = -1; +} + +static void prplcb_roomlist_set_fields(PurpleRoomlist *list, GList *fields) +{ + gint topic = -1; + GList *l; + guint i; + PurpleRoomlistField *field; + struct purple_roomlist_data *rld = list->ui_data; + + for (i = 0, l = fields; l; i++, l = l->next) { + field = l->data; + + /* Use the first visible string field as a fallback topic */ + if (i != 0 && topic < 0 && !field->hidden && + field->type == PURPLE_ROOMLIST_FIELD_STRING) { + topic = i; + } + + if ((g_strcasecmp(field->name, "description") == 0) || + (g_strcasecmp(field->name, "topic") == 0)) { + if (field->type == PURPLE_ROOMLIST_FIELD_STRING) { + rld->topic = i; + } + } else if (g_strcasecmp(field->name, "users") == 0) { + if (field->type == PURPLE_ROOMLIST_FIELD_INT) { + rld->userc = i; + } + } + } + + if (rld->topic < 0) { + rld->topic = topic; + } +} + +static void prplcb_roomlist_add_room(PurpleRoomlist *list, PurpleRoomlistRoom *room) +{ + bee_chat_info_t *ci; + const char *title; + const char *topic; + GList *fields; + gpointer data; + struct purple_roomlist_data *rld = list->ui_data; + + fields = purple_roomlist_room_get_fields(room); + title = purple_roomlist_room_get_name(room); + + if (rld->topic >= 0) { + topic = g_list_nth_data(fields, rld->topic); + } else { + topic = NULL; + } + + ci = g_new(bee_chat_info_t, 1); + ci->title = g_strdup(title); + ci->topic = g_strdup(topic); + rld->chats = g_slist_prepend(rld->chats, ci); + + if (rld->userc >= 0) { + data = g_list_nth_data(fields, rld->userc); + ci->userc = GPOINTER_TO_INT(data); + } else { + ci->userc = -1; + } +} + +static void prplcb_roomlist_in_progress(PurpleRoomlist *list, gboolean in_progress) +{ + struct im_connection *ic; + struct purple_roomlist_data *rld = list->ui_data; + + if (in_progress) { + return; + } + + ic = purple_ic_by_pa(list->account); + purple_chatlist_free(ic); + + ic->chatlist = g_slist_reverse(rld->chats); + rld->chats = NULL; + + bee_chat_list_finish(ic); + purple_roomlist_unref(list); +} + +static void prplcb_roomlist_destroy(PurpleRoomlist *list) +{ + /* bee_chat_list_finish() frees rld->chats */ + g_free(list->ui_data); + list->ui_data = NULL; +} + +static PurpleRoomlistUiOps bee_roomlist_uiops = +{ + NULL, /* show_with_account */ + prplcb_roomlist_create, /* create */ + prplcb_roomlist_set_fields, /* set_fields */ + prplcb_roomlist_add_room, /* add_room */ + prplcb_roomlist_in_progress, /* in_progress */ + prplcb_roomlist_destroy, /* destroy */ +}; + static void prplcb_debug_print(PurpleDebugLevel level, const char *category, const char *arg_s) { fprintf(stderr, "DEBUG %s: %s", category, arg_s); @@ -1430,6 +1582,7 @@ static void purple_ui_init() purple_conversations_set_ui_ops(&bee_conv_uiops); purple_request_set_ui_ops(&bee_request_uiops); purple_privacy_set_ui_ops(&bee_privacy_uiops); + purple_roomlist_set_ui_ops(&bee_roomlist_uiops); purple_notify_set_ui_ops(&bee_notify_uiops); purple_accounts_set_ui_ops(&bee_account_uiops); purple_xfers_set_ui_ops(&bee_xfer_uiops); @@ -1521,6 +1674,7 @@ void purple_initmodule() funcs.chat_kick = purple_chat_kick; funcs.chat_leave = purple_chat_leave; funcs.chat_join = purple_chat_join; + funcs.chat_list = purple_chat_list; funcs.transfer_request = purple_transfer_request; help = g_string_new("BitlBee libpurple module supports the following IM protocols:\n"); @@ -1539,8 +1693,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); @@ -1548,12 +1702,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 fa66fbe9..b8e31035 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 || @@ -584,6 +608,9 @@ static void twitter_init(account_t * acc) s = set_add(&acc->set, "long_ids", "2", set_eval_long_ids, 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; @@ -670,7 +697,9 @@ static void twitter_login(account_t * acc) 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) { @@ -1005,7 +1034,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; @@ -1044,7 +1074,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 22b9d561..6b7c0c0c 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,8 @@ 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; diff --git a/protocols/twitter/twitter_lib.c b/protocols/twitter/twitter_lib.c index 51963d39..a1e969a1 100644 --- a/protocols/twitter/twitter_lib.c +++ b/protocols/twitter/twitter_lib.c @@ -808,17 +808,29 @@ static char *twitter_msg_add_id(struct im_connection *ic, } } - 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); + 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")) { @@ -1010,8 +1022,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); @@ -1091,10 +1118,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; + tl = txs->from_filter ? td->filter_log : td->log; for (i = 0; i < td->log_length; i++) { - if (td->log[i].id == txs->id) { + if (tl[i].id == txs->id) { /* Got a duplicate (RT, probably). Drop it. */ return TRUE; } @@ -1186,6 +1215,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; } @@ -1241,6 +1271,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; } @@ -1792,3 +1823,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 |