/* * iq.c * * Copyright (C) 2012 - 2018 James Booth * * This file is part of Profanity. * * Profanity is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Profanity is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Profanity. If not, see . * * In addition, as a special exception, the copyright holders give permission to * link the code of portions of this program with the OpenSSL library under * certain conditions as described in each individual source file, and * distribute linked combinations including the two. * * You must obey the GNU General Public License in all respects for all of the * code used other than OpenSSL. If you modify file(s) with this exception, you * may extend this exception to your version of the file(s), but you are not * obligated to do so. If you do not wish to do so, delete this exception * statement from your version. If you delete this exception statement from all * source files in the program, then also delete it here. * */ #include "config.h" #ifdef HAVE_GIT_VERSION #include "gitversion.h" #endif #include #include #include #include #ifdef HAVE_LIBMESODE #include #endif #ifdef HAVE_LIBSTROPHE #include #endif #include "profanity.h" #include "log.h" #include "config/preferences.h" #include "event/server_events.h" #include "plugins/plugins.h" #include "tools/http_upload.h" #include "ui/ui.h" #include "ui/window_list.h" #include "xmpp/xmpp.h" #include "xmpp/connection.h" #include "xmpp/session.h" #include "xmpp/iq.h" #include "xmpp/capabilities.h" #include "xmpp/blocking.h" #include "xmpp/session.h" #include "xmpp/stanza.h" #include "xmpp/form.h" #include "xmpp/roster_list.h" #include "xmpp/roster.h" #include "xmpp/muc.h" typedef struct p_room_info_data_t { char *room; gboolean display; } ProfRoomInfoData; typedef struct p_id_handle_t { ProfIdCallback func; ProfIdFreeCallback free_func; void *userdata; } ProfIdHandler; typedef struct privilege_set_t { char *item; char *privilege; } ProfPrivilegeSet; static int _iq_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata); static void _error_handler(xmpp_stanza_t *const stanza); static void _disco_info_get_handler(xmpp_stanza_t *const stanza); static void _disco_items_get_handler(xmpp_stanza_t *const stanza); static void _disco_items_result_handler(xmpp_stanza_t *const stanza); static void _last_activity_get_handler(xmpp_stanza_t *const stanza); static void _version_get_handler(xmpp_stanza_t *const stanza); static void _ping_get_handler(xmpp_stanza_t *const stanza); static int _version_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _disco_info_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _disco_info_response_id_handler_onconnect(xmpp_stanza_t *const stanza, void *const userdata); static int _http_upload_response_id_handler(xmpp_stanza_t *const stanza, void *const upload_ctx); static int _last_activity_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_info_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _destroy_room_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_config_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_config_submit_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_affiliation_list_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_affiliation_set_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_role_set_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_role_list_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_kick_result_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _enable_carbons_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _disable_carbons_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _manual_pong_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _caps_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _caps_response_for_jid_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _caps_response_legacy_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _auto_pong_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static int _room_list_id_handler(xmpp_stanza_t *const stanza, void *const userdata); static void _iq_free_room_data(ProfRoomInfoData *roominfo); static void _iq_free_affiliation_set(ProfPrivilegeSet *affiliation_set); // scheduled static int _autoping_timed_send(xmpp_conn_t *const conn, void *const userdata); static void _identity_destroy(DiscoIdentity *identity); static void _item_destroy(DiscoItem *item); static gboolean autoping_wait = FALSE; static GTimer *autoping_time = NULL; static GHashTable *id_handlers; static int _iq_handler(xmpp_conn_t *const conn, xmpp_stanza_t *const stanza, void *const userdata) { log_debug("iq stanza handler fired"); char *text; size_t text_size; xmpp_stanza_to_text(stanza, &text, &text_size); gboolean cont = plugins_on_iq_stanza_receive(text); xmpp_free(connection_get_ctx(), text); if (!cont) { return 1; } const char *type = xmpp_stanza_get_type(stanza); if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) { _error_handler(stanza); } xmpp_stanza_t *discoinfo = xmpp_stanza_get_child_by_ns(stanza, XMPP_NS_DISCO_INFO); if (discoinfo && (g_strcmp0(type, STANZA_TYPE_GET) == 0)) { _disco_info_get_handler(stanza); } xmpp_stanza_t *discoitems = xmpp_stanza_get_child_by_ns(stanza, XMPP_NS_DISCO_ITEMS); if (discoitems && (g_strcmp0(type, STANZA_TYPE_GET) == 0)) { _disco_items_get_handler(stanza); } if (discoitems && (g_strcmp0(type, STANZA_TYPE_RESULT) == 0)) { _disco_items_result_handler(stanza); } xmpp_stanza_t *lastactivity = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_LASTACTIVITY); if (lastactivity && (g_strcmp0(type, STANZA_TYPE_GET) == 0)) { _last_activity_get_handler(stanza); } xmpp_stanza_t *version = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_VERSION); if (version && (g_strcmp0(type, STANZA_TYPE_GET) == 0)) { _version_get_handler(stanza); } xmpp_stanza_t *ping = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_PING); if (ping && (g_strcmp0(type, STANZA_TYPE_GET) == 0)) { _ping_get_handler(stanza); } xmpp_stanza_t *roster = xmpp_stanza_get_child_by_ns(stanza, XMPP_NS_ROSTER); if (roster && (g_strcmp0(type, STANZA_TYPE_SET) == 0)) { roster_set_handler(stanza); } if (roster && (g_strcmp0(type, STANZA_TYPE_RESULT) == 0)) { roster_result_handler(stanza); } xmpp_stanza_t *blocking = xmpp_stanza_get_child_by_ns(stanza, STANZA_NS_BLOCKING); if (blocking && (g_strcmp0(type, STANZA_TYPE_SET) == 0)) { blocked_set_handler(stanza); } const char *id = xmpp_stanza_get_id(stanza); if (id) { ProfIdHandler *handler = g_hash_table_lookup(id_handlers, id); if (handler) { int keep = handler->func(stanza, handler->userdata); if (!keep) { free(handler); g_hash_table_remove(id_handlers, id); } } } return 1; } void iq_handlers_init(void) { xmpp_conn_t * const conn = connection_get_conn(); xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_handler_add(conn, _iq_handler, NULL, STANZA_NAME_IQ, NULL, ctx); if (prefs_get_autoping() != 0) { int millis = prefs_get_autoping() * 1000; xmpp_timed_handler_add(conn, _autoping_timed_send, millis, ctx); } if (id_handlers) { GList *keys = g_hash_table_get_keys(id_handlers); GList *curr = keys; while (curr) { ProfIdHandler *handler = g_hash_table_lookup(id_handlers, curr->data); if (handler->free_func && handler->userdata) { handler->free_func(handler->userdata); } curr = g_list_next(curr); } g_list_free(keys); g_hash_table_destroy(id_handlers); } id_handlers = g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL); } void iq_id_handler_add(const char *const id, ProfIdCallback func, ProfIdFreeCallback free_func, void *userdata) { ProfIdHandler *handler = malloc(sizeof(ProfIdHandler)); handler->func = func; handler->free_func = free_func; handler->userdata = userdata; g_hash_table_insert(id_handlers, strdup(id), handler); } void iq_autoping_check(void) { if (connection_get_status() != JABBER_CONNECTED) { return; } if (autoping_wait == FALSE) { return; } if (autoping_time == NULL) { return; } gdouble elapsed = g_timer_elapsed(autoping_time, NULL); unsigned long seconds_elapsed = elapsed * 1.0; gint timeout = prefs_get_autoping_timeout(); if (timeout > 0 && seconds_elapsed >= timeout) { cons_show("Autoping response timed out after %u seconds.", timeout); log_debug("Autoping check: timed out after %u seconds, disconnecting", timeout); session_autoping_fail(); autoping_wait = FALSE; g_timer_destroy(autoping_time); autoping_time = NULL; } } void iq_set_autoping(const int seconds) { if (connection_get_status() != JABBER_CONNECTED) { return; } xmpp_conn_t * const conn = connection_get_conn(); xmpp_timed_handler_delete(conn, _autoping_timed_send); if (seconds == 0) { return; } int millis = seconds * 1000; xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_timed_handler_add(conn, _autoping_timed_send, millis, ctx); } void iq_room_list_request(gchar *conferencejid, gchar *filter) { xmpp_ctx_t * const ctx = connection_get_ctx(); char *id = create_unique_id("confreq"); xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, id, conferencejid); iq_id_handler_add(id, _room_list_id_handler, NULL, filter); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_enable_carbons(void) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_enable_carbons(ctx); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _enable_carbons_id_handler, NULL, NULL); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_disable_carbons(void) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_disable_carbons(ctx); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _disable_carbons_id_handler, NULL, NULL); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_http_upload_request(HTTPUpload *upload) { char *jid = connection_jid_for_feature(STANZA_NS_HTTP_UPLOAD); if (jid == NULL) { cons_show_error("XEP-0363 HTTP File Upload is not supported by the server"); return; } xmpp_ctx_t * const ctx = connection_get_ctx(); char *id = create_unique_id("http_upload_request"); xmpp_stanza_t *iq = stanza_create_http_upload_request(ctx, id, jid, upload); // TODO add free func iq_id_handler_add(id, _http_upload_response_id_handler, NULL, upload); free(id); iq_send_stanza(iq); xmpp_stanza_release(iq); return; } void iq_disco_info_request(gchar *jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); char *id = create_unique_id("disco_info"); xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, jid, NULL); iq_id_handler_add(id, _disco_info_response_id_handler, NULL, NULL); free(id); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_disco_info_request_onconnect(gchar *jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); char *id = create_unique_id("disco_info_onconnect"); xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, jid, NULL); iq_id_handler_add(id, _disco_info_response_id_handler_onconnect, NULL, NULL); free(id); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_last_activity_request(gchar *jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); char *id = create_unique_id("lastactivity"); xmpp_stanza_t *iq = stanza_create_last_activity_iq(ctx, id, jid); iq_id_handler_add(id, _last_activity_response_id_handler, NULL, NULL); free(id); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_room_info_request(const char *const room, gboolean display_result) { xmpp_ctx_t * const ctx = connection_get_ctx(); char *id = create_unique_id("room_disco_info"); xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, room, NULL); ProfRoomInfoData *cb_data = malloc(sizeof(ProfRoomInfoData)); cb_data->room = strdup(room); cb_data->display = display_result; iq_id_handler_add(id, _room_info_response_id_handler, (ProfIdFreeCallback)_iq_free_room_data, cb_data); free(id); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_send_caps_request_for_jid(const char *const to, const char *const id, const char *const node, const char *const ver) { xmpp_ctx_t * const ctx = connection_get_ctx(); if (!node) { log_error("Could not create caps request, no node"); return; } if (!ver) { log_error("Could not create caps request, no ver"); return; } GString *node_str = g_string_new(""); g_string_printf(node_str, "%s#%s", node, ver); xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, to, node_str->str); g_string_free(node_str, TRUE); iq_id_handler_add(id, _caps_response_for_jid_id_handler, free, strdup(to)); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_send_caps_request(const char *const to, const char *const id, const char *const node, const char *const ver) { xmpp_ctx_t * const ctx = connection_get_ctx(); if (!node) { log_error("Could not create caps request, no node"); return; } if (!ver) { log_error("Could not create caps request, no ver"); return; } GString *node_str = g_string_new(""); g_string_printf(node_str, "%s#%s", node, ver); xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, to, node_str->str); g_string_free(node_str, TRUE); iq_id_handler_add(id, _caps_response_id_handler, NULL, NULL); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_send_caps_request_legacy(const char *const to, const char *const id, const char *const node, const char *const ver) { xmpp_ctx_t * const ctx = connection_get_ctx(); if (!node) { log_error("Could not create caps request, no node"); return; } if (!ver) { log_error("Could not create caps request, no ver"); return; } GString *node_str = g_string_new(""); g_string_printf(node_str, "%s#%s", node, ver); xmpp_stanza_t *iq = stanza_create_disco_info_iq(ctx, id, to, node_str->str); iq_id_handler_add(id, _caps_response_legacy_id_handler, g_free, node_str->str); g_string_free(node_str, FALSE); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_disco_items_request(gchar *jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, "discoitemsreq", jid); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_disco_items_request_onconnect(gchar *jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_disco_items_iq(ctx, "discoitemsreq_onconnect", jid); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_send_software_version(const char *const fulljid) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_software_version_iq(ctx, fulljid); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _version_result_id_handler, free, strdup(fulljid)); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_confirm_instant_room(const char *const room_jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_instant_room_request_iq(ctx, room_jid); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_destroy_room(const char *const room_jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_instant_room_destroy_iq(ctx, room_jid); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _destroy_room_result_id_handler, NULL, NULL); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_request_room_config_form(const char *const room_jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_room_config_request_iq(ctx, room_jid); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _room_config_id_handler, NULL, NULL); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_submit_room_config(const char *const room, DataForm *form) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_room_config_submit_iq(ctx, room, form); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _room_config_submit_id_handler, NULL, NULL); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_room_config_cancel(const char *const room_jid) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_room_config_cancel_iq(ctx, room_jid); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_room_affiliation_list(const char *const room, char *affiliation) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_room_affiliation_list_iq(ctx, room, affiliation); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _room_affiliation_list_result_id_handler, free, strdup(affiliation)); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_room_kick_occupant(const char *const room, const char *const nick, const char *const reason) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_room_kick_iq(ctx, room, nick, reason); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _room_kick_result_id_handler, free, strdup(nick)); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_room_affiliation_set(const char *const room, const char *const jid, char *affiliation, const char *const reason) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_room_affiliation_set_iq(ctx, room, jid, affiliation, reason); const char *id = xmpp_stanza_get_id(iq); ProfPrivilegeSet *affiliation_set = malloc(sizeof(struct privilege_set_t)); affiliation_set->item = strdup(jid); affiliation_set->privilege = strdup(affiliation); iq_id_handler_add(id, _room_affiliation_set_result_id_handler, (ProfIdFreeCallback)_iq_free_affiliation_set, affiliation_set); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_room_role_set(const char *const room, const char *const nick, char *role, const char *const reason) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_room_role_set_iq(ctx, room, nick, role, reason); const char *id = xmpp_stanza_get_id(iq); struct privilege_set_t *role_set = malloc(sizeof(ProfPrivilegeSet)); role_set->item = strdup(nick); role_set->privilege = strdup(role); iq_id_handler_add(id, _room_role_set_result_id_handler, (ProfIdFreeCallback)_iq_free_affiliation_set, role_set); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_room_role_list(const char *const room, char *role) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_room_role_list_iq(ctx, room, role); const char *id = xmpp_stanza_get_id(iq); iq_id_handler_add(id, _room_role_list_result_id_handler, free, strdup(role)); iq_send_stanza(iq); xmpp_stanza_release(iq); } void iq_send_ping(const char *const target) { xmpp_ctx_t * const ctx = connection_get_ctx(); xmpp_stanza_t *iq = stanza_create_ping_iq(ctx, target); const char *id = xmpp_stanza_get_id(iq); GDateTime *now = g_date_time_new_now_local(); iq_id_handler_add(id, _manual_pong_id_handler, (ProfIdFreeCallback)g_date_time_unref, now); iq_send_stanza(iq); xmpp_stanza_release(iq); } static void _error_handler(xmpp_stanza_t *const stanza) { const char *id = xmpp_stanza_get_id(stanza); char *error_msg = stanza_get_error_message(stanza); if (id) { log_debug("IQ error handler fired, id: %s, error: %s", id, error_msg); log_error("IQ error received, id: %s, error: %s", id, error_msg); } else { log_debug("IQ error handler fired, error: %s", error_msg); log_error("IQ error received, error: %s", error_msg); } free(error_msg); } static int _caps_response_id_handler(xmpp_stanza_t *const stanza, void *const userdata) { const char *id = xmpp_stanza_get_id(stanza); xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY); const char *type = xmpp_stanza_get_type(stanza); // ignore non result if ((g_strcmp0(type, "get") == 0) || (g_strcmp0(type, "set") == 0)) { return 1; } if (id) { log_info("Capabilities response handler fired for id %s", id); } else { log_info("Capabilities response handler fired"); } const char *from = xmpp_stanza_get_from(stanza); if (!from) { log_info("No from attribute"); return 0; } // handle error responses if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) { char *error_message = stanza_get_error_message(stanza); log_warning("Error received for capabilities response from %s: ", from, error_message); free(error_message); return 0; } if (query == NULL) { log_info("No query element found."); return 0; } const char *node = xmpp_stanza_get_attribute(query, STANZA_ATTR_NODE); if (node == NULL) { log_info("No node attribute found"); return 0; } // validate sha1 gchar **split = g_strsplit(node, "#", -1); char *given_sha1 = split[1]; char *generated_sha1 = stanza_create_caps_sha1_from_query(query); if (g_strcmp0(given_sha1, generated_sha1) != 0) { log_warning("Generated sha-1 does not match given:"); log_warning("Generated : %s", generated_sha1); log_warning("Given : %s", given_sha1); } else { log_info("Valid SHA-1 hash found: %s", given_sha1); if (caps_cache_contains(given_sha1)) { log_info("Capabilties already cached: %s", given_sha1); } else { log_info("Capabilities not cached: %s, storing", given_sha1); EntityCapabilities *capabilities = stanza_create_caps_from_query_element(query); caps_add_by_ver(given_sha1, capabilities); caps_destroy(capabilities); } caps_map_jid_to_ver(from, given_sha1); } g_free(generated_sha1); g_strfreev(split); return 0; } static int _caps_response_for_jid_id_handler(xmpp_stanza_t *const stanza, void *const userdata) { char *jid = (char *)userdata; const char *id = xmpp_stanza_get_id(stanza); xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY); const char *type = xmpp_stanza_get_type(stanza); // ignore non result if ((g_strcmp0(type, "get") == 0) || (g_strcmp0(type, "set") == 0)) { free(jid); return 1; } if (id) { log_info("Capabilities response handler fired for id %s", id); } else { log_info("Capabilities response handler fired"); } const char *from = xmpp_stanza_get_from(stanza); if (!from) { log_info("No from attribute"); free(jid); return 0; } // handle error responses if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) { char *error_message = stanza_get_error_message(stanza); log_warning("Error received for capabilities response from %s: ", from, error_message); free(error_message); free(jid); return 0; } if (query == NULL) { log_info("No query element found."); free(jid); return 0; } const char *node = xmpp_stanza_get_attribute(query, STANZA_ATTR_NODE); if (node == NULL) { log_info("No node attribute found"); free(jid); return 0; } log_info("Associating capabilities with: %s", jid); EntityCapabilities *capabilities = stanza_create_caps_from_query_element(query); caps_add_by_jid(jid, capabilities); free(jid); return 0; } static int _caps_response_legacy_id_handler(xmpp_stanza_t *const stanza, void *const userdata) { const char *id = xmpp_stanza_get_id(stanza); xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY); char *expected_node = (char *)userdata; const char *type = xmpp_stanza_get_type(stanza); // ignore non result if ((g_strcmp0(type, "get") == 0) || (g_strcmp0(type, "set") == 0)) { free(expected_node); return 1; } if (id) { log_info("Capabilities response handler fired for id %s", id); } else { log_info("Capabilities response handler fired"); } const char *from = xmpp_stanza_get_from(stanza); if (!from) { log_info("No from attribute"); free(expected_node); return 0; } // handle error responses if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) { char *error_message = stanza_get_error_message(stanza); log_warning("Error received for capabilities response from %s: ", from, error_message); free(error_message); free(expected_node); return 0; } if (query == NULL) { log_info("No query element found."); free(expected_node); return 0; } const char *node = xmpp_stanza_get_attribute(query, STANZA_ATTR_NODE); if (node == NULL) { log_info("No node attribute found"); free(expected_node); return 0; } // nodes match if (g_strcmp0(expected_node, node) == 0) { log_info("Legacy capabilities, nodes match %s", node); if (caps_cache_contains(node)) { log_info("Capabilties already cached: %s", node); } else { log_info("Capabilities not cached: %s, storing", node); EntityCapabilities *capabilities = stanza_create_caps_from_query_element(query); caps_add_by_ver(node, capabilities); caps_destroy(capabilities); } caps_map_jid_to_ver(from, node); // node match fail } else { log_info("Legacy Capabilities nodes do not match, expeceted %s, given %s.", expected_node, node); } free(expected_node); return 0; } static int _room_list_id_handler(xmpp_stanza_t *const stanza, void *const userdata) { const char *id = xmpp_stanza_get_id(stanza); const char *from = xmpp_stanza_get_from(stanza); log_debug("Response to query: %s", id); xmpp_stanza_t *query = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_QUERY); if (query == NULL) { return 0; } xmpp_stanza_t *child = xmpp_stanza_get_children(query); if (child == NULL) { cons_show("No rooms found for service: %s", from); return 0; } GPatternSpec *glob = NULL; gchar *filter = (gchar*)userdata; if (filter != NULL) { glob = g_pattern_spec_new(filter); g_free(filter); } gboolean matched = FALSE; cons_show("Chat rooms at: %s", from); while (child) { const char *stanza_name = xmpp_stanza_get_name(child); if (stanza_name && (g_strcmp0(stanza_name, STANZA_NAME_ITEM) == 0)) { const char *item_jid = xmpp_stanza_get_attribute(child, STANZA_ATTR_JID); const char *item_name = xmpp_stanza_get_attribute(child, STANZA_ATTR_NAME); if ((item_jid) && ((glob == NULL) || ((g_pattern_match(glob, strlen(item_jid), item_jid, NULL)) || (item_name && g_pattern_match(glob, strlen(item_name), item_name, NULL))))) { if (glob) { matched = TRUE; } GString *item = g_string_new(item_jid); if (item_name) { g_string_append(item, " ("); g_string_append(item, item_name); g_string_append(item, ")"); } cons_show(" %s", item->str); g_string_free(item, TRUE); } } child = xmpp_stanza_get_next(child); } if (glob && matched == FALSE) { cons_show(" No rooms found matching pattern."); } g_pattern_spec_free(glob); return 0; } static int _enable_carbons_id_handler(xmpp_stanza_t *const stanza, void *const userdata) { const char *type = xmpp_stanza_get_type(stanza); if (g_strcmp0(type, "error") == 0) { char *error_message = stanza_get_error_message(stanza); cons_show_error("Server error enabling message carbons: %s", error_message); log_debug("Error enabling carbons: %s", error_message); free(error_message); } else { log_debug("Message carbons enabled."); } return 0; } static int _disable_carbons_id_handler(xmpp_stanza_t *const stanza, void *const userdata) { const char *type = xmpp_stanza_get_type(stanza); if (g_strcmp0(type, "error") == 0) { char *error_message = stanza_get_error_message(stanza); cons_show_error("Server error disabling message carbons: %s", error_message); log_debug("Error disabling carbons: %s", error_message); free(error_message); } else { log_debug("Message carbons disabled."); } return 0; } static int _manual_pong_id_handler(xmpp_stanza_t *const stanza, void *const userdata) { const char *from = xmpp_stanza_get_from(stanza); const char *type = xmpp_stanza_get_type(stanza); GDateTime *sent = (GDateTime *)userdata; // handle error responses if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) { char *error_message = stanza_get_error_message(stanza); if (!error_message) { cons_show_error("Error returned from pinging %s.", from); } else { cons_show_error("Error returned from pinging %s: %s.", from, error_message); } free(error_message); g_date_time_unref(sent); return 0; } GDateTime *now = g_date_time_new_now_local(); GTimeSpan elapsed = g_date_time_difference(now, sent); int elapsed_millis = elapsed / 1000; g_date_time_unref(sent); g_date_time_unref(now); if (from == NULL) { cons_show("Ping response from server: %dms.", elapsed_millis); } else { cons_show("Ping response from %s: %dms.", from, elapsed_millis); } return 0; } static int _autoping_timed_send(xmpp_conn_t *const conn, void *const userdata) { if (connection_get_status() != JABBER_CONNECTED) { return 1; } if (connection_supports(STANZA_NS_PING) == FALSE) { log_warning("Server doesn't advertise %s feature, disabling autoping.", STANZA_NS_PING); prefs_set_autoping(0); cons_show_error("Server ping not supported, autoping disabled."); xmpp_conn_t *conn = connection_get_conn(); xmpp_timed_handler_delete(conn, _autoping_timed_send); return 1; } if (autoping_wait) { log_debug("Autop
-- text editor, particularly text drawing, horizontal wrap, vertical scrolling
Text = {}

