1-- mod_reminders 2-- 3-- Copyright (C) 2020 Marcos de Vera Piquero <marcos@tenak.net> 4-- 5-- This file is MIT/X11 licensed. 6-- 7-- A module to support ProtoXEP: Reminders 8-- 9 10local id = require "util.id" 11local datetime = require"util.datetime"; 12local errors = require"util.error"; 13local jid = require"util.jid"; 14local st = require"util.stanza"; 15local os_time = os.time; 16 17local xmlns_reminders = "urn:xmpp:reminders:0"; 18 19local reminders_store = module:open_store(xmlns_reminders, "keyval"); 20 21local reminders_errors = { 22 missing_fields = { 23 type = "modify"; 24 condition = "bad-request"; 25 text = "Missing required value for date or text"; 26 }; 27 invalid_dateformat = { 28 type = "modify"; 29 condition = "bad-request"; 30 text = "Invalid date format"; 31 }; 32 past_date = { 33 type = "modify"; 34 condition = "gone"; 35 text = "Reminder date is in the past"; 36 }; 37 store_error = { 38 type = "cancel"; 39 condition = "internal-server-error"; 40 text = "Unable to persist data"; 41 }; 42}; 43 44local function reminder_error (name) 45 return errors.new(name, nil, reminders_errors); 46end 47 48local function store_reminder (reminder) 49 -- pushes the reminder to the store, and nothing else 50 return reminders_store:set(reminder.id, reminder); 51end 52 53local function delete_reminder (reminder_id) 54 -- empties the store for the given reminder_id 55 return reminders_store:set(reminder_id, nil); 56end 57 58local function get_reminder (reminder_id) 59 return reminders_store:get(reminder_id); 60end 61 62local function send_reminder (reminder) 63 -- actually delivers the <message /> with the reminder to the user 64 local bare = jid.bare(reminder.jid); 65 module:log("debug", "Sending reminder %s to %s", reminder.id, bare); 66 local message = st.message({ from = "localhost"; to = bare; id = id.short() }) 67 :tag("reminder", {id = reminder.id; xmlns = xmlns_reminders}) 68 :add_child(reminder.text) 69 :tag("date"):text(datetime.datetime(reminder.date)):up(); 70 module:send(message); 71 return delete_reminder(reminder.id) 72end 73 74local function schedule_reminder (reminder) 75 -- schedule a module:add_timer for the given reminder 76 module:log("debug", "Scheduling reminder to datetime %s", reminder.date); 77 local now = os_time(); 78 local when = reminder.date; 79 local delay = when - now; 80 module:log("debug", "Reminder text: %s", reminder.text); 81 local function callback () 82 send_reminder(reminder) 83 end 84 module:add_timer(delay, callback); 85end 86 87local function process_reminders_store () 88 -- retrieve all reminders in the store and schedule them 89 for reminder_id in reminders_store:users() do 90 module:log("debug", "Found stored reminder %s", reminder_id); 91 local reminder = get_reminder(reminder_id); 92 if reminder.date and reminder.text then 93 local text = st.deserialize(reminder.text) 94 module:log("debug", "Read reminder %s", reminder.id); 95 -- cleanup missed reminders 96 if reminder.date < os_time() then 97 module:log("debug", "Deleting outdated reminder %s", reminder.id) 98 delete_reminder(reminder.id) 99 end 100 schedule_reminder({ 101 date = reminder.date; 102 id = reminder.id; 103 jid = reminder.jid; 104 text = text; 105 }) 106 else 107 delete_reminder(reminder_id); 108 end 109 end 110end 111 112local function create_reminder (jid, reminder) 113 local date = reminder:get_child("date"); 114 local text = reminder:get_child("text"); 115 if date == nil or text == nil then 116 return nil, reminder_error("missing_fields") 117 end 118 local now = os_time(); 119 local _, parsed_date = pcall(datetime.parse, date:get_text()); 120 if parsed_date == nil then 121 return nil, reminder_error("invalid_dateformat") 122 end 123 if parsed_date < now then 124 return nil, reminder_error("past_date"), nil 125 end 126 local reminder_id = id.medium(); 127 local rem = st.stanza("reminder", {xmlns = xmlns_reminders; id = reminder_id}); 128 local data = { 129 id = reminder_id; 130 jid = jid; 131 text = text; 132 date = parsed_date; 133 } 134 local stored = store_reminder(data); 135 if not stored then 136 return nil, reminder_error("store_error") 137 end 138 schedule_reminder(data); 139 return rem 140end 141 142local function handle_set (event) 143 local origin, stanza = event.origin, event.stanza 144 local reminder = stanza:get_child("reminder", xmlns_reminders); 145 if reminder.attr.id ~= nil and reminder:get_child("date") == nil then 146 -- delete existing reminder 147 local ok = delete_reminder(reminder.attr.id); 148 if ok then 149 module:log("debug", "reminder %s deleted", reminder.attr.id); 150 origin.send(st.reply(stanza):add_child(reminder)); 151 else 152 module:log("debug", "failed to delete reminder %s", reminder.attr.id); 153 origin.send(st.error_reply(stanza, "cancel", "internal-server-error")); 154 end 155 return true; 156 else 157 -- create new reminder 158 local jid = stanza.attr.from 159 local created, err = create_reminder(jid, reminder); 160 if err ~= nil then 161 origin.send(st.error_reply(stanza, err)) 162 return true; 163 else 164 origin.send(st.reply(stanza):add_child(created)) 165 return true; 166 end 167 end 168 origin.send(st.error_reply(stanza, "modify", "bad-request")); 169 return true; 170end 171 172 173-- load saved reminders and set timers 174process_reminders_store(); 175 176module:hook("iq-set/host/"..xmlns_reminders..":reminder", handle_set) 177module:add_feature(xmlns_reminders); 178module:log("debug", "Module loaded"); 179