Commit 5750f7c608 for asterisk.org

commit 5750f7c60869715f080ac4ad2d72b8c74c7e1a48
Author: Joshua C. Colp <jcolp@sangoma.com>
Date:   Fri Sep 25 11:06:25 2026 -0300

    extstate/pjsip: Handle lack of or late loading of presence state.

    The extension state implementation is stateful resulting in state
    not being updated if a presence state provider does not publish an
    update after loading. To handle providers which don't do this we
    query for updated state if we are asked for it and the existing
    state is invalid. This ensures that if the presence state provider
    is loaded later we will get the updated state when asked.

    The func_presencestate module is written to publish the persisted
    custom presence states at load time. This failed for entries with
    a not_set state as this resulted in querying the presence provider
    that was not yet registered. The provider is now registered before
    this ensuring the update can be published.

    This change also fixes an issue in PJSIP where a SUBSCRIBE for a
    hint that had an invalid presence state configuration would not
    send an initial NOTIFY. We now treat an invalid presence state as
    not set instead.

    Fixes: #2179

diff --git a/funcs/func_presencestate.c b/funcs/func_presencestate.c
index 7fd69a0971..fe3ae0a2ed 100644
--- a/funcs/func_presencestate.c
+++ b/funcs/func_presencestate.c
@@ -843,6 +843,12 @@ static int load_module(void)
 	int res = 0;
 	struct ast_db_entry *db_entry, *db_tree;

+	/* The CustomPresence provider must be registered before populating the
+	 * cache as entries with a not_set state result in the provider being queried
+	 * for the state (which will be not_set anyway in this case).
+	 */
+	res |= ast_presence_state_prov_add("CustomPresence", custom_presence_callback);
+
 	/* Populate the presence state cache on the system with all of the currently
 	 * known custom presence states. */
 	db_entry = db_tree = ast_db_gettree(astdb_family, NULL);
@@ -863,7 +869,6 @@ static int load_module(void)
 	db_tree = NULL;

 	res |= ast_custom_function_register(&presence_function);
-	res |= ast_presence_state_prov_add("CustomPresence", custom_presence_callback);
 	res |= ast_cli_register_multiple(cli_funcpresencestate, ARRAY_LEN(cli_funcpresencestate));
 #ifdef TEST_FRAMEWORK
 	AST_TEST_REGISTER(test_valid_parse_data);
diff --git a/main/extension_state.c b/main/extension_state.c
index ebe54298bc..e5e89affdd 100644
--- a/main/extension_state.c
+++ b/main/extension_state.c
@@ -1333,6 +1333,65 @@ struct ast_extension_state_device_snapshot *ast_extension_state_get_latest_devic
 	return device_snapshot;
 }

+/*!
+ * \internal
+ * \brief Refresh the presence snapshot of an extension state if it is invalid
+ *
+ * \param state The extension state to refresh
+ *
+ * \pre The extension state must be locked by the caller before calling this function.
+ *
+ * This function checks whether the cached presence snapshot of the extension state is invalid
+ * and if so attempts to retrieve the latest presence state. If a valid presence state is
+ * retrieved then the snapshot is updated and an update message is published to any subscribers.
+ */
+static void extension_state_presence_snapshot_refresh(struct extension_state *state)
+{
+	enum ast_presence_state presence_state_new;
+	char *presence_subtype;
+	char *presence_message;
+
+	/*
+	 * If the cached presence state is invalid it may be because the presence state provider
+	 * was unable to be reached when it was last queried, or because the resulting update was
+	 * ignored. Since a caller is explicitly requesting the presence state we make another
+	 * attempt to retrieve it and if it has changed update it and notify any subscribers.
+	 */
+	if (state->presence_snapshot->presence_state != AST_PRESENCE_INVALID || !state->presence_sources_string) {
+		return;
+	}
+
+	presence_state_new = ast_presence_state(state->presence_sources_string, &presence_subtype, &presence_message);
+	if (presence_state_new != AST_PRESENCE_INVALID) {
+		struct ast_extension_state_presence_snapshot *new_presence_snapshot;
+
+		new_presence_snapshot = extension_state_presence_snapshot_create(presence_state_new, presence_subtype,
+			presence_message);
+		if (new_presence_snapshot) {
+			struct ast_extension_state_update_message *update_message;
+
+			update_message = extension_state_update_message_create(state->dialplan_context, state->dialplan_extension,
+				state->device_snapshot, state->device_snapshot, state->presence_snapshot, new_presence_snapshot);
+			ao2_replace(state->presence_snapshot, new_presence_snapshot);
+			ao2_ref(new_presence_snapshot, -1);
+
+			if (update_message) {
+				struct stasis_message *message = stasis_message_create(ast_extension_state_update_message_type(), update_message);
+
+				if (message) {
+					stasis_publish(state->extension_state_topic, message);
+					ao2_ref(message, -1);
+				}
+
+				ao2_ref(update_message, -1);
+			}
+		}
+	}
+
+	ast_free(presence_subtype);
+	ast_free(presence_message);
+}
+
 struct ast_extension_state_presence_snapshot *ast_extension_state_get_latest_presence_snapshot(struct ast_channel *chan,
 	const char *exten, const char *context)
 {
@@ -1345,6 +1404,7 @@ struct ast_extension_state_presence_snapshot *ast_extension_state_get_latest_pre
 	}

 	ao2_lock(state);
+	extension_state_presence_snapshot_refresh(state);
 	presence_snapshot = ao2_bump(state->presence_snapshot);
 	ao2_unlock(state);
 	ao2_ref(state, -1);
diff --git a/res/res_pjsip_exten_state.c b/res/res_pjsip_exten_state.c
index 374730bc82..9b555c9a72 100644
--- a/res/res_pjsip_exten_state.c
+++ b/res/res_pjsip_exten_state.c
@@ -526,10 +526,18 @@ static struct ast_sip_exten_state_data *exten_state_data_alloc(struct ast_sip_su

 	exten_state_data->exten = exten_state_sub->exten;
 	presence_state = ast_hint_presence_state(NULL, exten_state_sub->context, exten_state_sub->exten, &subtype, &message);
-	if (presence_state  == -1 || presence_state == AST_PRESENCE_INVALID) {
+	if (presence_state == -1) {
 		ao2_cleanup(exten_state_data);
 		return NULL;
 	}
+	/* If the presence state is invalid default it to not set so that the initial NOTIFY is still sent */
+	if (presence_state == AST_PRESENCE_INVALID) {
+		presence_state = AST_PRESENCE_NOT_SET;
+		ast_free(subtype);
+		ast_free(message);
+		subtype = NULL;
+		message = NULL;
+	}
 	exten_state_data->presence_state = presence_state;
 	exten_state_data->presence_subtype = subtype;
 	exten_state_data->presence_message = message;