local utf8 = require 'utf8'

require 'search'
require 'select'
require 'undo'
require 'text_tests'

-- return values:
--  y coordinate drawn until in px
--  position of start of final screen line drawn
function Text.draw(line, line_width, line_index)
--?   print('text.draw', line_index)
  love.graphics.setColor(0,0,0)
--?   love.graphics.line(Line_width,0, Line_width,App.screen.height)
  -- wrap long lines
  local x = Margin_left
  local y = line.y
  local pos = 1
  local screen_line_starting_pos = 1
  if line.fragments == nil then
    Text.compute_fragments(line, line_width)
  end
  Text.populate_screen_line_starting_pos(line_index)
--?   print('--')
  for _, f in ipairs(line.fragments) do
    local frag, frag_text = f.data, f.text
    -- render fragment
    local frag_width = App.width(frag_text)
    local frag_len = utf8.len(frag)
--?     local s=tostring
--?     print('('..s(x)..','..s(y)..') '..frag..'('..s(frag_width)..' vs '..s(line_width)..') '..s(line_index)..' vs '..s(Screen_top1.line)..'; '..s(pos)..' vs '..s(Screen_top1.pos)..'; bottom: '..s(Screen_bottom1.line)..'/'..s(Screen_bottom1.pos))
    if x + frag_width > line_width then
      assert(x > Margin_left)  -- no overfull lines
      -- update y only after drawing the first screen line of screen top
      if Text.lt1(Screen_top1, {line=line_index, pos=pos}) then
        y = y + Line_height
        if y + Line_height > App.screen.height then
