diff --git a/src/main.cpp b/src/main.cpp index bec76a6..e767e5e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,9 @@ #include "nocal/storage/ics_store.hpp" #include "nocal/storage/sync_cache.hpp" #include "nocal/sync/curl_http.hpp" +#include "nocal/sync/caldav_account.hpp" +#include "nocal/sync/caldav_sync.hpp" +#include "nocal/sync/caldav_sync_provider.hpp" #include "nocal/sync/desktop_oauth.hpp" #include "nocal/sync/microsoft_account.hpp" #include "nocal/sync/microsoft_sync_provider.hpp" @@ -101,8 +104,8 @@ int connect_microsoft_account() } int disconnect_account_command(nocal::storage::SyncCache& cache, - nocal::sync::OAuthSecretStore& secret_store, - std::string_view account_id) + nocal::sync::OAuthSecretStore& secret_store, + std::string_view account_id) { // Verify account exists in cache const auto snapshot = cache.snapshot(); @@ -111,8 +114,9 @@ int disconnect_account_command(nocal::storage::SyncCache& cache, if (it == snapshot.accounts.end()) { throw std::invalid_argument("unknown account: " + std::string{account_id}); } - // Erase from secret store + // Erase from secret store (OAuth tokens and/or CalDAV credentials) secret_store.erase(std::string{account_id}); + secret_store.erase_caldav_credential(std::string{account_id}); // Note: cached account/calendar rows remain in the database. // This matches the SyncProvider contract: cached provider data is left // in place so that reconnecting the same account is idempotent. @@ -134,12 +138,92 @@ int list_accounts_command(const nocal::storage::SyncCache& cache) return EXIT_SUCCESS; } +int connect_caldav_account(int argc, char** argv) +{ + std::string server_url; + std::string username; + std::string password; + + // Parse arguments: --server URL --user USERNAME --pass PASSWORD + for (int i = 1; i < argc; ++i) { + const std::string_view arg = argv[i]; + if (arg == "--server" && i + 1 < argc) { + server_url = argv[++i]; + } else if (arg == "--user" && i + 1 < argc) { + username = argv[++i]; + } else if (arg == "--pass" && i + 1 < argc) { + password = argv[++i]; + } else { + throw std::invalid_argument("unknown CalDAV argument: " + std::string{arg}); + } + } + + if (server_url.empty() || username.empty() || password.empty()) { + throw std::invalid_argument( + "usage: nocal account connect caldav --server URL --user USERNAME --pass PASSWORD\n" + " --server CalDAV server URL (e.g. https://cloud.example.com/remote.php/dav)\n" + " --user CalDAV username\n" + " --pass CalDAV password (or 'stdin' to read from stdin)"); + } + + // Read password from stdin if requested + if (password == "stdin") { + std::string line; + if (!std::getline(std::cin, line)) { + throw std::invalid_argument("failed to read password from stdin"); + } + password = line; + } + + const std::filesystem::path cache_path = default_sync_cache_path(); + std::error_code error; + std::filesystem::create_directories(cache_path.parent_path(), error); + if (error) { + throw std::runtime_error("cannot create " + cache_path.parent_path().string() + + ": " + error.message()); + } + nocal::storage::SyncCache cache(cache_path); + nocal::sync::CurlHttpTransport transport; + nocal::sync::LibsecretOAuthSecretStore secrets; + + // Store credentials in Secret Service + nocal::sync::CalDAVCredential credential{username, password}; + const std::string account_id = nocal::sync::caldav_account_id(server_url, username); + secrets.store_caldav_credential(account_id, credential); + + // Verify connectivity by discovering principal and calendar-home-set + nocal::sync::CalDAVAccountConfig account_config; + account_config.server_url = server_url; + account_config.account_id = account_id; + account_config.username = username; + account_config.password = password; + account_config.display_name = username + "@" + server_url; + + nocal::sync::CalDAVSync sync(transport, cache, account_config); + sync.discover(); + + // Upsert account in cache + nocal::storage::CachedAccount cached_account; + cached_account.id = account_id; + cached_account.provider = "caldav"; + cached_account.remote_subject = server_url; + cached_account.display_name = account_config.display_name; + cache.upsert_account(cached_account); + + std::cout << "Connected " << cached_account.display_name << " (" << account_id << ")\n"; + return std::cout ? EXIT_SUCCESS : EXIT_FAILURE; +} + int account_command(int argc, char** argv) { if (argc == 3 && std::string_view{argv[1]} == "connect" && std::string_view{argv[2]} == "microsoft") { return connect_microsoft_account(); } + if (argc >= 3 && std::string_view{argv[1]} == "connect" && + std::string_view{argv[2]} == "caldav") { + return connect_caldav_account(argc - 2, argv + 2); + } if (argc == 3 && std::string_view{argv[1]} == "disconnect") { const std::filesystem::path cache_path = default_sync_cache_path(); std::error_code error; @@ -165,6 +249,7 @@ int account_command(int argc, char** argv) } throw std::invalid_argument( "usage: nocal account connect microsoft\n" + " nocal account connect caldav --server URL --user USERNAME --pass PASSWORD\n" " nocal account disconnect \n" " nocal account list"); } @@ -185,12 +270,14 @@ void print_help(std::ostream& output) " -h, --help show this help\n" " -V, --version show the version\n\n" "Accounts: nocal account connect microsoft\n" - " links a Microsoft account (browser sign-in, tokens stay in\n" - " the Secret Service keyring) and exits.\n" - " nocal account disconnect \n" - " removes credentials for a connected account.\n" - " nocal account list\n" - " lists connected accounts.\n\n" + " links a Microsoft account (browser sign-in, tokens stay in\n" + " the Secret Service keyring) and exits.\n" + " nocal account connect caldav --server URL --user USERNAME --pass PASSWORD\n" + " links a CalDAV account (Nextcloud, Radicale, etc.) and exits.\n" + " nocal account disconnect \n" + " removes credentials for a connected account.\n" + " nocal account list\n" + " lists connected accounts.\n\n" "Keys: arrows/hjkl move days, PageUp/PageDown or p/n change month,\n" " Tab/Shift-Tab focus appointments, Enter reads, Esc returns,\n" " a adds, e edits, d deletes; Ctrl-S saves inside the editor,\n" @@ -511,36 +598,49 @@ int main(int argc, char** argv) if (!sync_snapshot.accounts.empty()) { app.set_connected_account_id(sync_snapshot.accounts.front().id); - const std::string client_id = nocal::sync::default_microsoft_client_id(); - if (client_id != nocal::sync::microsoft_client_id_placeholder) { - nocal::sync::XdgBrowserLauncher browser; - nocal::sync::LoopbackCallbackReceiver callback_receiver; - nocal::sync::SystemEntropySource entropy; + const std::string& provider = sync_snapshot.accounts.front().provider; + if (provider == "microsoft-graph") { + const std::string client_id = nocal::sync::default_microsoft_client_id(); + if (client_id != nocal::sync::microsoft_client_id_placeholder) { + nocal::sync::XdgBrowserLauncher browser; + nocal::sync::LoopbackCallbackReceiver callback_receiver; + nocal::sync::SystemEntropySource entropy; - nocal::sync::OAuthTokenBroker token_broker( - nocal::sync::microsoft_oauth_config(client_id), - http_transport, - secret_store, - [] { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - }); + nocal::sync::OAuthTokenBroker token_broker( + nocal::sync::microsoft_oauth_config(client_id), + http_transport, + secret_store, + [] { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + }); - auto sync_prov = std::make_unique( - token_broker, - secret_store, - sync_cache, - http_transport, - nocal::sync::microsoft_oauth_config(client_id), - [] { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - }, - browser, - callback_receiver, - entropy); + auto sync_prov = std::make_unique( + token_broker, + secret_store, + sync_cache, + http_transport, + nocal::sync::microsoft_oauth_config(client_id), + [] { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + }, + browser, + callback_receiver, + entropy); + + app.set_sync_provider(std::move(sync_prov)); + } + } else if (provider == "caldav") { + nocal::sync::CalDAVServerConfig server_config; + server_config.server_url = sync_snapshot.accounts.front().remote_subject; + server_config.account_id = sync_snapshot.accounts.front().id; + server_config.display_name = sync_snapshot.accounts.front().display_name; + + auto sync_prov = std::make_unique( + http_transport, sync_cache, secret_store, std::move(server_config)); app.set_sync_provider(std::move(sync_prov)); }