diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index 6b4663eba3..52448a547c 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -3,8 +3,15 @@ package jmap import ( "io" "time" + + "github.com/opencloud-eu/opencloud/pkg/jscalendar" ) +// TODO +type UTCDate struct { + time.Time +} + const ( JmapCore = "urn:ietf:params:jmap:core" JmapMail = "urn:ietf:params:jmap:mail" @@ -52,6 +59,30 @@ var ( } ) +// Should the calendar’s events be used as part of availability calculation? +// +// This MUST be one of: +// !- `all“: all events are considered. +// !- `attending“: events the user is a confirmed or tentative participant of are considered. +// !- `none“: all events are ignored (but may be considered if also in another calendar). +// +// This should default to “all” for the calendars in the user’s own account, and “none” for calendars shared with the user. +type IncludeInAvailability string + +const ( + IncludeInAvailabilityAll = IncludeInAvailability("all") + IncludeInAvailabilityAttending = IncludeInAvailability("attending") + IncludeInAvailabilityNone = IncludeInAvailability("none") +) + +var ( + IncludeInAvailabilities = []IncludeInAvailability{ + IncludeInAvailabilityAll, + IncludeInAvailabilityAttending, + IncludeInAvailabilityNone, + } +) + type SessionMailAccountCapabilities struct { // The maximum number of Mailboxes that can be can assigned to a single Email object. // @@ -2908,6 +2939,244 @@ type AddressBook struct { MyRights AddressBookRights `json:"myRights"` } +type CalendarRights struct { + // The user may read the free-busy information for this calendar. + MayReadFreeBusy bool `json:"mayReadFreeBusy"` + + // The user may fetch the events in this calendar. + MayReadItems bool `json:"mayReadItems"` + + // The user may create, modify or destroy all events in this calendar, or move events + // to or from this calendar. + // + // If this is `true`, the `mayWriteOwn`, `mayUpdatePrivate` and `mayRSVP` + // properties MUST all also be `true`. + MayWriteAll bool `json:"mayWriteAll"` + + // The user may create, modify or destroy an event on this calendar if either they are + // the owner of the event or the event has no owner. + // + // This means the user may also transfer ownership by updating an event so they are no longer an owner. + MayWriteOwn bool `json:"mayWriteOwn"` + + // The user may modify per-user properties on all events in the calendar, even if they would + // not otherwise have permission to modify that event. + // + // These properties MUST all be stored per-user, and changes do not affect any other user of the calendar. + // + // The user may also modify these properties on a per-occurrence basis for recurring events + // (updating the `recurrenceOverrides` property of the event to do so). + MayUpdatePrivate bool `json:"mayUpdatePrivate"` + + // The user may modify the following properties of any `Participant` object that corresponds + // to one of the user's `ParticipantIdentity` objects in the account, even if they would not + // otherwise have permission to modify that event. + // + // !- `participationStatus` + // !- `participationComment` + // !- `expectReply` + // !- `scheduleAgent` + // !- `scheduleSequence` + // !- `scheduleUpdated` + // + // If the event has its `mayInviteSelf` property set to `true`, then the user may also add a + // new `Participant` to the event with `scheduleId`/`sendTo` properties that are the same as + // the `scheduleId`/`sendTo` properties of one of the user's `ParticipantIdentity` objects in + // the account. + // + // The `roles` property of the participant MUST only contain `attendee`. + // + // If the event has its `mayInviteOthers` property set to `true` and there is an existing + // `Participant` in the event corresponding to one of the user's `ParticipantIdentity` objects + // in the account, then the user may also add new participants. + // + // The `roles` property of any new participant MUST only contain `attendee`. + // + // The user may also do all of the above on a per-occurrence basis for recurring events + // (updating the recurrenceOverrides property of the event to do so). + MayRSVP bool `json:"mayRSVP"` + + // The user may modify the `shareWith` property for this calendar. + MayAdmin bool `json:"mayAdmin"` + + // The user may delete the calendar itself. + MayDelete bool `json:"mayDelete"` +} + +// A Calendar is a named collection of events. +// +// All events are associated with at least one calendar. +// +// The user is an owner for an event if the `CalendarEvent` object has a `participants` +// property, and one of the `Participant` objects both: +// 1. Has the `owner` role. +// 2. Corresponds to one of the user's `ParticipantIdentity` objects in the account. +// +// An event has no owner if its `participants` property is null or omitted, or if none +// of the `Participant` objects have the `owner` role. +type Calendar struct { + // The id of the calendar (immutable; server-set). + Id string `json:"id"` + + // The user-visible name of the calendar. + // + // This may be any UTF-8 string of at least 1 character in length and maximum 255 octets in size. + Name string `json:"name"` + + // An optional longer-form description of the calendar, to provide context in shared environments + // where users need more than just the name. + Description string `json:"description,omitempty"` + + // A color to be used when displaying events associated with the calendar. + // + // If not null, the value MUST be a case-insensitive color name taken from the set of names + // defined in Section 4.3 of CSS Color Module Level 3 COLORS, or an RGB value in hexadecimal + // notation, as defined in Section 4.2.1 of CSS Color Module Level 3. + // + // The color SHOULD have sufficient contrast to be used as text on a white background. + Color string `json:"color,omitempty"` + + // Defines the sort order of calendars when presented in the client’s UI, so it is consistent + // between devices. + // + // The number MUST be an integer in the range 0 <= sortOrder < 2^31. + // + // A calendar with a lower order should be displayed before a calendar with a higher order in any + // list of calendars in the client’s UI. + // + // Calendars with equal order SHOULD be sorted in alphabetical order by name. + // + // The sorting should take into account locale-specific character order convention. + SortOrder uint `json:"sortOrder,omitzero"` + + // True if the user has indicated they wish to see this Calendar in their client. + // + // This SHOULD default to `false` for Calendars in shared accounts the user has access to and `true` + // for any new Calendars created by the user themself. + // + // If false, the calendar SHOULD only be displayed when the user explicitly requests it or to offer + // it for the user to subscribe to. + // + // For example, a company may have a large number of shared calendars which all employees have + // permission to access, but you would only subscribe to the ones you care about and want to be able + // to have normally accessible. + IsSubscribed bool `json:"isSubscribed"` + + // Should the calendar’s events be displayed to the user at the moment? + // + // Clients MUST ignore this property if `isSubscribed` is false. + // + // If an event is in multiple calendars, it should be displayed if `isVisible` is `true` + // for any of those calendars. + // + // default: true + IsVisible bool `json:"isVisible"` + + // This SHOULD be true for exactly one calendar in any account, and MUST NOT be true for more + // than one calendar within an account (server-set). + // + // The default calendar should be used by clients whenever they need to choose a calendar + // for the user within this account, and they do not have any other information on which to make + // a choice. + // + // For example, if the user creates a new event, the client may automatically set the event as + // belonging to the default calendar from the user’s primary account. + IsDefault bool `json:"isDefault,omitzero"` + + // Should the calendar’s events be used as part of availability calculation? + // + // This MUST be one of: + // !- `all``: all events are considered. + // !- `attending``: events the user is a confirmed or tentative participant of are considered. + // !- `none``: all events are ignored (but may be considered if also in another calendar). + // + // This should default to “all” for the calendars in the user’s own account, and “none” for calendars shared with the user. + IncludeInAvailability IncludeInAvailability `json:"includeInAvailability,omitempty"` + + // A map of alert ids to Alert objects (see [@!RFC8984], Section 4.5.2) to apply for events + // where `showWithoutTime` is `false` and `useDefaultAlerts` is `true`. + // + // Ids MUST be unique across all default alerts in the account, including those in other + // calendars; a UUID is recommended. + + // If omitted on creation, the default is server dependent. + // + // For example, servers may choose to always default to null, or may copy the alerts from the default calendar. + DefaultAlertsWithTime map[string]jscalendar.Alert `json:"defaultAlertsWithTime,omitempty"` + + // A map of alert ids to Alert objects (see [@!RFC8984], Section 4.5.2) to apply for events where + // `showWithoutTime` is `true` and `useDefaultAlerts` is `true`. + // + // Ids MUST be unique across all default alerts in the account, including those in other + // calendars; a UUID is recommended. + // + // If omitted on creation, the default is server dependent. + // + // For example, servers may choose to always default to null, or may copy the alerts from the default calendar. + DefaultAlertsWithoutTime map[string]jscalendar.Alert `json:"defaultAlertsWithoutTime,omitempty"` + + // The time zone to use for events without a time zone when the server needs to resolve them into + // absolute time, e.g., for alerts or availability calculation. + // + // The value MUST be a time zone id from the IANA Time Zone Database TZDB. + // + // If null, the `timeZone` of the account’s associated `Principal` will be used. + // + // Clients SHOULD use this as the default for new events in this calendar if set. + TimeZone string `json:"timeZone,omitempty"` + + // A map of `Principal` id to rights for principals this calendar is shared with. + // + // The principal to which this calendar belongs MUST NOT be in this set. + // + // This is null if the calendar is not shared with anyone. + // + // May be modified only if the user has the `mayAdmin` right. + // + // The account id for the principals may be found in the `urn:ietf:params:jmap:principals:owner` + // capability of the `Account` to which the calendar belongs. + ShareWith map[string]CalendarRights `json:"shareWith,omitempty"` + + // The set of access rights the user has in relation to this Calendar. + // + // If any event is in multiple calendars, the user has the following rights: + // !- The user may fetch the event if they have the mayReadItems right on any calendar the event is in. + // !- The user may remove an event from a calendar (by modifying the event’s “calendarIds” property) if the user + // has the appropriate permission for that calendar. + // !- The user may make other changes to the event if they have the right to do so in all calendars to which the + // event belongs. + MyRights *CalendarRights `json:"myRights,omitempty"` +} + +// A CalendarEvent object contains information about an event, or recurring series of events, +// that takes place at a particular time. +// +// It is a JSCalendar Event object, as defined in [@!RFC8984], with additional properties. +type CalendarEvent struct { + + // The id of the CalendarEvent (immutable; server-set). + // + // The id uniquely identifies a JSCalendar Event with a particular `uid` and + // `recurrenceId` within a particular account. + Id string `json:"id"` + + baseEventId string + + calendarIds map[string]bool + + isDraft bool + + isOrigin bool + + utcStart UTCDate + + utcEnd UTCDate + + // TODO https://jmap.io/spec-calendars.html#calendar-events + + jscalendar.Event +} + type ErrorResponse struct { Type string `json:"type"` Description string `json:"description,omitempty"` diff --git a/pkg/jscalendar/jscalendar_model.go b/pkg/jscalendar/jscalendar_model.go index db8fa9a5f6..2546f490ff 100644 --- a/pkg/jscalendar/jscalendar_model.go +++ b/pkg/jscalendar/jscalendar_model.go @@ -1001,7 +1001,7 @@ type RecurrenceRule struct { // // This is the `INTERVAL` part from iCalendar. // - // Default: `1` + // Default: 1 Interval uint `json:"interval,omitzero"` // This is the calendar system in which this recurrence rule operates, in lowercase. @@ -1011,7 +1011,7 @@ type RecurrenceRule struct { // // This is the `RSCALE` part from iCalendar RSCALE [RFC7529], converted to lowercase. // - // Default: `gregorian` + // Default: gregorian // // [CLDR]: https://github.com/unicode-org/cldr/blob/latest/common/bcp47/calendar.xml // [RFC7529]: https://www.rfc-editor.org/rfc/rfc7529.html @@ -1028,7 +1028,7 @@ type RecurrenceRule struct { // // This is the `SKIP` part from iCalendar `RSCALE` [RFC7529], converted to lowercase. // - // Default: `omit` + // Default: omit Skip Skip `json:"skip,omitempty"` // This is the day on which the week is considered to start, represented as a lowercase, abbreviated, @@ -1045,7 +1045,7 @@ type RecurrenceRule struct { // // This is the `WKST` part from iCalendar. // - // Default: `mo` + // Default: mo FirstDayOfWeek DayOfWeek `json:"firstDayOfWeek,omitempty"` // These are days of the week on which to repeat. @@ -1262,7 +1262,7 @@ type Participant struct { // !- `client`: The calendar client will send the scheduling messages. // !- `none`: No scheduling messages are to be sent to this participant. // - // Default: `server` + // Default: server ScheduleAgent ScheduleAgent `json:"scheduleAgent,omitempty"` // A client may set the property on a participant to true to request that the server send a scheduling @@ -1369,6 +1369,14 @@ type Participant struct { // // The property value MUST be a positive integer between 0 and 100. PercentComplete uint `json:"percentComplete,omitzero"` + + // This is a URI as defined by [@!RFC3986] or any other IANA-registered form for a URI. + // + // It is the same as the `CAL-ADDRESS` value of an `ATTENDEE` or `ORGANIZER` in iCalendar ([@!RFC5545]); + // it globally identifies a particular participant, even across different events. + // + // This is a JMAP addition to JSCalendar. + ScheduleId string `json:"scheduleId,omitempty"` } type Trigger interface { @@ -1464,7 +1472,7 @@ type Alert struct { // !- `email`: The alert should trigger an email sent out to the user, notifying them of the alert. This action is // typically only appropriate for server implementations. // - // Default: `display` + // Default: display Action AlertAction `json:"action,omitempty"` } @@ -1656,7 +1664,7 @@ type CommonObject struct { // Descriptions of type text/html MAY contain cid URLs [RFC2392] to reference links in the calendar // object by use of the cid property of the Link object. // - // Default: `text/plain` + // Default: text/plain DescriptionContentType string `json:"descriptionContentType,omitempty"` // This is a map of link ids to `Link` objects, representing external resources associated with the object. @@ -1789,7 +1797,7 @@ type Object struct { // // Such events are also commonly known as "all-day" events. // - // Default: `false` + // Default: false ShowWithoutTime bool `json:"showWithoutTime,omitzero"` // This is a map of location ids to `Location` objects, representing locations associated with the object. @@ -2011,7 +2019,7 @@ type Object struct { // If an implementation cannot determine the user's default alerts, or none are set, it MUST process // he alerts property as if `useDefaultAlerts` is set to false. // - // Default: `false` + // Default: false UseDefaultAlerts bool `json:"useDefaultAlerts,omitzero"` // This is a map of alert ids to Alert objects, representing alerts/reminders to display or send @@ -2052,6 +2060,44 @@ type Object struct { // // If omitted, this MUST be presumed to be `null` (i.e., floating time). TimeZone string `json:"timeZone,omitempty"` + + // If true, any user may add themselves to the event as a participant with the + // `attendee` role. + // + // This property MUST NOT be altered in the `recurrenceOverrides`; it may only be set on the base object. + // + // This indicates the event will accept "party crasher" RSVPs via iTIP, subject to any + // other domain-specific restrictions, and users may add themselves to the event via JMAP as + // long as they have the mayRSVP permission for the calendar. + // + // This is a JMAP addition to JSCalendar. + // + // default: false + MayInviteSelf bool `json:"mayInviteSelf,omitzero"` + + // If true, any current participant with the `attendee` role may add new participants with the + // `attendee` role to the event. + // + // This property MUST NOT be altered in the `recurrenceOverrides`; it may only be set on the base object. + // + // The `mayRSVP` permission for the calendar is also required in conjunction with this event property + // for users to be allowed to make this change via JMAP. + // + // This is a JMAP addition to JSCalendar. + // + // default: false + MayInviteOthers bool `json:"mayInviteOthers,omitzero"` + + // If true, only the owners of the event may see the full set of participants. + // + // Other sharees of the event may only see the owners and themselves. + // + // This property MUST NOT be altered in the `recurrenceOverrides`; it may only be set on the base object. + // + // This is a JMAP addition to JSCalendar. + // + // default: false + HideAttendees bool `json:"hideAttendees,omitzero"` } type Event struct { diff --git a/pkg/jscontact/jscontact_model.go b/pkg/jscontact/jscontact_model.go index 12c2b156af..e9d3c4794f 100644 --- a/pkg/jscontact/jscontact_model.go +++ b/pkg/jscontact/jscontact_model.go @@ -11,35 +11,373 @@ import ( "time" ) +// The kind of the name component. +// +// !- `title`: an honorific title or prefix, e.g., `Mr.`, `Ms.`, or `Dr.` +// !- `given`: a given name, also known as "first name" or "personal name" +// !- `given2`: a name that appears between the given and surname such as a middle name or patronymic name +// !- `surname`: a surname, also known as "last name" or "family name" +// !- `surname2`: a secondary surname (used in some cultures), also known as "maternal surname" +// !- `credential`: a credential, also known as "accreditation qualifier" or "honorific suffix", e.g., `B.A.`, `Esq.` +// !- `generation`: a generation marker or qualifier, e.g., `Jr.` or `III` +// !- `separator`: a formatting separator between two ordered name non-separator components; the value property of the component includes the verbatim separator, for example, a hyphen character or even an empty string. This value has higher precedence than the defaultSeparator property of the Name. Implementations MUST NOT insert two consecutive separator components; instead, they SHOULD insert a single separator component with the combined value; this component kind MUST NOT be set if the `Name` `isOrdered` property value is `false` type NameComponentKind string + +// The kind of the address component. +// +// The enumerated values are: +// ! `room`: the room, suite number, or identifier +// ! `apartment`: the extension designation such as the apartment number, unit, or box number +// ! `floor`: the floor or level the address is located on +// ! `building`: the building, tower, or condominium the address is located in +// ! `number`: the street number, e.g., `"123"`; this value is not restricted to numeric values and can include any value such +// as number ranges (`"112-10"`), grid style (`"39.2 RD"`), alphanumerics (`"N6W23001"`), or fractionals (`"123 1/2"`) +// ! `name`: the street name +// ! `block`: the block name or number +// ! `subdistrict`: the subdistrict, ward, or other subunit of a district +// ! `district`: the district name +// ! `locality`: the municipality, city, town, village, post town, or other locality +// ! `region`: the administrative area such as province, state, prefecture, county, or canton +// ! `postcode`: the postal code, post code, ZIP code, or other short code associated with the address by the relevant country's postal system +// ! `country`: the country name +// ! `direction`: the cardinal direction or quadrant, e.g., "north" +// ! `landmark`: the publicly known prominent feature that can substitute the street name and number, e.g., "White House" or "Taj Mahal" +// ! `postOfficeBox`: the post office box number or identifier +// ! `separator`: a formatting separator between two ordered address non-separator components; the value property of the component includes the +// verbatim separator, for example, a hyphen character or even an empty string; this value has higher precedence than the `defaultSeparator` property +// of the `Address`; implementations MUST NOT insert two consecutive separator components; instead, they SHOULD insert a single separator component +// with the combined value; this component kind MUST NOT be set if the `Address` `isOrdered` property value is `false`. type AddressComponentKind string + +// The relationship of the related Card to the Card, defined as a set of relation types. +// +// The keys in the set define the relation type; the values for each key in the set MUST be "true". +// +// The relationship between the two objects is undefined if the set is empty. +// +// The initial list of enumerated relation types matches the IANA-registered TYPE `IANA-vCard“ +// parameter values of the vCard RELATED property ([Section 6.6.6 of RFC6350]): +// !- `acquaintance` +// !- `agent` +// !- `child` +// !- `co-resident` +// !- `co-worker` +// !- `colleague` +// !- `contact` +// !- `crush` +// !- `date` +// !- `emergency` +// !- `friend` +// !- `kin` +// !- `me` +// !- `met` +// !- `muse` +// !- `neighbor` +// !- `parent` +// !- `sibling` +// !- `spouse` +// !- `sweetheart` +// +// [Section 6.6.6 of RFC6350]: https://www.rfc-editor.org/rfc/rfc6350.html#section-6.6.6 type Relationship string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type MediaContext string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type NicknameContext string + +// The contexts in which to use this address. +// +// The boolean value MUST be `true`. +// +// In addition to the common contexts, allowed key values are: +// ! `billing`: an address to be used for billing +// ! `delivery`: an address to be used for delivering physical items type AddressContext string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type DirectoryContext string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type EmailAddressContext string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type OnlineServiceContext string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type OrganizationContext string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type PronounsContext string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type PhoneContext string + +// The set of contact features that the phone number may be used for. +// +// The set is represented as an object, with each key being a method type. +// +// The boolean value MUST be `true`. +// +// The enumerated values are: +// !- `mobile`: this number is for a mobile phone +// !- `voice`: this number supports calling by voice +// !- `text`: this number supports text messages (SMS) +// !- `video`: this number supports video conferencing +// !- `main-number`: this number is a main phone number such as the number of the front desk at a company, as opposed to a direct-dial number of an individual employee +// !- `textphone`: this number is for a device for people with hearing or speech difficulties +// !- `fax`: this number supports sending faxes +// !- `pager`: this number is for a pager or beeper type PhoneFeature string + +// The organizational or situational kind of the title. +// +// Some organizations and individuals distinguish between titles as organizational +// positions and roles as more temporary assignments such as in project management. +// +// The enumerated values are: +// !- `title` +// !- `role` type TitleKind string + +// The grammatical gender to use in salutations and other grammatical constructs. +// +// For example, the German language distinguishes by grammatical gender in salutations such as +// `Sehr geehrte` (feminine) and `Sehr geehrter` (masculine). +// +// The enumerated values are: +// !- `animate` +// !- `common` +// !- `feminine` +// !- `inanimate` +// !- `masculine` +// !- `neuter` +// +// Note that the grammatical gender does not allow inferring the gender identities or assigned +// sex of the contact. type GrammaticalGenderType string + +// The kind of anniversary. +// +// The enumerated values are: +// ! `birth`: a birthday anniversary +// ! `death`: a deathday anniversary +// ! `wedding`: a wedding day anniversary type AnniversaryKind string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type LanguagePrefContext string + +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type SchedulingAddressContext string + +// The kind of personal information. +// +// The enumerated values are: +// ! `expertise`: a field of expertise or a credential +// ! `hobby`: a hobby +// ! `interest`: an interest type PersonalInfoKind string + +// The level of expertise or engagement in hobby or interest. +// +// The enumerated values are: +// ! `high` +// ! `medium` +// ! `low` type PersonalInfoLevel string + +// The kind of the entity the Card represents (default: `individual“). +// +// Values are: +// !- `individual`: a single person +// !- `group`: a group of people or entities +// !- `org`: an organization +// !- `location`: a named location +// !- `device`: a device such as an appliance, a computer, or a network element +// !- `application`: a software application +// +// example: individual type ContactCardKind string + +// The kind of the `Directory` resource. +// +// The allowed values are defined in the property definition that makes use of the Resource type. +// +// Some property definitions may change this property from being optional to mandatory. +// +// A contact card with a `kind` property equal to `group` represents a group of contacts. +// +// Clients often present these separately from other contact cards. +// +// The `members` property, as defined in [RFC 9553, Section 2.1.6], contains a set of UIDs for other +// contacts that are the members of this group. +// +// Clients should consider the group to contain any `ContactCard` with a matching UID, from +// any account they have access to with support for the `urn:ietf:params:jmap:contacts` capability. +// +// UIDs that cannot be found SHOULD be ignored but preserved. +// +// For example, suppose a user adds contacts from a shared address book to their private group, then +// temporarily loses access to this address book. The UIDs cannot be resolved so the contacts will +// disappear from the group. However, if they are given permission to access the data again the UIDs +// will be found and the contacts will reappear. +// +// [RFC 9553, Section 2.1.8]: https://www.rfc-editor.org/rfc/rfc9553#members type DirectoryKind string + +// The kind of the `Calendar` resource. +// +// The allowed values are defined in the property definition that makes use of the Resource type. +// +// Some property definitions may change this property from being optional to mandatory. +// +// A contact card with a `kind` property equal to `group` represents a group of contacts. +// +// Clients often present these separately from other contact cards. +// +// The `members` property, as defined in [RFC 9553, Section 2.1.6], contains a set of UIDs for other +// contacts that are the members of this group. +// +// Clients should consider the group to contain any `ContactCard` with a matching UID, from +// any account they have access to with support for the `urn:ietf:params:jmap:contacts` capability. +// +// UIDs that cannot be found SHOULD be ignored but preserved. +// +// For example, suppose a user adds contacts from a shared address book to their private group, then +// temporarily loses access to this address book. The UIDs cannot be resolved so the contacts will +// disappear from the group. However, if they are given permission to access the data again the UIDs +// will be found and the contacts will reappear. +// +// [RFC 9553, Section 2.1.8]: https://www.rfc-editor.org/rfc/rfc9553#members type CalendarKind string -type CryptoKeyKind string + +// The kind of the `Link` resource. +// +// The allowed values are defined in the property definition that makes use of the Resource type. +// +// Some property definitions may change this property from being optional to mandatory. +// +// A contact card with a `kind` property equal to `group` represents a group of contacts. +// +// Clients often present these separately from other contact cards. +// +// The `members` property, as defined in [RFC 9553, Section 2.1.6], contains a set of UIDs for other +// contacts that are the members of this group. +// +// Clients should consider the group to contain any `ContactCard` with a matching UID, from +// any account they have access to with support for the `urn:ietf:params:jmap:contacts` capability. +// +// UIDs that cannot be found SHOULD be ignored but preserved. +// +// For example, suppose a user adds contacts from a shared address book to their private group, then +// temporarily loses access to this address book. The UIDs cannot be resolved so the contacts will +// disappear from the group. However, if they are given permission to access the data again the UIDs +// will be found and the contacts will reappear. +// +// [RFC 9553, Section 2.1.8]: https://www.rfc-editor.org/rfc/rfc9553#members type LinkKind string + +// The kind of the `Media` resource. +// +// The allowed values are defined in the property definition that makes use of the Resource type. +// +// Some property definitions may change this property from being optional to mandatory. +// +// A contact card with a `kind` property equal to `group` represents a group of contacts. +// +// Clients often present these separately from other contact cards. +// +// The `members` property, as defined in [RFC 9553, Section 2.1.6], contains a set of UIDs for other +// contacts that are the members of this group. +// +// Clients should consider the group to contain any `ContactCard` with a matching UID, from +// any account they have access to with support for the `urn:ietf:params:jmap:contacts` capability. +// +// UIDs that cannot be found SHOULD be ignored but preserved. +// +// For example, suppose a user adds contacts from a shared address book to their private group, then +// temporarily loses access to this address book. The UIDs cannot be resolved so the contacts will +// disappear from the group. However, if they are given permission to access the data again the UIDs +// will be found and the contacts will reappear. +// +// [RFC 9553, Section 2.1.8]: https://www.rfc-editor.org/rfc/rfc9553#members type MediaKind string + +// The contexts in which to use this resource. +// +// The contexts in which to use the contact information. +// +// For example, someone might have distinct phone numbers for `work` and `private` contexts and may set the +// desired context on the respective phone number in the `phones` property. +// +// This section defines common contexts. +// +// Additional contexts may be defined in the properties or data types that make use of this property. +// +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type CalendarContext string + +// The contexts in which to use this resource. +// +// The contexts in which to use the contact information. +// +// For example, someone might have distinct phone numbers for `work` and `private` contexts and may set the +// desired context on the respective phone number in the `phones` property. +// +// This section defines common contexts. +// +// Additional contexts may be defined in the properties or data types that make use of this property. +// +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type CryptoKeyContext string + +// The contexts in which to use this resource. +// +// The contexts in which to use the contact information. +// +// For example, someone might have distinct phone numbers for `work` and `private` contexts and may set the +// desired context on the respective phone number in the `phones` property. +// +// This section defines common contexts. +// +// Additional contexts may be defined in the properties or data types that make use of this property. +// +// The enumerated common context values are: +// !- `private`: the contact information that may be used in a private context. +// !- `work`: the contact information that may be used in a professional context. type LinkContext string + +// The JSContact version of this Card. +// +// The value MUST be one of the IANA-registered JSContact Version values for the version property. +// +// example: 1.0 type JSContactVersion string type TypeOfAddress string @@ -598,32 +936,6 @@ type CryptoKey struct { // The value MUST be `CryptoKey`, if set. Type TypeOfCryptoKey `json:"@type,omitempty"` - // The kind of the resource. - // - // The allowed values are defined in the property definition that makes use of the Resource type. - // - // Some property definitions may change this property from being optional to mandatory. - // - // A contact card with a `kind` property equal to `group` represents a group of contacts. - // - // Clients often present these separately from other contact cards. - // - // The `members` property, as defined in [RFC 9553, Section 2.1.6], contains a set of UIDs for other - // contacts that are the members of this group. - // - // Clients should consider the group to contain any `ContactCard` with a matching UID, from - // any account they have access to with support for the `urn:ietf:params:jmap:contacts` capability. - // - // UIDs that cannot be found SHOULD be ignored but preserved. - // - // For example, suppose a user adds contacts from a shared address book to their private group, then - // temporarily loses access to this address book. The UIDs cannot be resolved so the contacts will - // disappear from the group. However, if they are given permission to access the data again the UIDs - // will be found and the contacts will reappear. - // - // [RFC 9553, Section 2.1.8]: https://www.rfc-editor.org/rfc/rfc9553#members - Kind CryptoKeyKind `json:"kind,omitempty"` - // The resource value. // // This MUST be a URI as defined in [Section 3 of RFC3986]. @@ -1229,7 +1541,7 @@ type Name struct { // The indicator if the name components in the components property are ordered. // - // Default: `false` + // default: false IsOrdered bool `json:"isOrdered,omitzero"` // The default separator to insert between name component values when concatenating all name component values to a single String. @@ -1495,24 +1807,24 @@ type AddressComponent struct { // The kind of the address component. // // The enumerated values are: - // !- `room`: the room, suite number, or identifier - // !- `apartment`: the extension designation such as the apartment number, unit, or box number - // !- `floor`: the floor or level the address is located on - // !- `building`: the building, tower, or condominium the address is located in - // !- `number`: the street number, e.g., `"123"`; this value is not restricted to numeric values and can include any value such + // ! `room`: the room, suite number, or identifier + // ! `apartment`: the extension designation such as the apartment number, unit, or box number + // ! `floor`: the floor or level the address is located on + // ! `building`: the building, tower, or condominium the address is located in + // ! `number`: the street number, e.g., `"123"`; this value is not restricted to numeric values and can include any value such // as number ranges (`"112-10"`), grid style (`"39.2 RD"`), alphanumerics (`"N6W23001"`), or fractionals (`"123 1/2"`) - // !- `name`: the street name - // !- `block`: the block name or number - // !- `subdistrict`: the subdistrict, ward, or other subunit of a district - // !- `district`: the district name - // !- `locality`: the municipality, city, town, village, post town, or other locality - // !- `region`: the administrative area such as province, state, prefecture, county, or canton - // !- `postcode`: the postal code, post code, ZIP code, or other short code associated with the address by the relevant country's postal system - // !- `country`: the country name - // !- `direction`: the cardinal direction or quadrant, e.g., "north" - // !- `landmark`: the publicly known prominent feature that can substitute the street name and number, e.g., "White House" or "Taj Mahal" - // !- `postOfficeBox`: the post office box number or identifier - // !- `separator`: a formatting separator between two ordered address non-separator components; the value property of the component includes the + // ! `name`: the street name + // ! `block`: the block name or number + // ! `subdistrict`: the subdistrict, ward, or other subunit of a district + // ! `district`: the district name + // ! `locality`: the municipality, city, town, village, post town, or other locality + // ! `region`: the administrative area such as province, state, prefecture, county, or canton + // ! `postcode`: the postal code, post code, ZIP code, or other short code associated with the address by the relevant country's postal system + // ! `country`: the country name + // ! `direction`: the cardinal direction or quadrant, e.g., "north" + // ! `landmark`: the publicly known prominent feature that can substitute the street name and number, e.g., "White House" or "Taj Mahal" + // ! `postOfficeBox`: the post office box number or identifier + // ! `separator`: a formatting separator between two ordered address non-separator components; the value property of the component includes the // verbatim separator, for example, a hyphen character or even an empty string; this value has higher precedence than the `defaultSeparator` property // of the `Address`; implementations MUST NOT insert two consecutive separator components; instead, they SHOULD insert a single separator component // with the combined value; this component kind MUST NOT be set if the `Address` `isOrdered` property value is `false`. @@ -1548,7 +1860,9 @@ type Address struct { // and the `defaultSeparator` property MUST NOT be set. Components []AddressComponent `json:"components,omitempty"` - // The indicator if the address components in the components property are ordered (default: `false`). + // The indicator if the address components in the components property are ordered + // + // default: false IsOrdered bool `json:"isOrdered,omitzero"` // The Alpha-2 country code as of [ISO.3166-1]. @@ -1573,8 +1887,8 @@ type Address struct { // The boolean value MUST be `true`. // // In addition to the common contexts, allowed key values are: - // !- `billing`: an address to be used for billing - // !- `delivery`: an address to be used for delivering physical items + // ! `billing`: an address to be used for billing + // ! `delivery`: an address to be used for delivering physical items Contexts map[AddressContext]bool `json:"contexts,omitempty"` // The full address, including street, region, or country. @@ -1704,9 +2018,9 @@ type Anniversary struct { // The kind of anniversary. // // The enumerated values are: - // !- `birth`: a birthday anniversary - // !- `death`: a deathday anniversary - // !- `wedding`: a wedding day anniversary + // ! `birth`: a birthday anniversary + // ! `death`: a deathday anniversary + // ! `wedding`: a wedding day anniversary Kind AnniversaryKind `json:"kind"` // The date of the anniversary in the Gregorian calendar. @@ -1748,9 +2062,9 @@ type PersonalInfo struct { // The kind of personal information. // // The enumerated values are: - // !- `expertise`: a field of expertise or a credential - // !- `hobby`: a hobby - // !- `interest`: an interest + // ! `expertise`: a field of expertise or a credential + // ! `hobby`: a hobby + // ! `interest`: an interest Kind PersonalInfoKind `json:"kind"` // The actual information. @@ -1759,9 +2073,9 @@ type PersonalInfo struct { // The level of expertise or engagement in hobby or interest. // // The enumerated values are: - // !- `high` - // !- `medium` - // !- `low` + // ! `high` + // ! `medium` + // ! `low` Level PersonalInfoLevel `json:"level,omitempty"` // The position of the personal information in the list of all `PersonalInfo` objects that @@ -1880,8 +2194,6 @@ type ContactCard struct { // // A group Card without the members property can be considered an abstract grouping or one whose members // are known empirically (e.g., `IETF Participants`). - // - // example: {"kind": "group", "name": {"full": "The Doe family"}, "uid": "urn:uuid:ab4310aa-fa43-11e9-8f0b-362b9e155667", "members": {"urn:uuid:03a0e51f-d1aa-4385-8a53-e29025acd8af": true, "urn:uuid:b8767877-b4a1-4c70-9acc-505d3819e519": true} Members map[string]bool `json:"members,omitempty"` // The identifier for the product that created the Card. @@ -1908,8 +2220,6 @@ type ContactCard struct { // } // } // ``` - // - // example: "relatedTo": {"urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6": {"relation": {"friend": true}}, "8cacdfb7d1ffdb59@example.com": {"relation": {}}} RelatedTo map[string]Relation `json:"relatedTo,omitempty"` // An identifier that associates the object as the same across different systems, address books, and views. diff --git a/services/groupware/Makefile b/services/groupware/Makefile index 5c353a67ac..afcae86926 100644 --- a/services/groupware/Makefile +++ b/services/groupware/Makefile @@ -22,7 +22,7 @@ node_modules: .PHONY: swagger.yml swagger.yml: apidoc.yml tsnode - swagger generate spec --include='groupware' --include='jmap' --scan-models --input=$< | NODE_OPTIONS='--no-warnings' pnpm exec ts-node apidoc-process.ts > $@ + swagger generate spec --include='groupware' --include='jmap' --include='jscalendar' --include='jscontact' --scan-models --input=$< | NODE_OPTIONS='--no-warnings' pnpm exec ts-node apidoc-process.ts > $@ APIDOC_PORT=9999 diff --git a/services/groupware/api-examples.yaml b/services/groupware/api-examples.yaml index 6729b24499..e8d97e073a 100644 --- a/services/groupware/api-examples.yaml +++ b/services/groupware/api-examples.yaml @@ -23,7 +23,7 @@ examples: email: 'secgen@earth.gov' emailFroms: - name: 'Chrissie' - email: 'secgen@earth.gov' + email: 'secgen@earth.gov' emailTos: - name: 'Camina Drummer' email: 'drummer@opa.org' @@ -66,4 +66,239 @@ examples: size: $emailSize EmailBodyPart: size: $attachmentSize + ContactCardKind: 'individual' + ContactCard: + '@type': Contact + addressBookIds: + - aaabc2aa + - c329aaze + addresses: + bu7icohc: + '@type': Address + components: + - '@type': AddressComponent + kind: number + value: '12' + - '@type': AddressComponent + kind: separator + value: ' ' + - '@type': AddressComponent + kind: name + value: 'Gravity Street' + - '@type': AddressComponent + kind: locality + value: 'Medina Station' + - '@type': AddressComponent + kind: region + value: 'Outer Belt' + - '@type': AddressComponent + kind: separator + value: ' ' + - '@type': AddressComponent + kind: postcode + value: '618291' + - '@type': AddressComponent + kind: country + value: 'Sol' + isOrdered: true + defaultSeparator: ', ' + countryCode: 'CA' + coordinates: 'geo:43.6466107,-79.3889872' + timeZone: EDT + contexts: + - delivery: true + - work: true + full: '12 Gravity Street, Medina Station, Outer Belt 618291, Sol' + pref: 1 + anniversaries: + yeex2wiu: + '@type': Anniversary + kind: birth + date: + '@type': PartialDate + year: 1983 + month: 7 + day: 18 + calendarScale: iso8601 + calendars: + uin5daen: + '@type': Calendar + kind: calendar + uri: 'https://ceres.org/calendars/@cdrummer/c1' + mediaType: application/jscontact+json + contexts: + private: true + work: true + pref: 1 + label: main + created: '2025-09-30T11:00:12Z' + cryptoKeys: + iez1thoo: + '@type': CryptoKey + uri: 'https://opa.org/keys/@cdrummer.gpg' + mediaType: application/pgp-keys + contexts: + private: true + work: true + pref: 10 + label: opa + directories: + cich5tah: + '@type': Directory + kind: entry + uri: https://directory.opa.org/addrbook/cdrummer/Camina%20Drummer.vcf + mediaType: text/vcard + ju5iemoh: + '@type': Directory + kind: directory + uri: ldap://ldap.opa.org/o=OPA,ou=Bosmangs + pref: 1 + emails: + xush7tae: + '@type': EmailAddress + address: cdrummer@opa.org + contexts: + work: true + private: true + pref: 10 + label: opa + ra1ohjah: + '@type': EmailAddress + address: camina.drummer@ceres.net + contexts: + private: true + pref: 20 + id: em8ahgha + keywords: + bosmang: true + opa: true + tycho: true + rebel: true + kind: 'individual' + language: en-GB + links: + eech3oib: + '@type': Link + kind: contact + uri: mailto:contact@opa.org + pref: 1 + localizations: {} + media: + ohchae4a: + '@type': Media + kind: photo + uri: https://static.wikia.nocookie.net/expanse/images/c/c7/Tycho-stn-14.png/revision/latest/scale-to-width-down/1000?cb=20170225140521 + mediaType: image/png + members: {} + name: + '@type': Name + components: + - '@type': NameComponent + kind: given + value: Camina + - '@type': NameComponent + kind: surname + value: Drummer + isOrdered: true + defaultSeparator: ' ' + full: 'Camina Drummer' + nicknames: + aumiez4y: + '@type': Nickname + name: Bosmang + contexts: + work: true + pref: 1 + notes: + aep1poov: + '@type': Note + created: '2025-09-30T11:00:12Z' + author: + '@type': Author + name: 'expanse.fandom.com' + uri: 'https://expanse.fandom.com/wiki/Camina_Drummer_(TV)' + note: 'Cammina Drummer is a strong-willed, pragmatic, and no-nonsense Belter captain. Having a strong connection to her roots and her cultural identity, Drummer is a Belter through and through: She is resilient and adaptable, treats her crew with respect and equality, and is committed to the Belter way of life, which involves hard work, communal life shared with others, and not taking anything for granted.' + onlineServices: + ohne9oum: + '@type': OnlineService + service: 'Ring Network' + uri: 'https://ring.example.com/contact/@cdrummer' + user: '@cdrummer18219' + contexts: + private: true + work: true + label: ring + organizations: + eesa1aiv: + '@type': Organization + name: 'Outer Planets Alliance' + sortAs: OPA + contexts: + work: true + personalInfo: + vibi6ine: + '@type': PersonalInfo + kind: expertise + value: loyalty + level: high + phones: + xaecie9e: + '@type': Phone + number: '+1-999-555-1234' + features: + main-number: true + mobile: true + voice: true + text: true + video: true + contexts: + private: true + work: true + pref: 1 + label: main + preferredLanguages: + en: + '@type': LanguagePref + language: en-GB + contexts: + private: true + work: true + prodId: 'Mock 0.0' + relatedTo: + 'urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6': + '@type': Relation + relation: + friend: true + '8cacdfb7d1ffdb59@example.com': + '@type': Relation + relation: {} + schedulingAddresses: + xeith5qu: + '@type': SchedulingAddress + uri: 'https://scheduling.example.com/@cdrummer/c1' + contexts: + private: true + work: true + pref: 1 + label: main + speakToAs: + '@type': SpeakToAs + grammaticalGender: feminine + pronouns: + taefie5a: + '@type': Pronouns + pronouns: 'she/her' + contexts: + privatr: true + pref: 1 + titles: + sheetei4: + '@type': Title + name: Bosmang + kind: title + organizationId: eesa1aiv + uid: '05a6dd3b-f393-438e-a858-9024471fd9fc' + updated: '2025-09-30T15:24:01Z' + version: '1.0' + diff --git a/services/groupware/api-params.yaml b/services/groupware/api-params.yaml index 15fad6e772..2b5b56ed1e 100644 --- a/services/groupware/api-params.yaml +++ b/services/groupware/api-params.yaml @@ -5,3 +5,7 @@ params: description: The identifier of the Mailbox to perform this operation on emailid: description: The identifier of the Email to perform this operation on + addressbookid: + description: The identifier of the AddressBook to perform this operation on + calendarid: + description: The identifier of the Calendar to perform this operation on diff --git a/services/groupware/apidoc-process.ts b/services/groupware/apidoc-process.ts index f1a19429a1..a85a63b62b 100644 --- a/services/groupware/apidoc-process.ts +++ b/services/groupware/apidoc-process.ts @@ -48,6 +48,8 @@ interface Definition { title: string required: string[] properties: {[property:string]:Property} + example: string + examples: string[] } interface OpenApi { @@ -82,6 +84,14 @@ process.stdin.on('data', (chunk) => { const usedExamples = new Set() const unresolvedExampleReferences = new Set() +function processDescription(description: string|null|undefined): string|null|undefined { + if (description !== null && description !== undefined) { + return description.split("\n").map(line => line.replace(/^(\s*)![\*\-]?/, '$1*')).join("\n") + } else { + return description + } +} + process.stdin.on('end', () => { try { const paramsConfig = yaml.load(fs.readFileSync(API_PARAMS_CONFIG_FILE, 'utf8')) as ParamsConfig @@ -116,18 +126,21 @@ process.stdin.on('end', () => { // do some magic with the formatting of endpoint descriptions: for (const verb in pathData) { const verbData = pathData[verb] - if (verbData.description !== null && verbData.description !== undefined) { - verbData.description = verbData.description.split("\n").map((line) => { - return line.replace(/^(\s*)!/, '$1*') - }).join("\n") - } + verbData.description = processDescription(verbData.description) } } for (const def in data.definitions) { const defData = data.definitions[def] + + if (def.startsWith('TypeOf')) { + const value = def.substring('TypeOf'.length) + defData.title = value + defData.example = value + } + + const injects = exampleInjects[def] || {} if (defData.properties !== null && defData.properties !== undefined) { - const injects = exampleInjects[def] || {} for (const prop in defData.properties as any) { const propData = defData.properties[prop] @@ -148,6 +161,14 @@ process.stdin.on('end', () => { } } } + + propData.description = processDescription(propData.description) + } + } else { + if (typeof(injects) === 'string') { + defData.example = injects + } else if (Array.isArray(injects)) { + defData.examples = injects } } } @@ -183,5 +204,4 @@ process.stdin.on('end', () => { console.error("Unknown error occurred") } } -}); - +}) diff --git a/services/groupware/apidoc.yml b/services/groupware/apidoc.yml index 069842ac6a..c4bcbc26f3 100644 --- a/services/groupware/apidoc.yml +++ b/services/groupware/apidoc.yml @@ -17,6 +17,18 @@ tags: - name: email x-displayName: Emails description: APIs about emails + - name: addressbook + x-displayName: Address Books + description: APIs about address books + - name: contact + x-displayName: Contacts + description: APIs about contacts + - name: calendar + x-displayName: Calendars + description: APIs about calendars + - name: event + x-displayName: Events + description: APIs about calendar events - name: vacation x-displayName: Vacation Responses description: APIs about vacation responses @@ -33,6 +45,14 @@ x-tagGroups: - mailbox - email - vacation + - name: Contacts + tags: + - addressbook + - contact + - name: Events + tags: + - calendar + - event components: securitySchemes: api: diff --git a/services/groupware/package.json b/services/groupware/package.json index 749d280df4..f3431d248c 100644 --- a/services/groupware/package.json +++ b/services/groupware/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "@redocly/cli": "^2.2.0", + "@redocly/cli": "^2.2.2", "@types/js-yaml": "^4.0.9", "cheerio": "^1.1.2", "js-yaml": "^4.1.0", diff --git a/services/groupware/pkg/groupware/groupware_api_calendars.go b/services/groupware/pkg/groupware/groupware_api_calendars.go new file mode 100644 index 0000000000..046130cbc8 --- /dev/null +++ b/services/groupware/pkg/groupware/groupware_api_calendars.go @@ -0,0 +1,89 @@ +package groupware + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/opencloud-eu/opencloud/pkg/jmap" +) + +// When the request succeeds. +// swagger:response GetCalendars200 +type SwaggerGetCalendars200 struct { + // in: body + Body []jmap.Calendar +} + +// swagger:route GET /groupware/accounts/{account}/calendars calendar calendars +// Get all calendars of an account. +// +// responses: +// +// 200: GetCalendars200 +// 400: ErrorResponse400 +// 404: ErrorResponse404 +// 500: ErrorResponse500 +func (g *Groupware) GetCalendars(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + return response(AllCalendars, req.session.State) + }) +} + +// When the request succeeds. +// swagger:response GetCalendarById200 +type SwaggerGetCalendarById200 struct { + // in: body + Body struct { + *jmap.Calendar + } +} + +// swagger:route GET /groupware/accounts/{account}/calendars/{calendarid} calendar calendar_by_id +// Get all calendars of an account. +// +// responses: +// +// 200: GetCalendarById200 +// 400: ErrorResponse400 +// 404: ErrorResponse404 +// 500: ErrorResponse500 +func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + calendarId := chi.URLParam(r, UriParamAddressBookId) + // TODO replace with proper implementation + for _, calendar := range AllCalendars { + if calendar.Id == calendarId { + return response(calendar, req.session.State) + } + } + return notFoundResponse(req.session.State) + }) +} + +// When the request succeeds. +// swagger:response GetEventsInCalendar200 +type SwaggerGetEventsInCalendar200 struct { + // in: body + Body []jmap.CalendarEvent +} + +// swagger:route GET /groupware/accounts/{account}/calendars/{calendarid}/events event events_in_addressbook +// Get all the events in a calendarof an account by its identifier. +// +// responses: +// +// 200: GetEventsInCalendar200 +// 400: ErrorResponse400 +// 404: ErrorResponse404 +// 500: ErrorResponse500 +func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + calendarId := chi.URLParam(r, UriParamAddressBookId) + // TODO replace with proper implementation + events, ok := EventsMapByCalendarId[calendarId] + if !ok { + return notFoundResponse(req.session.State) + } + return response(events, req.session.State) + }) +} diff --git a/services/groupware/pkg/groupware/groupware_api_contacts.go b/services/groupware/pkg/groupware/groupware_api_contacts.go new file mode 100644 index 0000000000..7eca524aad --- /dev/null +++ b/services/groupware/pkg/groupware/groupware_api_contacts.go @@ -0,0 +1,91 @@ +package groupware + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/opencloud-eu/opencloud/pkg/jmap" + "github.com/opencloud-eu/opencloud/pkg/jscontact" +) + +// When the request succeeds. +// swagger:response GetAddressbooks200 +type SwaggerGetAddressbooks200 struct { + // in: body + Body []jmap.AddressBook +} + +// swagger:route GET /groupware/accounts/{account}/addressbooks addressbook addressbooks +// Get all addressbooks of an account. +// +// responses: +// +// 200: GetAddressbooks200 +// 400: ErrorResponse400 +// 404: ErrorResponse404 +// 500: ErrorResponse500 +func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + // TODO replace with proper implementation + return response(AllAddressBooks, req.session.State) + }) +} + +// When the request succeeds. +// swagger:response GetAddressbookById200 +type SwaggerGetAddressbookById200 struct { + // in: body + Body struct { + *jmap.AddressBook + } +} + +// swagger:route GET /groupware/accounts/{account}/addressbooks/{addressbookid} addressbook addressbook_by_id +// Get an addressbook of an account by its identifier. +// +// responses: +// +// 200: GetAddressbookById200 +// 400: ErrorResponse400 +// 404: ErrorResponse404 +// 500: ErrorResponse500 +func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + addressBookId := chi.URLParam(r, UriParamAddressBookId) + // TODO replace with proper implementation + for _, ab := range AllAddressBooks { + if ab.Id == addressBookId { + return response(ab, req.session.State) + } + } + return notFoundResponse(req.session.State) + }) +} + +// When the request succeeds. +// swagger:response GetContactsInAddressbook200 +type SwaggerGetContactsInAddressbook200 struct { + // in: body + Body []jscontact.ContactCard +} + +// swagger:route GET /groupware/accounts/{account}/addressbooks/{addressbookid}/contacts contact contacts_in_addressbook +// Get all the contacts in an addressbook of an account by its identifier. +// +// responses: +// +// 200: GetContactsInAddressbook200 +// 400: ErrorResponse400 +// 404: ErrorResponse404 +// 500: ErrorResponse500 +func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + addressBookId := chi.URLParam(r, UriParamAddressBookId) + // TODO replace with proper implementation + contactCards, ok := ContactsMapByAddressBookId[addressBookId] + if !ok { + return notFoundResponse(req.session.State) + } + return response(contactCards, req.session.State) + }) +} diff --git a/services/groupware/pkg/groupware/groupware_mock_calendars.go b/services/groupware/pkg/groupware/groupware_mock_calendars.go new file mode 100644 index 0000000000..363fda8bc4 --- /dev/null +++ b/services/groupware/pkg/groupware/groupware_mock_calendars.go @@ -0,0 +1,151 @@ +package groupware + +import ( + "github.com/opencloud-eu/opencloud/pkg/jmap" + "github.com/opencloud-eu/opencloud/pkg/jscalendar" +) + +var C1 = jmap.Calendar{ + Id: "thoo5she", + Name: "Personal Calendar", + Description: "Camina Drummer's Personal Calendar", + Color: "purple", + SortOrder: 1, + IsSubscribed: true, + IsVisible: true, + IsDefault: true, + IncludeInAvailability: jmap.IncludeInAvailabilityAll, + DefaultAlertsWithTime: map[string]jscalendar.Alert{ + "eing7doh": { + Type: jscalendar.AlertType, + Trigger: jscalendar.AbsoluteTrigger{ + Type: jscalendar.AbsoluteTriggerType, + When: MustParse("2025-09-30T20:34:12Z"), + }, + }, + }, + DefaultAlertsWithoutTime: map[string]jscalendar.Alert{ + "oayooy0u": { + Type: jscalendar.AlertType, + Trigger: jscalendar.OffsetTrigger{ + Type: jscalendar.OffsetTriggerType, + Offset: "PT5M", + RelativeTo: jscalendar.RelativeToStart, + }, + }, + }, + TimeZone: "CEST", + ShareWith: map[string]jmap.CalendarRights{ + "ahn0doo8": { + MayReadFreeBusy: true, + MayReadItems: true, + MayWriteAll: true, + MayWriteOwn: true, + MayUpdatePrivate: true, + MayRSVP: false, + MayAdmin: false, + MayDelete: false, + }, + }, + MyRights: &jmap.CalendarRights{ + MayReadFreeBusy: true, + MayReadItems: true, + MayWriteAll: false, + MayWriteOwn: false, + MayUpdatePrivate: true, + MayRSVP: true, + MayAdmin: false, + MayDelete: false, + }, +} + +var AllCalendars = []jmap.Calendar{C1} + +var E1 = jmap.CalendarEvent{ + Id: "ovei9oqu", + Event: jscalendar.Event{ + Type: jscalendar.EventType, + Start: jscalendar.LocalDateTime{Time: MustParse("2025-09-30T12:00:00Z")}, + Duration: "PT30M", + Status: jscalendar.StatusConfirmed, + Object: jscalendar.Object{ + CommonObject: jscalendar.CommonObject{ + Uid: "9a7ab91a-edca-4988-886f-25e00743430d", + ProdId: "Mock 0.0", + Created: MustParse("2025-09-29T16:17:18Z"), + Updated: MustParse("2025-09-29T16:17:18Z"), + Title: "Meeting of the Minds", + Description: "Internal meeting about the grand strategy for the future", + DescriptionContentType: "text/plain", + Links: map[string]jscalendar.Link{ + "cai0thoh": { + Type: jscalendar.LinkType, + Href: "https://example.com/9a7ab91a-edca-4988-886f-25e00743430d", + Rel: jscalendar.RelAbout, + ContentType: "text/html", + }, + }, + Locale: "en-US", + Keywords: map[string]bool{ + "meeting": true, + "secret": true, + }, + Categories: map[string]bool{ + "secret": true, + "internal": true, + }, + Color: "purple", + TimeZones: map[string]jscalendar.TimeZone{ + "airee8ai": { + Type: jscalendar.TimeZoneType, + TzId: "CEST", + }, + }, + }, + RelatedTo: map[string]jscalendar.Relation{}, + Sequence: 0, + Method: jscalendar.MethodAdd, + ShowWithoutTime: false, + Locations: map[string]jscalendar.Location{ + "ux1uokie": { + Type: jscalendar.LocationType, + Name: "office", + Description: "Office meeting room upstairs", + LocationTypes: map[jscalendar.LocationTypeOption]bool{ + jscalendar.LocationTypeOptionOffice: true, + }, + RelativeTo: jscalendar.LocationRelationStart, + TimeZone: "CEST", + Coordinates: "geo:52.5334956,13.4079872", + Links: map[string]jscalendar.Link{ + "eefe2pax": { + Type: jscalendar.LinkType, + Href: "https://example.com/office", + }, + }, + }, + }, + VirtualLocations: map[string]jscalendar.VirtualLocation{ + "em4eal0o": { + Type: jscalendar.VirtualLocationType, + Name: "opentalk", + Description: "The opentalk Conference Room", + Uri: "https://meet.opentalk.eu", + Features: map[jscalendar.VirtualLocationFeature]bool{ + jscalendar.VirtualLocationFeatureAudio: true, + jscalendar.VirtualLocationFeatureChat: true, + jscalendar.VirtualLocationFeatureVideo: true, + jscalendar.VirtualLocationFeatureScreen: true, + }, + }, + }, + // TODO more properties, a lot more properties + }, + }, +} + +var EventsMapByCalendarId = map[string][]jmap.CalendarEvent{ + C1.Id: { + E1, + }, +} diff --git a/services/groupware/pkg/groupware/groupware_mock_contacts.go b/services/groupware/pkg/groupware/groupware_mock_contacts.go new file mode 100644 index 0000000000..009f0a4376 --- /dev/null +++ b/services/groupware/pkg/groupware/groupware_mock_contacts.go @@ -0,0 +1,568 @@ +package groupware + +import ( + "time" + + "github.com/opencloud-eu/opencloud/pkg/jmap" + "github.com/opencloud-eu/opencloud/pkg/jscontact" +) + +func MustParse(text string) time.Time { + t, err := time.Parse(time.RFC3339, text) + if err != nil { + panic(err) + } + return t +} + +var A1 = jmap.AddressBook{ + Id: "a1", + Name: "Contacts", + Description: "Your good old personal address book", + SortOrder: 1, + IsDefault: true, + IsSubscribed: true, + MyRights: jmap.AddressBookRights{ + MayRead: true, + MayWrite: true, + MayAdmin: true, + MayDelete: true, + }, +} + +var A2 = jmap.AddressBook{ + Id: "a2", + Name: "Collected Contacts", + Description: "This address book contains the contacts that were collected when sending and receiving emails", + SortOrder: 10, + IsDefault: false, + IsSubscribed: true, + MyRights: jmap.AddressBookRights{ + MayRead: true, + MayWrite: false, + MayAdmin: false, + MayDelete: false, + }, +} + +var CaminaDrummerContact = jscontact.ContactCard{ + Type: jscontact.ContactCardType, + Id: "cc1", + AddressBookIds: map[string]bool{ + A1.Id: true, + A2.Id: true, + }, + Version: jscontact.JSContactVersion_1_0, + Created: MustParse("2025-09-30T11:00:12Z").UTC(), + Updated: MustParse("2025-09-30T11:00:12Z").UTC(), + Kind: jscontact.ContactCardKindIndividual, + Language: "en-GB", + ProdId: "Mock 0.0", + Uid: "e8317f89-2a09-481d-8ce5-de3ab968dc63", + Name: &jscontact.Name{ + Type: jscontact.NameType, + Components: []jscontact.NameComponent{ + { + Type: jscontact.NameComponentType, + Kind: jscontact.NameComponentKindGiven, + Value: "Camina", + }, + { + Type: jscontact.NameComponentType, + Kind: jscontact.NameComponentKindSurname, + Value: "Drummer", + }, + }, + IsOrdered: true, + DefaultSeparator: " ", + Full: "Camina Drummer", + }, + Nicknames: map[string]jscontact.Nickname{ + "n1": { + Type: jscontact.NicknameType, + Name: "Bosmang", + Contexts: map[jscontact.NicknameContext]bool{ + jscontact.NicknameContextWork: true, + }, + Pref: 1, + }, + }, + Organizations: map[string]jscontact.Organization{ + "o1": { + Type: jscontact.OrganizationType, + Name: "Outer Planets Alliance", + SortAs: "OPA", + Contexts: map[jscontact.OrganizationContext]bool{ + jscontact.OrganizationContextWork: true, + }, + }, + }, + SpeakToAs: &jscontact.SpeakToAs{ + Type: jscontact.SpeakToAsType, + GrammaticalGender: jscontact.GrammaticalGenderFeminine, + Pronouns: map[string]jscontact.Pronouns{ + "p1": { + Type: jscontact.PronounsType, + Pronouns: "she/her", + Contexts: map[jscontact.PronounsContext]bool{ + jscontact.PronounsContextPrivate: true, + }, + Pref: 1, + }, + }, + }, + Titles: map[string]jscontact.Title{ + "t1": { + Type: jscontact.TitleType, + Name: "Bosmang", + Kind: jscontact.TitleKindTitle, + OrganizationId: "o1", + }, + }, + Emails: map[string]jscontact.EmailAddress{ + "e1": { + Type: jscontact.EmailAddressType, + Address: "cdrummer@opa.org", + Contexts: map[jscontact.EmailAddressContext]bool{ + jscontact.EmailAddressContextWork: true, + jscontact.EmailAddressContextPrivate: true, + }, + Pref: 10, + Label: "opa", + }, + "e2": { + Type: jscontact.EmailAddressType, + Address: "camina.drummer@ceres.net", + Contexts: map[jscontact.EmailAddressContext]bool{ + jscontact.EmailAddressContextPrivate: true, + }, + Pref: 20, + }, + }, + OnlineServices: map[string]jscontact.OnlineService{ + "s1": { + Type: jscontact.OnlineServiceType, + Service: "Ring Network", + Uri: "https://ring.example.com/contact/@cdrummer", + User: "@cdrummer18219", + Contexts: map[jscontact.OnlineServiceContext]bool{ + jscontact.OnlineServiceContextPrivate: true, + jscontact.OnlineServiceContextWork: true, + }, + Label: "ring", + }, + }, + Phones: map[string]jscontact.Phone{ + "p1": { + Type: jscontact.PhoneType, + Number: "+1-999-555-1234", + Features: map[jscontact.PhoneFeature]bool{ + jscontact.PhoneFeatureMainNumber: true, + jscontact.PhoneFeatureMobile: true, + jscontact.PhoneFeatureVoice: true, + jscontact.PhoneFeatureText: true, + jscontact.PhoneFeatureVideo: true, + }, + Contexts: map[jscontact.PhoneContext]bool{ + jscontact.PhoneContextPrivate: true, + jscontact.PhoneContextWork: true, + }, + Pref: 1, + Label: "main", + }, + }, + PreferredLanguages: map[string]jscontact.LanguagePref{ + "en": { + Type: jscontact.LanguagePrefType, + Language: "en-GB", + Contexts: map[jscontact.LanguagePrefContext]bool{ + jscontact.LanguagePrefContextPrivate: true, + jscontact.LanguagePrefContextWork: true, + }, + }, + }, + Calendars: map[string]jscontact.Calendar{ + "c1": { + Type: jscontact.CalendarType, + Kind: jscontact.CalendarKindCalendar, + Uri: "https://ceres.org/calendars/@cdrummer/c1", + MediaType: "application/jscontact+json", + Contexts: map[jscontact.CalendarContext]bool{ + jscontact.CalendarContextPrivate: true, + jscontact.CalendarContextWork: true, + }, + Pref: 1, + Label: "main", + }, + }, + SchedulingAddresses: map[string]jscontact.SchedulingAddress{ + "sa1": { + Type: jscontact.SchedulingAddressType, + Uri: "https://scheduling.example.com/@cdrummer/c1", + Contexts: map[jscontact.SchedulingAddressContext]bool{ + jscontact.SchedulingAddressContextPrivate: true, + jscontact.SchedulingAddressContextWork: true, + }, + Pref: 1, + Label: "main", + }, + }, + Addresses: map[string]jscontact.Address{ + "ad1": { + Type: jscontact.AddressType, + Components: []jscontact.AddressComponent{ + { + Kind: jscontact.AddressComponentKindNumber, + Value: "12", + }, + { + Kind: jscontact.AddressComponentKindSeparator, + Value: " ", + }, + { + Kind: jscontact.AddressComponentKindName, + Value: "Gravity Street", + }, + { + Kind: jscontact.AddressComponentKindLocality, + Value: "Medina Station", + }, + { + Kind: jscontact.AddressComponentKindRegion, + Value: "Outer Belt", + }, + { + Kind: jscontact.AddressComponentKindSeparator, + Value: " ", + }, + { + Kind: jscontact.AddressComponentKindPostcode, + Value: "618291", + }, + { + Kind: jscontact.AddressComponentKindCountry, + Value: "Sol", + }, + }, + IsOrdered: true, + DefaultSeparator: ", ", + CountryCode: "SOL", + Coordinates: "geo:43.6466107,-79.3889872", + TimeZone: "EDT", + Contexts: map[jscontact.AddressContext]bool{ + jscontact.AddressContextDelivery: true, + jscontact.AddressContextWork: true, + }, + Full: "12 Gravity Street, Medina Station, Outer Belt 618291, Sol", + Pref: 1, + }, + }, + CryptoKeys: map[string]jscontact.CryptoKey{ + "k1": { + Type: jscontact.CryptoKeyType, + Uri: "https://opa.org/keys/@cdrummer.gpg", + MediaType: "application/pgp-keys", + Contexts: map[jscontact.CryptoKeyContext]bool{ + jscontact.CryptoKeyContextPrivate: true, + jscontact.CryptoKeyContextWork: true, + }, + Pref: 10, + Label: "opa", + }, + }, + Directories: map[string]jscontact.Directory{ + "d1": { + Type: jscontact.DirectoryType, + Kind: jscontact.DirectoryKindEntry, + Uri: "https://directory.opa.org/addrbook/cdrummer/Camina%20Drummer.vcf", + MediaType: "text/vcard", + }, + "d2": { + Type: jscontact.DirectoryType, + Kind: jscontact.DirectoryKindDirectory, + Uri: "ldap://ldap.opa.org/o=OPA,ou=Bosmangs", + Pref: 1, + }, + }, + Links: map[string]jscontact.Link{ + "l1": { + Type: jscontact.LinkType, + Kind: jscontact.LinkKindContact, + Uri: "mailto:contact@opa.org", + Pref: 1, + }, + }, + Media: map[string]jscontact.Media{ + "m1": { + Type: jscontact.MediaType, + Kind: jscontact.MediaKindPhoto, + Uri: "https://static.wikia.nocookie.net/expanse/images/c/c7/Tycho-stn-14.png/revision/latest/scale-to-width-down/1000?cb=20170225140521", + MediaType: "image/png", + }, + }, + Anniversaries: map[string]jscontact.Anniversary{ + "an1": { + Type: jscontact.AnniversaryType, + Kind: jscontact.AnniversaryKindBirth, + Date: jscontact.PartialDate{ + Type: jscontact.PartialDateType, + Year: 1983, + Month: 7, + Day: 18, + CalendarScale: jscontact.RscaleIso8601, + }, + }, + }, + Keywords: map[string]bool{ + "bosmang": true, + "opa": true, + "tycho": true, + "rebel": true, + }, + PersonalInfo: map[string]jscontact.PersonalInfo{ + "p1": { + Type: jscontact.PersonalInfoType, + Kind: jscontact.PersonalInfoKindExpertise, + Value: "loyalty", + Level: jscontact.PersonalInfoLevelHigh, + }, + }, + Notes: map[string]jscontact.Note{ + "n1": { + Type: jscontact.NoteType, + Created: MustParse("2025-09-30T11:00:12Z").UTC(), + Author: &jscontact.Author{ + Type: jscontact.AuthorType, + Name: "expanse.fandom.com", + Uri: "https://expanse.fandom.com/wiki/Camina_Drummer_(TV)", + }, + Note: "Cammina Drummer is a strong-willed, pragmatic, and no-nonsense Belter captain. Having a strong connection to her roots and her cultural identity, Drummer is a Belter through and through: She is resilient and adaptable, treats her crew with respect and equality, and is committed to the Belter way of life, which involves hard work, communal life shared with others, and not taking anything for granted.", + }, + }, +} + +var AndersonDawesContact = jscontact.ContactCard{ + Type: jscontact.ContactCardType, + Id: "cc2", + AddressBookIds: map[string]bool{ + A1.Id: true, + }, + Version: jscontact.JSContactVersion_1_0, + Created: MustParse("2025-09-30T11:00:12Z").UTC(), + Updated: MustParse("2025-09-30T11:00:12Z").UTC(), + Kind: jscontact.ContactCardKindIndividual, + Language: "en-GB", + ProdId: "Mock 0.0", + Uid: "3c1c478e-ac6c-4c2f-a01d-5e528015958d", + Name: &jscontact.Name{ + Type: jscontact.NameType, + Components: []jscontact.NameComponent{ + { + Type: jscontact.NameComponentType, + Kind: jscontact.NameComponentKindGiven, + Value: "Anderson", + }, + { + Type: jscontact.NameComponentType, + Kind: jscontact.NameComponentKindSurname, + Value: "Dawes", + }, + }, + IsOrdered: true, + DefaultSeparator: " ", + Full: "Anderson Dawes", + }, + Organizations: map[string]jscontact.Organization{ + "o1": { + Type: jscontact.OrganizationType, + Name: "Outer Planets Alliance", + SortAs: "OPA", + Contexts: map[jscontact.OrganizationContext]bool{ + jscontact.OrganizationContextWork: true, + }, + }, + }, + SpeakToAs: &jscontact.SpeakToAs{ + Type: jscontact.SpeakToAsType, + GrammaticalGender: jscontact.GrammaticalGenderMasculine, + Pronouns: map[string]jscontact.Pronouns{ + "p1": { + Type: jscontact.PronounsType, + Pronouns: "he/him", + Contexts: map[jscontact.PronounsContext]bool{ + jscontact.PronounsContextPrivate: true, + }, + Pref: 1, + }, + }, + }, + Titles: map[string]jscontact.Title{ + "t1": { + Type: jscontact.TitleType, + Name: "President", + Kind: jscontact.TitleKindRole, + OrganizationId: "o1", + }, + }, + Emails: map[string]jscontact.EmailAddress{ + "e1": { + Type: jscontact.EmailAddressType, + Address: "adawes@opa.org", + Contexts: map[jscontact.EmailAddressContext]bool{ + jscontact.EmailAddressContextWork: true, + jscontact.EmailAddressContextPrivate: true, + }, + Pref: 10, + Label: "opa", + }, + }, + OnlineServices: map[string]jscontact.OnlineService{ + "s1": { + Type: jscontact.OnlineServiceType, + Service: "Ring Network", + Uri: "https://ring.example.com/contact/@adawes", + User: "@anderson.1882", + Contexts: map[jscontact.OnlineServiceContext]bool{ + jscontact.OnlineServiceContextPrivate: true, + jscontact.OnlineServiceContextWork: true, + }, + Label: "ring", + }, + }, + Phones: map[string]jscontact.Phone{ + "p1": { + Type: jscontact.PhoneType, + Number: "+1-999-555-5678", + Features: map[jscontact.PhoneFeature]bool{ + jscontact.PhoneFeatureMainNumber: true, + jscontact.PhoneFeatureMobile: true, + jscontact.PhoneFeatureVoice: true, + }, + Contexts: map[jscontact.PhoneContext]bool{ + jscontact.PhoneContextPrivate: true, + jscontact.PhoneContextWork: true, + }, + }, + }, + PreferredLanguages: map[string]jscontact.LanguagePref{ + "en": { + Type: jscontact.LanguagePrefType, + Language: "en-GB", + Contexts: map[jscontact.LanguagePrefContext]bool{ + jscontact.LanguagePrefContextPrivate: true, + jscontact.LanguagePrefContextWork: true, + }, + }, + }, + Calendars: map[string]jscontact.Calendar{ + "c5": { + Type: jscontact.CalendarType, + Kind: jscontact.CalendarKindCalendar, + Uri: "https://ceres.org/calendars/@adawes/c5", + MediaType: "application/jscontact+json", + Contexts: map[jscontact.CalendarContext]bool{ + jscontact.CalendarContextPrivate: true, + jscontact.CalendarContextWork: true, + }, + }, + }, + SchedulingAddresses: map[string]jscontact.SchedulingAddress{ + "sa1": { + Type: jscontact.SchedulingAddressType, + Uri: "mailto:adawes@opa.org", + Contexts: map[jscontact.SchedulingAddressContext]bool{ + jscontact.SchedulingAddressContextPrivate: true, + jscontact.SchedulingAddressContextWork: true, + }, + }, + }, + Addresses: map[string]jscontact.Address{ + "ad1": { + Type: jscontact.AddressType, + Components: []jscontact.AddressComponent{ + { + Kind: jscontact.AddressComponentKindNumber, + Value: "9218", + }, + { + Kind: jscontact.AddressComponentKindSeparator, + Value: " ", + }, + { + Kind: jscontact.AddressComponentKindName, + Value: "Main Street", + }, + { + Kind: jscontact.AddressComponentKindLocality, + Value: "Ceres Station", + }, + { + Kind: jscontact.AddressComponentKindSeparator, + Value: " ", + }, + { + Kind: jscontact.AddressComponentKindPostcode, + Value: "87A", + }, + { + Kind: jscontact.AddressComponentKindCountry, + Value: "Ceres", + }, + }, + IsOrdered: true, + DefaultSeparator: ", ", + CountryCode: "CRS", + Coordinates: "geo:43.6466107,-79.3889872", + TimeZone: "EDT", + Contexts: map[jscontact.AddressContext]bool{ + jscontact.AddressContextWork: true, + }, + }, + }, + CryptoKeys: map[string]jscontact.CryptoKey{ + "k1": { + Type: jscontact.CryptoKeyType, + Uri: "https://opa.org/keys/@adawes.gpg", + MediaType: "application/pgp-keys", + Contexts: map[jscontact.CryptoKeyContext]bool{ + jscontact.CryptoKeyContextPrivate: true, + jscontact.CryptoKeyContextWork: true, + }, + }, + }, + Media: map[string]jscontact.Media{ + "m1": { + Type: jscontact.MediaType, + Kind: jscontact.MediaKindPhoto, + Uri: "https://static.wikia.nocookie.net/expanse/images/0/0b/S02E07-JaredHarris_as_AndersonDawes_01c.jpg/revision/latest?cb=20170621040250", + MediaType: "image/png", + }, + }, + Anniversaries: map[string]jscontact.Anniversary{ + "an1": { + Type: jscontact.AnniversaryType, + Kind: jscontact.AnniversaryKindBirth, + Date: jscontact.Timestamp{ + Type: jscontact.TimestampType, + Utc: MustParse("1961-08-24T00:00:00Z"), + }, + }, + }, + Keywords: map[string]bool{ + "opa": true, + "ceres": true, + "rebel": true, + }, +} + +var AllAddressBooks = []jmap.AddressBook{A1, A2} + +var ContactsMapByAddressBookId = map[string][]jscontact.ContactCard{ + A1.Id: { + CaminaDrummerContact, + AndersonDawesContact, + }, + A2.Id: { + CaminaDrummerContact, + }, +} diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go index 8f4ac26174..82f717af74 100644 --- a/services/groupware/pkg/groupware/groupware_route.go +++ b/services/groupware/pkg/groupware/groupware_route.go @@ -16,6 +16,8 @@ const ( UriParamBlobName = "blobname" UriParamStreamId = "stream" UriParamRole = "role" + UriParamAddressBookId = "addressbookid" + UriParamCalendarId = "calendarid" QueryParamMailboxSearchName = "name" QueryParamMailboxSearchRole = "role" QueryParamMailboxSearchSubscribed = "subscribed" @@ -89,6 +91,16 @@ func (g *Groupware) Route(r chi.Router) { r.Get("/{blobid}", g.GetBlobMeta) r.Get("/{blobid}/{blobname}", g.DownloadBlob) // ?type= }) + r.Route("/addressbooks", func(r chi.Router) { + r.Get("/", g.GetAddressbooks) + r.Get("/{addressbookid}", g.GetAddressbook) + r.Get("/{addressbookid}/contacts", g.GetContactsInAddressbook) + }) + r.Route("/calendars", func(r chi.Router) { + r.Get("/", g.GetCalendars) + r.Get("/{calendarid}", g.GetCalendarById) + r.Get("/{calendarid}/events", g.GetEventsInCalendar) + }) }) r.HandleFunc("/events/{stream}", g.ServeSSE) diff --git a/services/groupware/pnpm-lock.yaml b/services/groupware/pnpm-lock.yaml index 389a3eb46a..ea8fd89b19 100644 --- a/services/groupware/pnpm-lock.yaml +++ b/services/groupware/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@redocly/cli': - specifier: ^2.2.0 - version: 2.2.0(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1) + specifier: ^2.2.2 + version: 2.2.2(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1) '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -41,10 +41,6 @@ packages: resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.3': - resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} @@ -223,8 +219,8 @@ packages: '@redocly/ajv@8.11.3': resolution: {integrity: sha512-4P3iZse91TkBiY+Dx5DUgxQ9GXkVJf++cmI0MOyLDxV9b5MUBI4II6ES8zA5JCbO72nKAJxWrw4PUPW+YP3ZDQ==} - '@redocly/cli@2.2.0': - resolution: {integrity: sha512-3kXAcA7JLElZH206XqOiFcYtNlIhJeGFsopRypygO3W36sRvFO1d824jZeboYq9ROUaqij7nBCqBoBRKtldtDQ==} + '@redocly/cli@2.2.2': + resolution: {integrity: sha512-LqC7VGoMxWZZC6P96vZO5JOXGSo5Aj6A2KiLcPYPLrZT4Rr/aMBjMauJ4lacX/Z2SuaWZ5CrcnprY1QlL/ZFkQ==} engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} hasBin: true @@ -238,12 +234,12 @@ packages: resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==} engines: {node: '>=18.17.0', npm: '>=9.5.0'} - '@redocly/openapi-core@2.2.0': - resolution: {integrity: sha512-gHedIv/1V5l7x1Nkb/kzRr8rlbGSqmDRs2AabO6BbV3cDofIwkRU7BJRFFNtjTq5LuLIWRTDYghfuIAG94/1Iw==} + '@redocly/openapi-core@2.2.2': + resolution: {integrity: sha512-7Db3yYOAH0k0dq+EkWEh0Cff+KzlzR82YG+R0UphG3sxgtVT7EE4OoDKGcVYey0Dh+Qn728WCKNXdBBKb3Svhw==} engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} - '@redocly/respect-core@2.2.0': - resolution: {integrity: sha512-agpspN6zZWSjSrpdtAKiT8vVMNSs/T0pKRZfgj3ZSvB/asfenJDdDRhDc0h0w2LTYlbcsHEn+TWj4d5czvwlWg==} + '@redocly/respect-core@2.2.2': + resolution: {integrity: sha512-OpiS9LFlBd8xfHAzH5WJ7zTDsjL7FO9jH5QNLCy6/macnL0b6tg/uDUmMjxyUU7/eL2eQUEr8wlIa5oZaN5ZNg==} engines: {node: '>=22.12.0 || >=20.19.0 <21.0.0', npm: '>=10'} '@sinclair/typebox@0.27.8': @@ -477,8 +473,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.2.6: - resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} + dompurify@3.2.7: + resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -769,8 +765,8 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mobx-react-lite@4.1.0: - resolution: {integrity: sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==} + mobx-react-lite@4.1.1: + resolution: {integrity: sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg==} peerDependencies: mobx: ^6.9.0 react: ^16.8.0 || ^17 || ^18 || ^19 @@ -955,8 +951,8 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - redoc@2.5.0: - resolution: {integrity: sha512-NpYsOZ1PD9qFdjbLVBZJWptqE+4Y6TkUuvEOqPUmoH7AKOmPcE+hYjotLxQNTqVoWL4z0T2uxILmcc8JGDci+Q==} + redoc@2.5.1: + resolution: {integrity: sha512-LmqA+4A3CmhTllGG197F0arUpmChukAj9klfSdxNRemT9Hr07xXr7OGKu4PHzBs359sgrJ+4JwmOlM7nxLPGMg==} engines: {node: '>=6.9', npm: '>=3.0.0'} peerDependencies: core-js: ^3.1.4 @@ -1224,8 +1220,6 @@ snapshots: '@babel/helper-validator-identifier@7.27.1': {} - '@babel/runtime@7.28.3': {} - '@babel/runtime@7.28.4': {} '@cspotcode/source-map-support@0.8.1': @@ -1397,14 +1391,14 @@ snapshots: require-from-string: 2.0.2 uri-js-replace: 1.0.1 - '@redocly/cli@2.2.0(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1)': + '@redocly/cli@2.2.2(@opentelemetry/api@1.9.0)(ajv@8.17.1)(core-js@3.45.1)': dependencies: '@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.34.0 - '@redocly/openapi-core': 2.2.0(ajv@8.17.1) - '@redocly/respect-core': 2.2.0(ajv@8.17.1) + '@redocly/openapi-core': 2.2.2(ajv@8.17.1) + '@redocly/respect-core': 2.2.2(ajv@8.17.1) abort-controller: 3.0.0 chokidar: 3.6.0 colorette: 1.4.0 @@ -1418,7 +1412,7 @@ snapshots: pluralize: 8.0.0 react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - redoc: 2.5.0(core-js@3.45.1)(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(styled-components@6.1.19(react-dom@19.1.1(react@19.1.1))(react@19.1.1)) + redoc: 2.5.1(core-js@3.45.1)(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(styled-components@6.1.19(react-dom@19.1.1(react@19.1.1))(react@19.1.1)) semver: 7.7.2 set-cookie-parser: 2.7.1 simple-websocket: 9.1.0 @@ -1455,7 +1449,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@redocly/openapi-core@2.2.0(ajv@8.17.1)': + '@redocly/openapi-core@2.2.2(ajv@8.17.1)': dependencies: '@redocly/ajv': 8.11.3 '@redocly/config': 0.31.0 @@ -1469,12 +1463,12 @@ snapshots: transitivePeerDependencies: - ajv - '@redocly/respect-core@2.2.0(ajv@8.17.1)': + '@redocly/respect-core@2.2.2(ajv@8.17.1)': dependencies: '@faker-js/faker': 7.6.0 '@noble/hashes': 1.8.0 '@redocly/ajv': 8.11.2 - '@redocly/openapi-core': 2.2.0(ajv@8.17.1) + '@redocly/openapi-core': 2.2.2(ajv@8.17.1) better-ajv-errors: 1.2.0(ajv@8.17.1) colorette: 2.0.20 jest-matcher-utils: 29.7.0 @@ -1706,7 +1700,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.2.6: + dompurify@3.2.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -1978,7 +1972,7 @@ snapshots: minipass@7.1.2: {} - mobx-react-lite@4.1.0(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + mobx-react-lite@4.1.1(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: mobx: 6.13.7 react: 19.1.1 @@ -1989,7 +1983,7 @@ snapshots: mobx-react@9.2.0(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: mobx: 6.13.7 - mobx-react-lite: 4.1.0(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + mobx-react-lite: 4.1.1(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react: 19.1.1 optionalDependencies: react-dom: 19.1.1(react@19.1.1) @@ -2095,7 +2089,7 @@ snapshots: polished@4.3.1: dependencies: - '@babel/runtime': 7.28.3 + '@babel/runtime': 7.28.4 postcss-value-parser@4.2.0: {} @@ -2167,13 +2161,13 @@ snapshots: dependencies: picomatch: 2.3.1 - redoc@2.5.0(core-js@3.45.1)(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(styled-components@6.1.19(react-dom@19.1.1(react@19.1.1))(react@19.1.1)): + redoc@2.5.1(core-js@3.45.1)(mobx@6.13.7)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(styled-components@6.1.19(react-dom@19.1.1(react@19.1.1))(react@19.1.1)): dependencies: '@redocly/openapi-core': 1.34.5 classnames: 2.5.1 core-js: 3.45.1 decko: 1.2.0 - dompurify: 3.2.6 + dompurify: 3.2.7 eventemitter3: 5.0.1 json-pointer: 0.6.2 lunr: 2.3.9