libpurple/purplescheduledtask.c

changeset 43292
03fe500d5aa5
child 43304
2599d35e9750
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/purplescheduledtask.c	Thu Jul 24 23:33:18 2025 -0500
@@ -0,0 +1,617 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Purple 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 <birb/birb.h>
+
+#include "purplescheduledtask.h"
+
+#include "purpleenums.h"
+
+struct _PurpleScheduledTask {
+	GObject parent;
+
+	gboolean cancellable;
+	GDateTime *execute_at;
+	char *id;
+	gboolean persistent;
+	guint source_id;
+	PurpleScheduledTaskState state;
+	char *subtitle;
+	PurpleTags *tags;
+	char *task_type;
+	char *title;
+};
+
+enum {
+	PROP_0,
+	PROP_CANCELLABLE,
+	PROP_EXECUTE_AT,
+	PROP_ID,
+	PROP_PERSISTENT,
+	PROP_STATE,
+	PROP_SUBTITLE,
+	PROP_TAGS,
+	PROP_TASK_TYPE,
+	PROP_TITLE,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+enum {
+	SIG_EXECUTE,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {0, };
+
+G_DEFINE_QUARK(purple-scheduled-task-error, purple_scheduled_task_error)
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_scheduled_task_set_cancellable(PurpleScheduledTask *task,
+                                      gboolean cancellable)
+{
+	g_return_if_fail(PURPLE_IS_SCHEDULED_TASK(task));
+
+	if(task->cancellable != cancellable) {
+		task->cancellable = cancellable;
+
+		g_object_notify_by_pspec(G_OBJECT(task), properties[PROP_CANCELLABLE]);
+	}
+}
+
+static void
+purple_scheduled_task_set_id(PurpleScheduledTask *task, const char *id) {
+	g_return_if_fail(PURPLE_IS_SCHEDULED_TASK(task));
+
+	if(g_set_str(&task->id, id)) {
+		g_object_notify_by_pspec(G_OBJECT(task), properties[PROP_ID]);
+	}
+}
+
+static void
+purple_scheduled_task_set_task_type(PurpleScheduledTask *task,
+                                    const char *task_type)
+{
+	g_return_if_fail(PURPLE_IS_SCHEDULED_TASK(task));
+
+	if(g_set_str(&task->task_type, task_type)) {
+		g_object_notify_by_pspec(G_OBJECT(task), properties[PROP_TASK_TYPE]);
+	}
+}
+
+static void
+purple_scheduled_task_set_title(PurpleScheduledTask *task,
+                                const char *title)
+{
+	g_return_if_fail(PURPLE_IS_SCHEDULED_TASK(task));
+
+	if(g_set_str(&task->title, title)) {
+		g_object_notify_by_pspec(G_OBJECT(task), properties[PROP_TITLE]);
+	}
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static gboolean
+purple_scheduled_task_timeout_cb(gpointer data) {
+	PurpleScheduledTask *task = data;
+
+	task->state = PURPLE_SCHEDULED_TASK_STATE_EXECUTING;
+	g_object_notify_by_pspec(G_OBJECT(task), properties[PROP_STATE]);
+
+	g_signal_emit(data, signals[SIG_EXECUTE], 0);
+
+	task->state = PURPLE_SCHEDULED_TASK_STATE_EXECUTED;
+	g_object_notify_by_pspec(G_OBJECT(task), properties[PROP_STATE]);
+
+	task->source_id = 0;
+
+	return G_SOURCE_REMOVE;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE(PurpleScheduledTask, purple_scheduled_task, G_TYPE_OBJECT)
+
+static void
+purple_scheduled_task_dispose(GObject *obj) {
+	PurpleScheduledTask *task = PURPLE_SCHEDULED_TASK(obj);
+
+	g_clear_handle_id(&task->source_id, g_source_remove);
+
+	G_OBJECT_CLASS(purple_scheduled_task_parent_class)->dispose(obj);
+}
+
+static void
+purple_scheduled_task_finalize(GObject *obj) {
+	PurpleScheduledTask *task = PURPLE_SCHEDULED_TASK(obj);
+
+	g_clear_pointer(&task->execute_at, g_date_time_unref);
+	g_clear_pointer(&task->id, g_free);
+	g_clear_pointer(&task->subtitle, g_free);
+	g_clear_object(&task->tags);
+	g_clear_pointer(&task->task_type, g_free);
+	g_clear_pointer(&task->title, g_free);
+
+	G_OBJECT_CLASS(purple_scheduled_task_parent_class)->finalize(obj);
+}
+
+static void
+purple_scheduled_task_get_property(GObject *obj, guint param_id, GValue *value,
+                                   GParamSpec *pspec)
+{
+	PurpleScheduledTask *task = PURPLE_SCHEDULED_TASK(obj);
+
+	switch(param_id) {
+	case PROP_CANCELLABLE:
+		g_value_set_boolean(value,
+		                    purple_scheduled_task_get_cancellable(task));
+		break;
+	case PROP_EXECUTE_AT:
+		g_value_set_boxed(value, purple_scheduled_task_get_execute_at(task));
+		break;
+	case PROP_ID:
+		g_value_set_string(value, purple_scheduled_task_get_id(task));
+		break;
+	case PROP_PERSISTENT:
+		g_value_set_boolean(value, purple_scheduled_task_get_persistent(task));
+		break;
+	case PROP_STATE:
+		g_value_set_enum(value, purple_scheduled_task_get_state(task));
+		break;
+	case PROP_SUBTITLE:
+		g_value_set_string(value, purple_scheduled_task_get_subtitle(task));
+		break;
+	case PROP_TAGS:
+		g_value_set_object(value, purple_scheduled_task_get_tags(task));
+		break;
+	case PROP_TASK_TYPE:
+		g_value_set_string(value, purple_scheduled_task_get_task_type(task));
+		break;
+	case PROP_TITLE:
+		g_value_set_string(value, purple_scheduled_task_get_title(task));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_scheduled_task_set_property(GObject *obj, guint param_id,
+                                   const GValue *value, GParamSpec *pspec)
+{
+	PurpleScheduledTask *task = PURPLE_SCHEDULED_TASK(obj);
+
+	switch(param_id) {
+	case PROP_CANCELLABLE:
+		purple_scheduled_task_set_cancellable(task,
+		                                      g_value_get_boolean(value));
+		break;
+	case PROP_ID:
+		purple_scheduled_task_set_id(task, g_value_get_string(value));
+		break;
+	case PROP_PERSISTENT:
+		purple_scheduled_task_set_persistent(task, g_value_get_boolean(value));
+		break;
+	case PROP_SUBTITLE:
+		purple_scheduled_task_set_subtitle(task, g_value_get_string(value));
+		break;
+	case PROP_TASK_TYPE:
+		purple_scheduled_task_set_task_type(task, g_value_get_string(value));
+		break;
+	case PROP_TITLE:
+		purple_scheduled_task_set_title(task, g_value_get_string(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_scheduled_task_init(PurpleScheduledTask *task) {
+	task->state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED;
+	task->tags = purple_tags_new();
+}
+
+static void
+purple_scheduled_task_class_init(PurpleScheduledTaskClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->dispose = purple_scheduled_task_dispose;
+	obj_class->finalize = purple_scheduled_task_finalize;
+	obj_class->get_property = purple_scheduled_task_get_property;
+	obj_class->set_property = purple_scheduled_task_set_property;
+
+	/**
+	 * PurpleScheduledTask:cancellable:
+	 *
+	 * Whether or not the task can be cancelled by the user.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_CANCELLABLE] = g_param_spec_boolean(
+		"cancellable", NULL, NULL,
+		TRUE,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
+		G_PARAM_EXPLICIT_NOTIFY);
+
+	/**
+	 * PurpleScheduledTask:execute-at:
+	 *
+	 * The date time when the task will be executed.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_EXECUTE_AT] = g_param_spec_boxed(
+		"execute-at", NULL, NULL,
+		G_TYPE_DATE_TIME,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleScheduledTask:id:
+	 *
+	 * The id of the task.
+	 *
+	 * This is primarily for internal use.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_ID] = g_param_spec_string(
+		"id", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
+		G_PARAM_EXPLICIT_NOTIFY);
+
+	/**
+	 * PurpleScheduledTask:persistent:
+	 *
+	 * Whether or not the task should remembered across invocations of the
+	 * program.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_PERSISTENT] = g_param_spec_boolean(
+		"persistent", NULL, NULL,
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+	/**
+	 * PurpleScheduledTask:state:
+	 *
+	 * The state of the task.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_STATE] = g_param_spec_enum(
+		"state", NULL, NULL,
+		PURPLE_TYPE_SCHEDULED_TASK_STATE,
+		PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+	/**
+	 * PurpleScheduledTask:subtitle:
+	 *
+	 * The subtitle for the task.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_SUBTITLE] = g_param_spec_string(
+		"subtitle", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+	/**
+	 * PurpleScheduledTask:tags:
+	 *
+	 * The tags for the task.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_TAGS] = g_param_spec_object(
+		"tags", NULL, NULL,
+		PURPLE_TYPE_TAGS,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleScheduledTask:task-type:
+	 *
+	 * The type of the task.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_TASK_TYPE] = g_param_spec_string(
+		"task-type", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
+		G_PARAM_EXPLICIT_NOTIFY);
+
+	/**
+	 * PurpleScheduledTask:title:
+	 *
+	 * The title of the task.
+	 *
+	 * Since: 3.0
+	 */
+	properties[PROP_TITLE] = g_param_spec_string(
+		"title", NULL, NULL,
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS |
+		G_PARAM_EXPLICIT_NOTIFY);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+
+	/**
+	 * PurpleScheduledTask::execute:
+	 * @task: the instance
+	 *
+	 * This signal is emitted the task is executing.
+	 *
+	 * The execution of the task is scheduled with
+	 * [method@ScheduledTask.schedule].
+	 *
+	 * Since: 3.0
+	 */
+	signals[SIG_EXECUTE] = g_signal_new_class_handler(
+		"execute",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		0);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+void
+purple_scheduled_task_cancel(PurpleScheduledTask *task) {
+	GObject *obj = NULL;
+
+	g_return_if_fail(PURPLE_IS_SCHEDULED_TASK(task));
+
+	if(task->state != PURPLE_SCHEDULED_TASK_STATE_SCHEDULED) {
+		return;
+	}
+
+	if(!task->cancellable) {
+		return;
+	}
+
+	g_clear_pointer(&task->execute_at, g_date_time_unref);
+	g_clear_handle_id(&task->source_id, g_source_remove);
+
+	task->state = PURPLE_SCHEDULED_TASK_STATE_CANCELLED;
+
+	obj = G_OBJECT(task);
+	g_object_freeze_notify(obj);
+	g_object_notify_by_pspec(obj, properties[PROP_EXECUTE_AT]);
+	g_object_notify_by_pspec(obj, properties[PROP_STATE]);
+	g_object_thaw_notify(obj);
+}
+
+gboolean
+purple_scheduled_task_get_cancellable(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), FALSE);
+
+	/* execute-at gets set to NULL when executed, so if it's NULL we can't
+	 * cancel a task that's already been executed.
+	 */
+	if(task->cancellable && task->execute_at == NULL) {
+		return FALSE;
+	}
+
+	return task->cancellable;
+}
+
+GDateTime *
+purple_scheduled_task_get_execute_at(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), NULL);
+
+	return task->execute_at;
+}
+
+const char *
+purple_scheduled_task_get_id(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), NULL);
+
+	/* If we weren't given an ID at construction generate one on the fly. */
+	if(birb_str_is_empty(task->id)) {
+		task->id = g_uuid_string_random();
+	}
+
+	return task->id;
+}
+
+gboolean
+purple_scheduled_task_get_persistent(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), FALSE);
+
+	return task->persistent;
+}
+
+PurpleScheduledTaskState
+purple_scheduled_task_get_state(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task),
+	                     PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED);
+
+	return task->state;
+}
+
+const char *
+purple_scheduled_task_get_subtitle(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), NULL);
+
+	return task->subtitle;
+}
+
+PurpleTags *
+purple_scheduled_task_get_tags(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), NULL);
+
+	return task->tags;
+}
+
+const char *
+purple_scheduled_task_get_task_type(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), NULL);
+
+	return task->task_type;
+}
+
+const char *
+purple_scheduled_task_get_title(PurpleScheduledTask *task) {
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), NULL);
+
+	return task->title;
+}
+
+PurpleScheduledTask *
+purple_scheduled_task_new(const char *task_type, const char *title,
+                          gboolean cancellable)
+{
+	g_return_val_if_fail(!birb_str_is_empty(task_type), NULL);
+	g_return_val_if_fail(!birb_str_is_empty(title), NULL);
+
+	return g_object_new(
+		PURPLE_TYPE_SCHEDULED_TASK,
+		"cancellable", cancellable,
+		"task-type", task_type,
+		"title", title,
+		NULL);
+}
+
+gboolean
+purple_scheduled_task_schedule(PurpleScheduledTask *task,
+                               GDateTime *execute_at, GError **error)
+{
+	GDateTime *now = NULL;
+	GObject *obj = NULL;
+	GTimeSpan difference = 0;
+
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), FALSE);
+	g_return_val_if_fail(execute_at != NULL, FALSE);
+
+	if(task->state == PURPLE_SCHEDULED_TASK_STATE_EXECUTING) {
+		g_set_error(error,
+		            PURPLE_SCHEDULED_TASK_ERROR,
+		            PURPLE_SCHEDULED_TASK_ERROR_RESCHEDULE_EXECUTING_TASK,
+		            "can not reschedule a task that is currently executing");
+
+		return FALSE;
+	}
+
+	if(task->state == PURPLE_SCHEDULED_TASK_STATE_SCHEDULED) {
+		purple_scheduled_task_cancel(task);
+	}
+
+	/* Check if our execute_at is valid. */
+	now = g_date_time_new_now_local();
+	difference = g_date_time_difference(execute_at, now);
+	g_date_time_unref(now);
+
+	if(difference < 0) {
+		char *iso8601 = g_date_time_format_iso8601(execute_at);
+
+		g_set_error(error,
+		            PURPLE_SCHEDULED_TASK_ERROR,
+		            PURPLE_SCHEDULED_TASK_ERROR_EXECUTE_AT_IN_PAST,
+		            "%s is in the past", iso8601);
+
+		g_free(iso8601);
+
+		return FALSE;
+	}
+
+	/* Save the execute_at. */
+	g_clear_pointer(&task->execute_at, g_date_time_unref);
+	task->execute_at = g_date_time_ref(execute_at);
+
+	task->source_id = g_timeout_add_seconds_full(G_PRIORITY_DEFAULT,
+	                                             difference / G_TIME_SPAN_SECOND,
+	                                             purple_scheduled_task_timeout_cb,
+	                                             g_object_ref(task),
+	                                             g_object_unref);
+
+	task->state = PURPLE_SCHEDULED_TASK_STATE_SCHEDULED;
+
+	obj = G_OBJECT(task);
+	g_object_freeze_notify(obj);
+	g_object_notify_by_pspec(obj, properties[PROP_EXECUTE_AT]);
+	g_object_notify_by_pspec(obj, properties[PROP_STATE]);
+	g_object_thaw_notify(obj);
+
+	return TRUE;
+}
+
+gboolean
+purple_scheduled_task_schedule_relative(PurpleScheduledTask *task,
+                                        GTimeSpan when, GError **error)
+{
+	GDateTime *now = NULL;
+	GDateTime *execute_at = NULL;
+	gboolean ret = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_SCHEDULED_TASK(task), FALSE);
+
+	now = g_date_time_new_now_local();
+	execute_at = g_date_time_add(now, when);
+	g_date_time_unref(now);
+
+	ret = purple_scheduled_task_schedule(task, execute_at, error);
+
+	g_date_time_unref(execute_at);
+
+	return ret;
+}
+
+void
+purple_scheduled_task_set_persistent(PurpleScheduledTask *task,
+                                     gboolean persistent)
+{
+	g_return_if_fail(PURPLE_IS_SCHEDULED_TASK(task));
+
+	if(task->persistent != persistent) {
+		task->persistent = persistent;
+
+		g_object_notify_by_pspec(G_OBJECT(task), properties[PROP_PERSISTENT]);
+	}
+}
+
+void
+purple_scheduled_task_set_subtitle(PurpleScheduledTask *task,
+                                   const char *subtitle)
+{
+	g_return_if_fail(PURPLE_IS_SCHEDULED_TASK(task));
+
+	if(g_set_str(&task->subtitle, subtitle)) {
+		g_object_notify_by_pspec(G_OBJECT(task), properties[PROP_SUBTITLE]);
+	}
+}

mercurial