1use crate::llm::actions::{
2 client_trait::{Client, ClientActionResult},
3 protocol_trait::Protocol,
4 ActionDefinition, ParameterDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::dav_common::actions::{action, parameter};
8use crate::server::dav_common::{client as engine, server::Kind};
9use anyhow::Result;
10use serde_json::{json, Value};
11use std::sync::LazyLock;
12
13#[derive(Default)]
14pub struct CalDavClientProtocol;
15impl CalDavClientProtocol {
16 pub const fn new() -> Self {
17 Self
18 }
19}
20
21pub static PROTOCOL: CalDavClientProtocol = CalDavClientProtocol;
22
23fn coll() -> crate::llm::actions::Parameter {
24 parameter(
25 "collection",
26 "string",
27 "Calendar name under the home (as listed on connect) or its absolute path",
28 true,
29 )
30}
31fn name() -> crate::llm::actions::Parameter {
32 parameter("name", "string", "Object file name, e.g. standup.ics", true)
33}
34fn actions() -> Vec<ActionDefinition> {
35 vec![
36 action("caldav_list", "List a calendar's objects with their ETags (PROPFIND Depth 1)", vec![coll()], json!({"type":"caldav_list","collection":"work"})),
37 action("caldav_get", "Fetch one object (GET)", vec![coll(), name()], json!({"type":"caldav_get","collection":"work","name":"standup.ics"})),
38 action(
39 "caldav_put",
40 "Create or replace an object (PUT); Rust checks the iCalendar text before sending",
41 vec![coll(), name(), parameter("data", "string", "iCalendar text, e.g. BEGIN:VCALENDAR … BEGIN:VEVENT … UID … DTSTART … END:VEVENT … END:VCALENDAR", true), parameter("if_match", "string", "ETag the object must still have (update without overwriting someone else's change)", false), parameter("create_only", "boolean", "Send If-None-Match: * so an existing object is not overwritten", false)],
42 json!({"type":"caldav_put","collection":"work","name":"standup.ics","data":"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//NetGet//EN\r\nBEGIN:VEVENT\r\nUID:standup\r\nDTSTAMP:20261005T090000Z\r\nDTSTART:20261005T100000Z\r\nDTEND:20261005T101500Z\r\nSUMMARY:Standup\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n","create_only":true}),
43 ),
44 action("caldav_delete", "Delete an object, or the whole calendar when name is omitted", vec![coll(), parameter("name", "string", "Object file name; omit to delete the calendar", false), parameter("if_match", "string", "ETag the object must still have", false)], json!({"type":"caldav_delete","collection":"work","name":"standup.ics"})),
45 action("caldav_query", "Find objects overlapping a time range (calendar-query REPORT); data comes back with them", vec![coll(), parameter("start", "string", "UTC start like 20261005T000000Z", false), parameter("end", "string", "UTC end like 20261012T000000Z", false), parameter("component", "string", "VEVENT (default) or VTODO", false)], json!({"type":"caldav_query","collection":"work","start":"20261005T000000Z","end":"20261012T000000Z"})),
46 action("caldav_make_collection", "Create a calendar under the home (MKCALENDAR)", vec![coll(), parameter("displayname", "string", "Display name for the new calendar", false)], json!({"type":"caldav_make_collection","collection":"work2","displayname":"Second"})),
47 action("disconnect", "Stop this CalDAV client", vec![], json!({"type":"disconnect"})),
48 ]
49}
50
51pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
52 EventType::new(
53 "caldav_connected",
54 "Discovery finished: the principal, the home and its calendars",
55 json!({"type":"caldav_list","collection":"work"}),
56 )
57 .with_parameters(vec![
58 parameter(
59 "principal",
60 "string",
61 "Path of the current user's principal",
62 true,
63 ),
64 parameter("home", "string", "Path of the calendar home", true),
65 parameter(
66 "collections",
67 "array",
68 "[{href, name, displayname, ctag}] for every calendar in the home",
69 true,
70 ),
71 ])
72 .with_actions(actions())
73});
74pub static RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
75 EventType::new(
76 "caldav_response",
77 "The server's answer to the last operation",
78 json!({"type":"disconnect"}),
79 )
80 .with_parameters(vec![
81 parameter(
82 "operation",
83 "string",
84 "list, get, put, delete, query or make_collection",
85 true,
86 ),
87 parameter(
88 "status",
89 "number",
90 "HTTP status of the answer (207 for list and query)",
91 true,
92 ),
93 parameter(
94 "objects",
95 "array",
96 "For list and query: [{href, name, etag, data (query only)}]",
97 false,
98 ),
99 parameter("data", "string", "For get: the object text", false),
100 parameter(
101 "etag",
102 "string",
103 "For get and put: the object's entity tag",
104 false,
105 ),
106 parameter(
107 "error",
108 "string",
109 "For a refused put: the server's error body",
110 false,
111 ),
112 ])
113 .with_actions(actions())
114});
115
116impl Protocol for CalDavClientProtocol {
117 fn protocol_name(&self) -> &'static str {
118 "CalDAV"
119 }
120 fn stack_name(&self) -> &'static str {
121 "ETH>IP>TCP>HTTP>WebDAV>CalDAV"
122 }
123 fn keywords(&self) -> Vec<&'static str> {
124 vec!["caldav", "calendar client", "ical"]
125 }
126 fn description(&self) -> &'static str {
127 "CalDAV client: discovers the user's calendars, lists, queries, reads, writes and deletes objects"
128 }
129 fn get_async_actions(&self, _: &crate::state::app_state::AppState) -> Vec<ActionDefinition> {
130 actions()
131 }
132 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
133 vec![]
134 }
135 fn get_event_types(&self) -> Vec<EventType> {
136 vec![CONNECTED_EVENT.clone(), RESPONSE_EVENT.clone()]
137 }
138 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
139 let p =
140 |name: &str, kind: &str, description: &str, example: Value, default: Option<Value>| {
141 ParameterDefinition {
142 name: name.into(),
143 type_hint: kind.into(),
144 description: description.into(),
145 required: false,
146 example,
147 default,
148 }
149 };
150 vec![
151 p(
152 "scheme",
153 "string",
154 "URL scheme to reach the server with: https (default) or http",
155 json!("http"),
156 Some(json!(engine::DEFAULT_SCHEME)),
157 ),
158 p(
159 "username",
160 "string",
161 "User name for HTTP Basic authentication",
162 json!("alice"),
163 None,
164 ),
165 p(
166 "password",
167 "string",
168 "Password for HTTP Basic authentication",
169 json!("secret"),
170 None,
171 ),
172 ]
173 }
174 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
175 use crate::protocol::metadata::*;
176 ProtocolMetadataV2::builder()
177 .state(DevelopmentState::Experimental)
178 .privilege_requirement(PrivilegeRequirement::None)
179 .implementation("Shared http_fetch client (reqwest natively, redirects followed by hand for discovery only); RFC 6764 well-known discovery, PROPFIND, calendar-query REPORT, multistatus parsing, If-Match / If-None-Match, MKCALENDAR; objects validated with the server's parser before PUT")
180 .llm_control("Which calendars and objects to read, query, write or delete")
181 .e2e_testing("tests/client/caldav: Radicale 3.8.1 (independent) answers discovery, collection creation, PUT, list, query, GET, conditional PUT and DELETE")
182 .notes("No sync-collection or scheduling; one request at a time.")
183 .max_inbound_bytes(crate::server::dav_common::xml::MAX_BODY * 4)
184 .build()
185 }
186 fn example_prompt(&self) -> &'static str {
187 "List this week's events on the CalDAV server at 127.0.0.1:5232"
188 }
189 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
190 let llm = json!({"type":"open_client","protocol":"caldav","remote_addr":"127.0.0.1:5232","instruction":"List this week's meetings","startup_params":{"scheme":"http","username":"alice","password":"secret"}});
191 let mut static_example = llm.clone();
192 static_example["event_handlers"] = json!([
193 {"event_pattern":"caldav_connected","handler":{"type":"static","actions":[{"type":"caldav_list","collection":"work"}]}},
194 {"event_pattern":"caldav_response","handler":{"type":"static","actions":[]}}
195 ]);
196 let mut scripted = static_example.clone();
197 scripted["event_handlers"][1]["handler"] = json!({"type":"script","language":"python","code":"import json\nprint(json.dumps({'actions':[]}))"});
198 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
199 }
200 fn group_name(&self) -> &'static str {
201 "Web & File"
202 }
203}
204
205impl Client for CalDavClientProtocol {
206 fn connect(
207 &self,
208 ctx: ConnectContext,
209 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
210 {
211 Box::pin(engine::connect(ctx, &super::FLAVOR))
212 }
213 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
214 engine::check_action("caldav", Kind::Calendar, &v)
215 }
216}