Thu, 05 Sep 2024 23:41:54 -0500
Prepare for the next development cycle
Testing Done:
Ran `meson dist`
Reviewed at https://reviews.imfreedom.org/r/3471/
/* * Ibis - IRCv3 Library * Copyright (C) 2022-2024 Ibis Developers <devel@pidgin.im> * * Ibis is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This library 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 2 of the License, or * (at your option) any later version. * * This library 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 this library; if not, see <https://www.gnu.org/licenses/>. */ #include <glib/gi18n-lib.h> #include <birb.h> #include "ibisclient.h" #include "ibisconstants.h" #include "ibissource.h" #include "ibisstring.h" #include "ibisnormalize.h" enum { PROP_0, PROP_ALT_NICK, PROP_CANCELLABLE, PROP_CAPABILITIES, PROP_CONNECTED, PROP_ERROR, PROP_FEATURES, PROP_NETWORK, PROP_NICK, PROP_REALNAME, PROP_USERNAME, PROP_HASL_CONTEXT, N_PROPERTIES, }; static GParamSpec *properties[N_PROPERTIES] = {NULL, }; enum { SIG_MESSAGE, SIG_WRITING_MESSAGE, SIG_WROTE_MESSAGE, N_SIGNALS, }; static guint signals[N_SIGNALS] = {0, }; struct _IbisClient { GObject parent; GCancellable *cancellable; IbisCapabilities *capabilities; gboolean connected; GError *error; IbisFeatures *features; GIOStream *stream; GDataInputStream *input; GOutputStream *output; char *network; char *nick; char *alt_nick; char *realname; char *username; /* This keeps track of whether or not the message tags capability has been * negotiated. */ gboolean message_tags_negotiated; /* The name of the server detected from the source of the CAP or * RPL_WELCOME messages. */ char *server_name; HaslContext *hasl_context; }; /****************************************************************************** * Helpers *****************************************************************************/ static void ibis_client_set_connected(IbisClient *client, gboolean connected) { g_return_if_fail(IBIS_IS_CLIENT(client)); if(client->connected != connected) { client->connected = connected; g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_CONNECTED]); } } static void ibis_client_set_error(IbisClient *client, GError *error) { g_return_if_fail(IBIS_IS_CLIENT(client)); if(client->error == NULL && error == NULL) { return; } if(error != NULL) { if(!g_error_matches(client->error, error->domain, error->code)) { /* If this is a different error, set it and notify. */ g_clear_error(&client->error); g_propagate_error(&client->error, error); g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ERROR]); } else { /* The error was the same, but this is transfer full, so we need * to remove the reference. */ g_error_free(error); } } else if(client->error != NULL) { /* We're clearing the air, so do that and notify. */ g_clear_error(&client->error); g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ERROR]); } } static void ibis_client_set_network(IbisClient *client, const char *network) { g_return_if_fail(IBIS_IS_CLIENT(client)); if(g_set_str(&client->network, network)) { g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_NETWORK]); } } static void ibis_client_register(IbisClient *client, const char *password) { IbisMessage *message = NULL; if(client->connected) { return; } if(!ibis_str_is_empty(password)) { message = ibis_message_new(IBIS_MSG_PASS); ibis_message_set_params(message, password, NULL); ibis_client_write(client, message); } message = ibis_message_new(IBIS_MSG_USER); ibis_message_set_params(message, ibis_client_get_username(client), "0", "*", ibis_client_get_realname(client), NULL); ibis_client_write(client, message); message = ibis_message_new(IBIS_MSG_NICK); ibis_message_set_params(message, client->nick, NULL); ibis_client_write(client, message); } /****************************************************************************** * Callbacks *****************************************************************************/ static void ibis_client_write_cb(GObject *source, GAsyncResult *result, gpointer data) { IbisMessage *message = data; IbisClient *client = g_object_get_data(G_OBJECT(message), "ibis-client"); BirbQueuedOutputStream *stream = BIRB_QUEUED_OUTPUT_STREAM(source); GError *error = NULL; gboolean success = FALSE; success = birb_queued_output_stream_push_bytes_finish(stream, result, &error); if(!success) { birb_queued_output_stream_clear_queue(stream); g_prefix_error(&error, "%s", _("Lost connection: ")); ibis_client_disconnect(client, error, NULL); } else { g_signal_emit(client, signals[SIG_WROTE_MESSAGE], ibis_message_get_command_quark(message), message); } g_clear_object(&message); } static void ibis_client_read_cb(GObject *source, GAsyncResult *result, gpointer data) { IbisClient *client = data; IbisMessage *message = NULL; GDataInputStream *input = G_DATA_INPUT_STREAM(source); GError *error = NULL; gchar *line = NULL; gsize length; line = g_data_input_stream_read_line_finish(input, result, &length, &error); if(line == NULL || error != NULL) { if(IBIS_IS_CLIENT(client)) { if(error == NULL) { g_set_error_literal(&error, IBIS_CLIENT_ERROR, 0, _("Server closed the connection")); } else { g_prefix_error(&error, "%s", _("Lost connection with server: ")); } ibis_client_disconnect(client, error, NULL); } /* In the off chance that line was returned, make sure we free it. */ g_free(line); return; } message = ibis_message_parse(line, &error); if(error != NULL) { g_warning("failed to handle '%s': %s", line, error != NULL ? error->message : "unknown error"); g_clear_error(&error); } else if(IBIS_IS_MESSAGE(message)) { GQuark quark = 0; const char *command = NULL; const char *source = NULL; gboolean handled = FALSE; /* We use the quark as we know it's been normalized. */ quark = ibis_message_get_command_quark(message); command = g_quark_to_string(quark); /* Grab the source. We save it if it's a CAP or RPL_WELCOME and use it * when the server doesn't provide a source. */ source = ibis_message_get_source(message); if(client->server_name == NULL && source != NULL) { if(ibis_str_equal(command, IBIS_MSG_CAP) || ibis_str_equal(command, IBIS_RPL_WELCOME)) { g_set_str(&client->server_name, source); } } if(!client->connected && ibis_str_equal(command, IBIS_RPL_WELCOME)) { ibis_client_set_connected(client, TRUE); } if(source == NULL) { ibis_message_set_source(message, client->server_name); } g_signal_emit(client, signals[SIG_MESSAGE], quark, command, message, &handled); if(!handled) { g_warning("unhandled message: '%s'", line); } } else { g_warning("parsing failed to generate a message or error for '%s'", line); } g_clear_pointer(&line, g_free); g_clear_object(&message); /* Call read_line_async again to continue reading lines. */ if(!ibis_client_get_error(client)) { g_data_input_stream_read_line_async(client->input, G_PRIORITY_DEFAULT, client->cancellable, ibis_client_read_cb, client); } } static void ibis_client_connect_cb(GObject *source, GAsyncResult *result, gpointer data) { IbisClient *client = data; GError *error = NULL; GSocketClient *socket_client = G_SOCKET_CLIENT(source); GSocketConnection *connection = NULL; connection = g_socket_client_connect_to_host_finish(socket_client, result, &error); if(error != NULL) { g_prefix_error_literal(&error, _("Unable to connect: ")); ibis_client_disconnect(client, error, NULL); } else { const char *password = NULL; password = g_object_get_data(G_OBJECT(client), "ibis-password"); ibis_client_start(client, G_IO_STREAM(connection), password, client->cancellable); } g_object_set_data(G_OBJECT(client), "ibis-password", NULL); g_clear_object(&socket_client); g_clear_object(&connection); } /****************************************************************************** * Default handlers *****************************************************************************/ static gboolean ibis_client_handle_ping(IbisClient *client, IbisMessage *message) { IbisMessage *pong = NULL; GStrv params = NULL; pong = ibis_message_new(IBIS_MSG_PONG); params = ibis_message_get_params(message); if(g_strv_length(params) >= 1) { ibis_message_set_params(pong, params[0], NULL); } ibis_client_write(client, pong); return TRUE; } static gboolean ibis_client_handle_rpl_isupport(IbisClient *client, IbisMessage *message) { ibis_features_parse(client->features, message); return TRUE; } static gboolean ibis_client_default_message_handler(IbisClient *client, const char *command, IbisMessage *message) { if(ibis_str_equal(command, IBIS_MSG_PING)) { return ibis_client_handle_ping(client, message); } else if(ibis_str_equal(command, IBIS_RPL_ISUPPORT)) { return ibis_client_handle_rpl_isupport(client, message); } return FALSE; } static void ibis_client_ack_message_tags_cb(G_GNUC_UNUSED IbisCapabilities *capabilities, G_GNUC_UNUSED const char *name, gpointer data) { IbisClient *client = data; client->message_tags_negotiated = TRUE; } static void ibis_client_network_feature_cb(IbisFeatures *features, const char *name, gpointer data) { IbisClient *client = data; const char *value = NULL; /* This is a detailed signal, so name should always be the value of * IBIS_FEATURE_NETWORK. */ value = ibis_features_get_string(features, name); ibis_client_set_network(client, value); } /****************************************************************************** * IbisClient default handlers *****************************************************************************/ static gboolean ibis_client_writing_message_default_handler(IbisClient *client, IbisMessage *message) { const char *command = NULL; command = ibis_message_get_command(message); if(ibis_str_equal(command, IBIS_MSG_TAGMSG)) { if(!client->message_tags_negotiated) { return TRUE; } } return FALSE; } /****************************************************************************** * GObject Implementation *****************************************************************************/ G_DEFINE_FINAL_TYPE(IbisClient, ibis_client, G_TYPE_OBJECT) static void ibis_client_finalize(GObject *obj) { IbisClient *client = IBIS_CLIENT(obj); if(client->connected) { ibis_client_disconnect(client, NULL, NULL); } g_clear_object(&client->cancellable); g_clear_object(&client->capabilities); g_clear_error(&client->error); g_clear_object(&client->features); g_clear_pointer(&client->network, g_free); g_clear_pointer(&client->alt_nick, g_free); g_clear_pointer(&client->nick, g_free); g_clear_pointer(&client->realname, g_free); g_clear_pointer(&client->username, g_free); g_clear_pointer(&client->server_name, g_free); G_OBJECT_CLASS(ibis_client_parent_class)->finalize(obj); } static void ibis_client_get_property(GObject *obj, guint param_id, GValue *value, GParamSpec *pspec) { IbisClient *client = IBIS_CLIENT(obj); switch(param_id) { case PROP_ALT_NICK: g_value_set_string(value, ibis_client_get_alt_nick(client)); break; case PROP_CANCELLABLE: g_value_set_object(value, ibis_client_get_cancellable(client)); break; case PROP_CAPABILITIES: g_value_set_object(value, ibis_client_get_capabilities(client)); break; case PROP_CONNECTED: g_value_set_boolean(value, ibis_client_get_connected(client)); break; case PROP_ERROR: g_value_set_boxed(value, ibis_client_get_error(client)); break; case PROP_FEATURES: g_value_set_object(value, ibis_client_get_features(client)); break; case PROP_NETWORK: g_value_set_string(value, ibis_client_get_network(client)); break; case PROP_NICK: g_value_set_string(value, ibis_client_get_nick(client)); break; case PROP_REALNAME: g_value_set_string(value, ibis_client_get_realname(client)); break; case PROP_USERNAME: g_value_set_string(value, ibis_client_get_username(client)); break; case PROP_HASL_CONTEXT: g_value_set_object(value, ibis_client_get_hasl_context(client)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); break; } } static void ibis_client_set_property(GObject *obj, guint param_id, const GValue *value, GParamSpec *pspec) { IbisClient *client = IBIS_CLIENT(obj); switch(param_id) { case PROP_ALT_NICK: ibis_client_set_alt_nick(client, g_value_get_string(value)); break; case PROP_NICK: ibis_client_set_nick(client, g_value_get_string(value)); break; case PROP_REALNAME: ibis_client_set_realname(client, g_value_get_string(value)); break; case PROP_USERNAME: ibis_client_set_username(client, g_value_get_string(value)); break; case PROP_HASL_CONTEXT: ibis_client_set_hasl_context(client, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); break; } } static void ibis_client_init(IbisClient *client) { client->message_tags_negotiated = FALSE; client->capabilities = ibis_capabilities_new(); g_signal_connect_object(client->capabilities, "ack::" IBIS_CAPABILITY_MESSAGE_TAGS, G_CALLBACK(ibis_client_ack_message_tags_cb), client, 0); client->features = ibis_features_new(); g_signal_connect_object(client->features, "changed::" IBIS_FEATURE_NETWORK, G_CALLBACK(ibis_client_network_feature_cb), client, 0); } static void ibis_client_class_init(IbisClientClass *klass) { GObjectClass *obj_class = G_OBJECT_CLASS(klass); obj_class->finalize = ibis_client_finalize; obj_class->get_property = ibis_client_get_property; obj_class->set_property = ibis_client_set_property; /** * IbisClient:alt-nick: * * The alternative nick to use if [property@Client:nick] is already in use. * * Since: 0.1 */ properties[PROP_ALT_NICK] = g_param_spec_string( "alt-nick", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisClient:cancellable: * * The [class@Gio.Cancellable] for the client. * * Since: 0.1 */ properties[PROP_CANCELLABLE] = g_param_spec_object( "cancellable", NULL, NULL, G_TYPE_CANCELLABLE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * IbisClient:capabilities: * * The [class@Capabilities] that this client is using. * * Since: 0.1 */ properties[PROP_CAPABILITIES] = g_param_spec_object( "capabilities", NULL, NULL, IBIS_TYPE_CAPABILITIES, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * IbisClient:connected: * * Set to whether or not the client is currently connected. * * Since: 0.1 */ properties[PROP_CONNECTED] = g_param_spec_boolean( "connected", NULL, NULL, FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * IbisClient:error: * * A #GError that the client encountered. * * Since: 0.1 */ properties[PROP_ERROR] = g_param_spec_boxed( "error", NULL, NULL, G_TYPE_ERROR, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * IbisClient:features: * * The [class@Features] for this connection. * * This property will be %NULL if the client is not currently connected. * * Since: 0.4 */ properties[PROP_FEATURES] = g_param_spec_object( "features", NULL, NULL, IBIS_TYPE_FEATURES, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * IbisClient:network: * * The advertised name of the network from the server sending the * [const@RPL_ISUPPORT] message. * * Since: 0.4 */ properties[PROP_NETWORK] = g_param_spec_string( "network", NULL, NULL, NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * IbisClient:nick: * * The primary nick to use. * * If the server tells us this nick is in use, an attempt to use * [property@Client:alt-nick] will be made. * * Since: 0.1 */ properties[PROP_NICK] = g_param_spec_string( "nick", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisClient:realname: * * The realname to use when sending the USER command. * * If this is %NULL, the value of [property@Client:nick] will be returned. * * Since: 0.1 */ properties[PROP_REALNAME] = g_param_spec_string( "realname", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisClient:username: * * The username to use when sending the USER command. * * If this is %NULL, the value of [property@Client:nick] will be returned. * * Since: 0.1 */ properties[PROP_USERNAME] = g_param_spec_string( "username", NULL, NULL, NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); /** * IbisClient:hasl-context: * * The [class@Hasl.Context] to use during SASL negotiation. * * If this is %NULL, SASL negotiation will not be attempted. * * Since: 0.1 */ properties[PROP_HASL_CONTEXT] = g_param_spec_object( "hasl-context", NULL, NULL, HASL_TYPE_CONTEXT, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(obj_class, N_PROPERTIES, properties); /** * IbisClient::message: * @client: The instance. * @command: The command. * @message: The message. * * Emitted when the client has received a new message. * * This signal supports *details* based on the upper case version of the * command from the message. For example, `PRIVMSG`, `NOTICE`, etc. * * Returns: %TRUE to stop other handlers from being invoked, otherwise * %FALSE to propagate further. * * Since: 0.1 */ signals[SIG_MESSAGE] = g_signal_new_class_handler( "message", G_OBJECT_CLASS_TYPE(klass), G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST, G_CALLBACK(ibis_client_default_message_handler), g_signal_accumulator_true_handled, NULL, NULL, G_TYPE_BOOLEAN, 2, G_TYPE_STRING, IBIS_TYPE_MESSAGE); /** * IbisClient::writing-message: * @client: The instance. * @message: The message. * * Emitted just before the client has queued @message to be written to the * output stream to allow dropping or modification of messages. * * This signal supports *details* based on the normalized uppercase version * of the command from the message. For example, `PRIVMSG`, `NOTICE`, etc. * * Returns: %TRUE to stop the message from being written, or %FALSE to * continue as normal. * * Since: 0.4 */ signals[SIG_WRITING_MESSAGE] = g_signal_new_class_handler( "writing-message", G_OBJECT_CLASS_TYPE(klass), G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST, G_CALLBACK(ibis_client_writing_message_default_handler), g_signal_accumulator_true_handled, NULL, NULL, G_TYPE_BOOLEAN, 1, IBIS_TYPE_MESSAGE); /** * IbisClient::wrote-message: * @client: The instance. * @message: The message. * * Emitted when the client has successfully written @message to output * stream. * * This signal supports *details* based on the normalized uppercase version * of the command from the message. For example, `PRIVMSG`, `NOTICE`, etc. * * Since: 0.4 */ signals[SIG_WROTE_MESSAGE] = g_signal_new_class_handler( "wrote-message", G_OBJECT_CLASS_TYPE(klass), G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST, NULL, NULL, NULL, NULL, G_TYPE_NONE, 1, IBIS_TYPE_MESSAGE); } /****************************************************************************** * Public API *****************************************************************************/ void ibis_client_connect(IbisClient *client, const char *hostname, guint16 port, const char *password, gboolean tls, GCancellable *cancellable, GProxyResolver *proxy_resolver) { GSocketClient *socket_client = NULL; g_return_if_fail(IBIS_IS_CLIENT(client)); g_return_if_fail(!ibis_str_is_empty(hostname)); socket_client = g_socket_client_new(); g_socket_client_set_proxy_resolver(socket_client, proxy_resolver); g_socket_client_set_tls(socket_client, tls); if(!ibis_str_is_empty(password)) { g_object_set_data_full(G_OBJECT(client), "ibis-password", g_strdup(password), g_free); } g_set_object(&client->cancellable, cancellable); g_socket_client_connect_to_host_async(socket_client, hostname, port, client->cancellable, ibis_client_connect_cb, client); } void ibis_client_disconnect(IbisClient *client, GError *error, const char *message) { GError *close_error = NULL; g_return_if_fail(IBIS_IS_CLIENT(client)); /* Disable the capabilities handler. */ ibis_capabilities_stop(client->capabilities); if(error == NULL && client->connected) { IbisMessage *msg = NULL; msg = ibis_message_new(IBIS_MSG_QUIT); if(!ibis_str_is_empty(message)) { ibis_message_set_params(msg, message, NULL); } ibis_client_write(client, msg); } /* We set multiple properties while we're disconnecting but consumers * shouldn't know about any of them changing until we've finished * disconnecting. */ g_object_freeze_notify(G_OBJECT(client)); ibis_client_set_error(client, error); if(G_IS_IO_STREAM(client->stream)) { if(!g_io_stream_is_closed(client->stream)) { gboolean success = FALSE; /* If closing the stream fails, the stream still gets closed, but * we can update the error message if it wasn't already set. */ success = g_io_stream_close(client->stream, client->cancellable, &close_error); if(!success) { if(close_error != NULL) { g_warning("failed to close stream: %s", close_error->message); } else { g_warning("failed to close stream: unknown error"); } if(client->error == NULL) { ibis_client_set_error(client, close_error); } g_clear_error(&close_error); } } } ibis_client_set_connected(client, FALSE); ibis_features_clear(client->features); g_cancellable_cancel(client->cancellable); g_clear_object(&client->cancellable); g_clear_object(&client->input); g_clear_object(&client->output); g_clear_pointer(&client->server_name, g_free); g_object_thaw_notify(G_OBJECT(client)); } const char * ibis_client_get_alt_nick(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); return client->alt_nick; } void ibis_client_set_alt_nick(IbisClient *client, const char *alt_nick) { g_return_if_fail(IBIS_IS_CLIENT(client)); g_return_if_fail(alt_nick == NULL || alt_nick[0] != '\0'); if(g_set_str(&client->alt_nick, alt_nick)) { g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_ALT_NICK]); } } GCancellable * ibis_client_get_cancellable(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); return client->cancellable; } IbisCapabilities * ibis_client_get_capabilities(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); return client->capabilities; } gboolean ibis_client_get_connected(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), FALSE); return client->connected; } GError * ibis_client_get_error(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); return client->error; } IbisFeatures * ibis_client_get_features(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); return client->features; } HaslContext * ibis_client_get_hasl_context(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); return client->hasl_context; } void ibis_client_set_hasl_context(IbisClient *client, HaslContext *hasl_context) { g_return_if_fail(IBIS_IS_CLIENT(client)); if(g_set_object(&client->hasl_context, hasl_context)) { g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_HASL_CONTEXT]); } } const char * ibis_client_get_network(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); return client->network; } const char * ibis_client_get_nick(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); return client->nick; } void ibis_client_set_nick(IbisClient *client, const char *nick) { g_return_if_fail(IBIS_IS_CLIENT(client)); g_return_if_fail(!ibis_str_is_empty(nick)); if(g_set_str(&client->nick, nick)) { g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_NICK]); } } const char * ibis_client_get_realname(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); if(client->realname != NULL) { return client->realname; } return client->nick; } void ibis_client_set_realname(IbisClient *client, const char *realname) { g_return_if_fail(IBIS_IS_CLIENT(client)); g_return_if_fail(realname == NULL || realname[0] != '\0'); if(g_set_str(&client->realname, realname)) { g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_REALNAME]); } } char * ibis_client_get_source_prefix(IbisClient *client, const char *source) { const char *prefixes = NULL; g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); g_return_val_if_fail(!ibis_str_is_empty(source), NULL); prefixes = ibis_features_get_prefix_prefixes(client->features); if(prefixes != NULL) { return ibis_source_get_prefix(source, prefixes); } return NULL; } const char * ibis_client_get_username(IbisClient *client) { g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); if(client->username != NULL) { return client->username; } return client->nick; } void ibis_client_set_username(IbisClient *client, const char *username) { g_return_if_fail(IBIS_IS_CLIENT(client)); g_return_if_fail(username == NULL || username[0] != '\0'); if(g_set_str(&client->username, username)) { g_object_notify_by_pspec(G_OBJECT(client), properties[PROP_USERNAME]); } } gboolean ibis_client_is_channel(IbisClient *client, const char *target) { const char *chantypes = NULL; g_return_val_if_fail(IBIS_IS_CLIENT(client), FALSE); g_return_val_if_fail(!ibis_str_is_empty(target), FALSE); chantypes = ibis_features_get_chantypes(client->features); if(ibis_str_is_empty(chantypes)) { return FALSE; } for(int i = 0; chantypes[i] != '\0'; i++) { if(chantypes[i] == target[0]) { return TRUE; } } return FALSE; } IbisClient * ibis_client_new(void) { return g_object_new(IBIS_TYPE_CLIENT, NULL); } char * ibis_client_normalize(IbisClient *client, const char *input) { const char *casemapping = NULL; g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); casemapping = ibis_features_get_casemapping(client->features); if(ibis_str_equal(casemapping, "ascii")) { return ibis_normalize_ascii(input); } else if(ibis_str_equal(casemapping, "rfc1459")) { return ibis_normalize_rfc1459(input); } else if(ibis_str_equal(casemapping, "rfc1459-strict")) { return ibis_normalize_rfc1459_strict(input); } return g_strdup(input); } void ibis_client_start(IbisClient *client, GIOStream *stream, const char *password, GCancellable *cancellable) { GInputStream *input = NULL; GOutputStream *output = NULL; g_return_if_fail(IBIS_IS_CLIENT(client)); g_return_if_fail(G_IS_IO_STREAM(stream)); g_return_if_fail(client->connected == FALSE); g_set_object(&client->cancellable, cancellable); if(!G_IS_CANCELLABLE(client->cancellable)) { client->cancellable = g_cancellable_new(); } /* Clear the features object. */ ibis_features_clear(client->features); /* Clear any previous error. */ ibis_client_set_error(client, NULL); /* Store the stream for later use in disconnect. */ client->stream = g_object_ref(stream); /* Wrap the output stream in a Birb QueuedOutputStream. */ output = g_io_stream_get_output_stream(stream); client->output = birb_queued_output_stream_new(output); /* Setup the input stream. */ input = g_io_stream_get_input_stream(G_IO_STREAM(stream)); client->input = g_data_input_stream_new(input); g_data_input_stream_set_newline_type(G_DATA_INPUT_STREAM(client->input), G_DATA_STREAM_NEWLINE_TYPE_CR_LF); /* Start chunking the input. */ g_data_input_stream_read_line_async(client->input, G_PRIORITY_DEFAULT, client->cancellable, ibis_client_read_cb, client); ibis_capabilities_start(client->capabilities, client); ibis_client_register(client, password); } char * ibis_client_strip_source_prefix(IbisClient *client, const char *source) { const char *prefixes = NULL; g_return_val_if_fail(IBIS_IS_CLIENT(client), NULL); g_return_val_if_fail(!ibis_str_is_empty(source), NULL); prefixes = ibis_features_get_prefix_prefixes(client->features); if(prefixes != NULL) { return ibis_source_strip_prefix(source, prefixes); } return g_strdup(source); } void ibis_client_write(IbisClient *client, IbisMessage *message) { GBytes *bytes = NULL; gboolean cancelled = FALSE; g_return_if_fail(IBIS_IS_CLIENT(client)); g_return_if_fail(IBIS_IS_MESSAGE(message)); g_signal_emit(client, signals[SIG_WRITING_MESSAGE], ibis_message_get_command_quark(message), message, &cancelled); if(cancelled) { g_clear_object(&message); return; } bytes = ibis_message_serialize(message, client->message_tags_negotiated); g_object_set_data(G_OBJECT(message), "ibis-client", client); birb_queued_output_stream_push_bytes_async(BIRB_QUEUED_OUTPUT_STREAM(client->output), bytes, G_PRIORITY_DEFAULT, client->cancellable, ibis_client_write_cb, message); g_bytes_unref(bytes); }