--?           print('b', y, App.screen.height, '=>', screen_line_starting_pos)
          return y, screen_line_starting_pos
        end
        screen_line_starting_pos = pos
--?         print('text: new screen line', y, App.screen.height, screen_line_starting_pos)
      end
      x = Margin_left
    end
--?     print('checking to draw', pos, Screen_top1.pos)
    -- don't draw text above screen top
    if Text.le1(Screen_top1, {line=line_index, pos=pos}) then
      if Selection1.line then
        local lo, hi = Text.clip_selection(line_index, pos, pos+frag_len)
        Text.draw_highlight(line, x,y, pos, lo,hi)
      end
--?       print('drawing '..frag)
      App.screen.draw(frag_text, x,y)
    end
    -- render cursor if necessary
    if line_index == Cursor1.line then
      if pos <= Cursor1.pos and pos + frag_len > Cursor1.pos then
        if Search_term then
          if Lines[Cursor1.line].data:sub(Cursor1.pos, Cursor1.pos+utf8.len(Search_term)-1) == Search_term then
            local lo_px = Text.draw_highlight(line, x,y, pos, Cursor1.pos, Cursor1.pos+utf8.len(Search_term))
            love.graphics.setColor(0,0,0)
            love.graphics.print(Search_term, x+lo_px,y)
          end
        else
          Text.draw_cursor(x+Text.x(frag, Cursor1.pos-pos+1), y)
        end
      end
    end
    x = x + frag_width
    pos = pos + frag_len
  end
  if Search_term == nil then
    if line_index == Cursor1.line and Cursor1.pos == pos then
      Text.draw_cursor(x, y)
    end
  end
  return y, screen_line_starting_pos
