Files
opencloud/pkg/jmap/templates.go
Pascal Bleser f144a7cc8b groupware: add addressbook and calendar creation APIs
* add Groupware APIs for creating and deleting addressbooks

 * add Groupware APIs for creating and deleting calendars

 * add JMAP APIs for creating and deleting addressbooks, calendars

 * add JMAP APIs to retrieve Principals

 * fix API tagging

 * move addressbook JMAP APIs into its own file

 * move addressbook Groupware APIs into its own file
2026-04-03 15:43:06 +02:00

346 lines
13 KiB
Go

package jmap
import (
"context"
"fmt"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/rs/zerolog"
)
func getTemplate[GETREQ any, GETRESP any, RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
getCommand Command, getCommandFactory func(string, []string) GETREQ,
mapper func(GETRESP) RESP,
stateMapper func(GETRESP) State,
accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) {
logger = client.logger(name, session, logger)
var zero RESP
cmd, err := client.request(session, logger, using,
invocation(getCommand, getCommandFactory(accountId, ids), "0"),
)
if err != nil {
return zero, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) {
var response GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, "0", &response)
if err != nil {
return zero, "", err
}
return mapper(response), stateMapper(response), nil
})
}
func getTemplateN[GETREQ any, GETRESP any, ITEM any, RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
getCommand Command, getCommandFactory func(string, []string) GETREQ,
itemMapper func(GETRESP) ITEM,
respMapper func(map[string]ITEM) RESP,
stateMapper func(GETRESP) State,
accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) {
logger = client.logger(name, session, logger)
var zero RESP
uniqueAccountIds := structs.Uniq(accountIds)
invocations := make([]Invocation, len(uniqueAccountIds))
for i, accountId := range uniqueAccountIds {
invocations[i] = invocation(getCommand, getCommandFactory(accountId, ids), mcid(accountId, "0"))
}
cmd, err := client.request(session, logger, using, invocations...)
if err != nil {
return zero, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) {
result := map[string]ITEM{}
responses := map[string]GETRESP{}
for _, accountId := range uniqueAccountIds {
var resp GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, mcid(accountId, "0"), &resp)
if err != nil {
return zero, "", err
}
responses[accountId] = resp
result[accountId] = itemMapper(resp)
}
return respMapper(result), squashStateFunc(responses, stateMapper), nil
})
}
func createTemplate[T any, C any, SETREQ any, GETREQ any, SETRESP any, GETRESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace, t ObjectType,
setCommand Command, getCommand Command,
setCommandFactory func(string, map[string]C) SETREQ,
getCommandFactory func(string, string) GETREQ,
createdMapper func(SETRESP) map[string]*T,
notCreatedMapper func(SETRESP) map[string]SetError,
listMapper func(GETRESP) []T,
stateMapper func(SETRESP) State,
accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create C) (*T, SessionState, State, Language, Error) {
logger = client.logger(name, session, logger)
createMap := map[string]C{"c": create}
cmd, err := client.request(session, logger, using,
invocation(setCommand, setCommandFactory(accountId, createMap), "0"),
invocation(getCommand, getCommandFactory(accountId, "#c"), "1"),
)
if err != nil {
return nil, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*T, State, Error) {
var setResponse SETRESP
err = retrieveResponseMatchParameters(logger, body, setCommand, "0", &setResponse)
if err != nil {
return nil, "", err
}
notCreatedMap := notCreatedMapper(setResponse)
setErr, notok := notCreatedMap["c"]
if notok {
logger.Error().Msgf("%T.NotCreated returned an error %v", setResponse, setErr)
return nil, "", setErrorError(setErr, t)
}
createdMap := createdMapper(setResponse)
if created, ok := createdMap["c"]; !ok || created == nil {
berr := fmt.Errorf("failed to find %s in %s response", string(t), string(setCommand))
logger.Error().Err(berr)
return nil, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
}
var getResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, "1", &getResponse)
if err != nil {
return nil, "", err
}
list := listMapper(getResponse)
if len(list) < 1 {
berr := fmt.Errorf("failed to find %s in %s response", string(t), string(getCommand))
logger.Error().Err(berr)
return nil, "", jmapError(berr, JmapErrorInvalidJmapResponsePayload)
}
return &list[0], stateMapper(setResponse), nil
})
}
func deleteTemplate[REQ any, RESP any](client *Client, name string, using []JmapNamespace, //NOSONAR
c Command, commandFactory func(string, []string) REQ,
notDestroyedMapper func(RESP) map[string]SetError,
stateMapper func(RESP) State,
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)
cmd, err := client.request(session, logger, using,
invocation(c, commandFactory(accountId, destroy), "0"),
)
if err != nil {
return nil, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]SetError, State, Error) {
var setResponse RESP
err = retrieveResponseMatchParameters(logger, body, c, "0", &setResponse)
if err != nil {
return nil, "", err
}
return notDestroyedMapper(setResponse), stateMapper(setResponse), nil
})
}
func changesTemplate[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, ITEM any, RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
changesCommand Command, getCommand Command,
changesCommandFactory func() CHANGESREQ,
getCommandFactory func(string, string) GETREQ,
changesMapper func(CHANGESRESP) (State, State, bool, []string),
getMapper func(GETRESP) []ITEM,
respMapper func(State, State, bool, []ITEM, []ITEM, []string) RESP,
stateMapper func(GETRESP) State,
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) {
logger = client.logger(name, session, logger)
var zero RESP
changes := changesCommandFactory()
getCreated := getCommandFactory("/created", "0") //NOSONAR
getUpdated := getCommandFactory("/updated", "0") //NOSONAR
cmd, err := client.request(session, logger, using,
invocation(changesCommand, changes, "0"),
invocation(getCommand, getCreated, "1"),
invocation(getCommand, getUpdated, "2"),
)
if err != nil {
return zero, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) {
var changesResponse CHANGESRESP
err = retrieveResponseMatchParameters(logger, body, changesCommand, "0", &changesResponse)
if err != nil {
return zero, "", err
}
var createdResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, "1", &createdResponse)
if err != nil {
logger.Error().Err(err).Send()
return zero, "", err
}
var updatedResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, "2", &updatedResponse)
if err != nil {
logger.Error().Err(err).Send()
return zero, "", err
}
oldState, newState, hasMoreChanges, destroyed := changesMapper(changesResponse)
created := getMapper(createdResponse)
updated := getMapper(updatedResponse)
result := respMapper(oldState, newState, hasMoreChanges, created, updated, destroyed)
return result, stateMapper(createdResponse), nil
})
}
func updatedTemplate[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, ITEM any, RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
changesCommand Command, getCommand Command,
changesCommandFactory func() CHANGESREQ,
getCommandFactory func(string, string) GETREQ,
changesMapper func(CHANGESRESP) (State, State, bool),
getMapper func(GETRESP) []ITEM,
respMapper func(State, State, bool, []ITEM) RESP,
stateMapper func(GETRESP) State,
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) {
logger = client.logger(name, session, logger)
var zero RESP
changes := changesCommandFactory()
getUpdated := getCommandFactory("/updated", "0") //NOSONAR
cmd, err := client.request(session, logger, using,
invocation(changesCommand, changes, "0"),
invocation(getCommand, getUpdated, "1"),
)
if err != nil {
return zero, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) {
var changesResponse CHANGESRESP
err = retrieveResponseMatchParameters(logger, body, changesCommand, "0", &changesResponse)
if err != nil {
return zero, "", err
}
var updatedResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, "1", &updatedResponse)
if err != nil {
logger.Error().Err(err).Send()
return zero, "", err
}
oldState, newState, hasMoreChanges := changesMapper(changesResponse)
updated := getMapper(updatedResponse)
result := respMapper(oldState, newState, hasMoreChanges, updated)
return result, stateMapper(updatedResponse), nil
})
}
func changesTemplateN[CHANGESREQ any, GETREQ any, CHANGESRESP any, GETRESP any, ITEM any, CHANGESITEM any, RESP any]( //NOSONAR
client *Client, name string, using []JmapNamespace,
accountIds []string, sinceStateMap map[string]State,
changesCommand Command, getCommand Command,
changesCommandFactory func(string, State) CHANGESREQ,
getCommandFactory func(string, string, string) GETREQ,
changesMapper func(CHANGESRESP) (State, State, bool, []string),
getMapper func(GETRESP) []ITEM,
changesItemMapper func(State, State, bool, []ITEM, []ITEM, []string) CHANGESITEM,
respMapper func(map[string]CHANGESITEM) RESP,
stateMapper func(GETRESP) State,
session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (RESP, SessionState, State, Language, Error) {
logger = client.loggerParams(name, session, logger, func(z zerolog.Context) zerolog.Context {
sinceStateLogDict := zerolog.Dict()
for k, v := range sinceStateMap {
sinceStateLogDict.Str(log.SafeString(k), log.SafeString(string(v)))
}
return z.Dict(logSinceState, sinceStateLogDict)
})
var zero RESP
uniqueAccountIds := structs.Uniq(accountIds)
n := len(uniqueAccountIds)
if n < 1 {
return zero, "", "", "", nil
}
invocations := make([]Invocation, n*3)
for i, accountId := range uniqueAccountIds {
sinceState, ok := sinceStateMap[accountId]
if !ok {
sinceState = ""
}
changes := changesCommandFactory(accountId, sinceState)
ref := mcid(accountId, "0")
getCreated := getCommandFactory(accountId, "/created", ref)
getUpdated := getCommandFactory(accountId, "/updated", ref)
invocations[i*3+0] = invocation(changesCommand, changes, ref)
invocations[i*3+1] = invocation(getCommand, getCreated, mcid(accountId, "1"))
invocations[i*3+2] = invocation(getCommand, getUpdated, mcid(accountId, "2"))
}
cmd, err := client.request(session, logger, using, invocations...)
if err != nil {
return zero, "", "", "", err
}
return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) {
changesItemByAccount := make(map[string]CHANGESITEM, n)
stateByAccountId := make(map[string]State, n)
for _, accountId := range uniqueAccountIds {
var changesResponse CHANGESRESP
err = retrieveResponseMatchParameters(logger, body, changesCommand, mcid(accountId, "0"), &changesResponse)
if err != nil {
return zero, "", err
}
var createdResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, mcid(accountId, "1"), &createdResponse)
if err != nil {
return zero, "", err
}
var updatedResponse GETRESP
err = retrieveResponseMatchParameters(logger, body, getCommand, mcid(accountId, "2"), &updatedResponse)
if err != nil {
return zero, "", err
}
oldState, newState, hasMoreChanges, destroyed := changesMapper(changesResponse)
created := getMapper(createdResponse)
updated := getMapper(updatedResponse)
changesItemByAccount[accountId] = changesItemMapper(oldState, newState, hasMoreChanges, created, updated, destroyed)
stateByAccountId[accountId] = stateMapper(createdResponse)
}
return respMapper(changesItemByAccount), squashState(stateByAccountId), nil
})
}