Thu, 24 Jul 2025 23:33:18 -0500
Create Purple.ScheduleTask
This is the first piece of creating a task scheduling API for things like
account reconnecting, scheduled messages, reminders, automatic avatar and
status changes and so on.
Testing Done:
Ran the unit tests under valgrind and called in the turtles.
Bugs closed: PIDGIN-18105
Reviewed at https://reviews.imfreedom.org/r/4072/
--- a/libpurple/meson.build Tue Jul 22 16:06:33 2025 -0500 +++ b/libpurple/meson.build Thu Jul 24 23:33:18 2025 -0500 @@ -70,6 +70,7 @@ 'purpleprotocolwhiteboard.c', 'purpleproxyinfo.c', 'purplesavedpresence.c', + 'purplescheduledtask.c', 'purplesqlitehistoryadapter.c', 'purpletags.c', 'purpleui.c', @@ -165,6 +166,7 @@ 'purpleprotocolwhiteboard.h', 'purpleproxyinfo.h', 'purplesavedpresence.h', + 'purplescheduledtask.h', 'purplesqlitehistoryadapter.h', 'purpletags.h', 'purpletyping.h', @@ -256,6 +258,7 @@ 'purplepresence.h', 'purpleprotocol.h', 'purpleproxyinfo.h', + 'purplescheduledtask.h', 'purpletyping.h', 'xmlnode.h' ]
--- /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]); + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purplescheduledtask.h Thu Jul 24 23:33:18 2025 -0500 @@ -0,0 +1,308 @@ +/* + * 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/>. + */ + +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#endif + +#ifndef PURPLE_SCHEDULED_TASK_H +#define PURPLE_SCHEDULED_TASK_H + +#include <glib.h> +#include <glib-object.h> + +#include "purpletags.h" +#include "purpleversion.h" + +G_BEGIN_DECLS + +#define PURPLE_SCHEDULED_TASK_ERROR purple_scheduled_task_error_quark() + +/** + * PurpleScheduledTaskError: + * @PURPLE_SCHEDULED_TASK_ERROR_EXECUTE_AT_IN_PAST: The given time is in the + * past. + * @PURPLE_SCHEDULED_TASK_ERROR_RESCHEDULE_EXECUTING_TASK: A task that is + * currently executing can not be cancelled. + * + * Error codes returned by scheduled tasks. + * + * Since: 3.0 + */ +typedef enum { + PURPLE_SCHEDULED_TASK_ERROR_EXECUTE_AT_IN_PAST PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, + PURPLE_SCHEDULED_TASK_ERROR_RESCHEDULE_EXECUTING_TASK PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, +} PurpleScheduledTaskError; + +/** + * purple_scheduled_task_error_quark: + * + * The error domain to identify errors with scheduled tasks. + * + * Returns: The error domain. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +GQuark purple_scheduled_task_error_quark(void); + +/** + * PurpleScheduledTaskState: + * @PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED: the task has not yet been + * scheduled + * @PURPLE_SCHEDULED_TASK_STATE_SCHEDULED: the task has been scheduled but not + * yet executed + * @PURPLE_SCHEDULED_TASK_STATE_CANCELLED: the task has been cancelled + * @PURPLE_SCHEDULED_TASK_STATE_EXECUTING: the task is currently executing + * @PURPLE_SCHEDULED_TASK_STATE_EXECUTED: the task has been executed + * + * + * The possible states that a task can be in. + * + * Since: 3.0 + */ +typedef enum { + PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, + PURPLE_SCHEDULED_TASK_STATE_SCHEDULED PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, + PURPLE_SCHEDULED_TASK_STATE_CANCELLED PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, + PURPLE_SCHEDULED_TASK_STATE_EXECUTING PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, + PURPLE_SCHEDULED_TASK_STATE_EXECUTED PURPLE_AVAILABLE_ENUMERATOR_IN_3_0, +} PurpleScheduledTaskState; + +/** + * PurpleScheduledTask: + * + * An object that represents a scheduled task. + * + * Since: 3.0 + */ + +#define PURPLE_TYPE_SCHEDULED_TASK (purple_scheduled_task_get_type()) + +PURPLE_AVAILABLE_IN_3_0 +G_DECLARE_FINAL_TYPE(PurpleScheduledTask, purple_scheduled_task, PURPLE, + SCHEDULED_TASK, GObject) + +/** + * purple_scheduled_task_cancel: + * + * Cancels the task. + * + * If the task is not scheduled, this does nothing. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +void purple_scheduled_task_cancel(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_cancellable: + * + * Gets whether or not the task can be cancelled by the user. + * + * Returns: true if the task can be cancelled. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_scheduled_task_get_cancellable(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_execute_at: + * + * Gets the date time when the task will execute. + * + * Returns: (transfer none) (nullable): The date time of when the task will + * execute. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +GDateTime *purple_scheduled_task_get_execute_at(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_id: + * + * Gets the id of the task. + * + * Returns: (transfer none) (nullable): The id of the task. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +const char *purple_scheduled_task_get_id(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_persistent: + * + * Gets whether or not the task will be remembered across invocations of the + * program. + * + * Returns: true if the task will be remembered. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_scheduled_task_get_persistent(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_state: + * + * Gets the current state of the task. + * + * Returns: The state of the task. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleScheduledTaskState purple_scheduled_task_get_state(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_subtitle: + * + * Gets the subtitle of the task. + * + * Returns: (nullable): The subtitle of the task. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +const char *purple_scheduled_task_get_subtitle(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_tags: + * + * Gets the tags for the task. + * + * Returns: (transfer none): The tags. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleTags *purple_scheduled_task_get_tags(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_task_type: + * + * Gets the type of the task. + * + * Returns: The type of the task. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +const char *purple_scheduled_task_get_task_type(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_get_title: + * + * Gets the title of the task. + * + * Returns: The title. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +const char *purple_scheduled_task_get_title(PurpleScheduledTask *task); + +/** + * purple_scheduled_task_new: + * @task_type: a well-known type name + * @title: a title that can be displayed to users + * @cancellable: true if the task can be cancelled by the user; false + * otherwise + * + * Creates a new scheduled task. + * + * Returns: (transfer full) (nullable): The new scheduled task. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +PurpleScheduledTask *purple_scheduled_task_new(const char *task_type, const char *title, gboolean cancellable); + +/** + * purple_scheduled_task_schedule: + * @execute_at: the date time for when the task should be executed + * @error: (out) (nullable): a return address for a #GError + * + * Schedules the task. + * + * If the task has already been scheduled it will be cancelled and rescheduled + * for the new time. + * + * If the @execute_at is in the past an error will be returned. + * + * Returns: true if the task was scheduled successfully. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_scheduled_task_schedule(PurpleScheduledTask *task, GDateTime *execute_at, GError **error); + +/** + * purple_scheduled_task_schedule_relative: + * @when: a relative time span when to execute + * @error: (out) (nullable): a return address for a #GError + * + * Schedules the task with a relative time. + * + * This is a wrapper around [method@ScheduledTask.schedule] that will add @when + * to the current time for you. + * + * If the task has already been scheduled it will be cancelled and rescheduled + * for the new time. + * + * If the @when is in the past an error will be returned. + * + * Returns: true if the task was scheduled successfully. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +gboolean purple_scheduled_task_schedule_relative(PurpleScheduledTask *task, GTimeSpan when, GError **error); + +/** + * purple_scheduled_task_set_persistent: + * @persistent: the new value + * + * Sets whether the task should be remembered across invocations or not. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +void purple_scheduled_task_set_persistent(PurpleScheduledTask *task, gboolean persistent); + +/** + * purple_scheduled_task_set_subtitle: + * @subtitle: the new subtitle + * + * Sets the subtitle for the task. + * + * Since: 3.0 + */ +PURPLE_AVAILABLE_IN_3_0 +void purple_scheduled_task_set_subtitle(PurpleScheduledTask *task, const char *subtitle); + +G_END_DECLS + +#endif /* PURPLE_SCHEDULED_TASK_H */
--- a/libpurple/tests/meson.build Tue Jul 22 16:06:33 2025 -0500 +++ b/libpurple/tests/meson.build Thu Jul 24 23:33:18 2025 -0500 @@ -53,6 +53,7 @@ 'request_group', 'request_page', 'saved_presence', + 'scheduled_task', 'str', 'tags', 'util',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/tests/test_scheduled_task.c Thu Jul 24 23:33:18 2025 -0500 @@ -0,0 +1,443 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <https://www.gnu.org/licenses/>. + */ + +#include <glib.h> + +#include <birb.h> + +#include <purple.h> + +/****************************************************************************** + * Tests + *****************************************************************************/ +static void +test_purple_scheduled_task_new(void) { + PurpleScheduledTask *task = NULL; + + task = purple_scheduled_task_new("test-task", "A task for testing", TRUE); + + birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK); + + g_assert_finalize_object(task); +} + +static void +test_purple_scheduled_task_properties(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + PurpleTags *tags = NULL; + GDateTime *execute_at = NULL; + gboolean cancellable = TRUE; + gboolean persistent = FALSE; + char *id = NULL; + char *subtitle = NULL; + char *task_type = NULL; + char *title = NULL; + + task = g_object_new( + PURPLE_TYPE_SCHEDULED_TASK, + "cancellable", FALSE, + "id", "0xabc123", + "persistent", TRUE, + "subtitle", "a task for testing", + "task-type", "test", + "title", "Test", + NULL); + + g_assert_true(PURPLE_IS_SCHEDULED_TASK(task)); + + g_object_get( + G_OBJECT(task), + "cancellable", &cancellable, + "execute-at", &execute_at, + "id", &id, + "persistent", &persistent, + "state", &state, + "subtitle", &subtitle, + "tags", &tags, + "task-type", &task_type, + "title", &title, + NULL); + + g_assert_false(cancellable); + + /* The task hasn't been scheduled so it doesn't have an execution time. */ + g_assert_null(execute_at); + + g_assert_cmpstr(id, ==, "0xabc123"); + g_clear_pointer(&id, g_free); + + g_assert_true(persistent); + + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + g_assert_cmpstr(subtitle, ==, "a task for testing"); + g_clear_pointer(&subtitle, g_free); + + birb_assert_type(tags, PURPLE_TYPE_TAGS); + g_clear_object(&tags); + + g_assert_cmpstr(task_type, ==, "test"); + g_clear_pointer(&task_type, g_free); + + g_assert_cmpstr(title, ==, "Test"); + g_clear_pointer(&title, g_free); + + g_assert_finalize_object(task); +} + +/****************************************************************************** + * Schedule Tests + *****************************************************************************/ +static void +test_purple_scheduled_task_executed_counter_cb(PurpleScheduledTask *task, + gpointer data) +{ + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + guint *counter = data; + + birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTING); + + *counter = *counter + 1; +} + +static void +test_purple_scheduled_task_executed_cancel_cb(PurpleScheduledTask *task, + G_GNUC_UNUSED gpointer data) +{ + GError *error = NULL; + gboolean result = FALSE; + + result = purple_scheduled_task_schedule_relative(task, + 10 * G_TIME_SPAN_MINUTE, + &error); + g_assert_error(error, + PURPLE_SCHEDULED_TASK_ERROR, + PURPLE_SCHEDULED_TASK_ERROR_RESCHEDULE_EXECUTING_TASK); + g_clear_error(&error); + g_assert_false(result); +} + +static void +test_purple_scheduled_task_executed_quit_cb(PurpleScheduledTask *task, + gpointer data) +{ + birb_assert_type(task, PURPLE_TYPE_SCHEDULED_TASK); + + g_main_loop_quit(data); +} + +static void +test_purple_scheduled_task_main_loop_timeout_cb(gpointer data) { + g_main_loop_quit(data); + + g_assert_not_reached(); +} + +static void +test_purple_scheduled_task_schedule_normal(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + GDateTime *execute_at = NULL; + GError *error = NULL; + GMainLoop *loop = NULL; + guint counter = 0; + gboolean result = FALSE; + + task = purple_scheduled_task_new("test", "Test task", TRUE); + g_signal_connect(task, "execute", + G_CALLBACK(test_purple_scheduled_task_executed_counter_cb), + &counter); + + /* Add another signal handler to verify that you can't reschedule an + * executing task. + */ + g_signal_connect(task, "execute", + G_CALLBACK(test_purple_scheduled_task_executed_cancel_cb), + NULL); + + /* Verify the default state is unscheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Make sure that we don't currently have an execute-at. */ + execute_at = purple_scheduled_task_get_execute_at(task); + g_assert_null(execute_at); + + /* Now schedule the task to execute 10 milliseconds from now. */ + result = purple_scheduled_task_schedule_relative(task, + 10 * G_TIME_SPAN_MILLISECOND, + &error); + g_assert_no_error(error); + g_assert_true(result); + + /* Make sure the execute-at property got set. */ + execute_at = purple_scheduled_task_get_execute_at(task); + g_assert_nonnull(execute_at); + + /* Make sure the state was set to scheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED); + + /* Create our main loop and add a timeout to avoid runaways. */ + loop = g_main_loop_new(NULL, FALSE); + g_signal_connect(task, "execute", + G_CALLBACK(test_purple_scheduled_task_executed_quit_cb), + loop); + + g_timeout_add_seconds_once(10, + test_purple_scheduled_task_main_loop_timeout_cb, + loop); + g_main_loop_run(loop); + g_main_loop_unref(loop); + + /* Make sure that the execute signal was emitted. */ + g_assert_cmpuint(counter, ==, 1); + + /* Make sure the state was updated to executed. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTED); + + g_assert_finalize_object(task); +} + +static void +test_purple_scheduled_task_schedule_cancelled(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + GError *error = NULL; + gboolean result = FALSE; + + task = purple_scheduled_task_new("test", "Test task", TRUE); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Cancelling a task that is not currently scheduled should do nothing. */ + purple_scheduled_task_cancel(task); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Schedule the task. */ + result = purple_scheduled_task_schedule_relative(task, + 10 * G_TIME_SPAN_MINUTE, + &error); + g_assert_no_error(error); + g_assert_true(result); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED); + + /* Cancel the task. */ + purple_scheduled_task_cancel(task); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_CANCELLED); + + g_assert_finalize_object(task); +} + +static void +test_purple_scheduled_task_schedule_reschedule(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + GDateTime *now = NULL; + GDateTime *later = NULL; + GDateTime *execute_at = NULL; + GError *error = NULL; + gboolean result = FALSE; + + task = purple_scheduled_task_new("test", "Test task", TRUE); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Schedule the task. */ + result = purple_scheduled_task_schedule_relative(task, + 10 * G_TIME_SPAN_MINUTE, + &error); + g_assert_no_error(error); + g_assert_true(result); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED); + + /* Reschedule the task for an hour. */ + now = g_date_time_new_now_local(); + later = g_date_time_add(now, 1 * G_TIME_SPAN_HOUR); + g_clear_pointer(&now, g_date_time_unref); + + result = purple_scheduled_task_schedule(task, later, &error); + g_assert_no_error(error); + g_assert_true(result); + + /* Verify that the execute-at property is correct. */ + execute_at = purple_scheduled_task_get_execute_at(task); + g_assert_true(birb_date_time_equal(execute_at, later)); + g_clear_pointer(&later, g_date_time_unref); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED); + + /* Cancel the task to clear the timeout and the reference it holds. */ + purple_scheduled_task_cancel(task); + + g_assert_finalize_object(task); +} + +static void +test_purple_scheduled_task_schedule_past(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + GError *error = NULL; + gboolean result = FALSE; + + task = purple_scheduled_task_new("test", "Test task", TRUE); + + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Schedule the task for 10 minutes ago. */ + result = purple_scheduled_task_schedule_relative(task, + -10 * G_TIME_SPAN_MINUTE, + &error); + g_assert_error(error, + PURPLE_SCHEDULED_TASK_ERROR, + PURPLE_SCHEDULED_TASK_ERROR_EXECUTE_AT_IN_PAST); + g_clear_error(&error); + g_assert_false(result); + + g_assert_finalize_object(task); +} + +static void +test_purple_scheduled_task_schedule_reuse(void) { + PurpleScheduledTask *task = NULL; + PurpleScheduledTaskState state = PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED; + GDateTime *execute_at = NULL; + GError *error = NULL; + GMainLoop *loop = NULL; + guint counter = 0; + gboolean result = FALSE; + + task = purple_scheduled_task_new("test", "Test task", TRUE); + g_signal_connect(task, "execute", + G_CALLBACK(test_purple_scheduled_task_executed_counter_cb), + &counter); + + /* Verify the default state is unscheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_UNSCHEDULED); + + /* Make sure that we don't currently have an execute-at. */ + execute_at = purple_scheduled_task_get_execute_at(task); + g_assert_null(execute_at); + + /* Now schedule the task to execute 10 milliseconds from now. */ + result = purple_scheduled_task_schedule_relative(task, + 10 * G_TIME_SPAN_MILLISECOND, + &error); + g_assert_no_error(error); + g_assert_true(result); + + /* Make sure the execute-at property got set. */ + execute_at = purple_scheduled_task_get_execute_at(task); + g_assert_nonnull(execute_at); + + /* Make sure the state was set to scheduled. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_SCHEDULED); + + /* Create our main loop and add a timeout to avoid runaways. */ + loop = g_main_loop_new(NULL, FALSE); + g_signal_connect(task, "execute", + G_CALLBACK(test_purple_scheduled_task_executed_quit_cb), + loop); + + g_timeout_add_seconds_once(10, + test_purple_scheduled_task_main_loop_timeout_cb, + loop); + g_main_loop_run(loop); + + /* Make sure that the execute signal was emitted. */ + g_assert_cmpuint(counter, ==, 1); + + /* Make sure the state was updated to executed. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTED); + + /* Now reset everything. */ + counter = 0; + g_signal_connect(task, "execute", + G_CALLBACK(test_purple_scheduled_task_executed_quit_cb), + loop); + g_timeout_add_seconds_once(10, + test_purple_scheduled_task_main_loop_timeout_cb, + loop); + + /* Now schedule the task to execute 10 milliseconds from now. */ + result = purple_scheduled_task_schedule_relative(task, + 10 * G_TIME_SPAN_MILLISECOND, + &error); + g_assert_no_error(error); + g_assert_true(result); + + /* Run the loop. */ + g_main_loop_run(loop); + + /* Make sure that the execute signal was emitted. */ + g_assert_cmpuint(counter, ==, 1); + + /* Make sure the state was updated to executed. */ + state = purple_scheduled_task_get_state(task); + g_assert_cmpuint(state, ==, PURPLE_SCHEDULED_TASK_STATE_EXECUTED); + + g_main_loop_unref(loop); + + g_assert_finalize_object(task); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +int +main(int argc, char *argv[]) { + g_test_init(&argc, &argv, NULL); + g_test_set_nonfatal_assertions(); + + g_test_add_func("/scheduled-task/new", test_purple_scheduled_task_new); + g_test_add_func("/scheduled-task/properties", + test_purple_scheduled_task_properties); + + g_test_add_func("/scheduled-task/schedule/normal", + test_purple_scheduled_task_schedule_normal); + g_test_add_func("/scheduled-task/schedule/cancelled", + test_purple_scheduled_task_schedule_cancelled); + g_test_add_func("/scheduled-task/schedule/reschedule", + test_purple_scheduled_task_schedule_reschedule); + g_test_add_func("/scheduled-task/schedule/past", + test_purple_scheduled_task_schedule_past); + g_test_add_func("/scheduled-task/schedule/reuse", + test_purple_scheduled_task_schedule_reuse); + + return g_test_run(); +}
--- a/po/POTFILES.in Tue Jul 22 16:06:33 2025 -0500 +++ b/po/POTFILES.in Thu Jul 24 23:33:18 2025 -0500 @@ -91,6 +91,7 @@ libpurple/purpleprotocolwhiteboard.c libpurple/purpleproxyinfo.c libpurple/purplesavedpresence.c +libpurple/purplescheduledtask.c libpurple/purplesqlitehistoryadapter.c libpurple/purpletags.c libpurple/purpleui.c