end
-- manual tests:
--  draw with small line_width of 100

function Text.draw_cursor(x, y)
  -- blink every 0.5s
  if math.floor(Cursor_time*2)%2 == 0 then
    love.graphics.setColor(1,0,0)
    love.graphics.rectangle('fill', x,y, 3,Line_height)
    love.graphics.setColor(0,0,0)
  end
  Cursor_x = x
  Cursor_y = y+Line_height
end

function Text.compute_fragments(line, line_width)
--?   print('compute_fragments', line_width)
  line.fragments = {}
  local x = Margin_left
  -- try to wrap at word boundaries
  for frag in line.data:gmatch('%S*%s*') do
    local frag_text = App.newText(love.graphics.getFont(), frag)
    local frag_width = App.width(frag_text)
--?     print('x: '..tostring(x)..'; '..tostring(line_width-x)..'px to go')
--?     print('frag: ^'..frag..'$ is '..tostring(frag_width)..'px wide')
    if x + frag_width > line_width then
      while x + frag_width > line_width do
--?         print(x, frag, frag_width, line_width)
        if x < 0.8*line_width then
--?           print(frag, x, frag_width, line_width)
          -- long word; chop it at some letter
          -- We're not going to reimplement TeX here.
          local bpos = Text.nearest_pos_less_than(frag, line_width - x)
          assert(bpos > 0)  -- avoid infinite loop when window is too narrow
          local boffset = Text.offset(frag, bpos+1)  -- byte _after_ bpos
