# HG changeset patch
# Node ID 8826a3c84cfb94238fcbdccddf5da2040c2ae870
# Parent  f7faa3b4a9e511abca3df1174bf7daeeeff1f76d
diff --git a/ChangeLog b/ChangeLog
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,4 @@
-0.9.1: ????-??-??
+0.10.0: ????-??-??
   * Nothing yet, be the first!
 
 0.9.0: 2024-10-15
diff --git a/ibis/ibisclient.c b/ibis/ibisclient.c
--- a/ibis/ibisclient.c
+++ b/ibis/ibisclient.c
@@ -42,6 +42,7 @@
 	PROP_NETWORK,
 	PROP_NICK,
 	PROP_REALNAME,
+	PROP_REGISTERED,
 	PROP_USERNAME,
 	PROP_HASL_CONTEXT,
 	N_PROPERTIES,
@@ -62,6 +63,7 @@
 	GCancellable *cancellable;
 	IbisCapabilities *capabilities;
 	gboolean connected;
+	gboolean registered;
 	GError *error;
 	IbisFeatures *features;
 
@@ -103,6 +105,18 @@
 }
 
 static void
+ibis_client_set_registered(IbisClient *client, gboolean registered) {
+	g_return_if_fail(IBIS_IS_CLIENT(client));
+
+	if(client->registered != registered) {
+		client->registered = registered;
+
+		g_object_notify_by_pspec(G_OBJECT(client),
+		                         properties[PROP_REGISTERED]);
+	}
+}
+
+static void
 ibis_client_set_error(IbisClient *client, GError *error) {
 	g_return_if_fail(IBIS_IS_CLIENT(client));
 
@@ -181,9 +195,12 @@
 	if(!success) {
 		birb_queued_output_stream_clear_queue(stream);
 
-		g_prefix_error(&error, "%s", _("Lost connection: "));
+		/* Ignore cancellations as they get set in _stop. */
+		if(!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+			g_prefix_error(&error, "%s", _("Lost connection: "));
 
-		ibis_client_disconnect(client, error, NULL);
+			ibis_client_stop(client, error);
+		}
 	} else {
 		g_signal_emit(client, signals[SIG_WROTE_MESSAGE],
 		              ibis_message_get_command_quark(message), message);
@@ -212,7 +229,7 @@
 				g_prefix_error(&error, "%s", _("Lost connection with server: "));
 			}
 
-			ibis_client_disconnect(client, error, NULL);
+			ibis_client_stop(client, error);
 		}
 
 		/* In the off chance that line was returned, make sure we free it. */
