--- /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]); + } +}