--?           print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset)..' bytes')
          local frag1 = string.sub(frag, 1, boffset-1)
          local frag1_text = App.newText(love.graphics.getFont(), frag1)
          local frag1_width = App.width(frag1_text)
--?           print(frag, x, frag1_width, line_width)
          assert(x + frag1_width <= line_width)
--?           print('inserting '..frag1..' of width '..tostring(frag1_width)..'px')
          table.insert(line.fragments, {data=frag1, text=frag1_text})
          frag = string.sub(frag, boffset)
          frag_text = App.newText(love.graphics.getFont(), frag)
          frag_width = App.width(frag_text)
        end
        x = Margin_left  -- new line
      end
    end
    if #frag > 0 then
--?       print('inserting '..frag..' of width '..tostring(frag_width)..'px')
      table.insert(line.fragments, {data=frag, text=frag_text})
    end
    x = x + frag_width
  end
end

function Text.textinput(t)
  if App.mouse_down(1) then return end
  assert(not App.ctrl_down())
  if App.alt_down() or App.cmd_down() then return end
  local before = snapshot(Cursor1.line)
--?   print(Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos, Screen_bottom1.line, Screen_bottom1.pos)
  Text.insert_at_cursor(t)
  if Cursor_y >= App.screen.height - Line_height then
    Text.populate_screen_line_starting_pos(Cursor1.line)
    Text.snap_cursor_to_bottom_of_screen()
--?     print('=>', Screen_top1.line, Screen_top1.pos, Cursor1.line, Cursor1.pos, Screen_bottom1.line, Screen_bottom1.pos)
  end
  record_undo_event({before=before, after=snapshot(Cursor1.line)})
end

function Text.insert_at_cursor(t)
  local byte_offset = Text.offset(Lines[Cursor1.line].data, Cursor1.pos)
  Lines[Cursor1.line].data = string.sub(Lines[Cursor1.line].data, 1, byte_offset-1)..t..string.sub(Lines[Cursor1.line].data, byte_offset)
  Text.clear_cache(Lines[Cursor1.line])
  Cursor1.pos = Cursor1.pos+1
end

-- Don't handle any keys here that would trigger love.textinput above.
function Text.keychord_pressed(chord)
--?   print('chord', chord, Selection1.line, Selection1.pos)
  --== shortcuts that mutate text
  if chord == 'return' then
    local before_line = Cursor1.line
    local before = snapshot(before_line)
    Text.insert_return()
    Selection1 = {}
    if (Cursor_y +</