Files
opencloud/pkg/jmap/client.go
Pascal Bleser 3e8c37a13b groupware: refactoring for pagination and support for multiple query suppliers
* refactor APIs in JMAP and Groupware in order to implement pagination
   across multiple accountIds and multiple suppliers (currently
   implemented using a mock supplier for contacts)

 * requires go 1.26 due to use of self-reflecting generics type
   constraints

 * still missing: query criteria and sorting parameters

 * still missing: multi-accountId support for emails

 * errors are now all just 'error' in the APIs, instead of the
   specialized implementations, and are interpreted dynamically where
   necessary in order to transform them into HTTP responses

 * remove position, anchor, anchorOffset as individual query parameters
   as we now only support a 'next=...' token for subsequent pages
   (except in emails for now), and use jmap.QueryParams instead; those
   tokens have a header character for the format, followed by a JSON
   encoded QueryParams map, all wrapped into base62 to make it clearer
   that it is meant to be an opaque token, and not a parameter clients
   should tinker with or construct themselves

 * introduce QueryParamsSupplier as an interface to provide QueryParams
   for various scenarios (single supplier, multiple supplier, ...) per
   accountId

 * implement multi-supplier template methods slist and squery
2026-06-16 16:51:37 +02:00

130 lines
3.6 KiB
Go

package jmap
import (
"context"
"errors"
"io"
"net/url"
"slices"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/structs"
"github.com/rs/zerolog"
)
type Client struct {
session SessionClient
api ApiClient
blob BlobClient
ws WsClientFactory
sessionEventListeners *eventListeners[SessionEventListener]
wsPushListeners *eventListeners[WsPushListener]
io.Closer
WsPushListener
}
type ApiSupplier interface {
Api() ApiClient
}
type Hooks interface {
OnSessionOutdated(session *Session, newState SessionState)
}
var _ io.Closer = &Client{}
var _ WsPushListener = &Client{}
var _ ApiSupplier = &Client{}
var _ Hooks = &Client{}
func (j *Client) Close() error {
return errors.Join(j.api.Close(), j.session.Close(), j.blob.Close(), j.ws.Close())
}
func (j *Client) Api() ApiClient {
return j.api
}
func NewClient(session SessionClient, api ApiClient, blob BlobClient, ws WsClientFactory) Client {
return Client{
session: session,
api: api,
blob: blob,
ws: ws,
sessionEventListeners: newEventListeners[SessionEventListener](),
wsPushListeners: newEventListeners[WsPushListener](),
}
}
func (j *Client) AddSessionEventListener(listener SessionEventListener) {
j.sessionEventListeners.add(listener)
}
func (j *Client) OnSessionOutdated(session *Session, newSessionState SessionState) {
j.sessionEventListeners.signal(func(listener SessionEventListener) {
listener.OnSessionOutdated(session, newSessionState)
})
}
func (j *Client) OnNotification(username string, stateChange StateChange) {
j.wsPushListeners.signal(func(listener WsPushListener) {
listener.OnNotification(username, stateChange)
})
}
// Retrieve JMAP well-known data from the Stalwart server and create a Session from that.
func (j *Client) FetchSession(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (Session, Error) {
wk, err := j.session.GetSession(ctx, sessionUrl, username, logger)
if err != nil {
return Session{}, err
}
return newSession(wk)
}
func (j *Client) logger(operation string, ctx Context) *log.Logger {
l := ctx.Logger.With().Str(logOperation, operation)
return log.From(l)
}
func (j *Client) loggerParams(operation string, ctx Context, params func(zerolog.Context) zerolog.Context) *log.Logger {
l := ctx.Logger.With().Str(logOperation, operation)
if params != nil {
l = params(l)
}
return log.From(l)
}
func (j *Client) maxCallsCheck(calls int, ctx Context) Error {
if calls > ctx.Session.Capabilities.Core.MaxCallsInRequest {
ctx.Logger.Error().
Int("max-calls-in-request", ctx.Session.Capabilities.Core.MaxCallsInRequest).
Int("calls-in-request", calls).
Msgf("number of calls in request payload (%d) exceeds the allowed maximum (%d)", ctx.Session.Capabilities.Core.MaxCallsInRequest, calls)
return jmapError(errTooManyMethodCalls, JmapErrorTooManyMethodCalls)
}
return nil
}
// Construct a Request from the given list of Invocation objects.
//
// If an issue occurs, then it is logged prior to returning it.
func (j *Client) request(ctx Context, using []JmapNamespace, methodCalls ...Invocation) (Request, Error) {
sanitized := structs.Filter(methodCalls, func(inv Invocation) bool { return inv.Command != "" })
err := j.maxCallsCheck(len(sanitized), ctx)
if err != nil {
return Request{}, err
}
if using == nil {
using = JmapNamespaces
}
if !slices.Contains(using, JmapCore) {
using = slices.Insert(using, 0, JmapCore)
}
return Request{
Using: using,
MethodCalls: sanitized,
CreatedIds: nil,
}, nil
}