Files
2026-08-06 15:29:52 +02:00

219 lines
7.7 KiB
Lua

Bridge.Database.AfterMigration("sky_phone", function()
local reminder_minutes = {
[0] = true,
[10] = true,
[30] = true,
[60] = true,
[1440] = true,
}
local function text_length(value)
return type(value) == "string" and utf8.len(value) or nil
end
local function affected_rows(result)
if type(result) == "number" then
return result
end
return type(result) == "table" and tonumber(result.affectedRows) or 0
end
local function event_dto(row)
return {
id = row.id,
title = row.title,
note = row.note,
startsAt = (tonumber(row.starts_at_unix) or 0) * 1000,
endsAt = (tonumber(row.ends_at_unix) or 0) * 1000,
reminderMinutes = row.reminder_minutes and tonumber(row.reminder_minutes) or nil,
remindedAt = row.reminded_at_unix and tonumber(row.reminded_at_unix) * 1000 or nil,
revision = tonumber(row.revision) or 1,
}
end
local function list_events(account_id, starts_at, ends_at)
local rows = Bridge.Database.Query([[
SELECT `id`, `title`, `note`, `reminder_minutes`, `revision`,
UNIX_TIMESTAMP(`starts_at`) AS `starts_at_unix`,
UNIX_TIMESTAMP(`ends_at`) AS `ends_at_unix`,
UNIX_TIMESTAMP(`reminded_at`) AS `reminded_at_unix`
FROM `sky_phone_calendar_events`
WHERE `account_id` = ?
AND `ends_at` >= FROM_UNIXTIME(?)
AND `starts_at` < FROM_UNIXTIME(?)
ORDER BY `starts_at`, `ends_at`, `id`
LIMIT 500
]], { account_id, starts_at, ends_at })
local events = {}
for _, row in ipairs(rows) do
events[#events + 1] = event_dto(row)
end
return events
end
local function valid_range(starts_at, ends_at)
local now = os.time()
return starts_at >= now - Config.Calendar.PastEditSeconds
and starts_at <= now + Config.Calendar.FutureSeconds
and ends_at > starts_at
and ends_at - starts_at <= Config.Calendar.MaximumDurationSeconds
end
local function validate_event(data)
if type(data) ~= "table" then
return nil
end
local title_length = text_length(data.title)
local note_length = text_length(data.note)
local starts_at = math.floor(tonumber(data.startsAt) or 0)
local ends_at = math.floor(tonumber(data.endsAt) or 0)
local reminder = data.reminderMinutes
if not title_length
or title_length < 1
or title_length > Config.Calendar.TitleMaxLength
or not note_length
or note_length > Config.Calendar.NoteMaxLength
or not valid_range(starts_at, ends_at)
or (reminder ~= nil and not reminder_minutes[tonumber(reminder)])
then
return nil
end
return {
title = data.title,
note = data.note,
starts_at = starts_at,
ends_at = ends_at,
reminder_minutes = reminder == nil and nil or tonumber(reminder),
}
end
Bridge.Callbacks.Register("sky_phone:calendar:list", function(source, data)
local account, error_response = SkyPhone.RequireAccount(source)
if not account then
return error_response
end
local starts_at = math.floor(tonumber(data and data.startsAt) or 0)
local ends_at = math.floor(tonumber(data and data.endsAt) or 0)
if starts_at <= 0 or ends_at <= starts_at or ends_at - starts_at > Config.Calendar.MaximumQuerySeconds then
return { success = false, error = "invalid_range" }
end
return { success = true, data = list_events(account.id, starts_at, ends_at) }
end)
Bridge.Callbacks.Register("sky_phone:calendar:create", function(source, data)
if not SkyPhone.AllowOperation(source, "calendar_write", 60, 60) then
return { success = false, error = "rate_limited" }
end
local account, error_response = SkyPhone.RequireAccount(source)
if not account then
return error_response
end
local event = validate_event(data)
if not event then
return { success = false, error = "invalid_event" }
end
local ids = Bridge.Database.Query("SELECT UUID() AS `id`", {})
local id = ids[1] and ids[1].id
if type(id) ~= "string" then
error("[sky_phone] Database did not generate a calendar event id.")
end
Bridge.Database.Query([[
INSERT INTO `sky_phone_calendar_events`
(`id`, `account_id`, `title`, `note`, `starts_at`, `ends_at`, `reminder_minutes`)
VALUES (?, ?, ?, ?, FROM_UNIXTIME(?), FROM_UNIXTIME(?), ?)
]], {
id,
account.id,
event.title,
event.note,
event.starts_at,
event.ends_at,
event.reminder_minutes,
})
return { success = true, data = { id = id } }
end)
Bridge.Callbacks.Register("sky_phone:calendar:update", function(source, data)
if not SkyPhone.AllowOperation(source, "calendar_write", 60, 60) then
return { success = false, error = "rate_limited" }
end
local account, error_response = SkyPhone.RequireAccount(source)
if not account then
return error_response
end
local event = validate_event(data)
if not event or type(data.id) ~= "string" then
return { success = false, error = "invalid_event" }
end
local revision = math.max(1, math.floor(tonumber(data.revision) or 0))
local result = Bridge.Database.Query([[
UPDATE `sky_phone_calendar_events`
SET `title` = ?, `note` = ?, `starts_at` = FROM_UNIXTIME(?),
`ends_at` = FROM_UNIXTIME(?), `reminder_minutes` = ?,
`reminded_at` = NULL, `revision` = `revision` + 1
WHERE `id` = ? AND `account_id` = ? AND `revision` = ?
]], {
event.title,
event.note,
event.starts_at,
event.ends_at,
event.reminder_minutes,
data.id,
account.id,
revision,
})
if affected_rows(result) ~= 1 then
return { success = false, error = "conflict" }
end
return { success = true }
end)
Bridge.Callbacks.Register("sky_phone:calendar:delete", function(source, data)
if not SkyPhone.AllowOperation(source, "calendar_write", 60, 60) then
return { success = false, error = "rate_limited" }
end
local account, error_response = SkyPhone.RequireAccount(source)
if not account then
return error_response
end
if type(data) ~= "table" or type(data.id) ~= "string" then
return { success = false, error = "invalid_event" }
end
Bridge.Database.Query(
"DELETE FROM `sky_phone_calendar_events` WHERE `id` = ? AND `account_id` = ?",
{ data.id, account.id }
)
return { success = true }
end)
CreateThread(function()
while true do
Wait(Config.Calendar.ReminderPollSeconds * 1000)
local due_events = Bridge.Database.Query([[
SELECT `id`, `account_id`, `title`, UNIX_TIMESTAMP(`starts_at`) AS `starts_at_unix`
FROM `sky_phone_calendar_events`
WHERE `reminder_minutes` IS NOT NULL
AND `reminded_at` IS NULL
AND DATE_SUB(`starts_at`, INTERVAL `reminder_minutes` MINUTE) <= CURRENT_TIMESTAMP
AND `starts_at` >= DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 1 DAY)
ORDER BY `starts_at`
LIMIT 100
]], {})
for _, event in ipairs(due_events) do
local result = Bridge.Database.Query([[
UPDATE `sky_phone_calendar_events`
SET `reminded_at` = CURRENT_TIMESTAMP
WHERE `id` = ? AND `reminded_at` IS NULL
]], { event.id })
if affected_rows(result) == 1 then
SkyPhone.NotifyAccountDevices(event.account_id, "sky_phone:calendar:reminder", {
eventId = event.id,
eventTitle = event.title,
startsAt = (tonumber(event.starts_at_unix) or 0) * 1000,
})
end
end
end
end)
end)