mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-06-17 04:18:53 -04:00
* 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
130 lines
3.6 KiB
Go
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
|
|
}
|