From db0b7e7ee8062c4cff29f899097d6a08ca035e28 Mon Sep 17 00:00:00 2001 From: adbenitez Date: Fri, 27 Oct 2023 07:52:44 +0200 Subject: [PATCH] update vendor folder --- vendor/github.com/creachadair/jrpc2/README.md | 10 +- vendor/github.com/creachadair/jrpc2/base.go | 24 +- .../creachadair/jrpc2/channel/channel.go | 10 +- .../creachadair/jrpc2/channel/hdr.go | 2 +- vendor/github.com/creachadair/jrpc2/client.go | 63 +- .../creachadair/jrpc2/{code => }/code.go | 48 +- vendor/github.com/creachadair/jrpc2/ctx.go | 16 +- vendor/github.com/creachadair/jrpc2/doc.go | 42 +- vendor/github.com/creachadair/jrpc2/error.go | 25 +- vendor/github.com/creachadair/jrpc2/json.go | 29 +- vendor/github.com/creachadair/jrpc2/opts.go | 10 +- vendor/github.com/creachadair/jrpc2/queue.go | 64 - vendor/github.com/creachadair/jrpc2/server.go | 158 ++- .../github.com/creachadair/jrpc2/special.go | 40 - vendor/github.com/creachadair/mds/LICENSE | 26 + .../github.com/creachadair/mds/mlink/list.go | 253 ++++ .../github.com/creachadair/mds/mlink/mlink.go | 19 + .../github.com/creachadair/mds/mlink/queue.go | 65 + .../github.com/creachadair/mds/mlink/ring.go | 186 +++ .../github.com/creachadair/mds/mlink/stack.go | 69 + .../deltabot-cli-go/botcli/botcli.go | 188 ++- .../deltabot-cli-go/botcli/cmd.go | 342 +++-- .../deltabot-cli-go/botcli/errors.go | 7 + .../deltachat/account.go | 444 ------- .../deltachat/acfactory.go | 302 +++-- .../deltachat-rpc-client-go/deltachat/bot.go | 176 ++- .../deltachat-rpc-client-go/deltachat/chat.go | 320 ----- .../deltachat/chatlist.go | 34 - .../deltachat/contact.go | 82 -- .../deltachat/errors.go | 15 - .../deltachat/event.go | 122 ++ .../deltachat/manager.go | 72 -- .../deltachat/message.go | 124 -- .../deltachat/msgsnapshot.go | 109 -- .../deltachat/option/option.go | 45 + .../deltachat/reactions.go | 6 - .../deltachat-rpc-client-go/deltachat/rpc.go | 1141 +++++++++++++---- .../deltachat/timestamp.go | 6 +- .../{rpcbin.go => transport/iobin.go} | 2 +- .../iobin_windows.go} | 2 +- .../deltachat/transport/iotransport.go | 83 ++ .../deltachat/transport/transport.go | 13 + .../deltachat/types.go | 209 +++ .../deltachat-rpc-client-go/deltachat/util.go | 78 ++ .../deltachat/webxdc.go | 10 - vendor/modules.txt | 14 +- 46 files changed, 2967 insertions(+), 2138 deletions(-) rename vendor/github.com/creachadair/jrpc2/{code => }/code.go (66%) delete mode 100644 vendor/github.com/creachadair/jrpc2/queue.go delete mode 100644 vendor/github.com/creachadair/jrpc2/special.go create mode 100644 vendor/github.com/creachadair/mds/LICENSE create mode 100644 vendor/github.com/creachadair/mds/mlink/list.go create mode 100644 vendor/github.com/creachadair/mds/mlink/mlink.go create mode 100644 vendor/github.com/creachadair/mds/mlink/queue.go create mode 100644 vendor/github.com/creachadair/mds/mlink/ring.go create mode 100644 vendor/github.com/creachadair/mds/mlink/stack.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/account.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/chat.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/chatlist.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/contact.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/errors.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/manager.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/message.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/msgsnapshot.go create mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/option/option.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/reactions.go rename vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/{rpcbin.go => transport/iobin.go} (83%) rename vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/{rpcbin_windows.go => transport/iobin_windows.go} (76%) create mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iotransport.go create mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/transport.go create mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/types.go create mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/util.go delete mode 100644 vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/webxdc.go diff --git a/vendor/github.com/creachadair/jrpc2/README.md b/vendor/github.com/creachadair/jrpc2/README.md index 9e05651..3fbcf30 100644 --- a/vendor/github.com/creachadair/jrpc2/README.md +++ b/vendor/github.com/creachadair/jrpc2/README.md @@ -3,16 +3,14 @@ [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=yellow)](https://pkg.go.dev/github.com/creachadair/jrpc2) This repository provides a Go module that implements a [JSON-RPC 2.0][spec] client and server. -There is also a working [example in the Go playground](https://go.dev/play/p/GddvtiynMd5). +There is also a working [example in the Go playground](https://go.dev/play/p/Nhg0smrxsoM). ## Packages -* Package [jrpc2](http://godoc.org/github.com/creachadair/jrpc2) implements the base client and server. +* Package [jrpc2](http://godoc.org/github.com/creachadair/jrpc2) implements the base client and server and standard error codes. * Package [channel](http://godoc.org/github.com/creachadair/jrpc2/channel) defines the communication channel abstraction used by the server & client. -* Package [code](http://godoc.org/github.com/creachadair/jrpc2/code) defines standard error codes as defined by the JSON-RPC 2.0 protocol. - * Package [handler](http://godoc.org/github.com/creachadair/jrpc2/handler) defines support for adapting functions to service methods. * Package [jhttp](http://godoc.org/github.com/creachadair/jrpc2/jhttp) allows clients and servers to use HTTP as a transport. @@ -23,9 +21,9 @@ There is also a working [example in the Go playground](https://go.dev/play/p/Gdd ### Versioning -This module is currently still at v0 and subject to change. To the extent practical, I try to avoid breaking changes to the API, but when I do make a breaking change I will update the minor version. For bug fixes and non-breaking minor changes I update only the patch. Hence, when going from (say) `v0.11.3` to `v0.12.0`, be advised that some API changes may occur. +From v1.0.0 onward, the API of this module is considered stable, and I intend to merge no breaking changes to the API without increasing the major version number. Following the conventions of semantic versioning, the minor version will be used to signify the presence of backward-compatible new features (for example, new methods, options, or types), while the patch version will be reserved for bug fixes, documentation updates, and other changes that do not modify the API surface. -I am planning to commit to a stable v1 at some point. If you have opinions about what that should mean, please feel free to comment on https://github.com/creachadair/jrpc2/issues/46. +Note, however, that this intent is limited to the package APIs as seen by the Go compiler: Changes to the implementation that change observable behaviour in ways not promised by the documentation, e.g., changing performance characteristics or the order of internal operations, are not protected. Breakage that results from reliance on undocumented side-effects of the current implementation are the caller's responsibility. ## Implementation Notes diff --git a/vendor/github.com/creachadair/jrpc2/base.go b/vendor/github.com/creachadair/jrpc2/base.go index 6cb54f5..7d0091b 100644 --- a/vendor/github.com/creachadair/jrpc2/base.go +++ b/vendor/github.com/creachadair/jrpc2/base.go @@ -8,14 +8,13 @@ import ( "encoding/json" "fmt" "strings" - - "github.com/creachadair/jrpc2/code" ) -// An Assigner assigns a Handler to handle the specified method name, or nil if -// no method is available to handle the request. +// An Assigner maps method names to Handler functions. type Assigner interface { - // Assign returns the handler for the named method, or nil. + // Assign returns the handler for the named method, or returns nil to + // indicate that the method is not known. + // // The implementation can obtain the complete request from ctx using the // jrpc2.InboundRequest function. Assign(ctx context.Context, method string) Handler @@ -28,10 +27,11 @@ type Namer interface { Names() []string } -// A Handler function implements a method given a request. The response value -// must be JSON-marshalable or nil. In case of error, the handler can return a -// value of type *jrpc2.Error to control the response code sent back to the -// caller; otherwise the server will wrap the resulting value. +// A Handler function implements a method. The request contains the method +// name, request ID, and parameters sent by the client. The result value must +// be JSON-marshalable or nil. In case of error, the handler can return a value +// of type *jrpc2.Error to control the response code sent back to the caller; +// otherwise the server will wrap the resulting value. // // The context passed to the handler by a *jrpc2.Server includes two special // values that the handler may extract. @@ -155,7 +155,7 @@ func (r *Response) UnmarshalResult(v any) error { return json.Unmarshal(r.result, v) } -// ResultString returns the encoded result message of r as a string. +// ResultString returns the encoded result value of r as a string. // If r has no result, for example if r is an error response, it returns "". func (r *Response) ResultString() string { return string(r.result) } @@ -236,9 +236,9 @@ func isServiceName(s string) bool { // error types. If err is not a context error, it is returned unchanged. func filterError(e *Error) error { switch e.Code { - case code.Cancelled: + case Cancelled: return context.Canceled - case code.DeadlineExceeded: + case DeadlineExceeded: return context.DeadlineExceeded } return e diff --git a/vendor/github.com/creachadair/jrpc2/channel/channel.go b/vendor/github.com/creachadair/jrpc2/channel/channel.go index 964a6e4..aa47583 100644 --- a/vendor/github.com/creachadair/jrpc2/channel/channel.go +++ b/vendor/github.com/creachadair/jrpc2/channel/channel.go @@ -1,10 +1,10 @@ // Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved. -// Package channel defines a basic communications channel. +// Package channel defines a communications channel. // -// A Channel encodes/transmits and decodes/receives data records over an -// unstructured stream, using a configurable framing discipline. This package -// provides some basic framing implementations. +// A Channel encodes/transmits and decodes/receives data records. The types in +// this package support sending and receiving over an unstructured stream using +// a configurable framing discipline. // // # Channels // @@ -42,7 +42,7 @@ import ( // A Channel represents the ability to transmit and receive data records. A // channel does not interpret the contents of a record, but may add and remove -// framing so that records can be embedded in higher-level protocols. +// framing so that records can be embedded in lower-level protocols. // // One sender and one receiver may use a Channel concurrently, but the methods // of a Channel are not otherwise required to be safe for concurrent use. The diff --git a/vendor/github.com/creachadair/jrpc2/channel/hdr.go b/vendor/github.com/creachadair/jrpc2/channel/hdr.go index 9268b6c..660fa30 100644 --- a/vendor/github.com/creachadair/jrpc2/channel/hdr.go +++ b/vendor/github.com/creachadair/jrpc2/channel/hdr.go @@ -12,7 +12,7 @@ import ( "strings" ) -// StrictHeader defines a framing that transmits and receives messages using a +// StrictHeader defines a Framing that transmits and receives messages using a // header prefix similar to HTTP, in which mimeType describes the content type. // // Specifically, each message is sent in the format: diff --git a/vendor/github.com/creachadair/jrpc2/client.go b/vendor/github.com/creachadair/jrpc2/client.go index 804201d..0d0b7f9 100644 --- a/vendor/github.com/creachadair/jrpc2/client.go +++ b/vendor/github.com/creachadair/jrpc2/client.go @@ -11,11 +11,10 @@ import ( "sync" "github.com/creachadair/jrpc2/channel" - "github.com/creachadair/jrpc2/code" ) // A Client is a JSON-RPC 2.0 client. The client sends requests and receives -// responses on a channel.Channel provided by the caller. +// responses on a channel.Channel provided by the constructor. type Client struct { done *sync.WaitGroup // done when the reader is finished at shutdown time @@ -24,8 +23,8 @@ type Client struct { scall func(context.Context, *jmessage) []byte chook func(*Client, *Response) - cbctx context.Context // terminates when the client is closed - cbcancel func() // cancels cbctx + cbctx context.Context // terminates when the client is closed + cbcancel context.CancelFunc // cancels cbctx mu sync.Mutex // protects the fields below ch channel.Channel // channel to the server @@ -83,7 +82,7 @@ func (c *Client) accept(ch receiver) error { c.log("Decoding error: %v", err) } c.mu.Lock() - c.stop(err) + c.stopLocked(err) c.mu.Unlock() return err } @@ -95,16 +94,16 @@ func (c *Client) accept(ch receiver) error { c.mu.Lock() defer c.mu.Unlock() for _, rsp := range in { - c.deliver(rsp) + c.deliverLocked(rsp) } }() return nil } -// handleRequest handles a callback or notification from the server. The +// handleRequestLocked handles a callback or notification from the server. The // caller must hold c.mu. This function does not block for the handler. // Precondition: msg is a request or notification, not a response or error. -func (c *Client) handleRequest(msg *jmessage) { +func (c *Client) handleRequestLocked(msg *jmessage) { if msg.isNotification() { if c.snote == nil { c.log("Discarding notification: %v", msg) @@ -135,14 +134,14 @@ func (c *Client) handleRequest(msg *jmessage) { } } -// For each response, find the request pending on its ID and deliver it. The -// caller must hold c.mu. Unknown response IDs are logged and discarded. As -// we are under the lock, we do not wait for the pending receiver to pick up -// the response; we just drop it in their channel. The channel is buffered so -// we don't need to rendezvous. -func (c *Client) deliver(rsp *jmessage) { +// deliverLocked delivers rsp to the request pending on its ID. The caller +// must hold c.mu. Unknown response IDs are logged and discarded. As we are +// under the lock, we do not wait for the pending receiver to pick up the +// response; we just drop it in their channel. The channel is buffered so we +// don't need to rendezvous. +func (c *Client) deliverLocked(rsp *jmessage) { if rsp.isRequestOrNotification() { - c.handleRequest(rsp) + c.handleRequestLocked(rsp) return } @@ -210,7 +209,7 @@ func (c *Client) send(ctx context.Context, reqs jmessages) ([]*Response, error) // on a closing path. b, err := reqs.toJSON() if err != nil { - return nil, Errorf(code.InternalError, "marshaling request failed: %v", err) + return nil, Errorf(InternalError, "marshaling request failed: %v", err) } var pends []*Response @@ -228,7 +227,7 @@ func (c *Client) send(ctx context.Context, reqs jmessages) ([]*Response, error) if c.err != nil { return nil, c.err } - c.log("Outgoing batch: %s", string(b)) + c.log("Outgoing batch: count=%d, bytes=%d", len(reqs), len(b)) if err := c.ch.Send(b); err != nil { return nil, err } @@ -266,9 +265,9 @@ func (c *Client) waitComplete(pctx context.Context, id string, p *Response) { var jerr *Error if c.err != nil && !isUninteresting(c.err) { - jerr = &Error{Code: code.InternalError, Message: c.err.Error()} + jerr = &Error{Code: InternalError, Message: c.err.Error()} } else if err != nil { - jerr = &Error{Code: code.FromError(err), Message: err.Error()} + jerr = &Error{Code: ErrorCode(err), Message: err.Error()} } p.ch <- &jmessage{ @@ -286,9 +285,9 @@ func (c *Client) waitComplete(pctx context.Context, id string, p *Response) { } } -// Call initiates a single request and blocks until the response returns. -// A successful call reports a nil error and a non-nil response. Errors from -// the server have concrete type *jrpc2.Error. +// Call initiates a single request and blocks until the response returns or ctx +// ends. A successful call reports a nil error and a non-nil response. Errors +// from the server have concrete type *jrpc2.Error. // // rsp, err := c.Call(ctx, method, params) // if e, ok := err.(*jrpc2.Error); ok { @@ -325,8 +324,8 @@ func (c *Client) CallResult(ctx context.Context, method string, params, result a } // Batch initiates a batch of concurrent requests, and blocks until all the -// responses return. The responses are returned in the same order as the -// original specs, omitting notifications. +// responses return or ctx ends. The responses are returned in the same order +// as the original specs, omitting notifications. // // Any error reported by Batch represents an error in encoding or sending the // batch to the server. Errors reported by the server in response to requests @@ -357,7 +356,7 @@ func (c *Client) Batch(ctx context.Context, specs []Spec) ([]*Response, error) { } // A Spec combines a method name and parameter value as part of a Batch. If -// the Notify field is true, the request is sent as a notification. +// the Notify flag is true, the request is sent as a notification. type Spec struct { Method string Params any @@ -365,7 +364,7 @@ type Spec struct { } // Notify transmits a notification to the specified method and parameters. It -// blocks until the notification has been sent. +// blocks until the notification has been sent or ctx ends. func (c *Client) Notify(ctx context.Context, method string, params any) error { req, err := c.note(ctx, method, params) if err != nil { @@ -378,7 +377,7 @@ func (c *Client) Notify(ctx context.Context, method string, params any) error { // Close shuts down the client, terminating any pending in-flight requests. func (c *Client) Close() error { c.mu.Lock() - c.stop(errClientStopped) + c.stopLocked(errClientStopped) c.mu.Unlock() c.done.Wait() @@ -393,10 +392,10 @@ func isUninteresting(err error) bool { return err == io.EOF || channel.IsErrClosing(err) || err == errClientStopped } -// stop closes down the reader for c and records err as its final state. The -// caller must hold c.mu. If multiple callers invoke stop, only the first will -// successfully record its error status. -func (c *Client) stop(err error) { +// stopLocked closes down the reader for c and records err as its final state. +// The caller must hold c.mu. If multiple callers invoke stop, only the first +// will successfully record its error status. +func (c *Client) stopLocked(err error) { if c.ch == nil { return // nothing is running } @@ -427,7 +426,7 @@ func (c *Client) marshalParams(ctx context.Context, method string, params any) ( if fb := firstByte(pbits); fb != '[' && fb != '{' && !isNull(pbits) { // JSON-RPC requires that if parameters are provided at all, they are // an array or an object. - return nil, &Error{Code: code.InvalidRequest, Message: "invalid parameters: array or object required"} + return nil, &Error{Code: InvalidRequest, Message: "invalid parameters: array or object required"} } return pbits, nil } diff --git a/vendor/github.com/creachadair/jrpc2/code/code.go b/vendor/github.com/creachadair/jrpc2/code.go similarity index 66% rename from vendor/github.com/creachadair/jrpc2/code/code.go rename to vendor/github.com/creachadair/jrpc2/code.go index 0973186..dd3aa71 100644 --- a/vendor/github.com/creachadair/jrpc2/code/code.go +++ b/vendor/github.com/creachadair/jrpc2/code.go @@ -1,7 +1,6 @@ // Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved. -// Package code defines error code values used by the jrpc2 package. -package code +package jrpc2 import ( "context" @@ -9,9 +8,9 @@ import ( "fmt" ) -// A Code is an error response code. +// A Code is an error code included in the JSON-RPC error object. // -// Code values from and including -32768 to -32000 are reserved for pre-defined +// Code values from and including -32768 to -32000 are reserved for predefined // JSON-RPC errors. Any code within this range, but not defined explicitly // below is reserved for future use. The remainder of the space is available // for application defined errors. @@ -36,8 +35,8 @@ type ErrCoder interface { // It also satisfies the ErrCoder interface, allowing the code to be recovered. type codeError Code -// Error satisfies the error interface using the registered string for the -// code, if one is defined, or else a placeholder that describes the value. +// Error satisfies the error interface using the built-in string for the code, +// if one is defined, or else a placeholder that describes the value. func (c codeError) Error() string { return Code(c).String() } // ErrCode trivially satisfies the ErrCoder interface. @@ -49,9 +48,9 @@ func (c codeError) Is(err error) bool { return ok && v.ErrCode() == Code(c) } -// Err converts c to an error value, which is nil for code.NoError and -// otherwise an error value whose code is c and whose text is based on the -// registered string for c if one exists. +// Err converts c to an error value, which is nil for NoError and otherwise an +// error value whose code is c and whose text is based on the built-in string +// for c if one exists. func (c Code) Err() error { if c == NoError { return nil @@ -69,7 +68,7 @@ const ( InvalidParams Code = -32602 // [std] Invalid method parameters InternalError Code = -32603 // [std] Internal JSON-RPC error - NoError Code = -32099 // Denotes a nil error (used by FromError) + NoError Code = -32099 // Denotes a nil error (used by ErrorCode) SystemError Code = -32098 // Errors from the operating environment Cancelled Code = -32097 // Request cancelled (context.Canceled) DeadlineExceeded Code = -32096 // Request deadline exceeded (context.DeadlineExceeded) @@ -88,29 +87,14 @@ var stdError = map[Code]string{ DeadlineExceeded: "deadline exceeded", } -// Register adds a new Code value with the specified message string. This -// function will panic if the proposed value is already registered with a -// different string. +// ErrorCode returns a Code to categorize the specified error. // -// Registering a code allows you to control the string returned by the String -// method for the code value you specify. It is not necessary to register a -// code before using it. An unregistered code renders a generic string. -func Register(value int32, message string) Code { - code := Code(value) - if s, ok := stdError[code]; ok && s != message { - panic(fmt.Sprintf("code %d is already registered for %q", code, s)) - } - stdError[code] = message - return code -} - -// FromError returns a Code to categorize the specified error. -// If err == nil, it returns code.NoError. -// If err is (or wraps) an ErrCoder, it returns the reported code value. -// If err is context.Canceled, it returns code.Cancelled. -// If err is context.DeadlineExceeded, it returns code.DeadlineExceeded. -// Otherwise it returns code.SystemError. -func FromError(err error) Code { +// - If err == nil, it returns jrpc2.NoError. +// - If err is (or wraps) an ErrCoder, it returns the reported code value. +// - If err is context.Canceled, it returns jrpc2.Cancelled. +// - If err is context.DeadlineExceeded, it returns jrpc2.DeadlineExceeded. +// - Otherwise it returns jrpc2.SystemError. +func ErrorCode(err error) Code { if err == nil { return NoError } diff --git a/vendor/github.com/creachadair/jrpc2/ctx.go b/vendor/github.com/creachadair/jrpc2/ctx.go index f27d3f8..bac2b35 100644 --- a/vendor/github.com/creachadair/jrpc2/ctx.go +++ b/vendor/github.com/creachadair/jrpc2/ctx.go @@ -6,9 +6,9 @@ import ( "context" ) -// InboundRequest returns the inbound request associated with the given -// context, or nil if ctx does not have an inbound request. The context passed -// to the handler by *jrpc2.Server will include this value. +// InboundRequest returns the inbound request associated with the context +// passed to a Handler, or nil if ctx does not have an inbound request. +// A *jrpc2.Server populates this value for handler contexts. // // This is mainly useful to wrapped server methods that do not have the request // as an explicit parameter; for direct implementations of the Handler type the @@ -23,9 +23,8 @@ func InboundRequest(ctx context.Context) *Request { type inboundRequestKey struct{} -// ServerFromContext returns the server associated with the given context. -// This will be populated on the context passed to request handlers. -// This function is for use by handlers, and will panic for a non-handler context. +// ServerFromContext returns the server associated with the context passed to a +// Handler by a *jrpc2.Server. It will panic for a non-handler context. // // It is safe to retain the server and invoke its methods beyond the lifetime // of the context from which it was extracted; however, a handler must not @@ -36,9 +35,10 @@ func ServerFromContext(ctx context.Context) *Server { return ctx.Value(serverKey type serverKey struct{} // ClientFromContext returns the client associated with the given context. -// This will be populated on the context passed to callback handlers. +// This will be populated on the context passed by a *jrpc2.Client to a +// client-side callback handler. // -// A callback handler must not close the client, as the close will deadlock +// A callback handler MUST NOT close the client, as the close will deadlock // waiting for the callback to return. func ClientFromContext(ctx context.Context) *Client { return ctx.Value(clientKey{}).(*Client) } diff --git a/vendor/github.com/creachadair/jrpc2/doc.go b/vendor/github.com/creachadair/jrpc2/doc.go index 81c27d4..a9db289 100644 --- a/vendor/github.com/creachadair/jrpc2/doc.go +++ b/vendor/github.com/creachadair/jrpc2/doc.go @@ -13,8 +13,8 @@ method handlers. Method handlers are functions with this signature: func(ctx Context.Context, req *jrpc2.Request) (any, error) A server finds the handler for a request by looking up its method name in a -jrpc2.Assigner provided when the server is set up. A Handler can decode the -request parameters using the UnmarshalParams method on the request: +jrpc2.Assigner. A Handler decodes request parameters using the UnmarshalParams +method on the Request: func Handle(ctx context.Context, req *jrpc2.Request) (any, error) { var args ArgType @@ -24,9 +24,8 @@ request parameters using the UnmarshalParams method on the request: return usefulStuffWith(args) } -The handler package makes it easier to use functions that do not have this -exact type signature as handlers, using reflection to wrap them in a Handler -function. For example, suppose we want to export this Add function: +The handler package uses reflection to adapt functions that do not have this +type to handlers. For example, given: // Add returns the sum of a slice of integers. func Add(ctx context.Context, values []int) int { @@ -37,7 +36,7 @@ function. For example, suppose we want to export this Add function: return sum } -To convert Add to a handler, call handler.New, which returns a handler.Func: +call handler.New to convert Add to a handler function: h := handler.New(Add) // h is now a handler.Func that calls Add @@ -55,8 +54,10 @@ Equipped with an Assigner we can now construct a Server: To start the server, we need a channel.Channel. Implementations of the Channel interface handle the framing, transmission, and receipt of JSON messages. The channel package implements some common framing disciplines for byte streams -like pipes and sockets. For this example, we'll use a channel that -communicates over stdin and stdout, with messages delimited by newlines: +like pipes and sockets. + +For this example, we'll use a channel that communicates over stdin and stdout, +with messages delimited by newlines: ch := channel.Line(os.Stdin, os.Stdout) @@ -64,13 +65,13 @@ Now we can start the server: srv.Start(ch) -The Start method does not block. The server runs until the channel closes, or -until it is stopped explicitly by calling srv.Stop(). To wait for the server to -finish, call: +The Start method does not block. A server runs until its channel closes or it +is stopped explicitly by calling srv.Stop(). To wait for the server to finish, +call: err := srv.Wait() -This will report the error that led to the server exiting. The code for this +This reports the error that led to the server exiting. The code for this example is available from tools/examples/adder/adder.go: $ go run tools/examples/adder/adder.go @@ -118,9 +119,9 @@ same order as the Spec values, save that notifications are omitted. To decode the result from a successful response, use its UnmarshalResult method: - var result int - if err := rsp.UnmarshalResult(&result); err != nil { - log.Fatalln("UnmarshalResult:", err) + var result int + if err := rsp.UnmarshalResult(&result); err != nil { + log.Fatalln("UnmarshalResult:", err) } To close a client and discard all its pending work, call cli.Close(). @@ -131,8 +132,9 @@ A JSON-RPC notification is a one-way request: The client sends the request to the server, but the server does not reply. Use the Notify method of a client to send a notification: - note := handler.Obj{"message": "A fire is burning!"} - err := cli.Notify(ctx, "Alert", note) + err := cli.Notify(ctx, "Alert", handler.Obj{ + "message": "A fire is burning!", + }) A notification is complete once it has been sent. Notifications can also be sent as part of a batch request: @@ -161,15 +163,15 @@ number of distinctly-named methods: "Mul": handler.New(Mul), } -Maps may be further combined with the handler.ServiceMap type to allow multiple -services to be exported from the same server: +Maps may be combined with the handler.ServiceMap type to export multiple +services from the same server: func getStatus(context.Context) string { return "all is well" } assigner := handler.ServiceMap{ "Math": mathService, "Status": handler.Map{ - "Get": handler.New(Status), + "Get": handler.New(getStatus), }, } diff --git a/vendor/github.com/creachadair/jrpc2/error.go b/vendor/github.com/creachadair/jrpc2/error.go index 812122c..ce238d7 100644 --- a/vendor/github.com/creachadair/jrpc2/error.go +++ b/vendor/github.com/creachadair/jrpc2/error.go @@ -6,22 +6,21 @@ import ( "encoding/json" "errors" "fmt" - - "github.com/creachadair/jrpc2/code" ) // Error is the concrete type of errors returned from RPC calls. +// It also represents the JSON encoding of the JSON-RPC error object. type Error struct { - Code code.Code `json:"code"` // the machine-readable error code + Code Code `json:"code"` // the machine-readable error code Message string `json:"message,omitempty"` // the human-readable error message Data json.RawMessage `json:"data,omitempty"` // optional ancillary error data } -// Error renders e to a human-readable string for the error interface. +// Error returns a human-readable description of e. func (e Error) Error() string { return fmt.Sprintf("[%d] %s", e.Code, e.Message) } -// ErrCode trivially satisfies the code.ErrCoder interface for an *Error. -func (e Error) ErrCode() code.Code { return e.Code } +// ErrCode trivially satisfies the ErrCoder interface for an *Error. +func (e Error) ErrCode() Code { return e.Code } // WithData marshals v as JSON and constructs a copy of e whose Data field // includes the result. If v == nil or if marshaling v fails, e is returned @@ -44,22 +43,22 @@ var errServerStopped = errors.New("the server has been stopped") var errClientStopped = errors.New("the client has been stopped") // errEmptyMethod is the error reported for an empty request method name. -var errEmptyMethod = &Error{Code: code.InvalidRequest, Message: "empty method name"} +var errEmptyMethod = &Error{Code: InvalidRequest, Message: "empty method name"} // errNoSuchMethod is the error reported for an unknown method name. -var errNoSuchMethod = &Error{Code: code.MethodNotFound, Message: code.MethodNotFound.String()} +var errNoSuchMethod = &Error{Code: MethodNotFound, Message: MethodNotFound.String()} // errDuplicateID is the error reported for a duplicated request ID. -var errDuplicateID = &Error{Code: code.InvalidRequest, Message: "duplicate request ID"} +var errDuplicateID = &Error{Code: InvalidRequest, Message: "duplicate request ID"} // errInvalidRequest is the error reported for an invalid request object or batch. -var errInvalidRequest = &Error{Code: code.ParseError, Message: "invalid request value"} +var errInvalidRequest = &Error{Code: ParseError, Message: "invalid request value"} // errEmptyBatch is the error reported for an empty request batch. -var errEmptyBatch = &Error{Code: code.InvalidRequest, Message: "empty request batch"} +var errEmptyBatch = &Error{Code: InvalidRequest, Message: "empty request batch"} // errInvalidParams is the error reported for invalid request parameters. -var errInvalidParams = &Error{Code: code.InvalidParams, Message: code.InvalidParams.String()} +var errInvalidParams = &Error{Code: InvalidParams, Message: InvalidParams.String()} // errTaskNotExecuted is the internal sentinel error for an unassigned task. var errTaskNotExecuted = new(Error) @@ -70,6 +69,6 @@ var ErrConnClosed = errors.New("client connection is closed") // Errorf returns an error value of concrete type *Error having the specified // code and formatted message string. -func Errorf(code code.Code, msg string, args ...any) *Error { +func Errorf(code Code, msg string, args ...any) *Error { return &Error{Code: code, Message: fmt.Sprintf(msg, args...)} } diff --git a/vendor/github.com/creachadair/jrpc2/json.go b/vendor/github.com/creachadair/jrpc2/json.go index 1dbb8b7..fd17591 100644 --- a/vendor/github.com/creachadair/jrpc2/json.go +++ b/vendor/github.com/creachadair/jrpc2/json.go @@ -5,13 +5,12 @@ package jrpc2 import ( "bytes" "encoding/json" - - "github.com/creachadair/jrpc2/code" ) -// ParseRequests parses a single request or a batch of requests from JSON. -// This function reports an error only if msg is not valid JSON. The caller -// must check the individual results for their validity. +// ParseRequests parses either a single request or a batch of requests from +// JSON. It reports an error only if msg is not valid JSON. The caller must +// check the Error field results to determine whether the individual requests +// are valid. func ParseRequests(msg []byte) ([]*ParsedRequest, error) { var reqs jmessages if err := reqs.parseJSON(msg); err != nil { @@ -151,7 +150,7 @@ func isValidID(v json.RawMessage) bool { // isValidVersion reports whether v is a valid JSON-RPC version string. func isValidVersion(v string) bool { return v == Version } -func (j *jmessage) fail(code code.Code, msg string) { +func (j *jmessage) fail(code Code, msg string) { if j.err == nil { j.err = &Error{Code: code, Message: msg} } @@ -203,7 +202,7 @@ func (j *jmessage) parseJSON(data []byte) error { var obj map[string]json.RawMessage if err := json.Unmarshal(data, &obj); err != nil { - j.fail(code.ParseError, "request is not a JSON object") + j.fail(ParseError, "request is not a JSON object") return j.err } @@ -213,17 +212,17 @@ func (j *jmessage) parseJSON(data []byte) error { switch key { case "jsonrpc": if json.Unmarshal(val, &j.V) != nil { - j.fail(code.ParseError, "invalid version key") + j.fail(ParseError, "invalid version key") } case "id": if isValidID(val) { j.ID = val } else { - j.fail(code.InvalidRequest, "invalid request ID") + j.fail(InvalidRequest, "invalid request ID") } case "method": if json.Unmarshal(val, &j.M) != nil { - j.fail(code.ParseError, "invalid method name") + j.fail(ParseError, "invalid method name") } case "params": // As a special case, reduce "null" to nil in the parameters. @@ -232,11 +231,11 @@ func (j *jmessage) parseJSON(data []byte) error { j.P = val } if fb := firstByte(j.P); fb != 0 && fb != '[' && fb != '{' { - j.fail(code.InvalidRequest, "parameters must be array or object") + j.fail(InvalidRequest, "parameters must be array or object") } case "error": if json.Unmarshal(val, &j.E) != nil { - j.fail(code.ParseError, "invalid error value") + j.fail(ParseError, "invalid error value") } case "result": j.R = val @@ -247,17 +246,17 @@ func (j *jmessage) parseJSON(data []byte) error { // Report an error for an invalid version marker if !isValidVersion(j.V) { - j.fail(code.InvalidRequest, "invalid version marker") + j.fail(InvalidRequest, "invalid version marker") } // Report an error if request/response fields overlap. if j.M != "" && (j.E != nil || j.R != nil) { - j.fail(code.InvalidRequest, "mixed request and reply fields") + j.fail(InvalidRequest, "mixed request and reply fields") } // Report an error for extraneous fields. if j.err == nil && len(extra) != 0 { - j.err = Errorf(code.InvalidRequest, "extra fields in request").WithData(extra) + j.err = Errorf(InvalidRequest, "extra fields in request").WithData(extra) } return nil } diff --git a/vendor/github.com/creachadair/jrpc2/opts.go b/vendor/github.com/creachadair/jrpc2/opts.go index ce7f593..8c4d551 100644 --- a/vendor/github.com/creachadair/jrpc2/opts.go +++ b/vendor/github.com/creachadair/jrpc2/opts.go @@ -9,12 +9,10 @@ import ( "log" "runtime" "time" - - "github.com/creachadair/jrpc2/code" ) // ServerOptions control the behaviour of a server created by NewServer. -// A nil *ServerOptions provides sensible defaults. +// A nil *ServerOptions is valid and provides sensible defaults. // It is safe to share server options among multiple server instances. type ServerOptions struct { // If not nil, send debug text logs here. @@ -90,7 +88,7 @@ func (s *ServerOptions) rpcLog() RPCLogger { } // ClientOptions control the behaviour of a client created by NewClient. -// A nil *ClientOptions provides sensible defaults. +// A nil *ClientOptions is valid and provides sensible defaults. type ClientOptions struct { // If not nil, send debug text logs here. Logger Logger @@ -113,7 +111,7 @@ type ClientOptions struct { // report a system error back to the server describing the error. // // Server callbacks are a non-standard extension of JSON-RPC. - OnCallback func(context.Context, *Request) (any, error) + OnCallback Handler // If set, this function is called when the context for a request terminates. // The function receives the client and the response that was cancelled. @@ -177,7 +175,7 @@ func (c *ClientOptions) handleCallback() func(context.Context, *jmessage) []byte if e, ok := err.(*Error); ok { rsp.E = e } else { - rsp.E = &Error{Code: code.FromError(err), Message: err.Error()} + rsp.E = &Error{Code: ErrorCode(err), Message: err.Error()} } } bits, _ := rsp.toJSON() diff --git a/vendor/github.com/creachadair/jrpc2/queue.go b/vendor/github.com/creachadair/jrpc2/queue.go deleted file mode 100644 index f8a7bfb..0000000 --- a/vendor/github.com/creachadair/jrpc2/queue.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (C) 2021 Michael J. Fromberger. All Rights Reserved. - -package jrpc2 - -type queue struct { - front, back *entry - free *entry - nelts int -} - -func newQueue() *queue { - sentinel := new(entry) - return &queue{front: sentinel, back: sentinel} -} - -func (q *queue) isEmpty() bool { return q.front.link == nil } -func (q *queue) size() int { return q.nelts } -func (q *queue) reset() { q.front.link = nil; q.back = q.front; q.nelts = 0 } - -func (q *queue) alloc(data jmessages) *entry { - if q.free == nil { - return &entry{data: data} - } - out := q.free - q.free = out.link - out.data = data - out.link = nil - return out -} - -func (q *queue) release(e *entry) { - e.link, q.free = q.free, e - e.data = nil -} - -func (q *queue) each(f func(jmessages)) { - for cur := q.front.link; cur != nil; cur = cur.link { - f(cur.data) - } -} - -func (q *queue) push(m jmessages) { - e := q.alloc(m) - q.back.link = e - q.back = e - q.nelts++ -} - -func (q *queue) pop() jmessages { - out := q.front.link - q.front.link = out.link - if out == q.back { - q.back = q.front - } - q.nelts-- - data := out.data - q.release(out) - return data -} - -type entry struct { - data jmessages - link *entry -} diff --git a/vendor/github.com/creachadair/jrpc2/server.go b/vendor/github.com/creachadair/jrpc2/server.go index c237e55..fc5b887 100644 --- a/vendor/github.com/creachadair/jrpc2/server.go +++ b/vendor/github.com/creachadair/jrpc2/server.go @@ -14,10 +14,14 @@ import ( "time" "github.com/creachadair/jrpc2/channel" - "github.com/creachadair/jrpc2/code" + "github.com/creachadair/mds/mlink" "golang.org/x/sync/semaphore" ) +const ( + rpcServerInfo = "rpc.serverInfo" +) + var ( serverMetrics = new(expvar.Map) @@ -49,8 +53,8 @@ func init() { // expvar.Publish or similar. func ServerMetrics() *expvar.Map { return serverMetrics } -// A Server is a JSON-RPC 2.0 server. The server receives requests and sends -// responses on a channel.Channel provided by the caller, and dispatches +// Server implements a JSON-RPC 2.0 server. The server receives requests and +// sends responses on a channel.Channel provided by the caller, and dispatches // requests to user-defined Handlers. type Server struct { wg sync.WaitGroup // ready when workers are done at shutdown time @@ -67,11 +71,11 @@ type Server struct { mu *sync.Mutex // protects the fields below - nbar sync.WaitGroup // notification barrier (see the dispatch method) - err error // error from a previous operation - work chan struct{} // for signaling message availability - inq *queue // inbound requests awaiting processing - ch channel.Channel // the channel to the client + nbar sync.WaitGroup // notification barrier (see the dispatch method) + err error // error from a previous operation + work chan struct{} // for signaling message availability + inq mlink.Queue[jmessages] // inbound requests awaiting processing + ch channel.Channel // the channel to the client // For each request ID currently in-flight, this map carries a cancel // function attached to the context that was sent to the handler. @@ -86,10 +90,9 @@ type Server struct { // NewServer returns a new unstarted server that will dispatch incoming // JSON-RPC requests according to mux. To start serving, call Start. // -// N.B. It is only safe to modify mux after the server has been started if mux -// itself is safe for concurrent use by multiple goroutines. -// -// This function will panic if mux == nil. +// This function will panic if mux == nil. It is not safe to modify mux after +// the server has been started unless mux itself is safe for concurrent use by +// multiple goroutines. func NewServer(mux Assigner, opts *ServerOptions) *Server { if mux == nil { panic("nil assigner") @@ -104,7 +107,6 @@ func NewServer(mux Assigner, opts *ServerOptions) *Server { mu: new(sync.Mutex), start: opts.startTime(), builtin: opts.allowBuiltin(), - inq: newQueue(), used: make(map[string]context.CancelFunc), call: make(map[string]*Response), callID: 1, @@ -112,9 +114,9 @@ func NewServer(mux Assigner, opts *ServerOptions) *Server { return s } -// Start enables processing of requests from c and returns. Start does not -// block while the server runs. This function will panic if the server is -// already running. It returns s to allow chaining with construction. +// Start initiates processing of requests from c and returns. Start does not +// block while the server runs. Start will panic if the server is already +// running. It returns s to allow chaining with construction. func (s *Server) Start(c channel.Channel) *Server { s.mu.Lock() defer s.mu.Unlock() @@ -196,21 +198,21 @@ func (s *Server) signal() { func (s *Server) nextRequest() (func() error, error) { s.mu.Lock() defer s.mu.Unlock() - for s.ch != nil && s.inq.isEmpty() { + for s.ch != nil && s.inq.IsEmpty() { s.mu.Unlock() <-s.work s.mu.Lock() } - if s.ch == nil && s.inq.isEmpty() { + if s.ch == nil && s.inq.IsEmpty() { return nil, s.err } ch := s.ch // capture - next := s.inq.pop() - s.log("Dequeued request batch of length %d (qlen=%d)", len(next), s.inq.size()) + next, _ := s.inq.Pop() + s.log("Dequeued request batch of length %d (qlen=%d)", len(next), s.inq.Len()) // Construct a dispatcher to run the handlers outside the lock. - return s.dispatch(next, ch), nil + return s.dispatchLocked(next, ch), nil } // waitForBarrier blocks until all notification handlers that have been issued @@ -227,18 +229,19 @@ func (s *Server) waitForBarrier(n int) { s.nbar.Add(n) } -// dispatch constructs a function that invokes each of the specified tasks. -// The caller must hold s.mu when calling dispatch, but the returned function -// should be executed outside the lock to wait for the handlers to return. +// dispatchLocked constructs a function that invokes each of the specified +// tasks. The caller must hold s.mu when calling dispatchLocked, but the +// returned function should be executed outside the lock to wait for the +// handlers to return. // -// dispatch blocks until any notification received prior to this batch has -// completed, to ensure that notifications are processed in a partial order +// dispatchLocked blocks until any notification received prior to this batch +// has completed, to ensure that notifications are processed in a partial order // that respects order of receipt. Notifications within a batch are handled // concurrently. -func (s *Server) dispatch(next jmessages, ch sender) func() error { +func (s *Server) dispatchLocked(next jmessages, ch sender) func() error { // Resolve all the task handlers or record errors. start := time.Now() - tasks := s.checkAndAssign(next) + tasks := s.checkAndAssignLocked(next) // Ensure all notifications already issued have completed; see #24. todo, notes := tasks.numToDo() @@ -291,7 +294,7 @@ func (s *Server) deliver(rsps jmessages, ch sender, elapsed time.Duration) error // cancelling its valid predecessor in that ID. for _, rsp := range rsps { if rsp.err == nil { - s.cancel(string(rsp.ID)) + s.cancelLocked(string(rsp.ID)) } } @@ -300,9 +303,9 @@ func (s *Server) deliver(rsps jmessages, ch sender, elapsed time.Duration) error return err } -// checkAndAssign resolves all the task handlers for the given batch, or +// checkAndAssignLocked resolves all the task handlers for the given batch, or // records errors for them as appropriate. The caller must hold s.mu. -func (s *Server) checkAndAssign(next jmessages) tasks { +func (s *Server) checkAndAssignLocked(next jmessages) tasks { var ts tasks var ids []string dup := make(map[string]*task) // :: id ⇒ first task in batch with id @@ -342,7 +345,7 @@ func (s *Server) checkAndAssign(next jmessages) tasks { t.err = errEmptyMethod } else { s.setContext(t, id) - t.m = s.assign(t.ctx, t.hreq.method) + t.m = s.assignLocked(t.ctx, t.hreq.method) if t.m == nil { t.err = errNoSuchMethod.WithData(t.hreq.method) } @@ -471,7 +474,7 @@ func (s *Server) waitCallback(pctx context.Context, id string, p *Response) { p.ch <- &jmessage{ ID: json.RawMessage(id), - E: &Error{Code: code.FromError(err), Message: err.Error()}, + E: &Error{Code: ErrorCode(err), Message: err.Error()}, } } @@ -521,12 +524,13 @@ func (s *Server) pushReq(ctx context.Context, wantID bool, method string, params return rsp, err } -// Stop shuts down the server. It is safe to call this method multiple times or -// from concurrent goroutines; it will only take effect once. +// Stop shuts down the server. All in-progress call handlers are cancelled. It +// is safe to call this method multiple times or from concurrent goroutines; it +// will only take effect once. func (s *Server) Stop() { s.mu.Lock() defer s.mu.Unlock() - s.stop(errServerStopped) + s.stopLocked(errServerStopped) } // ServerStatus describes the status of a stopped server. @@ -553,7 +557,7 @@ func (s ServerStatus) Success() bool { return s.Err == nil } func (s *Server) WaitStatus() ServerStatus { s.wg.Wait() // Postcondition check. - if !s.inq.isEmpty() { + if !s.inq.IsEmpty() { panic("s.inq is not empty at shutdown") } stat := ServerStatus{Err: s.err} @@ -571,10 +575,10 @@ func (s *Server) WaitStatus() ServerStatus { // It is equivalent to s.WaitStatus().Err. func (s *Server) Wait() error { return s.WaitStatus().Err } -// stop shuts down the connection and records err as its final state. The -// caller must hold s.mu. If multiple callers invoke stop, only the first will -// successfully record its error status. -func (s *Server) stop(err error) { +// stopLocked shuts down the connection and records err as its final state. +// The caller must hold s.mu. If multiple callers invoke stop, only the first +// will successfully record its error status. +func (s *Server) stopLocked(err error) { if s.ch == nil { return // nothing is running } @@ -586,19 +590,20 @@ func (s *Server) stop(err error) { // // TODO(@creachadair): We need better tests for this behaviour. var keep jmessages - s.inq.each(func(cur jmessages) { + s.inq.Each(func(cur jmessages) bool { for _, req := range cur { if req.isNotification() { keep = append(keep, req) s.log("Retaining notification %p", req) } else { - s.cancel(string(req.ID)) + s.cancelLocked(string(req.ID)) } } + return true }) - s.inq.reset() + s.inq.Clear() for _, elt := range keep { - s.inq.push(jmessages{elt}) + s.inq.Add(jmessages{elt}) } close(s.work) @@ -641,21 +646,21 @@ func (s *Server) read(ch receiver) { } s.mu.Lock() if err != nil { // receive failure; shut down - s.stop(err) + s.stopLocked(err) s.mu.Unlock() return } else if derr != nil { // parse failure; report and continue - s.pushError(derr) + s.pushErrorLocked(derr) } else if len(in) == 0 { - s.pushError(errEmptyBatch) + s.pushErrorLocked(errEmptyBatch) } else { // Filter out response messages. It's possible that the entire batch // was responses, so re-check the length after doing this. - keep := s.filterBatch(in) + keep := s.filterBatchLocked(in) if len(keep) != 0 { - s.log("Received request batch of size %d (qlen=%d)", len(keep), s.inq.size()) - s.inq.push(keep) - if s.inq.size() == 1 { // the queue was empty + s.log("Received request batch of size %d (qlen=%d)", len(keep), s.inq.Len()) + s.inq.Add(keep) + if s.inq.Len() == 1 { // the queue was empty s.signal() } } @@ -664,10 +669,11 @@ func (s *Server) read(ch receiver) { } } -// filterBatch removes and handles any response messages from next, dispatching -// replies to pending callbacks as required. The remainder is returned. -// The caller must hold s.mu, and must re-check that the result is not empty. -func (s *Server) filterBatch(next jmessages) jmessages { +// filterBatchLocked removes and handles any response messages from next, +// dispatching replies to pending callbacks as required. The remainder is +// returned. The caller must hold s.mu, and must re-check that the result is +// not empty. +func (s *Server) filterBatchLocked(next jmessages) jmessages { keep := make(jmessages, 0, len(next)) for _, req := range next { if req.isRequestOrNotification() { @@ -706,13 +712,15 @@ type ServerInfo struct { StartTime time.Time `json:"startTime,omitempty"` } -// assign returns a Handler to handle the specified name, or nil. -// The caller must hold s.mu. -func (s *Server) assign(ctx context.Context, name string) Handler { +// assignLocked returns a Handler to handle the specified name, or nil. The +// caller must hold s.mu. +func (s *Server) assignLocked(ctx context.Context, name string) Handler { if s.builtin && strings.HasPrefix(name, "rpc.") { switch name { case rpcServerInfo: - return methodFunc(s.handleRPCServerInfo) + return func(context.Context, *Request) (any, error) { + return s.ServerInfo(), nil + } default: return nil // reserved } @@ -720,16 +728,16 @@ func (s *Server) assign(ctx context.Context, name string) Handler { return s.mux.Assign(ctx, name) } -// pushError reports an error for the given request ID directly back to the -// client, bypassing the normal request handling mechanism. The caller must -// hold s.mu when calling this method. -func (s *Server) pushError(err error) { +// pushErrorLocked reports an error for the given request ID directly back to +// the client, bypassing the normal request handling mechanism. The caller +// must hold s.mu when calling this method. +func (s *Server) pushErrorLocked(err error) { s.log("Invalid request: %v", err) var jerr *Error if e, ok := err.(*Error); ok { jerr = e } else { - jerr = &Error{Code: code.FromError(err), Message: err.Error()} + jerr = &Error{Code: ErrorCode(err), Message: err.Error()} } nw, err := encode(s.ch, jmessages{{ @@ -743,10 +751,10 @@ func (s *Server) pushError(err error) { } } -// cancel reports whether id is an active call. If so, it also calls the +// cancelLocked reports whether id is an active call. If so, it also calls the // cancellation function associated with id and removes it from the // reservations. The caller must hold s.mu. -func (s *Server) cancel(id string) bool { +func (s *Server) cancelLocked(id string) bool { cancel, ok := s.used[id] if ok { cancel() @@ -781,7 +789,7 @@ func (ts tasks) responses(rpcLog RPCLogger) jmessages { // // However, parse and validation errors must still be reported, with // an ID of null if the request ID was not resolvable. - if c := code.FromError(task.err); c != code.ParseError && c != code.InvalidRequest { + if c := ErrorCode(task.err); c != ParseError && c != InvalidRequest { continue } } @@ -797,10 +805,10 @@ func (ts tasks) responses(rpcLog RPCLogger) jmessages { rsp.R = task.val } else if e, ok := task.err.(*Error); ok { rsp.E = e - } else if c := code.FromError(task.err); c != code.NoError { + } else if c := ErrorCode(task.err); c != NoError { rsp.E = &Error{Code: c, Message: task.err.Error()} } else { - rsp.E = &Error{Code: code.InternalError, Message: task.err.Error()} + rsp.E = &Error{Code: InternalError, Message: task.err.Error()} } rpcLog.LogResponse(task.ctx, &Response{ id: string(rsp.ID), @@ -825,3 +833,13 @@ func (ts tasks) numToDo() (todo, notes int) { } return } + +// CancelRequest instructs s to cancel the pending or in-flight request with +// the specified ID. If no request exists with that ID, this is a no-op. +func (s *Server) CancelRequest(id string) { + s.mu.Lock() + defer s.mu.Unlock() + if s.cancelLocked(id) { + s.log("Cancelled request %s by client order", id) + } +} diff --git a/vendor/github.com/creachadair/jrpc2/special.go b/vendor/github.com/creachadair/jrpc2/special.go deleted file mode 100644 index abf731c..0000000 --- a/vendor/github.com/creachadair/jrpc2/special.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (C) 2017 Michael J. Fromberger. All Rights Reserved. - -package jrpc2 - -import ( - "context" -) - -const ( - rpcServerInfo = "rpc.serverInfo" -) - -// CancelRequest instructs s to cancel the pending or in-flight request with -// the specified ID. If no request exists with that ID, this is a no-op. -func (s *Server) CancelRequest(id string) { - s.mu.Lock() - defer s.mu.Unlock() - if s.cancel(id) { - s.log("Cancelled request %s by client order", id) - } -} - -// methodFunc is a replication of handler.Func redeclared to avert a cycle. -type methodFunc func(context.Context, *Request) (any, error) - -func (m methodFunc) Handle(ctx context.Context, req *Request) (any, error) { - return m(ctx, req) -} - -// Handle the special rpc.serverInfo method, that requests server vitals. -func (s *Server) handleRPCServerInfo(context.Context, *Request) (any, error) { - return s.ServerInfo(), nil -} - -// RPCServerInfo calls the built-in rpc.serverInfo method exported by servers. -// It is a convenience wrapper for an invocation of cli.CallResult. -func RPCServerInfo(ctx context.Context, cli *Client) (result *ServerInfo, err error) { - err = cli.CallResult(ctx, rpcServerInfo, nil, &result) - return -} diff --git a/vendor/github.com/creachadair/mds/LICENSE b/vendor/github.com/creachadair/mds/LICENSE new file mode 100644 index 0000000..b587dcb --- /dev/null +++ b/vendor/github.com/creachadair/mds/LICENSE @@ -0,0 +1,26 @@ +Copyright (C) 2016-2022, Michael J. Fromberger +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + (1) Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + (2) Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + (3) The name of the author may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY +OF SUCH DAMAGE. diff --git a/vendor/github.com/creachadair/mds/mlink/list.go b/vendor/github.com/creachadair/mds/mlink/list.go new file mode 100644 index 0000000..232586d --- /dev/null +++ b/vendor/github.com/creachadair/mds/mlink/list.go @@ -0,0 +1,253 @@ +package mlink + +// A List is a singly-linked ordered list. A zero value is ready for use. +// +// The methods of a List value do not allow direct modification of the list. +// To insert and update entries in the list, use the At, Find, Last, or End +// methods to obtain a Cursor to a location in the list. A cursor can be used +// to insert, update, and delete elements of the list. +type List[T any] struct { + first entry[T] // sentinel; first.link points to the real first element +} + +// NewList returns a new empty list. +func NewList[T any]() *List[T] { return new(List[T]) } + +// IsEmpty reports whether lst is empty. +func (lst *List[T]) IsEmpty() bool { return lst.first.link == nil } + +// Clear discards all the values in lst, leaving it empty. Calling Clear +// invalidates all cursors to the list. +func (lst *List[T]) Clear() { lst.first.link = nil } + +// Peek reports whether lst has a value at offset n from the front of the list, +// and if so returns its value. +// +// This method takes time proportional to n. Peek will panic if n < 0. +func (lst *List[T]) Peek(n int) (T, bool) { + cur := lst.At(n) + return cur.Get(), !cur.AtEnd() +} + +// Each calls f with each value in lst, in order from first to last. +// If f returns false, Each stops and returns false. +// Otherwise, Each returns true after visiting all elements of lst. +func (lst *List[T]) Each(f func(T) bool) bool { + for cur := lst.cfirst(); !cur.AtEnd(); cur.Next() { + if !f(cur.Get()) { + return false + } + } + return true +} + +// Len reports the number of elements in lst. This method takes time proportional +// to the length of the list. +func (lst *List[T]) Len() int { + var n int + lst.Each(func(T) bool { n++; return true }) + return n +} + +// At returns a cursor to the element at index n ≥ 0 in the list. +// If n is greater than or equal to n.Len(), At returns a cursor to the end of +// the list (equivalent to End). +// +// At will panic if n < 0. +func (lst *List[T]) At(n int) *Cursor[T] { + if n < 0 { + panic("index out of range") + } + + cur := lst.cfirst() + for ; !cur.AtEnd(); cur.Next() { + if n == 0 { + break + } + n-- + } + return &cur +} + +// Last returns a cursor to the last element of the list. If lst is empty, it +// returns a cursor to the end of the list (equivalent to End). +// This method takes time proportional to the length of the list. +func (lst *List[T]) Last() *Cursor[T] { + cur := &lst.first + for cur.link != nil && cur.link.link != nil { + cur = cur.link + } + return &Cursor[T]{pred: cur} +} + +// End returns a cursor to the position just past the end of the list. +// This method takes time proportional to the length of the list. +func (lst *List[T]) End() *Cursor[T] { c := lst.Last(); c.Next(); return c } + +// Find returns a cursor to the first element of the list for which f returns +// true. If no such element is found, the resulting cursor is at the end of the +// list. +func (lst *List[T]) Find(f func(T) bool) *Cursor[T] { + cur := lst.cfirst() + for !cur.AtEnd() { + if f(cur.Get()) { + break + } + cur.Next() + } + return &cur +} + +func (lst *List[T]) cfirst() Cursor[T] { return Cursor[T]{pred: &lst.first} } + +// A Cursor represents a location in a list. A nil *Cursor is not valid, and +// operations on it will panic. +type Cursor[T any] struct { + // pred points to the entry prior to the target, so that the cursor can + // splice an element out of the list. + // + // pred--->[_, link]--->[X, _]-- ... + // ^ pred denotes this value + // + // If pred.link == nil, the cursor indicates the position past the end of + // the list. + pred *entry[T] +} + +// Get returns the value at c's location. If c is at the end of the list, Get +// returns a zero value. +func (c *Cursor[T]) Get() T { + if c.AtEnd() { + var zero T + return zero + } + return c.pred.link.X +} + +// Set replaces the value at c's location. If c is at the end of the list, +// calling Set is equivalent to calling Push. +// +// Before: +// +// [1, 2, 3] +// ^--- c +// +// After c.Set(9) +// +// [1, 9, 3] +// ^--- c +func (c *Cursor[T]) Set(v T) { + if c.AtEnd() { + c.pred.link = &entry[T]{X: v} + // N.B.: c is now no longer AtEnd + } else { + c.pred.link.X = v + } +} + +// AtEnd reports whether c is at the end of its list. +func (c *Cursor[T]) AtEnd() bool { return c.pred.link == nil } + +// Next advances c to the next position in the list if it is not at the end. If +// c was already at the end its position is unchanged. Next returns false if +// the resulting position is at the end of the list, otherwise true. +func (c *Cursor[T]) Next() bool { + if c.AtEnd() { + return false + } + c.pred = c.pred.link + return !c.AtEnd() +} + +// Push inserts a new value into the list at c's location. After insertion, c +// points to the newly-added item and the previous value is now at c.Next(). +// +// Before: +// +// [1, 2, 3] +// ^--- c +// +// After c.Push(4): +// +// [4, 1, 2, 3] +// ^--- c +func (c *Cursor[T]) Push(v T) { + added := &entry[T]{X: v, link: c.pred.link} + c.pred.link = added +} + +// Add inserts one or more new values into the list at c's location. After +// insertion, c points to the original item, now in the location after the +// newly-added values. This is a shorthand for Push followed by Next. +// +// Before: +// +// [1, 2, 3] +// ^--- c +// +// After c.Add(4): +// +// [4, 1, 2, 3] +// ^--- c +func (c *Cursor[T]) Add(vs ...T) { + for _, v := range vs { + c.Push(v) + c.Next() + } +} + +// Remove removes and returns the element at c's location from the list. If c +// is at the end of the list, Remove does nothing and returns a zero value. +// +// After removal, c is still valid and points the element after the one that +// was removed, or the end of the list. +// +// Calling Remove invalidates any cursors to the location immediately after c +// in the original list. +// +// Before: +// +// [1, 2, 3, 4] +// ^--- c +// +// After c.Remove() +// +// [1, 3, 4] +// ^--- c +func (c *Cursor[T]) Remove() T { + if c.AtEnd() { + var zero T + return zero + } + + // Detach the discarded entry from its neighbor so that any cursors pointing + // to that entry will be AtEnd, and changes made through them will not + // affect the remaining list. + out := c.pred.link + c.pred.link, out.link = out.link, nil + + return out.X +} + +// Truncate removes all the elements of the list at and after c's location. +// After calling Truncate, c is at the end of the remaining list. If c is at +// the end of the list, Truncate does nothing. After truncation, c remains +// valid. +// +// Calling Truncate invalidates any cursors to locations after c in the +// original list. +// +// Before: +// +// [1, 2, 3, 4] +// ^--- c +// +// After c.Truncate(): +// +// [1, 2] * +// ^--- c (c.AtEnd() == true) +func (c *Cursor[T]) Truncate() { c.pred.link = nil } + +// Copy returns a copy of c pointing to the same location. Changes to c do not +// affect the copy and vice versa. +func (c *Cursor[T]) Copy() *Cursor[T] { return &Cursor[T]{pred: c.pred} } diff --git a/vendor/github.com/creachadair/mds/mlink/mlink.go b/vendor/github.com/creachadair/mds/mlink/mlink.go new file mode 100644 index 0000000..2717fcc --- /dev/null +++ b/vendor/github.com/creachadair/mds/mlink/mlink.go @@ -0,0 +1,19 @@ +// Package mlink implements basic linked container data structures. +// +// Most types in this package share certain common behaviors: +// +// - A Clear method that discards all the contents of the container. +// - A Peek method that returns an order statistic of the container. +// - An Each method that iterates the container in its natural order. +// - An IsEmpty method that reports whether the container is empty. +// - A Len method that reports the number of elements in the container. +// +// The types defined here are not safe for concurrent use by multiple +// goroutines without external synchronization. +package mlink + +// An entry is a singly-linked value container. +type entry[T any] struct { + X T + link *entry[T] +} diff --git a/vendor/github.com/creachadair/mds/mlink/queue.go b/vendor/github.com/creachadair/mds/mlink/queue.go new file mode 100644 index 0000000..5809856 --- /dev/null +++ b/vendor/github.com/creachadair/mds/mlink/queue.go @@ -0,0 +1,65 @@ +package mlink + +// A Queue is a linked first-in, first out sequence of values. A zero value is +// ready for use. +type Queue[T any] struct { + list List[T] + back Cursor[T] + size int +} + +// NewQueue returns a new empty FIFO queue. +func NewQueue[T any]() *Queue[T] { + q := new(Queue[T]) + q.back = q.list.cfirst() + return q +} + +// Add adds v to the end of q. +func (q *Queue[T]) Add(v T) { + if q.back.pred == nil { + q.back = q.list.cfirst() + } + q.back.Add(v) + q.size++ +} + +// IsEmpty reports whether q is empty. +func (q *Queue[T]) IsEmpty() bool { return q.list.IsEmpty() } + +// Clear discards all the values in q, leaving it empty. +func (q *Queue[T]) Clear() { q.list.Clear(); q.back = q.list.cfirst(); q.size = 0 } + +// Front returns the frontmost (oldest) element of the queue. If the queue is +// empty, it returns a zero value. +func (q *Queue[T]) Front() T { v, _ := q.list.Peek(0); return v } + +// Peek reports whether q has a value at offset n from the front of the queue, +// and if so returns its value. Peek(0) returns the same value as Front. +// +// Peek will panic if n < 0. +func (q *Queue[T]) Peek(n int) (T, bool) { return q.list.Peek(n) } + +// Pop reports whether q is non-empty, and if so removes and returns its +// frontmost (oldest) value. +func (q *Queue[T]) Pop() (T, bool) { + cur := q.list.cfirst() + out := cur.Get() + if cur.AtEnd() { + return out, false + } + cur.Remove() + q.size-- + if q.list.IsEmpty() { + q.back = q.list.cfirst() + } + return out, true +} + +// Each calls f with each value in q, in order from oldest to newest. +// If f returns false, Each stops and returns false. +// Otherwise, Each returns true after visiting all elements of q. +func (q *Queue[T]) Each(f func(T) bool) bool { return q.list.Each(f) } + +// Len reports the number of elements in q. This is a constant-time operation. +func (q *Queue[T]) Len() int { return q.size } diff --git a/vendor/github.com/creachadair/mds/mlink/ring.go b/vendor/github.com/creachadair/mds/mlink/ring.go new file mode 100644 index 0000000..8f5a3dd --- /dev/null +++ b/vendor/github.com/creachadair/mds/mlink/ring.go @@ -0,0 +1,186 @@ +package mlink + +import ( + "fmt" +) + +// A Ring is a doubly-linked circular chain of data items. There is no +// designated beginning or end of a ring; each element is a valid entry point +// for the entire ring. A ring with no elements is represented as nil. +type Ring[T any] struct { + Value T + + prev, next *Ring[T] +} + +func (r *Ring[T]) ptr(p *Ring[T]) string { + if p == nil { + return "-" + } else if p == r { + return "@" + } else { + return "*" + } +} + +func (r *Ring[T]) String() string { + if r == nil { + return "Ring(empty)" + } + return fmt.Sprintf("Ring(%v, %v%v)", r.Value, r.ptr(r.prev), r.ptr(r.next)) +} + +// NewRing constructs a new ring with n zero-valued elements. +// If n ≤ 0, NewRing returns nil. +func NewRing[T any](n int) *Ring[T] { + if n <= 0 { + return nil + } + r := newRing[T]() + for n > 1 { + elt := newRing[T]() + elt.next = r.next + r.next.prev = elt + elt.prev = r + r.next = elt + n-- + } + return r +} + +// RingOf constructs a new ring containing the given elements. +func RingOf[T any](vs ...T) *Ring[T] { + r := NewRing[T](len(vs)) + cur := r + for _, v := range vs { + cur.Value = v + cur = cur.Next() + } + return r +} + +// Join splices ring s into a non-empty ring r. There are two cases: +// +// If r and s belong to different rings, [r1 ... rn] and [s1 ... sm], the +// elements of s are spliced in after r and the resulting ring is: +// +// [r1 s1 ... sm r2 ... rn] +// +// In this case Join returns the ring [r2 ... rn r1 ... sm]. +// +// If r and s belong to the same ring, [r1 r2 ... ri s1 ... sm ... rn], then +// the loop of the ring from r2 ... ri is spliced out of r and the resulting +// ring is: +// +// [r1 s1 ... sm ... rn] +// +// In this case Join returns the ring [r2 ... ri] that was spliced out. This +// may be empty (nil) if there were no elements between r1 and s1. +func (r *Ring[T]) Join(s *Ring[T]) *Ring[T] { + if r == s || r.next == s { + return nil // same ring, nothing to do + } + rnext, sprev := r.next, s.prev + + r.next = s // successor of r is now s + s.prev = r // predecessor of s is now r + sprev.next = rnext // successor of s end is now rnext + rnext.prev = sprev // predecessor of rnext is now s end + return rnext +} + +// Pop detaches r from its ring, leaving it linked only to itself. +// It returns r to permit method chaining. +func (r *Ring[T]) Pop() *Ring[T] { + if r != nil && r.prev != r { + rprev, rnext := r.prev, r.next + rprev.next = r.next + rnext.prev = r.prev + r.prev = r + r.next = r + } + return r +} + +// Next returns the successor of r (which may be r itself). +// This will panic if r == nil. +func (r *Ring[T]) Next() *Ring[T] { return r.next } + +// Prev returns the predecessor of r (which may be r itself). +// This will panic if r == nil. +func (r *Ring[T]) Prev() *Ring[T] { return r.prev } + +// At returns the entry at offset n from r. Negative values of n are +// permitted, and r.At(0) == r. If r == nil or the absolute value of n is +// greater than the length of the ring, At returns nil. +func (r *Ring[T]) At(n int) *Ring[T] { + if r == nil { + return nil + } + + next := (*Ring[T]).Next + if n < 0 { + n = -n + next = (*Ring[T]).Prev + } + + cur := r + for n > 0 { + cur = next(cur) + if cur == r { + return nil + } + n-- + } + return cur +} + +// Peek reports whether the ring has a value at offset n from r, and if so +// returns its value. Negative values of n are permitted. If the absolute value +// of n is greater than the length of the ring, Peek reports a zero value. +func (r *Ring[T]) Peek(n int) (T, bool) { + cur := r.At(n) + if cur == nil { + var zero T + return zero, false + } + return cur.Value, true +} + +// Each calls f with each value in r, in circular order. If f returns false, +// Each stops and returns false. Otherwise, Each returns true after visiting +// all elements of r. +func (r *Ring[T]) Each(f func(v T) bool) bool { + return scan(r, func(cur *Ring[T]) bool { return f(cur.Value) }) +} + +// Len reports the number of elements in r. If r == nil, Len is 0. +// This operation takes time proportional to the size of the ring. +func (r *Ring[T]) Len() int { + if r == nil { + return 0 + } + var n int + scan(r, func(*Ring[T]) bool { n++; return true }) + return n +} + +// IsEmpty reports whether r is the empty ring. +func (r *Ring[T]) IsEmpty() bool { return r == nil } + +func scan[T any](r *Ring[T], f func(*Ring[T]) bool) bool { + if r == nil { + return true + } + + cur := r + for f(cur) { + if cur.next == r { + return true + } + cur = cur.next + } + return false +} + +func newRing[T any]() *Ring[T] { r := new(Ring[T]); r.next = r; r.prev = r; return r } diff --git a/vendor/github.com/creachadair/mds/mlink/stack.go b/vendor/github.com/creachadair/mds/mlink/stack.go new file mode 100644 index 0000000..fc22d23 --- /dev/null +++ b/vendor/github.com/creachadair/mds/mlink/stack.go @@ -0,0 +1,69 @@ +package mlink + +// A Stack is a last-in, first-out sequence of values. +// A zero value is ready for use. +type Stack[T any] struct { + list []T +} + +// NewStack constructs a new empty stack. +func NewStack[T any]() *Stack[T] { return new(Stack[T]) } + +// Push adds an entry for v to the top of s. +func (s *Stack[T]) Push(v T) { s.list = append(s.list, v) } + +// Add is a synonym for Push. +func (s *Stack[T]) Add(v T) { s.list = append(s.list, v) } + +// IsEmpty reports whether s is empty. +func (s *Stack[T]) IsEmpty() bool { return len(s.list) == 0 } + +// Clear discards all the values in s, leaving it empty. +func (s *Stack[T]) Clear() { s.list = s.list[:0] } + +// Top returns the top element of the stack. If the stack is empty, it returns +// a zero value. +func (s *Stack[T]) Top() T { + if len(s.list) == 0 { + var zero T + return zero + } + return s.list[len(s.list)-1] +} + +// Peek reports whether s has value at offset n from the top of the stack, and +// if so returns its value. Peek(0) returns the same value as Top. +// +// Peek will panic if n < 0. +func (s *Stack[T]) Peek(n int) (T, bool) { + if n >= len(s.list) { + var zero T + return zero, false + } + return s.list[len(s.list)-1-n], true +} + +// Pop reports whether s is non-empty, and if so it removes and returns its top +// value. +func (s *Stack[T]) Pop() (T, bool) { + out, ok := s.Peek(0) + if ok { + s.list = s.list[:len(s.list)-1] + } + return out, ok +} + +// Each calls f with each value in s, in order from newest to oldest. +// If f returns false, Each stops and returns false. +// Otherwise, Each returns true after visiting all elements of s. +func (s *Stack[T]) Each(f func(T) bool) bool { + for i := len(s.list) - 1; i >= 0; i-- { + if !f(s.list[i]) { + return false + } + } + return true +} + +// Len reports the number of elements in s. This is a constant-time operation. +func (s *Stack[T]) Len() int { return len(s.list) } diff --git a/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/botcli.go b/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/botcli.go index 801d91e..aadd67a 100644 --- a/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/botcli.go +++ b/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/botcli.go @@ -1,10 +1,13 @@ package botcli import ( + "context" "os" "strconv" "github.com/deltachat/deltachat-rpc-client-go/deltachat" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/transport" "github.com/spf13/cobra" "go.uber.org/zap" ) @@ -15,18 +18,21 @@ type _ParsedCmd struct { } // A function that can be used as callback in OnBotInit(), OnBotStart() and AddCommand(). -type Callback func(bot *deltachat.Bot, cmd *cobra.Command, args []string) +type Callback func(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) // A CLI program, with subcommands that help configuring and running a Delta Chat bot. type BotCli struct { - AppName string - AppDir string - RootCmd *cobra.Command - Logger *zap.SugaredLogger - cmdsMap map[string]Callback - parsedCmd *_ParsedCmd - onInit Callback - onStart Callback + AppName string + // AppDir can be set by the --folder flag in command line + AppDir string + // SelectedAddr can be set by the --account flag in command line, if empty it means "all accounts" + SelectedAddr string + RootCmd *cobra.Command + Logger *zap.SugaredLogger + cmdsMap map[string]Callback + parsedCmd *_ParsedCmd + onInit Callback + onStart Callback } // Create a new BotCli instance. @@ -64,32 +70,46 @@ func (self *BotCli) Start() error { if err != nil { return err } - rpc := deltachat.NewRpcIO() - rpc.AccountsDir = getAccountsDir(self.AppDir) - defer rpc.Stop() - if err := rpc.Start(); err != nil { + + trans := transport.NewIOTransport() + trans.AccountsDir = getAccountsDir(self.AppDir) + rpc := &deltachat.Rpc{Context: context.Background(), Transport: trans} + defer trans.Close() + if err := trans.Open(); err != nil { self.Logger.Panicf("Failed to start RPC server, read https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-server for installation instructions. Error message: %v", err) } - bot := deltachat.NewBotFromAccountManager(&deltachat.AccountManager{Rpc: rpc}) - bot.On(deltachat.EventInfo{}, func(event deltachat.Event) { - self.Logger.Info(event.(deltachat.EventInfo).Msg) + + info, err := rpc.GetSystemInfo() + if err != nil { + self.Logger.Panic(err) + } + self.Logger.Infof("Running deltachat core %v", info["deltachat_core_version"]) + + bot := deltachat.NewBot(rpc) + bot.On(deltachat.EventInfo{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { + self.GetLogger(accId).Info(event.(deltachat.EventInfo).Msg) }) - bot.On(deltachat.EventWarning{}, func(event deltachat.Event) { - self.Logger.Warn(event.(deltachat.EventWarning).Msg) + bot.On(deltachat.EventWarning{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { + self.GetLogger(accId).Warn(event.(deltachat.EventWarning).Msg) }) - bot.On(deltachat.EventError{}, func(event deltachat.Event) { - self.Logger.Error(event.(deltachat.EventError).Msg) + bot.On(deltachat.EventError{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { + self.GetLogger(accId).Error(event.(deltachat.EventError).Msg) }) if self.onInit != nil { - self.onInit(bot, self.parsedCmd.cmd, self.parsedCmd.args) + self.onInit(self, bot, self.parsedCmd.cmd, self.parsedCmd.args) } callback := self.cmdsMap[self.parsedCmd.cmd.Use] - callback(bot, self.parsedCmd.cmd, self.parsedCmd.args) + callback(self, bot, self.parsedCmd.cmd, self.parsedCmd.args) } return nil } +// Get a logger for the given account. +func (self *BotCli) GetLogger(accId deltachat.AccountId) *zap.SugaredLogger { + return self.Logger.With("acc", accId) +} + // Add a subcommand to the CLI. The given callback will be executed when the command is used. func (self *BotCli) AddCommand(cmd *cobra.Command, callback Callback) { if cmd.Run != nil { @@ -105,97 +125,135 @@ func (self *BotCli) AddCommand(cmd *cobra.Command, callback Callback) { // Store a custom program setting in the given bot. The setting is specific to your application. // // The setting is stored using Bot.SetUiConfig() and the key is prefixed with BotCli.AppName. -func (self *BotCli) SetConfig(bot *deltachat.Bot, key, value string) error { - return bot.SetUiConfig(self.AppName+"."+key, value) +func (self *BotCli) SetConfig(bot *deltachat.Bot, accId deltachat.AccountId, key string, value option.Option[string]) error { + return bot.SetUiConfig(accId, self.AppName+"."+key, value) } // Get a custom program setting from the given bot. The setting is specific to your application. // // The setting is retrieved using Bot.GetUiConfig() and the key is prefixed with BotCli.AppName. -func (self *BotCli) GetConfig(bot *deltachat.Bot, key string) (string, error) { - return bot.GetUiConfig(self.AppName + "." + key) +func (self *BotCli) GetConfig(bot *deltachat.Bot, accId deltachat.AccountId, key string) (option.Option[string], error) { + return bot.GetUiConfig(accId, self.AppName+"."+key) } // Get the group of bot administrators. -func (self *BotCli) AdminChat(bot *deltachat.Bot) (*deltachat.Chat, error) { - if !bot.IsConfigured() { - return nil, &BotNotConfiguredErr{} +func (self *BotCli) AdminChat(bot *deltachat.Bot, accId deltachat.AccountId) (deltachat.ChatId, error) { + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + return 0, &BotNotConfiguredErr{} } - value, err := self.GetConfig(bot, "admin-chat") + value, err := self.GetConfig(bot, accId, "admin-chat") if err != nil { - return nil, err + return 0, err } - var chat *deltachat.Chat + var chatId deltachat.ChatId - if value != "" { - chatId, err := strconv.ParseUint(value, 10, 0) + if value.IsSome() { + chatIdInt, err := strconv.ParseUint(value.Unwrap(), 10, 0) if err != nil { - return nil, err + return 0, err } - chat = &deltachat.Chat{Account: bot.Account, Id: deltachat.ChatId(chatId)} - var selfInGroup bool - contacts, err := chat.Contacts() + chatId = deltachat.ChatId(chatIdInt) + selfInGroup, err := bot.Rpc.CanSend(accId, chatId) if err != nil { - return nil, err - } - me := bot.Me() - for _, contact := range contacts { - if me.Id == contact.Id { - selfInGroup = true - break - } + return 0, err } if !selfInGroup { - value = "" + value = option.None[string]() } } - if value == "" { - chat, err = self.ResetAdminChat(bot) + if value.IsNone() { + chatId, err = self.ResetAdminChat(bot, accId) if err != nil { - return nil, err + return 0, err } } - return chat, nil + return chatId, nil } // Reset the group of bot administrators, all the members of the old group are no longer admins. -func (self *BotCli) ResetAdminChat(bot *deltachat.Bot) (*deltachat.Chat, error) { - if !bot.IsConfigured() { - return nil, &BotNotConfiguredErr{} +func (self *BotCli) ResetAdminChat(bot *deltachat.Bot, accId deltachat.AccountId) (deltachat.ChatId, error) { + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + return 0, &BotNotConfiguredErr{} } - chat, err := bot.Account.CreateGroup("Bot Administrators", true) + chatId, err := bot.Rpc.CreateGroupChat(accId, "Bot Administrators", true) if err != nil { - return nil, err + return 0, err } - value := strconv.FormatUint(uint64(chat.Id), 10) - err = self.SetConfig(bot, "admin-chat", value) + value := strconv.FormatUint(uint64(chatId), 10) + err = self.SetConfig(bot, accId, "admin-chat", option.Some(value)) if err != nil { - return nil, err + return 0, err } - return chat, nil + return chatId, nil } // Returns true if contact is in the bot administrators group, false otherwise. -func (self *BotCli) IsAdmin(bot *deltachat.Bot, contact *deltachat.Contact) (bool, error) { - chat, err := self.AdminChat(bot) +func (self *BotCli) IsAdmin(bot *deltachat.Bot, accId deltachat.AccountId, contactId deltachat.ContactId) (bool, error) { + chatId, err := self.AdminChat(bot, accId) if err != nil { return false, err } - contacts, err := chat.Contacts() + contacts, err := bot.Rpc.GetChatContacts(accId, chatId) if err != nil { return false, err } - for _, member := range contacts { - if contact.Id == member.Id { + for _, memberId := range contacts { + if contactId == memberId { return true, nil } } return false, nil } + +// Get account for address, if no account exists create a new one +func (self *BotCli) GetOrCreateAccount(rpc *deltachat.Rpc, addr string) (deltachat.AccountId, error) { + accId, err := self.GetAccount(rpc, addr) + if err != nil { + accId, err = rpc.AddAccount() + if err != nil { + return 0, err + } + rpc.SetConfig(accId, "addr", option.Some(addr)) //nolint:errcheck + } + return accId, nil +} + +// Get account for address, if no account exists with the given address, an error is returned +func (self *BotCli) GetAccount(rpc *deltachat.Rpc, addr string) (deltachat.AccountId, error) { + chatIdInt, err := strconv.ParseUint(addr, 10, 0) + if err == nil { + return deltachat.AccountId(chatIdInt), nil + } + + accounts, _ := rpc.GetAllAccountIds() + for _, accId := range accounts { + addr2, _ := self.GetAddress(rpc, accId) + if addr == addr2 { + return accId, nil + } + } + return 0, &AccountNotFoundErr{Addr: addr} +} + +// Get the address of the given account +func (self *BotCli) GetAddress(rpc *deltachat.Rpc, accId deltachat.AccountId) (string, error) { + var addr option.Option[string] + var err error + isConf, err := rpc.IsConfigured(accId) + if err != nil { + return "", err + } + if isConf { + addr, err = rpc.GetConfig(accId, "configured_addr") + } else { + addr, err = rpc.GetConfig(accId, "addr") + } + return addr.UnwrapOr(""), err +} diff --git a/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/cmd.go b/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/cmd.go index 6726b2b..71bad9a 100644 --- a/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/cmd.go +++ b/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/cmd.go @@ -2,37 +2,52 @@ package botcli import ( "fmt" - "strconv" "strings" "github.com/deltachat/deltachat-rpc-client-go/deltachat" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" "github.com/spf13/cobra" ) func initializeRootCmd(cli *BotCli) { defDir := getDefaultAppDir(cli.AppName) cli.RootCmd.PersistentFlags().StringVarP(&cli.AppDir, "folder", "f", defDir, "program's data folder") + cli.RootCmd.PersistentFlags().StringVarP(&cli.SelectedAddr, "account", "a", "", "operate over this account only when running any subcommand") initCmd := &cobra.Command{ Use: "init", - Short: "initialize the Delta Chat account", + Short: "do initial login configuration of a new Delta Chat account, if the account already exist, the credentials are updated", Args: cobra.ExactArgs(2), } - cli.AddCommand(initCmd, cli.initCallback) + cli.AddCommand(initCmd, initCallback) + + listCmd := &cobra.Command{ + Use: "list", + Short: "show a list of existing bot accounts", + Args: cobra.ExactArgs(0), + } + cli.AddCommand(listCmd, listCallback) + + removeCmd := &cobra.Command{ + Use: "remove", + Short: "remove Delta Chat accounts from the bot", + Args: cobra.ExactArgs(0), + } + cli.AddCommand(removeCmd, removeCallback) configCmd := &cobra.Command{ Use: "config", Short: "set/get account configuration values", Args: cobra.MaximumNArgs(2), } - cli.AddCommand(configCmd, cli.configCallback) + cli.AddCommand(configCmd, configCallback) serveCmd := &cobra.Command{ Use: "serve", Short: "start processing messages", Args: cobra.ExactArgs(0), } - cli.AddCommand(serveCmd, cli.serveCallback) + cli.AddCommand(serveCmd, serveCallback) qrCmd := &cobra.Command{ Use: "qr", @@ -40,7 +55,7 @@ func initializeRootCmd(cli *BotCli) { Args: cobra.ExactArgs(0), } qrCmd.Flags().BoolP("invert", "i", false, "invert QR colors") - cli.AddCommand(qrCmd, cli.qrCallback) + cli.AddCommand(qrCmd, qrCallback) adminCmd := &cobra.Command{ Use: "admin", @@ -49,82 +64,218 @@ func initializeRootCmd(cli *BotCli) { } adminCmd.Flags().BoolP("invert", "i", false, "invert QR colors") adminCmd.Flags().BoolP("reset", "r", false, "reset admin chat, removes all existing admins") - cli.AddCommand(adminCmd, cli.adminCallback) + cli.AddCommand(adminCmd, adminCallback) } -func (self *BotCli) initCallback(bot *deltachat.Bot, cmd *cobra.Command, args []string) { - bot.On(deltachat.EventConfigureProgress{}, func(event deltachat.Event) { +func initCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + bot.On(deltachat.EventConfigureProgress{}, func(bot *deltachat.Bot, accId deltachat.AccountId, event deltachat.Event) { ev := event.(deltachat.EventConfigureProgress) - self.Logger.Infof("Configuration progress: %v", ev.Progress) + addr, _ := cli.GetAddress(bot.Rpc, accId) + if addr == "" { + addr = fmt.Sprintf("account #%v", accId) + } + cli.Logger.Infof("[%v] Configuration progress: %v", addr, ev.Progress) }) + var accId deltachat.AccountId + var err error + if cli.SelectedAddr == "" { // auto-select based on first argument (or create a new one if not found) + accId, err = cli.GetOrCreateAccount(bot.Rpc, args[0]) + } else { // re-configure the selected account + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + if err == nil { + _, err = cli.GetAccount(bot.Rpc, args[0]) + if err == nil { + cli.Logger.Errorf("Configuration failed: an account with address %q already exists", args[0]) + return + } + } + } + if err != nil { + cli.Logger.Errorf("Configuration failed: %v", err) + return + } + go func() { - if err := bot.Configure(args[0], args[1]); err != nil { - self.Logger.Errorf("Configuration failed: %v", err) + if err := bot.Configure(accId, args[0], args[1]); err != nil { + cli.Logger.Errorf("Configuration failed: %v", err) } else { - self.Logger.Info("Account configured successfully.") + cli.Logger.Infof("Account %q configured successfully.", args[0]) } bot.Stop() }() bot.Run() //nolint:errcheck } -func (self *BotCli) configCallback(bot *deltachat.Bot, cmd *cobra.Command, args []string) { - var val string +func configCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { var err error + var accounts []deltachat.AccountId + if cli.SelectedAddr == "" { // set config for all accounts + accounts, err = bot.Rpc.GetAllAccountIds() + } else { + var accId deltachat.AccountId + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + accounts = []deltachat.AccountId{accId} + } + if err != nil { + cli.Logger.Error(err) + return + } + + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) + if err != nil { + cli.Logger.Error(err) + continue + } + fmt.Printf("Account #%v (%v):\n", accId, addr) + configForAcc(cli, bot, cmd, args, accId) + fmt.Println("") + } + + if len(accounts) == 0 { + cli.Logger.Errorf("There are no accounts yet, add a new account using the init subcommand") + } +} + +func configForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId) { if len(args) == 0 { - val, _ := bot.GetConfig("sys.config_keys") - for _, key := range strings.Fields(val) { - val, _ := bot.GetConfig(key) - fmt.Printf("%v=%q\n", key, val) + keys, _ := bot.Rpc.GetConfig(accId, "sys.config_keys") + for _, key := range strings.Fields(keys.Unwrap()) { + val, _ := bot.Rpc.GetConfig(accId, key) + fmt.Printf("%v=%q\n", key, val.UnwrapOr("")) } return } + var val option.Option[string] + var err error if len(args) == 2 { - err = bot.SetConfig(args[0], args[1]) + err = bot.Rpc.SetConfig(accId, args[0], option.Some(args[1])) } if err == nil { - val, err = bot.GetConfig(args[0]) + val, err = bot.Rpc.GetConfig(accId, args[0]) } if err == nil { - fmt.Printf("%v=%v\n", args[0], val) + fmt.Printf("%v=%v\n", args[0], val.UnwrapOr("")) } else { - self.Logger.Error(err) + cli.Logger.Error(err) } } -func (self *BotCli) serveCallback(bot *deltachat.Bot, cmd *cobra.Command, args []string) { - if bot.IsConfigured() { - if self.onStart != nil { - self.onStart(bot, self.parsedCmd.cmd, self.parsedCmd.args) +func serveCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + if cli.SelectedAddr != "" { + cli.Logger.Errorf("operation not supported for a single account, discard the -a/--account option and try again") + return + } + + accounts, err := bot.Rpc.GetAllAccountIds() + if err != nil { + cli.Logger.Error(err) + return + } + var addrs []string + for _, accId := range accounts { + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + cli.Logger.Errorf("account #%v not configured", accId) + } else { + addr, _ := bot.Rpc.GetConfig(accId, "configured_addr") + if addr.UnwrapOr("") != "" { + addrs = append(addrs, addr.Unwrap()) + } + } + } + if len(addrs) != 0 { + cli.Logger.Infof("Listening at: %v", strings.Join(addrs, ", ")) + if cli.onStart != nil { + cli.onStart(cli, bot, cmd, args) } bot.Run() //nolint:errcheck } else { - self.Logger.Error("account not configured") + cli.Logger.Errorf("There are no configured accounts to serve") } } -func (self *BotCli) qrCallback(bot *deltachat.Bot, cmd *cobra.Command, args []string) { - if bot.IsConfigured() { - qrdata, _, err := bot.Account.QrCode() +func qrCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + var err error + var accounts []deltachat.AccountId + if cli.SelectedAddr == "" { // for all accounts + accounts, err = bot.Rpc.GetAllAccountIds() + } else { + var accId deltachat.AccountId + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + accounts = []deltachat.AccountId{accId} + } + if err != nil { + cli.Logger.Error(err) + return + } + + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) + if err != nil { + cli.Logger.Error(err) + continue + } + fmt.Printf("Account #%v (%v):\n", accId, addr) + qrForAcc(cli, bot, cmd, args, accId, addr) + fmt.Println("") + } + + if len(accounts) == 0 { + cli.Logger.Errorf("There are no accounts yet, add a new account using the init subcommand") + } +} + +func qrForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId, addr string) { + if isConf, _ := bot.Rpc.IsConfigured(accId); isConf { + qrdata, _, err := bot.Rpc.GetChatSecurejoinQrCodeSvg(accId, option.None[deltachat.ChatId]()) if err != nil { - self.Logger.Errorf("Failed to generate QR: %v", err) + cli.Logger.Errorf("Failed to generate QR: %v", err) return } - addr, _ := bot.GetConfig("configured_addr") fmt.Println("Scan this QR to verify", addr) invert, _ := cmd.Flags().GetBool("invert") printQr(qrdata, invert) fmt.Println(qrdata) } else { - self.Logger.Error("account not configured") + cli.Logger.Error("account not configured") } } -func (self *BotCli) adminCallback(bot *deltachat.Bot, cmd *cobra.Command, args []string) { - if !bot.IsConfigured() { - self.Logger.Error("account not configured") +func adminCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + var err error + var accounts []deltachat.AccountId + if cli.SelectedAddr == "" { // for all accounts + accounts, err = bot.Rpc.GetAllAccountIds() + if err == nil && len(accounts) == 0 { + cli.Logger.Errorf("There are no accounts yet, add a new account using the init subcommand") + } + } else { + var accId deltachat.AccountId + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + accounts = []deltachat.AccountId{accId} + } + if err != nil { + cli.Logger.Error(err) + return + } + + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) + if err != nil { + cli.Logger.Error(err) + continue + } + fmt.Printf("Account #%v (%v):\n", accId, addr) + adminForAcc(cli, bot, cmd, args, accId) + fmt.Println("") + } +} + +func adminForAcc(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string, accId deltachat.AccountId) { + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + cli.Logger.Error("account not configured") return } @@ -132,67 +283,90 @@ func (self *BotCli) adminCallback(bot *deltachat.Bot, cmd *cobra.Command, args [ reset, err := cmd.Flags().GetBool("reset") if err != nil { - self.Logger.Errorf(errMsg, err) + cli.Logger.Errorf(errMsg, err) return } - var value string - if !reset { - value, err = self.GetConfig(bot, "admin-chat") - if err != nil { - self.Logger.Errorf(errMsg, err) - return - } + var chatId deltachat.ChatId + if reset { + chatId, err = cli.ResetAdminChat(bot, accId) + } else { + chatId, err = cli.AdminChat(bot, accId) + } + if err != nil { + cli.Logger.Errorf(errMsg, err) + return } - var chat *deltachat.Chat + qrdata, _, err := bot.Rpc.GetChatSecurejoinQrCodeSvg(accId, option.Some(chatId)) + if err != nil { + cli.Logger.Errorf(errMsg, err) + return + } - if value != "" { - chatId, err := strconv.ParseUint(value, 10, 0) - if err != nil { - self.Logger.Errorf(errMsg, err) - return - } - chat = &deltachat.Chat{Account: bot.Account, Id: deltachat.ChatId(chatId)} - var selfInGroup bool - contacts, err := chat.Contacts() - if err != nil { - self.Logger.Errorf(errMsg, err) - return - } - me := bot.Me() - for _, contact := range contacts { - if me.Id == contact.Id { - selfInGroup = true - break - } - } - if !selfInGroup { - value = "" - } + fmt.Println("Scan this QR to become bot administrator") + invert, _ := cmd.Flags().GetBool("invert") + printQr(qrdata, invert) + fmt.Println(qrdata) +} + +func listCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + if cli.SelectedAddr != "" { + cli.Logger.Errorf("operation not supported for a single account, discard the -a/--account option and try again") + return } - if value == "" { - chat, err = bot.Account.CreateGroup("Bot Administrators", true) + accounts, err := bot.Rpc.GetAllAccountIds() + if err != nil { + cli.Logger.Error(err) + return + } + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) if err != nil { - self.Logger.Errorf(errMsg, err) - return + cli.Logger.Error(err) + continue } - value = strconv.FormatUint(uint64(chat.Id), 10) - err = self.SetConfig(bot, "admin-chat", value) - if err != nil { - self.Logger.Errorf(errMsg, err) - return + + if isConf, _ := bot.Rpc.IsConfigured(accId); !isConf { + addr = addr + " (not configured)" } + fmt.Printf("#%v - %v\n", accId, addr) } +} - qrdata, _, err := chat.QrCode() +func removeCallback(cli *BotCli, bot *deltachat.Bot, cmd *cobra.Command, args []string) { + var err error + var accounts []deltachat.AccountId + if cli.SelectedAddr == "" { // for all accounts + accounts, err = bot.Rpc.GetAllAccountIds() + if err == nil && len(accounts) == 0 { + cli.Logger.Errorf("There are no accounts yet, add a new account using the init subcommand") + } + } else { + var accId deltachat.AccountId + accId, err = cli.GetAccount(bot.Rpc, cli.SelectedAddr) + accounts = []deltachat.AccountId{accId} + } if err != nil { - self.Logger.Errorf(errMsg, err) + cli.Logger.Error(err) return } - fmt.Println("Scan this QR to become bot administrator") - invert, _ := cmd.Flags().GetBool("invert") - printQr(qrdata, invert) - fmt.Println(qrdata) + if len(accounts) > 1 { + cli.Logger.Error("There are more than one account, to remove one of them, pass the account address with -a/--account option") + return + } + + for _, accId := range accounts { + addr, err := cli.GetAddress(bot.Rpc, accId) + if err != nil { + cli.Logger.Error(err) + } + err = bot.Rpc.RemoveAccount(accId) + if err != nil { + cli.Logger.Error(err) + } else { + cli.Logger.Infof("Account #%v (%q) removed successfully.", accId, addr) + } + } } diff --git a/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/errors.go b/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/errors.go index ff29de1..ad66ad6 100644 --- a/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/errors.go +++ b/vendor/github.com/deltachat-bot/deltabot-cli-go/botcli/errors.go @@ -6,3 +6,10 @@ type BotNotConfiguredErr struct{} func (self *BotNotConfiguredErr) Error() string { return "bot account not configured" } + +// The account was not found. +type AccountNotFoundErr struct{ Addr string } + +func (self *AccountNotFoundErr) Error() string { + return "account not found: " + self.Addr +} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/account.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/account.go deleted file mode 100644 index abfac78..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/account.go +++ /dev/null @@ -1,444 +0,0 @@ -package deltachat - -import ( - "fmt" - "sort" -) - -type AccountId uint64 - -// Delta Chat account. Account instances are usually obtained from an AccountManager. -type Account struct { - Manager *AccountManager - Id AccountId -} - -// Implement Stringer. -func (self *Account) String() string { - return fmt.Sprintf("Account(Id=%v)", self.Id) -} - -// Get this account's event channel. -func (self *Account) GetEventChannel() <-chan Event { - return self.rpc().GetEventChannel(self.Id) -} - -// Remove the account. -func (self *Account) Remove() error { - return self.rpc().Call("remove_account", self.Id) -} - -// Select the account. The selected account will be returned by AccountManager.SelectedAccount() -func (self *Account) Select() error { - return self.rpc().Call("select_account", self.Id) -} - -// Start the account I/O. -func (self *Account) StartIO() error { - return self.rpc().Call("start_io", self.Id) -} - -// Stop the account I/O. -func (self *Account) StopIO() error { - return self.rpc().Call("stop_io", self.Id) -} - -// Get the current connectivity, i.e. whether the device is connected to the IMAP server. -// One of: -// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot -// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot -// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel -// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot -func (self *Account) Connectivity() (uint, error) { - var info uint - err := self.rpc().CallResult(&info, "get_connectivity", self.Id) - return info, err -} - -// Get an overview of the current connectivity, and possibly more statistics. -// Meant to give the user more insight about the current status than -// the basic connectivity info returned by get_connectivity(); show this -// e.g., if the user taps on said basic connectivity info. -// -// If this page changes, an EventConnectivityChanged will be emitted. -// -// This comes as an HTML from the core so that we can easily improve it -// and the improvement instantly reaches all UIs. -// -// If the account is not started (ex. by calling Account.StartIO()) an error -// will be returned. -func (self *Account) ConnectivityHtml() (string, error) { - var html string - err := self.rpc().CallResult(&html, "get_connectivity_html", self.Id) - return html, err -} - -// Return map of this account configuration parameters. -func (self *Account) Info() (map[string]string, error) { - var info map[string]string - err := self.rpc().CallResult(&info, "get_info", self.Id) - return info, err -} - -// Get the combined filesize of an account in bytes. -func (self *Account) Size() (int, error) { - var size int - err := self.rpc().CallResult(&size, "get_account_file_size", self.Id) - return size, err -} - -// Return true if this account is configured, false otherwise. -func (self *Account) IsConfigured() (bool, error) { - var configured bool - err := self.rpc().CallResult(&configured, "is_configured", self.Id) - return configured, err -} - -// Set custom UI-specific configuration value. -// This is useful for custom 3rd party settings set by Delta Chat clients and bot programs. -func (self *Account) SetUiConfig(key string, value string) error { - return self.rpc().Call("set_config", self.Id, "ui."+key, value) -} - -// Get custom UI-specific configuration value set with SetUiConfig(). -func (self *Account) GetUiConfig(key string) (string, error) { - var value string - err := self.rpc().CallResult(&value, "get_config", self.Id, "ui."+key) - return value, err -} - -// Set configuration value. -func (self *Account) SetConfig(key string, value string) error { - return self.rpc().Call("set_config", self.Id, key, value) -} - -// Get configuration value. -func (self *Account) GetConfig(key string) (string, error) { - var value string - err := self.rpc().CallResult(&value, "get_config", self.Id, key) - return value, err -} - -// Tweak several configuration values in a batch. -func (self *Account) UpdateConfig(config map[string]string) error { - return self.rpc().Call("batch_set_config", self.Id, config) -} - -// Set self avatar. Passing nil will discard the currently set avatar. -func (self *Account) SetAvatar(path string) error { - return self.SetConfig("selfavatar", path) -} - -// Get self avatar path. -func (self *Account) Avatar() (string, error) { - return self.GetConfig("selfavatar") -} - -// Configure an account. -func (self *Account) Configure() error { - return self.rpc().Call("configure", self.Id) -} - -// Create a new Contact or return an existing one. -// If there already is a Contact with that e-mail address, it is unblocked and its display -// name is updated if specified. -func (self *Account) CreateContact(addr string, name string) (*Contact, error) { - var id ContactId - err := self.rpc().CallResult(&id, "create_contact", self.Id, addr, name) - return &Contact{self, id}, err -} - -// Check if an e-mail address belongs to a known and unblocked contact. -func (self *Account) GetContactByAddr(addr string) (*Contact, error) { - var id ContactId - err := self.rpc().CallResult(&id, "lookup_contact_id_by_addr", self.Id, addr) - if id > 0 { - return &Contact{self, id}, err - } - return nil, err -} - -// Return a list with snapshots of all blocked contacts. -func (self *Account) BlockedContacts() ([]ContactSnapshot, error) { - var contacts []ContactSnapshot - err := self.rpc().CallResult(&contacts, "get_blocked_contacts", self.Id) - return contacts, err -} - -// Get the contacts list. -func (self *Account) Contacts() ([]*Contact, error) { - return self.QueryContacts("", 0) -} - -// Get the list of contacts matching the given query. -func (self *Account) QueryContacts(query string, listFlags uint) ([]*Contact, error) { - var ids []ContactId - err := self.rpc().CallResult(&ids, "get_contact_ids", self.Id, listFlags, query) - var contacts []*Contact - if err != nil { - return contacts, err - } - contacts = make([]*Contact, len(ids)) - for i := range ids { - contacts[i] = &Contact{self, ids[i]} - } - return contacts, err -} - -// This account's identity as a Contact. -func (self *Account) Me() *Contact { - return &Contact{self, ContactSelf} -} - -// Create a new group chat. -// After creation, the group has only self-contact as member and is in unpromoted state. -func (self *Account) CreateGroup(name string, protected bool) (*Chat, error) { - var id ChatId - err := self.rpc().CallResult(&id, "create_group_chat", self.Id, name, protected) - if err != nil { - return nil, err - } - return &Chat{self, id}, err -} - -// Create a new broadcast list. -func (self *Account) CreateBroadcastList() (*Chat, error) { - var id ChatId - err := self.rpc().CallResult(&id, "create_broadcast_list", self.Id) - if err != nil { - return nil, err - } - return &Chat{self, id}, err -} - -// Continue a Setup-Contact or Verified-Group-Invite protocol started on another device. -func (self *Account) SecureJoin(qrdata string) (*Chat, error) { - var id ChatId - err := self.rpc().CallResult(&id, "secure_join", self.Id, qrdata) - return &Chat{self, id}, err -} - -// Get Setup-Contact QR Code text and SVG data. -func (self *Account) QrCode() (string, string, error) { - var data [2]string - err := self.rpc().CallResult(&data, "get_chat_securejoin_qr_code_svg", self.Id, nil) - return data[0], data[1], err -} - -// Export public and private keys to the specified directory. -// Note that the account does not have to be started. -func (self *Account) ExportSelfKeys(dir string) error { - return self.rpc().Call("export_self_keys", self.Id, dir, nil) -} - -// Import private keys found in the specified directory. -func (self *Account) ImportSelfKeys(dir string) error { - return self.rpc().Call("import_self_keys", self.Id, dir, nil) -} - -// Export account backup. -func (self *Account) ExportBackup(dir, passphrase string) error { - var data any - if passphrase == "" { - data = nil - } else { - data = passphrase - } - return self.rpc().Call("export_backup", self.Id, dir, data) -} - -// Import account backup. -func (self *Account) ImportBackup(file, passphrase string) error { - var data any - if passphrase == "" { - data = nil - } else { - data = passphrase - } - return self.rpc().Call("import_backup", self.Id, file, data) -} - -// Offers a backup for remote devices to retrieve. -// Can be cancelled by stopping the ongoing process. Success or failure can be tracked -// via the `ImexProgress` event which should either reach `1000` for success or `0` for -// failure. -// -// This **stops IO** while it is running. -// -// Returns once a remote device has retrieved the backup, or is cancelled. -func (self *Account) ProvideBackup() error { - return self.rpc().Call("provide_backup", self.Id) -} - -// Returns the text of the QR code for the running ProvideBackup() call. -// -// This QR code text can be used in GetBackup() on a second device to -// retrieve the backup and setup this second device. -// -// This call will fail if there is currently no concurrent call to -// ProvideBackup(). This call may block if the QR code is not yet -// ready. -func (self *Account) GetBackupQr() (string, error) { - var result string - err := self.rpc().CallResult(&result, "get_backup_qr", self.Id) - return result, err -} - -// Returns the rendered QR code for the running ProvideBackup() call. -// -// This QR code can be used in GetBackup() on a second device to -// retrieve the backup and setup this second device. -// -// This call will fail if there is currently no concurrent call to -// ProvideBackup(). This call may block if the QR code is not yet -// ready. -// -// Returns the QR code rendered as an SVG image. -func (self *Account) GetBackupQrSvg() (string, error) { - var result string - err := self.rpc().CallResult(&result, "get_backup_qr_svg", self.Id) - return result, err -} - -// Gets a backup from a remote provider. -// -// This retrieves the backup from a remote device over the network and imports it into -// the current device. -// -// Can be cancelled by stopping the ongoing process. -func (self *Account) GetBackup(qrText string) error { - return self.rpc().Call("get_backup", self.Id, qrText) -} - -// Start the AutoCrypt key transfer process. -func (self *Account) InitiateAutocryptKeyTransfer() (string, error) { - var result string - err := self.rpc().CallResult(&result, "initiate_autocrypt_key_transfer", self.Id) - return result, err -} - -// Mark the given set of messages as seen. -func (self *Account) MarkSeenMsgs(messages []*Message) error { - ids := make([]MsgId, len(messages)) - for i := range messages { - ids[i] = messages[i].Id - } - return self.rpc().Call("markseen_msgs", self.Id, ids) -} - -// Delete the given set of messages (local and remote). -func (self *Account) DeleteMsgs(messages []*Message) error { - ids := make([]MsgId, len(messages)) - for i := range messages { - ids[i] = messages[i].Id - } - return self.rpc().Call("delete_messages", self.Id, ids) -} - -// Return the list of fresh messages, newest messages first. -// This call is intended for displaying notifications. -func (self *Account) FreshMsgs() ([]*Message, error) { - var msgs []*Message - var ids []MsgId - err := self.rpc().CallResult(&ids, "get_fresh_msgs", self.Id) - if err != nil { - return msgs, err - } - msgs = make([]*Message, len(ids)) - for i := range ids { - msgs[i] = &Message{self, ids[i]} - } - return msgs, nil -} - -// Return fresh messages list sorted in the order of their arrival, with ascending IDs. -func (self *Account) FreshMsgsInArrivalOrder() ([]*Message, error) { - var msgs []*Message - var ids []MsgId - err := self.rpc().CallResult(&ids, "get_fresh_msgs", self.Id) - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) - if err != nil { - return msgs, err - } - msgs = make([]*Message, len(ids)) - for i := range ids { - msgs[i] = &Message{self, ids[i]} - } - return msgs, nil -} - -// Global search for messages matching the given query. -func (self *Account) SearchMessages(query string) ([]*MsgSearchResult, error) { - return (&Chat{self, 0}).SearchMessages(query) -} - -// Return the default chat list items -func (self *Account) ChatListItems() ([]*ChatListItem, error) { - return self.QueryChatListItems("", nil, 0) -} - -// Return chat list items matching the given query. -func (self *Account) QueryChatListItems(query string, contact *Contact, listFlags uint) ([]*ChatListItem, error) { - var entries [][]uint64 - var query2 any - if query == "" { - query2 = nil - } else { - query2 = query - } - err := self.rpc().CallResult(&entries, "get_chatlist_entries", self.Id, listFlags, query2, contact) - var items []*ChatListItem - if err != nil { - return items, err - } - var itemsMap map[uint64]*ChatListItem - err = self.rpc().CallResult(&itemsMap, "get_chatlist_items_by_entries", self.Id, entries) - if err != nil { - return items, err - } - items = make([]*ChatListItem, len(entries)) - for i, entry := range entries { - items[i] = itemsMap[entry[0]] - } - return items, err -} - -// Return the default chat list entries. -func (self *Account) ChatListEntries() ([]*Chat, error) { - return self.QueryChatListEntries("", nil, 0) -} - -// Return chat list entries matching the given query. -func (self *Account) QueryChatListEntries(query string, contact *Contact, listFlags uint) ([]*Chat, error) { - var entries [][]ChatId - var query2 any - if query == "" { - query2 = nil - } else { - query2 = query - } - err := self.rpc().CallResult(&entries, "get_chatlist_entries", self.Id, listFlags, query2, contact) - var items []*Chat - if err != nil { - return items, err - } - items = make([]*Chat, len(entries)) - for i, entry := range entries { - items[i] = &Chat{self, entry[0]} - } - return items, nil -} - -// Add a text message in the "Device messages" chat and return the resulting Message instance. -func (self *Account) AddDeviceMsg(label, text string) (*Message, error) { - var id MsgId - err := self.rpc().CallResult(&id, "add_device_message", self.Id, label, text) - if err != nil { - return nil, err - } - return &Message{self, id}, nil -} - -func (self *Account) rpc() Rpc { - return self.Manager.Rpc -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/acfactory.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/acfactory.go index 3dfb28e..245a97b 100644 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/acfactory.go +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/acfactory.go @@ -2,37 +2,72 @@ package deltachat import ( "archive/zip" + "context" "fmt" "os" "path/filepath" "sync" "time" + + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/transport" ) // AcFactory facilitates unit testing Delta Chat clients/bots. // It must be used in conjunction with a test mail server service, for example: // https://github.com/deltachat/mail-server-tester +// +// Typical usage is as follows: +// +// import ( +// "testing" +// "github.com/deltachat/deltachat-rpc-client-go/deltachat" +// ) + +// var acfactory *deltachat.AcFactory + +// func TestMain(m *testing.M) { +// acfactory = &deltachat.AcFactory{} +// acfactory.TearUp() +// defer acfactory.TearDown() +// m.Run() +// } type AcFactory struct { + // DefaultCfg is the default settings to apply to new created accounts + DefaultCfg map[string]option.Option[string] + Debug bool tempDir string - acCfg map[string]string - debug bool serial int64 startTime int64 serialMutex sync.Mutex tearUp bool } -// Prepare the AcFactory, defaultAcConfig is the default settings to apply to -// the new accounts created with UnconfiguredAccount(), OnlineAccount(), OnlineBot() -// and RunningBot(). +// Prepare the AcFactory. // // If the test mail server has not standard configuration, you should set the custom configuration // here. -func (self *AcFactory) TearUp(defaultAcConfig map[string]string, tempDir string, debug bool) { - self.acCfg = defaultAcConfig - self.tempDir = tempDir - self.debug = debug +func (self *AcFactory) TearUp() { + if self.DefaultCfg == nil { + self.DefaultCfg = map[string]option.Option[string]{ + "mail_server": option.Some("localhost"), + "send_server": option.Some("localhost"), + "mail_port": option.Some("3143"), + "send_port": option.Some("3025"), + "mail_security": option.Some("3"), + "send_security": option.Some("3"), + "mvbox_move": option.Some("0"), + } + + } self.startTime = time.Now().Unix() + + dir, err := os.MkdirTemp("", "") + if err != nil { + panic(err) + } + self.tempDir = dir + self.tearUp = true } @@ -43,20 +78,6 @@ func (self *AcFactory) TearDown() { os.RemoveAll(self.tempDir) } -// Stop the Rpc of the given Account, Bot or AccountManager. -func (self *AcFactory) StopRpc(accountOrBot any) { - switch obj := accountOrBot.(type) { - case *Bot: - obj.Account.Manager.Rpc.Stop() - case *Account: - obj.Manager.Rpc.Stop() - case *AccountManager: - obj.Rpc.Stop() - default: - panic("invalid type provided to StopRpc()") - } -} - // MkdirTemp creates a new temporary directory. The directory is automatically removed on TearDown(). func (self *AcFactory) MkdirTemp() string { dir, err := os.MkdirTemp(self.tempDir, "") @@ -66,163 +87,181 @@ func (self *AcFactory) MkdirTemp() string { return dir } -// Create a new AccountManager. -func (self *AcFactory) NewAcManager() *AccountManager { +// Call the given function passing a new Rpc as parameter. +func (self *AcFactory) WithRpc(callback func(*Rpc)) { self.ensureTearUp() - rpc := NewRpcIO() - if !self.debug { - rpc.Stderr = nil + trans := transport.NewIOTransport() + if !self.Debug { + trans.Stderr = nil } dir := self.MkdirTemp() - rpc.AccountsDir = filepath.Join(dir, "accounts") - err := rpc.Start() + trans.AccountsDir = filepath.Join(dir, "accounts") + err := trans.Open() if err != nil { panic(err) } - return &AccountManager{rpc} + defer trans.Close() + + callback(&Rpc{Context: context.Background(), Transport: trans}) } -// Get a new Account that is not yet configured, but it is ready to be configured -// calling Account.Configure(). -func (self *AcFactory) UnconfiguredAccount() *Account { - account, err := self.NewAcManager().AddAccount() - if err != nil { - panic(err) - } - self.serialMutex.Lock() - self.serial++ - serial := self.serial - self.serialMutex.Unlock() +// Get a new Account that is not yet configured, but it is ready to be configured. +func (self *AcFactory) WithUnconfiguredAccount(callback func(*Rpc, AccountId)) { + self.WithRpc(func(rpc *Rpc) { + accId, err := rpc.AddAccount() + if err != nil { + panic(err) + } + self.serialMutex.Lock() + self.serial++ + serial := self.serial + self.serialMutex.Unlock() - if len(self.acCfg) != 0 { - err = account.UpdateConfig(self.acCfg) + if len(self.DefaultCfg) != 0 { + err = rpc.BatchSetConfig(accId, self.DefaultCfg) + if err != nil { + panic(err) + } + } + err = rpc.BatchSetConfig(accId, map[string]option.Option[string]{ + "addr": option.Some(fmt.Sprintf("acc%v.%v@localhost", serial, self.startTime)), + "mail_pw": option.Some(fmt.Sprintf("password%v", serial)), + }) if err != nil { panic(err) } - } - err = account.UpdateConfig(map[string]string{ - "addr": fmt.Sprintf("acc%v.%v@localhost", serial, self.startTime), - "mail_pw": fmt.Sprintf("password%v", serial), + + callback(rpc, accId) }) - if err != nil { - panic(err) - } - return account } // Get a new account configured and with I/O already started. -func (self *AcFactory) OnlineAccount() *Account { - account := self.UnconfiguredAccount() - err := account.Configure() - if err != nil { - panic(err) - } - return account +func (self *AcFactory) WithOnlineAccount(callback func(*Rpc, AccountId)) { + self.WithUnconfiguredAccount(func(rpc *Rpc, accId AccountId) { + err := rpc.Configure(accId) + if err != nil { + panic(err) + } + + callback(rpc, accId) + }) } -// Get a new bot configured and with its account I/O already started. The bot is not running yet. -func (self *AcFactory) OnlineBot() *Bot { - account := self.UnconfiguredAccount() - addr, _ := account.GetConfig("addr") - pass, _ := account.GetConfig("mail_pw") - bot := NewBot(account) - err := bot.Configure(addr, pass) - if err != nil { - panic(err) - } - return bot +// Get a new bot not yet configured, but its account is ready to be configured. +func (self *AcFactory) WithUnconfiguredBot(callback func(*Bot, AccountId)) { + self.WithUnconfiguredAccount(func(rpc *Rpc, accId AccountId) { + bot := NewBot(rpc) + callback(bot, accId) + }) } -// Get a new bot configured and already listening to new events/messages. -// It is ensured that Bot.IsRunning() is true for the returned bot. -func (self *AcFactory) RunningBot() *Bot { - bot := self.OnlineBot() - var err error - go func() { err = bot.Run() }() - for { - if bot.IsRunning() { - break - } +// Get a new bot configured and with its account I/O already started. The bot is not running yet. +func (self *AcFactory) WithOnlineBot(callback func(*Bot, AccountId)) { + self.WithUnconfiguredAccount(func(rpc *Rpc, accId AccountId) { + addr, _ := rpc.GetConfig(accId, "addr") + pass, _ := rpc.GetConfig(accId, "mail_pw") + bot := NewBot(rpc) + err := bot.Configure(accId, addr.Unwrap(), pass.Unwrap()) if err != nil { panic(err) } - } - return bot + + callback(bot, accId) + }) } -// Wait for the next incoming message in the given account. -func (self *AcFactory) NextMsg(account *Account) (*MsgSnapshot, error) { - event := self.WaitForEvent(account, EventIncomingMsg{}).(EventIncomingMsg) - msg := Message{account, event.MsgId} - return msg.Snapshot() +// Get a new bot configured and already listening to new events/messages. +// It is ensured that Bot.IsRunning() is true for the returned bot. +func (self *AcFactory) WithRunningBot(callback func(*Bot, AccountId)) { + self.WithOnlineBot(func(bot *Bot, accId AccountId) { + var err error + go func() { err = bot.Run() }() + for { + if bot.IsRunning() { + break + } + if err != nil { + panic(err) + } + } + + callback(bot, accId) + }) } -// Introduce two accounts to each other creating a 1:1 chat between them and exchanging messages. -func (self *AcFactory) IntroduceEachOther(account1, account2 *Account) { - chat, err := self.CreateChat(account1, account2) +// Wait for the next incoming message in the given account. +func (self *AcFactory) NextMsg(rpc *Rpc, accId AccountId) *MsgSnapshot { + event := self.WaitForEvent(rpc, accId, EventIncomingMsg{}).(EventIncomingMsg) + msg, err := rpc.GetMessage(accId, event.MsgId) if err != nil { panic(err) } - _, err = chat.SendText("hi") + return msg +} + +// Introduce two accounts to each other creating a 1:1 chat between them and exchanging messages. +func (self *AcFactory) IntroduceEachOther(rpc1 *Rpc, accId1 AccountId, rpc2 *Rpc, accId2 AccountId) { + chatId := self.CreateChat(rpc1, accId1, rpc2, accId2) + _, err := rpc1.MiscSendTextMessage(accId1, chatId, "hi") if err != nil { panic(err) } - self.WaitForEventInChat(account1, EventMsgsChanged{}, chat.Id) - snapshot, _ := self.NextMsg(account2) + self.WaitForEventInChat(rpc1, accId1, chatId, EventMsgsChanged{}) + snapshot := self.NextMsg(rpc2, accId2) if snapshot.Text != "hi" { panic("unexpected message: " + snapshot.Text) } - chat = &Chat{account2, snapshot.ChatId} - err = chat.Accept() + err = rpc2.AcceptChat(accId2, snapshot.ChatId) if err != nil { panic(err) } - _, err = chat.SendText("hello") + _, err = rpc2.MiscSendTextMessage(accId2, snapshot.ChatId, "hello") if err != nil { panic(err) } - self.WaitForEventInChat(account2, EventMsgsChanged{}, chat.Id) - snapshot, _ = self.NextMsg(account1) + self.WaitForEventInChat(rpc2, accId2, snapshot.ChatId, EventMsgsChanged{}) + snapshot = self.NextMsg(rpc1, accId1) if snapshot.Text != "hello" { panic("unexpected message: " + snapshot.Text) } } -// Create a 1:1 chat with acc2 in the chatlist of acc1. -func (self *AcFactory) CreateChat(acc1, acc2 *Account) (*Chat, error) { - addr2, err := acc2.GetConfig("configured_addr") +// Create a 1:1 chat with accId2 in the chatlist of accId1. +func (self *AcFactory) CreateChat(rpc1 *Rpc, accId1 AccountId, rpc2 *Rpc, accId2 AccountId) ChatId { + addr2, err := rpc2.GetConfig(accId2, "configured_addr") if err != nil { - return nil, err + panic(err) } - contact, err := acc1.CreateContact(addr2, "") + contactId, err := rpc1.CreateContact(accId1, addr2.Unwrap(), "") if err != nil { - fmt.Println("WARNING: Failed to create contact with: ", addr2) - return nil, err + panic(err) } - chat, err := contact.CreateChat() + chatId, err := rpc1.CreateChatByContactId(accId1, contactId) if err != nil { - return nil, err + panic(err) } - return chat, nil + return chatId } // Get a path to an image file that can be used for testing. func (self *AcFactory) TestImage() string { - acc := self.OnlineAccount() - defer self.StopRpc(acc) - chat, err := acc.Me().CreateChat() - if err != nil { - panic(err) - } - chatData, err := chat.BasicSnapshot() - if err != nil { - panic(err) - } - return chatData.ProfileImage + var img string + self.WithOnlineAccount(func(rpc *Rpc, accId AccountId) { + chatId, err := rpc.CreateChatByContactId(accId, ContactSelf) + if err != nil { + panic(err) + } + chatData, err := rpc.GetBasicChatInfo(accId, chatId) + if err != nil { + panic(err) + } + img = chatData.ProfileImage + }) + return img } // Get a path to a Webxdc file that can be used for testing. @@ -265,9 +304,9 @@ func (self *AcFactory) TestWebxdc() string { // Wait for an event of the same type as the given event, the event must belong to the chat // with the given ChatId. -func (self *AcFactory) WaitForEventInChat(account *Account, event Event, chatId ChatId) Event { +func (self *AcFactory) WaitForEventInChat(rpc *Rpc, accId AccountId, chatId ChatId, event Event) Event { for { - event = self.WaitForEvent(account, event) + event = self.WaitForEvent(rpc, accId, event) if getChatId(event) == chatId { return event } @@ -275,16 +314,25 @@ func (self *AcFactory) WaitForEventInChat(account *Account, event Event, chatId } // Wait for an event of the same type as the given event. -func (self *AcFactory) WaitForEvent(account *Account, event Event) Event { - eventChan := account.GetEventChannel() +func (self *AcFactory) WaitForEvent(rpc *Rpc, accId AccountId, event Event) Event { for { - ev := <-eventChan - if self.debug { - fmt.Printf("Waiting for event %v, got: %v\n", event.eventType(), ev.eventType()) + accId2, ev, err := rpc.GetNextEvent() + if err != nil { + panic(err) + } + if accId != accId2 { + fmt.Printf("WARNING: Waiting for event in account %v, but got event for account %v, discarding event %#v.\n", accId, accId2, event) + continue } if ev.eventType() == event.eventType() { + if self.Debug { + fmt.Printf("Got awaited event %v\n", ev.eventType()) + } return ev } + if self.Debug { + fmt.Printf("Waiting for event %v, got: %v\n", event.eventType(), ev.eventType()) + } } } @@ -311,6 +359,8 @@ func getChatId(event Event) ChatId { chatId = ev.ChatId case EventMsgRead: chatId = ev.ChatId + case EventMsgDeleted: + chatId = ev.ChatId case EventChatModified: chatId = ev.ChatId case EventChatEphemeralTimerModified: diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/bot.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/bot.go index 0547693..caa3cad 100644 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/bot.go +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/bot.go @@ -4,43 +4,35 @@ import ( "context" "fmt" "sync" + + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" ) -type EventHandler func(event Event) -type NewMsgHandler func(msg *Message) +type EventHandler func(bot *Bot, accId AccountId, event Event) +type NewMsgHandler func(bot *Bot, accId AccountId, msgId MsgId) -// Delta Chat bot that listen to events of a single account. -type Bot struct { - Account *Account - newMsgHandler NewMsgHandler - handlerMap map[eventType]EventHandler - handlerMapMutex sync.RWMutex - ctxMutex sync.Mutex - ctx context.Context - stop context.CancelFunc -} +// BotRunningErr is returned by Bot.Run() if the Bot is already running +type BotRunningErr struct{} -// Create a new Bot that will process events from the given account -func NewBot(account *Account) *Bot { - return &Bot{Account: account, handlerMap: make(map[eventType]EventHandler)} +func (self *BotRunningErr) Error() string { + return "bot is already running" } -// Helper function to create a new Bot from the given AccountManager. -// The first available account will be used, a new account will be created if none exists. -func NewBotFromAccountManager(manager *AccountManager) *Bot { - accounts, _ := manager.Accounts() - var acc *Account - if len(accounts) == 0 { - acc, _ = manager.AddAccount() - } else { - acc = accounts[0] - } - return NewBot(acc) +// Delta Chat bot that listen to account events, multiple accounts supported. +type Bot struct { + Rpc *Rpc + newMsgHandler NewMsgHandler + onUnhandledEvent EventHandler + handlerMap map[eventType]EventHandler + handlerMapMutex sync.RWMutex + ctxMutex sync.Mutex + ctx context.Context + stop context.CancelFunc } -// Implement Stringer. -func (self *Bot) String() string { - return fmt.Sprintf("Bot(Account=%v)", self.Account.Id) +// Create a new Bot that will process events for all created accounts. +func NewBot(rpc *Rpc) *Bot { + return &Bot{Rpc: rpc, handlerMap: make(map[eventType]EventHandler)} } // Set an EventHandler for the given event type. Calling On() several times @@ -51,6 +43,12 @@ func (self *Bot) On(event Event, handler EventHandler) { self.handlerMapMutex.Unlock() } +// Set an EventHandler to handle events whithout an EventHandler set via On(). +// Calling OnUnhandledEvent() several times will override the previously set EventHandler. +func (self *Bot) OnUnhandledEvent(handler EventHandler) { + self.onUnhandledEvent = handler +} + // Remove EventHandler for the given event type. func (self *Bot) RemoveEventHandler(event Event) { self.handlerMapMutex.Lock() @@ -63,56 +61,31 @@ func (self *Bot) OnNewMsg(handler NewMsgHandler) { self.newMsgHandler = handler } -// Configure the bot's account. -func (self *Bot) Configure(addr string, password string) error { - err := self.Account.UpdateConfig( - map[string]string{ - "bot": "1", - "addr": addr, - "mail_pw": password, +// Configure one of the bot's accounts. +func (self *Bot) Configure(accId AccountId, addr string, password string) error { + err := self.Rpc.BatchSetConfig( + accId, + map[string]option.Option[string]{ + "bot": option.Some("1"), + "addr": option.Some(addr), + "mail_pw": option.Some(password), }, ) if err != nil { return err } - return self.Account.Configure() + return self.Rpc.Configure(accId) } -// Return true if the bot's account is configured, false otherwise. -func (self *Bot) IsConfigured() bool { - configured, _ := self.Account.IsConfigured() - return configured -} - -// Tweak several account configuration values in a batch. -func (self *Bot) UpdateConfig(config map[string]string) error { - return self.Account.UpdateConfig(config) -} - -// Set account configuration value. -func (self *Bot) SetConfig(key string, value string) error { - return self.Account.SetConfig(key, value) -} - -// Get account configuration value. -func (self *Bot) GetConfig(key string) (string, error) { - return self.Account.GetConfig(key) -} - -// Set UI-specific configuration value in the bot's account. +// Set UI-specific configuration value in the given account. // This is useful for custom 3rd party settings set by bot programs. -func (self *Bot) SetUiConfig(key string, value string) error { - return self.Account.SetUiConfig(key, value) +func (self *Bot) SetUiConfig(accId AccountId, key string, value option.Option[string]) error { + return self.Rpc.SetConfig(accId, "ui."+key, value) } // Get custom UI-specific configuration value set with SetUiConfig(). -func (self *Bot) GetUiConfig(key string) (string, error) { - return self.Account.GetUiConfig(key) -} - -// The bot's self-contact. -func (self *Bot) Me() *Contact { - return self.Account.Me() +func (self *Bot) GetUiConfig(accId AccountId, key string) (option.Option[string], error) { + return self.Rpc.GetConfig(accId, "ui."+key) } // Process events until Stop() is called. If the bot is already running, BotRunningErr is returned. @@ -125,25 +98,42 @@ func (self *Bot) Run() error { self.ctx, self.stop = context.WithCancel(context.Background()) self.ctxMutex.Unlock() - if self.IsConfigured() { - self.Account.StartIO() //nolint:errcheck - self.processMessages() // Process old messages. + self.Rpc.StartIoForAllAccounts() //nolint:errcheck + ids, _ := self.Rpc.GetAllAccountIds() + for _, accId := range ids { + if isConf, _ := self.Rpc.IsConfigured(accId); isConf { + self.processMessages(accId) // Process old messages. + } } - eventChan := self.Account.GetEventChannel() + eventChan := make(chan struct { + AccountId + Event + }) + go func() { + for { + rpc := &Rpc{Context: self.ctx, Transport: self.Rpc.Transport} + accId, event, err := rpc.GetNextEvent() + if err != nil { + close(eventChan) + break + } + eventChan <- struct { + AccountId + Event + }{accId, event} + } + }() + for { - select { - case <-self.ctx.Done(): + evData, ok := <-eventChan + if !ok { + self.Stop() return nil - case event, ok := <-eventChan: - if !ok { - self.stop() - return nil - } - self.onEvent(event) - if event.eventType() == eventTypeIncomingMsg { - self.processMessages() - } + } + self.onEvent(evData.AccountId, evData.Event) + if evData.Event.eventType() == eventTypeIncomingMsg { + self.processMessages(evData.AccountId) } } } @@ -157,29 +147,33 @@ func (self *Bot) IsRunning() bool { // Stop processing events. func (self *Bot) Stop() { - if self.ctx != nil { + self.ctxMutex.Lock() + defer self.ctxMutex.Unlock() + if self.ctx != nil && self.ctx.Err() == nil { self.stop() } } -func (self *Bot) onEvent(event Event) { +func (self *Bot) onEvent(accId AccountId, event Event) { self.handlerMapMutex.RLock() handler, ok := self.handlerMap[event.eventType()] self.handlerMapMutex.RUnlock() if ok { - handler(event) + handler(self, accId, event) + } else if self.onUnhandledEvent != nil { + self.onUnhandledEvent(self, accId, event) } } -func (self *Bot) processMessages() { - msgs, err := self.Account.FreshMsgsInArrivalOrder() +func (self *Bot) processMessages(accId AccountId) { + msgIds, err := self.Rpc.GetNextMsgs(accId) if err != nil { return } - for _, msg := range msgs { + for _, msgId := range msgIds { + self.Rpc.SetConfig(accId, "last_msg_id", option.Some(fmt.Sprintf("%v", msgId))) //nolint:errcheck if self.newMsgHandler != nil { - self.newMsgHandler(msg) + self.newMsgHandler(self, accId, msgId) } - msg.MarkSeen() //nolint:errcheck } } diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/chat.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/chat.go deleted file mode 100644 index 1affc44..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/chat.go +++ /dev/null @@ -1,320 +0,0 @@ -package deltachat - -import "fmt" - -type ChatId uint64 - -// Values in const.go -type ChatType uint - -// Full chat snapshot. -type FullChatSnapshot struct { - Id ChatId - Name string - IsProtected bool - ProfileImage string - Archived bool - ChatType ChatType - IsUnpromoted bool - IsSelfTalk bool - Contacts []*ContactSnapshot - ContactIds []ContactId - Color string - FreshMessageCounter uint - IsContactRequest bool - IsDeviceChat bool - SelfInGroup bool - IsMuted bool - EphemeralTimer uint - CanSend bool - WasSeenRecently bool - MailingListAddress string -} - -// Cheaper version of FullChatSnapshot. -type BasicChatSnapshot struct { - Id ChatId - Name string - IsProtected bool - ProfileImage string - Archived bool - ChatType ChatType - IsUnpromoted bool - IsSelfTalk bool - Color string - IsContactRequest bool - IsDeviceChat bool - IsMuted bool -} - -// Delta Chat Chat. -type Chat struct { - Account *Account - Id ChatId -} - -// Implement Stringer. -func (self *Chat) String() string { - return fmt.Sprintf("Chat(Id=%v, Account=%v)", self.Id, self.Account.Id) -} - -// Delete this chat and all its messages. -func (self *Chat) Delete() error { - return self.rpc().Call("delete_chat", self.Account.Id, self.Id) -} - -// Block this chat. -func (self *Chat) Block() error { - return self.rpc().Call("block_chat", self.Account.Id, self.Id) -} - -// Accept this contact request chat. -func (self *Chat) Accept() error { - return self.rpc().Call("accept_chat", self.Account.Id, self.Id) -} - -// Leave this group chat. -func (self *Chat) Leave() error { - return self.rpc().Call("leave_group", self.Account.Id, self.Id) -} - -// Mark all messages in this chat as noticed. -func (self *Chat) MarkNoticed() error { - return self.rpc().Call("marknoticed_chat", self.Account.Id, self.Id) -} - -// Set mute duration of this chat. -// duration value can be: -// -// 0 - Chat is not muted. -// -// -1 - Chat is muted until the user unmutes the chat. -// -// t - Chat is muted for a limited period of time. -func (self *Chat) SetMuteDuration(duration int64) error { - var data any - switch duration { - case -1: - data = "Forever" - case 0: - data = "NotMuted" - default: - data = map[string]int64{"Until": duration} - } - return self.rpc().Call("set_chat_mute_duration", self.Account.Id, self.Id, data) -} - -// Set name of this chat. -func (self *Chat) SetName(name string) error { - return self.rpc().Call("set_chat_name", self.Account.Id, self.Id, name) -} - -// Set profile image of this chat. -func (self *Chat) SetImage(path string) error { - return self.rpc().Call("set_chat_profile_image", self.Account.Id, self.Id, path) -} - -// Remove profile image of this chat. -func (self *Chat) RemoveImage() error { - return self.rpc().Call("set_chat_profile_image", self.Account.Id, self.Id, nil) -} - -// Pin this chat. -func (self *Chat) Pin() error { - return self.rpc().Call("set_chat_visibility", self.Account.Id, self.Id, ChatVisibilityPinned) -} - -// Unpin this chat. -func (self *Chat) Unpin() error { - return self.rpc().Call("set_chat_visibility", self.Account.Id, self.Id, ChatVisibilityNormal) -} - -// Archive this chat. -func (self *Chat) Archive() error { - return self.rpc().Call("set_chat_visibility", self.Account.Id, self.Id, ChatVisibilityArchived) -} - -// Unarchive this chat.a -func (self *Chat) Unarchive() error { - return self.rpc().Call("set_chat_visibility", self.Account.Id, self.Id, ChatVisibilityNormal) -} - -// Add contact to this group. -func (self *Chat) AddContact(contact *Contact) error { - return self.rpc().Call("add_contact_to_chat", self.Account.Id, self.Id, contact.Id) -} - -// Remove contact from this group. -func (self *Chat) RemoveContact(contact *Contact) error { - return self.rpc().Call("remove_contact_from_chat", self.Account.Id, self.Id, contact.Id) -} - -// Get the list of contacts in this chat. -func (self *Chat) Contacts() ([]*Contact, error) { - var contacts []*Contact - var ids []ContactId - err := self.rpc().CallResult(&ids, "get_chat_contacts", self.Account.Id, self.Id) - if err != nil { - return contacts, err - } - contacts = make([]*Contact, len(ids)) - for i := range ids { - contacts[i] = &Contact{self.Account, ids[i]} - } - return contacts, nil -} - -// Set ephemeral timer of this chat. -func (self *Chat) SetEphemeralTimer(timer uint) error { - return self.rpc().Call("set_chat_ephemeral_timer", self.Account.Id, self.Id, timer) -} - -// Get ephemeral timer of this chat. -func (self *Chat) EphemeralTimer() (uint, error) { - var timer uint - err := self.rpc().CallResult(&timer, "get_chat_ephemeral_timer", self.Account.Id, self.Id) - return timer, err -} - -// Get Join-Group QR code text and SVG data. -func (self *Chat) QrCode() (string, string, error) { - var data [2]string - err := self.rpc().CallResult(&data, "get_chat_securejoin_qr_code_svg", self.Account.Id, self.Id) - return data[0], data[1], err -} - -// Get encryption info for this chat. -// Get a multi-line encryption info, containing encryption preferences of all members. -// Can be used to find out why messages sent to group are not encrypted. -// -// returns Multi-line text -func (self *Chat) EncryptionInfo() (string, error) { - var data string - err := self.rpc().CallResult(&data, "get_chat_encryption_info", self.Account.Id, self.Id) - return data, err -} - -// Get the list of messages in this chat. -func (self *Chat) Messages(infoOnly, addDaymarker bool) ([]*Message, error) { - var msgs []*Message - var ids []MsgId - err := self.rpc().CallResult(&ids, "get_message_ids", self.Account.Id, self.Id, infoOnly, addDaymarker) - if err != nil { - return msgs, err - } - msgs = make([]*Message, len(ids)) - for i := range ids { - msgs[i] = &Message{self.Account, ids[i]} - } - return msgs, nil -} - -// Search for messages in this chat containing the given query string. -func (self *Chat) SearchMessages(query string) ([]*MsgSearchResult, error) { - var results []*MsgSearchResult - - var msgIds []MsgId - var chatId any - if self.Id == 0 { - chatId = nil - } else { - chatId = self.Id - } - err := self.rpc().CallResult(&msgIds, "search_messages", self.Account.Id, query, chatId) - if err != nil { - return results, err - } - - var resultsMap map[MsgId]*MsgSearchResult - err = self.rpc().CallResult(&resultsMap, "message_ids_to_search_results", self.Account.Id, msgIds) - if err != nil { - return results, err - } - - results = make([]*MsgSearchResult, len(msgIds)) - for i, msgId := range msgIds { - results[i] = resultsMap[msgId] - } - - return results, nil -} - -// Get the number of fresh messages in this chat. -func (self *Chat) FreshMsgCount() (uint, error) { - var count uint - err := self.rpc().CallResult(&count, "get_fresh_msg_cnt", self.Account.Id, self.Id) - return count, err -} - -// Send a message and return the resulting Message instance. -func (self *Chat) SendMsg(msgData MsgData) (*Message, error) { - var id MsgId - err := self.rpc().CallResult(&id, "send_msg", self.Account.Id, self.Id, msgData) - if err != nil { - return nil, err - } - return &Message{self.Account, id}, nil -} - -// Send a text message and return the resulting Message instance. -func (self *Chat) SendText(text string) (*Message, error) { - var id MsgId - err := self.rpc().CallResult(&id, "misc_send_text_message", self.Account.Id, self.Id, text) - if err != nil { - return nil, err - } - return &Message{self.Account, id}, nil -} - -// Send a video chat invitation. -func (self *Chat) SendVideoChatInvitation() (*Message, error) { - var id MsgId - err := self.rpc().CallResult(&id, "send_videochat_invitation", self.Account.Id, self.Id) - if err != nil { - return nil, err - } - return &Message{self.Account, id}, nil -} - -// Get first unread message in this chat. -func (self *Chat) FirstUnreadMsg() (*Message, error) { - var id MsgId - err := self.rpc().CallResult(&id, "get_first_unread_message_of_chat", self.Account.Id, self.Id) - if err != nil { - return nil, err - } - return &Message{self.Account, id}, nil -} - -// Get a chat snapshot with basic info about this chat. -func (self *Chat) BasicSnapshot() (*BasicChatSnapshot, error) { - var result BasicChatSnapshot - err := self.rpc().CallResult(&result, "get_basic_chat_info", self.Account.Id, self.Id) - if err != nil { - return nil, err - } - return &result, nil -} - -// Get a full snapshot of this chat. -func (self *Chat) FullSnapshot() (*FullChatSnapshot, error) { - var result FullChatSnapshot - err := self.rpc().CallResult(&result, "get_full_chat_by_id", self.Account.Id, self.Id) - if err != nil { - return nil, err - } - return &result, nil -} - -// Forward a list of messages to this chat. -func (self *Chat) DeleteMsgs(messages []*Message) error { - ids := make([]MsgId, len(messages)) - for i := range messages { - ids[i] = messages[i].Id - } - return self.rpc().Call("forward_messages", self.Account.Id, ids, self.Id) -} - -func (self *Chat) rpc() Rpc { - return self.Account.Manager.Rpc -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/chatlist.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/chatlist.go deleted file mode 100644 index f4c714b..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/chatlist.go +++ /dev/null @@ -1,34 +0,0 @@ -package deltachat - -// Chat list item snapshot -type ChatListItem struct { - Id ChatId - Name string - AvatarPath string - Color string - LastUpdated Timestamp - SummaryText1 string - SummaryText2 string - SummaryStatus uint32 - IsProtected bool - IsGroup bool - FreshMessageCounter uint - IsSelfTalk bool - IsDeviceTalk bool - IsSendingLocation bool - IsSelfInGroup bool - IsArchived bool - IsPinned bool - IsMuted bool - IsContactRequest bool - IsBroadcast bool - DmChatContact ContactId - WasSeenRecently bool - - // ArchiveLink - // FreshMessageCounter uint - - // Error - // Id uint64 - Error string -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/contact.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/contact.go deleted file mode 100644 index 510c1f0..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/contact.go +++ /dev/null @@ -1,82 +0,0 @@ -package deltachat - -import "fmt" - -type ContactId uint64 - -// Delta Chat Contact snapshot. -type ContactSnapshot struct { - Address string - Color string - AuthName string - Status string - DisplayName string - Id ContactId - Name string - ProfileImage string - NameAndAddr string - IsBlocked bool - IsVerified bool - VerifierAddr string - VerifierId ContactId - LastSeen Timestamp - WasSeenRecently bool -} - -// Delta Chat Contact. -type Contact struct { - Account *Account - Id ContactId -} - -// Implement Stringer. -func (self *Contact) String() string { - return fmt.Sprintf("Contact(Id=%v, Account=%v)", self.Id, self.Account.Id) -} - -// Block contact. -func (self *Contact) Block() error { - return self.rpc().Call("block_contact", self.Account.Id, self.Id) -} - -// Unblock contact. -func (self *Contact) Unblock() error { - return self.rpc().Call("unblock_contact", self.Account.Id, self.Id) -} - -// Delete contact. -func (self *Contact) Delete() error { - return self.rpc().Call("delete_contact", self.Account.Id, self.Id) -} - -// Set name of this contact. -func (self *Contact) SetName(name string) error { - return self.rpc().Call("change_contact_name", self.Account.Id, self.Id, name) -} - -// Get encryption info for this contact. -// Get a multi-line encryption info, containing your fingerprint and the -// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification. -func (self *Contact) EncryptionInfo() (string, error) { - var data string - err := self.rpc().CallResult(&data, "get_contact_encryption_info", self.Account.Id, self.Id) - return data, err -} - -// Return a map with a snapshot of all contact properties. -func (self *Contact) Snapshot() (*ContactSnapshot, error) { - var snapshot ContactSnapshot - err := self.rpc().CallResult(&snapshot, "get_contact", self.Account.Id, self.Id) - return &snapshot, err -} - -// Create or get an existing 1:1 chat for this contact. -func (self *Contact) CreateChat() (*Chat, error) { - var id ChatId - err := self.rpc().CallResult(&id, "create_chat_by_contact_id", self.Account.Id, self.Id) - return &Chat{self.Account, id}, err -} - -func (self *Contact) rpc() Rpc { - return self.Account.Manager.Rpc -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/errors.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/errors.go deleted file mode 100644 index bd87c26..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/errors.go +++ /dev/null @@ -1,15 +0,0 @@ -package deltachat - -// BotRunningErr is returned by Bot.Run() if the Bot is already running -type BotRunningErr struct{} - -func (self *BotRunningErr) Error() string { - return "bot is already running" -} - -// RpcRunningErr is returned by Rpc.Start() if the Rpc is already running -type RpcRunningErr struct{} - -func (self *RpcRunningErr) Error() string { - return "RPC is already running" -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/event.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/event.go index 2cd9be0..7ece295 100644 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/event.go +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/event.go @@ -23,6 +23,7 @@ const ( eventTypeMsgDelivered eventType = "MsgDelivered" eventTypeMsgFailed eventType = "MsgFailed" eventTypeMsgRead eventType = "MsgRead" + eventTypeMsgDeleted eventType = "MsgDeleted" eventTypeChatModified eventType = "ChatModified" eventTypeChatEphemeralTimerModified eventType = "ChatEphemeralTimerModified" eventTypeContactsChanged eventType = "ContactsChanged" @@ -38,6 +39,117 @@ const ( eventTypeWebxdcInstanceDeleted eventType = "WebxdcInstanceDeleted" ) +type _Event struct { + ContextId AccountId + Event *_EventData +} + +type _EventData struct { + Kind eventType + Msg string + File string + ChatId ChatId + MsgId MsgId + ContactId ContactId + MsgIds []MsgId + Timer int + Progress uint + Comment string + Path string + StatusUpdateSerial uint +} + +func (self *_EventData) ToEvent() Event { + var event Event + switch self.Kind { + case eventTypeInfo: + event = EventInfo{Msg: self.Msg} + case eventTypeSmtpConnected: + event = EventSmtpConnected{Msg: self.Msg} + case eventTypeImapConnected: + event = EventImapConnected{Msg: self.Msg} + case eventTypeSmtpMessageSent: + event = EventSmtpMessageSent{Msg: self.Msg} + case eventTypeImapMessageDeleted: + event = EventImapMessageDeleted{Msg: self.Msg} + case eventTypeImapMessageMoved: + event = EventImapMessageMoved{Msg: self.Msg} + case eventTypeImapInboxIdle: + event = EventImapInboxIdle{} + case eventTypeNewBlobFile: + event = EventNewBlobFile{File: self.File} + case eventTypeDeletedBlobFile: + event = EventDeletedBlobFile{File: self.File} + case eventTypeWarning: + event = EventWarning{Msg: self.Msg} + case eventTypeError: + event = EventError{Msg: self.Msg} + case eventTypeErrorSelfNotInGroup: + event = EventErrorSelfNotInGroup{Msg: self.Msg} + case eventTypeMsgsChanged: + event = EventMsgsChanged{ChatId: self.ChatId, MsgId: self.MsgId} + case eventTypeReactionsChanged: + event = EventReactionsChanged{ + ChatId: self.ChatId, + MsgId: self.MsgId, + ContactId: self.ContactId, + } + case eventTypeIncomingMsg: + event = EventIncomingMsg{ChatId: self.ChatId, MsgId: self.MsgId} + case eventTypeIncomingMsgBunch: + event = EventIncomingMsgBunch{MsgIds: self.MsgIds} + case eventTypeMsgsNoticed: + event = EventMsgsNoticed{ChatId: self.ChatId} + case eventTypeMsgDelivered: + event = EventMsgDelivered{ChatId: self.ChatId, MsgId: self.MsgId} + case eventTypeMsgFailed: + event = EventMsgFailed{ChatId: self.ChatId, MsgId: self.MsgId} + case eventTypeMsgRead: + event = EventMsgRead{ChatId: self.ChatId, MsgId: self.MsgId} + case eventTypeMsgDeleted: + event = EventMsgDeleted{ChatId: self.ChatId, MsgId: self.MsgId} + case eventTypeChatModified: + event = EventChatModified{ChatId: self.ChatId} + case eventTypeChatEphemeralTimerModified: + event = EventChatEphemeralTimerModified{ + ChatId: self.ChatId, + Timer: self.Timer, + } + case eventTypeContactsChanged: + event = EventContactsChanged{ContactId: self.ContactId} + case eventTypeLocationChanged: + event = EventLocationChanged{ContactId: self.ContactId} + case eventTypeConfigureProgress: + event = EventConfigureProgress{Progress: self.Progress, Comment: self.Comment} + case eventTypeImexProgress: + event = EventImexProgress{Progress: self.Progress} + case eventTypeImexFileWritten: + event = EventImexFileWritten{Path: self.Path} + case eventTypeSecurejoinInviterProgress: + event = EventSecurejoinInviterProgress{ + ContactId: self.ContactId, + Progress: self.Progress, + } + case eventTypeSecurejoinJoinerProgress: + event = EventSecurejoinJoinerProgress{ + ContactId: self.ContactId, + Progress: self.Progress, + } + case eventTypeConnectivityChanged: + event = EventConnectivityChanged{} + case eventTypeSelfavatarChanged: + event = EventSelfavatarChanged{} + case eventTypeWebxdcStatusUpdate: + event = EventWebxdcStatusUpdate{ + MsgId: self.MsgId, + StatusUpdateSerial: self.StatusUpdateSerial, + } + case eventTypeWebxdcInstanceDeleted: + event = EventWebxdcInstanceDeleted{MsgId: self.MsgId} + } + return event +} + // Delta Chat core Event type Event interface { eventType() eventType @@ -265,6 +377,16 @@ func (self EventMsgRead) eventType() eventType { return eventTypeMsgRead } +// A single message is deleted. +type EventMsgDeleted struct { + ChatId ChatId + MsgId MsgId +} + +func (self EventMsgDeleted) eventType() eventType { + return eventTypeMsgDeleted +} + // Chat changed. The name or the image of a chat group was changed or members were added or removed. // Or the verify state of a chat has changed. // See Chat.SetName(), Chat.SetImage(), Chat.AddContact() diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/manager.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/manager.go deleted file mode 100644 index 9028337..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/manager.go +++ /dev/null @@ -1,72 +0,0 @@ -package deltachat - -import "fmt" - -// Delta Chat accounts manager. This is the root of the API. -type AccountManager struct { - Rpc Rpc -} - -// Implement Stringer. -func (self *AccountManager) String() string { - return fmt.Sprintf("AccountManager(Rpc=%#v)", self.Rpc.String()) -} - -// Create a new account. -func (self *AccountManager) AddAccount() (*Account, error) { - var id AccountId - err := self.Rpc.CallResult(&id, "add_account") - return &Account{self, id}, err -} - -// Get the selected account. -func (self *AccountManager) SelectedAccount() (*Account, error) { - var id AccountId - err := self.Rpc.CallResult(&id, "get_selected_account_id") - if id == 0 { - return nil, err - } - return &Account{self, id}, err -} - -// Return all available accounts. -func (self *AccountManager) Accounts() ([]*Account, error) { - var ids []AccountId - err := self.Rpc.CallResult(&ids, "get_all_account_ids") - var accounts []*Account - if err != nil { - return accounts, err - } - accounts = make([]*Account, len(ids)) - for i := range ids { - accounts[i] = &Account{self, ids[i]} - } - return accounts, err -} - -// Start the I/O of all accounts. -func (self *AccountManager) StartIO() error { - return self.Rpc.Call("start_io_for_all_accounts") -} - -// Stop the I/O of all accounts. -func (self *AccountManager) StopIO() error { - return self.Rpc.Call("stop_io_for_all_accounts") -} - -// Indicate that the network likely has come back or just that the network conditions might have changed. -func (self *AccountManager) MaybeNetwork() error { - return self.Rpc.Call("maybe_network") -} - -// Get information about the Delta Chat core in this system. -func (self *AccountManager) SystemInfo() (map[string]string, error) { - var info map[string]string - err := self.Rpc.CallResult(&info, "get_system_info") - return info, err -} - -// Set stock translation strings. -func (self *AccountManager) SetTranslations(translations map[uint]string) error { - return self.Rpc.Call("set_stock_strings", translations) -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/message.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/message.go deleted file mode 100644 index 2070766..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/message.go +++ /dev/null @@ -1,124 +0,0 @@ -package deltachat - -import "fmt" - -type MsgId uint64 - -// Message data provided to Chat.SendMsg() -type MsgData struct { - Text string `json:"text,omitempty"` - Html string `json:"html,omitempty"` - ViewType MsgType `json:"viewtype,omitempty"` - File string `json:"file,omitempty"` - Location *[2]float64 `json:"location,omitempty"` - OverrideSenderName string `json:"overrideSenderName,omitempty"` - QuotedMessageId MsgId `json:"quotedMessageId,omitempty"` -} - -// Message quote. Only the Text property is warrantied to be present, all other fields are optional. -type MsgQuote struct { - Text string - MessageId MsgId - AuthorDisplayName string - AuthorDisplayColor string - OverrideSenderName string - Image string - IsForwarded bool - ViewType MsgType -} - -// Message search result. -type MsgSearchResult struct { - Id MsgId - AuthorProfileImage string - AuthorName string - AuthorColor string - ChatName string - Message string - Timestamp Timestamp -} - -// Delta Chat Message. -type Message struct { - Account *Account - Id MsgId -} - -// Implement Stringer. -func (self *Message) String() string { - return fmt.Sprintf("Message(Id=%v, Account=%v)", self.Id, self.Account.Id) -} - -// Return map of this account configuration parameters. -func (self *Message) Snapshot() (*MsgSnapshot, error) { - var snapshot MsgSnapshot - err := self.rpc().CallResult(&snapshot, "get_message", self.Account.Id, self.Id) - if err != nil { - return nil, err - } - snapshot.Account = self.Account - return &snapshot, err -} - -// Get the HTML part of this message. -func (self *Message) Html() (string, error) { - var html string - err := self.rpc().CallResult(&html, "get_message_html", self.Account.Id, self.Id) - return html, err -} - -// Get an informational text for a single message. -func (self *Message) Info() (string, error) { - var info string - err := self.rpc().CallResult(&info, "get_message_info", self.Account.Id, self.Id) - return info, err -} - -// Delete message. -func (self *Message) Delete() error { - return self.rpc().Call("delete_messages", self.Account.Id, []MsgId{self.Id}) -} - -// Asks the core to start downloading a message fully. -func (self *Message) Download() error { - return self.rpc().Call("download_full_message", self.Account.Id, self.Id) -} - -// Mark the message as seen. -func (self *Message) MarkSeen() error { - return self.rpc().Call("markseen_msgs", self.Account.Id, []MsgId{self.Id}) -} - -// Send a reaction to this message. -func (self *Message) SendReaction(reaction ...string) error { - err := self.rpc().Call("send_reaction", self.Account.Id, self.Id, reaction) - return err -} - -// Continue the AutoCrypt key transfer process. -func (self *Message) ContinueAutocryptKeyTransfer(setupCode string) error { - return self.rpc().Call("continue_autocrypt_key_transfer", self.Account.Id, self.Id, setupCode) -} - -// Send status update for the webxdc instance of this message. -func (self *Message) SendStatusUpdate(update, description string) error { - return self.rpc().Call("send_webxdc_status_update", self.Account.Id, self.Id, update, description) -} - -// Get the status updates of this webxdc message as a JSON string. -func (self *Message) StatusUpdates(lastKnownSerial uint) (string, error) { - var data string - err := self.rpc().CallResult(&data, "get_webxdc_status_updates", self.Account.Id, self.Id, lastKnownSerial) - return data, err -} - -// Get info from this webxdc message. -func (self *Message) WebxdcInfo() (*WebxdcMsgInfo, error) { - var info WebxdcMsgInfo - err := self.rpc().CallResult(&info, "get_webxdc_info", self.Account.Id, self.Id) - return &info, err -} - -func (self *Message) rpc() Rpc { - return self.Account.Manager.Rpc -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/msgsnapshot.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/msgsnapshot.go deleted file mode 100644 index 5436a3b..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/msgsnapshot.go +++ /dev/null @@ -1,109 +0,0 @@ -package deltachat - -import ( - "fmt" - "regexp" - "strings" -) - -// Message snapshot. -type MsgSnapshot struct { - Account *Account - - Id MsgId - ChatId ChatId - FromId ContactId - Quote *MsgQuote - ParentId MsgId - Text string - HasLocation bool - HasHtml bool - ViewType MsgType - State MsgState - Error string - Timestamp Timestamp - SortTimestamp Timestamp - ReceivedTimestamp Timestamp - HasDeviatingTimestamp bool - Subject string - ShowPadlock bool - IsSetupmessage bool - IsInfo bool - IsForwarded bool - IsBot bool - SystemMessageType SysmsgType - Duration int - DimensionsHeight int - DimensionsWidth int - VideochatType int - VideochatUrl string - OverrideSenderName string - Sender *ContactSnapshot - SetupCodeBegin string - File string - FileMime string - FileBytes uint64 - FileName string - WebxdcInfo *WebxdcMsgInfo - DownloadState DownloadState - Reactions *Reactions -} - -// Extract metadata from system message with type SysmsgTypeMemberAddedToGroup. -func (self *MsgSnapshot) ParseMemberAdded() (actor *Contact, target *Contact, err error) { - return self.parseMemberAddRemove("added") -} - -// Extract metadata from system message with type SysmsgTypeMemberRemovedFromGroup. -func (self *MsgSnapshot) ParseMemberRemoved() (actor *Contact, target *Contact, err error) { - return self.parseMemberAddRemove("removed") -} - -func (self *MsgSnapshot) parseMemberAddRemove(action string) (actor *Contact, target *Contact, err error) { - text := strings.ToLower(self.Text) - actor = &Contact{self.Account, self.FromId} - - regex := regexp.MustCompile(`^member (.+) ` + action + ` by .+\.$`) - match := regex.FindStringSubmatch(text) - if len(match) > 0 { - target, err := self.extractContact(match[1]) - if err != nil { - return nil, nil, err - } - return actor, target, nil - } - - regex = regexp.MustCompile(`^you ` + action + ` member (.+)\.$`) - match = regex.FindStringSubmatch(text) - if len(match) > 0 { - target, err := self.extractContact(match[1]) - if err != nil { - return nil, nil, err - } - return actor, target, nil - } - - if action == "removed" { - regex = regexp.MustCompile(`^group left by .+\.$`) - match = regex.FindStringSubmatch(text) - if len(match) > 0 { - return actor, actor, nil - } - - regex = regexp.MustCompile(`^you left the group\.$`) - if regex.MatchString(text) { - return actor, actor, nil - } - } - - return nil, nil, fmt.Errorf("System message does not match") -} - -func (self *MsgSnapshot) extractContact(text string) (*Contact, error) { - regex := regexp.MustCompile(`^.*\((.+@.+)\)$`) - match := regex.FindStringSubmatch(text) - if len(match) > 0 { - text = match[1] - } - return self.Account.GetContactByAddr(text) -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/option/option.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/option/option.go new file mode 100644 index 0000000..b6b642d --- /dev/null +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/option/option.go @@ -0,0 +1,45 @@ +package option + +import ( + "encoding/json" +) + +type Option[T any] struct { + Value *T +} + +func Some[T any](value T) Option[T] { + return Option[T]{Value: &value} +} + +func None[T any]() Option[T] { + return Option[T]{} +} + +func (self Option[T]) IsSome() bool { + return self.Value != nil +} + +func (self Option[T]) IsNone() bool { + return self.Value == nil +} + +func (self Option[T]) Unwrap() T { + return *self.Value +} + +func (self Option[T]) UnwrapOr(or T) T { + if self.IsNone() { + return or + } + return *self.Value +} + +func (self Option[T]) MarshalJSON() ([]byte, error) { + val, err := json.Marshal(self.Value) + return val, err +} + +func (self *Option[T]) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &self.Value) +} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/reactions.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/reactions.go deleted file mode 100644 index 6e6adac..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/reactions.go +++ /dev/null @@ -1,6 +0,0 @@ -package deltachat - -type Reactions struct { - ReactionsByContact map[ContactId][]string - Reactions map[string]int // Unique reactions and their count -} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpc.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpc.go index aba0911..400c945 100644 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpc.go +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpc.go @@ -2,248 +2,945 @@ package deltachat import ( "context" - "fmt" - "io" - "os" - "os/exec" - "sync" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/channel" + + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" + "github.com/deltachat/deltachat-rpc-client-go/deltachat/transport" ) -type _Event struct { - Type eventType - Msg string - File string - ChatId ChatId - MsgId MsgId - ContactId ContactId - MsgIds []MsgId - Timer int - Progress uint - Comment string - Path string - StatusUpdateSerial uint -} - -type _Params struct { - ContextId uint64 - Event *_Event -} - -// Delta Chat core RPC -type Rpc interface { - Start() error - Stop() - GetEventChannel(accountId AccountId) <-chan Event - Call(method string, params ...any) error - CallResult(result any, method string, params ...any) error - String() string -} - -// Delta Chat core RPC working over IO -type RpcIO struct { - Stderr io.Writer - AccountsDir string - Cmd string - cmd *exec.Cmd - stdin io.WriteCloser - client *jrpc2.Client - ctx context.Context - cancel context.CancelFunc - events map[AccountId]chan Event - mu sync.Mutex -} - -func NewRpcIO() *RpcIO { - return &RpcIO{Cmd: deltachatRpcServerBin, Stderr: os.Stderr} -} - -// Implement Stringer. -func (self *RpcIO) String() string { - return fmt.Sprintf("Rpc(AccountsDir=%#v)", self.AccountsDir) -} - -func (self *RpcIO) Start() error { - self.mu.Lock() - defer self.mu.Unlock() - - if self.ctx != nil && self.ctx.Err() == nil { - return &RpcRunningErr{} - } +// Delta Chat RPC client. This is the root of the API. +type Rpc struct { + // Context to be used on calls to Transport.CallResult() and Transport.Call() + Context context.Context + Transport transport.RpcTransport +} - self.ctx, self.cancel = context.WithCancel(context.Background()) - self.cmd = exec.CommandContext(self.ctx, self.Cmd) - if self.AccountsDir != "" { - self.cmd.Env = append(os.Environ(), "DC_ACCOUNTS_PATH="+self.AccountsDir) - } - self.cmd.Stderr = self.Stderr - self.stdin, _ = self.cmd.StdinPipe() - stdout, _ := self.cmd.StdoutPipe() - if err := self.cmd.Start(); err != nil { - self.cancel() - return err - } +// --------------------------------------------- +// Misc top level functions +// --------------------------------------------- - self.events = make(map[AccountId]chan Event) - options := jrpc2.ClientOptions{OnNotify: self.onNotify} - self.client = jrpc2.NewClient(channel.Line(stdout, self.stdin), &options) - return nil +// Check if an email address is valid. +func (self *Rpc) CheckEmailValidity(email string) (bool, error) { + var valid bool + err := self.Transport.CallResult(self.Context, &valid, "check_email_validity", email) + return valid, err } -func (self *RpcIO) Stop() { - self.mu.Lock() - defer self.mu.Unlock() +// Get general system info. +func (self *Rpc) GetSystemInfo() (map[string]string, error) { + var info map[string]string + err := self.Transport.CallResult(self.Context, &info, "get_system_info") + return info, err +} - if self.ctx == nil { - return - } - select { - case <-self.ctx.Done(): - return - default: +// Get the next event. +func (self *Rpc) GetNextEvent() (AccountId, Event, error) { + var event _Event + err := self.Transport.CallResult(self.Context, &event, "get_next_event") + if err != nil { + return 0, nil, err } + return event.ContextId, event.Event.ToEvent(), nil +} - self.stdin.Close() - self.cancel() - self.cmd.Process.Wait() //nolint:errcheck - for _, channel := range self.events { - close(channel) - } +// --------------------------------------------- +// Account Management +// --------------------------------------------- + +// Create a new account. +func (self *Rpc) AddAccount() (AccountId, error) { + var id AccountId + err := self.Transport.CallResult(self.Context, &id, "add_account") + return id, err } -func (self *RpcIO) GetEventChannel(accountId AccountId) <-chan Event { - return self.getEventChannel(accountId) +// Remove an account. +func (self *Rpc) RemoveAccount(accountId AccountId) error { + return self.Transport.Call(self.Context, "remove_account", accountId) } -func (self *RpcIO) Call(method string, params ...any) error { - _, err := self.client.Call(self.ctx, method, params) - return err +// Return all available accounts. +func (self *Rpc) GetAllAccountIds() ([]AccountId, error) { + var ids []AccountId + err := self.Transport.CallResult(self.Context, &ids, "get_all_account_ids") + return ids, err } -func (self *RpcIO) CallResult(result any, method string, params ...any) error { - return self.client.CallResult(self.ctx, method, params, &result) +// Select account id for internally selected state. +func (self *Rpc) SelectAccount(accountId AccountId) error { + return self.Transport.Call(self.Context, "select_account", accountId) } -func (self *RpcIO) getEventChannel(accountId AccountId) chan Event { - self.mu.Lock() - defer self.mu.Unlock() +// Get the selected account id of the internal state. +func (self *Rpc) GetSelectedAccountId() (option.Option[AccountId], error) { + var id option.Option[AccountId] + err := self.Transport.CallResult(self.Context, &id, "get_selected_account_id") + return id, err +} - channel, ok := self.events[accountId] - if !ok { - channel = make(chan Event, 1000) - self.events[accountId] = channel - } - return channel -} - -func (self *RpcIO) onNotify(req *jrpc2.Request) { - if req.Method() == "event" { - var params _Params - err := req.UnmarshalParams(¶ms) - if err != nil { - return - } - channel := self.getEventChannel(AccountId(params.ContextId)) - event := toEvent(params.Event) - select { - case <-self.ctx.Done(): - return - default: - } - select { - case channel <- event: - default: - } +// TODO: get_all_accounts + +// Start the I/O of all accounts. +func (self *Rpc) StartIoForAllAccounts() error { + return self.Transport.Call(self.Context, "start_io_for_all_accounts") +} + +// Stop the I/O of all accounts. +func (self *Rpc) StopIoForAllAccounts() error { + return self.Transport.Call(self.Context, "stop_io_for_all_accounts") +} + +// --------------------------------------------- +// Methods that work on individual accounts +// --------------------------------------------- + +// Start the account I/O. +func (self *Rpc) StartIo(accountId AccountId) error { + return self.Transport.Call(self.Context, "start_io", accountId) +} + +// Stop the account I/O. +func (self *Rpc) StopIo(accountId AccountId) error { + return self.Transport.Call(self.Context, "stop_io", accountId) +} + +// TODO: get_account_info + +// Get the combined filesize of an account in bytes. +func (self *Rpc) GetAccountFileSize(accountId AccountId) (uint64, error) { + var size uint64 + err := self.Transport.CallResult(self.Context, &size, "get_account_file_size", accountId) + return size, err +} + +// TODO: get_provider_info + +// Checks if the account is already configured. +func (self *Rpc) IsConfigured(accountId AccountId) (bool, error) { + var configured bool + err := self.Transport.CallResult(self.Context, &configured, "is_configured", accountId) + return configured, err +} + +// Get system info for an account. +func (self *Rpc) GetInfo(accountId AccountId) (map[string]string, error) { + var info map[string]string + err := self.Transport.CallResult(self.Context, &info, "get_info", accountId) + return info, err +} + +// Set account configuration value. +func (self *Rpc) SetConfig(accountId AccountId, key string, value option.Option[string]) error { + return self.Transport.Call(self.Context, "set_config", accountId, key, value) +} + +// Tweak several account configuration values in a batch. +func (self *Rpc) BatchSetConfig(accountId AccountId, config map[string]option.Option[string]) error { + return self.Transport.Call(self.Context, "batch_set_config", accountId, config) +} + +// TODO: set_config_from_qr +// TODO: check_qr + +// Get custom UI-specific configuration value set with SetUiConfig(). +func (self *Rpc) GetConfig(accountId AccountId, key string) (option.Option[string], error) { + var value option.Option[string] + err := self.Transport.CallResult(self.Context, &value, "get_config", accountId, key) + return value, err +} + +// Get a batch of account configuration values. +func (self *Rpc) BatchGetConfig(accountId AccountId, keys []string) (map[string]option.Option[string], error) { + var values map[string]option.Option[string] + err := self.Transport.CallResult(self.Context, &values, "batch_get_config", accountId, keys) + return values, err +} + +// Set stock strings. +func (self *Rpc) SetStockStrings(translations map[uint]string) error { + return self.Transport.Call(self.Context, "set_stock_strings", translations) +} + +// Configures an account with the currently set parameters. +// Setup the credential config before calling this. +func (self *Rpc) Configure(accountId AccountId) error { + return self.Transport.Call(self.Context, "configure", accountId) +} + +// Signal an ongoing process to stop. +func (self *Rpc) StopOngoingProcess(accountId AccountId) error { + return self.Transport.Call(self.Context, "stop_ongoing_process", accountId) +} + +// Export public and private keys to the specified directory. +// Note that the account does not have to be started. +func (self *Rpc) ExportSelfKeys(accountId AccountId, path string) error { + return self.Transport.Call(self.Context, "export_self_keys", accountId, path, nil) +} + +// Import private keys found in the specified directory. +func (self *Rpc) ImportSelfKeys(accountId AccountId, path string) error { + return self.Transport.Call(self.Context, "import_self_keys", accountId, path, nil) +} + +// Returns the message IDs of all fresh messages of any chat. +// Typically used for implementing notification summaries +// or badge counters e.g. on the app icon. +// The list is already sorted and starts with the most recent fresh message. +// +// Messages belonging to muted chats or to the contact requests are not returned; +// these messages should not be notified +// and also badge counters should not include these messages. +// +// To get the number of fresh messages for a single chat, muted or not, +// use GetFreshMsgCnt(). +func (self *Rpc) GetFreshMsgs(accountId AccountId) ([]MsgId, error) { + var ids []MsgId + err := self.Transport.CallResult(self.Context, &ids, "get_fresh_msgs", accountId) + return ids, err +} + +// Get the number of fresh messages in a chat. +// Typically used to implement a badge with a number in the chatlist. +// +// If the specified chat is muted, +// the UI should show the badge counter "less obtrusive", +// e.g. using "gray" instead of "red" color. +func (self *Rpc) GetFreshMsgCnt(accountId AccountId, chatId ChatId) (uint, error) { + var count uint + err := self.Transport.CallResult(self.Context, &count, "get_fresh_msg_cnt", accountId, chatId) + return count, err +} + +// Gets messages to be processed by the bot and returns their IDs. +// +// Only messages with database ID higher than last_msg_id config value +// are returned. After processing the messages, the bot should +// update last_msg_id by calling MarkseenMsgs() +// or manually updating the value to avoid getting already +// processed messages. +func (self *Rpc) GetNextMsgs(accountId AccountId) ([]MsgId, error) { + var ids []MsgId + err := self.Transport.CallResult(self.Context, &ids, "get_next_msgs", accountId) + return ids, err +} + +// Waits for messages to be processed by the bot and returns their IDs. +// +// This function is similar to GetNextMsgs(), +// but waits for internal new message notification before returning. +// New message notification is sent when new message is added to the database, +// on initialization, when I/O is started and when I/O is stopped. +// This allows bots to use WaitNextMsgs() in a loop to process +// old messages after initialization and during the bot runtime. +// To shutdown the bot, stopping I/O can be used to interrupt +// pending or next WaitNextMsgs() call. +func (self *Rpc) WaitNextMsgs(accountId AccountId) ([]MsgId, error) { + var ids []MsgId + err := self.Transport.CallResult(self.Context, &ids, "wait_next_msgs", accountId) + return ids, err +} + +// Estimate the number of messages that will be deleted +// by the SetConfig()-options `delete_device_after` or `delete_server_after`. +// This is typically used to show the estimated impact to the user +// before actually enabling deletion of old messages. +func (self *Rpc) EstimateAutoDeletionCount(accountId AccountId, fromServer bool, seconds int64) (uint, error) { + var count uint + err := self.Transport.CallResult(self.Context, &count, "estimate_auto_deletion_count", accountId, fromServer, seconds) + return count, err +} + +// --------------------------------------------- +// autocrypt +// --------------------------------------------- + +// Start the AutoCrypt key transfer process. +func (self *Rpc) InitiateAutocryptKeyTransfer(accountId AccountId) (string, error) { + var result string + err := self.Transport.CallResult(self.Context, &result, "initiate_autocrypt_key_transfer", accountId) + return result, err +} + +// Continue the AutoCrypt key transfer process. +func (self *Rpc) ContinueAutocryptKeyTransfer(accountId AccountId, msgId MsgId, setupCode string) error { + return self.Transport.Call(self.Context, "continue_autocrypt_key_transfer", accountId, msgId, setupCode) +} + +// --------------------------------------------- +// chat list +// --------------------------------------------- + +func (self *Rpc) GetChatlistEntries(accountId AccountId, listFlags option.Option[uint], query option.Option[string], contactId option.Option[ContactId]) ([]ChatId, error) { + var entries []ChatId + err := self.Transport.CallResult(self.Context, &entries, "get_chatlist_entries", accountId, listFlags, query, contactId) + return entries, err +} + +func (self *Rpc) GetChatlistItemsByEntries(accountId AccountId, entries []ChatId) (map[ChatId]*ChatListItem, error) { + var itemsMap map[ChatId]*ChatListItem + err := self.Transport.CallResult(self.Context, &itemsMap, "get_chatlist_items_by_entries", accountId, entries) + return itemsMap, err +} + +// --------------------------------------------- +// chat +// --------------------------------------------- + +func (self *Rpc) GetFullChatById(accountId AccountId, chatId ChatId) (*FullChatSnapshot, error) { + var result FullChatSnapshot + err := self.Transport.CallResult(self.Context, &result, "get_full_chat_by_id", accountId, chatId) + if err != nil { + return nil, err } + return &result, nil } -func toEvent(ev *_Event) Event { - var event Event - switch ev.Type { - case eventTypeInfo: - event = EventInfo{Msg: ev.Msg} - case eventTypeSmtpConnected: - event = EventSmtpConnected{Msg: ev.Msg} - case eventTypeImapConnected: - event = EventImapConnected{Msg: ev.Msg} - case eventTypeSmtpMessageSent: - event = EventSmtpMessageSent{Msg: ev.Msg} - case eventTypeImapMessageDeleted: - event = EventImapMessageDeleted{Msg: ev.Msg} - case eventTypeImapMessageMoved: - event = EventImapMessageMoved{Msg: ev.Msg} - case eventTypeImapInboxIdle: - event = EventImapInboxIdle{} - case eventTypeNewBlobFile: - event = EventNewBlobFile{File: ev.File} - case eventTypeDeletedBlobFile: - event = EventDeletedBlobFile{File: ev.File} - case eventTypeWarning: - event = EventWarning{Msg: ev.Msg} - case eventTypeError: - event = EventError{Msg: ev.Msg} - case eventTypeErrorSelfNotInGroup: - event = EventErrorSelfNotInGroup{Msg: ev.Msg} - case eventTypeMsgsChanged: - event = EventMsgsChanged{ChatId: ev.ChatId, MsgId: ev.MsgId} - case eventTypeReactionsChanged: - event = EventReactionsChanged{ - ChatId: ev.ChatId, - MsgId: ev.MsgId, - ContactId: ev.ContactId, - } - case eventTypeIncomingMsg: - event = EventIncomingMsg{ChatId: ev.ChatId, MsgId: ev.MsgId} - case eventTypeIncomingMsgBunch: - event = EventIncomingMsgBunch{MsgIds: ev.MsgIds} - case eventTypeMsgsNoticed: - event = EventMsgsNoticed{ChatId: ev.ChatId} - case eventTypeMsgDelivered: - event = EventMsgDelivered{ChatId: ev.ChatId, MsgId: ev.MsgId} - case eventTypeMsgFailed: - event = EventMsgFailed{ChatId: ev.ChatId, MsgId: ev.MsgId} - case eventTypeMsgRead: - event = EventMsgRead{ChatId: ev.ChatId, MsgId: ev.MsgId} - case eventTypeChatModified: - event = EventChatModified{ChatId: ev.ChatId} - case eventTypeChatEphemeralTimerModified: - event = EventChatEphemeralTimerModified{ - ChatId: ev.ChatId, - Timer: ev.Timer, - } - case eventTypeContactsChanged: - event = EventContactsChanged{ContactId: ev.ContactId} - case eventTypeLocationChanged: - event = EventLocationChanged{ContactId: ev.ContactId} - case eventTypeConfigureProgress: - event = EventConfigureProgress{Progress: ev.Progress, Comment: ev.Comment} - case eventTypeImexProgress: - event = EventImexProgress{Progress: ev.Progress} - case eventTypeImexFileWritten: - event = EventImexFileWritten{Path: ev.Path} - case eventTypeSecurejoinInviterProgress: - event = EventSecurejoinInviterProgress{ - ContactId: ev.ContactId, - Progress: ev.Progress, - } - case eventTypeSecurejoinJoinerProgress: - event = EventSecurejoinJoinerProgress{ - ContactId: ev.ContactId, - Progress: ev.Progress, - } - case eventTypeConnectivityChanged: - event = EventConnectivityChanged{} - case eventTypeSelfavatarChanged: - event = EventSelfavatarChanged{} - case eventTypeWebxdcStatusUpdate: - event = EventWebxdcStatusUpdate{ - MsgId: ev.MsgId, - StatusUpdateSerial: ev.StatusUpdateSerial, - } - case eventTypeWebxdcInstanceDeleted: - event = EventWebxdcInstanceDeleted{MsgId: ev.MsgId} +// get basic info about a chat, +// use GetFullChatById() instead if you need more information +func (self *Rpc) GetBasicChatInfo(accountId AccountId, chatId ChatId) (*BasicChatSnapshot, error) { + var result BasicChatSnapshot + err := self.Transport.CallResult(self.Context, &result, "get_basic_chat_info", accountId, chatId) + if err != nil { + return nil, err } - return event + return &result, nil +} + +func (self *Rpc) AcceptChat(accountId AccountId, chatId ChatId) error { + return self.Transport.Call(self.Context, "accept_chat", accountId, chatId) +} + +func (self *Rpc) BlockChat(accountId AccountId, chatId ChatId) error { + return self.Transport.Call(self.Context, "block_chat", accountId, chatId) +} + +// Delete a chat. +// +// Messages are deleted from the device and the chat database entry is deleted. +// After that, the event #DC_EVENT_MSGS_CHANGED is posted. +// +// Things that are _not done_ implicitly: +// +// - Messages are **not deleted from the server**. +// - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request +// and the user may create the chat again. +// - **Groups are not left** - this would +// be unexpected as (1) deleting a normal chat also does not prevent new mails +// from arriving, (2) leaving a group requires sending a message to +// all group members - especially for groups not used for a longer time, this is +// really unexpected when deletion results in contacting all members again, +// (3) only leaving groups is also a valid usecase. +// +// To leave a chat explicitly, use leave_group() +func (self *Rpc) DeleteChat(accountId AccountId, chatId ChatId) error { + return self.Transport.Call(self.Context, "delete_chat", accountId, chatId) +} + +// Get encryption info for this chat. +// Get a multi-line encryption info, containing encryption preferences of all members. +// Can be used to find out why messages sent to group are not encrypted. +// +// returns Multi-line text +func (self *Rpc) GetChatEncryptionInfo(accountId AccountId, chatId ChatId) (string, error) { + var data string + err := self.Transport.CallResult(self.Context, &data, "get_chat_encryption_info", accountId, chatId) + return data, err +} + +// Get Join-Group QR code text and SVG data. +func (self *Rpc) GetChatSecurejoinQrCodeSvg(accountId AccountId, chatId option.Option[ChatId]) (string, string, error) { + var data [2]string + err := self.Transport.CallResult(self.Context, &data, "get_chat_securejoin_qr_code_svg", accountId, chatId) + return data[0], data[1], err +} + +// Continue a Setup-Contact or Verified-Group-Invite protocol started on another device. +func (self *Rpc) SecureJoin(accountId AccountId, qrdata string) (ChatId, error) { + var id ChatId + err := self.Transport.CallResult(self.Context, &id, "secure_join", accountId, qrdata) + return id, err +} + +func (self *Rpc) LeaveGroup(accountId AccountId, chatId ChatId) error { + return self.Transport.Call(self.Context, "leave_group", accountId, chatId) +} + +// Remove a member from a group. +func (self *Rpc) RemoveContactFromChat(accountId AccountId, chatId ChatId, contactId ContactId) error { + return self.Transport.Call(self.Context, "remove_contact_from_chat", accountId, chatId, contactId) +} + +// Add a member to a group. +func (self *Rpc) AddContactToChat(accountId AccountId, chatId ChatId, contactId ContactId) error { + return self.Transport.Call(self.Context, "add_contact_to_chat", accountId, chatId, contactId) +} + +// Get the contact IDs belonging to a chat. +// +// - for normal chats, the function always returns exactly one contact, +// DC_CONTACT_ID_SELF is returned only for SELF-chats. +// +// - for group chats all members are returned, DC_CONTACT_ID_SELF is returned +// explicitly as it may happen that oneself gets removed from a still existing +// group +// +// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included +// +// - for mailing lists, the behavior is not documented currently, we will decide on that later. +// for now, the UI should not show the list for mailing lists. +// (we do not know all members and there is not always a global mailing list address, +// so we could return only SELF or the known members; this is not decided yet) +func (self *Rpc) GetChatContacts(accountId AccountId, chatId ChatId) ([]ContactId, error) { + var ids []ContactId + err := self.Transport.CallResult(self.Context, &ids, "get_chat_contacts", accountId, chatId) + return ids, err +} + +// Create a new group chat. +// After creation, the group has only self-contact as member and is in unpromoted state. +func (self *Rpc) CreateGroupChat(accountId AccountId, name string, protected bool) (ChatId, error) { + var id ChatId + err := self.Transport.CallResult(self.Context, &id, "create_group_chat", accountId, name, protected) + return id, err +} + +// Create a new broadcast list. +func (self *Rpc) CreateBroadcastList(accountId AccountId) (ChatId, error) { + var id ChatId + err := self.Transport.CallResult(self.Context, &id, "create_broadcast_list", accountId) + return id, err +} + +// Set group name. +func (self *Rpc) SetChatName(accountId AccountId, chatId ChatId, name string) error { + return self.Transport.Call(self.Context, "set_chat_name", accountId, chatId, name) +} + +// Set group profile image. +// +// If the group is already _promoted_ (any message was sent to the group), +// all group members are informed by a special status message that is sent automatically by this function. +// +// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. +// +// To find out the profile image of a chat, use dc_chat_get_profile_image() +// +// @param image_path Full path of the image to use as the group image. The image will immediately be copied to the +// +// `blobdir`; the original image will not be needed anymore. +// If you pass null here, the group image is deleted (for promoted groups, all members are informed about +// this change anyway). +func (self *Rpc) SetChatProfileImage(accountId AccountId, chatId ChatId, path option.Option[string]) error { + return self.Transport.Call(self.Context, "set_chat_profile_image", accountId, chatId, path) +} + +func (self *Rpc) SetChatVisibility(accountId AccountId, chatId ChatId, visibility ChatVisibility) error { + return self.Transport.Call(self.Context, "set_chat_visibility", accountId, chatId, visibility) +} + +func (self *Rpc) SetChatEphemeralTimer(accountId AccountId, chatId ChatId, timer uint) error { + return self.Transport.Call(self.Context, "set_chat_ephemeral_timer", accountId, chatId, timer) +} + +func (self *Rpc) GetChatEphemeralTimer(accountId AccountId, chatId ChatId) (uint, error) { + var timer uint + err := self.Transport.CallResult(self.Context, &timer, "get_chat_ephemeral_timer", accountId, chatId) + return timer, err +} + +// for now only text messages, because we only used text messages in desktop thusfar +func (self *Rpc) AddDeviceMessage(accountId AccountId, label string, text string) (MsgId, error) { + var id MsgId + err := self.Transport.CallResult(self.Context, &id, "add_device_message", accountId, label, text) + return id, err +} + +// Mark all messages in a chat as _noticed_. +// _Noticed_ messages are no longer _fresh_ and do not count as being unseen +// but are still waiting for being marked as "seen" using markseen_msgs() +// (IMAP/MDNs is not done for noticed messages). +// +// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED. +// See also markseen_msgs(). +func (self *Rpc) MarknoticedChat(accountId AccountId, chatId ChatId) error { + return self.Transport.Call(self.Context, "marknoticed_chat", accountId, chatId) +} + +func (self *Rpc) GetFirstUnreadMessageOfChat(accountId AccountId, chatId ChatId) (option.Option[MsgId], error) { + var id option.Option[MsgId] + err := self.Transport.CallResult(self.Context, &id, "get_first_unread_message_of_chat", accountId, chatId) + return id, err +} + +// TODO: set_chat_mute_duration +// TODO: is_chat_muted + +// --------------------------------------------- +// message list +// --------------------------------------------- + +// Mark messages as presented to the user. +// Typically, UIs call this function on scrolling through the message list, +// when the messages are presented at least for a little moment. +// The concrete action depends on the type of the chat and on the users settings +// (dc_msgs_presented() may be a better name therefore, but well. :) +// +// - For normal chats, the IMAP state is updated, MDN is sent +// (if set_config()-options `mdns_enabled` is set) +// and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions. +// +// - For contact requests, no IMAP or MDNs is done +// and the internal state is not changed therefore. +// See also marknoticed_chat(). +// +// Moreover, timer is started for incoming ephemeral messages. +// This also happens for contact requests chats. +// +// This function updates `last_msg_id` configuration value +// to the maximum of the current value and IDs passed to this function. +// Bots which mark messages as seen can rely on this side effect +// to avoid updating `last_msg_id` value manually. +// +// One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat. +func (self *Rpc) MarkseenMsgs(accountId AccountId, msgIds []MsgId) error { + return self.Transport.Call(self.Context, "markseen_msgs", accountId, msgIds) +} + +func (self *Rpc) GetMessageIds(accountId AccountId, chatId ChatId, infoOnly, addDaymarker bool) ([]MsgId, error) { + var ids []MsgId + err := self.Transport.CallResult(self.Context, &ids, "get_message_ids", accountId, chatId, infoOnly, addDaymarker) + return ids, err +} + +// TODO: get_message_list_items + +// Return map of this account configuration parameters. +func (self *Rpc) GetMessage(accountId AccountId, msgId MsgId) (*MsgSnapshot, error) { + var snapshot MsgSnapshot + err := self.Transport.CallResult(self.Context, &snapshot, "get_message", accountId, msgId) + return &snapshot, err +} + +// Get the HTML part of this message. +func (self *Rpc) GetMessageHtml(accountId AccountId, msgId MsgId) (option.Option[string], error) { + var html option.Option[string] + err := self.Transport.CallResult(self.Context, &html, "get_message_html", accountId, msgId) + return html, err +} + +// TODO: get_messages +// TODO: get_message_notification_info + +// Delete messages. The messages are deleted on the current device and +// on the IMAP server. +func (self *Rpc) DeleteMessages(accountId AccountId, msgIds []MsgId) error { + return self.Transport.Call(self.Context, "delete_messages", accountId, msgIds) +} + +// Get an informational text for a single message. The text is multiline and may +// contain e.g. the raw text of the message. +// +// The max. text returned is typically longer (about 100000 characters) than the +// max. text returned by dc_msg_get_text() (about 30000 characters). +func (self *Rpc) GetMessageInfo(accountId AccountId, msgId MsgId) (string, error) { + var info string + err := self.Transport.CallResult(self.Context, &info, "get_message_info", accountId, msgId) + return info, err +} + +// Asks the core to start downloading a message fully. +// This function is typically called when the user hits the "Download" button +// that is shown by the UI in case `download_state` is `'Available'` or `'Failure'` +// +// On success, the @ref DC_MSG "view type of the message" may change +// or the message may be replaced completely by one or more messages with other message IDs. +// That may happen e.g. in cases where the message was encrypted +// and the type could not be determined without fully downloading. +// Downloaded content can be accessed as usual after download. +// +// To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted. +func (self *Rpc) DownloadFullMessage(accountId AccountId, msgId MsgId) error { + return self.Transport.Call(self.Context, "download_full_message", accountId, msgId) +} + +// Search messages containing the given query string. +// Searching can be done globally (chat_id=None) or in a specified chat only (chat_id set). +// +// Global search results are typically displayed using dc_msg_get_summary(), chat +// search results may just highlight the corresponding messages and present a +// prev/next button. +// +// For the global search, the result is limited to 1000 messages, +// this allows an incremental search done fast. +// So, when getting exactly 1000 messages, the result actually may be truncated; +// the UIs may display sth. like "1000+ messages found" in this case. +// The chat search (if chat_id is set) is not limited. +func (self *Rpc) SearchMessages(accountId AccountId, query string, chatId option.Option[ChatId]) ([]MsgId, error) { + var msgIds []MsgId + err := self.Transport.CallResult(self.Context, &msgIds, "search_messages", accountId, query, chatId) + return msgIds, err +} + +func (self *Rpc) MessageIdsToSearchResults(accountId AccountId, msgIds []MsgId) (map[MsgId]*MsgSearchResult, error) { + var results map[MsgId]*MsgSearchResult + err := self.Transport.CallResult(self.Context, &results, "message_ids_to_search_results", accountId, msgIds) + return results, err +} + +// --------------------------------------------- +// contact +// --------------------------------------------- + +// Get the properties of a single contact by ID. +func (self *Rpc) GetContact(accountId AccountId, contactId ContactId) (*ContactSnapshot, error) { + var snapshot ContactSnapshot + err := self.Transport.CallResult(self.Context, &snapshot, "get_contact", accountId, contactId) + return &snapshot, err +} + +// Add a single contact as a result of an explicit user action. +// +// Returns contact id of the created or existing contact +func (self *Rpc) CreateContact(accountId AccountId, email string, name string) (ContactId, error) { + var id ContactId + err := self.Transport.CallResult(self.Context, &id, "create_contact", accountId, email, name) + return id, err +} + +// Returns contact id of the created or existing DM chat with that contact +func (self *Rpc) CreateChatByContactId(accountId AccountId, contactId ContactId) (ChatId, error) { + var id ChatId + err := self.Transport.CallResult(self.Context, &id, "create_chat_by_contact_id", accountId, contactId) + return id, err +} + +func (self *Rpc) BlockContact(accountId AccountId, contactId ContactId) error { + return self.Transport.Call(self.Context, "block_contact", accountId, contactId) +} + +func (self *Rpc) UnblockContact(accountId AccountId, contactId ContactId) error { + return self.Transport.Call(self.Context, "unblock_contact", accountId, contactId) +} + +func (self *Rpc) GetBlockedContacts(accountId AccountId) ([]*ContactSnapshot, error) { + var contacts []*ContactSnapshot + err := self.Transport.CallResult(self.Context, &contacts, "get_blocked_contacts", accountId) + return contacts, err +} + +func (self *Rpc) GetContactIds(accountId AccountId, listFlags uint, query option.Option[string]) ([]ContactId, error) { + var ids []ContactId + err := self.Transport.CallResult(self.Context, &ids, "get_contact_ids", accountId, listFlags, query) + return ids, err +} + +// TODO: get_contacts +// TODO: get_contacts_by_ids + +func (self *Rpc) DeleteContact(accountId AccountId, contactId ContactId) error { + return self.Transport.Call(self.Context, "delete_contact", accountId, contactId) +} + +func (self *Rpc) ChangeContactName(accountId AccountId, contactId ContactId, name string) error { + return self.Transport.Call(self.Context, "change_contact_name", accountId, contactId, name) +} + +// Get encryption info for a contact. +// Get a multi-line encryption info, containing your fingerprint and the +// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification. +func (self *Rpc) GetContactEncryptionInfo(accountId AccountId, contactId ContactId) (string, error) { + var data string + err := self.Transport.CallResult(self.Context, &data, "get_contact_encryption_info", accountId, contactId) + return data, err +} + +// Check if an e-mail address belongs to a known and unblocked contact. +// To get a list of all known and unblocked contacts, use contacts_get_contacts(). +// +// To validate an e-mail address independently of the contact database +// use check_email_validity(). +func (self *Rpc) LookupContactIdByAddr(accountId AccountId, addr string) (option.Option[ContactId], error) { + var id option.Option[ContactId] + err := self.Transport.CallResult(self.Context, &id, "lookup_contact_id_by_addr", accountId, addr) + return id, err +} + +// --------------------------------------------- +// chat +// --------------------------------------------- + +// Returns all message IDs of the given types in a chat. +// Typically used to show a gallery. +// +// The list is already sorted and starts with the oldest message. +// Clients should not try to re-sort the list as this would be an expensive action +// and would result in inconsistencies between clients. +// +// Setting `chat_id` to `None` (`null` in typescript) means get messages with media +// from any chat of the currently used account. +func (self *Rpc) GetChatMedia(accountId AccountId, chatId ChatId, messageType MsgType, orMessageType2 option.Option[MsgType], orMessageType3 option.Option[MsgType]) ([]MsgId, error) { + var ids []MsgId + err := self.Transport.CallResult(self.Context, &ids, "get_chat_media", accountId, chatId, messageType, orMessageType2, orMessageType3) + return ids, err +} + +// TODO: get_neighboring_chat_media + +// --------------------------------------------- +// backup +// --------------------------------------------- + +// Export account backup. +func (self *Rpc) ExportBackup(accountId AccountId, destination string, passphrase option.Option[string]) error { + return self.Transport.Call(self.Context, "export_backup", accountId, destination, passphrase) +} + +// Import account backup. +func (self *Rpc) ImportBackup(accountId AccountId, path string, passphrase option.Option[string]) error { + return self.Transport.Call(self.Context, "import_backup", accountId, path, passphrase) +} + +// Offers a backup for remote devices to retrieve. +// +// Can be cancelled by stopping the ongoing process. Success or failure can be tracked +// via the `ImexProgress` event which should either reach `1000` for success or `0` for +// failure. +// +// This **stops IO** while it is running. +// +// Returns once a remote device has retrieved the backup, or is cancelled. +func (self *Rpc) ProvideBackup(accountId AccountId) error { + return self.Transport.Call(self.Context, "provide_backup", accountId) +} + +// Returns the text of the QR code for the running [`CommandApi::provide_backup`]. +// +// This QR code text can be used in [`CommandApi::get_backup`] on a second device to +// retrieve the backup and setup this second device. +// +// This call will fail if there is currently no concurrent call to +// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet +// ready. +func (self *Rpc) GetBackupQr(accountId AccountId) (string, error) { + var result string + err := self.Transport.CallResult(self.Context, &result, "get_backup_qr", accountId) + return result, err +} + +// Returns the rendered QR code for the running [`CommandApi::provide_backup`]. +// +// This QR code can be used in [`CommandApi::get_backup`] on a second device to +// retrieve the backup and setup this second device. +// +// This call will fail if there is currently no concurrent call to +// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet +// ready. +// +// Returns the QR code rendered as an SVG image. +func (self *Rpc) GetBackupQrSvg(accountId AccountId) (string, error) { + var result string + err := self.Transport.CallResult(self.Context, &result, "get_backup_qr_svg", accountId) + return result, err +} + +// Gets a backup from a remote provider. +// +// This retrieves the backup from a remote device over the network and imports it into +// the current device. +// +// Can be cancelled by stopping the ongoing process. +func (self *Rpc) GetBackup(accountId AccountId, qrText string) error { + return self.Transport.Call(self.Context, "get_backup", accountId, qrText) +} + +// --------------------------------------------- +// connectivity +// --------------------------------------------- + +// Indicate that the network likely has come back. +// or just that the network conditions might have changed +func (self *Rpc) MaybeNetwork() error { + return self.Transport.Call(self.Context, "maybe_network") +} + +// Get the current connectivity, i.e. whether the device is connected to the IMAP server. +// One of: +// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot +// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot +// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel +// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot +// +// We don't use exact values but ranges here so that we can split up +// states into multiple states in the future. +// +// Meant as a rough overview that can be shown +// e.g. in the title of the main screen. +// +// If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted. +func (self *Rpc) GetConnectivity(accountId AccountId) (uint, error) { + var info uint + err := self.Transport.CallResult(self.Context, &info, "get_connectivity", accountId) + return info, err +} + +// Get an overview of the current connectivity, and possibly more statistics. +// Meant to give the user more insight about the current status than +// the basic connectivity info returned by get_connectivity(); show this +// e.g., if the user taps on said basic connectivity info. +// +// If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted. +// +// This comes as an HTML from the core so that we can easily improve it +// and the improvement instantly reaches all UIs. +func (self *Rpc) GetConnectivityHtml(accountId AccountId) (string, error) { + var html string + err := self.Transport.CallResult(self.Context, &html, "get_connectivity_html", accountId) + return html, err +} + +// --------------------------------------------- +// locations +// --------------------------------------------- + +// TODO: get_locations + +// --------------------------------------------- +// webxdc +// --------------------------------------------- + +func (self *Rpc) SendWebxdcStatusUpdate(accountId AccountId, msgId MsgId, update string, description string) error { + return self.Transport.Call(self.Context, "send_webxdc_status_update", accountId, msgId, update, description) +} + +func (self *Rpc) GetWebxdcStatusUpdates(accountId AccountId, msgId MsgId, lastKnownSerial uint) (string, error) { + var data string + err := self.Transport.CallResult(self.Context, &data, "get_webxdc_status_updates", accountId, msgId, lastKnownSerial) + return data, err +} + +// Get info from this webxdc message. +func (self *Rpc) GetWebxdcInfo(accountId AccountId, msgId MsgId) (*WebxdcMsgInfo, error) { + var info WebxdcMsgInfo + err := self.Transport.CallResult(self.Context, &info, "get_webxdc_info", accountId, msgId) + return &info, err +} + +// Get blob encoded as base64 from a webxdc message +// +// path is the path of the file within webxdc archive +func (self *Rpc) GetWebxdcBlob(accountId AccountId, msgId MsgId, path string) (string, error) { + var data string + err := self.Transport.CallResult(self.Context, &data, "get_webxdc_blob", accountId, msgId, path) + return data, err +} + +// TODO: get_http_response + +// Forward messages to another chat. +// +// All types of messages can be forwarded, +// however, they will be flagged as such (dc_msg_is_forwarded() is set). +// +// Original sender, info-state and webxdc updates are not forwarded on purpose. +func (self *Rpc) ForwardMessages(accountId AccountId, msgIds []MsgId, chatId ChatId) error { + return self.Transport.Call(self.Context, "forward_messages", accountId, msgIds, chatId) +} + +// Resend messages and make information available for newly added chat members. +// Resending sends out the original message, however, recipients and webxdc-status may differ. +// Clients that already have the original message can still ignore the resent message as +// they have tracked the state by dedicated updates. +// +// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF. +// +// msgIds all message IDs that should be resend. All messages must belong to the same chat. +func (self *Rpc) ResendMessages(accountId AccountId, msgIds []MsgId) error { + return self.Transport.Call(self.Context, "resend_messages", accountId, msgIds) +} + +func (self *Rpc) SendSticker(accountId AccountId, chatId ChatId, path string) (MsgId, error) { + var id MsgId + err := self.Transport.CallResult(self.Context, &id, "send_sticker", accountId, chatId, path) + return id, err +} + +// Send a reaction to message. +// +// Reaction is a string of emojis separated by spaces. Reaction to a +// single message can be sent multiple times. The last reaction +// received overrides all previously received reactions. It is +// possible to remove all reactions by sending an empty string. +func (self *Rpc) SendReaction(accountId AccountId, msgId MsgId, reaction ...string) (MsgId, error) { + var id MsgId + err := self.Transport.CallResult(self.Context, &id, "send_reaction", accountId, msgId, reaction) + return id, err +} + +// Returns reactions to the message. +func (self *Rpc) GetMessageReactions(accountId AccountId, msgId MsgId) (option.Option[Reactions], error) { + var reactions option.Option[Reactions] + err := self.Transport.CallResult(self.Context, &reactions, "get_message_reactions", accountId, msgId) + return reactions, err +} + +// Send a message and return the resulting Message instance. +func (self *Rpc) SendMsg(accountId AccountId, chatId ChatId, msgData MsgData) (MsgId, error) { + var id MsgId + err := self.Transport.CallResult(self.Context, &id, "send_msg", accountId, chatId, msgData) + return id, err +} + +// Checks if messages can be sent to a given chat. +func (self *Rpc) CanSend(accountId AccountId, chatId ChatId) (bool, error) { + var canSend bool + err := self.Transport.CallResult(self.Context, &canSend, "can_send", accountId, chatId) + return canSend, err +} + +// --------------------------------------------- +// functions for the composer +// the composer is the message input field +// --------------------------------------------- + +func (self *Rpc) RemoveDraft(accountId AccountId, chatId ChatId) error { + return self.Transport.Call(self.Context, "remove_draft", accountId, chatId) +} + +// Get draft for a chat, if any. +func (self *Rpc) GetDraft(accountId AccountId, chatId ChatId) (option.Option[MsgSnapshot], error) { + var msg option.Option[MsgSnapshot] + err := self.Transport.CallResult(self.Context, &msg, "get_draft", accountId, chatId) + return msg, err +} + +func (self *Rpc) SendVideoChatInvitation(accountId AccountId, chatId ChatId) (MsgId, error) { + var id MsgId + err := self.Transport.CallResult(self.Context, &id, "send_videochat_invitation", accountId, chatId) + return id, err +} + +// --------------------------------------------- +// misc prototyping functions +// that might get removed later again +// --------------------------------------------- + +// TODO: misc_get_sticker_folder() +// TODO: misc_save_sticker() +// TODO: misc_get_stickers() + +// Send a text message and return the resulting Message instance. +func (self *Rpc) MiscSendTextMessage(accountId AccountId, chatId ChatId, text string) (MsgId, error) { + var id MsgId + err := self.Transport.CallResult(self.Context, &id, "misc_send_text_message", accountId, chatId, text) + return id, err +} + +// TODO: misc_send_msg() + +// mimics the old desktop call, will get replaced with something better in the composer rewrite, +// the better version should support: +// - changing viewtype to enable/disable compression +// - keeping same message id as long as attachment does not change for webxdc messages +func (self *Rpc) MiscSetDraft(accountId AccountId, chatId ChatId, text option.Option[string], file option.Option[string], quotedMessageId option.Option[MsgId]) error { + return self.Transport.Call(self.Context, "misc_set_draft", accountId, chatId, text, file, quotedMessageId) } diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/timestamp.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/timestamp.go index b5bab45..b2988a4 100644 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/timestamp.go +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/timestamp.go @@ -10,7 +10,7 @@ type Timestamp struct { time.Time } -// UnmarshalJSON is the method that satisfies the Unmarshaller interface. +// UnmarshalJSON parses a Delta Chat timestamp into a Timestamp type. func (self *Timestamp) UnmarshalJSON(b []byte) error { var timestamp int64 err := json.Unmarshal(b, ×tamp) @@ -21,7 +21,7 @@ func (self *Timestamp) UnmarshalJSON(b []byte) error { return nil } -// MarshalJSON turns Timestamp back into an int. -func (self *Timestamp) MarshalJSON() ([]byte, error) { +// MarshalJSON turns Timestamp back into the format expected by Delta Chat core. +func (self Timestamp) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("%d", self.Time.Unix())), nil } diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpcbin.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iobin.go similarity index 83% rename from vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpcbin.go rename to vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iobin.go index dee9899..bd3007c 100644 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpcbin.go +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iobin.go @@ -1,6 +1,6 @@ //go:build !windows // +build !windows -package deltachat +package transport const deltachatRpcServerBin = "deltachat-rpc-server" diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpcbin_windows.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iobin_windows.go similarity index 76% rename from vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpcbin_windows.go rename to vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iobin_windows.go index 8ebe3f2..2798d69 100644 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/rpcbin_windows.go +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iobin_windows.go @@ -1,3 +1,3 @@ -package deltachat +package transport const deltachatRpcServerBin = "deltachat-rpc-server.exe" diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iotransport.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iotransport.go new file mode 100644 index 0000000..e35cba2 --- /dev/null +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/iotransport.go @@ -0,0 +1,83 @@ +package transport + +import ( + "context" + "io" + "os" + "os/exec" + "sync" + + "github.com/creachadair/jrpc2" + "github.com/creachadair/jrpc2/channel" +) + +// Delta Chat RPC transport using external deltachat-rpc-server program +type IOTransport struct { + Stderr io.Writer + AccountsDir string + Cmd string + cmd *exec.Cmd + stdin io.WriteCloser + client *jrpc2.Client + ctx context.Context + cancel context.CancelFunc + mu sync.Mutex +} + +func NewIOTransport() *IOTransport { + return &IOTransport{Cmd: deltachatRpcServerBin, Stderr: os.Stderr} +} + +func (self *IOTransport) Open() error { + self.mu.Lock() + defer self.mu.Unlock() + + if self.ctx != nil && self.ctx.Err() == nil { + return &TransportStartedErr{} + } + + self.ctx, self.cancel = context.WithCancel(context.Background()) + self.cmd = exec.CommandContext(self.ctx, self.Cmd) + if self.AccountsDir != "" { + self.cmd.Env = append(os.Environ(), "DC_ACCOUNTS_PATH="+self.AccountsDir) + } + self.cmd.Stderr = self.Stderr + self.stdin, _ = self.cmd.StdinPipe() + stdout, _ := self.cmd.StdoutPipe() + if err := self.cmd.Start(); err != nil { + self.cancel() + return err + } + + self.client = jrpc2.NewClient(channel.Line(stdout, self.stdin), nil) + return nil +} + +func (self *IOTransport) Close() { + self.mu.Lock() + defer self.mu.Unlock() + + if self.ctx == nil || self.ctx.Err() != nil { + return + } + + self.stdin.Close() + self.cancel() + self.cmd.Process.Wait() //nolint:errcheck +} + +func (self *IOTransport) Call(ctx context.Context, method string, params ...any) error { + _, err := self.client.Call(ctx, method, params) + return err +} + +func (self *IOTransport) CallResult(ctx context.Context, result any, method string, params ...any) error { + return self.client.CallResult(ctx, method, params, &result) +} + +// TransportStartedErr is returned by IOTransport.Open() if the Transport is already started +type TransportStartedErr struct{} + +func (self *TransportStartedErr) Error() string { + return "Transport is already started" +} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/transport.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/transport.go new file mode 100644 index 0000000..99dab68 --- /dev/null +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/transport/transport.go @@ -0,0 +1,13 @@ +package transport + +import ( + "context" +) + +// Delta Chat RPC client's transport. +type RpcTransport interface { + // Request the RPC server to call a function that does not have a return value. + Call(ctx context.Context, method string, params ...any) error + // Request the RPC server to call a function that does have a return value. + CallResult(ctx context.Context, result any, method string, params ...any) error +} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/types.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/types.go new file mode 100644 index 0000000..4d909f2 --- /dev/null +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/types.go @@ -0,0 +1,209 @@ +package deltachat + +import ( + "github.com/deltachat/deltachat-rpc-client-go/deltachat/option" +) + +type AccountId uint64 + +type ContactId uint64 + +type MsgId uint64 + +type ChatId uint64 + +// Values in const.go +type ChatType uint + +type Account struct { + // Configured + Id AccountId + DisplayName option.Option[string] + Addr option.Option[string] + ProfileImage option.Option[string] + Color string + + // Unconfigured + // Id AccountId +} + +// Delta Chat Contact snapshot. +type ContactSnapshot struct { + Address string + Color string + AuthName string + Status string + DisplayName string + Id ContactId + Name string + ProfileImage string + NameAndAddr string + IsBlocked bool + IsVerified bool + VerifierAddr string + VerifierId ContactId + LastSeen Timestamp + WasSeenRecently bool +} + +// Full chat snapshot. +type FullChatSnapshot struct { + Id ChatId + Name string + IsProtected bool + ProfileImage string + Archived bool + ChatType ChatType + IsUnpromoted bool + IsSelfTalk bool + Contacts []*ContactSnapshot + ContactIds []ContactId + Color string + FreshMessageCounter uint + IsContactRequest bool + IsDeviceChat bool + SelfInGroup bool + IsMuted bool + EphemeralTimer uint + CanSend bool + WasSeenRecently bool + MailingListAddress string +} + +// Cheaper version of FullChatSnapshot. +type BasicChatSnapshot struct { + Id ChatId + Name string + IsProtected bool + ProfileImage string + Archived bool + ChatType ChatType + IsUnpromoted bool + IsSelfTalk bool + Color string + IsContactRequest bool + IsDeviceChat bool + IsMuted bool +} + +// Chat list item snapshot +type ChatListItem struct { + Id ChatId + Name string + AvatarPath string + Color string + LastUpdated Timestamp + SummaryText1 string + SummaryText2 string + SummaryStatus uint32 + IsProtected bool + IsGroup bool + FreshMessageCounter uint + IsSelfTalk bool + IsDeviceTalk bool + IsSendingLocation bool + IsSelfInGroup bool + IsArchived bool + IsPinned bool + IsMuted bool + IsContactRequest bool + IsBroadcast bool + DmChatContact ContactId + WasSeenRecently bool + + // ArchiveLink + // FreshMessageCounter uint + + // Error + // Id uint64 + Error string +} + +// Message data provided to Chat.SendMsg() +type MsgData struct { + Text string `json:"text,omitempty"` + Html string `json:"html,omitempty"` + ViewType MsgType `json:"viewtype,omitempty"` + File string `json:"file,omitempty"` + Location *[2]float64 `json:"location,omitempty"` + OverrideSenderName string `json:"overrideSenderName,omitempty"` + QuotedMessageId MsgId `json:"quotedMessageId,omitempty"` +} + +// Message quote. Only the Text property is warrantied to be present, all other fields are optional. +type MsgQuote struct { + Text string + MessageId MsgId + AuthorDisplayName string + AuthorDisplayColor string + OverrideSenderName string + Image string + IsForwarded bool + ViewType MsgType +} + +// Message search result. +type MsgSearchResult struct { + Id MsgId + AuthorProfileImage string + AuthorName string + AuthorColor string + ChatName string + Message string + Timestamp Timestamp +} + +// Message snapshot. +type MsgSnapshot struct { + Id MsgId + ChatId ChatId + FromId ContactId + Quote *MsgQuote + ParentId MsgId + Text string + HasLocation bool + HasHtml bool + ViewType MsgType + State MsgState + Error string + Timestamp Timestamp + SortTimestamp Timestamp + ReceivedTimestamp Timestamp + HasDeviatingTimestamp bool + Subject string + ShowPadlock bool + IsSetupmessage bool + IsInfo bool + IsForwarded bool + IsBot bool + SystemMessageType SysmsgType + Duration int + DimensionsHeight int + DimensionsWidth int + VideochatType int + VideochatUrl string + OverrideSenderName string + Sender *ContactSnapshot + SetupCodeBegin string + File string + FileMime string + FileBytes uint64 + FileName string + WebxdcInfo *WebxdcMsgInfo + DownloadState DownloadState + Reactions *Reactions +} + +type WebxdcMsgInfo struct { + Name string + Icon string + Document string + Summary string + SourceCodeUrl string + InternetAccess bool +} + +type Reactions struct { + ReactionsByContact map[ContactId][]string + Reactions map[string]int // Unique reactions and their count +} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/util.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/util.go new file mode 100644 index 0000000..fec1925 --- /dev/null +++ b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/util.go @@ -0,0 +1,78 @@ +package deltachat + +import ( + "fmt" + "regexp" + "strings" +) + +// Get the first existing account or create a new one if there is no existing accounts. +// Useful for single-account bots/clients. +func GetAccount(rpc *Rpc) AccountId { + accounts, _ := rpc.GetAllAccountIds() + if len(accounts) == 0 { + accountId, _ := rpc.AddAccount() + return accountId + } + return accounts[0] +} + +// Extract metadata from system message with type SysmsgTypeMemberAddedToGroup. +func ParseMemberAdded(rpc Rpc, accountId AccountId, msg *MsgSnapshot) (actor ContactId, target ContactId, err error) { + return parseMemberAddRemove(rpc, accountId, msg, "added") +} + +// Extract metadata from system message with type SysmsgTypeMemberRemovedFromGroup. +func ParseMemberRemoved(rpc Rpc, accountId AccountId, msg *MsgSnapshot) (actor ContactId, target ContactId, err error) { + return parseMemberAddRemove(rpc, accountId, msg, "removed") +} + +func parseMemberAddRemove(rpc Rpc, accountId AccountId, msg *MsgSnapshot, action string) (actor ContactId, target ContactId, err error) { + text := strings.ToLower(msg.Text) + actor = msg.FromId + + regex := regexp.MustCompile(`^member (.+) ` + action + ` by .+\.$`) + match := regex.FindStringSubmatch(text) + if len(match) > 0 { + target, err := extractContact(rpc, accountId, match[1]) + if err != nil { + return 0, 0, err + } + return actor, target, nil + } + + regex = regexp.MustCompile(`^you ` + action + ` member (.+)\.$`) + match = regex.FindStringSubmatch(text) + if len(match) > 0 { + target, err := extractContact(rpc, accountId, match[1]) + if err != nil { + return 0, 0, err + } + return actor, target, nil + } + + if action == "removed" { + regex = regexp.MustCompile(`^group left by .+\.$`) + match = regex.FindStringSubmatch(text) + if len(match) > 0 { + return actor, actor, nil + } + + regex = regexp.MustCompile(`^you left the group\.$`) + if regex.MatchString(text) { + return actor, actor, nil + } + } + + return 0, 0, fmt.Errorf("System message does not match") +} + +func extractContact(rpc Rpc, accountId AccountId, text string) (ContactId, error) { + regex := regexp.MustCompile(`^.*\((.+@.+)\)$`) + match := regex.FindStringSubmatch(text) + if len(match) > 0 { + text = match[1] + } + opt, err := rpc.LookupContactIdByAddr(accountId, text) + return opt.UnwrapOr(0), err +} diff --git a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/webxdc.go b/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/webxdc.go deleted file mode 100644 index a362749..0000000 --- a/vendor/github.com/deltachat/deltachat-rpc-client-go/deltachat/webxdc.go +++ /dev/null @@ -1,10 +0,0 @@ -package deltachat - -type WebxdcMsgInfo struct { - Name string - Icon string - Document string - Summary string - SourceCodeUrl string - InternetAccess bool -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 561ed68..3d01a87 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,14 +1,18 @@ -# github.com/creachadair/jrpc2 v0.44.0 +# github.com/creachadair/jrpc2 v1.0.0 ## explicit; go 1.19 github.com/creachadair/jrpc2 github.com/creachadair/jrpc2/channel -github.com/creachadair/jrpc2/code -# github.com/deltachat-bot/deltabot-cli-go v0.4.0 +# github.com/creachadair/mds v0.0.1 ## explicit; go 1.19 +github.com/creachadair/mds/mlink +# github.com/deltachat-bot/deltabot-cli-go v0.5.0 +## explicit; go 1.21 github.com/deltachat-bot/deltabot-cli-go/botcli -# github.com/deltachat/deltachat-rpc-client-go v0.17.1-0.20230414134334-71f41fbdb931 -## explicit; go 1.19 +# github.com/deltachat/deltachat-rpc-client-go v1.127.0 +## explicit; go 1.21 github.com/deltachat/deltachat-rpc-client-go/deltachat +github.com/deltachat/deltachat-rpc-client-go/deltachat/option +github.com/deltachat/deltachat-rpc-client-go/deltachat/transport # github.com/inconshreveable/mousetrap v1.0.1 ## explicit; go 1.18 github.com/inconshreveable/mousetrap