From 2d9b8082946fd808932afe89340092c88f907495 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Thu, 28 Sep 2023 02:15:48 +0100 Subject: [PATCH] Foundational work for generic extension handling --- Cargo.lock | 24 ++++++++++++++++++++++++ jmap-proto/Cargo.toml | 1 + jogre-server/Cargo.toml | 1 + jmap-proto/src/errors.rs | 30 +++++++++++++++++++++++++----- jmap-proto/src/lib.rs | 2 ++ jogre-server/src/context.rs | 16 ++++++++++++++++ jogre-server/src/main.rs | 1 + jmap-proto/src/endpoints/mod.rs | 49 ++++++++++++++++++++++++++++++++++++------------- jmap-proto/src/endpoints/session.rs | 14 +++++--------- jmap-proto/src/extensions/js_contact.rs | 640 -------------------------------------------------------------------------------- jmap-proto/src/extensions/mod.rs | 3 ++- jmap-proto/src/extensions/sharing.rs | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jogre-server/src/context/oauth2.rs | 1 - jogre-server/src/extensions/contacts.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ jogre-server/src/extensions/core.rs | 35 +++++++++++++++++++++++++++++++++++ jogre-server/src/extensions/mod.rs | 158 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jogre-server/src/extensions/sharing.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jogre-server/src/methods/mod.rs | 7 ++++++- jogre-server/src/methods/session.rs | 21 +++++---------------- jmap-proto/src/extensions/contacts/js_contact.rs | 640 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ jmap-proto/src/extensions/contacts/mod.rs | 1 + jogre-server/src/methods/api/mod.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 22 files changed, 1304 insertions(+), 686 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 350a0c1..ce94e31 100644 --- a/Cargo.lock +++ a/Cargo.lock @@ -904,6 +904,7 @@ "serde", "serde_json", "serde_with", + "strum", ] [[package]] @@ -935,6 +936,7 @@ "rand", "rocksdb", "serde", + "serde_json", "sha3", "tokio", "toml", @@ -1651,6 +1653,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] [[package]] name = "subtle" diff --git a/jmap-proto/Cargo.toml b/jmap-proto/Cargo.toml index cdf8f8d..12558a8 100644 --- a/jmap-proto/Cargo.toml +++ a/jmap-proto/Cargo.toml @@ -10,3 +10,4 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = { version = "3.3", features = ["macros"] } +strum = { version = "0.25", features = ["derive"] } diff --git a/jogre-server/Cargo.toml b/jogre-server/Cargo.toml index 1bf6862..cea0972 100644 --- a/jogre-server/Cargo.toml +++ a/jogre-server/Cargo.toml @@ -31,4 +31,5 @@ url = { version = "2.4", features = ["serde"] } uuid = { version = "1.4", features = ["v4", "serde"] } serde = { version = "1.0.188", features = ["derive"] } +serde_json = "1.0" sha3 = "0.10" diff --git a/jmap-proto/src/errors.rs b/jmap-proto/src/errors.rs index 97c1e69..f0555c9 100644 --- a/jmap-proto/src/errors.rs +++ a/jmap-proto/src/errors.rs @@ -1,16 +1,19 @@ use std::{borrow::Cow, collections::HashMap}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use strum::Display; +use crate::endpoints::{Argument, Arguments, Invocation}; + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct RequestError { #[serde(rename = "type")] - type_: ProblemType, - status: u16, - detail: Cow<'static, str>, + pub type_: ProblemType, + pub status: u16, + pub detail: Cow<'static, str>, #[serde(flatten)] - meta: HashMap, + pub meta: HashMap, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -44,7 +47,8 @@ /// Any further method calls in the request MUST then be processed as /// normal. Errors at the method level MUST NOT generate an HTTP-level /// error. -#[derive(Serialize, Deserialize, Debug, Copy, Clone)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Display)] +#[serde(tag = "type")] pub enum MethodError { /// Some internal server resource was temporarily unavailable. /// @@ -86,4 +90,20 @@ /// This method modifies state, but the account is read-only (as returned on /// the corresponding Account object in the JMAP Session resource). AccountReadOnly, +} + +impl MethodError { + pub fn into_invocation(self, request_id: Cow<'_, str>) -> Invocation<'_> { + let mut arguments = Arguments::default(); + arguments.0.insert( + Cow::Borrowed("type"), + Argument::Absolute(Value::String(self.to_string())), + ); + + Invocation { + name: "error".into(), + arguments, + request_id, + } + } } diff --git a/jmap-proto/src/lib.rs b/jmap-proto/src/lib.rs index f89ca07..07b6dc0 100644 --- a/jmap-proto/src/lib.rs +++ a/jmap-proto/src/lib.rs @@ -1,6 +1,8 @@ pub mod common; pub mod endpoints; pub mod errors; pub mod events; pub mod extensions; pub(crate) mod util; + +pub use serde_json::Value; diff --git a/jogre-server/src/context.rs b/jogre-server/src/context.rs index bbf431a..0cde0da 100644 --- a/jogre-server/src/context.rs +++ a/jogre-server/src/context.rs @@ -1,7 +1,12 @@ use std::sync::Arc; use crate::{ config::{Config, CoreCapabilities}, + extensions, + extensions::{ + sharing::{Principals, PrincipalsOwner}, + ExtensionRegistry, + }, store::Store, }; @@ -12,6 +17,7 @@ pub store: Arc, pub base_url: url::Url, pub core_capabilities: CoreCapabilities, + pub extension_registry: ExtensionRegistry, } impl Context { @@ -19,11 +25,21 @@ let derived_keys = Arc::new(DerivedKeys::new(&config.private_key)); let store = Arc::new(Store::from_config(config.store)); + let extension_registry = ExtensionRegistry { + core: extensions::core::Core { + core_capabilities: config.core_capabilities, + }, + contacts: extensions::contacts::Contacts {}, + sharing_principals: Principals {}, + sharing_principals_owner: PrincipalsOwner {}, + }; + Self { oauth2: oauth2::OAuth2::new(store.clone(), derived_keys), store, base_url: config.base_url, core_capabilities: config.core_capabilities, + extension_registry, } } } diff --git a/jogre-server/src/main.rs b/jogre-server/src/main.rs index 2dd4f5b..f212c5e 100644 --- a/jogre-server/src/main.rs +++ a/jogre-server/src/main.rs @@ -1,8 +1,9 @@ #![deny(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] mod config; mod context; +mod extensions; mod layers; mod methods; mod store; diff --git a/jmap-proto/src/endpoints/mod.rs b/jmap-proto/src/endpoints/mod.rs index be81b4f..8670dc7 100644 --- a/jmap-proto/src/endpoints/mod.rs +++ a/jmap-proto/src/endpoints/mod.rs @@ -27,8 +27,31 @@ const REFERENCE_OCTOTHORPE: &str = "#"; #[derive(Debug, Clone, Default)] -pub struct Arguments<'a>(HashMap, Argument<'a>>); +pub struct Arguments<'a>(pub HashMap, Argument<'a>>); +impl Arguments<'_> { + /// Resolves a pointer, as defined in [RFC 6901] + /// + /// [RFC 6901]: https://datatracker.ietf.org/doc/html/rfc6901 + pub fn pointer(&self, pointer: &str) -> Option> { + if pointer.is_empty() { + return Some(Cow::Owned(serde_json::to_value(self).unwrap())); + } + + let pointer = pointer.strip_prefix('/')?; + + let mut pointer = pointer.splitn(2, pointer); + + if let Argument::Absolute(value) = self.0.get(pointer.next()?)? { + value + .pointer(pointer.next().unwrap_or("")) + .map(Cow::Borrowed) + } else { + None + } + } +} + impl<'a> Serialize for Arguments<'a> { fn serialize(&self, serializer: S) -> Result where @@ -101,15 +124,15 @@ /// The method call id (see Section 3.2) of a previous method call in /// the current request. #[serde(borrow)] - result_of: Cow<'a, str>, + pub result_of: Cow<'a, str>, /// The required name of a response to that method call. #[serde(borrow)] - name: Cow<'a, str>, + pub name: Cow<'a, str>, /// A pointer into the arguments of the response selected via the name /// and resultOf properties. This is a JSON Pointer [RFC6901], except /// it also allows the use of "*" to map through an array. #[serde(borrow)] - path: Cow<'a, str>, + pub path: Cow<'a, str>, } /// Method calls and responses are represented by the *Invocation* data @@ -118,16 +141,16 @@ #[derive(Clone, Debug)] pub struct Invocation<'a> { /// A "String" *name* of the method to call or of the response. - name: Cow<'a, str>, + pub name: Cow<'a, str>, /// A "String[*]" object containing named *arguments* for that method /// or response. - arguments: Arguments<'a>, + pub arguments: Arguments<'a>, /// A "String" *method call id*: an arbitrary string from the client /// to be echoed back with the responses emitted by that method call /// (a method may return 1 or more responses, as it may make implicit /// calls to other methods; all responses initiated by this method /// call get the same method call id in the response). - request_id: Cow<'a, str>, + pub request_id: Cow<'a, str>, } impl<'a> Serialize for Invocation<'a> { @@ -195,11 +218,11 @@ /// of specifications it supports in the Session object (see /// Section 2), as keys on the "capabilities" property. #[serde_as(as = "Vec")] - using: Vec>, + pub using: Vec>, /// An array of method calls to process on the server. The method /// calls MUST be processed sequentially, in order. #[serde(borrow)] - method_calls: Vec>, + pub method_calls: Vec>, /// A map of a (client-specified) creation id to the id the server /// assigned when a record was successfully created. /// @@ -210,7 +233,7 @@ /// specify the creation id it assigned, prefixed with a "#" (see /// Section 5.3 for more details). #[serde(borrow)] - created_ids: Option, Id<'a>>>, + pub created_ids: Option, Id<'a>>>, } #[serde_as] @@ -222,16 +245,16 @@ /// the "methodResponses" array in the same order that the methods are /// processed. #[serde(borrow)] - method_responses: Invocation<'a>, + pub method_responses: Vec>, /// A map of a (client-specified) creation id to the id the server /// assigned when a record was successfully created. This MUST /// include all creation ids passed in the original createdIds /// parameter of the Request object, as well as any additional ones /// added for newly created records. #[serde(borrow)] - created_ids: Option, Id<'a>>>, + pub created_ids: Option, Id<'a>>>, /// The current value of the "state" string on the Session object, as /// described in Section 2. Clients may use this to detect if this /// object has changed and needs to be refetched. - session_state: SessionState<'a>, + pub session_state: SessionState<'a>, } diff --git a/jmap-proto/src/endpoints/session.rs b/jmap-proto/src/endpoints/session.rs index 01d3725..5be1d96 100644 --- a/jmap-proto/src/endpoints/session.rs +++ a/jmap-proto/src/endpoints/session.rs @@ -1,9 +1,10 @@ use std::{ borrow::Cow, collections::{BTreeSet, HashMap}, }; use serde::{Deserialize, Serialize}; +use serde_json::Value; use serde_with::{serde_as, BorrowCow}; use crate::common::{Id, SessionState, UnsignedInt}; @@ -24,8 +25,11 @@ /// a URI for a capability supported by the server. The value for /// each of these keys is an object with further information about the /// server's capabilities in relation to that capability. + /// + /// The capabilities object MUST include a property called + /// "urn:ietf:params:jmap:core". #[serde(borrow)] - pub capabilities: ServerCapabilities<'a>, + pub capabilities: HashMap, Value>, /// A map of an account id to an Account object for each account (see /// Section 1.6.2) the user has access to. #[serde(borrow)] @@ -76,14 +80,6 @@ /// need to refetch the object. #[serde(borrow)] pub state: SessionState<'a>, -} - -#[derive(Deserialize, Serialize, Clone, Debug)] -pub struct ServerCapabilities<'a> { - /// The capabilities object MUST include a property called - /// "urn:ietf:params:jmap:core". - #[serde(rename = "urn:ietf:params:jmap:core", borrow)] - pub core: CoreCapability<'a>, } #[serde_as] diff --git a/jmap-proto/src/extensions/js_contact.rs b/jmap-proto/src/extensions/js_contact.rs deleted file mode 100644 index 56646be..0000000 100644 --- a/jmap-proto/src/extensions/js_contact.rs +++ /dev/null @@ -1,640 +1,0 @@ -use std::{borrow::Cow, collections::HashMap}; - -use chrono::NaiveDate; -use serde::{ - ser::SerializeMap, Deserialize, Serialize, Serializer, __private::ser::FlatMapSerializer, -}; -use serde_json::Value; - -use crate::common::{Id, UnsignedInt, UtcDate}; - -#[derive(Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] -pub struct TypeWrapper(T); - -impl Serialize for TypeWrapper { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(None)?; - map.serialize_entry("@type", T::KIND)?; - self.0.serialize(FlatMapSerializer(&mut map))?; - map.end() - } -} - -pub trait TypedStruct { - const KIND: &'static str; -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "PascalCase", tag = "@type")] -pub enum Data<'a> { - Card(#[serde(borrow)] Card<'a>), - CardGroup(CardGroup<'a>), -} - -/// A CardGroup object represents a group of cards. Its members may be Cards or CardGroups. -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct CardGroup<'a> { - /// An identifier, used to associate the object as the same across different - /// systems, addressbooks and views. - #[serde(borrow)] - uid: Id<'a>, - /// The set is represented as an object, with each key being the uid of another Card or - /// CardGroup. The value for each key in the object MUST be true. - members: HashMap, bool>, - /// The user-visible name for the group, e.g. "Friends". This may be any UTF-8 string of at - /// least 1 character in length and maximum 255 octets in size. The same name may be used by - /// two different groups. - #[serde(default)] - name: Cow<'a, str>, - /// The card that represents this group. - card: Option>, -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -#[serde(rename_all = "camelCase")] -pub struct Card<'a> { - /// An identifier, used to associate the object as the same across different - /// systems, addressbooks and views. - #[serde(borrow)] - uid: Id<'a>, - /// The identifier for the product that created the Card object. - prod_id: Option>, - /// The date and time when this Card object was created. - created: Option, - /// The date and time when the data in this Card object was last modified. - updated: Option, - /// The kind of the entity the Card represents. - kind: Option, - /// Relates the object to other Card and CardGroup objects. This is - /// represented as a map, where each key is the - #[serde(default)] - related_to: HashMap, TypeWrapper>, - /// Language used for free-form text on this card. - language: Option>, - /// The name components of the name of the entity represented by this Card. - #[serde(default)] - name: Vec>>, - /// The full name (e.g. the personal name and surname of an individual, the - /// name of an organization) of the entity represented by this card. The - /// purpose of this property is to define a name, even if the individual - /// name components are not known. In addition, it is meant to provide - /// alternative versions of the name for internationalisation. - /// - /// Implementations SHOULD prefer using the name property over this one - /// and SHOULD NOT store the concatenated name component values in this - /// property. - #[serde(default)] - full_name: Cow<'a, str>, - /// The nick names of the entity represented by this card. - #[serde(default)] - nick_names: Vec>, - /// The companies or organization names and units associated with this - /// card. - #[serde(default)] - organizations: HashMap, TypeWrapper>>, - /// The job titles or functional positions of the entity represented by - /// this card. - #[serde(default)] - titles: HashMap, TypeWrapper>>, - /// The email addresses to contact the entity represented by this card. - #[serde(default)] - emails: HashMap, TypeWrapper>>, - /// The phone numbers to contact the entity represented by this card. - #[serde(default)] - phones: HashMap, TypeWrapper>>, - /// The online resources and services that are associated with the entity - /// represented by this card. - #[serde(default)] - online: HashMap, TypeWrapper>>, - /// A map of photo ids to File objects that contain photographs or images - /// associated with this card. A typical use case is to include an avatar for display along the - /// contact name. - #[serde(default)] - photos: HashMap, TypeWrapper>>, - /// Defines the preferred method to contact the holder of this card. - preferred_contact_method: Option, - /// Defines the preferred languages for contacting the entity associated with this card. The - /// keys in the object MUST be [RFC5646] language tags. The values are a (possibly empty) list - /// of contact language preferences for this language. A valid ContactLanguage object MUST have - /// at least one of its properties set. - #[serde(default)] - preferred_contact_languages: HashMap>, - /// A map of address ids to Address objects, containing physical locations. - #[serde(default)] - address: HashMap, TypeWrapper>>, - /// A map of language tags [RFC5646] to patches, which localize a property value into the - /// locale of the respective language tag. - /// - /// A patch MUST NOT target the localizations property. - #[serde(default)] - localizations: HashMap, Value>, - /// These are memorable dates and events for the entity represented by this card. - #[serde(default)] - anniversaries: HashMap, TypeWrapper>>, - /// Defines personal information about the entity represented by this card. - #[serde(default)] - personal_info: HashMap, TypeWrapper>>, - /// Arbitrary notes about the entity represented by this card. - #[serde(default)] - notes: Cow<'a, str>, - /// The set of free-text or URI categories that relate to the card. The set is represented as - /// an object, with each key being a category. The value for each key in the object MUST be - /// true. - #[serde(default)] - categories: HashMap, bool>, - /// Maps identifiers of custom time zones to their time zone definitions. For a description of - /// this property see the timeZones property definition in [RFC8984]. - #[serde(default)] - time_zones: HashMap, Value>, -} - -/// Defines personal information about the entity represented by this card. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct PersonalInfo<'a> { - /// Specifies the type for this personal information. - #[serde(rename = "type")] - type_: PersonalInfoType, - /// The actual information. This generally is free-text, but future - /// specifications MAY restrict allowed values depending on the type of - /// this PersonalInformation. - value: Cow<'a, str>, - /// Indicates the level of expertise, or engagement in hobby or interest. - level: Option, -} - -impl TypedStruct for PersonalInfo<'_> { - const KIND: &'static str = "PersonalInfo"; -} - -/// Specifies the type for this personal information. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum PersonalInfoType { - /// A field of expertise or credential - Expertise, - /// A hobby - Hobby, - /// An interest - Interest, - /// An information not covered by the above categories - Other, -} - -/// Indicates the level of expertise, or engagement in hobby or interest. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum PersonalInfoLevel { - High, - Medium, - Low, -} - -/// These are memorable dates and events for the entity represented by this card. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct Anniversary<'a> { - /// Specifies the type of the anniversary. - #[serde(rename = "type")] - type_: AnniversaryType, - /// A label describing the value in more detail, especially if the type - /// property has value other (but MAY be included with any type). - #[serde(default)] - label: Cow<'a, str>, - /// The date of this anniversary, in the form "YYYY-MM-DD" - /// (any part may be all 0s for unknown) or a [RFC3339] timestamp. - date: NaiveDate, - /// An address associated with this anniversary, e.g. the place of birth or - /// death. - place: Option>, -} - -impl TypedStruct for Anniversary<'_> { - const KIND: &'static str = "Anniversary"; -} - -/// Specifies the type of the anniversary. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum AnniversaryType { - /// A birth day anniversary - Birth, - /// A death day anniversary - Death, - /// An anniversary not covered by any of the known types. - Other, -} - -/// A physical location. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct Address<'a> { - /// The complete address, excluding type and label. This property is mainly useful to - /// represent addresses of which the individual address components are unknown, or to provide - /// localized representations. - #[serde(default)] - full_address: Cow<'a, str>, - /// The street address. The concatenation of the component values, separated by whitespace, - /// SHOULD result in a valid street address for the address locale. Doing so, implementations - /// MAY ignore any separator components. The StreetComponent object type is defined in the - /// paragraph below. - #[serde(default)] - street: Vec>>, - /// The city, town, village, post town, or other locality within which the street address may - /// be found. - #[serde(default)] - locality: Cow<'a, str>, - /// The province, such as a state, county, or canton within which the locality may be found. - #[serde(default)] - region: Cow<'a, str>, - /// The country name. - #[serde(default)] - country: Cow<'a, str>, - /// The postal code, post code, ZIP code or other short code associated with the address by the - /// relevant country's postal system. - #[serde(default)] - postcode: Cow<'a, str>, - /// The ISO-3166-1 country code. - #[serde(default)] - country_code: Cow<'a, str>, - /// A [RFC5870] "geo:" URI for the address. - #[serde(default)] - coordinates: Cow<'a, str>, - /// Identifies the time zone this address is located in. This either MUST be a time zone name - /// registered in the IANA Time Zone Database, or it MUST be a valid TimeZoneId as defined in - /// [RFC8984]. For the latter, a corresponding time zone MUST be defined in the timeZones - /// property. - #[serde(default)] - time_zone: Cow<'a, str>, - /// The contexts of the address information. - #[serde(default)] - context: HashMap, - /// A label describing the value in more detail. - #[serde(default)] - label: Cow<'a, str>, - /// The preference of this address in relation to other addresses. - pref: Option, -} - -impl TypedStruct for Address<'_> { - const KIND: &'static str = "Address"; -} - -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[serde(rename_all = "camelCase", untagged)] -pub enum AddressContext { - /// An address to be used for billing. - Billing, - /// An address to be used for delivering physical items - Postal, - /// A normal context - Other(Context), -} - -/// The street address. The concatenation of the component values, separated by whitespace, SHOULD -/// result in a valid street address for the address locale. Doing so, implementations MAY ignore -/// any separator components. The StreetComponent object type is defined in the paragraph below. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct StreetComponent<'a> { - /// The type of this street component. - #[serde(rename = "type")] - type_: StreetComponentKind, - /// The value of this street component. - value: Cow<'a, str>, -} - -impl TypedStruct for StreetComponent<'_> { - const KIND: &'static str = "StreetComponent"; -} - -/// The type of this street component. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum StreetComponentKind { - Name, - Number, - Apartment, - Room, - Extension, - Direction, - Building, - Floor, - PostOfficeBox, - Separator, - Unknown, -} - -/// Defines the preferred method to contact the holder of this card. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct ContactLanguage { - /// Defines the context in which to use this language. - context: Option, - /// Defines the preference of this language in relation to other - /// languages of the same context. - pref: Option, -} - -impl TypedStruct for ContactLanguage { - const KIND: &'static str = "ContactLanguage"; -} - -/// Defines the preferred method to contact the holder of this card. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum PreferredContactMethod { - Emails, - Phones, - Online, -} - -/// A map of photo ids to File objects that contain photographs or images -/// associated with this card. A typical use case is to include an avatar for display along the -/// contact name. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct File<'a> { - /// A URI where to fetch the data of this file. - href: Cow<'a, str>, - /// The content-type of the file, if known. - media_type: Cow<'a, str>, - /// The size, in octets, of the file when fully decoded (i.e., the number - /// of octets in the file the user would download), if known. - size: Option, - /// The preference of this photo in relation to other photos. - pref: Option, -} - -impl TypedStruct for File<'_> { - const KIND: &'static str = "File"; -} - -/// The online resources and services that are associated with the entity -/// represented by this card. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct Resource<'a> { - /// resource value, where the allowed value form is defined by the the type - /// property. In any case the value MUST NOT be empty. - resource: Cow<'a, str>, - /// The type of the resource value. - #[serde(rename = "type")] - type_: ResourceType, - /// Used for URI resource values. Provides the media type [RFC2046] of the - /// resource identified by the URI. - #[serde(default)] - media_type: Cow<'a, str>, - /// The contexts in which to use this resource. - #[serde(default)] - context: HashMap, - /// A label describing the value in more detail, especially if the type - /// property has value other (but MAY be included with any type). - #[serde(default)] - label: Cow<'a, str>, - /// The preference of this resource in relation to other resources. - pref: Option, -} - -impl TypedStruct for Resource<'_> { - const KIND: &'static str = "Resource"; -} - -/// The type of the resource value. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub enum ResourceType { - /// The resource value is a URI, e.g. a website link. This MUST be a valid URI as defined in - /// Section 3 of [RFC3986] and updates. - Uri, - /// The resource value is a username associated with the entity represented by this card (e.g. - /// for social media, or an IM client). The label property SHOULD be included to identify what - /// service this is for. For compatibility between clients, this label SHOULD be the canonical - /// service name, including capitalisation. e.g. Twitter, Facebook, Skype, GitHub, XMPP. The - /// resource value may be any non-empty free text. - Username, - /// The resource value is something else not covered by the above categories. A label property - /// MAY be included to display next to the number to help the user identify its purpose. The - /// resource value may be any non-empty free text. - Other, -} - -/// The phone numbers to contact the entity represented by this card. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct Phone<'a> { - /// The phone value, as either a URI or a free-text phone number. Typical - /// URI schemes are the [RFC3966] tel or [RFC3261] sip schemes, but any - /// URI scheme is allowed. - phone: Cow<'a, str>, - /// The set of contact features that this phone number may be used for. The - /// set is represented as an object, with each key being a method type. The - /// value for each key in the object MUST be true. - #[serde(default)] - features: HashMap, - // The contexts in which to use this number. The value for each - /// key in the object MUST be true. - #[serde(default)] - contexts: HashMap, - /// A label describing the value in more detail, especially if the type - /// property has value other (but MAY be included with any type). - #[serde(default)] - label: Cow<'a, str>, - /// The preference of this email address in relation to other email addresses. - pref: Option, -} - -impl TypedStruct for Phone<'_> { - const KIND: &'static str = "Phone"; -} - -/// The email addresses to contact the entity represented by this card. -#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)] -#[serde(rename_all = "kebab-case")] -pub enum PhoneFeature { - /// The number is for calling by voice. - Voice, - /// The number is for sending faxes. - Fax, - /// The number is for a pager or beeper. - Pager, - /// The number supports text messages (SMS). - Text, - /// The number is for a cell phone. - Cell, - /// The number is for a device for people with hearing or speech difficulties. - Textphone, - /// The number supports video conferencing. - Video, - /// The number is for some other purpose. The label property MAY be included - /// to display next to the number to help the user identify its purpose. - Other, -} - -/// The email addresses to contact the entity represented by this card. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct EmailAddress<'a> { - /// The email address. This MUST be an addr-spec value as defined in - /// Section 3.4.1 of [RFC5322]. - email: Cow<'a, str>, - /// The contexts in which to use this email address. The value for each - /// key in the object MUST be true. - #[serde(default)] - contexts: HashMap, - /// The preference of this email address in relation to other email addresses. - pref: Option, -} - -impl TypedStruct for EmailAddress<'_> { - const KIND: &'static str = "EmailAddress"; -} - -/// This data type allows to define a preference order on same-typed contact -/// information. For example, a card holder may have two email addresses and -/// prefer to be contacted with one of them. -/// -/// A preference value MUST be an integer number in the range 1 and 100. Lower -/// values correspond to a higher level of preference, with 1 being most -/// preferred. If no preference is set, then the contact information MUST be -/// interpreted as being least preferred. -/// -/// Note that the preference only is defined in relation to contact information -/// of the same type. For example, the preference orders within emails and -/// phone numbers are indendepent of each other. Also note that the -/// preferredContactMethod property allows to define a preferred contact method -/// across method types. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -pub struct Preference(u8); - -/// The companies or organization names and units associated with this card. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct Title<'a> { - /// The name of this organization. - #[serde(borrow)] - name: Cow<'a, str>, - /// The id of the organization in which this title is held. - #[serde(default)] - organization: Vec>, -} - -impl TypedStruct for Title<'_> { - const KIND: &'static str = "Title"; -} - -/// The companies or organization names and units associated with this card. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct Organization<'a> { - /// The name of this organization. - name: Cow<'a, str>, - /// Additional levels of organizational unit names. - #[serde(default)] - units: Vec>, -} - -impl TypedStruct for Organization<'_> { - const KIND: &'static str = "Organization"; -} - -/// The name components of the name of the entity represented by this Card. Name -/// components SHOULD be ordered such that their values joined by whitespace -/// produce a valid full name of this entity. Doing so, implementations MAY -/// ignore any separator components. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct NameComponent<'a> { - value: Cow<'a, str>, - #[serde(rename = "type")] - type_: NameComponentKind, -} - -impl TypedStruct for NameComponent<'_> { - const KIND: &'static str = "NameComponent"; -} - -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum NameComponentKind { - /// The value is a honorific title(s), e.g. "Mr", "Ms", "Dr". - Prefix, - /// The value is a personal name(s), also known as "first name", "given name". - Personal, - /// The value is a surname, also known as "last name", "family name". - Surname, - /// The value is an additional name, also known as "middle name". - Additional, - /// The value is a honorific suffix, e.g. "B.A.", "Esq.". - Suffix, - /// A separator for two name components. The value property of the component - /// includes the verbatim separator, for example a newline character. - Separator, -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] -pub struct Relation { - relation: HashMap, -} - -impl TypedStruct for Relation { - const KIND: &'static str = "Relation"; -} - -/// Contact information typically is associated with a context in which it -/// should be used. For example, someone might have distinct phone numbers -/// for work and private contexts. The Context data type enumerates common -/// contexts. -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] -#[serde(rename_all = "kebab-case")] -pub enum Context { - /// The contact information may be used to contact the card holder in a - /// private context. - Private, - /// The contact information may be used to contact the card holder in a - /// professional context. - Work, - /// The contact information may be used to contact the card holder in some - /// other context. - Other, -} - -#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum RelationKind { - Contact, - Acquaintance, - Friend, - Met, - CoWorker, - Colleague, - CoResident, - Neighbor, - Child, - Parent, - Sibling, - Spouse, - Kin, - Muse, - Crush, - Date, - Sweetheart, - Me, - Agent, - Emergency, -} - -/// The kind of the entity the Card represents. -#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub enum CardKind { - /// A single person - Individual, - /// An organization - Org, - /// A named location - Location, - /// A device, such as appliances, computers, or network elements - Device, - /// A software application - Application, -} diff --git a/jmap-proto/src/extensions/mod.rs b/jmap-proto/src/extensions/mod.rs index 034ca9c..e5b1541 100644 --- a/jmap-proto/src/extensions/mod.rs +++ a/jmap-proto/src/extensions/mod.rs @@ -1,1 +1,2 @@ -pub mod js_contact; +pub mod contacts; +pub mod sharing; diff --git a/jmap-proto/src/extensions/sharing.rs b/jmap-proto/src/extensions/sharing.rs new file mode 100644 index 0000000..5970ebe 100644 --- /dev/null +++ a/jmap-proto/src/extensions/sharing.rs @@ -1,0 +1,137 @@ +use std::{borrow::Cow, collections::HashMap}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{ + common::{Id, UtcDate}, + endpoints::session::Account, +}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PrincipalsSessionCapabilities {} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PrincipalsAccountCapabilities<'a> { + /// The id of the principal in this account that corresponds to the user + /// fetching this object, if any. + #[serde(borrow)] + pub current_user_principal_id: Option>, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PrincipalsOwnerAccountCapabilities<'a> { + /// The id of an account with the `urn:ietf:params:jmap:principals` + /// capability that contains the corresponding Principal object. + #[serde(borrow)] + pub account_id_for_principal: Id<'a>, + /// The id of the Principal that owns this account. + #[serde(borrow)] + pub principal_id: Id<'a>, +} + +/// A Principal represents an individual, group, location (e.g. a room), +/// resource (e.g. a projector) or other entity in a collaborative environment. +/// Sharing in JMAP is generally configured by assigning rights to certain data +/// within an account to other principals, for example a user may assign +/// permission to read their calendar to a principal representing another user, +/// or their team. +/// +/// In a shared environment such as a workplace, a user may have access to a +/// large number of principals. +/// +/// In most systems the user will have access to a single Account containing +/// Principal objects, but they may have access to multiple if, for example, +/// aggregating data from different places. +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Principal<'a> { + /// The id of the principal. + #[serde(borrow)] + pub id: Id<'a>, + pub type_: PrincipalType, + /// The name of the principal, e.g. “Jane Doe”, or “Room 4B”. + #[serde(borrow)] + pub name: Cow<'a, str>, + /// A longer description of the principal, for example details about the facilities of a + /// resource, or null if no description available. + #[serde(borrow)] + pub description: Option>, + /// An email address for the principal, or null if no email is available. + #[serde(borrow)] + pub email: Option>, + /// The time zone for this principal, if known. If not null, the value MUST + /// be a time zone id from the IANA Time Zone Database TZDB. + #[serde(borrow)] + pub time_zone: Option>, + /// A map of JMAP capability URIs to domain specific information about the principal in + /// relation to that capability, as defined in the document that registered the capability. + #[serde(borrow)] + pub capabilities: HashMap, Value>, + /// A map of account id to Account object for each JMAP Account containing data for this + /// principal that the user has access to, or null if none. + #[serde(borrow)] + pub accounts: Option, Account<'a>>>, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +#[serde(rename_all = "camelCase")] +pub enum PrincipalType { + /// This represents a single person. + Individual, + /// This represents a group of people. + Group, + /// This represents some resource, e.g. a projector. + Resource, + /// This represents a location. + Location, + /// This represents some other undefined principal. + Other, +} + +/// The ShareNotification data type records when the user’s permissions to access a shared object +/// changes. ShareNotification are only created by the server; users cannot create them explicitly. +/// Notifications are stored in the same Account as the Principals. +/// +/// Clients SHOULD present the list of notifications to the user and allow them to dismiss them. To +/// dismiss a notification you use a standard “/set” call to destroy it. +/// +/// The server SHOULD create a ShareNotification whenever the user’s permissions change on an +/// object. It SHOULD NOT create a notification for permission changes to a group principal, even if +/// the user is in the group. +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ShareNotification<'a> { + /// The id of the ShareNotification. + pub id: Cow<'a, str>, + /// The time this notification was created. + pub created: UtcDate, + /// Who made the change. + pub changed_by: Person<'a>, + /// The name of the data type for the object whose permissions have changed, e.g. “Calendar” or + /// “Mailbox”. + pub object_id: Cow<'a, str>, + /// The id of the account where this object exists. + pub object_account_id: Cow<'a, str>, + /// The name of the object at the time the notification was made. + pub name: Cow<'a, str>, + /// The “myRights” property of the object for the user before the change. + pub old_rights: Cow<'a, str>, + /// The “myRights” property of the object for the user after the change. + pub new_rights: Cow<'a, str>, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Person<'a> { + /// The name of the person who made the change. + pub name: Cow<'a, str>, + /// The email of the person who made the change, or null if no email is available. + pub email: Option>, + /// The id of the Principal corresponding to the person who made the change, or null if no + /// associated principal. + pub principal: Option>, +} diff --git a/jogre-server/src/context/oauth2.rs b/jogre-server/src/context/oauth2.rs index 3e74426..f1a1c60 100644 --- a/jogre-server/src/context/oauth2.rs +++ a/jogre-server/src/context/oauth2.rs @@ -325,7 +325,6 @@ return AuthState::Unauthenticated(Some(UnauthenticatedState::InvalidCsrfToken)); } - // TODO: actually await here let Some(user) = store.get_by_username(username).await.unwrap() else { return AuthState::Unauthenticated(Some(UnauthenticatedState::InvalidUserPass)); }; diff --git a/jogre-server/src/extensions/contacts.rs b/jogre-server/src/extensions/contacts.rs new file mode 100644 index 0000000..8b5ecca 100644 --- /dev/null +++ a/jogre-server/src/extensions/contacts.rs @@ -1,0 +1,42 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::extensions::{JmapDataExtension, JmapExtension}; + +pub struct Contacts {} + +impl JmapExtension for Contacts { + const EXTENSION: &'static str = "urn:ietf:params:jmap:contacts"; +} + +impl JmapDataExtension for Contacts { + const ENDPOINT: &'static str = "AddressBook"; +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ContactMetadata { + pub may_create_address_book: bool, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AddressBook { + id: Uuid, + name: String, + is_subscribed: bool, + owner: Uuid, + share_with: HashMap, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[allow(clippy::struct_excessive_bools)] +pub struct AddressBookRights { + may_read: bool, + may_write: bool, + may_admin: bool, + may_delete: bool, +} diff --git a/jogre-server/src/extensions/core.rs b/jogre-server/src/extensions/core.rs new file mode 100644 index 0000000..c0a709c 100644 --- /dev/null +++ a/jogre-server/src/extensions/core.rs @@ -1,0 +1,35 @@ +use std::collections::BTreeSet; + +use jmap_proto::endpoints::session::CoreCapability; +use uuid::Uuid; + +use crate::{ + config::CoreCapabilities, + extensions::{JmapExtension, JmapSessionCapabilityExtension}, +}; + +#[derive(Clone)] +pub struct Core { + pub(crate) core_capabilities: CoreCapabilities, +} + +impl JmapExtension for Core { + const EXTENSION: &'static str = "urn:ietf:params:jmap:core"; +} + +impl JmapSessionCapabilityExtension for Core { + type Metadata = CoreCapability<'static>; + + fn build(&self, _user: Uuid) -> Self::Metadata { + CoreCapability { + max_size_upload: self.core_capabilities.max_size_upload.into(), + max_concurrent_upload: self.core_capabilities.max_concurrent_upload.into(), + max_size_request: self.core_capabilities.max_size_request.into(), + max_concurrent_requests: self.core_capabilities.max_concurrent_requests.into(), + max_calls_in_request: self.core_capabilities.max_calls_in_request.into(), + max_objects_in_get: self.core_capabilities.max_objects_in_get.into(), + max_objects_in_set: self.core_capabilities.max_objects_in_set.into(), + collation_algorithms: BTreeSet::default(), + } + } +} diff --git a/jogre-server/src/extensions/mod.rs b/jogre-server/src/extensions/mod.rs new file mode 100644 index 0000000..28a4b17 100644 --- /dev/null +++ a/jogre-server/src/extensions/mod.rs @@ -1,0 +1,158 @@ +use std::{borrow::Cow, collections::HashMap}; + +use jmap_proto::{extensions::sharing as proto_sharing, Value}; +use serde::{ + de::{value::CowStrDeserializer, DeserializeSeed, MapAccess, Visitor}, + forward_to_deserialize_any, Deserialize, Deserializer, Serialize, +}; +use uuid::Uuid; + +pub mod contacts; +pub mod core; +pub mod sharing; + +/// Defines a base extension to the JMAP specification. +pub trait JmapExtension { + /// A URI that describes this extension (eg. `urn:ietf:params:jmap:contacts`). + const EXTENSION: &'static str; +} + +/// Defines an extension that can handle reads/writes. +pub trait JmapDataExtension: JmapExtension { + /// Endpoint from which this data type is exposed from (ie. `ContactBook`). + const ENDPOINT: &'static str; +} + +/// Defines an extension which should be exposed via session capabilities. +pub trait JmapSessionCapabilityExtension: JmapExtension { + /// The metadata returned by this endpoint from the session endpoint. + type Metadata: Serialize; + + fn build(&self, user: Uuid) -> Self::Metadata; +} + +/// Defines an extension which should be exposed via account capabilities. +pub trait JmapAccountCapabilityExtension: JmapExtension { + /// The metadata returned by this endpoint within account capabilities + /// from the session endpoint. + type Metadata: Serialize; + + fn build(&self, user: Uuid, account: Uuid) -> Self::Metadata; +} + +/// Registry containing all extensions that can be handled by Jogre. +pub struct ExtensionRegistry { + pub core: core::Core, + pub contacts: contacts::Contacts, + pub sharing_principals: sharing::Principals, + pub sharing_principals_owner: sharing::PrincipalsOwner, +} + +impl ExtensionRegistry { + /// Builds the session capability payload from the .well-known/jmap endpoint + pub fn build_session_capabilities(&self, user: Uuid) -> HashMap, Value> { + let mut out = HashMap::new(); + out.insert( + Cow::Borrowed(core::Core::EXTENSION), + serde_json::to_value(JmapSessionCapabilityExtension::build(&self.core, user)).unwrap(), + ); + out.insert( + Cow::Borrowed(sharing::Principals::EXTENSION), + serde_json::to_value(JmapSessionCapabilityExtension::build( + &self.sharing_principals, + user, + )) + .unwrap(), + ); + out + } +} + +/// Defines all the data types that can be handled by our [`JmapDataExtension`] +/// extensions. +pub enum ConcreteData<'a> { + AddressBook(contacts::AddressBook), + Principal(proto_sharing::Principal<'a>), + ShareNotification(proto_sharing::ShareNotification<'a>), +} + +impl<'a> ConcreteData<'a> { + /// Determines which extension should handle an incoming request by + /// the defined endpoint, and deserializes the request into the + /// relevant data type. + pub fn parse(endpoint: &str, data: ResolvedArguments<'a>) -> Option { + match endpoint { + >::ENDPOINT => { + Some(Self::AddressBook(Deserialize::deserialize(data).unwrap())) + } + >::ENDPOINT => { + Some(Self::Principal(Deserialize::deserialize(data).unwrap())) + }, + >::ENDPOINT => { + Some(Self::ShareNotification(Deserialize::deserialize(data).unwrap())) + }, + _ => None, + } + } +} + +/// A list of key => value pairs representing the built parameters for the +/// incoming request with all references to other requests resolved. +pub struct ResolvedArguments<'a>(pub HashMap, Cow<'a, Value>>); + +impl<'de> Deserializer<'de> for ResolvedArguments<'de> { + type Error = serde_json::Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_map(ResolvedArgumentsVisitor { + iter: self.0.into_iter(), + value: None, + }) + } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct enum identifier ignored_any + } +} + +struct ResolvedArgumentsVisitor<'de> { + iter: , Cow<'de, Value>> as IntoIterator>::IntoIter, + value: Option>, +} + +impl<'de> MapAccess<'de> for ResolvedArgumentsVisitor<'de> { + type Error = serde_json::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: DeserializeSeed<'de>, + { + let Some((key, value)) = self.iter.next() else { + return Ok(None); + }; + + self.value = Some(value); + + seed.deserialize(CowStrDeserializer::new(key)).map(Some) + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: DeserializeSeed<'de>, + { + let value = self + .value + .take() + .ok_or(serde::de::Error::custom("value is missing"))?; + + match value { + Cow::Owned(v) => seed.deserialize(v), + Cow::Borrowed(v) => seed.deserialize(v), + } + } +} diff --git a/jogre-server/src/extensions/sharing.rs b/jogre-server/src/extensions/sharing.rs new file mode 100644 index 0000000..b206864 100644 --- /dev/null +++ a/jogre-server/src/extensions/sharing.rs @@ -1,0 +1,67 @@ +use jmap_proto::{ + common::Id, + extensions::sharing::{ + Principal, PrincipalsAccountCapabilities, PrincipalsOwnerAccountCapabilities, + PrincipalsSessionCapabilities, ShareNotification, + }, +}; +use uuid::Uuid; + +use crate::extensions::{ + JmapAccountCapabilityExtension, JmapDataExtension, JmapExtension, + JmapSessionCapabilityExtension, +}; + +/// Represents support for the `Principal` and `ShareNotification` data types and associated API +/// methods. +pub struct Principals {} + +impl JmapExtension for Principals { + const EXTENSION: &'static str = "urn:ietf:params:jmap:principals"; +} + +impl JmapSessionCapabilityExtension for Principals { + type Metadata = PrincipalsSessionCapabilities; + + fn build(&self, _user: Uuid) -> Self::Metadata { + PrincipalsSessionCapabilities {} + } +} + +impl JmapAccountCapabilityExtension for Principals { + type Metadata = PrincipalsAccountCapabilities<'static>; + + fn build(&self, _user: Uuid, _account: Uuid) -> Self::Metadata { + PrincipalsAccountCapabilities { + current_user_principal_id: None, + } + } +} + +impl JmapDataExtension> for Principals { + const ENDPOINT: &'static str = "Principal"; +} + +impl JmapDataExtension> for Principals { + const ENDPOINT: &'static str = "ShareNotification"; +} + +/// This URI is solely used as a key in an account’s accountCapabilities property; +/// it does not appear in the JMAP Session capabilities. Support is implied by the +/// `urn:ietf:params:jmap:principals` session capability. +pub struct PrincipalsOwner {} + +impl JmapExtension for PrincipalsOwner { + const EXTENSION: &'static str = "urn:ietf:params:jmap:principals:owner"; +} + +impl JmapAccountCapabilityExtension for PrincipalsOwner { + type Metadata = PrincipalsOwnerAccountCapabilities<'static>; + + fn build(&self, _user: Uuid, _account: Uuid) -> Self::Metadata { + PrincipalsOwnerAccountCapabilities { + account_id_for_principal: Id("test".into()), + principal_id: Id("test".into()), + } + } +} diff --git a/jogre-server/src/methods/mod.rs b/jogre-server/src/methods/mod.rs index 3d1ee69..8910624 100644 --- a/jogre-server/src/methods/mod.rs +++ a/jogre-server/src/methods/mod.rs @@ -1,9 +1,13 @@ +mod api; mod oauth; mod session; use std::sync::Arc; -use axum::{routing::get, Router}; +use axum::{ + routing::{any, get}, + Router, +}; use tower::layer::layer_fn; use tower_cookies::CookieManagerLayer; @@ -15,6 +19,7 @@ pub fn router(context: Arc) -> Router { Router::new() .route("/.well-known/jmap", get(session::get)) + .route("/api/*", any(api::handle)) // only apply auth requirement on endpoints above .layer(axum::middleware::from_fn_with_state( context.clone(), diff --git a/jogre-server/src/methods/session.rs b/jogre-server/src/methods/session.rs index ede525f..e1ee4b3 100644 --- a/jogre-server/src/methods/session.rs +++ a/jogre-server/src/methods/session.rs @@ -1,14 +1,12 @@ use std::{ - collections::{BTreeSet, HashMap}, + collections::HashMap, sync::{Arc, OnceLock}, }; use axum::{extract::State, Extension, Json}; use jmap_proto::{ common::{Id, SessionState}, - endpoints::session::{ - Account, AccountCapabilities, CoreCapability, ServerCapabilities, Session, - }, + endpoints::session::{Account, AccountCapabilities, Session}, }; use oxide_auth::primitives::grant::Grant; @@ -66,18 +64,9 @@ ); Json(Session { - capabilities: ServerCapabilities { - core: CoreCapability { - max_size_upload: context.core_capabilities.max_size_upload.into(), - max_concurrent_upload: context.core_capabilities.max_concurrent_upload.into(), - max_size_request: context.core_capabilities.max_size_request.into(), - max_concurrent_requests: context.core_capabilities.max_concurrent_requests.into(), - max_calls_in_request: context.core_capabilities.max_calls_in_request.into(), - max_objects_in_get: context.core_capabilities.max_objects_in_get.into(), - max_objects_in_set: context.core_capabilities.max_objects_in_set.into(), - collation_algorithms: BTreeSet::default(), - }, - }, + capabilities: context + .extension_registry + .build_session_capabilities(user.id), accounts, primary_accounts: HashMap::default(), username: username.into(), diff --git a/jmap-proto/src/extensions/contacts/js_contact.rs b/jmap-proto/src/extensions/contacts/js_contact.rs new file mode 100644 index 0000000..56646be 100644 --- /dev/null +++ a/jmap-proto/src/extensions/contacts/js_contact.rs @@ -1,0 +1,640 @@ +use std::{borrow::Cow, collections::HashMap}; + +use chrono::NaiveDate; +use serde::{ + ser::SerializeMap, Deserialize, Serialize, Serializer, __private::ser::FlatMapSerializer, +}; +use serde_json::Value; + +use crate::common::{Id, UnsignedInt, UtcDate}; + +#[derive(Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +pub struct TypeWrapper(T); + +impl Serialize for TypeWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(None)?; + map.serialize_entry("@type", T::KIND)?; + self.0.serialize(FlatMapSerializer(&mut map))?; + map.end() + } +} + +pub trait TypedStruct { + const KIND: &'static str; +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "PascalCase", tag = "@type")] +pub enum Data<'a> { + Card(#[serde(borrow)] Card<'a>), + CardGroup(CardGroup<'a>), +} + +/// A CardGroup object represents a group of cards. Its members may be Cards or CardGroups. +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct CardGroup<'a> { + /// An identifier, used to associate the object as the same across different + /// systems, addressbooks and views. + #[serde(borrow)] + uid: Id<'a>, + /// The set is represented as an object, with each key being the uid of another Card or + /// CardGroup. The value for each key in the object MUST be true. + members: HashMap, bool>, + /// The user-visible name for the group, e.g. "Friends". This may be any UTF-8 string of at + /// least 1 character in length and maximum 255 octets in size. The same name may be used by + /// two different groups. + #[serde(default)] + name: Cow<'a, str>, + /// The card that represents this group. + card: Option>, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Card<'a> { + /// An identifier, used to associate the object as the same across different + /// systems, addressbooks and views. + #[serde(borrow)] + uid: Id<'a>, + /// The identifier for the product that created the Card object. + prod_id: Option>, + /// The date and time when this Card object was created. + created: Option, + /// The date and time when the data in this Card object was last modified. + updated: Option, + /// The kind of the entity the Card represents. + kind: Option, + /// Relates the object to other Card and CardGroup objects. This is + /// represented as a map, where each key is the + #[serde(default)] + related_to: HashMap, TypeWrapper>, + /// Language used for free-form text on this card. + language: Option>, + /// The name components of the name of the entity represented by this Card. + #[serde(default)] + name: Vec>>, + /// The full name (e.g. the personal name and surname of an individual, the + /// name of an organization) of the entity represented by this card. The + /// purpose of this property is to define a name, even if the individual + /// name components are not known. In addition, it is meant to provide + /// alternative versions of the name for internationalisation. + /// + /// Implementations SHOULD prefer using the name property over this one + /// and SHOULD NOT store the concatenated name component values in this + /// property. + #[serde(default)] + full_name: Cow<'a, str>, + /// The nick names of the entity represented by this card. + #[serde(default)] + nick_names: Vec>, + /// The companies or organization names and units associated with this + /// card. + #[serde(default)] + organizations: HashMap, TypeWrapper>>, + /// The job titles or functional positions of the entity represented by + /// this card. + #[serde(default)] + titles: HashMap, TypeWrapper>>, + /// The email addresses to contact the entity represented by this card. + #[serde(default)] + emails: HashMap, TypeWrapper>>, + /// The phone numbers to contact the entity represented by this card. + #[serde(default)] + phones: HashMap, TypeWrapper>>, + /// The online resources and services that are associated with the entity + /// represented by this card. + #[serde(default)] + online: HashMap, TypeWrapper>>, + /// A map of photo ids to File objects that contain photographs or images + /// associated with this card. A typical use case is to include an avatar for display along the + /// contact name. + #[serde(default)] + photos: HashMap, TypeWrapper>>, + /// Defines the preferred method to contact the holder of this card. + preferred_contact_method: Option, + /// Defines the preferred languages for contacting the entity associated with this card. The + /// keys in the object MUST be [RFC5646] language tags. The values are a (possibly empty) list + /// of contact language preferences for this language. A valid ContactLanguage object MUST have + /// at least one of its properties set. + #[serde(default)] + preferred_contact_languages: HashMap>, + /// A map of address ids to Address objects, containing physical locations. + #[serde(default)] + address: HashMap, TypeWrapper>>, + /// A map of language tags [RFC5646] to patches, which localize a property value into the + /// locale of the respective language tag. + /// + /// A patch MUST NOT target the localizations property. + #[serde(default)] + localizations: HashMap, Value>, + /// These are memorable dates and events for the entity represented by this card. + #[serde(default)] + anniversaries: HashMap, TypeWrapper>>, + /// Defines personal information about the entity represented by this card. + #[serde(default)] + personal_info: HashMap, TypeWrapper>>, + /// Arbitrary notes about the entity represented by this card. + #[serde(default)] + notes: Cow<'a, str>, + /// The set of free-text or URI categories that relate to the card. The set is represented as + /// an object, with each key being a category. The value for each key in the object MUST be + /// true. + #[serde(default)] + categories: HashMap, bool>, + /// Maps identifiers of custom time zones to their time zone definitions. For a description of + /// this property see the timeZones property definition in [RFC8984]. + #[serde(default)] + time_zones: HashMap, Value>, +} + +/// Defines personal information about the entity represented by this card. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PersonalInfo<'a> { + /// Specifies the type for this personal information. + #[serde(rename = "type")] + type_: PersonalInfoType, + /// The actual information. This generally is free-text, but future + /// specifications MAY restrict allowed values depending on the type of + /// this PersonalInformation. + value: Cow<'a, str>, + /// Indicates the level of expertise, or engagement in hobby or interest. + level: Option, +} + +impl TypedStruct for PersonalInfo<'_> { + const KIND: &'static str = "PersonalInfo"; +} + +/// Specifies the type for this personal information. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum PersonalInfoType { + /// A field of expertise or credential + Expertise, + /// A hobby + Hobby, + /// An interest + Interest, + /// An information not covered by the above categories + Other, +} + +/// Indicates the level of expertise, or engagement in hobby or interest. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum PersonalInfoLevel { + High, + Medium, + Low, +} + +/// These are memorable dates and events for the entity represented by this card. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Anniversary<'a> { + /// Specifies the type of the anniversary. + #[serde(rename = "type")] + type_: AnniversaryType, + /// A label describing the value in more detail, especially if the type + /// property has value other (but MAY be included with any type). + #[serde(default)] + label: Cow<'a, str>, + /// The date of this anniversary, in the form "YYYY-MM-DD" + /// (any part may be all 0s for unknown) or a [RFC3339] timestamp. + date: NaiveDate, + /// An address associated with this anniversary, e.g. the place of birth or + /// death. + place: Option>, +} + +impl TypedStruct for Anniversary<'_> { + const KIND: &'static str = "Anniversary"; +} + +/// Specifies the type of the anniversary. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum AnniversaryType { + /// A birth day anniversary + Birth, + /// A death day anniversary + Death, + /// An anniversary not covered by any of the known types. + Other, +} + +/// A physical location. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Address<'a> { + /// The complete address, excluding type and label. This property is mainly useful to + /// represent addresses of which the individual address components are unknown, or to provide + /// localized representations. + #[serde(default)] + full_address: Cow<'a, str>, + /// The street address. The concatenation of the component values, separated by whitespace, + /// SHOULD result in a valid street address for the address locale. Doing so, implementations + /// MAY ignore any separator components. The StreetComponent object type is defined in the + /// paragraph below. + #[serde(default)] + street: Vec>>, + /// The city, town, village, post town, or other locality within which the street address may + /// be found. + #[serde(default)] + locality: Cow<'a, str>, + /// The province, such as a state, county, or canton within which the locality may be found. + #[serde(default)] + region: Cow<'a, str>, + /// The country name. + #[serde(default)] + country: Cow<'a, str>, + /// The postal code, post code, ZIP code or other short code associated with the address by the + /// relevant country's postal system. + #[serde(default)] + postcode: Cow<'a, str>, + /// The ISO-3166-1 country code. + #[serde(default)] + country_code: Cow<'a, str>, + /// A [RFC5870] "geo:" URI for the address. + #[serde(default)] + coordinates: Cow<'a, str>, + /// Identifies the time zone this address is located in. This either MUST be a time zone name + /// registered in the IANA Time Zone Database, or it MUST be a valid TimeZoneId as defined in + /// [RFC8984]. For the latter, a corresponding time zone MUST be defined in the timeZones + /// property. + #[serde(default)] + time_zone: Cow<'a, str>, + /// The contexts of the address information. + #[serde(default)] + context: HashMap, + /// A label describing the value in more detail. + #[serde(default)] + label: Cow<'a, str>, + /// The preference of this address in relation to other addresses. + pref: Option, +} + +impl TypedStruct for Address<'_> { + const KIND: &'static str = "Address"; +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase", untagged)] +pub enum AddressContext { + /// An address to be used for billing. + Billing, + /// An address to be used for delivering physical items + Postal, + /// A normal context + Other(Context), +} + +/// The street address. The concatenation of the component values, separated by whitespace, SHOULD +/// result in a valid street address for the address locale. Doing so, implementations MAY ignore +/// any separator components. The StreetComponent object type is defined in the paragraph below. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct StreetComponent<'a> { + /// The type of this street component. + #[serde(rename = "type")] + type_: StreetComponentKind, + /// The value of this street component. + value: Cow<'a, str>, +} + +impl TypedStruct for StreetComponent<'_> { + const KIND: &'static str = "StreetComponent"; +} + +/// The type of this street component. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum StreetComponentKind { + Name, + Number, + Apartment, + Room, + Extension, + Direction, + Building, + Floor, + PostOfficeBox, + Separator, + Unknown, +} + +/// Defines the preferred method to contact the holder of this card. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ContactLanguage { + /// Defines the context in which to use this language. + context: Option, + /// Defines the preference of this language in relation to other + /// languages of the same context. + pref: Option, +} + +impl TypedStruct for ContactLanguage { + const KIND: &'static str = "ContactLanguage"; +} + +/// Defines the preferred method to contact the holder of this card. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum PreferredContactMethod { + Emails, + Phones, + Online, +} + +/// A map of photo ids to File objects that contain photographs or images +/// associated with this card. A typical use case is to include an avatar for display along the +/// contact name. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct File<'a> { + /// A URI where to fetch the data of this file. + href: Cow<'a, str>, + /// The content-type of the file, if known. + media_type: Cow<'a, str>, + /// The size, in octets, of the file when fully decoded (i.e., the number + /// of octets in the file the user would download), if known. + size: Option, + /// The preference of this photo in relation to other photos. + pref: Option, +} + +impl TypedStruct for File<'_> { + const KIND: &'static str = "File"; +} + +/// The online resources and services that are associated with the entity +/// represented by this card. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Resource<'a> { + /// resource value, where the allowed value form is defined by the the type + /// property. In any case the value MUST NOT be empty. + resource: Cow<'a, str>, + /// The type of the resource value. + #[serde(rename = "type")] + type_: ResourceType, + /// Used for URI resource values. Provides the media type [RFC2046] of the + /// resource identified by the URI. + #[serde(default)] + media_type: Cow<'a, str>, + /// The contexts in which to use this resource. + #[serde(default)] + context: HashMap, + /// A label describing the value in more detail, especially if the type + /// property has value other (but MAY be included with any type). + #[serde(default)] + label: Cow<'a, str>, + /// The preference of this resource in relation to other resources. + pref: Option, +} + +impl TypedStruct for Resource<'_> { + const KIND: &'static str = "Resource"; +} + +/// The type of the resource value. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum ResourceType { + /// The resource value is a URI, e.g. a website link. This MUST be a valid URI as defined in + /// Section 3 of [RFC3986] and updates. + Uri, + /// The resource value is a username associated with the entity represented by this card (e.g. + /// for social media, or an IM client). The label property SHOULD be included to identify what + /// service this is for. For compatibility between clients, this label SHOULD be the canonical + /// service name, including capitalisation. e.g. Twitter, Facebook, Skype, GitHub, XMPP. The + /// resource value may be any non-empty free text. + Username, + /// The resource value is something else not covered by the above categories. A label property + /// MAY be included to display next to the number to help the user identify its purpose. The + /// resource value may be any non-empty free text. + Other, +} + +/// The phone numbers to contact the entity represented by this card. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Phone<'a> { + /// The phone value, as either a URI or a free-text phone number. Typical + /// URI schemes are the [RFC3966] tel or [RFC3261] sip schemes, but any + /// URI scheme is allowed. + phone: Cow<'a, str>, + /// The set of contact features that this phone number may be used for. The + /// set is represented as an object, with each key being a method type. The + /// value for each key in the object MUST be true. + #[serde(default)] + features: HashMap, + // The contexts in which to use this number. The value for each + /// key in the object MUST be true. + #[serde(default)] + contexts: HashMap, + /// A label describing the value in more detail, especially if the type + /// property has value other (but MAY be included with any type). + #[serde(default)] + label: Cow<'a, str>, + /// The preference of this email address in relation to other email addresses. + pref: Option, +} + +impl TypedStruct for Phone<'_> { + const KIND: &'static str = "Phone"; +} + +/// The email addresses to contact the entity represented by this card. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum PhoneFeature { + /// The number is for calling by voice. + Voice, + /// The number is for sending faxes. + Fax, + /// The number is for a pager or beeper. + Pager, + /// The number supports text messages (SMS). + Text, + /// The number is for a cell phone. + Cell, + /// The number is for a device for people with hearing or speech difficulties. + Textphone, + /// The number supports video conferencing. + Video, + /// The number is for some other purpose. The label property MAY be included + /// to display next to the number to help the user identify its purpose. + Other, +} + +/// The email addresses to contact the entity represented by this card. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct EmailAddress<'a> { + /// The email address. This MUST be an addr-spec value as defined in + /// Section 3.4.1 of [RFC5322]. + email: Cow<'a, str>, + /// The contexts in which to use this email address. The value for each + /// key in the object MUST be true. + #[serde(default)] + contexts: HashMap, + /// The preference of this email address in relation to other email addresses. + pref: Option, +} + +impl TypedStruct for EmailAddress<'_> { + const KIND: &'static str = "EmailAddress"; +} + +/// This data type allows to define a preference order on same-typed contact +/// information. For example, a card holder may have two email addresses and +/// prefer to be contacted with one of them. +/// +/// A preference value MUST be an integer number in the range 1 and 100. Lower +/// values correspond to a higher level of preference, with 1 being most +/// preferred. If no preference is set, then the contact information MUST be +/// interpreted as being least preferred. +/// +/// Note that the preference only is defined in relation to contact information +/// of the same type. For example, the preference orders within emails and +/// phone numbers are indendepent of each other. Also note that the +/// preferredContactMethod property allows to define a preferred contact method +/// across method types. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +pub struct Preference(u8); + +/// The companies or organization names and units associated with this card. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Title<'a> { + /// The name of this organization. + #[serde(borrow)] + name: Cow<'a, str>, + /// The id of the organization in which this title is held. + #[serde(default)] + organization: Vec>, +} + +impl TypedStruct for Title<'_> { + const KIND: &'static str = "Title"; +} + +/// The companies or organization names and units associated with this card. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Organization<'a> { + /// The name of this organization. + name: Cow<'a, str>, + /// Additional levels of organizational unit names. + #[serde(default)] + units: Vec>, +} + +impl TypedStruct for Organization<'_> { + const KIND: &'static str = "Organization"; +} + +/// The name components of the name of the entity represented by this Card. Name +/// components SHOULD be ordered such that their values joined by whitespace +/// produce a valid full name of this entity. Doing so, implementations MAY +/// ignore any separator components. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct NameComponent<'a> { + value: Cow<'a, str>, + #[serde(rename = "type")] + type_: NameComponentKind, +} + +impl TypedStruct for NameComponent<'_> { + const KIND: &'static str = "NameComponent"; +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum NameComponentKind { + /// The value is a honorific title(s), e.g. "Mr", "Ms", "Dr". + Prefix, + /// The value is a personal name(s), also known as "first name", "given name". + Personal, + /// The value is a surname, also known as "last name", "family name". + Surname, + /// The value is an additional name, also known as "middle name". + Additional, + /// The value is a honorific suffix, e.g. "B.A.", "Esq.". + Suffix, + /// A separator for two name components. The value property of the component + /// includes the verbatim separator, for example a newline character. + Separator, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Relation { + relation: HashMap, +} + +impl TypedStruct for Relation { + const KIND: &'static str = "Relation"; +} + +/// Contact information typically is associated with a context in which it +/// should be used. For example, someone might have distinct phone numbers +/// for work and private contexts. The Context data type enumerates common +/// contexts. +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[serde(rename_all = "kebab-case")] +pub enum Context { + /// The contact information may be used to contact the card holder in a + /// private context. + Private, + /// The contact information may be used to contact the card holder in a + /// professional context. + Work, + /// The contact information may be used to contact the card holder in some + /// other context. + Other, +} + +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum RelationKind { + Contact, + Acquaintance, + Friend, + Met, + CoWorker, + Colleague, + CoResident, + Neighbor, + Child, + Parent, + Sibling, + Spouse, + Kin, + Muse, + Crush, + Date, + Sweetheart, + Me, + Agent, + Emergency, +} + +/// The kind of the entity the Card represents. +#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum CardKind { + /// A single person + Individual, + /// An organization + Org, + /// A named location + Location, + /// A device, such as appliances, computers, or network elements + Device, + /// A software application + Application, +} diff --git a/jmap-proto/src/extensions/contacts/mod.rs b/jmap-proto/src/extensions/contacts/mod.rs new file mode 100644 index 0000000..034ca9c 100644 --- /dev/null +++ a/jmap-proto/src/extensions/contacts/mod.rs @@ -1,0 +1,1 @@ +pub mod js_contact; diff --git a/jogre-server/src/methods/api/mod.rs b/jogre-server/src/methods/api/mod.rs new file mode 100644 index 0000000..ba2c847 100644 --- /dev/null +++ a/jogre-server/src/methods/api/mod.rs @@ -1,0 +1,100 @@ +use std::{borrow::Cow, collections::HashMap, sync::Arc}; + +use axum::{body::Bytes, extract::State, Extension}; +use jmap_proto::{ + common::SessionState, + endpoints::{Argument, Arguments, Invocation, Request, Response}, + errors::MethodError, +}; +use oxide_auth::primitives::grant::Grant; + +use crate::{ + context::Context, + extensions::{ConcreteData, ResolvedArguments}, + store::UserProvider, +}; + +pub async fn handle( + State(context): State>, + Extension(grant): Extension, + body: Bytes, +) { + let payload: Request<'_> = serde_json::from_slice(&body).unwrap(); + + // TODO: `using` + // TODO: `method_calls` + // TODO: `created_ids` + + let username = grant.owner_id; + + let user = context + .store + .get_by_username(&username) + .await + .unwrap() + .unwrap(); + + let session_state = context + .store + .fetch_seq_number_for_user(user.id) + .await + .unwrap(); + + let mut response = Response { + method_responses: Vec::with_capacity(payload.method_calls.len()), + created_ids: None, + session_state: SessionState(session_state.to_string().into()), + }; + + for invocation_request in payload.method_calls { + let Some(resolved_arguments) = resolve_arguments(&response, invocation_request.arguments) + else { + response.method_responses.push( + MethodError::InvalidResultReference.into_invocation(invocation_request.request_id), + ); + continue; + }; + + let Some(_request) = + ConcreteData::parse(invocation_request.name.as_ref(), resolved_arguments) + else { + response + .method_responses + .push(MethodError::UnknownMethod.into_invocation(invocation_request.request_id)); + continue; + }; + + // TODO: call handler + + response.method_responses.push(Invocation { + name: invocation_request.name, + arguments: Arguments(HashMap::new()), + request_id: invocation_request.request_id, + }); + } +} + +fn resolve_arguments<'a>( + response: &'a Response, + args: Arguments<'a>, +) -> Option> { + let mut res = HashMap::with_capacity(args.0.len()); + + for (key, value) in args.0 { + let value = match value { + Argument::Reference(refer) => { + let referenced_response = response + .method_responses + .iter() + .find(|inv| inv.request_id == refer.result_of && inv.name == refer.name)?; + + referenced_response.arguments.pointer(&refer.path)? + } + Argument::Absolute(value) => Cow::Owned(value), + }; + + res.insert(key, value); + } + + Some(ResolvedArguments(res)) +} -- rgit 0.1.3