@@ -251,7 +268,7 @@
 		}
 
 		if(!client->connected && ibis_str_equal(command, IBIS_RPL_WELCOME)) {
-			ibis_client_set_connected(client, TRUE);
+			ibis_client_set_registered(client, TRUE);
 		}
 
 		if(source == NULL) {
@@ -293,7 +310,7 @@
 	                                                    &error);
 	if(error != NULL) {
 		g_prefix_error_literal(&error, _("Unable to connect: "));
-		ibis_client_disconnect(client, error, NULL);
+		ibis_client_stop(client, error);
 	} else {
 		const char *password = NULL;
 
@@ -408,6 +425,7 @@
 	g_clear_object(&client->capabilities);
 	g_clear_error(&client->error);
 	g_clear_object(&client->features);
+	g_clear_object(&client->hasl_context);
 
 	g_clear_pointer(&client->network, g_free);
 	g_clear_pointer(&client->alt_nick, g_free);
@@ -454,6 +472,9 @@
 	case PROP_REALNAME:
 		g_value_set_string(value, ibis_client_get_realname(client));
 		break;
+	case PROP_REGISTERED:
+		g_value_set_boolean(value, ibis_client_get_registered(client));
+		break;
 	case PROP_USERNAME:
 		g_value_set_string(value, ibis_client_get_username(client));
 		break;
@@ -559,6 +580,10 @@
 	 *
 	 * Set to whether or not the client is currently connected.
 	 *
+	 * Before 0.10 this was set to %TRUE after registration. In 0.10
+	 * [property@Client:registered] was added to keep that behavior as this is
+	 * now set to %TRUE in [method@Client.start].
+	 *
 	 * Since: 0.1
 	 */
 	properties[PROP_CONNECTED] = g_param_spec_boolean(
@@ -635,6 +660,21 @@
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
 	/**
+	 * IbisClient:registered:
+	 *
+	 * Whether or not user registration has been completed.
+	 *
+	 * User registration must be completed at the beginning of a connection and
+	 * no other commands may be sent while it is occurring.
+	 *
+	 * Since: 0.10
+	 */
+	properties[PROP_REGISTERED] = g_param_spec_boolean(
+		"registered", NULL, NULL,
+		FALSE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
 	 * IbisClient:username:
 	 *
 	 * The username to use when sending the USER command.
@@ -778,73 +818,21 @@
 }
 
 void
-ibis_client_disconnect(IbisClient *client, GError *error, const char *message)
-{
-	GError *close_error = NULL;
-
+ibis_client_disconnect(IbisClient *client, GError *error, const char *reason) {
 	g_return_if_fail(IBIS_IS_CLIENT(client));
 
-	/* Disable the capabilities handler. */
-	ibis_capabilities_stop(client->capabilities);
+	if(client->connected) {
+		IbisMessage *message = NULL;
 
-	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);
+		message = ibis_message_new(IBIS_MSG_QUIT);
+		if(!ibis_str_is_empty(reason)) {
+			ibis_message_set_params(message, reason, NULL);
 		}
 
-		ibis_client_write(client, msg);
+		ibis_client_write(client, message);
 	}
 
-	/* 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));
+	ibis_client_stop(client, error);
 }
 
 const char *
@@ -951,6 +939,13 @@
 	return client->nick;
 }
 
+gboolean
+ibis_client_get_registered(IbisClient *client) {
+	g_return_val_if_fail(IBIS_IS_CLIENT(client), FALSE);
+
+	return client->registered;
+}
+
 void
 ibis_client_set_realname(IbisClient *client, const char *realname) {
 	g_return_if_fail(IBIS_IS_CLIENT(client));
@@ -1077,6 +1072,8 @@
 	g_data_input_stream_set_newline_type(G_DATA_INPUT_STREAM(client->input),
 	                                     G_DATA_STREAM_NEWLINE_TYPE_CR_LF);
 
+	ibis_client_set_connected(client, TRUE);
+
 	/* Start chunking the input. */
 	g_data_input_stream_read_line_async(client->input,
 	                                    G_PRIORITY_DEFAULT,
@@ -1088,6 +1085,65 @@
 	ibis_client_register(client, password);
 }
 
+void
+ibis_client_stop(IbisClient *client, GError *error) {
+	g_return_if_fail(IBIS_IS_CLIENT(client));
+
+	/* 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));
+
+	/* Disable the capabilities handler. */
+	ibis_capabilities_stop(client->capabilities);
+
+	ibis_client_set_error(client, error);
+
+	if(G_IS_IO_STREAM(client->stream)) {
+		if(!g_io_stream_is_closed(client->stream)) {
+			GError *close_error = NULL;
+			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) {
+					g_warning("client error was null, setting '%s'", close_error->message);
+					ibis_client_set_error(client, close_error);
+				}
+
+				g_clear_error(&close_error);
+			}
+		}
+	}
+
+	ibis_client_set_connected(client, FALSE);
+	ibis_client_set_registered(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));
+}
+
 char *
 ibis_client_strip_source_prefix(IbisClient *client, const char *source) {
 	const char *prefixes = NULL;
diff --git a/ibis/ibisclient.h b/ibis/ibisclient.h
--- a/ibis/ibisclient.h
+++ b/ibis/ibisclient.h
@@ -89,16 +89,19 @@
  * ibis_client_disconnect:
  * @client: The instance.
  * @error: (transfer full) (nullable): A #GError.
- * @message: (nullable): An option disconnection message.
+ * @reason: (nullable): An optional disconnect reason.
  *
  * Disconnects @client.
  *
+ * This will send [const@MSG_QUIT] with @message and then call
+ * [method@Client.stop].
+ *
  * If @error is not %NULL, it will be assigned to [property@Client:error].
  *
  * Since: 0.1
  */
 IBIS_AVAILABLE_IN_0_1
-void ibis_client_disconnect(IbisClient *client, GError *error, const char *message);
+void ibis_client_disconnect(IbisClient *client, GError *error, const char *reason);
 
 /**
  * ibis_client_get_alt_nick:
@@ -289,6 +292,23 @@
 void ibis_client_set_realname(IbisClient *client, const char *realname);
 
 /**
+ * ibis_client_get_registered:
+ * @client: The instance.
+ *
+ * Gets whether or not the client has completed user registration.
+ *
+ * User registration is the first thing that happens while connecting and other
+ * commands like JOIN, PRIVMSG, etc can not be sent until registration is
+ * completed.
+ *
+ * Returns: %TRUE if registration has been completed, otherwise %FALSE.
+ *
+ * Since: 0.10
+ */
+IBIS_AVAILABLE_IN_0_10
+gboolean ibis_client_get_registered(IbisClient *client);
+
+/**
  * ibis_client_get_source_prefix:
  * @client: The instance.
  * @source: The source.
@@ -396,6 +416,23 @@
 void ibis_client_start(IbisClient *client, GIOStream *stream, const char *password, GCancellable *cancellable);
 
 /**
+ * ibis_client_stop:
+ * @client: The instance.
+ * @error: (transfer full) (nullable): A #GError.
+ *
+ * Stops @client.
+ *
+ * This will close all of the streams and reset everything to its pre-connected
+ * state.
+ *
+ * If @error is not %NULL, it will be assigned to [property@Client:error].
+ *
+ * Since: 0.10
+ */
+IBIS_AVAILABLE_IN_0_10
+void ibis_client_stop(IbisClient *client, GError *error);
+
+/**
  * ibis_client_strip_source_prefix:
  * @client: The instance.
  * @source: The source.
diff --git a/ibis/ibisversion.h b/ibis/ibisversion.h
--- a/ibis/ibisversion.h
+++ b/ibis/ibisversion.h
@@ -317,7 +317,6 @@
 #define IBIS_AVAILABLE_TYPE_IN_0_8
 #endif
 
-
 /**
  * IBIS_VERSION_0_9:
  *
@@ -342,6 +341,30 @@
 #define IBIS_AVAILABLE_TYPE_IN_0_9
 #endif
 
+/**
+ * IBIS_VERSION_0_10:
+ *
+ * A macro that evaluates to the 0.10 version of ibis, in a format that can be
+ * used by the C pre-processor.
+ *
+ * Since: 0.10
+ */
+#define IBIS_VERSION_0_10 (G_ENCODE_VERSION(0, 10))
+
+#if IBIS_VERSION_MAX_ALLOWED < IBIS_VERSION_0_10
+#define IBIS_AVAILABLE_IN_0_10 IBIS_UNAVAILABLE(0, 10)
+#define IBIS_AVAILABLE_STATIC_INLINE_IN_0_10 IBIS_UNAVAILABLE_STATIC_INLINE(0, 10)
+#define IBIS_AVAILABLE_MACRO_IN_0_10 IBIS_UNAVAILABLE_MACRO(0, 10)
+#define IBIS_AVAILABLE_ENUMERATOR_IN_0_10 IBIS_UNAVAILABLE_ENUMERATOR(0, 10)
+#define IBIS_AVAILABLE_TYPE_IN_0_10 IBIS_UNAVAILABLE_TYPE(0, 10)
+#else
+#define IBIS_AVAILABLE_IN_0_10 _IBIS_EXTERN
+#define IBIS_AVAILABLE_STATIC_INLINE_IN_0_10
+#define IBIS_AVAILABLE_MACRO_IN_0_10
+#define IBIS_AVAILABLE_ENUMERATOR_IN_0_10
+#define IBIS_AVAILABLE_TYPE_IN_0_10
+#endif
+
 G_BEGIN_DECLS
 
 /**
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -2,7 +2,7 @@
   default_options : ['c_std=c17', 'warning_level=2'],
   license : 'LGPL-2.1-or-later',
   meson_version : '>=1.0.0',
-  version : '0.9.1-dev')
+  version : '0.10.0-dev')
 
 toplevel_inc = include_directories('.')
 
