Skip to main content

netget/client/carddav/
actions.rs

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 CardDavClientProtocol;
15impl CardDavClientProtocol {
16    pub const fn new() -> Self {
17        Self
18    }
19}
20
21pub static PROTOCOL: CardDavClientProtocol = CardDavClientProtocol;
22
23fn coll() -> crate::llm::actions::Parameter {
24    parameter(
25        "collection",
26        "string",
27        "Address book 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. ada.vcf", true)
33}
34fn actions() -> Vec<ActionDefinition> {
35    vec![
36        action("carddav_list", "List a address book's objects with their ETags (PROPFIND Depth 1)", vec![coll()], json!({"type":"carddav_list","collection":"contacts"})),
37        action("carddav_get", "Fetch one object (GET)", vec![coll(), name()], json!({"type":"carddav_get","collection":"contacts","name":"ada.vcf"})),
38        action(
39            "carddav_put",
40            "Create or replace an object (PUT); Rust checks the vCard text before sending",
41            vec![coll(), name(), parameter("data", "string", "vCard text, e.g. BEGIN:VCARD … VERSION:3.0 … UID … FN … END:VCARD", 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":"carddav_put","collection":"contacts","name":"ada.vcf","data":"BEGIN:VCARD\r\nVERSION:3.0\r\nUID:ada\r\nFN:Ada Lovelace\r\nEMAIL:ada@example.com\r\nEND:VCARD\r\n","create_only":true}),
43        ),
44        action("carddav_delete", "Delete an object, or the whole address book when name is omitted", vec![coll(), parameter("name", "string", "Object file name; omit to delete the address book", false), parameter("if_match", "string", "ETag the object must still have", false)], json!({"type":"carddav_delete","collection":"contacts","name":"ada.vcf"})),
45        action("carddav_query", "Find cards whose property matches text (addressbook-query REPORT); data comes back with them", vec![coll(), parameter("property", "string", "vCard property to match, e.g. FN or EMAIL (default FN)", false), parameter("text", "string", "Text to look for", true), parameter("match_type", "string", "contains (default), equals, starts-with or ends-with", false)], json!({"type":"carddav_query","collection":"contacts","property":"EMAIL","text":"example.com"})),
46        action("carddav_make_collection", "Create a address book under the home (extended MKCOL)", vec![coll(), parameter("displayname", "string", "Display name for the new address book", false)], json!({"type":"carddav_make_collection","collection":"contacts2","displayname":"Second"})),
47        action("disconnect", "Stop this CardDAV client", vec![], json!({"type":"disconnect"})),
48    ]
49}
50
51pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
52    EventType::new(
53        "carddav_connected",
54        "Discovery finished: the principal, the home and its address books",
55        json!({"type":"carddav_list","collection":"contacts"}),
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 address book home", true),
65        parameter(
66            "collections",
67            "array",
68            "[{href, name, displayname, ctag}] for every address book in the home",
69            true,
70        ),
71    ])
72    .with_actions(actions())
73});
74pub static RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
75    EventType::new(
76        "carddav_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 CardDavClientProtocol {
117    fn protocol_name(&self) -> &'static str {
118        "CardDAV"
119    }
120    fn stack_name(&self) -> &'static str {
121        "ETH>IP>TCP>HTTP>WebDAV>CardDAV"
122    }
123    fn keywords(&self) -> Vec<&'static str> {
124        vec!["carddav", "contacts client", "vcard"]
125    }
126    fn description(&self) -> &'static str {
127        "CardDAV client: discovers the user's address books, 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, addressbook-query REPORT, multistatus parsing, If-Match / If-None-Match, extended MKCOL; objects validated with the server's parser before PUT")
180            .llm_control("Which address books and objects to read, query, write or delete")
181            .e2e_testing("tests/client/carddav: 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        "Find Ada's email in the CardDAV address book 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":"carddav","remote_addr":"127.0.0.1:5232","instruction":"Find everyone at example.com","startup_params":{"scheme":"http","username":"alice","password":"secret"}});
191        let mut static_example = llm.clone();
192        static_example["event_handlers"] = json!([
193            {"event_pattern":"carddav_connected","handler":{"type":"static","actions":[{"type":"carddav_list","collection":"contacts"}]}},
194            {"event_pattern":"carddav_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 CardDavClientProtocol {
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("carddav", Kind::AddressBook, &v)
215    }
216}