mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-13 11:57:33 -04:00
groupware: model refactoring, introducing typed interfaces and Foo
* move ContactCard from jscontact to jmap, as it is actually a JMAP specification item, but also was causing too many issues with circular references from jscontact -> jmap * introduce Foo, Idable, GetRequest, GetResponse, etc... types and generics * first attempt at a Foo factory type for Mailboxes, needs to be expanded to further minimize repetition * add more specialized template functions to avoid repetition * introduce ChangesTemplate[T] for *Changes structs
This commit is contained in:
2
go.mod
2
go.mod
@@ -122,6 +122,7 @@ require (
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/text v0.35.0
|
||||
golang.org/x/tools v0.42.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
@@ -405,7 +406,6 @@ require (
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
|
||||
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
|
||||
|
||||
@@ -8,41 +8,28 @@ import (
|
||||
|
||||
var NS_ADDRESSBOOKS = ns(JmapContacts)
|
||||
|
||||
type AddressBooksResponse struct {
|
||||
AddressBooks []AddressBook `json:"addressbooks"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBooksResponse, SessionState, State, Language, Error) {
|
||||
func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBookGetResponse, SessionState, State, Language, Error) {
|
||||
return get(j, "GetAddressbooks", NS_ADDRESSBOOKS,
|
||||
func(accountId string, ids []string) AddressBookGetCommand {
|
||||
return AddressBookGetCommand{AccountId: accountId, Ids: ids}
|
||||
},
|
||||
AddressBookGetResponse{},
|
||||
func(resp AddressBookGetResponse) AddressBooksResponse {
|
||||
return AddressBooksResponse{AddressBooks: resp.List, NotFound: resp.NotFound}
|
||||
},
|
||||
identity1,
|
||||
accountId, session, ctx, logger, acceptLanguage, ids,
|
||||
)
|
||||
}
|
||||
|
||||
type AddressBookChanges struct {
|
||||
HasMoreChanges bool `json:"hasMoreChanges"`
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState"`
|
||||
Created []AddressBook `json:"created,omitempty"`
|
||||
Updated []AddressBook `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
}
|
||||
type AddressBookChanges = ChangesTemplate[AddressBook]
|
||||
|
||||
// Retrieve Address Book changes since a given state.
|
||||
// @apidoc addressbook,changes
|
||||
func (j *Client) GetAddressbookChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, maxChanges uint) (AddressBookChanges, SessionState, State, Language, Error) {
|
||||
return changes(j, "GetAddressbookChanges", NS_ADDRESSBOOKS,
|
||||
return changesA(j, "GetAddressbookChanges", NS_ADDRESSBOOKS,
|
||||
func() AddressBookChangesCommand {
|
||||
return AddressBookChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)}
|
||||
},
|
||||
AddressBookChangesResponse{},
|
||||
AddressBookGetResponse{},
|
||||
func(path string, rof string) AddressBookGetRefCommand {
|
||||
return AddressBookGetRefCommand{
|
||||
AccountId: accountId,
|
||||
@@ -53,7 +40,6 @@ func (j *Client) GetAddressbookChanges(accountId string, session *Session, ctx c
|
||||
},
|
||||
}
|
||||
},
|
||||
func(resp AddressBookGetResponse) []AddressBook { return resp.List },
|
||||
func(oldState, newState State, hasMoreChanges bool, created, updated []AddressBook, destroyed []string) AddressBookChanges {
|
||||
return AddressBookChanges{
|
||||
OldState: oldState,
|
||||
|
||||
@@ -29,32 +29,18 @@ func (j *Client) ParseICalendarBlob(accountId string, session *Session, ctx cont
|
||||
})
|
||||
}
|
||||
|
||||
type CalendarsResponse struct {
|
||||
Calendars []Calendar `json:"calendars"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
}
|
||||
|
||||
func (j *Client) GetCalendars(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (CalendarsResponse, SessionState, State, Language, Error) {
|
||||
func (j *Client) GetCalendars(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (CalendarGetResponse, SessionState, State, Language, Error) {
|
||||
return get(j, "GetCalendars", NS_CALENDARS,
|
||||
func(accountId string, ids []string) CalendarGetCommand {
|
||||
return CalendarGetCommand{AccountId: accountId, Ids: ids}
|
||||
},
|
||||
CalendarGetResponse{},
|
||||
func(resp CalendarGetResponse) CalendarsResponse {
|
||||
return CalendarsResponse{Calendars: resp.List, NotFound: resp.NotFound}
|
||||
},
|
||||
identity1,
|
||||
accountId, session, ctx, logger, acceptLanguage, ids,
|
||||
)
|
||||
}
|
||||
|
||||
type CalendarChanges struct {
|
||||
HasMoreChanges bool `json:"hasMoreChanges"`
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState"`
|
||||
Created []Calendar `json:"created,omitempty"`
|
||||
Updated []Calendar `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
}
|
||||
type CalendarChanges = ChangesTemplate[Calendar]
|
||||
|
||||
// Retrieve Calendar changes since a given state.
|
||||
// @apidoc calendar,changes
|
||||
@@ -148,14 +134,7 @@ func (j *Client) QueryCalendarEvents(accountIds []string, session *Session, ctx
|
||||
})
|
||||
}
|
||||
|
||||
type CalendarEventChanges struct {
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState"`
|
||||
HasMoreChanges bool `json:"hasMoreChanges"`
|
||||
Created []CalendarEvent `json:"created,omitempty"`
|
||||
Updated []CalendarEvent `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
}
|
||||
type CalendarEventChanges = ChangesTemplate[CalendarEvent]
|
||||
|
||||
// Retrieve the changes in Calendar Events since a given State.
|
||||
// @api:tags event,changes
|
||||
@@ -236,7 +215,7 @@ func (j *Client) CreateCalendar(accountId string, session *Session, ctx context.
|
||||
}
|
||||
|
||||
func (j *Client) DeleteCalendar(accountId string, destroyIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) {
|
||||
return destroy(j, "DeleteCalendar", NS_ADDRESSBOOKS,
|
||||
return destroy(j, "DeleteCalendar", NS_CALENDARS,
|
||||
func(accountId string, destroy []string) CalendarSetCommand {
|
||||
return CalendarSetCommand{AccountId: accountId, Destroy: destroy}
|
||||
},
|
||||
@@ -244,3 +223,17 @@ func (j *Client) DeleteCalendar(accountId string, destroyIds []string, session *
|
||||
accountId, destroyIds, session, ctx, logger, acceptLanguage,
|
||||
)
|
||||
}
|
||||
|
||||
func (j *Client) UpdateCalendar(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string, changes CalendarChange) (Calendar, SessionState, State, Language, Error) {
|
||||
return update(j, "UpdateCalendar", NS_CALENDARS,
|
||||
func(update map[string]PatchObject) CalendarSetCommand {
|
||||
return CalendarSetCommand{AccountId: accountId, Update: update}
|
||||
},
|
||||
func(id string) CalendarGetCommand {
|
||||
return CalendarGetCommand{AccountId: accountId, Ids: []string{id}}
|
||||
},
|
||||
func(resp CalendarSetResponse) map[string]SetError { return resp.NotUpdated },
|
||||
func(resp CalendarGetResponse) Calendar { return resp.List[0] },
|
||||
id, changes, session, ctx, logger, acceptLanguage,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,9 +95,6 @@ func (j *Client) GetChanges(accountId string, session *Session, ctx context.Cont
|
||||
if stateMap.Addressbooks != nil {
|
||||
methodCalls = append(methodCalls, invocation(AddressBookChangesCommand{AccountId: accountId, SinceState: *stateMap.Addressbooks, MaxChanges: posUIntPtr(maxChanges)}, "addressbooks"))
|
||||
}
|
||||
if stateMap.Addressbooks != nil {
|
||||
methodCalls = append(methodCalls, invocation(AddressBookChangesCommand{AccountId: accountId, SinceState: *stateMap.Addressbooks, MaxChanges: posUIntPtr(maxChanges)}, "addressbooks"))
|
||||
}
|
||||
if stateMap.Contacts != nil {
|
||||
methodCalls = append(methodCalls, invocation(ContactCardChangesCommand{AccountId: accountId, SinceState: *stateMap.Contacts, MaxChanges: posUIntPtr(maxChanges)}, "contacts"))
|
||||
}
|
||||
|
||||
@@ -4,59 +4,25 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
var NS_CONTACTS = ns(JmapContacts)
|
||||
|
||||
func (j *Client) GetContactCardsById(accountId string, session *Session, ctx context.Context, logger *log.Logger,
|
||||
acceptLanguage string, contactIds []string) (map[string]jscontact.ContactCard, SessionState, State, Language, Error) {
|
||||
logger = j.logger("GetContactCardsById", session, logger)
|
||||
|
||||
cmd, err := j.request(session, logger, NS_CONTACTS, invocation(ContactCardGetCommand{
|
||||
Ids: contactIds,
|
||||
AccountId: accountId,
|
||||
}, "0"))
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]jscontact.ContactCard, State, Error) {
|
||||
var response ContactCardGetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "0", &response)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
m := map[string]jscontact.ContactCard{}
|
||||
for _, contact := range response.List {
|
||||
m[contact.Id] = contact
|
||||
}
|
||||
return m, response.State, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Client) GetContactCards(accountId string, session *Session, ctx context.Context, logger *log.Logger,
|
||||
acceptLanguage string, contactIds []string) ([]jscontact.ContactCard, SessionState, State, Language, Error) {
|
||||
acceptLanguage string, contactIds []string) (ContactCardGetResponse, SessionState, State, Language, Error) {
|
||||
return get(j, "GetContactCards", NS_CONTACTS,
|
||||
func(accountId string, ids []string) ContactCardGetCommand {
|
||||
return ContactCardGetCommand{AccountId: accountId, Ids: contactIds}
|
||||
},
|
||||
ContactCardGetResponse{},
|
||||
func(resp ContactCardGetResponse) []jscontact.ContactCard { return resp.List },
|
||||
identity1,
|
||||
accountId, session, ctx, logger, acceptLanguage, contactIds,
|
||||
)
|
||||
}
|
||||
|
||||
type ContactCardChanges struct {
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState"`
|
||||
HasMoreChanges bool `json:"hasMoreChanges"`
|
||||
Created []jscontact.ContactCard `json:"created,omitempty"`
|
||||
Updated []jscontact.ContactCard `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
}
|
||||
type ContactCardChanges = ChangesTemplate[ContactCard]
|
||||
|
||||
// Retrieve the changes in Contact Cards since a given State.
|
||||
// @api:tags contact,changes
|
||||
@@ -77,8 +43,8 @@ func (j *Client) GetContactCardChanges(accountId string, session *Session, ctx c
|
||||
},
|
||||
}
|
||||
},
|
||||
func(resp ContactCardGetResponse) []jscontact.ContactCard { return resp.List },
|
||||
func(oldState, newState State, hasMoreChanges bool, created, updated []jscontact.ContactCard, destroyed []string) ContactCardChanges {
|
||||
func(resp ContactCardGetResponse) []ContactCard { return resp.List },
|
||||
func(oldState, newState State, hasMoreChanges bool, created, updated []ContactCard, destroyed []string) ContactCardChanges {
|
||||
return ContactCardChanges{
|
||||
OldState: oldState,
|
||||
NewState: newState,
|
||||
@@ -94,13 +60,13 @@ func (j *Client) GetContactCardChanges(accountId string, session *Session, ctx c
|
||||
|
||||
func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, //NOSONAR
|
||||
filter ContactCardFilterElement, sortBy []ContactCardComparator,
|
||||
position uint, limit uint) (map[string][]jscontact.ContactCard, SessionState, State, Language, Error) {
|
||||
position uint, limit uint) (map[string][]ContactCard, SessionState, State, Language, Error) {
|
||||
logger = j.logger("QueryContactCards", session, logger)
|
||||
|
||||
uniqueAccountIds := structs.Uniq(accountIds)
|
||||
|
||||
if sortBy == nil {
|
||||
sortBy = []ContactCardComparator{{Property: jscontact.ContactCardPropertyUpdated, IsAscending: false}}
|
||||
sortBy = []ContactCardComparator{{Property: ContactCardPropertyUpdated, IsAscending: false}}
|
||||
}
|
||||
|
||||
invocations := make([]Invocation, len(uniqueAccountIds)*2)
|
||||
@@ -131,8 +97,8 @@ func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx co
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]jscontact.ContactCard, State, Error) {
|
||||
resp := map[string][]jscontact.ContactCard{}
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]ContactCard, State, Error) {
|
||||
resp := map[string][]ContactCard{}
|
||||
stateByAccountId := map[string]State{}
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
var response ContactCardGetResponse
|
||||
@@ -150,13 +116,13 @@ func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx co
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Client) CreateContactCard(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create jscontact.ContactCard) (*jscontact.ContactCard, SessionState, State, Language, Error) {
|
||||
func (j *Client) CreateContactCard(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create ContactCard) (*ContactCard, SessionState, State, Language, Error) {
|
||||
logger = j.logger("CreateContactCard", session, logger)
|
||||
|
||||
cmd, err := j.request(session, logger, NS_CONTACTS,
|
||||
invocation(ContactCardSetCommand{
|
||||
AccountId: accountId,
|
||||
Create: map[string]jscontact.ContactCard{
|
||||
Create: map[string]ContactCard{
|
||||
"c": create,
|
||||
},
|
||||
}, "0"),
|
||||
@@ -169,7 +135,7 @@ func (j *Client) CreateContactCard(accountId string, session *Session, ctx conte
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*jscontact.ContactCard, State, Error) {
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*ContactCard, State, Error) {
|
||||
var setResponse ContactCardSetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandContactCardSet, "0", &setResponse)
|
||||
if err != nil {
|
||||
@@ -204,25 +170,12 @@ func (j *Client) CreateContactCard(accountId string, session *Session, ctx conte
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Client) DeleteContactCard(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) {
|
||||
logger = j.logger("DeleteContactCard", session, logger)
|
||||
|
||||
cmd, err := j.request(session, logger, NS_CONTACTS,
|
||||
invocation(ContactCardSetCommand{
|
||||
AccountId: accountId,
|
||||
Destroy: destroy,
|
||||
}, "0"),
|
||||
func (j *Client) DeleteContactCard(accountId string, destroyIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) {
|
||||
return destroy(j, "DeleteContactCard", NS_CONTACTS,
|
||||
func(accountId string, destroy []string) ContactCardSetCommand {
|
||||
return ContactCardSetCommand{AccountId: accountId, Destroy: destroy}
|
||||
},
|
||||
ContactCardSetResponse{},
|
||||
accountId, destroyIds, session, ctx, logger, acceptLanguage,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]SetError, State, Error) {
|
||||
var setResponse ContactCardSetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandContactCardSet, "0", &setResponse)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return setResponse.NotDestroyed, setResponse.NewState, nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1072,14 +1072,7 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx
|
||||
})
|
||||
}
|
||||
|
||||
type EmailSubmissionChanges struct {
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState"`
|
||||
HasMoreChanges bool `json:"hasMoreChanges"`
|
||||
Created []EmailSubmission `json:"created,omitempty"`
|
||||
Updated []EmailSubmission `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
}
|
||||
type EmailSubmissionChanges = ChangesTemplate[EmailSubmission]
|
||||
|
||||
// Retrieve the changes in Email Submissions since a given State.
|
||||
// @api:tags email,changes
|
||||
|
||||
@@ -11,23 +11,21 @@ import (
|
||||
var NS_IDENTITY = ns(JmapMail)
|
||||
|
||||
func (j *Client) GetAllIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) ([]Identity, SessionState, State, Language, Error) {
|
||||
return get(j, "GetAllIdentities", NS_IDENTITY,
|
||||
return getA(j, "GetAllIdentities", NS_IDENTITY,
|
||||
func(accountId string, ids []string) IdentityGetCommand {
|
||||
return IdentityGetCommand{AccountId: accountId}
|
||||
},
|
||||
IdentityGetResponse{},
|
||||
func(resp IdentityGetResponse) []Identity { return resp.List },
|
||||
accountId, session, ctx, logger, acceptLanguage, []string{},
|
||||
)
|
||||
}
|
||||
|
||||
func (j *Client) GetIdentities(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, identityIds []string) ([]Identity, SessionState, State, Language, Error) {
|
||||
return get(j, "GetIdentities", NS_IDENTITY,
|
||||
return getA(j, "GetIdentities", NS_IDENTITY,
|
||||
func(accountId string, ids []string) IdentityGetCommand {
|
||||
return IdentityGetCommand{AccountId: accountId, Ids: ids}
|
||||
},
|
||||
IdentityGetResponse{},
|
||||
func(resp IdentityGetResponse) []Identity { return resp.List },
|
||||
accountId, session, ctx, logger, acceptLanguage, identityIds,
|
||||
)
|
||||
}
|
||||
@@ -171,14 +169,7 @@ func (j *Client) DeleteIdentity(accountId string, session *Session, ctx context.
|
||||
})
|
||||
}
|
||||
|
||||
type IdentityChanges struct {
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState"`
|
||||
HasMoreChanges bool `json:"hasMoreChanges"`
|
||||
Created []Identity `json:"created,omitempty"`
|
||||
Updated []Identity `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
}
|
||||
type IdentityChanges = ChangesTemplate[Identity]
|
||||
|
||||
// Retrieve the changes in Email Identities since a given State.
|
||||
// @api:tags email,changes
|
||||
|
||||
@@ -11,37 +11,33 @@ import (
|
||||
|
||||
var NS_MAILBOX = ns(JmapMail)
|
||||
|
||||
type MailboxesResponse struct {
|
||||
Mailboxes []Mailbox `json:"mailboxes"`
|
||||
NotFound []string `json:"notFound"`
|
||||
}
|
||||
func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (MailboxGetResponse, SessionState, State, Language, Error) {
|
||||
/*
|
||||
return get(j, "GetMailbox", NS_MAILBOX,
|
||||
func(accountId string, ids []string) MailboxGetCommand {
|
||||
return MailboxGetCommand{AccountId: accountId, Ids: ids}
|
||||
},
|
||||
MailboxGetResponse{},
|
||||
identity1,
|
||||
accountId, session, ctx, logger, acceptLanguage, ids,
|
||||
)
|
||||
*/
|
||||
|
||||
func (j *Client) GetMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (MailboxesResponse, SessionState, State, Language, Error) {
|
||||
return get(j, "GetMailbox", NS_MAILBOX,
|
||||
func(accountId string, ids []string) MailboxGetCommand {
|
||||
return MailboxGetCommand{AccountId: accountId, Ids: ids}
|
||||
},
|
||||
MailboxGetResponse{},
|
||||
func(resp MailboxGetResponse) MailboxesResponse {
|
||||
return MailboxesResponse{
|
||||
Mailboxes: resp.List,
|
||||
NotFound: resp.NotFound,
|
||||
}
|
||||
},
|
||||
accountId, session, ctx, logger, acceptLanguage, ids,
|
||||
)
|
||||
return fget[Mailboxes](MAILBOX, j, "GetMailbox", accountId, ids, session, ctx, logger, acceptLanguage)
|
||||
}
|
||||
|
||||
func (j *Client) GetAllMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string][]Mailbox, SessionState, State, Language, Error) {
|
||||
return getN(j, "GetAllMailboxes", NS_MAILBOX,
|
||||
func(accountId string, ids []string) MailboxGetCommand {
|
||||
return MailboxGetCommand{AccountId: accountId}
|
||||
},
|
||||
MailboxGetResponse{},
|
||||
func(resp MailboxGetResponse) []Mailbox { return resp.List },
|
||||
identity1,
|
||||
accountIds, session, ctx, logger, acceptLanguage, []string{},
|
||||
)
|
||||
/*
|
||||
return getAN(j, "GetAllMailboxes", NS_MAILBOX,
|
||||
func(accountId string, ids []string) MailboxGetCommand {
|
||||
return MailboxGetCommand{AccountId: accountId}
|
||||
},
|
||||
MailboxGetResponse{},
|
||||
identity1,
|
||||
accountIds, session, ctx, logger, acceptLanguage, []string{},
|
||||
)
|
||||
*/
|
||||
return fgetAN[Mailboxes](MAILBOX, j, "GetAllMailboxes", identity1, accountIds, []string{}, session, ctx, logger, acceptLanguage)
|
||||
}
|
||||
|
||||
func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter MailboxFilterElement) (map[string][]Mailbox, SessionState, State, Language, Error) {
|
||||
@@ -123,14 +119,7 @@ func (j *Client) SearchMailboxIdsPerRole(accountIds []string, session *Session,
|
||||
})
|
||||
}
|
||||
|
||||
type MailboxChanges struct {
|
||||
HasMoreChanges bool `json:"hasMoreChanges"`
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState"`
|
||||
Created []Mailbox `json:"created,omitempty"`
|
||||
Updated []Mailbox `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
}
|
||||
type MailboxChanges = ChangesTemplate[Mailbox]
|
||||
|
||||
func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, updated []Mailbox, destroyed []string) MailboxChanges {
|
||||
return MailboxChanges{
|
||||
@@ -146,11 +135,12 @@ func newMailboxChanges(oldState, newState State, hasMoreChanges bool, created, u
|
||||
// Retrieve Mailbox changes since a given state.
|
||||
// @apidoc mailboxes,changes
|
||||
func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, maxChanges uint) (MailboxChanges, SessionState, State, Language, Error) {
|
||||
return changes(j, "GetMailboxChanges", NS_MAILBOX,
|
||||
return changesA(j, "GetMailboxChanges", NS_MAILBOX,
|
||||
func() MailboxChangesCommand {
|
||||
return MailboxChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)}
|
||||
},
|
||||
MailboxChangesResponse{},
|
||||
MailboxGetResponse{},
|
||||
func(path string, rof string) MailboxGetRefCommand {
|
||||
return MailboxGetRefCommand{
|
||||
AccountId: accountId,
|
||||
@@ -161,7 +151,6 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte
|
||||
},
|
||||
}
|
||||
},
|
||||
func(resp MailboxGetResponse) []Mailbox { return resp.List },
|
||||
newMailboxChanges,
|
||||
session, ctx, logger, acceptLanguage,
|
||||
)
|
||||
@@ -353,24 +342,25 @@ func (j *Client) CreateMailbox(accountId string, session *Session, ctx context.C
|
||||
|
||||
func (j *Client) DeleteMailboxes(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ifInState string, mailboxIds []string) ([]string, SessionState, State, Language, Error) {
|
||||
logger = j.logger("DeleteMailbox", session, logger)
|
||||
cmd, err := j.request(session, logger, NS_MAILBOX, invocation(MailboxSetCommand{
|
||||
set := MailboxSetCommand{
|
||||
AccountId: accountId,
|
||||
IfInState: ifInState,
|
||||
Destroy: mailboxIds,
|
||||
}, "0"))
|
||||
}
|
||||
cmd, err := j.request(session, logger, NS_MAILBOX, invocation(set, "0"))
|
||||
if err != nil {
|
||||
return nil, "", "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) ([]string, State, Error) {
|
||||
var setResp MailboxSetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandMailboxSet, "0", &setResp)
|
||||
err = retrieveSet(logger, body, set, "0", &setResp)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
setErr, notok := setResp.NotUpdated["u"]
|
||||
setErr, notok := setResp.NotDestroyed["u"]
|
||||
if notok {
|
||||
logger.Error().Msgf("%T.NotUpdated returned an error %v", setResp, setErr)
|
||||
logger.Error().Msgf("%T.NotDestroyed returned an error %v", setResp, setErr)
|
||||
return nil, "", setErrorError(setErr, MailboxType)
|
||||
}
|
||||
return setResp.Destroyed, setResp.NewState, nil
|
||||
|
||||
@@ -20,24 +20,18 @@ func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Co
|
||||
)
|
||||
}
|
||||
|
||||
type QuotaChanges struct {
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
NewState State `json:"newState"`
|
||||
HasMoreChanges bool `json:"hasMoreChanges"`
|
||||
Created []Quota `json:"created,omitempty"`
|
||||
Updated []Quota `json:"updated,omitempty"`
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
}
|
||||
type QuotaChanges = ChangesTemplate[Quota]
|
||||
|
||||
// Retrieve the changes in Quotas since a given State.
|
||||
// @api:tags quota,changes
|
||||
func (j *Client) GetQuotaChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger,
|
||||
acceptLanguage string, sinceState State, maxChanges uint) (QuotaChanges, SessionState, State, Language, Error) {
|
||||
return changes(j, "GetQuotaChanges", NS_QUOTA,
|
||||
return changesA(j, "GetQuotaChanges", NS_QUOTA,
|
||||
func() QuotaChangesCommand {
|
||||
return QuotaChangesCommand{AccountId: accountId, SinceState: sinceState, MaxChanges: posUIntPtr(maxChanges)}
|
||||
},
|
||||
QuotaChangesResponse{},
|
||||
QuotaGetResponse{},
|
||||
func(path string, rof string) QuotaGetRefCommand {
|
||||
return QuotaGetRefCommand{
|
||||
AccountId: accountId,
|
||||
@@ -48,7 +42,6 @@ func (j *Client) GetQuotaChanges(accountId string, session *Session, ctx context
|
||||
},
|
||||
}
|
||||
},
|
||||
func(resp QuotaGetResponse) []Quota { return resp.List },
|
||||
func(oldState, newState State, hasMoreChanges bool, created, updated []Quota, destroyed []string) QuotaChanges {
|
||||
return QuotaChanges{
|
||||
OldState: oldState,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
golog "log"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"slices"
|
||||
@@ -11,7 +13,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/brianvoe/gofakeit/v7"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
@@ -27,7 +29,7 @@ const (
|
||||
EnableMediaWithBlobId = false
|
||||
)
|
||||
|
||||
type AddressBooksBoxes struct {
|
||||
type AddressBookBoxes struct {
|
||||
sharedReadOnly bool
|
||||
sharedReadWrite bool
|
||||
sharedDelete bool
|
||||
@@ -39,109 +41,34 @@ func TestAddressBooks(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
require := require.New(t)
|
||||
|
||||
s, err := newStalwartTest(t, withDirectoryQueries(true))
|
||||
require.NoError(err)
|
||||
defer s.Close()
|
||||
|
||||
user := pickUser()
|
||||
session := s.Session(user.name)
|
||||
|
||||
// we first need to retrieve the list of all the Principals in order to be able to use and test AddressBook sharing
|
||||
principalIds := []string{}
|
||||
{
|
||||
principals, _, _, _, err := s.client.GetPrincipals(session.PrimaryAccounts.Mail, session, s.ctx, s.logger, "", []string{})
|
||||
require.NoError(err)
|
||||
require.NotEmpty(principals.Principals)
|
||||
principalIds = structs.Map(principals.Principals, func(p Principal) string { return p.Id })
|
||||
}
|
||||
|
||||
accountId := session.PrimaryAccounts.Contacts
|
||||
|
||||
ss := SessionState("")
|
||||
as := EmptyState
|
||||
|
||||
// we need to fetch the ID of the default AddressBook that automatically exists for each user, in order to exclude it
|
||||
// from the tests below
|
||||
defaultAddressBookId := ""
|
||||
{
|
||||
resp, sessionState, state, _, err := s.client.GetAddressbooks(accountId, session, s.ctx, s.logger, "", []string{})
|
||||
require.NoError(err)
|
||||
require.Empty(resp.NotFound)
|
||||
require.Len(resp.AddressBooks, 1) // the personal addressbook that exists by default
|
||||
defaultAddressBookId = resp.AddressBooks[0].Id
|
||||
ss = sessionState
|
||||
as = state
|
||||
}
|
||||
|
||||
// we are going to create a random amount of AddressBook objects
|
||||
num := uint(5 + rand.Intn(30))
|
||||
{
|
||||
boxes, abooks, sessionState, state, err := s.fillAddressBook(t, accountId, num, session, user, principalIds)
|
||||
require.NoError(err)
|
||||
require.Len(abooks, int(num))
|
||||
ss = sessionState
|
||||
as = state
|
||||
|
||||
{
|
||||
// lets retrieve all the existing AddressBook objects by passing an empty ID slice
|
||||
resp, sessionState, state, _, err := s.client.GetAddressbooks(accountId, session, s.ctx, s.logger, "", []string{})
|
||||
require.NoError(err)
|
||||
require.Empty(resp.NotFound)
|
||||
// lets skip the default AddressBook since we did not create that one
|
||||
found := structs.Filter(resp.AddressBooks, func(a AddressBook) bool { return a.Id != defaultAddressBookId })
|
||||
require.Len(found, int(num))
|
||||
m := structs.Index(found, func(a AddressBook) string { return a.Id })
|
||||
require.Len(m, int(num))
|
||||
require.Equal(sessionState, ss)
|
||||
require.Equal(state, as)
|
||||
|
||||
for _, a := range abooks {
|
||||
require.Contains(m, a.Id)
|
||||
found, ok := m[a.Id]
|
||||
require.True(ok)
|
||||
require.Equal(a, found)
|
||||
containerTest(t,
|
||||
func(session *Session) string { return session.PrimaryAccounts.Contacts },
|
||||
list,
|
||||
getid,
|
||||
func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBookGetResponse, SessionState, State, Language, Error) {
|
||||
return s.client.GetAddressbooks(accountId, session, ctx, logger, acceptLanguage, ids)
|
||||
},
|
||||
func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string, change AddressBookChange) (AddressBook, SessionState, State, Language, Error) { //NOSONAR
|
||||
return s.client.UpdateAddressBook(accountId, session, ctx, logger, acceptLanguage, id, change)
|
||||
},
|
||||
func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (map[string]SetError, SessionState, State, Language, Error) { //NOSONAR
|
||||
return s.client.DeleteAddressBook(accountId, ids, session, ctx, logger, acceptLanguage)
|
||||
},
|
||||
func(s *StalwartTest, t *testing.T, accountId string, count uint, session *Session, user User, principalIds []string) (AddressBookBoxes, []AddressBook, SessionState, State, error) {
|
||||
return s.fillAddressBook(t, accountId, count, session, user, principalIds)
|
||||
},
|
||||
func(orig AddressBook) AddressBookChange {
|
||||
return AddressBookChange{
|
||||
Description: strPtr(orig.Description + " (changed)"),
|
||||
IsSubscribed: boolPtr(!orig.IsSubscribed),
|
||||
}
|
||||
|
||||
ss = sessionState
|
||||
as = state
|
||||
}
|
||||
|
||||
// lets retrieve every AddressBook object we created by its ID
|
||||
for _, a := range abooks {
|
||||
resp, sessionState, state, _, err := s.client.GetAddressbooks(accountId, session, s.ctx, s.logger, "", []string{a.Id})
|
||||
require.NoError(err)
|
||||
require.Empty(resp.NotFound)
|
||||
require.Len(resp.AddressBooks, 1)
|
||||
require.Equal(sessionState, ss)
|
||||
require.Equal(state, as)
|
||||
require.Equal(resp.AddressBooks[0], a)
|
||||
}
|
||||
|
||||
// lets modify each AddressBook
|
||||
for _, a := range abooks {
|
||||
changed, sessionState, state, _, err := s.client.UpdateAddressBook(accountId, session, s.ctx, s.logger, "", a.Id, AddressBookChange{Description: strPtr(a.Description + " (changed)"), IsSubscribed: boolPtr(!a.IsSubscribed)})
|
||||
require.NoError(err)
|
||||
require.NotEqual(a, changed)
|
||||
require.Equal(sessionState, ss)
|
||||
require.NotEqual(state, as)
|
||||
require.Equal(a.Description+" (changed)", changed.Description)
|
||||
require.Equal(!a.IsSubscribed, changed.IsSubscribed)
|
||||
}
|
||||
|
||||
// now lets delete each AddressBook that we created, all at once
|
||||
ids := structs.Map(abooks, func(a AddressBook) string { return a.Id })
|
||||
{
|
||||
errMap, sessionState, state, _, err := s.client.DeleteAddressBook(accountId, ids, session, s.ctx, s.logger, "")
|
||||
require.NoError(err)
|
||||
require.Empty(errMap)
|
||||
require.Equal(sessionState, ss)
|
||||
require.NotEqual(state, as)
|
||||
}
|
||||
|
||||
allBoxesAreTicked(t, boxes)
|
||||
}
|
||||
},
|
||||
func(t *testing.T, orig AddressBook, _ AddressBookChange, changed AddressBook) {
|
||||
require.Equal(t, orig.Name, changed.Name)
|
||||
require.Equal(t, orig.Description+" (changed)", changed.Description)
|
||||
require.Equal(t, !orig.IsSubscribed, changed.IsSubscribed)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestContacts(t *testing.T) {
|
||||
@@ -169,7 +96,7 @@ func TestContacts(t *testing.T) {
|
||||
InAddressBook: addressbookId,
|
||||
}
|
||||
sortBy := []ContactCardComparator{
|
||||
{Property: jscontact.ContactCardPropertyCreated, IsAscending: true},
|
||||
{Property: ContactCardPropertyCreated, IsAscending: true},
|
||||
}
|
||||
|
||||
contactsByAccount, _, _, _, err := s.client.QueryContactCards([]string{accountId}, session, t.Context(), s.logger, "", filter, sortBy, 0, 0)
|
||||
@@ -186,6 +113,29 @@ func TestContacts(t *testing.T) {
|
||||
matchContact(t, actual, expected)
|
||||
}
|
||||
|
||||
// retrieve all objects at once
|
||||
{
|
||||
ids := structs.Map(contacts, func(c ContactCard) string { return c.Id })
|
||||
fetched, _, _, _, err := s.client.GetContactCards(accountId, session, t.Context(), s.logger, "", ids)
|
||||
require.NoError(err)
|
||||
require.Empty(fetched.NotFound)
|
||||
require.Len(fetched.List, len(ids))
|
||||
byId := structs.Index(fetched.List, func(r ContactCard) string { return r.Id })
|
||||
for _, actual := range contacts {
|
||||
expected, ok := byId[actual.Id]
|
||||
require.True(ok, "failed to find created contact by its id")
|
||||
matchContact(t, actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve each object one by one
|
||||
for _, actual := range contacts {
|
||||
fetched, _, _, _, err := s.client.GetContactCards(accountId, session, t.Context(), s.logger, "", []string{actual.Id})
|
||||
require.NoError(err)
|
||||
require.Len(fetched.List, 1)
|
||||
matchContact(t, fetched.List[0], actual)
|
||||
}
|
||||
|
||||
exceptions := []string{}
|
||||
if !EnableMediaWithBlobId {
|
||||
exceptions = append(exceptions, "mediaWithBlobId")
|
||||
@@ -193,7 +143,7 @@ func TestContacts(t *testing.T) {
|
||||
allBoxesAreTicked(t, boxes, exceptions...)
|
||||
}
|
||||
|
||||
func matchContact(t *testing.T, actual jscontact.ContactCard, expected jscontact.ContactCard) {
|
||||
func matchContact(t *testing.T, actual ContactCard, expected ContactCard) {
|
||||
// require.Equal(t, expected, actual)
|
||||
deepEqual(t, expected, actual)
|
||||
}
|
||||
@@ -222,15 +172,15 @@ func (s *StalwartTest) fillAddressBook( //NOSONAR
|
||||
session *Session,
|
||||
_ User,
|
||||
principalIds []string,
|
||||
) (AddressBooksBoxes, []AddressBook, SessionState, State, error) {
|
||||
) (AddressBookBoxes, []AddressBook, SessionState, State, error) {
|
||||
require := require.New(t)
|
||||
|
||||
boxes := AddressBooksBoxes{}
|
||||
boxes := AddressBookBoxes{}
|
||||
created := []AddressBook{}
|
||||
ss := SessionState("")
|
||||
as := EmptyState
|
||||
|
||||
printer := func(s string) { log.Println(s) }
|
||||
printer := func(s string) { golog.Println(s) }
|
||||
|
||||
for i := range count {
|
||||
name := gofakeit.Company()
|
||||
@@ -295,7 +245,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
count uint,
|
||||
session *Session,
|
||||
user User,
|
||||
) (string, string, map[string]jscontact.ContactCard, ContactsBoxes, error) {
|
||||
) (string, string, map[string]ContactCard, ContactsBoxes, error) {
|
||||
require := require.New(t)
|
||||
c, err := NewTestJmapClient(session, user.name, user.password, true, true)
|
||||
require.NoError(err)
|
||||
@@ -303,7 +253,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
|
||||
boxes := ContactsBoxes{}
|
||||
|
||||
printer := func(s string) { log.Println(s) }
|
||||
printer := func(s string) { golog.Println(s) }
|
||||
|
||||
accountId := c.session.PrimaryAccounts.Contacts
|
||||
require.NotEmpty(accountId, "no primary account for contacts in session")
|
||||
@@ -331,7 +281,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
}
|
||||
require.NotEmpty(addressbookId)
|
||||
|
||||
filled := map[string]jscontact.ContactCard{}
|
||||
filled := map[string]ContactCard{}
|
||||
for i := range count {
|
||||
person := gofakeit.Person()
|
||||
nameMap, nameObj := createName(person)
|
||||
@@ -345,7 +295,7 @@ func (s *StalwartTest) fillContacts( //NOSONAR
|
||||
"kind": "individual",
|
||||
"name": nameMap,
|
||||
}
|
||||
card := jscontact.ContactCard{
|
||||
card := ContactCard{
|
||||
Type: jscontact.ContactCardType,
|
||||
Version: "1.0",
|
||||
AddressBookIds: toBoolMap([]string{addressbookId}),
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
golog "log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscalendar"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
@@ -25,6 +27,41 @@ const (
|
||||
EnableEventParticipantDescriptionFields = false
|
||||
)
|
||||
|
||||
func TestCalendars(t *testing.T) { //NOSONAR
|
||||
if skip(t) {
|
||||
return
|
||||
}
|
||||
|
||||
containerTest(t,
|
||||
func(session *Session) string { return session.PrimaryAccounts.Calendars },
|
||||
func(resp CalendarGetResponse) []Calendar { return resp.List },
|
||||
func(obj Calendar) string { return obj.Id },
|
||||
func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (CalendarGetResponse, SessionState, State, Language, Error) {
|
||||
return s.client.GetCalendars(accountId, session, ctx, logger, acceptLanguage, ids)
|
||||
},
|
||||
func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, id string, change CalendarChange) (Calendar, SessionState, State, Language, Error) { //NOSONAR
|
||||
return s.client.UpdateCalendar(accountId, session, ctx, logger, acceptLanguage, id, change)
|
||||
},
|
||||
func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (map[string]SetError, SessionState, State, Language, Error) { //NOSONAR
|
||||
return s.client.DeleteCalendar(accountId, ids, session, ctx, logger, acceptLanguage)
|
||||
},
|
||||
func(s *StalwartTest, t *testing.T, accountId string, count uint, session *Session, user User, principalIds []string) (CalendarBoxes, []Calendar, SessionState, State, error) {
|
||||
return s.fillCalendar(t, accountId, count, session, user, principalIds)
|
||||
},
|
||||
func(orig Calendar) CalendarChange {
|
||||
return CalendarChange{
|
||||
Description: strPtr(orig.Description + " (changed)"),
|
||||
IsSubscribed: boolPtr(!orig.IsSubscribed),
|
||||
}
|
||||
},
|
||||
func(t *testing.T, orig Calendar, _ CalendarChange, changed Calendar) {
|
||||
require.Equal(t, orig.Name, changed.Name)
|
||||
require.Equal(t, orig.Description+" (changed)", changed.Description)
|
||||
require.Equal(t, !orig.IsSubscribed, changed.IsSubscribed)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestEvents(t *testing.T) {
|
||||
if skip(t) {
|
||||
return
|
||||
@@ -79,6 +116,145 @@ func matchEvent(t *testing.T, actual CalendarEvent, expected CalendarEvent) {
|
||||
deepEqual(t, expected, actual)
|
||||
}
|
||||
|
||||
type CalendarBoxes struct {
|
||||
sharedReadOnly bool
|
||||
sharedReadWrite bool
|
||||
sharedDelete bool
|
||||
sortOrdered bool
|
||||
}
|
||||
|
||||
func (s *StalwartTest) fillCalendar( //NOSONAR
|
||||
t *testing.T,
|
||||
accountId string,
|
||||
count uint,
|
||||
session *Session,
|
||||
_ User,
|
||||
principalIds []string,
|
||||
) (CalendarBoxes, []Calendar, SessionState, State, error) {
|
||||
require := require.New(t)
|
||||
|
||||
boxes := CalendarBoxes{}
|
||||
created := []Calendar{}
|
||||
ss := SessionState("")
|
||||
as := EmptyState
|
||||
|
||||
printer := func(s string) { golog.Println(s) }
|
||||
|
||||
for i := range count {
|
||||
name := gofakeit.Company()
|
||||
description := gofakeit.SentenceSimple()
|
||||
subscribed := gofakeit.Bool()
|
||||
visible := gofakeit.Bool()
|
||||
color := gofakeit.HexColor()
|
||||
include := pickRandom(IncludeInAvailabilities...)
|
||||
dawtId := gofakeit.UUID()
|
||||
daotId := gofakeit.UUID()
|
||||
cal := CalendarChange{
|
||||
Name: &name,
|
||||
Description: &description,
|
||||
IsSubscribed: &subscribed,
|
||||
Color: &color,
|
||||
IsVisible: &visible,
|
||||
IncludeInAvailability: &include,
|
||||
DefaultAlertsWithTime: map[string]jscalendar.Alert{
|
||||
dawtId: {
|
||||
Type: jscalendar.AlertType,
|
||||
Trigger: jscalendar.OffsetTrigger{
|
||||
Type: jscalendar.OffsetTriggerType,
|
||||
Offset: "-PT5M",
|
||||
RelativeTo: jscalendar.RelativeToStart,
|
||||
},
|
||||
Action: jscalendar.AlertActionDisplay,
|
||||
},
|
||||
},
|
||||
DefaultAlertsWithoutTime: map[string]jscalendar.Alert{
|
||||
daotId: {
|
||||
Type: jscalendar.AlertType,
|
||||
Trigger: jscalendar.OffsetTrigger{
|
||||
Type: jscalendar.OffsetTriggerType,
|
||||
Offset: "-PT24H",
|
||||
RelativeTo: jscalendar.RelativeToStart,
|
||||
},
|
||||
Action: jscalendar.AlertActionDisplay,
|
||||
},
|
||||
},
|
||||
}
|
||||
if i%2 == 0 {
|
||||
cal.SortOrder = posUIntPtr(gofakeit.Uint())
|
||||
boxes.sortOrdered = true
|
||||
}
|
||||
var sharing *CalendarRights = nil
|
||||
switch i % 4 {
|
||||
default:
|
||||
// no sharing
|
||||
case 1:
|
||||
sharing = &CalendarRights{
|
||||
MayReadFreeBusy: true,
|
||||
MayReadItems: true,
|
||||
MayRSVP: true,
|
||||
MayAdmin: false,
|
||||
MayDelete: false,
|
||||
MayWriteAll: false,
|
||||
MayWriteOwn: false,
|
||||
MayUpdatePrivate: false,
|
||||
}
|
||||
boxes.sharedReadWrite = true
|
||||
case 2:
|
||||
sharing = &CalendarRights{
|
||||
MayReadFreeBusy: true,
|
||||
MayReadItems: true,
|
||||
MayRSVP: true,
|
||||
MayAdmin: false,
|
||||
MayDelete: false,
|
||||
MayWriteAll: false,
|
||||
MayWriteOwn: true,
|
||||
MayUpdatePrivate: true,
|
||||
}
|
||||
boxes.sharedReadOnly = true
|
||||
case 3:
|
||||
sharing = &CalendarRights{
|
||||
MayReadFreeBusy: true,
|
||||
MayReadItems: true,
|
||||
MayRSVP: true,
|
||||
MayAdmin: false,
|
||||
MayDelete: true,
|
||||
MayWriteAll: true,
|
||||
MayWriteOwn: true,
|
||||
MayUpdatePrivate: true,
|
||||
}
|
||||
boxes.sharedDelete = true
|
||||
}
|
||||
if sharing != nil {
|
||||
numPrincipals := 1 + rand.Intn(len(principalIds)-1)
|
||||
m := make(map[string]CalendarRights, numPrincipals)
|
||||
for _, p := range pickRandomN(numPrincipals, principalIds...) {
|
||||
m[p] = *sharing
|
||||
}
|
||||
cal.ShareWith = m
|
||||
}
|
||||
|
||||
a, sessionState, state, _, err := s.client.CreateCalendar(accountId, session, s.ctx, s.logger, "", cal)
|
||||
if err != nil {
|
||||
return boxes, created, ss, as, err
|
||||
}
|
||||
require.NotEmpty(sessionState)
|
||||
require.NotEmpty(state)
|
||||
if ss != SessionState("") {
|
||||
require.Equal(ss, sessionState)
|
||||
}
|
||||
if as != EmptyState {
|
||||
require.NotEqual(as, state)
|
||||
}
|
||||
require.NotNil(a)
|
||||
created = append(created, *a)
|
||||
ss = sessionState
|
||||
as = state
|
||||
|
||||
printer(fmt.Sprintf("📅 created %*s/%v id=%v", int(math.Log10(float64(count))+1), strconv.Itoa(int(i+1)), count, a.Id))
|
||||
}
|
||||
return boxes, created, ss, as, nil
|
||||
}
|
||||
|
||||
type EventsBoxes struct {
|
||||
categories bool
|
||||
keywords bool
|
||||
@@ -98,7 +274,7 @@ func (s *StalwartTest) fillEvents( //NOSONAR
|
||||
|
||||
boxes := EventsBoxes{}
|
||||
|
||||
printer := func(s string) { log.Println(s) }
|
||||
printer := func(s string) { golog.Println(s) }
|
||||
|
||||
accountId := c.session.PrimaryAccounts.Calendars
|
||||
require.NotEmpty(accountId, "no primary account for calendars in session")
|
||||
|
||||
@@ -1141,7 +1141,7 @@ func pickUser() User {
|
||||
}
|
||||
|
||||
func pickRandoms[T any](s ...T) []T {
|
||||
return pickRandomN[T](rand.Intn(len(s)), s...)
|
||||
return pickRandomN(rand.Intn(len(s)), s...)
|
||||
}
|
||||
|
||||
func pickRandomN[T any](n int, s ...T) []T {
|
||||
@@ -1213,3 +1213,125 @@ func deepEqual[T any](t *testing.T, expected, actual T) {
|
||||
}
|
||||
require.Empty(t, diff)
|
||||
}
|
||||
|
||||
func containerTest[OBJ Idable, RESP GetResponse[OBJ], BOXES any, CHANGE Change](t *testing.T, //NOSONAR
|
||||
acc func(session *Session) string,
|
||||
obj func(RESP) []OBJ,
|
||||
id func(OBJ) string,
|
||||
get func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *clog.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error),
|
||||
update func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *clog.Logger, acceptLanguage string, id string, change CHANGE) (OBJ, SessionState, State, Language, Error),
|
||||
destroy func(s *StalwartTest, accountId string, session *Session, ctx context.Context, logger *clog.Logger, acceptLanguage string, ids []string) (map[string]SetError, SessionState, State, Language, Error),
|
||||
fill func(s *StalwartTest, t *testing.T, accountId string, count uint, session *Session, _ User, principalIds []string) (BOXES, []OBJ, SessionState, State, error),
|
||||
change func(OBJ) CHANGE,
|
||||
checkChanged func(t *testing.T, orig OBJ, change CHANGE, changed OBJ),
|
||||
) {
|
||||
require := require.New(t)
|
||||
|
||||
s, err := newStalwartTest(t, withDirectoryQueries(true))
|
||||
require.NoError(err)
|
||||
defer s.Close()
|
||||
|
||||
user := pickUser()
|
||||
session := s.Session(user.name)
|
||||
|
||||
accountId := acc(session)
|
||||
|
||||
// we first need to retrieve the list of all the Principals in order to be able to use and test sharing
|
||||
principalIds := []string{}
|
||||
{
|
||||
principals, _, _, _, err := s.client.GetPrincipals(accountId, session, s.ctx, s.logger, "", []string{})
|
||||
require.NoError(err)
|
||||
require.NotEmpty(principals.Principals)
|
||||
principalIds = structs.Map(principals.Principals, func(p Principal) string { return p.Id })
|
||||
}
|
||||
|
||||
ss := SessionState("")
|
||||
as := EmptyState
|
||||
|
||||
// we need to fetch the ID of the default object that automatically exists for each user, in order to exclude it
|
||||
// from the tests below
|
||||
defaultContainerId := ""
|
||||
{
|
||||
resp, sessionState, state, _, err := get(s, accountId, session, s.ctx, s.logger, "", []string{})
|
||||
require.NoError(err)
|
||||
require.Empty(resp.GetNotFound())
|
||||
objs := obj(resp)
|
||||
require.Len(objs, 1) // the personal calendar that exists by default
|
||||
defaultContainerId = id(objs[0])
|
||||
ss = sessionState
|
||||
as = state
|
||||
}
|
||||
|
||||
// we are going to create a random amount of objects
|
||||
num := uint(5 + rand.Intn(30))
|
||||
{
|
||||
boxes, all, sessionState, state, err := fill(s, t, accountId, num, session, user, principalIds)
|
||||
require.NoError(err)
|
||||
require.Len(all, int(num))
|
||||
ss = sessionState
|
||||
as = state
|
||||
|
||||
{
|
||||
// lets retrieve all the existing objects by passing an empty ID slice
|
||||
resp, sessionState, state, _, err := get(s, accountId, session, s.ctx, s.logger, "", []string{})
|
||||
require.NoError(err)
|
||||
require.Empty(resp.GetNotFound())
|
||||
objs := obj(resp)
|
||||
// lets skip the default object since we did not create that one
|
||||
found := structs.Filter(objs, func(a OBJ) bool { return id(a) != defaultContainerId })
|
||||
require.Len(found, int(num))
|
||||
m := structs.Index(found, id)
|
||||
require.Len(m, int(num))
|
||||
require.Equal(sessionState, ss)
|
||||
require.Equal(state, as)
|
||||
|
||||
for _, a := range all {
|
||||
i := id(a)
|
||||
require.Contains(m, i)
|
||||
found, ok := m[i]
|
||||
require.True(ok)
|
||||
require.Equal(a, found)
|
||||
}
|
||||
|
||||
ss = sessionState
|
||||
as = state
|
||||
}
|
||||
|
||||
// lets retrieve every object we created by its ID
|
||||
for _, a := range all {
|
||||
i := id(a)
|
||||
resp, sessionState, state, _, err := get(s, accountId, session, s.ctx, s.logger, "", []string{i})
|
||||
require.NoError(err)
|
||||
require.Empty(resp.GetNotFound())
|
||||
objs := obj(resp)
|
||||
require.Len(objs, 1)
|
||||
require.Equal(sessionState, ss)
|
||||
require.Equal(state, as)
|
||||
require.Equal(objs[0], a)
|
||||
}
|
||||
|
||||
// lets modify each AddressBook
|
||||
for _, a := range all {
|
||||
i := id(a)
|
||||
ch := change(a)
|
||||
changed, sessionState, state, _, err := update(s, accountId, session, s.ctx, s.logger, "", i, ch)
|
||||
require.NoError(err)
|
||||
require.NotEqual(a, changed)
|
||||
require.Equal(sessionState, ss)
|
||||
require.NotEqual(state, as)
|
||||
checkChanged(t, a, ch, changed)
|
||||
}
|
||||
|
||||
// now lets delete each object that we created, all at once
|
||||
ids := structs.Map(all, id)
|
||||
{
|
||||
errMap, sessionState, state, _, err := destroy(s, accountId, session, s.ctx, s.logger, "", ids)
|
||||
require.NoError(err)
|
||||
require.Empty(errMap)
|
||||
require.Equal(sessionState, ss)
|
||||
require.NotEqual(state, as)
|
||||
}
|
||||
|
||||
allBoxesAreTicked(t, boxes)
|
||||
}
|
||||
}
|
||||
|
||||
1019
pkg/jmap/model.go
1019
pkg/jmap/model.go
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscalendar"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
c "github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
)
|
||||
|
||||
@@ -924,14 +923,6 @@ func (e Exemplar) AddressBookGetResponse() AddressBookGetResponse {
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exemplar) AddressBooksResponse() AddressBooksResponse {
|
||||
a := e.AddressBook()
|
||||
b, _, _ := e.OtherAddressBook()
|
||||
return AddressBooksResponse{
|
||||
AddressBooks: []AddressBook{a, b},
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exemplar) JSContactEmailAddress() c.EmailAddress {
|
||||
return c.EmailAddress{
|
||||
Type: c.EmailAddressType,
|
||||
@@ -1077,13 +1068,6 @@ func (e Exemplar) Calendar() Calendar {
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exemplar) CalendarsResponse() CalendarsResponse {
|
||||
a := e.Calendar()
|
||||
return CalendarsResponse{
|
||||
Calendars: []Calendar{a},
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exemplar) CalendarGetResponse() CalendarGetResponse {
|
||||
a := e.Calendar()
|
||||
return CalendarGetResponse{
|
||||
@@ -1400,10 +1384,10 @@ func (e Exemplar) PersonalInfo() c.PersonalInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exemplar) DesignContactCard() (c.ContactCard, string, string) {
|
||||
func (e Exemplar) DesignContactCard() (ContactCard, string, string) {
|
||||
created, _ := time.Parse(time.RFC3339, "2025-07-09T07:12:28+02:00")
|
||||
updated, _ := time.Parse(time.RFC3339, "2025-07-10T09:58:01+02:00")
|
||||
return c.ContactCard{
|
||||
return ContactCard{
|
||||
Type: c.ContactCardType,
|
||||
Kind: c.ContactCardKindIndividual,
|
||||
Id: "loTh8ahmubei",
|
||||
@@ -1572,10 +1556,10 @@ func (e Exemplar) DesignContactCard() (c.ContactCard, string, string) {
|
||||
}, "Another Contact Card", "other"
|
||||
}
|
||||
|
||||
func (e Exemplar) ContactCard() c.ContactCard {
|
||||
func (e Exemplar) ContactCard() ContactCard {
|
||||
created, _ := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00")
|
||||
updated, _ := time.Parse(time.RFC3339, "2025-09-26T09:58:01+02:00")
|
||||
return c.ContactCard{
|
||||
return ContactCard{
|
||||
Type: c.ContactCardType,
|
||||
Kind: c.ContactCardKindGroup,
|
||||
Id: "20fba820-2f8e-432d-94f1-5abbb59d3ed7",
|
||||
@@ -1876,7 +1860,7 @@ func (e Exemplar) ContactCardGetResponse() ContactCardGetResponse {
|
||||
AccountId: e.AccountId,
|
||||
State: "ewohl8ie",
|
||||
NotFound: []string{"eeaa2"},
|
||||
List: []c.ContactCard{a, b},
|
||||
List: []ContactCard{a, b},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1899,7 +1883,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
Description: "James Holden will be confirmed as the President of the Transport Union, in room 2201 on station TSL-5.",
|
||||
DescriptionContentType: "text/plain",
|
||||
Links: map[string]jscalendar.Link{
|
||||
"aig1oh": jscalendar.Link{
|
||||
"aig1oh": {
|
||||
Type: jscalendar.LinkType,
|
||||
Href: "https://expanse.fandom.com/wiki/TSL-5",
|
||||
ContentType: "text/html",
|
||||
@@ -1916,7 +1900,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
},
|
||||
ShowWithoutTime: false,
|
||||
Locations: map[string]jscalendar.Location{
|
||||
"eigha6": jscalendar.Location{
|
||||
"eigha6": {
|
||||
Type: jscalendar.LocationType,
|
||||
Name: "Room 2201",
|
||||
LocationTypes: map[jscalendar.LocationTypeOption]bool{
|
||||
@@ -1924,7 +1908,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
},
|
||||
Coordinates: "geo:40.7495,-73.9681",
|
||||
Links: map[string]jscalendar.Link{
|
||||
"ohb6qu": jscalendar.Link{
|
||||
"ohb6qu": {
|
||||
Type: jscalendar.LinkType,
|
||||
Href: "https://nss.org/what-is-l5/",
|
||||
ContentType: "text/html",
|
||||
@@ -1937,7 +1921,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
Sequence: 0,
|
||||
MainLocationId: "eigha6",
|
||||
VirtualLocations: map[string]jscalendar.VirtualLocation{
|
||||
"eec4ei": jscalendar.VirtualLocation{
|
||||
"eec4ei": {
|
||||
Type: jscalendar.VirtualLocationType,
|
||||
Name: "OpenTalk",
|
||||
Uri: "https://earth.gov.example.com/opentalk/l5/2022",
|
||||
@@ -1953,7 +1937,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
Privacy: jscalendar.PrivacyPublic,
|
||||
SentBy: "avasarala@earth.gov.example.com",
|
||||
Participants: map[string]jscalendar.Participant{
|
||||
"xaku3f": jscalendar.Participant{
|
||||
"xaku3f": {
|
||||
Type: jscalendar.ParticipantType,
|
||||
Name: "Christjen Avasarala",
|
||||
Email: "crissy@earth.gov.example.com",
|
||||
@@ -1965,7 +1949,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
},
|
||||
ParticipationStatus: jscalendar.ParticipationStatusAccepted,
|
||||
},
|
||||
"chao1a": jscalendar.Participant{
|
||||
"chao1a": {
|
||||
Type: jscalendar.ParticipantType,
|
||||
Name: "Camina Drummer",
|
||||
Email: "camina@opa.org.example.com",
|
||||
@@ -1978,7 +1962,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
ExpectReply: true,
|
||||
InvitedBy: "xaku3f",
|
||||
},
|
||||
"ees0oo": jscalendar.Participant{
|
||||
"ees0oo": {
|
||||
Type: jscalendar.ParticipantType,
|
||||
Name: "James Holden",
|
||||
Email: "james.holden@rocinante.space",
|
||||
@@ -1992,7 +1976,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
},
|
||||
},
|
||||
Alerts: map[string]jscalendar.Alert{
|
||||
"kus9fa": jscalendar.Alert{
|
||||
"kus9fa": {
|
||||
Type: jscalendar.AlertType,
|
||||
Action: jscalendar.AlertActionDisplay,
|
||||
Trigger: jscalendar.OffsetTrigger{
|
||||
@@ -2001,7 +1985,7 @@ func (e Exemplar) CalendarEvent() CalendarEvent {
|
||||
RelativeTo: jscalendar.RelativeToStart,
|
||||
},
|
||||
},
|
||||
"lohve9": jscalendar.Alert{
|
||||
"lohve9": {
|
||||
Type: jscalendar.AlertType,
|
||||
Action: jscalendar.AlertActionDisplay,
|
||||
Trigger: jscalendar.OffsetTrigger{
|
||||
@@ -2047,7 +2031,7 @@ func (e Exemplar) ContactCardChanges() (ContactCardChanges, string, string) {
|
||||
OldState: "xai3iiraipoo",
|
||||
NewState: "ni7thah7eeY4",
|
||||
HasMoreChanges: true,
|
||||
Created: []jscontact.ContactCard{c},
|
||||
Created: []ContactCard{c},
|
||||
Destroyed: []string{"eaae", "bcba"},
|
||||
}, "A created ContactCard and two deleted ones", "created"
|
||||
}
|
||||
@@ -2058,7 +2042,7 @@ func (e Exemplar) OtherContactCardChanges() (ContactCardChanges, string, string)
|
||||
OldState: "xai3iiraipoo",
|
||||
NewState: "ni7thah7eeY4",
|
||||
HasMoreChanges: false,
|
||||
Updated: []jscontact.ContactCard{c},
|
||||
Updated: []ContactCard{c},
|
||||
}, "An updated ContactCard", "updated"
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -17,3 +20,606 @@ func TestObjectNames(t *testing.T) { //NOSONAR
|
||||
require.Equal(prefix, v)
|
||||
}
|
||||
}
|
||||
|
||||
func jsoneq[X any](t *testing.T, expected string, object X) {
|
||||
data, err := json.MarshalIndent(object, "", "")
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, expected, string(data))
|
||||
|
||||
var rec X
|
||||
err = json.Unmarshal(data, &rec)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, object, rec)
|
||||
}
|
||||
|
||||
func TestContactCard(t *testing.T) {
|
||||
created, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00")
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := time.Parse(time.RFC3339, "2025-09-26T09:58:01+02:00")
|
||||
require.NoError(t, err)
|
||||
|
||||
jsoneq(t, `{
|
||||
"@type": "Card",
|
||||
"kind": "group",
|
||||
"id": "20fba820-2f8e-432d-94f1-5abbb59d3ed7",
|
||||
"addressBookIds": {
|
||||
"79047052-ae0e-4299-8860-5bff1a139f3d": true,
|
||||
"44eb6105-08c1-458b-895e-4ad1149dfabd": true
|
||||
},
|
||||
"version": "1.0",
|
||||
"created": "2025-09-25T18:26:14.094725532+02:00",
|
||||
"language": "fr-BE",
|
||||
"members": {
|
||||
"314815dd-81c8-4640-aace-6dc83121616d": true,
|
||||
"c528b277-d8cb-45f2-b7df-1aa3df817463": true,
|
||||
"81dea240-c0a4-4929-82e7-79e713a8bbe4": true
|
||||
},
|
||||
"prodId": "OpenCloud Groupware 1.0",
|
||||
"relatedTo": {
|
||||
"urn:uid:ca9d2a62-e068-43b6-a470-46506976d505": {
|
||||
"@type": "Relation",
|
||||
"relation": {
|
||||
"contact": true
|
||||
}
|
||||
},
|
||||
"urn:uid:72183ec2-b218-4983-9c89-ff117eeb7c5e": {
|
||||
"relation": {
|
||||
"emergency": true,
|
||||
"spouse": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"uid": "1091f2bb-6ae6-4074-bb64-df74071d7033",
|
||||
"updated": "2025-09-26T09:58:01+02:00",
|
||||
"name": {
|
||||
"@type": "Name",
|
||||
"components": [
|
||||
{"@type": "NameComponent", "value": "OpenCloud", "kind": "surname"},
|
||||
{"value": " ", "kind": "separator"},
|
||||
{"value": "Team", "kind": "surname2"}
|
||||
],
|
||||
"isOrdered": true,
|
||||
"defaultSeparator": ", ",
|
||||
"sortAs": {
|
||||
"surname": "OpenCloud Team"
|
||||
},
|
||||
"full": "OpenCloud Team"
|
||||
},
|
||||
"nicknames": {
|
||||
"a": {
|
||||
"@type": "Nickname",
|
||||
"name": "The Team",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1
|
||||
}
|
||||
},
|
||||
"organizations": {
|
||||
"o": {
|
||||
"@type": "Organization",
|
||||
"name": "OpenCloud GmbH",
|
||||
"units": [
|
||||
{"@type": "OrgUnit", "name": "Marketing", "sortAs": "marketing"},
|
||||
{"@type": "OrgUnit", "name": "Sales"},
|
||||
{"name": "Operations", "sortAs": "ops"}
|
||||
],
|
||||
"sortAs": "opencloud",
|
||||
"contexts": {
|
||||
"work": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"speakToAs": {
|
||||
"@type": "SpeakToAs",
|
||||
"grammaticalGender": "inanimate",
|
||||
"pronouns": {
|
||||
"p": {
|
||||
"@type": "Pronouns",
|
||||
"pronouns": "it",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"titles": {
|
||||
"t": {
|
||||
"@type": "Title",
|
||||
"name": "The",
|
||||
"kind": "title",
|
||||
"organizationId": "o"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"e": {
|
||||
"@type": "EmailAddress",
|
||||
"address": "info@opencloud.eu.example.com",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "work"
|
||||
}
|
||||
},
|
||||
"onlineServices": {
|
||||
"s": {
|
||||
"@type": "OnlineService",
|
||||
"service": "The Misinformation Game",
|
||||
"uri": "https://misinfogame.com/91886aa0-3586-4ade-b9bb-ec031464a251",
|
||||
"user": "opencloudeu",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "imaginary"
|
||||
}
|
||||
},
|
||||
"phones": {
|
||||
"p": {
|
||||
"@type": "Phone",
|
||||
"number": "+1-804-222-1111",
|
||||
"features": {
|
||||
"voice": true,
|
||||
"text": true
|
||||
},
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "imaginary"
|
||||
}
|
||||
},
|
||||
"preferredLanguages": {
|
||||
"wa": {
|
||||
"@type": "LanguagePref",
|
||||
"language": "wa-BE",
|
||||
"contexts": {
|
||||
"private": true
|
||||
},
|
||||
"pref": 1
|
||||
},
|
||||
"de": {
|
||||
"language": "de-DE",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 2
|
||||
}
|
||||
},
|
||||
"calendars": {
|
||||
"c": {
|
||||
"@type": "Calendar",
|
||||
"kind": "calendar",
|
||||
"uri": "https://opencloud.eu/calendars/521b032b-a2b3-4540-81b9-3f6bccacaab2",
|
||||
"mediaType": "application/jscontact+json",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "work"
|
||||
}
|
||||
},
|
||||
"schedulingAddresses": {
|
||||
"s": {
|
||||
"@type": "SchedulingAddress",
|
||||
"uri": "mailto:scheduling@opencloud.eu.example.com",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "work"
|
||||
}
|
||||
},
|
||||
"addresses": {
|
||||
"k26": {
|
||||
"@type": "Address",
|
||||
"components": [
|
||||
{"@type": "AddressComponent", "kind": "block", "value": "2-7"},
|
||||
{"kind": "separator", "value": "-"},
|
||||
{"kind": "number", "value": "2"},
|
||||
{"kind": "separator", "value": " "},
|
||||
{"kind": "district", "value": "Marunouchi"},
|
||||
{"kind": "locality", "value": "Chiyoda-ku"},
|
||||
{"kind": "region", "value": "Tokyo"},
|
||||
{"kind": "separator", "value": " "},
|
||||
{"kind": "postcode", "value": "100-8994"}
|
||||
],
|
||||
"isOrdered": true,
|
||||
"defaultSeparator": ", ",
|
||||
"full": "2-7-2 Marunouchi, Chiyoda-ku, Tokyo 100-8994",
|
||||
"countryCode": "JP",
|
||||
"coordinates": "geo:35.6796373,139.7616907",
|
||||
"timeZone": "JST",
|
||||
"contexts": {
|
||||
"delivery": true,
|
||||
"work": true
|
||||
},
|
||||
"pref": 2
|
||||
}
|
||||
},
|
||||
"cryptoKeys": {
|
||||
"k1": {
|
||||
"@type": "CryptoKey",
|
||||
"uri": "https://opencloud.eu.example.com/keys/d550f57c-582c-43cc-8d94-822bded9ab36",
|
||||
"mediaType": "application/pgp-keys",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "keys"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"d1": {
|
||||
"@type": "Directory",
|
||||
"kind": "entry",
|
||||
"uri": "https://opencloud.eu.example.com/addressbook/8c2f0363-af0a-4d16-a9d5-8a9cd885d722",
|
||||
"listAs": 1
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"r1": {
|
||||
"@type": "Link",
|
||||
"kind": "contact",
|
||||
"uri": "mailto:contact@opencloud.eu.example.com",
|
||||
"contexts": {
|
||||
"work": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"m": {
|
||||
"@type": "Media",
|
||||
"kind": "logo",
|
||||
"uri": "https://opencloud.eu.example.com/opencloud.svg",
|
||||
"mediaType": "image/svg+xml",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 123,
|
||||
"label": "svg",
|
||||
"blobId": "53feefbabeb146fcbe3e59e91462fa5f"
|
||||
}
|
||||
},
|
||||
"anniversaries": {
|
||||
"birth": {
|
||||
"@type": "Anniversary",
|
||||
"kind": "birth",
|
||||
"date": {
|
||||
"@type": "PartialDate",
|
||||
"year": 2025,
|
||||
"month": 9,
|
||||
"day": 26,
|
||||
"calendarScale": "iso8601"
|
||||
}
|
||||
}
|
||||
},
|
||||
"keywords": {
|
||||
"imaginary": true,
|
||||
"test": true
|
||||
},
|
||||
"notes": {
|
||||
"n1": {
|
||||
"@type": "Note",
|
||||
"note": "This is a note.",
|
||||
"created": "2025-09-25T18:26:14.094725532+02:00",
|
||||
"author": {
|
||||
"@type": "Author",
|
||||
"name": "Test Data",
|
||||
"uri": "https://isbn.example.com/a461f292-6bf1-470e-b08d-f6b4b0223fe3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"personalInfo": {
|
||||
"p1": {
|
||||
"@type": "PersonalInfo",
|
||||
"kind": "expertise",
|
||||
"value": "Clouds",
|
||||
"level": "high",
|
||||
"listAs": 1,
|
||||
"label": "experts"
|
||||
}
|
||||
},
|
||||
"localizations": {
|
||||
"fr": {
|
||||
"personalInfo": {
|
||||
"value": "Nuages"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, ContactCard{
|
||||
Type: jscontact.ContactCardType,
|
||||
Kind: jscontact.ContactCardKindGroup,
|
||||
Id: "20fba820-2f8e-432d-94f1-5abbb59d3ed7",
|
||||
AddressBookIds: map[string]bool{
|
||||
"79047052-ae0e-4299-8860-5bff1a139f3d": true,
|
||||
"44eb6105-08c1-458b-895e-4ad1149dfabd": true,
|
||||
},
|
||||
Version: jscontact.JSContactVersion_1_0,
|
||||
Created: created,
|
||||
Language: "fr-BE",
|
||||
Members: map[string]bool{
|
||||
"314815dd-81c8-4640-aace-6dc83121616d": true,
|
||||
"c528b277-d8cb-45f2-b7df-1aa3df817463": true,
|
||||
"81dea240-c0a4-4929-82e7-79e713a8bbe4": true,
|
||||
},
|
||||
ProdId: "OpenCloud Groupware 1.0",
|
||||
RelatedTo: map[string]jscontact.Relation{
|
||||
"urn:uid:ca9d2a62-e068-43b6-a470-46506976d505": {
|
||||
Type: jscontact.RelationType,
|
||||
Relation: map[jscontact.Relationship]bool{
|
||||
jscontact.RelationContact: true,
|
||||
},
|
||||
},
|
||||
"urn:uid:72183ec2-b218-4983-9c89-ff117eeb7c5e": {
|
||||
Relation: map[jscontact.Relationship]bool{
|
||||
jscontact.RelationEmergency: true,
|
||||
jscontact.RelationSpouse: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Uid: "1091f2bb-6ae6-4074-bb64-df74071d7033",
|
||||
Updated: updated,
|
||||
Name: &jscontact.Name{
|
||||
Type: jscontact.NameType,
|
||||
Components: []jscontact.NameComponent{
|
||||
{Type: jscontact.NameComponentType, Value: "OpenCloud", Kind: jscontact.NameComponentKindSurname},
|
||||
{Value: " ", Kind: jscontact.NameComponentKindSeparator},
|
||||
{Value: "Team", Kind: jscontact.NameComponentKindSurname2},
|
||||
},
|
||||
IsOrdered: true,
|
||||
DefaultSeparator: ", ",
|
||||
SortAs: map[string]string{
|
||||
string(jscontact.NameComponentKindSurname): "OpenCloud Team",
|
||||
},
|
||||
Full: "OpenCloud Team",
|
||||
},
|
||||
Nicknames: map[string]jscontact.Nickname{
|
||||
"a": {
|
||||
Type: jscontact.NicknameType,
|
||||
Name: "The Team",
|
||||
Contexts: map[jscontact.NicknameContext]bool{
|
||||
jscontact.NicknameContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
},
|
||||
},
|
||||
Organizations: map[string]jscontact.Organization{
|
||||
"o": {
|
||||
Type: jscontact.OrganizationType,
|
||||
Name: "OpenCloud GmbH",
|
||||
Units: []jscontact.OrgUnit{
|
||||
{Type: jscontact.OrgUnitType, Name: "Marketing", SortAs: "marketing"},
|
||||
{Type: jscontact.OrgUnitType, Name: "Sales"},
|
||||
{Name: "Operations", SortAs: "ops"},
|
||||
},
|
||||
SortAs: "opencloud",
|
||||
Contexts: map[jscontact.OrganizationContext]bool{
|
||||
jscontact.OrganizationContextWork: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
SpeakToAs: &jscontact.SpeakToAs{
|
||||
Type: jscontact.SpeakToAsType,
|
||||
GrammaticalGender: jscontact.GrammaticalGenderInanimate,
|
||||
Pronouns: map[string]jscontact.Pronouns{
|
||||
"p": {
|
||||
Type: jscontact.PronounsType,
|
||||
Pronouns: "it",
|
||||
Contexts: map[jscontact.PronounsContext]bool{
|
||||
jscontact.PronounsContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
Titles: map[string]jscontact.Title{
|
||||
"t": {
|
||||
Type: jscontact.TitleType,
|
||||
Name: "The",
|
||||
Kind: jscontact.TitleKindTitle,
|
||||
OrganizationId: "o",
|
||||
},
|
||||
},
|
||||
Emails: map[string]jscontact.EmailAddress{
|
||||
"e": {
|
||||
Type: jscontact.EmailAddressType,
|
||||
Address: "info@opencloud.eu.example.com",
|
||||
Contexts: map[jscontact.EmailAddressContext]bool{
|
||||
jscontact.EmailAddressContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "work",
|
||||
},
|
||||
},
|
||||
OnlineServices: map[string]jscontact.OnlineService{
|
||||
"s": {
|
||||
Type: jscontact.OnlineServiceType,
|
||||
Service: "The Misinformation Game",
|
||||
Uri: "https://misinfogame.com/91886aa0-3586-4ade-b9bb-ec031464a251",
|
||||
User: "opencloudeu",
|
||||
Contexts: map[jscontact.OnlineServiceContext]bool{
|
||||
jscontact.OnlineServiceContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "imaginary",
|
||||
},
|
||||
},
|
||||
Phones: map[string]jscontact.Phone{
|
||||
"p": {
|
||||
Type: jscontact.PhoneType,
|
||||
Number: "+1-804-222-1111",
|
||||
Features: map[jscontact.PhoneFeature]bool{
|
||||
jscontact.PhoneFeatureVoice: true,
|
||||
jscontact.PhoneFeatureText: true,
|
||||
},
|
||||
Contexts: map[jscontact.PhoneContext]bool{
|
||||
jscontact.PhoneContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "imaginary",
|
||||
},
|
||||
},
|
||||
PreferredLanguages: map[string]jscontact.LanguagePref{
|
||||
"wa": {
|
||||
Type: jscontact.LanguagePrefType,
|
||||
Language: "wa-BE",
|
||||
Contexts: map[jscontact.LanguagePrefContext]bool{
|
||||
jscontact.LanguagePrefContextPrivate: true,
|
||||
},
|
||||
Pref: 1,
|
||||
},
|
||||
"de": {
|
||||
Language: "de-DE",
|
||||
Contexts: map[jscontact.LanguagePrefContext]bool{
|
||||
jscontact.LanguagePrefContextWork: true,
|
||||
},
|
||||
Pref: 2,
|
||||
},
|
||||
},
|
||||
Calendars: map[string]jscontact.Calendar{
|
||||
"c": {
|
||||
Type: jscontact.CalendarType,
|
||||
Kind: jscontact.CalendarKindCalendar,
|
||||
Uri: "https://opencloud.eu/calendars/521b032b-a2b3-4540-81b9-3f6bccacaab2",
|
||||
MediaType: "application/jscontact+json",
|
||||
Contexts: map[jscontact.CalendarContext]bool{
|
||||
jscontact.CalendarContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "work",
|
||||
},
|
||||
},
|
||||
SchedulingAddresses: map[string]jscontact.SchedulingAddress{
|
||||
"s": {
|
||||
Type: jscontact.SchedulingAddressType,
|
||||
Uri: "mailto:scheduling@opencloud.eu.example.com",
|
||||
Contexts: map[jscontact.SchedulingAddressContext]bool{
|
||||
jscontact.SchedulingAddressContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "work",
|
||||
},
|
||||
},
|
||||
Addresses: map[string]jscontact.Address{
|
||||
"k26": {
|
||||
Type: jscontact.AddressType,
|
||||
Components: []jscontact.AddressComponent{
|
||||
{Type: jscontact.AddressComponentType, Kind: jscontact.AddressComponentKindBlock, Value: "2-7"},
|
||||
{Kind: jscontact.AddressComponentKindSeparator, Value: "-"},
|
||||
{Kind: jscontact.AddressComponentKindNumber, Value: "2"},
|
||||
{Kind: jscontact.AddressComponentKindSeparator, Value: " "},
|
||||
{Kind: jscontact.AddressComponentKindDistrict, Value: "Marunouchi"},
|
||||
{Kind: jscontact.AddressComponentKindLocality, Value: "Chiyoda-ku"},
|
||||
{Kind: jscontact.AddressComponentKindRegion, Value: "Tokyo"},
|
||||
{Kind: jscontact.AddressComponentKindSeparator, Value: " "},
|
||||
{Kind: jscontact.AddressComponentKindPostcode, Value: "100-8994"},
|
||||
},
|
||||
IsOrdered: true,
|
||||
DefaultSeparator: ", ",
|
||||
Full: "2-7-2 Marunouchi, Chiyoda-ku, Tokyo 100-8994",
|
||||
CountryCode: "JP",
|
||||
Coordinates: "geo:35.6796373,139.7616907",
|
||||
TimeZone: "JST",
|
||||
Contexts: map[jscontact.AddressContext]bool{
|
||||
jscontact.AddressContextDelivery: true,
|
||||
jscontact.AddressContextWork: true,
|
||||
},
|
||||
Pref: 2,
|
||||
},
|
||||
},
|
||||
CryptoKeys: map[string]jscontact.CryptoKey{
|
||||
"k1": {
|
||||
Type: jscontact.CryptoKeyType,
|
||||
Uri: "https://opencloud.eu.example.com/keys/d550f57c-582c-43cc-8d94-822bded9ab36",
|
||||
MediaType: "application/pgp-keys",
|
||||
Contexts: map[jscontact.CryptoKeyContext]bool{
|
||||
jscontact.CryptoKeyContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "keys",
|
||||
},
|
||||
},
|
||||
Directories: map[string]jscontact.Directory{
|
||||
"d1": {
|
||||
Type: jscontact.DirectoryType,
|
||||
Kind: jscontact.DirectoryKindEntry,
|
||||
Uri: "https://opencloud.eu.example.com/addressbook/8c2f0363-af0a-4d16-a9d5-8a9cd885d722",
|
||||
ListAs: 1,
|
||||
},
|
||||
},
|
||||
Links: map[string]jscontact.Link{
|
||||
"r1": {
|
||||
Type: jscontact.LinkType,
|
||||
Kind: jscontact.LinkKindContact,
|
||||
Contexts: map[jscontact.LinkContext]bool{
|
||||
jscontact.LinkContextWork: true,
|
||||
},
|
||||
Uri: "mailto:contact@opencloud.eu.example.com",
|
||||
},
|
||||
},
|
||||
Media: map[string]jscontact.Media{
|
||||
"m": {
|
||||
Type: jscontact.MediaType,
|
||||
Kind: jscontact.MediaKindLogo,
|
||||
Uri: "https://opencloud.eu.example.com/opencloud.svg",
|
||||
MediaType: "image/svg+xml",
|
||||
Contexts: map[jscontact.MediaContext]bool{
|
||||
jscontact.MediaContextWork: true,
|
||||
},
|
||||
Pref: 123,
|
||||
Label: "svg",
|
||||
BlobId: "53feefbabeb146fcbe3e59e91462fa5f",
|
||||
},
|
||||
},
|
||||
Anniversaries: map[string]jscontact.Anniversary{
|
||||
"birth": {
|
||||
Type: jscontact.AnniversaryType,
|
||||
Kind: jscontact.AnniversaryKindBirth,
|
||||
Date: &jscontact.PartialDate{
|
||||
Type: jscontact.PartialDateType,
|
||||
Year: 2025,
|
||||
Month: 9,
|
||||
Day: 26,
|
||||
CalendarScale: "iso8601",
|
||||
},
|
||||
},
|
||||
},
|
||||
Keywords: map[string]bool{
|
||||
"imaginary": true,
|
||||
"test": true,
|
||||
},
|
||||
Notes: map[string]jscontact.Note{
|
||||
"n1": {
|
||||
Type: jscontact.NoteType,
|
||||
Note: "This is a note.",
|
||||
Created: created,
|
||||
Author: &jscontact.Author{
|
||||
Type: jscontact.AuthorType,
|
||||
Name: "Test Data",
|
||||
Uri: "https://isbn.example.com/a461f292-6bf1-470e-b08d-f6b4b0223fe3",
|
||||
},
|
||||
},
|
||||
},
|
||||
PersonalInfo: map[string]jscontact.PersonalInfo{
|
||||
"p1": {
|
||||
Type: jscontact.PersonalInfoType,
|
||||
Kind: jscontact.PersonalInfoKindExpertise,
|
||||
Value: "Clouds",
|
||||
Level: jscontact.PersonalInfoLevelHigh,
|
||||
ListAs: 1,
|
||||
Label: "experts",
|
||||
},
|
||||
},
|
||||
Localizations: map[string]jscontact.PatchObject{
|
||||
"fr": {
|
||||
"personalInfo": map[string]any{
|
||||
"value": "Nuages",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,68 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func get[GETREQ GetCommand, GETRESP GetResponse, RESP any]( //NOSONAR
|
||||
type Factory[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any] interface {
|
||||
Namespaces() []JmapNamespace
|
||||
CreateGetCommand(accountId string, ids []string) GETREQ
|
||||
CreateGetResponse() GETRESP
|
||||
MapChanges(oldState, newState State, hasMoreChanges bool, created, updated []T, destroyed []string) CHANGES
|
||||
}
|
||||
|
||||
type Mailboxes string
|
||||
|
||||
const MAILBOX = Mailboxes("MAILBOX")
|
||||
|
||||
var _ Factory[Mailbox, MailboxGetCommand, MailboxGetResponse, MailboxChanges] = MAILBOX
|
||||
|
||||
func (f Mailboxes) Namespaces() []JmapNamespace {
|
||||
return NS_MAILBOX
|
||||
}
|
||||
|
||||
func (f Mailboxes) CreateGetCommand(accountId string, ids []string) MailboxGetCommand {
|
||||
return MailboxGetCommand{AccountId: accountId, Ids: ids}
|
||||
}
|
||||
|
||||
func (f Mailboxes) CreateGetResponse() MailboxGetResponse {
|
||||
return MailboxGetResponse{}
|
||||
}
|
||||
|
||||
func (f Mailboxes) MapChanges(oldState, newState State, hasMoreChanges bool, created, updated []Mailbox, destroyed []string) MailboxChanges {
|
||||
return MailboxChanges{
|
||||
OldState: oldState,
|
||||
NewState: newState,
|
||||
HasMoreChanges: hasMoreChanges,
|
||||
Created: created,
|
||||
Updated: updated,
|
||||
Destroyed: destroyed,
|
||||
}
|
||||
}
|
||||
|
||||
func fget[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR
|
||||
client *Client, name string,
|
||||
accountId string, ids []string,
|
||||
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (GETRESP, SessionState, State, Language, Error) {
|
||||
var getresp GETRESP
|
||||
return get(client, name, f.Namespaces(),
|
||||
f.CreateGetCommand,
|
||||
getresp,
|
||||
identity1,
|
||||
accountId, session, ctx, logger, acceptLanguage, ids,
|
||||
)
|
||||
}
|
||||
|
||||
func fgetA[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR
|
||||
client *Client, name string,
|
||||
accountId string, ids []string,
|
||||
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) ([]T, SessionState, State, Language, Error) {
|
||||
var getresp GETRESP
|
||||
return getA(client, name, f.Namespaces(),
|
||||
f.CreateGetCommand,
|
||||
getresp,
|
||||
accountId, session, ctx, logger, acceptLanguage, ids,
|
||||
)
|
||||
}
|
||||
|
||||
func get[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
getCommandFactory func(string, []string) GETREQ,
|
||||
_ GETRESP,
|
||||
@@ -38,7 +99,42 @@ func get[GETREQ GetCommand, GETRESP GetResponse, RESP any]( //NOSONAR
|
||||
})
|
||||
}
|
||||
|
||||
func getN[GETREQ GetCommand, GETRESP GetResponse, ITEM any, RESP any]( //NOSONAR
|
||||
func getA[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T]]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
getCommandFactory func(string, []string) GETREQ,
|
||||
resp GETRESP,
|
||||
accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) ([]T, SessionState, State, Language, Error) {
|
||||
return get(client, name, using, getCommandFactory, resp, func(r GETRESP) []T { return r.GetList() }, accountId, session, ctx, logger, acceptLanguage, ids)
|
||||
}
|
||||
|
||||
func fgetAN[F Factory[T, GETREQ, GETRESP, CHANGES], T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any, CHANGES any](f Factory[T, GETREQ, GETRESP, CHANGES], //NOSONAR
|
||||
client *Client, name string,
|
||||
respMapper func(map[string][]T) RESP,
|
||||
accountIds []string, ids []string,
|
||||
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) {
|
||||
var getresp GETRESP
|
||||
return getAN(client, name, f.Namespaces(),
|
||||
f.CreateGetCommand,
|
||||
getresp,
|
||||
respMapper,
|
||||
accountIds, session, ctx, logger, acceptLanguage, ids,
|
||||
)
|
||||
}
|
||||
|
||||
func getAN[T Foo, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
getCommandFactory func(string, []string) GETREQ,
|
||||
resp GETRESP,
|
||||
respMapper func(map[string][]T) RESP,
|
||||
accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) {
|
||||
return getN(client, name, using, getCommandFactory, resp,
|
||||
func(r GETRESP) []T { return r.GetList() },
|
||||
respMapper,
|
||||
accountIds, session, ctx, logger, acceptLanguage, ids,
|
||||
)
|
||||
}
|
||||
|
||||
func getN[T Foo, ITEM any, GETREQ GetCommand[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
getCommandFactory func(string, []string) GETREQ,
|
||||
_ GETRESP,
|
||||
@@ -80,7 +176,7 @@ func getN[GETREQ GetCommand, GETRESP GetResponse, ITEM any, RESP any]( //NOSONAR
|
||||
})
|
||||
}
|
||||
|
||||
func create[T any, C any, SETREQ SetCommand, GETREQ GetCommand, SETRESP SetResponse, GETRESP GetResponse]( //NOSONAR
|
||||
func create[T Foo, C any, SETREQ SetCommand[T], GETREQ GetCommand[T], SETRESP SetResponse[T], GETRESP GetResponse[T]]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
setCommandFactory func(string, map[string]C) SETREQ,
|
||||
getCommandFactory func(string, string) GETREQ,
|
||||
@@ -139,7 +235,7 @@ func create[T any, C any, SETREQ SetCommand, GETREQ GetCommand, SETRESP SetRespo
|
||||
})
|
||||
}
|
||||
|
||||
func destroy[REQ SetCommand, RESP SetResponse](client *Client, name string, using []JmapNamespace, //NOSONAR
|
||||
func destroy[T Foo, REQ SetCommand[T], RESP SetResponse[T]](client *Client, name string, using []JmapNamespace, //NOSONAR
|
||||
setCommandFactory func(string, []string) REQ, _ RESP,
|
||||
accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) {
|
||||
logger = client.logger(name, session, logger)
|
||||
@@ -162,7 +258,23 @@ func destroy[REQ SetCommand, RESP SetResponse](client *Client, name string, usin
|
||||
})
|
||||
}
|
||||
|
||||
func changes[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesResponse, GETRESP GetResponse, ITEM any, RESP any]( //NOSONAR
|
||||
func changesA[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], RESP any]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
changesCommandFactory func() CHANGESREQ,
|
||||
changesResp CHANGESRESP,
|
||||
_ GETRESP,
|
||||
getCommandFactory func(string, string) GETREQ,
|
||||
respMapper func(State, State, bool, []T, []T, []string) RESP,
|
||||
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) {
|
||||
|
||||
return changes(client, name, using, changesCommandFactory, changesResp, getCommandFactory,
|
||||
func(r GETRESP) []T { return r.GetList() },
|
||||
respMapper,
|
||||
session, ctx, logger, acceptLanguage,
|
||||
)
|
||||
}
|
||||
|
||||
func changes[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, RESP any]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
changesCommandFactory func() CHANGESREQ,
|
||||
_ CHANGESRESP,
|
||||
@@ -216,7 +328,7 @@ func changes[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesRe
|
||||
})
|
||||
}
|
||||
|
||||
func changesN[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesResponse, GETRESP GetResponse, ITEM any, CHANGESITEM any, RESP any]( //NOSONAR
|
||||
func changesN[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, CHANGESITEM any, RESP any]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
accountIds []string, sinceStateMap map[string]State,
|
||||
changesCommandFactory func(string, State) CHANGESREQ,
|
||||
@@ -301,7 +413,7 @@ func changesN[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesR
|
||||
})
|
||||
}
|
||||
|
||||
func updates[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesResponse, GETRESP GetResponse, ITEM any, RESP any]( //NOSONAR
|
||||
func updates[T Foo, CHANGESREQ ChangesCommand[T], GETREQ GetCommand[T], CHANGESRESP ChangesResponse[T], GETRESP GetResponse[T], ITEM any, RESP any]( //NOSONAR
|
||||
client *Client, name string, using []JmapNamespace,
|
||||
changesCommandFactory func() CHANGESREQ,
|
||||
_ CHANGESRESP,
|
||||
@@ -343,7 +455,7 @@ func updates[CHANGESREQ ChangesCommand, GETREQ GetCommand, CHANGESRESP ChangesRe
|
||||
})
|
||||
}
|
||||
|
||||
func update[CHANGES Change, SET SetCommand, GET GetCommand, RESP any, SETRESP SetResponse, GETRESP GetResponse](client *Client, name string, using []JmapNamespace, //NOSONAR
|
||||
func update[T Foo, CHANGES Change, SET SetCommand[T], GET GetCommand[T], RESP any, SETRESP SetResponse[T], GETRESP GetResponse[T]](client *Client, name string, using []JmapNamespace, //NOSONAR
|
||||
setCommandFactory func(map[string]PatchObject) SET,
|
||||
getCommandFactory func(string) GET,
|
||||
notUpdatedExtractor func(SETRESP) map[string]SetError,
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscalendar"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
type eventListeners[T any] struct {
|
||||
@@ -87,6 +86,7 @@ func command[T any](api ApiClient, //NOSONAR
|
||||
for _, mr := range response.MethodResponses {
|
||||
if mr.Command == ErrorCommand {
|
||||
if errorParameters, ok := mr.Parameters.(ErrorResponse); ok {
|
||||
// TODO deal with stateMismatch differently, as it's not an error per se, but rather "optimistic update"
|
||||
code := JmapErrorServerFail
|
||||
switch errorParameters.Type {
|
||||
case MethodLevelErrorServerUnavailable:
|
||||
@@ -233,19 +233,15 @@ func tryRetrieveResponseMatchParameters[T any](logger *log.Logger, data *Respons
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func retrieveGet[C GetCommand, T GetResponse](logger *log.Logger, data *Response, command C, tag string, target *T) Error {
|
||||
func retrieveGet[T Foo, C GetCommand[T], R GetResponse[T]](logger *log.Logger, data *Response, command C, tag string, target *R) Error {
|
||||
return retrieveResponseMatchParameters(logger, data, command.GetCommand(), tag, target)
|
||||
}
|
||||
|
||||
func retrieveSet[C SetCommand, T SetResponse](logger *log.Logger, data *Response, command C, tag string, target *T) Error {
|
||||
func retrieveSet[T Foo, C SetCommand[T], R SetResponse[T]](logger *log.Logger, data *Response, command C, tag string, target *R) Error {
|
||||
return retrieveResponseMatchParameters(logger, data, command.GetCommand(), tag, target)
|
||||
}
|
||||
|
||||
func retrieveQuery[C QueryCommand, T QueryResponse](logger *log.Logger, data *Response, command C, tag string, target *T) Error {
|
||||
return retrieveResponseMatchParameters(logger, data, command.GetCommand(), tag, target)
|
||||
}
|
||||
|
||||
func retrieveChanges[C ChangesCommand, T ChangesResponse](logger *log.Logger, data *Response, command C, tag string, target *T) Error {
|
||||
func retrieveChanges[T Foo, C ChangesCommand[T], R ChangesResponse[T]](logger *log.Logger, data *Response, command C, tag string, target *R) Error {
|
||||
return retrieveResponseMatchParameters(logger, data, command.GetCommand(), tag, target)
|
||||
}
|
||||
|
||||
@@ -304,9 +300,11 @@ func squashState(all map[string]State) State {
|
||||
return squashStateFunc(all, func(s State) State { return s })
|
||||
}
|
||||
|
||||
/*
|
||||
func squashStates(states ...State) State {
|
||||
return State(strings.Join(structs.Map(states, func(s State) string { return string(s) }), ","))
|
||||
}
|
||||
*/
|
||||
|
||||
func squashKeyedStates(m map[string]State) State {
|
||||
return squashStateFunc(m, identity1)
|
||||
@@ -395,6 +393,9 @@ func identity1[T any](t T) T {
|
||||
return t
|
||||
}
|
||||
|
||||
func list[T Foo, GETRESP GetResponse[T]](r GETRESP) []T { return r.GetList() }
|
||||
func getid[T Idable](r T) string { return r.GetId() }
|
||||
|
||||
func posUIntPtr(i uint) *uint {
|
||||
if i > 0 {
|
||||
return &i
|
||||
|
||||
@@ -2260,6 +2260,8 @@ type ContactCard struct {
|
||||
PersonalInfo map[string]PersonalInfo `json:"personalInfo,omitempty"`
|
||||
}
|
||||
|
||||
func (f ContactCard) GetId() string { return f.Id }
|
||||
|
||||
const (
|
||||
ContactCardPropertyId = "id"
|
||||
ContactCardPropertyAddressBookIds = "addressBookIds"
|
||||
|
||||
@@ -658,595 +658,3 @@ func TestPersonalInfo(t *testing.T) {
|
||||
Label: "opa",
|
||||
})
|
||||
}
|
||||
|
||||
func TestContactCard(t *testing.T) {
|
||||
created, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00")
|
||||
require.NoError(t, err)
|
||||
|
||||
updated, err := time.Parse(time.RFC3339, "2025-09-26T09:58:01+02:00")
|
||||
require.NoError(t, err)
|
||||
|
||||
jsoneq(t, `{
|
||||
"@type": "Card",
|
||||
"kind": "group",
|
||||
"id": "20fba820-2f8e-432d-94f1-5abbb59d3ed7",
|
||||
"addressBookIds": {
|
||||
"79047052-ae0e-4299-8860-5bff1a139f3d": true,
|
||||
"44eb6105-08c1-458b-895e-4ad1149dfabd": true
|
||||
},
|
||||
"version": "1.0",
|
||||
"created": "2025-09-25T18:26:14.094725532+02:00",
|
||||
"language": "fr-BE",
|
||||
"members": {
|
||||
"314815dd-81c8-4640-aace-6dc83121616d": true,
|
||||
"c528b277-d8cb-45f2-b7df-1aa3df817463": true,
|
||||
"81dea240-c0a4-4929-82e7-79e713a8bbe4": true
|
||||
},
|
||||
"prodId": "OpenCloud Groupware 1.0",
|
||||
"relatedTo": {
|
||||
"urn:uid:ca9d2a62-e068-43b6-a470-46506976d505": {
|
||||
"@type": "Relation",
|
||||
"relation": {
|
||||
"contact": true
|
||||
}
|
||||
},
|
||||
"urn:uid:72183ec2-b218-4983-9c89-ff117eeb7c5e": {
|
||||
"relation": {
|
||||
"emergency": true,
|
||||
"spouse": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"uid": "1091f2bb-6ae6-4074-bb64-df74071d7033",
|
||||
"updated": "2025-09-26T09:58:01+02:00",
|
||||
"name": {
|
||||
"@type": "Name",
|
||||
"components": [
|
||||
{"@type": "NameComponent", "value": "OpenCloud", "kind": "surname"},
|
||||
{"value": " ", "kind": "separator"},
|
||||
{"value": "Team", "kind": "surname2"}
|
||||
],
|
||||
"isOrdered": true,
|
||||
"defaultSeparator": ", ",
|
||||
"sortAs": {
|
||||
"surname": "OpenCloud Team"
|
||||
},
|
||||
"full": "OpenCloud Team"
|
||||
},
|
||||
"nicknames": {
|
||||
"a": {
|
||||
"@type": "Nickname",
|
||||
"name": "The Team",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1
|
||||
}
|
||||
},
|
||||
"organizations": {
|
||||
"o": {
|
||||
"@type": "Organization",
|
||||
"name": "OpenCloud GmbH",
|
||||
"units": [
|
||||
{"@type": "OrgUnit", "name": "Marketing", "sortAs": "marketing"},
|
||||
{"@type": "OrgUnit", "name": "Sales"},
|
||||
{"name": "Operations", "sortAs": "ops"}
|
||||
],
|
||||
"sortAs": "opencloud",
|
||||
"contexts": {
|
||||
"work": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"speakToAs": {
|
||||
"@type": "SpeakToAs",
|
||||
"grammaticalGender": "inanimate",
|
||||
"pronouns": {
|
||||
"p": {
|
||||
"@type": "Pronouns",
|
||||
"pronouns": "it",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"titles": {
|
||||
"t": {
|
||||
"@type": "Title",
|
||||
"name": "The",
|
||||
"kind": "title",
|
||||
"organizationId": "o"
|
||||
}
|
||||
},
|
||||
"emails": {
|
||||
"e": {
|
||||
"@type": "EmailAddress",
|
||||
"address": "info@opencloud.eu.example.com",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "work"
|
||||
}
|
||||
},
|
||||
"onlineServices": {
|
||||
"s": {
|
||||
"@type": "OnlineService",
|
||||
"service": "The Misinformation Game",
|
||||
"uri": "https://misinfogame.com/91886aa0-3586-4ade-b9bb-ec031464a251",
|
||||
"user": "opencloudeu",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "imaginary"
|
||||
}
|
||||
},
|
||||
"phones": {
|
||||
"p": {
|
||||
"@type": "Phone",
|
||||
"number": "+1-804-222-1111",
|
||||
"features": {
|
||||
"voice": true,
|
||||
"text": true
|
||||
},
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "imaginary"
|
||||
}
|
||||
},
|
||||
"preferredLanguages": {
|
||||
"wa": {
|
||||
"@type": "LanguagePref",
|
||||
"language": "wa-BE",
|
||||
"contexts": {
|
||||
"private": true
|
||||
},
|
||||
"pref": 1
|
||||
},
|
||||
"de": {
|
||||
"language": "de-DE",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 2
|
||||
}
|
||||
},
|
||||
"calendars": {
|
||||
"c": {
|
||||
"@type": "Calendar",
|
||||
"kind": "calendar",
|
||||
"uri": "https://opencloud.eu/calendars/521b032b-a2b3-4540-81b9-3f6bccacaab2",
|
||||
"mediaType": "application/jscontact+json",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "work"
|
||||
}
|
||||
},
|
||||
"schedulingAddresses": {
|
||||
"s": {
|
||||
"@type": "SchedulingAddress",
|
||||
"uri": "mailto:scheduling@opencloud.eu.example.com",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "work"
|
||||
}
|
||||
},
|
||||
"addresses": {
|
||||
"k26": {
|
||||
"@type": "Address",
|
||||
"components": [
|
||||
{"@type": "AddressComponent", "kind": "block", "value": "2-7"},
|
||||
{"kind": "separator", "value": "-"},
|
||||
{"kind": "number", "value": "2"},
|
||||
{"kind": "separator", "value": " "},
|
||||
{"kind": "district", "value": "Marunouchi"},
|
||||
{"kind": "locality", "value": "Chiyoda-ku"},
|
||||
{"kind": "region", "value": "Tokyo"},
|
||||
{"kind": "separator", "value": " "},
|
||||
{"kind": "postcode", "value": "100-8994"}
|
||||
],
|
||||
"isOrdered": true,
|
||||
"defaultSeparator": ", ",
|
||||
"full": "2-7-2 Marunouchi, Chiyoda-ku, Tokyo 100-8994",
|
||||
"countryCode": "JP",
|
||||
"coordinates": "geo:35.6796373,139.7616907",
|
||||
"timeZone": "JST",
|
||||
"contexts": {
|
||||
"delivery": true,
|
||||
"work": true
|
||||
},
|
||||
"pref": 2
|
||||
}
|
||||
},
|
||||
"cryptoKeys": {
|
||||
"k1": {
|
||||
"@type": "CryptoKey",
|
||||
"uri": "https://opencloud.eu.example.com/keys/d550f57c-582c-43cc-8d94-822bded9ab36",
|
||||
"mediaType": "application/pgp-keys",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 1,
|
||||
"label": "keys"
|
||||
}
|
||||
},
|
||||
"directories": {
|
||||
"d1": {
|
||||
"@type": "Directory",
|
||||
"kind": "entry",
|
||||
"uri": "https://opencloud.eu.example.com/addressbook/8c2f0363-af0a-4d16-a9d5-8a9cd885d722",
|
||||
"listAs": 1
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"r1": {
|
||||
"@type": "Link",
|
||||
"kind": "contact",
|
||||
"uri": "mailto:contact@opencloud.eu.example.com",
|
||||
"contexts": {
|
||||
"work": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"m": {
|
||||
"@type": "Media",
|
||||
"kind": "logo",
|
||||
"uri": "https://opencloud.eu.example.com/opencloud.svg",
|
||||
"mediaType": "image/svg+xml",
|
||||
"contexts": {
|
||||
"work": true
|
||||
},
|
||||
"pref": 123,
|
||||
"label": "svg",
|
||||
"blobId": "53feefbabeb146fcbe3e59e91462fa5f"
|
||||
}
|
||||
},
|
||||
"anniversaries": {
|
||||
"birth": {
|
||||
"@type": "Anniversary",
|
||||
"kind": "birth",
|
||||
"date": {
|
||||
"@type": "PartialDate",
|
||||
"year": 2025,
|
||||
"month": 9,
|
||||
"day": 26,
|
||||
"calendarScale": "iso8601"
|
||||
}
|
||||
}
|
||||
},
|
||||
"keywords": {
|
||||
"imaginary": true,
|
||||
"test": true
|
||||
},
|
||||
"notes": {
|
||||
"n1": {
|
||||
"@type": "Note",
|
||||
"note": "This is a note.",
|
||||
"created": "2025-09-25T18:26:14.094725532+02:00",
|
||||
"author": {
|
||||
"@type": "Author",
|
||||
"name": "Test Data",
|
||||
"uri": "https://isbn.example.com/a461f292-6bf1-470e-b08d-f6b4b0223fe3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"personalInfo": {
|
||||
"p1": {
|
||||
"@type": "PersonalInfo",
|
||||
"kind": "expertise",
|
||||
"value": "Clouds",
|
||||
"level": "high",
|
||||
"listAs": 1,
|
||||
"label": "experts"
|
||||
}
|
||||
},
|
||||
"localizations": {
|
||||
"fr": {
|
||||
"personalInfo": {
|
||||
"value": "Nuages"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, ContactCard{
|
||||
Type: ContactCardType,
|
||||
Kind: ContactCardKindGroup,
|
||||
Id: "20fba820-2f8e-432d-94f1-5abbb59d3ed7",
|
||||
AddressBookIds: map[string]bool{
|
||||
"79047052-ae0e-4299-8860-5bff1a139f3d": true,
|
||||
"44eb6105-08c1-458b-895e-4ad1149dfabd": true,
|
||||
},
|
||||
Version: JSContactVersion_1_0,
|
||||
Created: created,
|
||||
Language: "fr-BE",
|
||||
Members: map[string]bool{
|
||||
"314815dd-81c8-4640-aace-6dc83121616d": true,
|
||||
"c528b277-d8cb-45f2-b7df-1aa3df817463": true,
|
||||
"81dea240-c0a4-4929-82e7-79e713a8bbe4": true,
|
||||
},
|
||||
ProdId: "OpenCloud Groupware 1.0",
|
||||
RelatedTo: map[string]Relation{
|
||||
"urn:uid:ca9d2a62-e068-43b6-a470-46506976d505": {
|
||||
Type: RelationType,
|
||||
Relation: map[Relationship]bool{
|
||||
RelationContact: true,
|
||||
},
|
||||
},
|
||||
"urn:uid:72183ec2-b218-4983-9c89-ff117eeb7c5e": {
|
||||
Relation: map[Relationship]bool{
|
||||
RelationEmergency: true,
|
||||
RelationSpouse: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Uid: "1091f2bb-6ae6-4074-bb64-df74071d7033",
|
||||
Updated: updated,
|
||||
Name: &Name{
|
||||
Type: NameType,
|
||||
Components: []NameComponent{
|
||||
{Type: NameComponentType, Value: "OpenCloud", Kind: NameComponentKindSurname},
|
||||
{Value: " ", Kind: NameComponentKindSeparator},
|
||||
{Value: "Team", Kind: NameComponentKindSurname2},
|
||||
},
|
||||
IsOrdered: true,
|
||||
DefaultSeparator: ", ",
|
||||
SortAs: map[string]string{
|
||||
string(NameComponentKindSurname): "OpenCloud Team",
|
||||
},
|
||||
Full: "OpenCloud Team",
|
||||
},
|
||||
Nicknames: map[string]Nickname{
|
||||
"a": {
|
||||
Type: NicknameType,
|
||||
Name: "The Team",
|
||||
Contexts: map[NicknameContext]bool{
|
||||
NicknameContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
},
|
||||
},
|
||||
Organizations: map[string]Organization{
|
||||
"o": {
|
||||
Type: OrganizationType,
|
||||
Name: "OpenCloud GmbH",
|
||||
Units: []OrgUnit{
|
||||
{Type: OrgUnitType, Name: "Marketing", SortAs: "marketing"},
|
||||
{Type: OrgUnitType, Name: "Sales"},
|
||||
{Name: "Operations", SortAs: "ops"},
|
||||
},
|
||||
SortAs: "opencloud",
|
||||
Contexts: map[OrganizationContext]bool{
|
||||
OrganizationContextWork: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
SpeakToAs: &SpeakToAs{
|
||||
Type: SpeakToAsType,
|
||||
GrammaticalGender: GrammaticalGenderInanimate,
|
||||
Pronouns: map[string]Pronouns{
|
||||
"p": {
|
||||
Type: PronounsType,
|
||||
Pronouns: "it",
|
||||
Contexts: map[PronounsContext]bool{
|
||||
PronounsContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
Titles: map[string]Title{
|
||||
"t": {
|
||||
Type: TitleType,
|
||||
Name: "The",
|
||||
Kind: TitleKindTitle,
|
||||
OrganizationId: "o",
|
||||
},
|
||||
},
|
||||
Emails: map[string]EmailAddress{
|
||||
"e": {
|
||||
Type: EmailAddressType,
|
||||
Address: "info@opencloud.eu.example.com",
|
||||
Contexts: map[EmailAddressContext]bool{
|
||||
EmailAddressContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "work",
|
||||
},
|
||||
},
|
||||
OnlineServices: map[string]OnlineService{
|
||||
"s": {
|
||||
Type: OnlineServiceType,
|
||||
Service: "The Misinformation Game",
|
||||
Uri: "https://misinfogame.com/91886aa0-3586-4ade-b9bb-ec031464a251",
|
||||
User: "opencloudeu",
|
||||
Contexts: map[OnlineServiceContext]bool{
|
||||
OnlineServiceContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "imaginary",
|
||||
},
|
||||
},
|
||||
Phones: map[string]Phone{
|
||||
"p": {
|
||||
Type: PhoneType,
|
||||
Number: "+1-804-222-1111",
|
||||
Features: map[PhoneFeature]bool{
|
||||
PhoneFeatureVoice: true,
|
||||
PhoneFeatureText: true,
|
||||
},
|
||||
Contexts: map[PhoneContext]bool{
|
||||
PhoneContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "imaginary",
|
||||
},
|
||||
},
|
||||
PreferredLanguages: map[string]LanguagePref{
|
||||
"wa": {
|
||||
Type: LanguagePrefType,
|
||||
Language: "wa-BE",
|
||||
Contexts: map[LanguagePrefContext]bool{
|
||||
LanguagePrefContextPrivate: true,
|
||||
},
|
||||
Pref: 1,
|
||||
},
|
||||
"de": {
|
||||
Language: "de-DE",
|
||||
Contexts: map[LanguagePrefContext]bool{
|
||||
LanguagePrefContextWork: true,
|
||||
},
|
||||
Pref: 2,
|
||||
},
|
||||
},
|
||||
Calendars: map[string]Calendar{
|
||||
"c": {
|
||||
Type: CalendarType,
|
||||
Kind: CalendarKindCalendar,
|
||||
Uri: "https://opencloud.eu/calendars/521b032b-a2b3-4540-81b9-3f6bccacaab2",
|
||||
MediaType: "application/jscontact+json",
|
||||
Contexts: map[CalendarContext]bool{
|
||||
CalendarContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "work",
|
||||
},
|
||||
},
|
||||
SchedulingAddresses: map[string]SchedulingAddress{
|
||||
"s": {
|
||||
Type: SchedulingAddressType,
|
||||
Uri: "mailto:scheduling@opencloud.eu.example.com",
|
||||
Contexts: map[SchedulingAddressContext]bool{
|
||||
SchedulingAddressContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "work",
|
||||
},
|
||||
},
|
||||
Addresses: map[string]Address{
|
||||
"k26": {
|
||||
Type: AddressType,
|
||||
Components: []AddressComponent{
|
||||
{Type: AddressComponentType, Kind: AddressComponentKindBlock, Value: "2-7"},
|
||||
{Kind: AddressComponentKindSeparator, Value: "-"},
|
||||
{Kind: AddressComponentKindNumber, Value: "2"},
|
||||
{Kind: AddressComponentKindSeparator, Value: " "},
|
||||
{Kind: AddressComponentKindDistrict, Value: "Marunouchi"},
|
||||
{Kind: AddressComponentKindLocality, Value: "Chiyoda-ku"},
|
||||
{Kind: AddressComponentKindRegion, Value: "Tokyo"},
|
||||
{Kind: AddressComponentKindSeparator, Value: " "},
|
||||
{Kind: AddressComponentKindPostcode, Value: "100-8994"},
|
||||
},
|
||||
IsOrdered: true,
|
||||
DefaultSeparator: ", ",
|
||||
Full: "2-7-2 Marunouchi, Chiyoda-ku, Tokyo 100-8994",
|
||||
CountryCode: "JP",
|
||||
Coordinates: "geo:35.6796373,139.7616907",
|
||||
TimeZone: "JST",
|
||||
Contexts: map[AddressContext]bool{
|
||||
AddressContextDelivery: true,
|
||||
AddressContextWork: true,
|
||||
},
|
||||
Pref: 2,
|
||||
},
|
||||
},
|
||||
CryptoKeys: map[string]CryptoKey{
|
||||
"k1": {
|
||||
Type: CryptoKeyType,
|
||||
Uri: "https://opencloud.eu.example.com/keys/d550f57c-582c-43cc-8d94-822bded9ab36",
|
||||
MediaType: "application/pgp-keys",
|
||||
Contexts: map[CryptoKeyContext]bool{
|
||||
CryptoKeyContextWork: true,
|
||||
},
|
||||
Pref: 1,
|
||||
Label: "keys",
|
||||
},
|
||||
},
|
||||
Directories: map[string]Directory{
|
||||
"d1": {
|
||||
Type: DirectoryType,
|
||||
Kind: DirectoryKindEntry,
|
||||
Uri: "https://opencloud.eu.example.com/addressbook/8c2f0363-af0a-4d16-a9d5-8a9cd885d722",
|
||||
ListAs: 1,
|
||||
},
|
||||
},
|
||||
Links: map[string]Link{
|
||||
"r1": {
|
||||
Type: LinkType,
|
||||
Kind: LinkKindContact,
|
||||
Contexts: map[LinkContext]bool{
|
||||
LinkContextWork: true,
|
||||
},
|
||||
Uri: "mailto:contact@opencloud.eu.example.com",
|
||||
},
|
||||
},
|
||||
Media: map[string]Media{
|
||||
"m": {
|
||||
Type: MediaType,
|
||||
Kind: MediaKindLogo,
|
||||
Uri: "https://opencloud.eu.example.com/opencloud.svg",
|
||||
MediaType: "image/svg+xml",
|
||||
Contexts: map[MediaContext]bool{
|
||||
MediaContextWork: true,
|
||||
},
|
||||
Pref: 123,
|
||||
Label: "svg",
|
||||
BlobId: "53feefbabeb146fcbe3e59e91462fa5f",
|
||||
},
|
||||
},
|
||||
Anniversaries: map[string]Anniversary{
|
||||
"birth": {
|
||||
Type: AnniversaryType,
|
||||
Kind: AnniversaryKindBirth,
|
||||
Date: &PartialDate{
|
||||
Type: PartialDateType,
|
||||
Year: 2025,
|
||||
Month: 9,
|
||||
Day: 26,
|
||||
CalendarScale: "iso8601",
|
||||
},
|
||||
},
|
||||
},
|
||||
Keywords: map[string]bool{
|
||||
"imaginary": true,
|
||||
"test": true,
|
||||
},
|
||||
Notes: map[string]Note{
|
||||
"n1": {
|
||||
Type: NoteType,
|
||||
Note: "This is a note.",
|
||||
Created: created,
|
||||
Author: &Author{
|
||||
Type: AuthorType,
|
||||
Name: "Test Data",
|
||||
Uri: "https://isbn.example.com/a461f292-6bf1-470e-b08d-f6b4b0223fe3",
|
||||
},
|
||||
},
|
||||
},
|
||||
PersonalInfo: map[string]PersonalInfo{
|
||||
"p1": {
|
||||
Type: PersonalInfoType,
|
||||
Kind: PersonalInfoKindExpertise,
|
||||
Value: "Clouds",
|
||||
Level: PersonalInfoLevelHigh,
|
||||
ListAs: 1,
|
||||
Label: "experts",
|
||||
},
|
||||
},
|
||||
Localizations: map[string]PatchObject{
|
||||
"fr": {
|
||||
"personalInfo": map[string]any{
|
||||
"value": "Nuages",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) {
|
||||
return req.jmapError(accountId, jerr, sessionState, lang)
|
||||
}
|
||||
|
||||
var body jmap.AddressBooksResponse = addressbooks
|
||||
var body jmap.AddressBookGetResponse = addressbooks
|
||||
return req.respond(accountId, body, sessionState, AddressBookResponseObjectType, state)
|
||||
})
|
||||
}
|
||||
@@ -47,10 +47,14 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
|
||||
return req.jmapError(accountId, jerr, sessionState, lang)
|
||||
}
|
||||
|
||||
if len(addressbooks.NotFound) > 0 {
|
||||
return req.notFound(accountId, sessionState, AddressBookResponseObjectType, state)
|
||||
} else {
|
||||
return req.respond(accountId, addressbooks.AddressBooks[0], sessionState, AddressBookResponseObjectType, state)
|
||||
switch len(addressbooks.List) {
|
||||
case 0:
|
||||
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
|
||||
case 1:
|
||||
return req.respond(accountId, addressbooks.List[0], sessionState, ContactResponseObjectType, state)
|
||||
default:
|
||||
logger.Error().Msgf("found %d addressbooks matching '%s' instead of 1", len(addressbooks.List), addressBookId)
|
||||
return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,10 +46,14 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) {
|
||||
return req.jmapError(accountId, jerr, sessionState, lang)
|
||||
}
|
||||
|
||||
if len(calendars.NotFound) > 0 {
|
||||
return req.notFound(accountId, sessionState, CalendarResponseObjectType, state)
|
||||
} else {
|
||||
return req.respond(accountId, calendars.Calendars[0], sessionState, CalendarResponseObjectType, state)
|
||||
switch len(calendars.List) {
|
||||
case 0:
|
||||
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
|
||||
case 1:
|
||||
return req.respond(accountId, calendars.List[0], sessionState, ContactResponseObjectType, state)
|
||||
default:
|
||||
logger.Error().Msgf("found %d calendars matching '%s' instead of 1", len(calendars.List), calendarId)
|
||||
return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
@@ -27,17 +26,17 @@ var (
|
||||
*/
|
||||
// So we have to settle for this, as only 'updated' and 'created' are supported for now:
|
||||
DefaultContactSort = []jmap.ContactCardComparator{
|
||||
{Property: jscontact.ContactCardPropertyUpdated, IsAscending: true},
|
||||
{Property: jmap.ContactCardPropertyUpdated, IsAscending: true},
|
||||
}
|
||||
|
||||
SupportedContactSortingProperties = []string{
|
||||
jscontact.ContactCardPropertyUpdated,
|
||||
jscontact.ContactCardPropertyCreated,
|
||||
jmap.ContactCardPropertyUpdated,
|
||||
jmap.ContactCardPropertyCreated,
|
||||
}
|
||||
|
||||
ContactSortingPropertyMapping = map[string]string{
|
||||
"surname": string(jscontact.ContactCardPropertyName) + "/surname",
|
||||
"given": string(jscontact.ContactCardPropertyName) + "/given",
|
||||
"surname": string(jmap.ContactCardPropertyName) + "/surname",
|
||||
"given": string(jmap.ContactCardPropertyName) + "/given",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,15 +113,19 @@ func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
|
||||
l = l.Str(UriParamContactId, log.SafeString(contactId))
|
||||
|
||||
logger := log.From(l)
|
||||
contactsById, sessionState, state, lang, jerr := g.jmap.GetContactCardsById(accountId, req.session, req.ctx, logger, req.language(), []string{contactId})
|
||||
contacts, sessionState, state, lang, jerr := g.jmap.GetContactCards(accountId, req.session, req.ctx, logger, req.language(), []string{contactId})
|
||||
if jerr != nil {
|
||||
return req.jmapError(accountId, jerr, sessionState, lang)
|
||||
}
|
||||
|
||||
if contact, ok := contactsById[contactId]; ok {
|
||||
return req.respond(accountId, contact, sessionState, ContactResponseObjectType, state)
|
||||
} else {
|
||||
switch len(contacts.List) {
|
||||
case 0:
|
||||
return req.notFound(accountId, sessionState, ContactResponseObjectType, state)
|
||||
case 1:
|
||||
return req.respond(accountId, contacts.List[0], sessionState, ContactResponseObjectType, state)
|
||||
default:
|
||||
logger.Error().Msgf("found %d contacts matching '%s' instead of 1", len(contacts.List), contactId)
|
||||
return req.errorS(accountId, req.apiError(&ErrorMultipleIdMatches), sessionState)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -141,7 +144,7 @@ func (g *Groupware) GetAllContacts(w http.ResponseWriter, r *http.Request) {
|
||||
if jerr != nil {
|
||||
return req.jmapError(accountId, jerr, sessionState, lang)
|
||||
}
|
||||
var body []jscontact.ContactCard = contacts
|
||||
var body []jmap.ContactCard = contacts.List
|
||||
|
||||
return req.respond(accountId, body, sessionState, ContactResponseObjectType, state)
|
||||
})
|
||||
@@ -195,7 +198,7 @@ func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
|
||||
|
||||
var create jscontact.ContactCard
|
||||
var create jmap.ContactCard
|
||||
err = req.bodydoc(&create, "The contact to create, which may not have its id attribute set")
|
||||
if err != nil {
|
||||
return req.error(accountId, err)
|
||||
|
||||
@@ -35,8 +35,8 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
return req.jmapError(accountId, jerr, sessionState, lang)
|
||||
}
|
||||
|
||||
if len(mailboxes.Mailboxes) == 1 {
|
||||
return req.respond(accountId, mailboxes.Mailboxes[0], sessionState, MailboxResponseObjectType, state)
|
||||
if len(mailboxes.List) == 1 {
|
||||
return req.respond(accountId, mailboxes.List[0], sessionState, MailboxResponseObjectType, state)
|
||||
} else {
|
||||
return req.notFound(accountId, sessionState, MailboxResponseObjectType, state)
|
||||
}
|
||||
|
||||
@@ -24,9 +24,8 @@ func (g *Groupware) GetQuota(w http.ResponseWriter, r *http.Request) {
|
||||
if jerr != nil {
|
||||
return req.jmapError(accountId, jerr, sessionState, lang)
|
||||
}
|
||||
for _, v := range res {
|
||||
body := v.List
|
||||
return req.respond(accountId, body, sessionState, QuotaResponseObjectType, state)
|
||||
for _, quotas := range res {
|
||||
return req.respond(accountId, quotas, sessionState, QuotaResponseObjectType, state)
|
||||
}
|
||||
return req.notFound(accountId, sessionState, QuotaResponseObjectType, state)
|
||||
})
|
||||
|
||||
@@ -208,6 +208,7 @@ const (
|
||||
ErrorCodeInvalidSortSpecification = "INVSSP"
|
||||
ErrorCodeInvalidSortProperty = "INVSPR"
|
||||
ErrorCodeInvalidObjectState = "INVOST"
|
||||
ErrorCodeMultipleIdMatches = "MIDMAT"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -493,6 +494,12 @@ var (
|
||||
Title: "Invalid Object State",
|
||||
Detail: "The request included an object state that does not exist.",
|
||||
}
|
||||
ErrorMultipleIdMatches = GroupwareError{
|
||||
Status: http.StatusConflict,
|
||||
Code: ErrorCodeMultipleIdMatches,
|
||||
Title: "Multiple unique identifier matches",
|
||||
Detail: "A supposedly unique identifier matched multiple objects.",
|
||||
}
|
||||
)
|
||||
|
||||
type ErrorOpt interface {
|
||||
|
||||
Reference in New Issue
Block a user