From d8531378205447e16ee249e72018ace6c0bedd76 Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sun, 19 Jul 2026 15:26:21 +0200 Subject: [PATCH] feat(plugins): Add `c2.DateTime` (#7096) This adds the `DateTime` API described in #7021. Reviewed-by: Mm2PL Reviewed-by: pajlada --- docs/chatterino.d.ts | 14 +++ docs/lua-meta/globals.lua | 53 +++++++++ docs/wip-plugins.md | 44 ++++++++ src/CMakeLists.txt | 2 + src/controllers/plugins/LuaAPI.hpp | 1 + src/controllers/plugins/PluginController.cpp | 2 + src/controllers/plugins/api/DateTime.cpp | 68 ++++++++++++ src/controllers/plugins/api/DateTime.hpp | 65 +++++++++++ tests/lua/datetime/basic.lua | 109 +++++++++++++++++++ tests/src/Plugins.cpp | 13 +++ 10 files changed, 371 insertions(+) create mode 100644 src/controllers/plugins/api/DateTime.cpp create mode 100644 src/controllers/plugins/api/DateTime.hpp create mode 100644 tests/lua/datetime/basic.lua diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index 3d736fe81..872ded538 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -664,6 +664,20 @@ declare namespace c2 { } var windows: WindowManager; + + class DateTime { + static from_iso_string(str: string): DateTime; + to_iso_string(): string; + to_iso_string_without_ms(): string; + + static current_local(): DateTime; + static current_utc(): DateTime; + + static from_unix_milliseconds(ts: number): DateTime; + static from_unix_seconds(ts: number): DateTime; + to_unix_milliseconds(): number; + to_unix_seconds(): number; + } } declare module "chatterino.json" { diff --git a/docs/lua-meta/globals.lua b/docs/lua-meta/globals.lua index d46899bd9..14d8354c2 100644 --- a/docs/lua-meta/globals.lua +++ b/docs/lua-meta/globals.lua @@ -332,6 +332,58 @@ function c2.ConnectionHandle:is_connected() end -- End src/controllers/plugins/api/ConnectionHandle.hpp +-- Begin src/controllers/plugins/api/DateTime.hpp + + + +---A zoned date and time. +---@class c2.DateTime +c2.DateTime = {} + +---Parse a date from an ISO 8601 string with milliseconds (yyyy-MM-ddTHH:mm:ss.zzz) +---@param str string +---@return c2.DateTime +function c2.DateTime.from_iso_string(str) end + +---Format the datetime as an ISO string with milliseconds (yyyy-MM-ddTHH:mm:ss.zzz) +---@return string +function c2.DateTime:to_iso_string() end + +---Format the datetime as an ISO string without milliseconds (yyyy-MM-ddTHH:mm:ss) +---@return string +function c2.DateTime:to_iso_string_without_ms() end + +---Get the current datetime in the system's local time zone. +---@return c2.DateTime +function c2.DateTime.current_local() end + +---Get the current datetime in the UTC time zone (00:00). +---@return c2.DateTime +function c2.DateTime.current_utc() end + +---Get a datetime from a Unix timestamp (offset from 1970-01-01 00:00 UTC) in milliseconds. +--- +---The returned date time will be in the local time zone. +---@param ts number +---@return c2.DateTime +function c2.DateTime.from_unix_milliseconds(ts) end + +---Get a datetime from a Unix timestamp (offset from 1970-01-01 00:00 UTC) in seconds. +--- +---The returned date time will be in the local time zone. +---@param ts number +---@return c2.DateTime +function c2.DateTime.from_unix_seconds(ts) end + +---Convert a datetime to a Unix timestamp (offset from 1970-01-01 00:00 UTC) in milliseconds. +---@return number +function c2.DateTime:to_unix_milliseconds() end + +---Convert a datetime to a Unix timestamp (offset from 1970-01-01 00:00 UTC) in seconds. +---@return number +function c2.DateTime:to_unix_seconds() end +-- End src/controllers/plugins/api/DateTime.hpp + -- Begin src/controllers/plugins/api/HTTPResponse.hpp ---@class c2.HTTPResponse @@ -801,6 +853,7 @@ c2.MessageFlag = { InvalidReplyTarget = 0, WatchStreak = 0, Announcement = 0, + UncategorizedNotification = 0, } -- End src/messages/MessageFlag.hpp diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index 719e9b88b..eb95ed846 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -981,6 +981,50 @@ Get all open windows. The global [`WindowManager`](#windowmanager). +#### `DateTime` + +A zoned date and time. + +##### `DateTime.from_iso_string(str)` + +Parse a date from an ISO 8601 string with milliseconds (`yyyy-MM-ddTHH:mm:ss.zzz±hh:mm`) + +##### `DateTime:to_iso_string()` + +Format the datetime as an ISO string with milliseconds (`yyyy-MM-ddTHH:mm:ss.zzz±hh:mm`) + +##### `DateTime:to_iso_string_without_ms()` + +Format the datetime as an ISO string without milliseconds (`yyyy-MM-ddTHH:mm:ss±hh:mm`) + +##### `DateTime.current_local()` + +Get the current datetime in the system's local time zone. + +##### `DateTime.current_utc()` + +Get the current datetime in the UTC time zone (00:00). + +##### `DateTime.from_unix_milliseconds(ts)` + +Get a datetime from a Unix timestamp (offset from 1970-01-01 00:00 UTC) in milliseconds. + +The returned date time will be in the local time zone. + +##### `DateTime.from_unix_seconds(ts)` + +Get a datetime from a Unix timestamp (offset from 1970-01-01 00:00 UTC) in seconds. + +The returned date time will be in the local time zone. + +##### `DateTime:to_unix_milliseconds()` + +Convert a datetime to a Unix timestamp (offset from 1970-01-01 00:00 UTC) in milliseconds. + +##### `DateTime:to_unix_seconds()` + +Convert a datetime to a Unix timestamp (offset from 1970-01-01 00:00 UTC) in seconds. + ### Input/Output API These functions are wrappers for Lua's I/O library. Functions on file pointer diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a67bd84e6..a6661e889 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -262,6 +262,8 @@ set(SOURCE_FILES controllers/plugins/api/ChannelRef.hpp controllers/plugins/api/ConnectionHandle.cpp controllers/plugins/api/ConnectionHandle.hpp + controllers/plugins/api/DateTime.cpp + controllers/plugins/api/DateTime.hpp controllers/plugins/api/DebugLibrary.cpp controllers/plugins/api/DebugLibrary.hpp controllers/plugins/api/EventType.hpp diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index 0d6cac342..e7a52831e 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -95,6 +95,7 @@ sol::table toTable(lua_State *L, const CompletionEvent &ev); * @includefile controllers/plugins/api/Accounts.hpp * @includefile controllers/plugins/api/ChannelRef.hpp * @includefile controllers/plugins/api/ConnectionHandle.hpp + * @includefile controllers/plugins/api/DateTime.hpp * @includefile controllers/plugins/api/HTTPResponse.hpp * @includefile controllers/plugins/api/HTTPRequest.hpp * @includefile controllers/plugins/api/Images.hpp diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index f01bc2b10..c91f119a7 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -14,6 +14,7 @@ # include "controllers/plugins/api/Accounts.hpp" # include "controllers/plugins/api/ChannelRef.hpp" # include "controllers/plugins/api/ConnectionHandle.hpp" +# include "controllers/plugins/api/DateTime.hpp" # include "controllers/plugins/api/DebugLibrary.hpp" # include "controllers/plugins/api/HTTPRequest.hpp" # include "controllers/plugins/api/HTTPResponse.hpp" @@ -255,6 +256,7 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin) lua::api::images::createUserTypes(c2); lua::api::createAccounts(c2); lua::api::windowmanager::createUserTypes(c2); + lua::api::datetime::createUserTypes(c2); c2["ChannelType"] = lua::createEnumTable(lua); c2["HTTPMethod"] = lua::createEnumTable(lua); c2["EventType"] = lua::createEnumTable(lua); diff --git a/src/controllers/plugins/api/DateTime.cpp b/src/controllers/plugins/api/DateTime.cpp new file mode 100644 index 000000000..a98a6fd93 --- /dev/null +++ b/src/controllers/plugins/api/DateTime.cpp @@ -0,0 +1,68 @@ +#include "controllers/plugins/api/DateTime.hpp" + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include "controllers/plugins/SolTypes.hpp" // IWYU pragma: keep + +# include + +namespace chatterino::lua::api::datetime { + +void createUserTypes(sol::table &c2) +{ + c2.new_usertype( + "DateTime", sol::no_constructor, // + + // __tostring uses the ISO date. + sol::meta_method::to_string, + [](const QDateTime &self) { + return self.toString(Qt::ISODateWithMs); + }, + + // ISO 8601 conversion. + "from_iso_string", + [](const QString &s) { + return QDateTime::fromString(s, Qt::ISODateWithMs); + }, + "to_iso_string", + [](const QDateTime &self) { + return self.toString(Qt::ISODateWithMs); + }, + "to_iso_string_without_ms", + [](const QDateTime &self) { + return self.toString(Qt::ISODate); + }, + + // System time constructors. + "current_local", + [] { + return QDateTime::currentDateTime(); + }, + "current_utc", + [] { + return QDateTime::currentDateTimeUtc(); + }, + + // Unix (milli)seconds conversion. + "from_unix_milliseconds", + [](int64_t ts) { + return QDateTime::fromMSecsSinceEpoch(ts); + }, + "from_unix_seconds", + [](int64_t ts) { + return QDateTime::fromSecsSinceEpoch(ts); + }, + "to_unix_milliseconds", + [](const QDateTime &self) { + return self.toMSecsSinceEpoch(); + }, + "to_unix_seconds", + [](const QDateTime &self) { + return self.toSecsSinceEpoch(); + } // + ); +} + +} // namespace chatterino::lua::api::datetime + +#endif diff --git a/src/controllers/plugins/api/DateTime.hpp b/src/controllers/plugins/api/DateTime.hpp new file mode 100644 index 000000000..220425da3 --- /dev/null +++ b/src/controllers/plugins/api/DateTime.hpp @@ -0,0 +1,65 @@ +#pragma once + +#ifdef CHATTERINO_HAVE_PLUGINS + +# include + +namespace chatterino::lua::api::datetime { + +/* @lua-fragment + +---A zoned date and time. +---@class c2.DateTime +c2.DateTime = {} + +---Parse a date from an ISO 8601 string with milliseconds (yyyy-MM-ddTHH:mm:ss.zzz) +---@param str string +---@return c2.DateTime +function c2.DateTime.from_iso_string(str) end + +---Format the datetime as an ISO string with milliseconds (yyyy-MM-ddTHH:mm:ss.zzz) +---@return string +function c2.DateTime:to_iso_string() end + +---Format the datetime as an ISO string without milliseconds (yyyy-MM-ddTHH:mm:ss) +---@return string +function c2.DateTime:to_iso_string_without_ms() end + +---Get the current datetime in the system's local time zone. +---@return c2.DateTime +function c2.DateTime.current_local() end + +---Get the current datetime in the UTC time zone (00:00). +---@return c2.DateTime +function c2.DateTime.current_utc() end + +---Get a datetime from a Unix timestamp (offset from 1970-01-01 00:00 UTC) in milliseconds. +--- +---The returned date time will be in the local time zone. +---@param ts number +---@return c2.DateTime +function c2.DateTime.from_unix_milliseconds(ts) end + +---Get a datetime from a Unix timestamp (offset from 1970-01-01 00:00 UTC) in seconds. +--- +---The returned date time will be in the local time zone. +---@param ts number +---@return c2.DateTime +function c2.DateTime.from_unix_seconds(ts) end + +---Convert a datetime to a Unix timestamp (offset from 1970-01-01 00:00 UTC) in milliseconds. +---@return number +function c2.DateTime:to_unix_milliseconds() end + +---Convert a datetime to a Unix timestamp (offset from 1970-01-01 00:00 UTC) in seconds. +---@return number +function c2.DateTime:to_unix_seconds() end +*/ + +/// Creates +/// - `c2.DateTime` (QDateTime) +void createUserTypes(sol::table &c2); + +} // namespace chatterino::lua::api::datetime + +#endif diff --git a/tests/lua/datetime/basic.lua b/tests/lua/datetime/basic.lua new file mode 100644 index 000000000..66c81332e --- /dev/null +++ b/tests/lua/datetime/basic.lua @@ -0,0 +1,109 @@ +---@generic T +---@param a T +---@param b T +local function assert_eq(a, b) + if a ~= b then + error(tostring(a) .. " ~= " .. tostring(b)) + end +end + +local specs_ms = { + { + iso_ms = "2026-07-10T20:14:41.477Z", + iso_sec = "2026-07-10T20:14:41Z", + unix_ms = 1783714481477, + unix_sec = 1783714481, + }, + { + iso_ms = "1930-11-18T00:28:29.123Z", + iso_sec = "1930-11-18T00:28:29Z", + unix_ms = -1234567890877, + unix_sec = -1234567890, + }, +} +local specs_sec = { + { + iso_ms = "2026-07-10T20:14:41.000Z", + iso_sec = "2026-07-10T20:14:41Z", + unix_ms = 1783714481000, + unix_sec = 1783714481, + }, + { + iso_ms = "1930-11-18T00:28:29.000Z", + iso_sec = "1930-11-18T00:28:29Z", + unix_ms = -1234567891000, + unix_sec = -1234567891, + }, +} + +local tests = { + iso_string_invalid = function() + local dt = c2.DateTime.from_iso_string("invalid") + assert_eq(dt:to_unix_milliseconds(), 0) + assert_eq(dt:to_iso_string(), "") + end, + iso_string_ms = function() + for _, spec in pairs(specs_ms) do + local dt = c2.DateTime.from_iso_string(spec.iso_ms) + + assert_eq(dt, c2.DateTime.from_iso_string(spec.iso_ms)) + assert_eq(dt:to_iso_string(), tostring(dt)) + assert_eq(dt:to_iso_string(), spec.iso_ms) + assert_eq(dt:to_iso_string_without_ms(), spec.iso_sec) + assert_eq(dt:to_unix_milliseconds(), spec.unix_ms) + assert_eq(dt:to_unix_seconds(), spec.unix_sec) + end + end, + iso_string_sec = function() + for _, spec in pairs(specs_sec) do + local dt = c2.DateTime.from_iso_string(spec.iso_sec) + + assert_eq(dt, c2.DateTime.from_iso_string(spec.iso_ms)) + assert_eq(dt, c2.DateTime.from_iso_string(spec.iso_sec)) + assert_eq(dt:to_iso_string(), tostring(dt)) + assert_eq(dt:to_iso_string(), spec.iso_ms) + assert_eq(dt:to_iso_string_without_ms(), spec.iso_sec) + assert_eq(dt:to_unix_milliseconds(), spec.unix_ms) + assert_eq(dt:to_unix_seconds(), spec.unix_sec) + end + end, + + current_time = function() + local dt = c2.DateTime.current_local() + -- We don't know what it should be, but we know that it must be valid. + assert(dt:to_iso_string() ~= "") + dt = c2.DateTime.current_utc() + assert(dt:to_iso_string() ~= "") + end, + + unix_timestamp_ms = function() + for _, spec in pairs(specs_ms) do + local dt = c2.DateTime.from_unix_milliseconds(spec.unix_ms) + assert_eq(dt, c2.DateTime.from_unix_milliseconds(spec.unix_ms)) + assert_eq(dt:to_unix_milliseconds(), spec.unix_ms) + assert_eq(dt:to_unix_seconds(), spec.unix_sec) + + -- FIXME: Check the ISO string here, but it's in the local time zone right now. + -- assert_eq(dt, c2.DateTime.from_iso_string(spec.iso_ms)) + -- assert_eq(dt:to_iso_string(), spec.iso_ms) + -- assert_eq(dt:to_iso_string_without_ms(), spec.iso_sec) + end + end, + unix_timestamp_sec = function() + for _, spec in pairs(specs_sec) do + -- without milliseconds + local dt = c2.DateTime.from_unix_seconds(spec.unix_sec) + assert_eq(dt, c2.DateTime.from_unix_seconds(spec.unix_sec)) + assert_eq(dt, c2.DateTime.from_unix_milliseconds(spec.unix_ms)) + assert_eq(dt:to_unix_milliseconds(), spec.unix_ms) + assert_eq(dt:to_unix_seconds(), spec.unix_sec) + end + end, +} + +for name, fn in pairs(tests) do + local ok, res = xpcall(fn, debug.traceback) + if not ok then + error(name .. " failed: " .. res) + end +end diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp index fe4b799db..057494589 100644 --- a/tests/src/Plugins.cpp +++ b/tests/src/Plugins.cpp @@ -1698,6 +1698,19 @@ TEST_F(PluginImageTest, NoPerms) INSTANTIATE_TEST_SUITE_P(PluginImage, PluginImageTest, testing::ValuesIn(discoverLuaTests("images"))); +class PluginDateTimeTest : public PluginTest, + public ::testing::WithParamInterface +{ +}; +TEST_P(PluginDateTimeTest, Run) +{ + this->configure(); + runLuaTest("datetime", GetParam(), *this->lua); +} + +INSTANTIATE_TEST_SUITE_P(PluginChannel, PluginDateTimeTest, + testing::ValuesIn(discoverLuaTests("datetime"))); + // verify that all snapshots are included TEST(PluginMessageConstructionTest, Integrity) {