From 9504a6100b1c23909976ac1473285cab5397aece Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:31:25 +0000 Subject: [PATCH 01/42] chore(internal): minor cleanup --- client_test.go | 2 +- internal/apiform/form_test.go | 9 ++++++--- internal/apiform/tag.go | 2 ++ internal/apijson/decoder.go | 9 +++------ internal/apijson/encoder.go | 21 --------------------- internal/apijson/enum.go | 9 ++------- internal/apijson/json_test.go | 6 +++--- internal/apijson/tag.go | 2 ++ internal/apiquery/encoder.go | 23 +---------------------- internal/requestconfig/requestconfig.go | 6 +++--- 10 files changed, 23 insertions(+), 66 deletions(-) diff --git a/client_test.go b/client_test.go index 29200a2..b427d46 100644 --- a/client_test.go +++ b/client_test.go @@ -38,7 +38,7 @@ func TestUserAgentHeader(t *testing.T) { }, }), ) - client.Accounts.List(context.Background()) + _, _ = client.Accounts.List(context.Background()) if userAgent != fmt.Sprintf("BeeperDesktop/Go %s", internal.PackageVersion) { t.Errorf("Expected User-Agent to be correct, but got: %#v", userAgent) } diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index 31fb2f6..4e669d2 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -585,14 +585,17 @@ func TestEncode(t *testing.T) { t.Run(name, func(t *testing.T) { buf := bytes.NewBuffer(nil) writer := multipart.NewWriter(buf) - writer.SetBoundary("xxx") + err := writer.SetBoundary("xxx") + if err != nil { + t.Errorf("setting boundary for %v failed with error %v", test.val, err) + } - var arrayFmt string = "indices:dots" + arrayFmt := "indices:dots" if tags := strings.Split(name, ","); len(tags) > 1 { arrayFmt = tags[1] } - err := MarshalWithSettings(test.val, writer, arrayFmt) + err = MarshalWithSettings(test.val, writer, arrayFmt) if err != nil { t.Errorf("serialization of %v failed with error %v", test.val, err) } diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index b353617..5dd0b45 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -60,6 +60,8 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { tag.extras = true case "required": tag.required = true + case "metadata": + tag.metadata = true } } } diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index f9eef63..57aef4f 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -393,7 +393,7 @@ func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc { for _, decoder := range anonymousDecoders { // ignore errors - decoder.fn(node, value.FieldByIndex(decoder.idx), state) + _ = decoder.fn(node, value.FieldByIndex(decoder.idx), state) } for _, inlineDecoder := range inlineDecoders { @@ -462,7 +462,7 @@ func (d *decoderBuilder) newStructTypeDecoder(t reflect.Type) decoderFunc { // Handle null [param.Opt] if itemNode.Type == gjson.Null && dest.IsValid() && dest.Type().Implements(reflect.TypeOf((*param.Optional)(nil)).Elem()) { - dest.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(itemNode.Raw)) + _ = dest.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(itemNode.Raw)) continue } @@ -684,8 +684,5 @@ func guardUnknown(state *decoderState, v reflect.Value) bool { constantString, ok := v.Interface().(interface{ Default() string }) named := v.Type() != stringType - if guardStrict(state, ok && named && v.Equal(reflect.ValueOf(constantString.Default()))) { - return true - } - return false + return guardStrict(state, ok && named && v.Equal(reflect.ValueOf(constantString.Default()))) } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index ab7a3c1..f4e3a5c 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -290,27 +290,6 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } } -func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { - f, _ := t.FieldByName("Value") - enc := e.typeEncoder(f.Type) - - return func(value reflect.Value) (json []byte, err error) { - present := value.FieldByName("Present") - if !present.Bool() { - return nil, nil - } - null := value.FieldByName("Null") - if null.Bool() { - return []byte("null"), nil - } - raw := value.FieldByName("Raw") - if !raw.IsNil() { - return e.typeEncoder(raw.Type())(raw) - } - return enc(value.FieldByName("Value")) - } -} - func (e *encoder) newTimeTypeEncoder() encoderFunc { format := e.dateFormat return func(value reflect.Value) (json []byte, err error) { diff --git a/internal/apijson/enum.go b/internal/apijson/enum.go index 5bef11c..a1626a5 100644 --- a/internal/apijson/enum.go +++ b/internal/apijson/enum.go @@ -4,7 +4,6 @@ import ( "fmt" "reflect" "slices" - "sync" "github.com/tidwall/gjson" ) @@ -15,7 +14,6 @@ import ( type validationEntry struct { field reflect.StructField - required bool legalValues struct { strings []string // 1 represents true, 0 represents false, -1 represents either @@ -24,9 +22,6 @@ type validationEntry struct { } } -type validatorFunc func(reflect.Value) exactness - -var validators sync.Map var validationRegistry = map[reflect.Type][]validationEntry{} func RegisterFieldValidator[T any, V string | bool | int | float64](fieldName string, values ...V) { @@ -111,9 +106,9 @@ func (state *decoderState) validateBool(v reflect.Value) { return } b := v.Bool() - if state.validator.legalValues.bools == 1 && b == false { + if state.validator.legalValues.bools == 1 && !b { state.exactness = loose - } else if state.validator.legalValues.bools == 0 && b == true { + } else if state.validator.legalValues.bools == 0 && b { state.exactness = loose } } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index fac9fcc..6932a7b 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -87,7 +87,7 @@ type JSONFieldStruct struct { C string `json:"c"` D string `json:"d"` ExtraFields map[string]int64 `json:"" api:"extrafields"` - JSON JSONFieldStructJSON `json:",metadata"` + JSON JSONFieldStructJSON `json:"-" api:"metadata"` } type JSONFieldStructJSON struct { @@ -113,12 +113,12 @@ type Union interface { type Inline struct { InlineField Primitives `json:",inline"` - JSON InlineJSON `json:",metadata"` + JSON InlineJSON `json:"-" api:"metadata"` } type InlineArray struct { InlineField []string `json:",inline"` - JSON InlineJSON `json:",metadata"` + JSON InlineJSON `json:"-" api:"metadata"` } type InlineJSON struct { diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 49731b8..0511d69 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -57,6 +57,8 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { tag.extras = true case "required": tag.required = true + case "metadata": + tag.metadata = true } } } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 3cfc65e..9b05b62 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -193,7 +193,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { return func(key string, value reflect.Value) (pairs []Pair, err error) { for _, ef := range encoderFields { - var subkey string = e.renderKeyPath(key, ef.tag.name) + subkey := e.renderKeyPath(key, ef.tag.name) if ef.tag.inline { subkey = key } @@ -372,27 +372,6 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { } } -func (e *encoder) newFieldTypeEncoder(t reflect.Type) encoderFunc { - f, _ := t.FieldByName("Value") - enc := e.typeEncoder(f.Type) - - return func(key string, value reflect.Value) ([]Pair, error) { - present := value.FieldByName("Present") - if !present.Bool() { - return nil, nil - } - null := value.FieldByName("Null") - if null.Bool() { - return nil, fmt.Errorf("apiquery: field cannot be null") - } - raw := value.FieldByName("Raw") - if !raw.IsNil() { - return e.typeEncoder(raw.Type())(key, raw) - } - return enc(key, value.FieldByName("Value")) - } -} - func (e *encoder) newTimeTypeEncoder(_ reflect.Type) encoderFunc { format := e.dateFormat return func(key string, value reflect.Value) ([]Pair, error) { diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index a8694b9..ab6d9cd 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -461,7 +461,7 @@ func (cfg *RequestConfig) Execute() (err error) { // Close the response body before retrying to prevent connection leaks if res != nil && res.Body != nil { - res.Body.Close() + _ = res.Body.Close() } select { @@ -489,7 +489,7 @@ func (cfg *RequestConfig) Execute() (err error) { if res.StatusCode >= 400 { contents, err := io.ReadAll(res.Body) - res.Body.Close() + _ = res.Body.Close() if err != nil { return err } @@ -520,7 +520,7 @@ func (cfg *RequestConfig) Execute() (err error) { } contents, err := io.ReadAll(res.Body) - res.Body.Close() + _ = res.Body.Close() if err != nil { return fmt.Errorf("error reading response body: %w", err) } From ce4f065cc711a045df715b8c0a7c4338f7d114db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:40:18 +0000 Subject: [PATCH 02/42] chore(internal): use explicit returns --- account.go | 2 +- accountcontact.go | 6 +++--- asset.go | 8 ++++---- chat.go | 10 +++++----- chatmessagereaction.go | 12 ++++++------ chatreminder.go | 8 ++++---- client.go | 4 ++-- info.go | 2 +- message.go | 12 ++++++------ 9 files changed, 32 insertions(+), 32 deletions(-) diff --git a/account.go b/account.go index 22c0aef..23880a5 100644 --- a/account.go +++ b/account.go @@ -44,7 +44,7 @@ func (r *AccountService) List(ctx context.Context, opts ...option.RequestOption) opts = slices.Concat(r.Options, opts) path := "v1/accounts" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } // A chat account added to Beeper diff --git a/accountcontact.go b/accountcontact.go index e202b79..f38a21c 100644 --- a/accountcontact.go +++ b/accountcontact.go @@ -48,7 +48,7 @@ func (r *AccountContactService) List(ctx context.Context, accountID string, quer opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...) if accountID == "" { err = errors.New("missing required accountID parameter") - return + return nil, err } path := fmt.Sprintf("v1/accounts/%s/contacts/list", accountID) cfg, err := requestconfig.NewRequestConfig(ctx, http.MethodGet, path, query, &res, opts...) @@ -74,11 +74,11 @@ func (r *AccountContactService) Search(ctx context.Context, accountID string, qu opts = slices.Concat(r.Options, opts) if accountID == "" { err = errors.New("missing required accountID parameter") - return + return nil, err } path := fmt.Sprintf("v1/accounts/%s/contacts", accountID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } type AccountContactSearchResponse struct { diff --git a/asset.go b/asset.go index 3f5ba72..da268b1 100644 --- a/asset.go +++ b/asset.go @@ -47,7 +47,7 @@ func (r *AssetService) Download(ctx context.Context, body AssetDownloadParams, o opts = slices.Concat(r.Options, opts) path := "v1/assets/download" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Stream a file given an mxc://, localmxc://, or file:// URL. Downloads first if @@ -57,7 +57,7 @@ func (r *AssetService) Serve(ctx context.Context, query AssetServeParams, opts . opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) path := "v1/assets/serve" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, nil, opts...) - return + return err } // Upload a file to a temporary location using multipart/form-data. Returns an @@ -66,7 +66,7 @@ func (r *AssetService) Upload(ctx context.Context, body AssetUploadParams, opts opts = slices.Concat(r.Options, opts) path := "v1/assets/upload" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Upload a file using a JSON body with base64-encoded content. Returns an uploadID @@ -76,7 +76,7 @@ func (r *AssetService) UploadBase64(ctx context.Context, body AssetUploadBase64P opts = slices.Concat(r.Options, opts) path := "v1/assets/upload/base64" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } type AssetDownloadResponse struct { diff --git a/chat.go b/chat.go index 1e6a6f2..ca2a80a 100644 --- a/chat.go +++ b/chat.go @@ -54,7 +54,7 @@ func (r *ChatService) New(ctx context.Context, body ChatNewParams, opts ...optio opts = slices.Concat(r.Options, opts) path := "v1/chats" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Retrieve chat details including metadata, participants, and latest message @@ -62,11 +62,11 @@ func (r *ChatService) Get(ctx context.Context, chatID string, query ChatGetParam opts = slices.Concat(r.Options, opts) if chatID == "" { err = errors.New("missing required chatID parameter") - return + return nil, err } path := fmt.Sprintf("v1/chats/%s", chatID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } // List all chats sorted by last activity (most recent first). Combines all @@ -101,11 +101,11 @@ func (r *ChatService) Archive(ctx context.Context, chatID string, body ChatArchi opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if chatID == "" { err = errors.New("missing required chatID parameter") - return + return err } path := fmt.Sprintf("v1/chats/%s/archive", chatID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, nil, opts...) - return + return err } // Search chats by title/network or participants using Beeper Desktop's renderer diff --git a/chatmessagereaction.go b/chatmessagereaction.go index f53de17..a567da0 100644 --- a/chatmessagereaction.go +++ b/chatmessagereaction.go @@ -44,15 +44,15 @@ func (r *ChatMessageReactionService) Delete(ctx context.Context, messageID strin opts = slices.Concat(r.Options, opts) if params.ChatID == "" { err = errors.New("missing required chatID parameter") - return + return nil, err } if messageID == "" { err = errors.New("missing required messageID parameter") - return + return nil, err } path := fmt.Sprintf("v1/chats/%s/messages/%s/reactions", params.ChatID, messageID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, params, &res, opts...) - return + return res, err } // Add a reaction to an existing message. @@ -60,15 +60,15 @@ func (r *ChatMessageReactionService) Add(ctx context.Context, messageID string, opts = slices.Concat(r.Options, opts) if params.ChatID == "" { err = errors.New("missing required chatID parameter") - return + return nil, err } if messageID == "" { err = errors.New("missing required messageID parameter") - return + return nil, err } path := fmt.Sprintf("v1/chats/%s/messages/%s/reactions", params.ChatID, messageID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, params, &res, opts...) - return + return res, err } type ChatMessageReactionDeleteResponse struct { diff --git a/chatreminder.go b/chatreminder.go index c6b979b..7d65d41 100644 --- a/chatreminder.go +++ b/chatreminder.go @@ -42,11 +42,11 @@ func (r *ChatReminderService) New(ctx context.Context, chatID string, body ChatR opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if chatID == "" { err = errors.New("missing required chatID parameter") - return + return err } path := fmt.Sprintf("v1/chats/%s/reminders", chatID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, nil, opts...) - return + return err } // Clear an existing reminder from a chat @@ -55,11 +55,11 @@ func (r *ChatReminderService) Delete(ctx context.Context, chatID string, opts .. opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) if chatID == "" { err = errors.New("missing required chatID parameter") - return + return err } path := fmt.Sprintf("v1/chats/%s/reminders", chatID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) - return + return err } type ChatReminderNewParams struct { diff --git a/client.go b/client.go index d05c0b7..6ccd7c0 100644 --- a/client.go +++ b/client.go @@ -135,7 +135,7 @@ func (r *Client) Focus(ctx context.Context, body FocusParams, opts ...option.Req opts = slices.Concat(r.Options, opts) path := "v1/focus" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } // Returns matching chats, participant name matches in groups, and the first page @@ -145,5 +145,5 @@ func (r *Client) Search(ctx context.Context, query SearchParams, opts ...option. opts = slices.Concat(r.Options, opts) path := "v1/search" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return + return res, err } diff --git a/info.go b/info.go index 6d53b58..e95dfbf 100644 --- a/info.go +++ b/info.go @@ -40,7 +40,7 @@ func (r *InfoService) Get(ctx context.Context, opts ...option.RequestOption) (re opts = slices.Concat(r.Options, opts) path := "v1/info" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) - return + return res, err } type InfoGetResponse struct { diff --git a/message.go b/message.go index 1d09982..68799a9 100644 --- a/message.go +++ b/message.go @@ -48,15 +48,15 @@ func (r *MessageService) Update(ctx context.Context, messageID string, params Me opts = slices.Concat(r.Options, opts) if params.ChatID == "" { err = errors.New("missing required chatID parameter") - return + return nil, err } if messageID == "" { err = errors.New("missing required messageID parameter") - return + return nil, err } path := fmt.Sprintf("v1/chats/%s/messages/%s", params.ChatID, messageID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, params, &res, opts...) - return + return res, err } // List all messages in a chat with cursor-based pagination. Sorted by timestamp. @@ -66,7 +66,7 @@ func (r *MessageService) List(ctx context.Context, chatID string, query MessageL opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...) if chatID == "" { err = errors.New("missing required chatID parameter") - return + return nil, err } path := fmt.Sprintf("v1/chats/%s/messages", chatID) cfg, err := requestconfig.NewRequestConfig(ctx, http.MethodGet, path, query, &res, opts...) @@ -115,11 +115,11 @@ func (r *MessageService) Send(ctx context.Context, chatID string, body MessageSe opts = slices.Concat(r.Options, opts) if chatID == "" { err = errors.New("missing required chatID parameter") - return + return nil, err } path := fmt.Sprintf("v1/chats/%s/messages", chatID) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + return res, err } type MessageUpdateResponse struct { From f102b8ecdfa469185a1ed3700f73952f7c3bfec5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 07:44:11 +0000 Subject: [PATCH 03/42] chore(internal): use explicit returns in more places --- internal/apiform/encoder.go | 2 +- internal/apiform/tag.go | 6 +++--- internal/apijson/encoder.go | 2 +- internal/apijson/json_test.go | 2 +- internal/apijson/tag.go | 6 +++--- internal/apiquery/encoder.go | 10 +++++----- internal/apiquery/tag.go | 6 +++--- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 3a61344..dc75c3e 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -469,5 +469,5 @@ func WriteExtras(writer *multipart.Writer, extras map[string]any) (err error) { break } } - return + return err } diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index 5dd0b45..d9915d4 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -24,7 +24,7 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool raw, ok = field.Tag.Lookup(jsonStructTag) } if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -45,7 +45,7 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool } parseApiStructTag(field, &tag) - return + return tag, ok } func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { @@ -68,5 +68,5 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index f4e3a5c..0decb73 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -286,7 +286,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { return nil, err } } - return + return json, err } } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index 6932a7b..19b3614 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -268,7 +268,7 @@ type MarshallingUnionStruct struct { func (r *MarshallingUnionStruct) UnmarshalJSON(data []byte) (err error) { *r = MarshallingUnionStruct{} err = UnmarshalRoot(data, &r.Union) - return + return err } func (r MarshallingUnionStruct) MarshalJSON() (data []byte, err error) { diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 0511d69..17b2130 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -20,7 +20,7 @@ type parsedStructTag struct { func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { raw, ok := field.Tag.Lookup(jsonStructTag) if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -42,7 +42,7 @@ func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool // the `api` struct tag is only used alongside `json` for custom behaviour parseApiStructTag(field, &tag) - return + return tag, ok } func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { @@ -65,5 +65,5 @@ func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 9b05b62..361902c 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -103,7 +103,7 @@ func (e *encoder) newTypeEncoder(t reflect.Type) encoderFunc { encoder := e.typeEncoder(t.Elem()) return func(key string, value reflect.Value) (pairs []Pair, err error) { if !value.IsValid() || value.IsNil() { - return + return pairs, err } return encoder(key, value.Elem()) } @@ -205,7 +205,7 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } } @@ -256,7 +256,7 @@ func (e *encoder) newMapEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } } @@ -300,7 +300,7 @@ func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } case ArrayQueryFormatIndices: panic("The array indices format is not supported yet") @@ -315,7 +315,7 @@ func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { } pairs = append(pairs, subpairs...) } - return + return pairs, err } default: panic(fmt.Sprintf("Unknown ArrayFormat value: %d", e.settings.ArrayFormat)) diff --git a/internal/apiquery/tag.go b/internal/apiquery/tag.go index 772c40e..9e413ad 100644 --- a/internal/apiquery/tag.go +++ b/internal/apiquery/tag.go @@ -18,7 +18,7 @@ type parsedStructTag struct { func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { raw, ok := field.Tag.Lookup(queryStructTag) if !ok { - return + return tag, ok } parts := strings.Split(raw, ",") if len(parts) == 0 { @@ -35,10 +35,10 @@ func parseQueryStructTag(field reflect.StructField) (tag parsedStructTag, ok boo tag.inline = true } } - return + return tag, ok } func parseFormatStructTag(field reflect.StructField) (format string, ok bool) { format, ok = field.Tag.Lookup(formatStructTag) - return + return format, ok } From 962727a0eeb15c02888a70fc55a6a6356b224e50 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:45:23 +0000 Subject: [PATCH 04/42] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 004aab8..06ba3c3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: aa49273410d42fb96c5515dbce1f182f +config_hash: bfb432c69dc0a8d273043a3cdd87ffe1 From 26470e35c0c109d1b0c4c2d37878ca31183f51bb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:46:41 +0000 Subject: [PATCH 05/42] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 06ba3c3..72a5288 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: bfb432c69dc0a8d273043a3cdd87ffe1 +config_hash: a7eb18d34cd75f7dbffe1940df6b2e9e From 9fd07e44e68014b1ba88891a8e89d2994540df19 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:21:54 +0000 Subject: [PATCH 06/42] feat(api): manual updates --- .stats.yml | 2 +- README.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 72a5288..5dbc3d6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: a7eb18d34cd75f7dbffe1940df6b2e9e +config_hash: 6fc4359a793fc3fc9ac01712b5ef8c0d diff --git a/README.md b/README.md index f3f60c7..1ee7fc3 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,14 @@ The Beeper Desktop Go library provides convenient access to the [Beeper Desktop REST API](https://developers.beeper.com/desktop-api/) from applications written in Go. +It is generated with [Stainless](https://www.stainless.com/). + ## MCP Server Use the Beeper Desktop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-api-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1hcGktbWNwIl0sImVudiI6eyJCRUVQRVJfQUNDRVNTX1RPS0VOIjoiTXkgQWNjZXNzIFRva2VuIn19) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-api-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-api-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40beeper%2Fdesktop-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBiZWVwZXIvZGVza3RvcC1tY3AiXSwiZW52Ijp7IkJFRVBFUl9BQ0NFU1NfVE9LRU4iOiJNeSBBY2Nlc3MgVG9rZW4ifX0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40beeper%2Fdesktop-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40beeper%2Fdesktop-mcp%22%5D%2C%22env%22%3A%7B%22BEEPER_ACCESS_TOKEN%22%3A%22My%20Access%20Token%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From 92a6968dbbaea7049dc004d140b2f440738ba91d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 13 Mar 2026 14:36:04 +0000 Subject: [PATCH 07/42] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 5dbc3d6..2b39be6 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: 6fc4359a793fc3fc9ac01712b5ef8c0d +config_hash: ca148af6be59ec54295b2c5f852a38d1 From b7458ceb902a6e768e54adc05d5532f63147e3db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 03:38:17 +0000 Subject: [PATCH 08/42] chore(internal): tweak CI branches --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfc4d92..cb0bafd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,14 @@ name: CI on: push: - branches-ignore: - - 'generated' - - 'codegen/**' - - 'integrated/**' - - 'stl-preview-head/**' - - 'stl-preview-base/**' + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' pull_request: branches-ignore: - 'stl-preview-head/**' From bd09ac321d342e5f197f8ceac6abf4ee5e766822 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:09:34 +0000 Subject: [PATCH 09/42] refactor(tests): switch from prism to steady --- CONTRIBUTING.md | 2 +- scripts/mock | 26 +++++++++++++------------- scripts/test | 16 ++++++++-------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a63ac6b..9b64439 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,7 @@ $ go mod edit -replace github.com/beeper/desktop-api-go=/path/to/desktop-api-go ## Running tests -Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests. +Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests. ```sh $ ./scripts/mock diff --git a/scripts/mock b/scripts/mock index bcf3b39..00b490b 100755 --- a/scripts/mock +++ b/scripts/mock @@ -19,34 +19,34 @@ fi echo "==> Starting mock server with URL ${URL}" -# Run prism mock on the given spec +# Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version + npm exec --package=@stdy/cli@0.19.3 -- steady --version - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log & + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" &> .stdy.log & - # Wait for server to come online (max 30s) + # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" attempts=0 - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do + while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do + if ! kill -0 $! 2>/dev/null; then + echo + cat .stdy.log + exit 1 + fi attempts=$((attempts + 1)) if [ "$attempts" -ge 300 ]; then echo - echo "Timed out waiting for Prism server to start" - cat .prism.log + echo "Timed out waiting for Steady server to start" + cat .stdy.log exit 1 fi echo -n "." sleep 0.1 done - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - echo else - npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" + npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index c26b122..6c81b85 100755 --- a/scripts/test +++ b/scripts/test @@ -9,8 +9,8 @@ GREEN='\033[0;32m' YELLOW='\033[0;33m' NC='\033[0m' # No Color -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 +function steady_is_running() { + curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1 } kill_server_on_port() { @@ -25,7 +25,7 @@ function is_overriding_api_base_url() { [ -n "$TEST_API_BASE_URL" ] } -if ! is_overriding_api_base_url && ! prism_is_running ; then +if ! is_overriding_api_base_url && ! steady_is_running ; then # When we exit this script, make sure to kill the background mock server process trap 'kill_server_on_port 4010' EXIT @@ -36,19 +36,19 @@ fi if is_overriding_api_base_url ; then echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" +elif ! steady_is_running ; then + echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server" echo -e "running against your OpenAPI spec." echo echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" + echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets${NC}" echo exit 1 else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" + echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}" echo fi From 026019176b91a6cc8a06f3e34cf6df2365f14e50 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:10:54 +0000 Subject: [PATCH 10/42] chore(tests): bump steady to v0.19.4 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 00b490b..f310477 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.3 -- steady --version + npm exec --package=@stdy/cli@0.19.4 -- steady --version - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 6c81b85..7f5dc36 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From bb2044f9230e575f610398a5f28a43bd7b7e8cf2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:18:14 +0000 Subject: [PATCH 11/42] chore(tests): bump steady to v0.19.5 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index f310477..54fc791 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.4 -- steady --version + npm exec --package=@stdy/cli@0.19.5 -- steady --version - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.4 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 7f5dc36..7794c48 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.4 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 229284fcf7e552241afaebbe24df184a5db693af Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:09:25 +0000 Subject: [PATCH 12/42] chore(internal): update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c6d0501..8554aff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .prism.log +.stdy.log codegen.log Brewfile.lock.json .idea/ From 30b897ce73a2216204876ab5fb3132cf549228ee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:16:17 +0000 Subject: [PATCH 13/42] chore(tests): bump steady to v0.19.6 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 54fc791..0f82c95 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.5 -- steady --version + npm exec --package=@stdy/cli@0.19.6 -- steady --version - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.5 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 7794c48..8d7ef08 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.5 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From 2a83146c4d9b2f29ddce45b6f8484f7533f98c58 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:04:59 +0000 Subject: [PATCH 14/42] chore(ci): skip lint on metadata-only changes Note that we still want to run tests, as these depend on the metadata. --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb0bafd..d507577 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,8 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: |- github.repository == 'stainless-sdks/beeper-desktop-api-go' && - (github.event_name == 'push' || github.event.pull_request.head.repo.fork) + (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && + (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 From b64d42348319e02f4cad0107d9848f57743d04c7 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:05:47 +0000 Subject: [PATCH 15/42] chore(tests): bump steady to v0.19.7 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 0f82c95..3732f8e 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.6 -- steady --version + npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.6 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 8d7ef08..15b524c 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.6 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" echo exit 1 From eaa5a1a03cd615f3b260be38865dd96a85a96e4f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:24:53 +0000 Subject: [PATCH 16/42] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 2b39be6..60bb453 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: ca148af6be59ec54295b2c5f852a38d1 +config_hash: e342a96262eaf44c54e8bbb93cc8d7a7 From 72229ec58a8722b79fcb7d13921edcb7d58a1594 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:25:13 +0000 Subject: [PATCH 17/42] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 60bb453..16d5bba 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: e342a96262eaf44c54e8bbb93cc8d7a7 +config_hash: f99f904573839260bdb6d428bad17613 From 7721cb4dd498b5b8638146d649d91435ee809e37 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 25 Mar 2026 02:25:22 +0000 Subject: [PATCH 18/42] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 16d5bba..2c47924 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: f99f904573839260bdb6d428bad17613 +config_hash: 7d85c0b454fc78a59db6474c5c4d73c6 From 3e39971ec0ee20accae2c2eae5e64004f6cfe810 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:05:29 +0000 Subject: [PATCH 19/42] chore(internal): support default value struct tag --- internal/apiform/tag.go | 26 +++++++++++++++++++++----- internal/apijson/encoder.go | 8 ++++++++ internal/apijson/json_test.go | 17 +++++++++++++++++ internal/apijson/tag.go | 26 +++++++++++++++++++++----- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/internal/apiform/tag.go b/internal/apiform/tag.go index d9915d4..f0c9d14 100644 --- a/internal/apiform/tag.go +++ b/internal/apiform/tag.go @@ -9,13 +9,15 @@ const apiStructTag = "api" const jsonStructTag = "json" const formStructTag = "form" const formatStructTag = "format" +const defaultStructTag = "default" type parsedStructTag struct { - name string - required bool - extras bool - metadata bool - omitzero bool + name string + required bool + extras bool + metadata bool + omitzero bool + defaultValue any } func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { @@ -45,9 +47,23 @@ func parseFormStructTag(field reflect.StructField) (tag parsedStructTag, ok bool } parseApiStructTag(field, &tag) + parseDefaultStructTag(field, &tag) return tag, ok } +func parseDefaultStructTag(field reflect.StructField, tag *parsedStructTag) { + if field.Type.Kind() != reflect.String { + // Only strings are currently supported + return + } + + raw, ok := field.Tag.Lookup(defaultStructTag) + if !ok { + return + } + tag.defaultValue = raw +} + func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { raw, ok := field.Tag.Lookup(apiStructTag) if !ok { diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index 0decb73..5fc57b4 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -12,6 +12,8 @@ import ( "time" "github.com/tidwall/sjson" + + shimjson "github.com/beeper/desktop-api-go/internal/encoding/json" ) var encoders sync.Map // map[encoderEntry]encoderFunc @@ -271,6 +273,12 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { if err != nil { return nil, err } + if ef.tag.defaultValue != nil && (!field.IsValid() || field.IsZero()) { + encoded, err = shimjson.Marshal(ef.tag.defaultValue) + if err != nil { + return nil, err + } + } if encoded == nil { continue } diff --git a/internal/apijson/json_test.go b/internal/apijson/json_test.go index 19b3614..2853bf9 100644 --- a/internal/apijson/json_test.go +++ b/internal/apijson/json_test.go @@ -614,3 +614,20 @@ func TestEncode(t *testing.T) { }) } } + +type StructWithDefault struct { + Type string `json:"type" default:"foo"` +} + +func TestDefault(t *testing.T) { + value := StructWithDefault{} + expected := `{"type":"foo"}` + + raw, err := Marshal(value) + if err != nil { + t.Fatalf("serialization of %v failed with error %v", value, err) + } + if string(raw) != expected { + t.Fatalf("expected %+#v to serialize to %s but got %s", value, expected, string(raw)) + } +} diff --git a/internal/apijson/tag.go b/internal/apijson/tag.go index 17b2130..efcaf8c 100644 --- a/internal/apijson/tag.go +++ b/internal/apijson/tag.go @@ -8,13 +8,15 @@ import ( const apiStructTag = "api" const jsonStructTag = "json" const formatStructTag = "format" +const defaultStructTag = "default" type parsedStructTag struct { - name string - required bool - extras bool - metadata bool - inline bool + name string + required bool + extras bool + metadata bool + inline bool + defaultValue any } func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool) { @@ -42,9 +44,23 @@ func parseJSONStructTag(field reflect.StructField) (tag parsedStructTag, ok bool // the `api` struct tag is only used alongside `json` for custom behaviour parseApiStructTag(field, &tag) + parseDefaultStructTag(field, &tag) return tag, ok } +func parseDefaultStructTag(field reflect.StructField, tag *parsedStructTag) { + if field.Type.Kind() != reflect.String { + // Only strings are currently supported + return + } + + raw, ok := field.Tag.Lookup(defaultStructTag) + if !ok { + return + } + tag.defaultValue = raw +} + func parseApiStructTag(field reflect.StructField, tag *parsedStructTag) { raw, ok := field.Tag.Lookup(apiStructTag) if !ok { From 029f5d67da0158cf1ed913fc3f6dfd213681d363 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 02:08:00 +0000 Subject: [PATCH 20/42] chore(client): fix multipart serialisation of Default() fields --- internal/apiform/encoder.go | 8 ++++++++ internal/apiform/form_test.go | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index dc75c3e..da6d80f 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -265,6 +265,14 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { } return typeEncoderFn(key, value, writer) } + } else if ptag.defaultValue != nil { + typeEncoderFn := e.typeEncoder(field.Type) + encoderFn = func(key string, value reflect.Value, writer *multipart.Writer) error { + if value.IsZero() { + return typeEncoderFn(key, reflect.ValueOf(ptag.defaultValue), writer) + } + return typeEncoderFn(key, value, writer) + } } else { encoderFn = e.typeEncoder(field.Type) } diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index 4e669d2..286ea13 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -123,6 +123,11 @@ type StructUnion struct { param.APIUnion } +type ConstantStruct struct { + Anchor string `form:"anchor" default:"created_at"` + Seconds int `form:"seconds"` +} + type MultipartMarshalerParent struct { Middle MultipartMarshalerMiddleNext `form:"middle"` } @@ -554,6 +559,37 @@ Content-Disposition: form-data; name="union" Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), }, }, + "constant_zero_value": { + `--xxx +Content-Disposition: form-data; name="anchor" + +created_at +--xxx +Content-Disposition: form-data; name="seconds" + +3600 +--xxx-- +`, + ConstantStruct{ + Seconds: 3600, + }, + }, + "constant_explicit_value": { + `--xxx +Content-Disposition: form-data; name="anchor" + +created_at_override +--xxx +Content-Disposition: form-data; name="seconds" + +3600 +--xxx-- +`, + ConstantStruct{ + Anchor: "created_at_override", + Seconds: 3600, + }, + }, "deeply-nested-struct,brackets": { `--xxx Content-Disposition: form-data; name="middle[middleNext][child]" From 8293eb76584617003ec104770810028b3e4076b0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:05:29 +0000 Subject: [PATCH 21/42] fix: prevent duplicate ? in query params --- internal/requestconfig/requestconfig.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index ab6d9cd..ee5042c 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -121,7 +121,16 @@ func NewRequestConfig(ctx context.Context, method string, u string, body any, ds } params := q.Encode() if params != "" { - u = u + "?" + params + parsed, err := url.Parse(u) + if err != nil { + return nil, err + } + if parsed.RawQuery != "" { + parsed.RawQuery = parsed.RawQuery + "&" + params + u = parsed.String() + } else { + u = u + "?" + params + } } } if body, ok := body.([]byte); ok { From 76dc981c3f95e4b7935aaefd95f1de5e119d9298 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:08:47 +0000 Subject: [PATCH 22/42] chore: remove unnecessary error check for url parsing --- internal/requestconfig/requestconfig.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index ee5042c..c80db54 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -121,10 +121,7 @@ func NewRequestConfig(ctx context.Context, method string, u string, body any, ds } params := q.Encode() if params != "" { - parsed, err := url.Parse(u) - if err != nil { - return nil, err - } + parsed, _ := url.Parse(u) if parsed.RawQuery != "" { parsed.RawQuery = parsed.RawQuery + "&" + params u = parsed.String() From d4f7e8ef134cb9a535fa05458ee7af93532c8c2b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:09:23 +0000 Subject: [PATCH 23/42] feat(internal): support comma format in multipart form encoding --- internal/apiform/encoder.go | 12 ++++++++++++ scripts/mock | 4 ++-- scripts/test | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index da6d80f..12122c0 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -183,6 +183,18 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { itemEncoder := e.typeEncoder(t.Elem()) keyFn := e.arrayKeyEncoder() + if e.arrayFmt == "comma" { + return func(key string, v reflect.Value, writer *multipart.Writer) error { + if v.Len() == 0 { + return nil + } + elements := make([]string, v.Len()) + for i := 0; i < v.Len(); i++ { + elements[i] = fmt.Sprint(v.Index(i).Interface()) + } + return writer.WriteField(key, strings.Join(elements, ",")) + } + } return func(key string, v reflect.Value, writer *multipart.Writer) error { if keyFn == nil { return fmt.Errorf("apiform: unsupported array format") diff --git a/scripts/mock b/scripts/mock index 3732f8e..58e4628 100755 --- a/scripts/mock +++ b/scripts/mock @@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout npm exec --package=@stdy/cli@0.19.7 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 15b524c..5e1da8e 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-form-array-format=repeat --validator-query-array-format=repeat --validator-form-object-format=brackets --validator-query-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From b4dec8f324d1c18699f81d6a060b33c1b0d4772a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 02:15:28 +0000 Subject: [PATCH 24/42] chore(ci): support opting out of skipping builds on metadata-only commits --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d507577..e2a6180 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/beeper-desktop-api-go' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: |- github.repository == 'stainless-sdks/beeper-desktop-api-go' && - (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && - (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') + (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@v6 From 350f111de7c4045065804f2425729f72a65a1388 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 28 Mar 2026 02:22:23 +0000 Subject: [PATCH 25/42] chore: update docs for api:"required" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ee7fc3..8d2a8c3 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ func main() { The beeperdesktopapi library uses the [`omitzero`](https://tip.golang.org/doc/go1.24#encodingjsonpkgencodingjson) semantics from the Go 1.24+ `encoding/json` release for request fields. -Required primitive fields (`int64`, `string`, etc.) feature the tag \`json:"...,required"\`. These +Required primitive fields (`int64`, `string`, etc.) feature the tag \`api:"required"\`. These fields are always serialized, even their zero values. Optional primitive types are wrapped in a `param.Opt[T]`. These fields can be set with the provided constructors, `beeperdesktopapi.String(string)`, `beeperdesktopapi.Int(int64)`, etc. From 8e7e3a6fcedaf7b4eaeb0f7e880e5347f5c2ac93 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:17:43 +0000 Subject: [PATCH 26/42] chore(tests): bump steady to v0.20.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 58e4628..5ea72a2 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.19.7 -- steady --version + npm exec --package=@stdy/cli@0.20.1 -- steady --version - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.19.7 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 5e1da8e..cb5aa8e 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.7 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From a5fbcc7e74ba585849c839bffb71ee1b42f6af2d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:22:11 +0000 Subject: [PATCH 27/42] chore(tests): bump steady to v0.20.2 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 5ea72a2..7c58865 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.1 -- steady --version + npm exec --package=@stdy/cli@0.20.2 -- steady --version - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index cb5aa8e..3c31f6d 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 50422719fe1149b4a0a33cac7a7605e499fac9e4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:31:42 +0000 Subject: [PATCH 28/42] feat(api): add network, bridge fields to accounts --- .stats.yml | 6 +-- README.md | 7 +-- account.go | 32 ++++++++++++++ chat.go | 122 +++++++++++++++++++++++++++++++++------------------ chat_test.go | 25 +++++------ 5 files changed, 131 insertions(+), 61 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2c47924..229f6b5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-5a8ac7b545c48dc892e5c680303e305254921554dabee848e40a808659dbcf1e.yml -openapi_spec_hash: 0103975601aac1445d3a4ef418c5d17a -config_hash: 7d85c0b454fc78a59db6474c5c4d73c6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml +openapi_spec_hash: d6c0a1776048dab04f6c5625c9893c9c +config_hash: 39ed0717b5f415499aaace2468346e1a diff --git a/README.md b/README.md index 8d2a8c3..8459ce7 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,6 @@ The Beeper Desktop Go library provides convenient access to the [Beeper Desktop REST API](https://developers.beeper.com/desktop-api/) from applications written in Go. -It is generated with [Stainless](https://www.stainless.com/). - ## MCP Server Use the Beeper Desktop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. @@ -58,10 +56,13 @@ import ( "fmt" "github.com/beeper/desktop-api-go" + "github.com/beeper/desktop-api-go/option" ) func main() { - client := beeperdesktopapi.NewClient() + client := beeperdesktopapi.NewClient( + option.WithAccessToken("My Access Token"), // defaults to os.LookupEnv("BEEPER_ACCESS_TOKEN") + ) page, err := client.Chats.Search(context.TODO(), beeperdesktopapi.ChatSearchParams{ IncludeMuted: beeperdesktopapi.Bool(true), Limit: beeperdesktopapi.Int(3), diff --git a/account.go b/account.go index 23880a5..eb19809 100644 --- a/account.go +++ b/account.go @@ -51,11 +51,17 @@ func (r *AccountService) List(ctx context.Context, opts ...option.RequestOption) type Account struct { // Chat account added to Beeper. Use this to route account-scoped actions. AccountID string `json:"accountID" api:"required"` + // Bridge metadata for the account. Available from Beeper Desktop v.4.2.719+. + Bridge AccountBridge `json:"bridge" api:"required"` + // Human-friendly network name for the account. + Network string `json:"network" api:"required"` // User the account belongs to. User shared.User `json:"user" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { AccountID respjson.Field + Bridge respjson.Field + Network respjson.Field User respjson.Field ExtraFields map[string]respjson.Field raw string @@ -67,3 +73,29 @@ func (r Account) RawJSON() string { return r.JSON.raw } func (r *Account) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } + +// Bridge metadata for the account. Available from Beeper Desktop v.4.2.719+. +type AccountBridge struct { + // Bridge instance identifier. + ID string `json:"id" api:"required"` + // Bridge provider for the account. + // + // Any of "cloud", "self-hosted", "local", "platform-sdk". + Provider string `json:"provider" api:"required"` + // Bridge type. + Type string `json:"type" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + Provider respjson.Field + Type respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r AccountBridge) RawJSON() string { return r.JSON.raw } +func (r *AccountBridge) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/chat.go b/chat.go index ca2a80a..593a240 100644 --- a/chat.go +++ b/chat.go @@ -268,61 +268,60 @@ func (r *ChatListResponse) UnmarshalJSON(data []byte) error { } type ChatNewParams struct { + + // + // Request body variants + // + + // This field is a request body variant, only one variant field can be set. + OfObject *ChatNewParamsParamsObject `json:",inline"` + // This field is a request body variant, only one variant field can be set. + OfChatNewsParamsObject2 *ChatNewParamsParamsObject2 `json:",inline"` + + paramObj +} + +func (u ChatNewParams) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfObject, u.OfChatNewsParamsObject2) +} +func (r *ChatNewParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The properties AccountID, Mode, User are required. +type ChatNewParamsParamsObject struct { // Account to create or start the chat on. AccountID string `json:"accountID" api:"required"` + // Operation mode. Use 'start' to resolve a user/contact and start a direct chat. + // + // Any of "start". + Mode string `json:"mode,omitzero" api:"required"` + // Merged user-like contact payload used to resolve the best identifier. + User ChatNewParamsParamsObjectUser `json:"user,omitzero" api:"required"` // Whether invite-based DM creation is allowed when required by the platform. Used // for mode='start'. AllowInvite param.Opt[bool] `json:"allowInvite,omitzero"` // Optional first message content if the platform requires it to create the chat. MessageText param.Opt[string] `json:"messageText,omitzero"` - // Optional title for group chats when mode='create'; ignored for single chats on - // most platforms. - Title param.Opt[string] `json:"title,omitzero"` - // Operation mode. Defaults to 'create' when omitted. - // - // Any of "create", "start". - Mode ChatNewParamsMode `json:"mode,omitzero"` - // Required when mode='create'. User IDs to include in the new chat. - ParticipantIDs []string `json:"participantIDs,omitzero"` - // Required when mode='create'. 'single' requires exactly one participantID; - // 'group' supports multiple participants and optional title. - // - // Any of "single", "group". - Type ChatNewParamsType `json:"type,omitzero"` - // Required when mode='start'. Merged user-like contact payload used to resolve the - // best identifier. - User ChatNewParamsUser `json:"user,omitzero"` paramObj } -func (r ChatNewParams) MarshalJSON() (data []byte, err error) { - type shadow ChatNewParams +func (r ChatNewParamsParamsObject) MarshalJSON() (data []byte, err error) { + type shadow ChatNewParamsParamsObject return param.MarshalObject(r, (*shadow)(&r)) } -func (r *ChatNewParams) UnmarshalJSON(data []byte) error { +func (r *ChatNewParamsParamsObject) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Operation mode. Defaults to 'create' when omitted. -type ChatNewParamsMode string - -const ( - ChatNewParamsModeCreate ChatNewParamsMode = "create" - ChatNewParamsModeStart ChatNewParamsMode = "start" -) - -// Required when mode='create'. 'single' requires exactly one participantID; -// 'group' supports multiple participants and optional title. -type ChatNewParamsType string - -const ( - ChatNewParamsTypeSingle ChatNewParamsType = "single" - ChatNewParamsTypeGroup ChatNewParamsType = "group" -) +func init() { + apijson.RegisterFieldValidator[ChatNewParamsParamsObject]( + "mode", "start", + ) +} -// Required when mode='start'. Merged user-like contact payload used to resolve the -// best identifier. -type ChatNewParamsUser struct { +// Merged user-like contact payload used to resolve the best identifier. +type ChatNewParamsParamsObjectUser struct { // Known user ID when available. ID param.Opt[string] `json:"id,omitzero"` // Email candidate. @@ -336,14 +335,53 @@ type ChatNewParamsUser struct { paramObj } -func (r ChatNewParamsUser) MarshalJSON() (data []byte, err error) { - type shadow ChatNewParamsUser +func (r ChatNewParamsParamsObjectUser) MarshalJSON() (data []byte, err error) { + type shadow ChatNewParamsParamsObjectUser return param.MarshalObject(r, (*shadow)(&r)) } -func (r *ChatNewParamsUser) UnmarshalJSON(data []byte) error { +func (r *ChatNewParamsParamsObjectUser) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// The properties AccountID, ParticipantIDs, Type are required. +type ChatNewParamsParamsObject2 struct { + // Account to create or start the chat on. + AccountID string `json:"accountID" api:"required"` + // User IDs to include in the new chat. + ParticipantIDs []string `json:"participantIDs,omitzero" api:"required"` + // 'single' requires exactly one participantID; 'group' supports multiple + // participants and optional title. + // + // Any of "single", "group". + Type string `json:"type,omitzero" api:"required"` + // Optional first message content if the platform requires it to create the chat. + MessageText param.Opt[string] `json:"messageText,omitzero"` + // Optional title for group chats; ignored for single chats on most platforms. + Title param.Opt[string] `json:"title,omitzero"` + // Operation mode. Defaults to 'create' when omitted. + // + // Any of "create". + Mode string `json:"mode,omitzero"` + paramObj +} + +func (r ChatNewParamsParamsObject2) MarshalJSON() (data []byte, err error) { + type shadow ChatNewParamsParamsObject2 + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatNewParamsParamsObject2) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func init() { + apijson.RegisterFieldValidator[ChatNewParamsParamsObject2]( + "type", "single", "group", + ) + apijson.RegisterFieldValidator[ChatNewParamsParamsObject2]( + "mode", "create", + ) +} + type ChatGetParams struct { // Maximum number of participants to return. Use -1 for all; otherwise 0–500. // Defaults to all (-1). diff --git a/chat_test.go b/chat_test.go index 9e32411..ea760af 100644 --- a/chat_test.go +++ b/chat_test.go @@ -27,19 +27,18 @@ func TestChatNewWithOptionalParams(t *testing.T) { option.WithAccessToken("My Access Token"), ) _, err := client.Chats.New(context.TODO(), beeperdesktopapi.ChatNewParams{ - AccountID: "accountID", - AllowInvite: beeperdesktopapi.Bool(true), - MessageText: beeperdesktopapi.String("messageText"), - Mode: beeperdesktopapi.ChatNewParamsModeCreate, - ParticipantIDs: []string{"string"}, - Title: beeperdesktopapi.String("title"), - Type: beeperdesktopapi.ChatNewParamsTypeSingle, - User: beeperdesktopapi.ChatNewParamsUser{ - ID: beeperdesktopapi.String("id"), - Email: beeperdesktopapi.String("email"), - FullName: beeperdesktopapi.String("fullName"), - PhoneNumber: beeperdesktopapi.String("phoneNumber"), - Username: beeperdesktopapi.String("username"), + OfObject: &beeperdesktopapi.ChatNewParamsParamsObject{ + AccountID: "accountID", + Mode: "start", + User: beeperdesktopapi.ChatNewParamsParamsObjectUser{ + ID: beeperdesktopapi.String("id"), + Email: beeperdesktopapi.String("email"), + FullName: beeperdesktopapi.String("fullName"), + PhoneNumber: beeperdesktopapi.String("phoneNumber"), + Username: beeperdesktopapi.String("username"), + }, + AllowInvite: beeperdesktopapi.Bool(true), + MessageText: beeperdesktopapi.String("messageText"), }, }) if err != nil { From 423bb69d2de050be324c3389a759747693eb55ed Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:32:41 +0000 Subject: [PATCH 29/42] chore(tests): bump steady to v0.22.1 --- scripts/mock | 6 +++--- scripts/test | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mock b/scripts/mock index 7c58865..9c7c439 100755 --- a/scripts/mock +++ b/scripts/mock @@ -22,9 +22,9 @@ echo "==> Starting mock server with URL ${URL}" # Run steady mock on the given spec if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout - npm exec --package=@stdy/cli@0.20.2 -- steady --version + npm exec --package=@stdy/cli@0.22.1 -- steady --version - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.20.2 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index 3c31f6d..8b48c7f 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.20.2 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-form-array-format=repeat --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 From 25b94e4324b7c4b7f0c8decbaed4cd8ed979a3b4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 02:11:34 +0000 Subject: [PATCH 30/42] chore(internal): more robust bootstrap script --- scripts/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bootstrap b/scripts/bootstrap index 5ab3066..46547f1 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -4,7 +4,7 @@ set -e cd "$(dirname "$0")/.." -if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then brew bundle check >/dev/null 2>&1 || { echo -n "==> Install Homebrew dependencies? (y/N): " read -r response From 7c347297b7df763e97e3554777ffebdecd37009c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 02:08:25 +0000 Subject: [PATCH 31/42] feat(go): add default http client with timeout --- client.go | 2 +- default_http_client.go | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 default_http_client.go diff --git a/client.go b/client.go index 6ccd7c0..9e53d05 100644 --- a/client.go +++ b/client.go @@ -32,7 +32,7 @@ type Client struct { // DefaultClientOptions read from the environment (BEEPER_ACCESS_TOKEN, // BEEPER_DESKTOP_BASE_URL). This should be used to initialize new clients. func DefaultClientOptions() []option.RequestOption { - defaults := []option.RequestOption{option.WithEnvironmentLocal()} + defaults := []option.RequestOption{option.WithHTTPClient(defaultHTTPClient()), option.WithEnvironmentLocal()} if o, ok := os.LookupEnv("BEEPER_DESKTOP_BASE_URL"); ok { defaults = append(defaults, option.WithBaseURL(o)) } diff --git a/default_http_client.go b/default_http_client.go new file mode 100644 index 0000000..a05bc48 --- /dev/null +++ b/default_http_client.go @@ -0,0 +1,24 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package beeperdesktopapi + +import ( + "net/http" + "time" +) + +// defaultResponseHeaderTimeout bounds the time between a fully written request +// and the server's response headers. It does not apply to the response body, +// so long-running streams are unaffected. Without this, a server that accepts +// the connection but never responds would hang the request indefinitely. +const defaultResponseHeaderTimeout = 10 * time.Minute + +// defaultHTTPClient returns an [*http.Client] used when the caller does not +// supply one via [option.WithHTTPClient]. It clones [http.DefaultTransport] +// and adds a [http.Transport.ResponseHeaderTimeout] so stuck connections +// fail fast instead of compounding across retries. +func defaultHTTPClient() *http.Client { + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout + return &http.Client{Transport: transport} +} From d4fdbe157b2634c7e0c16be61583f9ebb99a8ed4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:07:11 +0000 Subject: [PATCH 32/42] feat: support setting headers via env --- client.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client.go b/client.go index 9e53d05..734b570 100644 --- a/client.go +++ b/client.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "slices" + "strings" "github.com/beeper/desktop-api-go/internal/requestconfig" "github.com/beeper/desktop-api-go/option" @@ -39,6 +40,14 @@ func DefaultClientOptions() []option.RequestOption { if o, ok := os.LookupEnv("BEEPER_ACCESS_TOKEN"); ok { defaults = append(defaults, option.WithAccessToken(o)) } + if o, ok := os.LookupEnv("BEEPER_DESKTOP_CUSTOM_HEADERS"); ok { + for _, line := range strings.Split(o, "\n") { + colon := strings.Index(line, ":") + if colon >= 0 { + defaults = append(defaults, option.WithHeader(strings.TrimSpace(line[:colon]), strings.TrimSpace(line[colon+1:]))) + } + } + } return defaults } From bfbab4197f777658cb8b7a7890aba85225cfab03 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:50:28 +0000 Subject: [PATCH 33/42] Update Desktop API Stainless config and OpenAPI spec --- .github/workflows/detect-breaking-changes.yml | 33 +++++ .stats.yml | 4 +- account.go | 19 +-- api.md | 4 +- asset.go | 8 +- asset_test.go | 30 +++- chat.go | 138 +++++++----------- chat_test.go | 25 ++-- chatmessagereaction.go | 2 +- client.go | 10 +- info.go | 3 +- internal/requestconfig/requestconfig.go | 36 +++++ message.go | 10 +- option/requestoption.go | 2 +- packages/pagination/pagination.go | 107 -------------- scripts/detect-breaking-changes | 33 +++++ 16 files changed, 219 insertions(+), 245 deletions(-) create mode 100644 .github/workflows/detect-breaking-changes.yml create mode 100755 scripts/detect-breaking-changes diff --git a/.github/workflows/detect-breaking-changes.yml b/.github/workflows/detect-breaking-changes.yml new file mode 100644 index 0000000..c6d5f5b --- /dev/null +++ b/.github/workflows/detect-breaking-changes.yml @@ -0,0 +1,33 @@ +name: CI +on: + pull_request: + branches: + - main + - next + +jobs: + detect_breaking_changes: + runs-on: 'ubuntu-latest' + name: detect-breaking-changes + if: github.repository == 'beeper/desktop-api-go' + steps: + - name: Calculate fetch-depth + run: | + echo "FETCH_DEPTH=$(expr ${{ github.event.pull_request.commits }} + 1)" >> $GITHUB_ENV + + - uses: actions/checkout@v6 + with: + # Ensure we can check out the pull request base in the script below. + fetch-depth: ${{ env.FETCH_DEPTH }} + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + + - name: Detect breaking changes + run: | + # Try to check out previous versions of the breaking change detection script. This ensures that + # we still detect breaking changes when entire files and their tests are removed. + git checkout "${{ github.event.pull_request.base.sha }}" -- ./scripts/detect-breaking-changes 2>/dev/null || true + ./scripts/detect-breaking-changes ${{ github.event.pull_request.base.sha }} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 229f6b5..e925f68 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml -openapi_spec_hash: d6c0a1776048dab04f6c5625c9893c9c -config_hash: 39ed0717b5f415499aaace2468346e1a +openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef +config_hash: 05ebdec072113f63395372504da98192 diff --git a/account.go b/account.go index eb19809..825691a 100644 --- a/account.go +++ b/account.go @@ -47,22 +47,23 @@ func (r *AccountService) List(ctx context.Context, opts ...option.RequestOption) return res, err } -// A chat account added to Beeper +// A chat account added to Beeper. type Account struct { // Chat account added to Beeper. Use this to route account-scoped actions. AccountID string `json:"accountID" api:"required"` - // Bridge metadata for the account. Available from Beeper Desktop v.4.2.719+. + // Bridge metadata for the account. Available in Beeper Desktop v4.2.789+. Bridge AccountBridge `json:"bridge" api:"required"` - // Human-friendly network name for the account. - Network string `json:"network" api:"required"` // User the account belongs to. User shared.User `json:"user" api:"required"` + // Human-friendly network name for the account. Omitted when the network is + // unknown. + Network string `json:"network"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { AccountID respjson.Field Bridge respjson.Field - Network respjson.Field User respjson.Field + Network respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -74,15 +75,15 @@ func (r *Account) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Bridge metadata for the account. Available from Beeper Desktop v.4.2.719+. +// Bridge metadata for the account. Available in Beeper Desktop v4.2.789+. type AccountBridge struct { - // Bridge instance identifier. + // Bridge instance identifier. Available in Beeper Desktop v4.2.789+. ID string `json:"id" api:"required"` - // Bridge provider for the account. + // Bridge provider for the account. Available in Beeper Desktop v4.2.789+. // // Any of "cloud", "self-hosted", "local", "platform-sdk". Provider string `json:"provider" api:"required"` - // Bridge type. + // Bridge type. Available in Beeper Desktop v4.2.789+. Type string `json:"type" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { diff --git a/api.md b/api.md index 209be51..6c1eff8 100644 --- a/api.md +++ b/api.md @@ -85,7 +85,7 @@ Response Types: Methods: - client.Messages.Update(ctx context.Context, messageID string, params beeperdesktopapi.MessageUpdateParams) (\*beeperdesktopapi.MessageUpdateResponse, error) -- client.Messages.List(ctx context.Context, chatID string, query beeperdesktopapi.MessageListParams) (\*pagination.CursorSortKey[shared.Message], error) +- client.Messages.List(ctx context.Context, chatID string, query beeperdesktopapi.MessageListParams) (\*pagination.CursorNoLimit[shared.Message], error) - client.Messages.Search(ctx context.Context, query beeperdesktopapi.MessageSearchParams) (\*pagination.CursorSearch[shared.Message], error) - client.Messages.Send(ctx context.Context, chatID string, body beeperdesktopapi.MessageSendParams) (\*beeperdesktopapi.MessageSendResponse, error) @@ -100,7 +100,7 @@ Response Types: Methods: - client.Assets.Download(ctx context.Context, body beeperdesktopapi.AssetDownloadParams) (\*beeperdesktopapi.AssetDownloadResponse, error) -- client.Assets.Serve(ctx context.Context, query beeperdesktopapi.AssetServeParams) error +- client.Assets.Serve(ctx context.Context, query beeperdesktopapi.AssetServeParams) (\*http.Response, error) - client.Assets.Upload(ctx context.Context, body beeperdesktopapi.AssetUploadParams) (\*beeperdesktopapi.AssetUploadResponse, error) - client.Assets.UploadBase64(ctx context.Context, body beeperdesktopapi.AssetUploadBase64Params) (\*beeperdesktopapi.AssetUploadBase64Response, error) diff --git a/asset.go b/asset.go index da268b1..0f2c5ba 100644 --- a/asset.go +++ b/asset.go @@ -52,12 +52,12 @@ func (r *AssetService) Download(ctx context.Context, body AssetDownloadParams, o // Stream a file given an mxc://, localmxc://, or file:// URL. Downloads first if // not cached. Supports Range requests for seeking in large files. -func (r *AssetService) Serve(ctx context.Context, query AssetServeParams, opts ...option.RequestOption) (err error) { +func (r *AssetService) Serve(ctx context.Context, query AssetServeParams, opts ...option.RequestOption) (res *http.Response, err error) { opts = slices.Concat(r.Options, opts) - opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) + opts = append([]option.RequestOption{option.WithHeader("Accept", "application/octet-stream")}, opts...) path := "v1/assets/serve" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, nil, opts...) - return err + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return res, err } // Upload a file to a temporary location using multipart/form-data. Returns an diff --git a/asset_test.go b/asset_test.go index 5ebe90c..3b7face 100644 --- a/asset_test.go +++ b/asset_test.go @@ -7,6 +7,8 @@ import ( "context" "errors" "io" + "net/http" + "net/http/httptest" "os" "testing" @@ -40,18 +42,17 @@ func TestAssetDownload(t *testing.T) { } func TestAssetServe(t *testing.T) { - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("abc")) + })) + defer server.Close() + baseURL := server.URL client := beeperdesktopapi.NewClient( option.WithBaseURL(baseURL), option.WithAccessToken("My Access Token"), ) - err := client.Assets.Serve(context.TODO(), beeperdesktopapi.AssetServeParams{ + resp, err := client.Assets.Serve(context.TODO(), beeperdesktopapi.AssetServeParams{ URL: "x", }) if err != nil { @@ -61,6 +62,19 @@ func TestAssetServe(t *testing.T) { } t.Fatalf("err should be nil: %s", err.Error()) } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } + if !bytes.Equal(b, []byte("abc")) { + t.Fatalf("return value not %s: %s", "abc", b) + } } func TestAssetUploadWithOptionalParams(t *testing.T) { diff --git a/chat.go b/chat.go index 593a240..741c926 100644 --- a/chat.go +++ b/chat.go @@ -48,8 +48,8 @@ func NewChatService(opts ...option.RequestOption) (r ChatService) { return } -// Create a single/group chat (mode='create') or start a direct chat from merged -// user data (mode='start'). +// Create a direct or group chat with mode="create", or use mode="start" to resolve +// a contact and open a direct chat. func (r *ChatService) New(ctx context.Context, body ChatNewParams, opts ...option.RequestOption) (res *ChatNewResponse, err error) { opts = slices.Concat(r.Options, opts) path := "v1/chats" @@ -108,8 +108,7 @@ func (r *ChatService) Archive(ctx context.Context, chatID string, body ChatArchi return err } -// Search chats by title/network or participants using Beeper Desktop's renderer -// algorithm. +// Search chats by title, network, or participant names. func (r *ChatService) Search(ctx context.Context, query ChatSearchParams, opts ...option.RequestOption) (res *pagination.CursorSearch[Chat], err error) { var raw *http.Response opts = slices.Concat(r.Options, opts) @@ -127,8 +126,7 @@ func (r *ChatService) Search(ctx context.Context, query ChatSearchParams, opts . return res, nil } -// Search chats by title/network or participants using Beeper Desktop's renderer -// algorithm. +// Search chats by title, network, or participant names. func (r *ChatService) SearchAutoPaging(ctx context.Context, query ChatSearchParams, opts ...option.RequestOption) *pagination.CursorSearchAutoPager[Chat] { return pagination.NewCursorSearchAutoPager(r.Search(ctx, query, opts...)) } @@ -268,60 +266,63 @@ func (r *ChatListResponse) UnmarshalJSON(data []byte) error { } type ChatNewParams struct { - - // - // Request body variants - // - - // This field is a request body variant, only one variant field can be set. - OfObject *ChatNewParamsParamsObject `json:",inline"` - // This field is a request body variant, only one variant field can be set. - OfChatNewsParamsObject2 *ChatNewParamsParamsObject2 `json:",inline"` - - paramObj -} - -func (u ChatNewParams) MarshalJSON() ([]byte, error) { - return param.MarshalUnion(u, u.OfObject, u.OfChatNewsParamsObject2) -} -func (r *ChatNewParams) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -// The properties AccountID, Mode, User are required. -type ChatNewParamsParamsObject struct { // Account to create or start the chat on. AccountID string `json:"accountID" api:"required"` - // Operation mode. Use 'start' to resolve a user/contact and start a direct chat. - // - // Any of "start". - Mode string `json:"mode,omitzero" api:"required"` - // Merged user-like contact payload used to resolve the best identifier. - User ChatNewParamsParamsObjectUser `json:"user,omitzero" api:"required"` - // Whether invite-based DM creation is allowed when required by the platform. Used - // for mode='start'. + // Only used for mode='start'. Whether invite-based DM creation is allowed when + // required by the platform. AllowInvite param.Opt[bool] `json:"allowInvite,omitzero"` // Optional first message content if the platform requires it to create the chat. MessageText param.Opt[string] `json:"messageText,omitzero"` + // Optional title for group chats; ignored for single chats on most networks. + Title param.Opt[string] `json:"title,omitzero"` + // Operation mode. Use 'start' to resolve a user/contact and start a direct chat. + // Omit or set 'create' to create a chat directly. + // + // Any of "start", "create". + Mode ChatNewParamsMode `json:"mode,omitzero"` + // Required for create mode. Provide exactly one user ID for 'single' chats and one + // or more for 'group' chats. + ParticipantIDs []string `json:"participantIDs,omitzero"` + // Required for create mode. 'single' creates a direct message chat; 'group' + // creates a group chat. + // + // Any of "single", "group". + Type ChatNewParamsType `json:"type,omitzero"` + // Required for mode='start'. Merged user-like contact payload used to resolve the + // best identifier. + User ChatNewParamsUser `json:"user,omitzero"` paramObj } -func (r ChatNewParamsParamsObject) MarshalJSON() (data []byte, err error) { - type shadow ChatNewParamsParamsObject +func (r ChatNewParams) MarshalJSON() (data []byte, err error) { + type shadow ChatNewParams return param.MarshalObject(r, (*shadow)(&r)) } -func (r *ChatNewParamsParamsObject) UnmarshalJSON(data []byte) error { +func (r *ChatNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -func init() { - apijson.RegisterFieldValidator[ChatNewParamsParamsObject]( - "mode", "start", - ) -} +// Operation mode. Use 'start' to resolve a user/contact and start a direct chat. +// Omit or set 'create' to create a chat directly. +type ChatNewParamsMode string -// Merged user-like contact payload used to resolve the best identifier. -type ChatNewParamsParamsObjectUser struct { +const ( + ChatNewParamsModeStart ChatNewParamsMode = "start" + ChatNewParamsModeCreate ChatNewParamsMode = "create" +) + +// Required for create mode. 'single' creates a direct message chat; 'group' +// creates a group chat. +type ChatNewParamsType string + +const ( + ChatNewParamsTypeSingle ChatNewParamsType = "single" + ChatNewParamsTypeGroup ChatNewParamsType = "group" +) + +// Required for mode='start'. Merged user-like contact payload used to resolve the +// best identifier. +type ChatNewParamsUser struct { // Known user ID when available. ID param.Opt[string] `json:"id,omitzero"` // Email candidate. @@ -335,53 +336,14 @@ type ChatNewParamsParamsObjectUser struct { paramObj } -func (r ChatNewParamsParamsObjectUser) MarshalJSON() (data []byte, err error) { - type shadow ChatNewParamsParamsObjectUser +func (r ChatNewParamsUser) MarshalJSON() (data []byte, err error) { + type shadow ChatNewParamsUser return param.MarshalObject(r, (*shadow)(&r)) } -func (r *ChatNewParamsParamsObjectUser) UnmarshalJSON(data []byte) error { +func (r *ChatNewParamsUser) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// The properties AccountID, ParticipantIDs, Type are required. -type ChatNewParamsParamsObject2 struct { - // Account to create or start the chat on. - AccountID string `json:"accountID" api:"required"` - // User IDs to include in the new chat. - ParticipantIDs []string `json:"participantIDs,omitzero" api:"required"` - // 'single' requires exactly one participantID; 'group' supports multiple - // participants and optional title. - // - // Any of "single", "group". - Type string `json:"type,omitzero" api:"required"` - // Optional first message content if the platform requires it to create the chat. - MessageText param.Opt[string] `json:"messageText,omitzero"` - // Optional title for group chats; ignored for single chats on most platforms. - Title param.Opt[string] `json:"title,omitzero"` - // Operation mode. Defaults to 'create' when omitted. - // - // Any of "create". - Mode string `json:"mode,omitzero"` - paramObj -} - -func (r ChatNewParamsParamsObject2) MarshalJSON() (data []byte, err error) { - type shadow ChatNewParamsParamsObject2 - return param.MarshalObject(r, (*shadow)(&r)) -} -func (r *ChatNewParamsParamsObject2) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -func init() { - apijson.RegisterFieldValidator[ChatNewParamsParamsObject2]( - "type", "single", "group", - ) - apijson.RegisterFieldValidator[ChatNewParamsParamsObject2]( - "mode", "create", - ) -} - type ChatGetParams struct { // Maximum number of participants to return. Use -1 for all; otherwise 0–500. // Defaults to all (-1). diff --git a/chat_test.go b/chat_test.go index ea760af..6b771a9 100644 --- a/chat_test.go +++ b/chat_test.go @@ -27,18 +27,19 @@ func TestChatNewWithOptionalParams(t *testing.T) { option.WithAccessToken("My Access Token"), ) _, err := client.Chats.New(context.TODO(), beeperdesktopapi.ChatNewParams{ - OfObject: &beeperdesktopapi.ChatNewParamsParamsObject{ - AccountID: "accountID", - Mode: "start", - User: beeperdesktopapi.ChatNewParamsParamsObjectUser{ - ID: beeperdesktopapi.String("id"), - Email: beeperdesktopapi.String("email"), - FullName: beeperdesktopapi.String("fullName"), - PhoneNumber: beeperdesktopapi.String("phoneNumber"), - Username: beeperdesktopapi.String("username"), - }, - AllowInvite: beeperdesktopapi.Bool(true), - MessageText: beeperdesktopapi.String("messageText"), + AccountID: "accountID", + AllowInvite: beeperdesktopapi.Bool(true), + MessageText: beeperdesktopapi.String("messageText"), + Mode: beeperdesktopapi.ChatNewParamsModeStart, + ParticipantIDs: []string{"string"}, + Title: beeperdesktopapi.String("title"), + Type: beeperdesktopapi.ChatNewParamsTypeSingle, + User: beeperdesktopapi.ChatNewParamsUser{ + ID: beeperdesktopapi.String("id"), + Email: beeperdesktopapi.String("email"), + FullName: beeperdesktopapi.String("fullName"), + PhoneNumber: beeperdesktopapi.String("phoneNumber"), + Username: beeperdesktopapi.String("username"), }, }) if err != nil { diff --git a/chatmessagereaction.go b/chatmessagereaction.go index a567da0..25299ed 100644 --- a/chatmessagereaction.go +++ b/chatmessagereaction.go @@ -39,7 +39,7 @@ func NewChatMessageReactionService(opts ...option.RequestOption) (r ChatMessageR return } -// Remove the authenticated user's reaction from an existing message. +// Remove the reaction added by the authenticated user from an existing message. func (r *ChatMessageReactionService) Delete(ctx context.Context, messageID string, params ChatMessageReactionDeleteParams, opts ...option.RequestOption) (res *ChatMessageReactionDeleteResponse, err error) { opts = slices.Concat(r.Options, opts) if params.ChatID == "" { diff --git a/client.go b/client.go index 734b570..8f60a56 100644 --- a/client.go +++ b/client.go @@ -31,16 +31,16 @@ type Client struct { } // DefaultClientOptions read from the environment (BEEPER_ACCESS_TOKEN, -// BEEPER_DESKTOP_BASE_URL). This should be used to initialize new clients. +// BEEPER_BASE_URL). This should be used to initialize new clients. func DefaultClientOptions() []option.RequestOption { defaults := []option.RequestOption{option.WithHTTPClient(defaultHTTPClient()), option.WithEnvironmentLocal()} - if o, ok := os.LookupEnv("BEEPER_DESKTOP_BASE_URL"); ok { + if o, ok := os.LookupEnv("BEEPER_BASE_URL"); ok { defaults = append(defaults, option.WithBaseURL(o)) } if o, ok := os.LookupEnv("BEEPER_ACCESS_TOKEN"); ok { defaults = append(defaults, option.WithAccessToken(o)) } - if o, ok := os.LookupEnv("BEEPER_DESKTOP_CUSTOM_HEADERS"); ok { + if o, ok := os.LookupEnv("BEEPER_CUSTOM_HEADERS"); ok { for _, line := range strings.Split(o, "\n") { colon := strings.Index(line, ":") if colon >= 0 { @@ -52,8 +52,8 @@ func DefaultClientOptions() []option.RequestOption { } // NewClient generates a new client with the default option read from the -// environment (BEEPER_ACCESS_TOKEN, BEEPER_DESKTOP_BASE_URL). The option passed in -// as arguments are applied after these default arguments, and all option will be +// environment (BEEPER_ACCESS_TOKEN, BEEPER_BASE_URL). The option passed in as +// arguments are applied after these default arguments, and all option will be // passed down to the services and requests that this client makes. func NewClient(opts ...option.RequestOption) (r Client) { opts = append(DefaultClientOptions(), opts...) diff --git a/info.go b/info.go index e95dfbf..2871536 100644 --- a/info.go +++ b/info.go @@ -37,7 +37,8 @@ func NewInfoService(opts ...option.RequestOption) (r InfoService) { // Returns app, platform, server, and endpoint discovery metadata for this Beeper // Desktop instance. func (r *InfoService) Get(ctx context.Context, opts ...option.RequestOption) (res *InfoGetResponse, err error) { - opts = slices.Concat(r.Options, opts) + var preClientOpts = []option.RequestOption{requestconfig.WithSecurity(requestconfig.Security{})} + opts = slices.Concat(preClientOpts, r.Options, opts) path := "v1/info" err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) return res, err diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index c80db54..ebeff43 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -177,11 +177,17 @@ func NewRequestConfig(ctx context.Context, method string, u string, body any, ds Body: reader, } cfg.ResponseBodyInto = dst + cfg.Security = Security{ + BearerAuth: true, + } err = cfg.Apply(opts...) if err != nil { return nil, err } + // This must run after `cfg.Apply(...)` above so we know which specific security scheme to add + ApplySecurity(cfg) + // This must run after `cfg.Apply(...)` above in case the request timeout gets modified. We also only // apply our own logic for it if it's still "0" from above. If it's not, then it was deleted or modified // by the user and we should respect that. @@ -219,6 +225,8 @@ type RequestConfig struct { HTTPClient *http.Client Middlewares []middleware AccessToken string + // Configure which security scheme(s) should be enabled for this request + Security Security // If ResponseBodyInto not nil, then we will attempt to deserialize into // ResponseBodyInto. If Destination is a []byte, then it will return the body as // is. @@ -640,3 +648,31 @@ func WithDefaultBaseURL(baseURL string) RequestOption { return nil }) } + +type Security struct { + BearerAuth bool +} + +func WithSecurity(security Security) RequestOption { + return RequestOptionFunc(func(r *RequestConfig) error { + r.Security = security + return nil + }) +} + +// WithBearerAuthSecurity() should only be used within a method, not provided to at +// the client-level. +func WithBearerAuthSecurity() RequestOption { + return RequestOptionFunc(func(r *RequestConfig) error { + r.Security = Security{ + BearerAuth: true, + } + return nil + }) +} + +func ApplySecurity(r RequestConfig) { + if r.Security.BearerAuth && r.AccessToken != "" && r.Request.Header.Get("Authorization") == "" { + r.Request.Header.Set("authorization", fmt.Sprintf("Bearer %s", r.AccessToken)) + } +} diff --git a/message.go b/message.go index 68799a9..2ebbbb4 100644 --- a/message.go +++ b/message.go @@ -60,7 +60,7 @@ func (r *MessageService) Update(ctx context.Context, messageID string, params Me } // List all messages in a chat with cursor-based pagination. Sorted by timestamp. -func (r *MessageService) List(ctx context.Context, chatID string, query MessageListParams, opts ...option.RequestOption) (res *pagination.CursorSortKey[shared.Message], err error) { +func (r *MessageService) List(ctx context.Context, chatID string, query MessageListParams, opts ...option.RequestOption) (res *pagination.CursorNoLimit[shared.Message], err error) { var raw *http.Response opts = slices.Concat(r.Options, opts) opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...) @@ -82,11 +82,11 @@ func (r *MessageService) List(ctx context.Context, chatID string, query MessageL } // List all messages in a chat with cursor-based pagination. Sorted by timestamp. -func (r *MessageService) ListAutoPaging(ctx context.Context, chatID string, query MessageListParams, opts ...option.RequestOption) *pagination.CursorSortKeyAutoPager[shared.Message] { - return pagination.NewCursorSortKeyAutoPager(r.List(ctx, chatID, query, opts...)) +func (r *MessageService) ListAutoPaging(ctx context.Context, chatID string, query MessageListParams, opts ...option.RequestOption) *pagination.CursorNoLimitAutoPager[shared.Message] { + return pagination.NewCursorNoLimitAutoPager(r.List(ctx, chatID, query, opts...)) } -// Search messages across chats using Beeper's message index +// Search messages across chats. func (r *MessageService) Search(ctx context.Context, query MessageSearchParams, opts ...option.RequestOption) (res *pagination.CursorSearch[shared.Message], err error) { var raw *http.Response opts = slices.Concat(r.Options, opts) @@ -104,7 +104,7 @@ func (r *MessageService) Search(ctx context.Context, query MessageSearchParams, return res, nil } -// Search messages across chats using Beeper's message index +// Search messages across chats. func (r *MessageService) SearchAutoPaging(ctx context.Context, query MessageSearchParams, opts ...option.RequestOption) *pagination.CursorSearchAutoPager[shared.Message] { return pagination.NewCursorSearchAutoPager(r.Search(ctx, query, opts...)) } diff --git a/option/requestoption.go b/option/requestoption.go index 8c24072..6093527 100644 --- a/option/requestoption.go +++ b/option/requestoption.go @@ -270,6 +270,6 @@ func WithEnvironmentLocal() RequestOption { func WithAccessToken(value string) RequestOption { return requestconfig.RequestOptionFunc(func(r *requestconfig.RequestConfig) error { r.AccessToken = value - return r.Apply(WithHeader("authorization", fmt.Sprintf("Bearer %s", r.AccessToken))) + return nil }) } diff --git a/packages/pagination/pagination.go b/packages/pagination/pagination.go index fd2a5d9..49b2e64 100644 --- a/packages/pagination/pagination.go +++ b/packages/pagination/pagination.go @@ -4,7 +4,6 @@ package pagination import ( "net/http" - "reflect" "github.com/beeper/desktop-api-go/internal/apijson" "github.com/beeper/desktop-api-go/internal/requestconfig" @@ -234,109 +233,3 @@ func (r *CursorNoLimitAutoPager[T]) Err() error { func (r *CursorNoLimitAutoPager[T]) Index() int { return r.run } - -type CursorSortKey[T any] struct { - Items []T `json:"items"` - HasMore bool `json:"hasMore"` - // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. - JSON struct { - Items respjson.Field - HasMore respjson.Field - ExtraFields map[string]respjson.Field - raw string - } `json:"-"` - cfg *requestconfig.RequestConfig - res *http.Response -} - -// Returns the unmodified JSON received from the API -func (r CursorSortKey[T]) RawJSON() string { return r.JSON.raw } -func (r *CursorSortKey[T]) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -// GetNextPage returns the next page as defined by this pagination style. When -// there is no next page, this function will return a 'nil' for the page value, but -// will not return an error -func (r *CursorSortKey[T]) GetNextPage() (res *CursorSortKey[T], err error) { - if len(r.Items) == 0 { - return nil, nil - } - - if r.JSON.HasMore.Valid() && r.HasMore == false { - return nil, nil - } - items := r.Items - if items == nil || len(items) == 0 { - return nil, nil - } - cfg := r.cfg.Clone(r.cfg.Context) - value := reflect.ValueOf(items[len(items)-1]) - field := value.FieldByName("SortKey") - err = cfg.Apply(option.WithQuery("cursor", field.Interface().(string))) - if err != nil { - return nil, err - } - var raw *http.Response - cfg.ResponseInto = &raw - cfg.ResponseBodyInto = &res - err = cfg.Execute() - if err != nil { - return nil, err - } - res.SetPageConfig(cfg, raw) - return res, nil -} - -func (r *CursorSortKey[T]) SetPageConfig(cfg *requestconfig.RequestConfig, res *http.Response) { - if r == nil { - r = &CursorSortKey[T]{} - } - r.cfg = cfg - r.res = res -} - -type CursorSortKeyAutoPager[T any] struct { - page *CursorSortKey[T] - cur T - idx int - run int - err error - paramObj -} - -func NewCursorSortKeyAutoPager[T any](page *CursorSortKey[T], err error) *CursorSortKeyAutoPager[T] { - return &CursorSortKeyAutoPager[T]{ - page: page, - err: err, - } -} - -func (r *CursorSortKeyAutoPager[T]) Next() bool { - if r.page == nil || len(r.page.Items) == 0 { - return false - } - if r.idx >= len(r.page.Items) { - r.idx = 0 - r.page, r.err = r.page.GetNextPage() - if r.err != nil || r.page == nil || len(r.page.Items) == 0 { - return false - } - } - r.cur = r.page.Items[r.idx] - r.run += 1 - r.idx += 1 - return true -} - -func (r *CursorSortKeyAutoPager[T]) Current() T { - return r.cur -} - -func (r *CursorSortKeyAutoPager[T]) Err() error { - return r.err -} - -func (r *CursorSortKeyAutoPager[T]) Index() int { - return r.run -} diff --git a/scripts/detect-breaking-changes b/scripts/detect-breaking-changes new file mode 100755 index 0000000..a0655e0 --- /dev/null +++ b/scripts/detect-breaking-changes @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Detecting breaking changes" + +TEST_PATHS=( + client_test.go + beeperdesktopapi_test.go + account_test.go + accountcontact_test.go + chat_test.go + chatreminder_test.go + chatmessagereaction_test.go + message_test.go + asset_test.go + info_test.go + usage_test.go + paginationauto_test.go + paginationmanual_test.go +) + +for PATHSPEC in "${TEST_PATHS[@]}"; do + # Try to check out previous versions of the test files + # with the current SDK. + git checkout "$1" -- "${PATHSPEC}" 2>/dev/null || true +done + +# Instead of running the tests, use the linter to check if an +# older test is no longer compatible with the latest SDK. +./scripts/lint From 16eed65d5f6d4c668b6e49d56d13fc3c3a6096c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:53:09 +0000 Subject: [PATCH 34/42] Preserve asset serve SDK compatibility --- .stats.yml | 2 +- api.md | 2 +- asset.go | 8 ++++---- asset_test.go | 30 ++++++++---------------------- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/.stats.yml b/.stats.yml index e925f68..1d3cc36 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml -openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef +openapi_spec_hash: 8dff13848934c5b20f3804236e8286d3 config_hash: 05ebdec072113f63395372504da98192 diff --git a/api.md b/api.md index 6c1eff8..e46c562 100644 --- a/api.md +++ b/api.md @@ -100,7 +100,7 @@ Response Types: Methods: - client.Assets.Download(ctx context.Context, body beeperdesktopapi.AssetDownloadParams) (\*beeperdesktopapi.AssetDownloadResponse, error) -- client.Assets.Serve(ctx context.Context, query beeperdesktopapi.AssetServeParams) (\*http.Response, error) +- client.Assets.Serve(ctx context.Context, query beeperdesktopapi.AssetServeParams) error - client.Assets.Upload(ctx context.Context, body beeperdesktopapi.AssetUploadParams) (\*beeperdesktopapi.AssetUploadResponse, error) - client.Assets.UploadBase64(ctx context.Context, body beeperdesktopapi.AssetUploadBase64Params) (\*beeperdesktopapi.AssetUploadBase64Response, error) diff --git a/asset.go b/asset.go index 0f2c5ba..da268b1 100644 --- a/asset.go +++ b/asset.go @@ -52,12 +52,12 @@ func (r *AssetService) Download(ctx context.Context, body AssetDownloadParams, o // Stream a file given an mxc://, localmxc://, or file:// URL. Downloads first if // not cached. Supports Range requests for seeking in large files. -func (r *AssetService) Serve(ctx context.Context, query AssetServeParams, opts ...option.RequestOption) (res *http.Response, err error) { +func (r *AssetService) Serve(ctx context.Context, query AssetServeParams, opts ...option.RequestOption) (err error) { opts = slices.Concat(r.Options, opts) - opts = append([]option.RequestOption{option.WithHeader("Accept", "application/octet-stream")}, opts...) + opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) path := "v1/assets/serve" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) - return res, err + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, nil, opts...) + return err } // Upload a file to a temporary location using multipart/form-data. Returns an diff --git a/asset_test.go b/asset_test.go index 3b7face..5ebe90c 100644 --- a/asset_test.go +++ b/asset_test.go @@ -7,8 +7,6 @@ import ( "context" "errors" "io" - "net/http" - "net/http/httptest" "os" "testing" @@ -42,17 +40,18 @@ func TestAssetDownload(t *testing.T) { } func TestAssetServe(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(200) - w.Write([]byte("abc")) - })) - defer server.Close() - baseURL := server.URL + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } client := beeperdesktopapi.NewClient( option.WithBaseURL(baseURL), option.WithAccessToken("My Access Token"), ) - resp, err := client.Assets.Serve(context.TODO(), beeperdesktopapi.AssetServeParams{ + err := client.Assets.Serve(context.TODO(), beeperdesktopapi.AssetServeParams{ URL: "x", }) if err != nil { @@ -62,19 +61,6 @@ func TestAssetServe(t *testing.T) { } t.Fatalf("err should be nil: %s", err.Error()) } - defer resp.Body.Close() - - b, err := io.ReadAll(resp.Body) - if err != nil { - var apierr *beeperdesktopapi.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } - if !bytes.Equal(b, []byte("abc")) { - t.Fatalf("return value not %s: %s", "abc", b) - } } func TestAssetUploadWithOptionalParams(t *testing.T) { From 4297fcca81065183df7c4d8fa2cb408e692ad568 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 19:54:14 +0000 Subject: [PATCH 35/42] Document asset serve stream response --- .stats.yml | 2 +- api.md | 2 +- asset.go | 8 ++++---- asset_test.go | 30 ++++++++++++++++++++++-------- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1d3cc36..e925f68 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml -openapi_spec_hash: 8dff13848934c5b20f3804236e8286d3 +openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef config_hash: 05ebdec072113f63395372504da98192 diff --git a/api.md b/api.md index e46c562..6c1eff8 100644 --- a/api.md +++ b/api.md @@ -100,7 +100,7 @@ Response Types: Methods: - client.Assets.Download(ctx context.Context, body beeperdesktopapi.AssetDownloadParams) (\*beeperdesktopapi.AssetDownloadResponse, error) -- client.Assets.Serve(ctx context.Context, query beeperdesktopapi.AssetServeParams) error +- client.Assets.Serve(ctx context.Context, query beeperdesktopapi.AssetServeParams) (\*http.Response, error) - client.Assets.Upload(ctx context.Context, body beeperdesktopapi.AssetUploadParams) (\*beeperdesktopapi.AssetUploadResponse, error) - client.Assets.UploadBase64(ctx context.Context, body beeperdesktopapi.AssetUploadBase64Params) (\*beeperdesktopapi.AssetUploadBase64Response, error) diff --git a/asset.go b/asset.go index da268b1..0f2c5ba 100644 --- a/asset.go +++ b/asset.go @@ -52,12 +52,12 @@ func (r *AssetService) Download(ctx context.Context, body AssetDownloadParams, o // Stream a file given an mxc://, localmxc://, or file:// URL. Downloads first if // not cached. Supports Range requests for seeking in large files. -func (r *AssetService) Serve(ctx context.Context, query AssetServeParams, opts ...option.RequestOption) (err error) { +func (r *AssetService) Serve(ctx context.Context, query AssetServeParams, opts ...option.RequestOption) (res *http.Response, err error) { opts = slices.Concat(r.Options, opts) - opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) + opts = append([]option.RequestOption{option.WithHeader("Accept", "application/octet-stream")}, opts...) path := "v1/assets/serve" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, nil, opts...) - return err + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &res, opts...) + return res, err } // Upload a file to a temporary location using multipart/form-data. Returns an diff --git a/asset_test.go b/asset_test.go index 5ebe90c..3b7face 100644 --- a/asset_test.go +++ b/asset_test.go @@ -7,6 +7,8 @@ import ( "context" "errors" "io" + "net/http" + "net/http/httptest" "os" "testing" @@ -40,18 +42,17 @@ func TestAssetDownload(t *testing.T) { } func TestAssetServe(t *testing.T) { - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write([]byte("abc")) + })) + defer server.Close() + baseURL := server.URL client := beeperdesktopapi.NewClient( option.WithBaseURL(baseURL), option.WithAccessToken("My Access Token"), ) - err := client.Assets.Serve(context.TODO(), beeperdesktopapi.AssetServeParams{ + resp, err := client.Assets.Serve(context.TODO(), beeperdesktopapi.AssetServeParams{ URL: "x", }) if err != nil { @@ -61,6 +62,19 @@ func TestAssetServe(t *testing.T) { } t.Fatalf("err should be nil: %s", err.Error()) } + defer resp.Body.Close() + + b, err := io.ReadAll(resp.Body) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } + if !bytes.Equal(b, []byte("abc")) { + t.Fatalf("return value not %s: %s", "abc", b) + } } func TestAssetUploadWithOptionalParams(t *testing.T) { From 6d4b2b15e6e7bb944ac2728ced75ccea86f2b1fd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 07:52:39 +0000 Subject: [PATCH 36/42] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e925f68..a2edbe5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper%2Fbeeper-desktop-api-611aa7641fbca8cf31d626bf86f9efd3c2b92778e897ebbb25c6ea44185ed1ed.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-fdefa92da44ff91a34438591d022c182a83d9daf9a28b452556e325c6ef7b3f0.yml openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef config_hash: 05ebdec072113f63395372504da98192 From 3da8f588371852b540da4c71db383d23b7c8d77d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:38:30 +0000 Subject: [PATCH 37/42] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index a2edbe5..ec75571 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-fdefa92da44ff91a34438591d022c182a83d9daf9a28b452556e325c6ef7b3f0.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-356444646dafe352d3ef7c2e01aedf030197a5519b41cf2c3fd8be2571456b43.yml openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef config_hash: 05ebdec072113f63395372504da98192 From 6a8ff6a1189beb742794e0f7b11ce2aa5651c63e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:41:44 +0000 Subject: [PATCH 38/42] chore: avoid embedding reflect.Type for dead code elimination --- internal/apiform/encoder.go | 4 ++-- internal/apijson/decoder.go | 4 ++-- internal/apijson/encoder.go | 4 ++-- internal/apiquery/encoder.go | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 12122c0..b2ac6fe 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -58,7 +58,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string arrayFmt string root bool @@ -76,7 +76,7 @@ func (e *encoder) marshal(value any, writer *multipart.Writer) error { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, arrayFmt: e.arrayFmt, root: e.root, diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index 57aef4f..e9eb7a9 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -80,7 +80,7 @@ type decoderField struct { } type decoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -108,7 +108,7 @@ func (d *decoderBuilder) unmarshalWithExactness(raw []byte, to any) (exactness, func (d *decoderBuilder) typeDecoder(t reflect.Type) decoderFunc { entry := decoderEntry{ - Type: t, + typ: t, dateFormat: d.dateFormat, root: d.root, } diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index 5fc57b4..c903d90 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -46,7 +46,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool } @@ -63,7 +63,7 @@ func (e *encoder) marshal(value any) ([]byte, error) { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, } diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 361902c..3444566 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -29,7 +29,7 @@ type encoderField struct { } type encoderEntry struct { - reflect.Type + typ reflect.Type dateFormat string root bool settings QuerySettings @@ -42,7 +42,7 @@ type Pair struct { func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ - Type: t, + typ: t, dateFormat: e.dateFormat, root: e.root, settings: e.settings, From d2d6223284dade370d2671176ff36811203d9981 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 18:06:49 +0000 Subject: [PATCH 39/42] feat(api): api update --- .github/workflows/detect-breaking-changes.yml | 33 ---- .stats.yml | 8 +- account.go | 10 +- api.md | 2 + chat.go | 150 +++++++++++------- chat_test.go | 47 ++++-- scripts/detect-breaking-changes | 33 ---- 7 files changed, 141 insertions(+), 142 deletions(-) delete mode 100644 .github/workflows/detect-breaking-changes.yml delete mode 100755 scripts/detect-breaking-changes diff --git a/.github/workflows/detect-breaking-changes.yml b/.github/workflows/detect-breaking-changes.yml deleted file mode 100644 index c6d5f5b..0000000 --- a/.github/workflows/detect-breaking-changes.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: CI -on: - pull_request: - branches: - - main - - next - -jobs: - detect_breaking_changes: - runs-on: 'ubuntu-latest' - name: detect-breaking-changes - if: github.repository == 'beeper/desktop-api-go' - steps: - - name: Calculate fetch-depth - run: | - echo "FETCH_DEPTH=$(expr ${{ github.event.pull_request.commits }} + 1)" >> $GITHUB_ENV - - - uses: actions/checkout@v6 - with: - # Ensure we can check out the pull request base in the script below. - fetch-depth: ${{ env.FETCH_DEPTH }} - - - name: Setup go - uses: actions/setup-go@v5 - with: - go-version-file: ./go.mod - - - name: Detect breaking changes - run: | - # Try to check out previous versions of the breaking change detection script. This ensures that - # we still detect breaking changes when entire files and their tests are removed. - git checkout "${{ github.event.pull_request.base.sha }}" -- ./scripts/detect-breaking-changes 2>/dev/null || true - ./scripts/detect-breaking-changes ${{ github.event.pull_request.base.sha }} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index ec75571..75ad795 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 23 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-356444646dafe352d3ef7c2e01aedf030197a5519b41cf2c3fd8be2571456b43.yml -openapi_spec_hash: 4840f003552e8b48eb8e689b59a819ef -config_hash: 05ebdec072113f63395372504da98192 +configured_endpoints: 24 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-8b89ffbfeb39b4186328fefd81f7dab1d28c786012201feb8035c7f920c4fbae.yml +openapi_spec_hash: de40e013fcc83fa44d5c51ddecb543c0 +config_hash: 08b781db5f1857ed601d1b2f4b6b45d9 diff --git a/account.go b/account.go index 825691a..80fdac1 100644 --- a/account.go +++ b/account.go @@ -51,7 +51,7 @@ func (r *AccountService) List(ctx context.Context, opts ...option.RequestOption) type Account struct { // Chat account added to Beeper. Use this to route account-scoped actions. AccountID string `json:"accountID" api:"required"` - // Bridge metadata for the account. Available in Beeper Desktop v4.2.789+. + // Bridge metadata for the account. Available in Beeper Desktop v4.2.799+. Bridge AccountBridge `json:"bridge" api:"required"` // User the account belongs to. User shared.User `json:"user" api:"required"` @@ -75,15 +75,15 @@ func (r *Account) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Bridge metadata for the account. Available in Beeper Desktop v4.2.789+. +// Bridge metadata for the account. Available in Beeper Desktop v4.2.799+. type AccountBridge struct { - // Bridge instance identifier. Available in Beeper Desktop v4.2.789+. + // Bridge instance identifier. Available in Beeper Desktop v4.2.799+. ID string `json:"id" api:"required"` - // Bridge provider for the account. Available in Beeper Desktop v4.2.789+. + // Bridge provider for the account. Available in Beeper Desktop v4.2.799+. // // Any of "cloud", "self-hosted", "local", "platform-sdk". Provider string `json:"provider" api:"required"` - // Bridge type. Available in Beeper Desktop v4.2.789+. + // Bridge type. Available in Beeper Desktop v4.2.799+. Type string `json:"type" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { diff --git a/api.md b/api.md index 6c1eff8..1c49a65 100644 --- a/api.md +++ b/api.md @@ -45,6 +45,7 @@ Response Types: - beeperdesktopapi.Chat - beeperdesktopapi.ChatNewResponse - beeperdesktopapi.ChatListResponse +- beeperdesktopapi.ChatStartResponse Methods: @@ -53,6 +54,7 @@ Methods: - client.Chats.List(ctx context.Context, query beeperdesktopapi.ChatListParams) (\*pagination.CursorNoLimit[beeperdesktopapi.ChatListResponse], error) - client.Chats.Archive(ctx context.Context, chatID string, body beeperdesktopapi.ChatArchiveParams) error - client.Chats.Search(ctx context.Context, query beeperdesktopapi.ChatSearchParams) (\*pagination.CursorSearch[beeperdesktopapi.Chat], error) +- client.Chats.Start(ctx context.Context, body beeperdesktopapi.ChatStartParams) (\*beeperdesktopapi.ChatStartResponse, error) ## Reminders diff --git a/chat.go b/chat.go index 741c926..1bf9591 100644 --- a/chat.go +++ b/chat.go @@ -48,8 +48,7 @@ func NewChatService(opts ...option.RequestOption) (r ChatService) { return } -// Create a direct or group chat with mode="create", or use mode="start" to resolve -// a contact and open a direct chat. +// Create a direct or group chat from participant IDs. func (r *ChatService) New(ctx context.Context, body ChatNewParams, opts ...option.RequestOption) (res *ChatNewResponse, err error) { opts = slices.Concat(r.Options, opts) path := "v1/chats" @@ -131,6 +130,15 @@ func (r *ChatService) SearchAutoPaging(ctx context.Context, query ChatSearchPara return pagination.NewCursorSearchAutoPager(r.Search(ctx, query, opts...)) } +// Resolve a user/contact and open a direct chat. Reuses an existing direct chat +// when one is found. Available in Beeper Desktop v4.2.799+. +func (r *ChatService) Start(ctx context.Context, body ChatStartParams, opts ...option.RequestOption) (res *ChatStartResponse, err error) { + opts = slices.Concat(r.Options, opts) + path := "v1/chats.start" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return res, err +} + type Chat struct { // Unique identifier of the chat across Beeper. ID string `json:"id" api:"required"` @@ -265,32 +273,52 @@ func (r *ChatListResponse) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type ChatStartResponse struct { + // Newly created chat ID. + ChatID string `json:"chatID" api:"required"` + // Only returned in start mode. 'existing' means an existing chat was reused; + // 'created' means a new chat was created. + // + // Any of "existing", "created". + Status ChatStartResponseStatus `json:"status"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ChatID respjson.Field + Status respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatStartResponse) RawJSON() string { return r.JSON.raw } +func (r *ChatStartResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Only returned in start mode. 'existing' means an existing chat was reused; +// 'created' means a new chat was created. +type ChatStartResponseStatus string + +const ( + ChatStartResponseStatusExisting ChatStartResponseStatus = "existing" + ChatStartResponseStatusCreated ChatStartResponseStatus = "created" +) + type ChatNewParams struct { // Account to create or start the chat on. AccountID string `json:"accountID" api:"required"` - // Only used for mode='start'. Whether invite-based DM creation is allowed when - // required by the platform. - AllowInvite param.Opt[bool] `json:"allowInvite,omitzero"` + // User IDs to include in the new chat. + ParticipantIDs []string `json:"participantIDs,omitzero" api:"required"` + // 'single' requires exactly one participantID; 'group' supports multiple + // participants and optional title. + // + // Any of "single", "group". + Type ChatNewParamsType `json:"type,omitzero" api:"required"` // Optional first message content if the platform requires it to create the chat. MessageText param.Opt[string] `json:"messageText,omitzero"` // Optional title for group chats; ignored for single chats on most networks. Title param.Opt[string] `json:"title,omitzero"` - // Operation mode. Use 'start' to resolve a user/contact and start a direct chat. - // Omit or set 'create' to create a chat directly. - // - // Any of "start", "create". - Mode ChatNewParamsMode `json:"mode,omitzero"` - // Required for create mode. Provide exactly one user ID for 'single' chats and one - // or more for 'group' chats. - ParticipantIDs []string `json:"participantIDs,omitzero"` - // Required for create mode. 'single' creates a direct message chat; 'group' - // creates a group chat. - // - // Any of "single", "group". - Type ChatNewParamsType `json:"type,omitzero"` - // Required for mode='start'. Merged user-like contact payload used to resolve the - // best identifier. - User ChatNewParamsUser `json:"user,omitzero"` paramObj } @@ -302,17 +330,8 @@ func (r *ChatNewParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Operation mode. Use 'start' to resolve a user/contact and start a direct chat. -// Omit or set 'create' to create a chat directly. -type ChatNewParamsMode string - -const ( - ChatNewParamsModeStart ChatNewParamsMode = "start" - ChatNewParamsModeCreate ChatNewParamsMode = "create" -) - -// Required for create mode. 'single' creates a direct message chat; 'group' -// creates a group chat. +// 'single' requires exactly one participantID; 'group' supports multiple +// participants and optional title. type ChatNewParamsType string const ( @@ -320,30 +339,6 @@ const ( ChatNewParamsTypeGroup ChatNewParamsType = "group" ) -// Required for mode='start'. Merged user-like contact payload used to resolve the -// best identifier. -type ChatNewParamsUser struct { - // Known user ID when available. - ID param.Opt[string] `json:"id,omitzero"` - // Email candidate. - Email param.Opt[string] `json:"email,omitzero"` - // Display name hint used for ranking only. - FullName param.Opt[string] `json:"fullName,omitzero"` - // Phone number candidate (E.164 preferred). - PhoneNumber param.Opt[string] `json:"phoneNumber,omitzero"` - // Username/handle candidate. - Username param.Opt[string] `json:"username,omitzero"` - paramObj -} - -func (r ChatNewParamsUser) MarshalJSON() (data []byte, err error) { - type shadow ChatNewParamsUser - return param.MarshalObject(r, (*shadow)(&r)) -} -func (r *ChatNewParamsUser) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - type ChatGetParams struct { // Maximum number of participants to return. Use -1 for all; otherwise 0–500. // Defaults to all (-1). @@ -493,3 +488,46 @@ const ( ChatSearchParamsTypeGroup ChatSearchParamsType = "group" ChatSearchParamsTypeAny ChatSearchParamsType = "any" ) + +type ChatStartParams struct { + // Account to create or start the chat on. + AccountID string `json:"accountID" api:"required"` + // Merged user-like contact payload used to resolve the best identifier. + User ChatStartParamsUser `json:"user,omitzero" api:"required"` + // Whether invite-based DM creation is allowed when required by the platform. + AllowInvite param.Opt[bool] `json:"allowInvite,omitzero"` + // Optional first message content if the platform requires it to create the chat. + MessageText param.Opt[string] `json:"messageText,omitzero"` + paramObj +} + +func (r ChatStartParams) MarshalJSON() (data []byte, err error) { + type shadow ChatStartParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatStartParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Merged user-like contact payload used to resolve the best identifier. +type ChatStartParamsUser struct { + // Known user ID when available. + ID param.Opt[string] `json:"id,omitzero"` + // Email candidate. + Email param.Opt[string] `json:"email,omitzero"` + // Display name hint used for ranking only. + FullName param.Opt[string] `json:"fullName,omitzero"` + // Phone number candidate (E.164 preferred). + PhoneNumber param.Opt[string] `json:"phoneNumber,omitzero"` + // Username/handle candidate. + Username param.Opt[string] `json:"username,omitzero"` + paramObj +} + +func (r ChatStartParamsUser) MarshalJSON() (data []byte, err error) { + type shadow ChatStartParamsUser + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatStartParamsUser) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} diff --git a/chat_test.go b/chat_test.go index 6b771a9..4f509e3 100644 --- a/chat_test.go +++ b/chat_test.go @@ -28,19 +28,10 @@ func TestChatNewWithOptionalParams(t *testing.T) { ) _, err := client.Chats.New(context.TODO(), beeperdesktopapi.ChatNewParams{ AccountID: "accountID", - AllowInvite: beeperdesktopapi.Bool(true), - MessageText: beeperdesktopapi.String("messageText"), - Mode: beeperdesktopapi.ChatNewParamsModeStart, ParticipantIDs: []string{"string"}, - Title: beeperdesktopapi.String("title"), Type: beeperdesktopapi.ChatNewParamsTypeSingle, - User: beeperdesktopapi.ChatNewParamsUser{ - ID: beeperdesktopapi.String("id"), - Email: beeperdesktopapi.String("email"), - FullName: beeperdesktopapi.String("fullName"), - PhoneNumber: beeperdesktopapi.String("phoneNumber"), - Username: beeperdesktopapi.String("username"), - }, + MessageText: beeperdesktopapi.String("messageText"), + Title: beeperdesktopapi.String("title"), }) if err != nil { var apierr *beeperdesktopapi.Error @@ -167,3 +158,37 @@ func TestChatSearchWithOptionalParams(t *testing.T) { t.Fatalf("err should be nil: %s", err.Error()) } } + +func TestChatStartWithOptionalParams(t *testing.T) { + t.Skip("Stainless mock tests currently load the project-published OpenAPI spec URL, which may not include newly-added local-only endpoints during build checks.") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := beeperdesktopapi.NewClient( + option.WithBaseURL(baseURL), + option.WithAccessToken("My Access Token"), + ) + _, err := client.Chats.Start(context.TODO(), beeperdesktopapi.ChatStartParams{ + AccountID: "accountID", + User: beeperdesktopapi.ChatStartParamsUser{ + ID: beeperdesktopapi.String("id"), + Email: beeperdesktopapi.String("email"), + FullName: beeperdesktopapi.String("fullName"), + PhoneNumber: beeperdesktopapi.String("phoneNumber"), + Username: beeperdesktopapi.String("username"), + }, + AllowInvite: beeperdesktopapi.Bool(true), + MessageText: beeperdesktopapi.String("messageText"), + }) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/scripts/detect-breaking-changes b/scripts/detect-breaking-changes deleted file mode 100755 index a0655e0..0000000 --- a/scripts/detect-breaking-changes +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -echo "==> Detecting breaking changes" - -TEST_PATHS=( - client_test.go - beeperdesktopapi_test.go - account_test.go - accountcontact_test.go - chat_test.go - chatreminder_test.go - chatmessagereaction_test.go - message_test.go - asset_test.go - info_test.go - usage_test.go - paginationauto_test.go - paginationmanual_test.go -) - -for PATHSPEC in "${TEST_PATHS[@]}"; do - # Try to check out previous versions of the test files - # with the current SDK. - git checkout "$1" -- "${PATHSPEC}" 2>/dev/null || true -done - -# Instead of running the tests, use the linter to check if an -# older test is no longer compatible with the latest SDK. -./scripts/lint From 85ced8c13f1d373cd36f5fb2c74ae3aad2a99016 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 18:54:14 +0000 Subject: [PATCH 40/42] feat(api): api update --- .stats.yml | 4 +- README.md | 18 ----- asset.go | 38 ++++------ asset_test.go | 8 +-- chat.go | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 50 deletions(-) diff --git a/.stats.yml b/.stats.yml index 75ad795..8180b73 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 24 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-8b89ffbfeb39b4186328fefd81f7dab1d28c786012201feb8035c7f920c4fbae.yml -openapi_spec_hash: de40e013fcc83fa44d5c51ddecb543c0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-7a6e57b0b4f45713b9a6e87421b1e6d867e83151b2536cde07165c6b7e1ffc87.yml +openapi_spec_hash: 68a4fd30bd7eb32aea004dc2af15e9ac config_hash: 08b781db5f1857ed601d1b2f4b6b45d9 diff --git a/README.md b/README.md index 8459ce7..4b9371c 100644 --- a/README.md +++ b/README.md @@ -386,24 +386,6 @@ file returned by `os.Open` will be sent with the file name on disk. We also provide a helper `beeperdesktopapi.File(reader io.Reader, filename string, contentType string)` which can be used to wrap any `io.Reader` with the appropriate file name and content type. -```go -// A file from the file system -file, err := os.Open("/path/to/file") -beeperdesktopapi.AssetUploadParams{ - File: file, -} - -// A file from a string -beeperdesktopapi.AssetUploadParams{ - File: strings.NewReader("my file contents"), -} - -// With a custom filename and contentType -beeperdesktopapi.AssetUploadParams{ - File: beeperdesktopapi.File(strings.NewReader(`{"hello": "foo"}`), "file.go", "application/json"), -} -``` - ### Retries Certain errors will be automatically retried 2 times by default, with a short exponential backoff. diff --git a/asset.go b/asset.go index 0f2c5ba..c0e3ec3 100644 --- a/asset.go +++ b/asset.go @@ -3,15 +3,11 @@ package beeperdesktopapi import ( - "bytes" "context" - "io" - "mime/multipart" "net/http" "net/url" "slices" - "github.com/beeper/desktop-api-go/internal/apiform" "github.com/beeper/desktop-api-go/internal/apijson" "github.com/beeper/desktop-api-go/internal/apiquery" "github.com/beeper/desktop-api-go/internal/requestconfig" @@ -210,31 +206,21 @@ func (r AssetServeParams) URLQuery() (v url.Values, err error) { } type AssetUploadParams struct { - // The file to upload (max 500 MB). - File io.Reader `json:"file,omitzero" api:"required" format:"binary"` - // Original filename. Defaults to the uploaded file name if omitted - FileName param.Opt[string] `json:"fileName,omitzero"` - // MIME type. Auto-detected from magic bytes if omitted - MimeType param.Opt[string] `json:"mimeType,omitzero"` + // Base64-encoded file content (max ~500MB decoded) + Content string `json:"content" api:"required"` + // Original filename. Required for the JSON form of /v1/assets/upload. + FileName string `json:"fileName" api:"required"` + // MIME type. Required for the JSON form of /v1/assets/upload. + MimeType string `json:"mimeType" api:"required"` paramObj } -func (r AssetUploadParams) MarshalMultipart() (data []byte, contentType string, err error) { - buf := bytes.NewBuffer(nil) - writer := multipart.NewWriter(buf) - err = apiform.MarshalRoot(r, writer) - if err == nil { - err = apiform.WriteExtras(writer, r.ExtraFields()) - } - if err != nil { - writer.Close() - return nil, "", err - } - err = writer.Close() - if err != nil { - return nil, "", err - } - return buf.Bytes(), writer.FormDataContentType(), nil +func (r AssetUploadParams) MarshalJSON() (data []byte, err error) { + type shadow AssetUploadParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *AssetUploadParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) } type AssetUploadBase64Params struct { diff --git a/asset_test.go b/asset_test.go index 3b7face..4baddad 100644 --- a/asset_test.go +++ b/asset_test.go @@ -77,7 +77,7 @@ func TestAssetServe(t *testing.T) { } } -func TestAssetUploadWithOptionalParams(t *testing.T) { +func TestAssetUpload(t *testing.T) { baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -90,9 +90,9 @@ func TestAssetUploadWithOptionalParams(t *testing.T) { option.WithAccessToken("My Access Token"), ) _, err := client.Assets.Upload(context.TODO(), beeperdesktopapi.AssetUploadParams{ - File: io.Reader(bytes.NewBuffer([]byte("Example data"))), - FileName: beeperdesktopapi.String("fileName"), - MimeType: beeperdesktopapi.String("mimeType"), + Content: "x", + FileName: "x", + MimeType: "x", }) if err != nil { var apierr *beeperdesktopapi.Error diff --git a/chat.go b/chat.go index 1bf9591..2d779a1 100644 --- a/chat.go +++ b/chat.go @@ -4,6 +4,7 @@ package beeperdesktopapi import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -144,6 +145,8 @@ type Chat struct { ID string `json:"id" api:"required"` // Account ID this chat belongs to. AccountID string `json:"accountID" api:"required"` + // Display-only human-readable account/network name. + Network string `json:"network" api:"required"` // Chat participants information. Participants ChatParticipants `json:"participants" api:"required"` // Display title of the chat as computed by the client/server. @@ -154,8 +157,16 @@ type Chat struct { Type ChatType `json:"type" api:"required"` // Number of unread messages. UnreadCount int64 `json:"unreadCount" api:"required"` + // Group chat description/topic when available. + Description string `json:"description" api:"nullable"` + // Current draft object for this chat, or null when no draft is set. + Draft ChatDraft `json:"draft" api:"nullable"` + // Local filesystem path to the chat avatar image when available. + ImgURL string `json:"imgURL" api:"nullable"` // True if chat is archived. IsArchived bool `json:"isArchived"` + // True if chat is marked low priority. + IsLowPriority bool `json:"isLowPriority"` // True if chat notifications are muted. IsMuted bool `json:"isMuted"` // True if chat is pinned. @@ -166,20 +177,31 @@ type Chat struct { LastReadMessageSortKey string `json:"lastReadMessageSortKey"` // Local chat ID specific to this Beeper Desktop installation. LocalChatID string `json:"localChatID" api:"nullable"` + // Disappearing-message timer in seconds when available. + MessageExpirySeconds int64 `json:"messageExpirySeconds" api:"nullable"` + // Mute expiration timestamp, forever, or null when not muted. + MutedUntil ChatMutedUntilUnion `json:"mutedUntil" api:"nullable" format:"date-time"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field AccountID respjson.Field + Network respjson.Field Participants respjson.Field Title respjson.Field Type respjson.Field UnreadCount respjson.Field + Description respjson.Field + Draft respjson.Field + ImgURL respjson.Field IsArchived respjson.Field + IsLowPriority respjson.Field IsMuted respjson.Field IsPinned respjson.Field LastActivity respjson.Field LastReadMessageSortKey respjson.Field LocalChatID respjson.Field + MessageExpirySeconds respjson.Field + MutedUntil respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -223,6 +245,175 @@ const ( ChatTypeGroup ChatType = "group" ) +// Current draft object for this chat, or null when no draft is set. +type ChatDraft struct { + // Draft attachments keyed by attachment ID. + Attachments map[string]ChatDraftAttachment `json:"attachments"` + // Rich-text draft content as Tiptap JSON. + Json ChatDraftJson `json:"json" api:"nullable"` + // Plain-text draft projection. + Text string `json:"text" api:"nullable"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Attachments respjson.Field + Json respjson.Field + Text respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatDraft) RawJSON() string { return r.JSON.raw } +func (r *ChatDraft) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type ChatDraftAttachment struct { + // Draft attachment identifier. + ID string `json:"id" api:"required"` + // Audio duration in seconds if known. + AudioDurationSeconds float64 `json:"audioDurationSeconds"` + // Original filename if available. + FileName string `json:"fileName"` + // Local filesystem path for the draft attachment. + FilePath string `json:"filePath"` + // File size in bytes if known. + FileSize float64 `json:"fileSize"` + // True if the attachment is a GIF. + IsGif bool `json:"isGif"` + // True if the attachment is recorded audio. + IsRecordedAudio bool `json:"isRecordedAudio"` + // MIME type if known. + MimeType string `json:"mimeType"` + // Pixel dimensions of the attachment. + Size ChatDraftAttachmentSize `json:"size"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ID respjson.Field + AudioDurationSeconds respjson.Field + FileName respjson.Field + FilePath respjson.Field + FileSize respjson.Field + IsGif respjson.Field + IsRecordedAudio respjson.Field + MimeType respjson.Field + Size respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatDraftAttachment) RawJSON() string { return r.JSON.raw } +func (r *ChatDraftAttachment) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Pixel dimensions of the attachment. +type ChatDraftAttachmentSize struct { + Height float64 `json:"height"` + Width float64 `json:"width"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Height respjson.Field + Width respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatDraftAttachmentSize) RawJSON() string { return r.JSON.raw } +func (r *ChatDraftAttachmentSize) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Rich-text draft content as Tiptap JSON. +type ChatDraftJson struct { + Attrs map[string]any `json:"attrs"` + Content []map[string]any `json:"content"` + Marks []ChatDraftJsonMark `json:"marks"` + Text string `json:"text"` + Type string `json:"type"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Attrs respjson.Field + Content respjson.Field + Marks respjson.Field + Text respjson.Field + Type respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatDraftJson) RawJSON() string { return r.JSON.raw } +func (r *ChatDraftJson) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type ChatDraftJsonMark struct { + Type string `json:"type" api:"required"` + Attrs map[string]any `json:"attrs"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Type respjson.Field + Attrs respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatDraftJsonMark) RawJSON() string { return r.JSON.raw } +func (r *ChatDraftJsonMark) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// ChatMutedUntilUnion contains all possible properties and values from +// [time.Time], [string]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +// +// If the underlying value is not a json object, one of the following properties +// will be valid: OfTime OfChatMutedUntilString] +type ChatMutedUntilUnion struct { + // This field will be present if the value is a [time.Time] instead of an object. + OfTime time.Time `json:",inline"` + // This field will be present if the value is a [string] instead of an object. + OfChatMutedUntilString string `json:",inline"` + JSON struct { + OfTime respjson.Field + OfChatMutedUntilString respjson.Field + raw string + } `json:"-"` +} + +func (u ChatMutedUntilUnion) AsTime() (v time.Time) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u ChatMutedUntilUnion) AsChatMutedUntilString() (v string) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u ChatMutedUntilUnion) RawJSON() string { return u.JSON.raw } + +func (r *ChatMutedUntilUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type ChatMutedUntilString string + +const ( + ChatMutedUntilStringForever ChatMutedUntilString = "forever" +) + type ChatNewResponse struct { // Newly created chat ID. ChatID string `json:"chatID" api:"required"` From 4681bd6077032db3dfe8d8e6d4b4edaddcb7292c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 21:17:30 +0000 Subject: [PATCH 41/42] feat(api): api update --- .stats.yml | 8 +- README.md | 27 +- account.go | 22 +- aliases.go | 30 ++ api.md | 10 +- asset.go | 63 +-- asset_test.go | 8 +- beeperdesktopapi.go | 4 +- chat.go | 800 ++++++++++++++++++++++++++++++------ chat_test.go | 141 ++++++- chatmessagereaction.go | 55 ++- chatmessagereaction_test.go | 8 +- chatreminder.go | 7 +- chatreminder_test.go | 3 +- client.go | 5 +- info.go | 9 +- message.go | 96 ++++- message_test.go | 63 ++- paginationauto_test.go | 4 +- paginationmanual_test.go | 4 +- shared/shared.go | 272 ++++++++++-- usage_test.go | 1 + 22 files changed, 1385 insertions(+), 255 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8180b73..2dd3fee 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 24 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-7a6e57b0b4f45713b9a6e87421b1e6d867e83151b2536cde07165c6b7e1ffc87.yml -openapi_spec_hash: 68a4fd30bd7eb32aea004dc2af15e9ac -config_hash: 08b781db5f1857ed601d1b2f4b6b45d9 +configured_endpoints: 30 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/beeper/beeper-desktop-api-c08c14bb754b4cb0e02b21fabb680469368286be339dec0aaa8c69d04a1f021a.yml +openapi_spec_hash: a10246aaf7cdc33b682fc245bd5f893b +config_hash: 72f9d43b9b51a5da912e9f3730e53ae2 diff --git a/README.md b/README.md index 4b9371c..5f42211 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ func main() { option.WithAccessToken("My Access Token"), // defaults to os.LookupEnv("BEEPER_ACCESS_TOKEN") ) page, err := client.Chats.Search(context.TODO(), beeperdesktopapi.ChatSearchParams{ + AccountIDs: []string{"matrix", "discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, IncludeMuted: beeperdesktopapi.Bool(true), Limit: beeperdesktopapi.Int(3), Type: beeperdesktopapi.ChatSearchParamsTypeSingle, @@ -297,9 +298,9 @@ You can use `.ListAutoPaging()` methods to iterate through items across all page ```go iter := client.Messages.SearchAutoPaging(context.TODO(), beeperdesktopapi.MessageSearchParams{ - AccountIDs: []string{"local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"}, + AccountIDs: []string{"discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, Limit: beeperdesktopapi.Int(10), - Query: beeperdesktopapi.String("deployment"), + Query: beeperdesktopapi.String("oauth"), }) // Automatically fetches more pages as needed. for iter.Next() { @@ -316,9 +317,9 @@ with additional helper methods like `.GetNextPage()`, e.g.: ```go page, err := client.Messages.Search(context.TODO(), beeperdesktopapi.MessageSearchParams{ - AccountIDs: []string{"local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"}, + AccountIDs: []string{"discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, Limit: beeperdesktopapi.Int(10), - Query: beeperdesktopapi.String("deployment"), + Query: beeperdesktopapi.String("oauth"), }) for page != nil { for _, message := range page.Items { @@ -386,6 +387,24 @@ file returned by `os.Open` will be sent with the file name on disk. We also provide a helper `beeperdesktopapi.File(reader io.Reader, filename string, contentType string)` which can be used to wrap any `io.Reader` with the appropriate file name and content type. +```go +// A file from the file system +file, err := os.Open("/path/to/file") +beeperdesktopapi.AssetUploadParams{ + File: file, +} + +// A file from a string +beeperdesktopapi.AssetUploadParams{ + File: strings.NewReader("my file contents"), +} + +// With a custom filename and contentType +beeperdesktopapi.AssetUploadParams{ + File: beeperdesktopapi.File(strings.NewReader(`{"hello": "foo"}`), "file.go", "application/json"), +} +``` + ### Retries Certain errors will be automatically retried 2 times by default, with a short exponential backoff. diff --git a/account.go b/account.go index 80fdac1..5a0b582 100644 --- a/account.go +++ b/account.go @@ -38,8 +38,8 @@ func NewAccountService(opts ...option.RequestOption) (r AccountService) { return } -// Lists chat accounts across networks (WhatsApp, Telegram, Twitter/X, etc.) -// actively connected to this Beeper Desktop instance +// List Chat Accounts connected to this Beeper Desktop instance, including bridge +// metadata and network identity. func (r *AccountService) List(ctx context.Context, opts ...option.RequestOption) (res *[]Account, err error) { opts = slices.Concat(r.Options, opts) path := "v1/accounts" @@ -49,9 +49,12 @@ func (r *AccountService) List(ctx context.Context, opts ...option.RequestOption) // A chat account added to Beeper. type Account struct { - // Chat account added to Beeper. Use this to route account-scoped actions. + // Chat account added to Beeper. Use this to route account-scoped actions. Examples + // include matrix for Beeper/Matrix, discordgo for a cloud bridge, + // slackgo.TEAM-USER for workspace-scoped cloud bridges, and local-whatsapp*ba*... + // for local bridges. AccountID string `json:"accountID" api:"required"` - // Bridge metadata for the account. Available in Beeper Desktop v4.2.799+. + // Bridge metadata for the account. Available in Beeper Desktop v4.2.785+. Bridge AccountBridge `json:"bridge" api:"required"` // User the account belongs to. User shared.User `json:"user" api:"required"` @@ -75,15 +78,18 @@ func (r *Account) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Bridge metadata for the account. Available in Beeper Desktop v4.2.799+. +// Bridge metadata for the account. Available in Beeper Desktop v4.2.785+. type AccountBridge struct { - // Bridge instance identifier. Available in Beeper Desktop v4.2.799+. + // Bridge instance identifier. Matrix and cloud bridges often use the bridge type + // (for example matrix or discordgo); local bridges use a local bridge ID (for + // example local-whatsapp). Available in Beeper Desktop v4.2.785+. ID string `json:"id" api:"required"` - // Bridge provider for the account. Available in Beeper Desktop v4.2.799+. + // Bridge provider for the account. Available in Beeper Desktop v4.2.785+. // // Any of "cloud", "self-hosted", "local", "platform-sdk". Provider string `json:"provider" api:"required"` - // Bridge type. Available in Beeper Desktop v4.2.799+. + // Bridge type, such as matrix, discordgo, slackgo, whatsapp, telegram, or twitter. + // Available in Beeper Desktop v4.2.785+. Type string `json:"type" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { diff --git a/aliases.go b/aliases.go index 958c462..6860625 100644 --- a/aliases.go +++ b/aliases.go @@ -41,9 +41,39 @@ const AttachmentTypeAudio = shared.AttachmentTypeAudio // This is an alias to an internal type. type AttachmentSize = shared.AttachmentSize +// Attachment transcription if available. +// +// This is an alias to an internal type. +type AttachmentTranscription = shared.AttachmentTranscription + // This is an alias to an internal type. type Message = shared.Message +// Link preview included with a message. +// +// This is an alias to an internal type. +type MessageLink = shared.MessageLink + +// Preview image dimensions. +// +// This is an alias to an internal type. +type MessageLinkImgSize = shared.MessageLinkImgSize + +// Read receipt state for this message, when available. +// +// This is an alias to an internal type. +type MessageSeenUnion = shared.MessageSeenUnion + +// ISO 8601 timestamp. +// +// This is an alias to an internal type. +type MessageSeenByParticipantItemUnion = shared.MessageSeenByParticipantItemUnion + +// Message send status for this message, when reported by the bridge. +// +// This is an alias to an internal type. +type MessageSendStatus = shared.MessageSendStatus + // Message content type. Useful for distinguishing reactions, media messages, and // state events from regular text messages. // diff --git a/api.md b/api.md index 1c49a65..7f2a38f 100644 --- a/api.md +++ b/api.md @@ -51,10 +51,14 @@ Methods: - client.Chats.New(ctx context.Context, body beeperdesktopapi.ChatNewParams) (\*beeperdesktopapi.ChatNewResponse, error) - client.Chats.Get(ctx context.Context, chatID string, query beeperdesktopapi.ChatGetParams) (\*beeperdesktopapi.Chat, error) +- client.Chats.Update(ctx context.Context, chatID string, body beeperdesktopapi.ChatUpdateParams) (\*beeperdesktopapi.Chat, error) - client.Chats.List(ctx context.Context, query beeperdesktopapi.ChatListParams) (\*pagination.CursorNoLimit[beeperdesktopapi.ChatListResponse], error) - client.Chats.Archive(ctx context.Context, chatID string, body beeperdesktopapi.ChatArchiveParams) error +- client.Chats.MarkRead(ctx context.Context, chatID string, body beeperdesktopapi.ChatMarkReadParams) (\*beeperdesktopapi.Chat, error) +- client.Chats.MarkUnread(ctx context.Context, chatID string, body beeperdesktopapi.ChatMarkUnreadParams) (\*beeperdesktopapi.Chat, error) +- client.Chats.NotifyAnyway(ctx context.Context, chatID string, body beeperdesktopapi.ChatNotifyAnywayParams) (\*beeperdesktopapi.Chat, error) - client.Chats.Search(ctx context.Context, query beeperdesktopapi.ChatSearchParams) (\*pagination.CursorSearch[beeperdesktopapi.Chat], error) -- client.Chats.Start(ctx context.Context, body beeperdesktopapi.ChatStartParams) (\*beeperdesktopapi.ChatStartResponse, error) +- client.Chats.Start(ctx context.Context, body beeperdesktopapi.ChatStartParams) (\*beeperdesktopapi.ChatStartResponse, error) ## Reminders @@ -74,7 +78,7 @@ Response Types: Methods: -- client.Chats.Messages.Reactions.Delete(ctx context.Context, messageID string, params beeperdesktopapi.ChatMessageReactionDeleteParams) (\*beeperdesktopapi.ChatMessageReactionDeleteResponse, error) +- client.Chats.Messages.Reactions.Delete(ctx context.Context, reactionKey string, body beeperdesktopapi.ChatMessageReactionDeleteParams) (\*beeperdesktopapi.ChatMessageReactionDeleteResponse, error) - client.Chats.Messages.Reactions.Add(ctx context.Context, messageID string, params beeperdesktopapi.ChatMessageReactionAddParams) (\*beeperdesktopapi.ChatMessageReactionAddResponse, error) # Messages @@ -86,8 +90,10 @@ Response Types: Methods: +- client.Messages.Get(ctx context.Context, messageID string, query beeperdesktopapi.MessageGetParams) (\*shared.Message, error) - client.Messages.Update(ctx context.Context, messageID string, params beeperdesktopapi.MessageUpdateParams) (\*beeperdesktopapi.MessageUpdateResponse, error) - client.Messages.List(ctx context.Context, chatID string, query beeperdesktopapi.MessageListParams) (\*pagination.CursorNoLimit[shared.Message], error) +- client.Messages.Delete(ctx context.Context, messageID string, params beeperdesktopapi.MessageDeleteParams) error - client.Messages.Search(ctx context.Context, query beeperdesktopapi.MessageSearchParams) (\*pagination.CursorSearch[shared.Message], error) - client.Messages.Send(ctx context.Context, chatID string, body beeperdesktopapi.MessageSendParams) (\*beeperdesktopapi.MessageSendResponse, error) diff --git a/asset.go b/asset.go index c0e3ec3..0b98119 100644 --- a/asset.go +++ b/asset.go @@ -3,11 +3,15 @@ package beeperdesktopapi import ( + "bytes" "context" + "io" + "mime/multipart" "net/http" "net/url" "slices" + "github.com/beeper/desktop-api-go/internal/apiform" "github.com/beeper/desktop-api-go/internal/apijson" "github.com/beeper/desktop-api-go/internal/apiquery" "github.com/beeper/desktop-api-go/internal/requestconfig" @@ -37,8 +41,8 @@ func NewAssetService(opts ...option.RequestOption) (r AssetService) { return } -// Download a Matrix asset using its mxc:// or localmxc:// URL to the device -// running Beeper Desktop and return the local file URL. +// Download a Matrix file using its mxc:// or localmxc:// URL to the device running +// Beeper Desktop and return the local file URL. func (r *AssetService) Download(ctx context.Context, body AssetDownloadParams, opts ...option.RequestOption) (res *AssetDownloadResponse, err error) { opts = slices.Concat(r.Options, opts) path := "v1/assets/download" @@ -57,7 +61,8 @@ func (r *AssetService) Serve(ctx context.Context, query AssetServeParams, opts . } // Upload a file to a temporary location using multipart/form-data. Returns an -// uploadID that can be referenced when sending messages with attachments. +// uploadID that can be referenced when sending a message or materializing a draft +// attachment. func (r *AssetService) Upload(ctx context.Context, body AssetUploadParams, opts ...option.RequestOption) (res *AssetUploadResponse, err error) { opts = slices.Concat(r.Options, opts) path := "v1/assets/upload" @@ -66,8 +71,8 @@ func (r *AssetService) Upload(ctx context.Context, body AssetUploadParams, opts } // Upload a file using a JSON body with base64-encoded content. Returns an uploadID -// that can be referenced when sending messages with attachments. Alternative to -// the multipart upload endpoint. +// that can be referenced when sending a message or materializing a draft +// attachment. Alternative to the multipart upload endpoint. func (r *AssetService) UploadBase64(ctx context.Context, body AssetUploadBase64Params, opts ...option.RequestOption) (res *AssetUploadBase64Response, err error) { opts = slices.Concat(r.Options, opts) path := "v1/assets/upload/base64" @@ -78,7 +83,7 @@ func (r *AssetService) UploadBase64(ctx context.Context, body AssetUploadBase64P type AssetDownloadResponse struct { // Error message if the download failed. Error string `json:"error"` - // Local file URL to the downloaded asset. + // Local file URL to the downloaded file. SrcURL string `json:"srcURL"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -108,9 +113,9 @@ type AssetUploadResponse struct { Height float64 `json:"height"` // Detected or provided MIME type MimeType string `json:"mimeType"` - // Local file URL (file://) for the uploaded asset + // Local file URL (file://) for the uploaded file SrcURL string `json:"srcURL"` - // Unique upload ID for this asset + // Unique upload ID for this temporary file UploadID string `json:"uploadID"` // Width in pixels (images/videos) Width float64 `json:"width"` @@ -149,9 +154,9 @@ type AssetUploadBase64Response struct { Height float64 `json:"height"` // Detected or provided MIME type MimeType string `json:"mimeType"` - // Local file URL (file://) for the uploaded asset + // Local file URL (file://) for the uploaded file SrcURL string `json:"srcURL"` - // Unique upload ID for this asset + // Unique upload ID for this temporary file UploadID string `json:"uploadID"` // Width in pixels (images/videos) Width float64 `json:"width"` @@ -178,7 +183,7 @@ func (r *AssetUploadBase64Response) UnmarshalJSON(data []byte) error { } type AssetDownloadParams struct { - // Matrix content URL (mxc:// or localmxc://) for the asset to download. + // Matrix content URL (mxc:// or localmxc://) for the file to download. URL string `json:"url" api:"required"` paramObj } @@ -192,7 +197,7 @@ func (r *AssetDownloadParams) UnmarshalJSON(data []byte) error { } type AssetServeParams struct { - // Asset URL to serve. Accepts mxc://, localmxc://, or file:// URLs. + // File URL to serve. Accepts mxc://, localmxc://, or file:// URLs. URL string `query:"url" api:"required" json:"-"` paramObj } @@ -206,21 +211,31 @@ func (r AssetServeParams) URLQuery() (v url.Values, err error) { } type AssetUploadParams struct { - // Base64-encoded file content (max ~500MB decoded) - Content string `json:"content" api:"required"` - // Original filename. Required for the JSON form of /v1/assets/upload. - FileName string `json:"fileName" api:"required"` - // MIME type. Required for the JSON form of /v1/assets/upload. - MimeType string `json:"mimeType" api:"required"` + // The file to upload (max 500 MB). + File io.Reader `json:"file,omitzero" api:"required" format:"binary"` + // Original filename. Defaults to the uploaded file name if omitted + FileName param.Opt[string] `json:"fileName,omitzero"` + // MIME type. Auto-detected from magic bytes if omitted + MimeType param.Opt[string] `json:"mimeType,omitzero"` paramObj } -func (r AssetUploadParams) MarshalJSON() (data []byte, err error) { - type shadow AssetUploadParams - return param.MarshalObject(r, (*shadow)(&r)) -} -func (r *AssetUploadParams) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) +func (r AssetUploadParams) MarshalMultipart() (data []byte, contentType string, err error) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + err = apiform.MarshalRoot(r, writer) + if err == nil { + err = apiform.WriteExtras(writer, r.ExtraFields()) + } + if err != nil { + writer.Close() + return nil, "", err + } + err = writer.Close() + if err != nil { + return nil, "", err + } + return buf.Bytes(), writer.FormDataContentType(), nil } type AssetUploadBase64Params struct { diff --git a/asset_test.go b/asset_test.go index 4baddad..3b7face 100644 --- a/asset_test.go +++ b/asset_test.go @@ -77,7 +77,7 @@ func TestAssetServe(t *testing.T) { } } -func TestAssetUpload(t *testing.T) { +func TestAssetUploadWithOptionalParams(t *testing.T) { baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL @@ -90,9 +90,9 @@ func TestAssetUpload(t *testing.T) { option.WithAccessToken("My Access Token"), ) _, err := client.Assets.Upload(context.TODO(), beeperdesktopapi.AssetUploadParams{ - Content: "x", - FileName: "x", - MimeType: "x", + File: io.Reader(bytes.NewBuffer([]byte("Example data"))), + FileName: beeperdesktopapi.String("fileName"), + MimeType: beeperdesktopapi.String("mimeType"), }) if err != nil { var apierr *beeperdesktopapi.Error diff --git a/beeperdesktopapi.go b/beeperdesktopapi.go index 5a97814..482dab9 100644 --- a/beeperdesktopapi.go +++ b/beeperdesktopapi.go @@ -103,9 +103,9 @@ type FocusParams struct { // Optional Beeper chat ID (or local chat ID) to focus after opening the app. If // omitted, only opens/focuses the app. ChatID param.Opt[string] `json:"chatID,omitzero"` - // Optional draft attachment path to populate in the message input field. + // Optional image path to populate in the message input field. DraftAttachmentPath param.Opt[string] `json:"draftAttachmentPath,omitzero"` - // Optional draft text to populate in the message input field. + // Optional plain text to populate in the message input field. DraftText param.Opt[string] `json:"draftText,omitzero"` // Optional message ID. Jumps to that message in the chat when opening. MessageID param.Opt[string] `json:"messageID,omitzero"` diff --git a/chat.go b/chat.go index 2d779a1..ab1b4f1 100644 --- a/chat.go +++ b/chat.go @@ -4,7 +4,6 @@ package beeperdesktopapi import ( "context" - "encoding/json" "errors" "fmt" "net/http" @@ -49,7 +48,7 @@ func NewChatService(opts ...option.RequestOption) (r ChatService) { return } -// Create a direct or group chat from participant IDs. +// Create a direct or group chat from participant IDs. Returns the created chat. func (r *ChatService) New(ctx context.Context, body ChatNewParams, opts ...option.RequestOption) (res *ChatNewResponse, err error) { opts = slices.Concat(r.Options, opts) path := "v1/chats" @@ -69,6 +68,20 @@ func (r *ChatService) Get(ctx context.Context, chatID string, query ChatGetParam return res, err } +// Update supported chat fields. Non-empty draft objects are accepted only when the +// current draft is empty. Send draft=null to clear the draft before setting new +// draft text or attachments. +func (r *ChatService) Update(ctx context.Context, chatID string, body ChatUpdateParams, opts ...option.RequestOption) (res *Chat, err error) { + opts = slices.Concat(r.Options, opts) + if chatID == "" { + err = errors.New("missing required chatID parameter") + return nil, err + } + path := fmt.Sprintf("v1/chats/%s", chatID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...) + return res, err +} + // List all chats sorted by last activity (most recent first). Combines all // accounts into a single paginated list. func (r *ChatService) List(ctx context.Context, query ChatListParams, opts ...option.RequestOption) (res *pagination.CursorNoLimit[ChatListResponse], err error) { @@ -108,6 +121,43 @@ func (r *ChatService) Archive(ctx context.Context, chatID string, body ChatArchi return err } +// Mark a chat as read, optionally through a specific message ID. +func (r *ChatService) MarkRead(ctx context.Context, chatID string, body ChatMarkReadParams, opts ...option.RequestOption) (res *Chat, err error) { + opts = slices.Concat(r.Options, opts) + if chatID == "" { + err = errors.New("missing required chatID parameter") + return nil, err + } + path := fmt.Sprintf("v1/chats/%s/read", chatID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return res, err +} + +// Mark a chat as unread, optionally from a specific message ID. +func (r *ChatService) MarkUnread(ctx context.Context, chatID string, body ChatMarkUnreadParams, opts ...option.RequestOption) (res *Chat, err error) { + opts = slices.Concat(r.Options, opts) + if chatID == "" { + err = errors.New("missing required chatID parameter") + return nil, err + } + path := fmt.Sprintf("v1/chats/%s/unread", chatID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return res, err +} + +// Force a delivery notification when supported by the underlying network. +// Currently intended for iMessage on macOS; unsupported networks return an error. +func (r *ChatService) NotifyAnyway(ctx context.Context, chatID string, body ChatNotifyAnywayParams, opts ...option.RequestOption) (res *Chat, err error) { + opts = slices.Concat(r.Options, opts) + if chatID == "" { + err = errors.New("missing required chatID parameter") + return nil, err + } + path := fmt.Sprintf("v1/chats/%s/notify-anyway", chatID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return res, err +} + // Search chats by title, network, or participant names. func (r *ChatService) Search(ctx context.Context, query ChatSearchParams, opts ...option.RequestOption) (res *pagination.CursorSearch[Chat], err error) { var raw *http.Response @@ -131,11 +181,11 @@ func (r *ChatService) SearchAutoPaging(ctx context.Context, query ChatSearchPara return pagination.NewCursorSearchAutoPager(r.Search(ctx, query, opts...)) } -// Resolve a user/contact and open a direct chat. Reuses an existing direct chat -// when one is found. Available in Beeper Desktop v4.2.799+. +// Resolve a user/contact and open a direct chat. Reuses and returns an existing +// direct chat when one is found. Available in Beeper Desktop v4.2.808+. func (r *ChatService) Start(ctx context.Context, body ChatStartParams, opts ...option.RequestOption) (res *ChatStartResponse, err error) { opts = slices.Concat(r.Options, opts) - path := "v1/chats.start" + path := "v1/chats/start" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return res, err } @@ -157,6 +207,8 @@ type Chat struct { Type ChatType `json:"type" api:"required"` // Number of unread messages. UnreadCount int64 `json:"unreadCount" api:"required"` + // Chat capabilities reported by the platform. + Capabilities ChatCapabilities `json:"capabilities"` // Group chat description/topic when available. Description string `json:"description" api:"nullable"` // Current draft object for this chat, or null when no draft is set. @@ -167,10 +219,14 @@ type Chat struct { IsArchived bool `json:"isArchived"` // True if chat is marked low priority. IsLowPriority bool `json:"isLowPriority"` + // True if the chat was explicitly marked unread by the authenticated user. + IsMarkedUnread bool `json:"isMarkedUnread"` // True if chat notifications are muted. IsMuted bool `json:"isMuted"` // True if chat is pinned. IsPinned bool `json:"isPinned"` + // True if messages cannot be sent in this chat. + IsReadOnly bool `json:"isReadOnly"` // Timestamp of last activity. LastActivity time.Time `json:"lastActivity" format:"date-time"` // Last read message sortKey. @@ -179,8 +235,12 @@ type Chat struct { LocalChatID string `json:"localChatID" api:"nullable"` // Disappearing-message timer in seconds when available. MessageExpirySeconds int64 `json:"messageExpirySeconds" api:"nullable"` - // Mute expiration timestamp, forever, or null when not muted. - MutedUntil ChatMutedUntilUnion `json:"mutedUntil" api:"nullable" format:"date-time"` + // Current reminder for this chat, or null when no reminder is set. + Reminder ChatReminder `json:"reminder" api:"nullable"` + // Current snooze state for this chat, or null when no snooze is set. + Snooze ChatSnooze `json:"snooze" api:"nullable"` + // Number of unread messages that mention the authenticated user or @room. + UnreadMentionsCount int64 `json:"unreadMentionsCount"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field @@ -190,18 +250,23 @@ type Chat struct { Title respjson.Field Type respjson.Field UnreadCount respjson.Field + Capabilities respjson.Field Description respjson.Field Draft respjson.Field ImgURL respjson.Field IsArchived respjson.Field IsLowPriority respjson.Field + IsMarkedUnread respjson.Field IsMuted respjson.Field IsPinned respjson.Field + IsReadOnly respjson.Field LastActivity respjson.Field LastReadMessageSortKey respjson.Field LocalChatID respjson.Field MessageExpirySeconds respjson.Field - MutedUntil respjson.Field + Reminder respjson.Field + Snooze respjson.Field + UnreadMentionsCount respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -218,7 +283,7 @@ type ChatParticipants struct { // True if there are more participants than included in items. HasMore bool `json:"hasMore" api:"required"` // Participants returned for this chat (limited by the request; may be a subset). - Items []shared.User `json:"items" api:"required"` + Items []ChatParticipantsItem `json:"items" api:"required"` // Total number of participants in the chat. Total int64 `json:"total" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. @@ -237,6 +302,31 @@ func (r *ChatParticipants) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// A chat participant. Extends User with chat membership metadata. +type ChatParticipantsItem struct { + // True if this participant has admin privileges in the chat. + IsAdmin bool `json:"isAdmin"` + // True if this participant represents a network or bridge bot. + IsNetworkBot bool `json:"isNetworkBot"` + // True if this participant has been invited but has not joined yet. + IsPending bool `json:"isPending"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + IsAdmin respjson.Field + IsNetworkBot respjson.Field + IsPending respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` + shared.User +} + +// Returns the unmodified JSON received from the API +func (r ChatParticipantsItem) RawJSON() string { return r.JSON.raw } +func (r *ChatParticipantsItem) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Chat type: 'single' for direct messages, 'group' for group chats. type ChatType string @@ -245,19 +335,394 @@ const ( ChatTypeGroup ChatType = "group" ) +// Chat capabilities reported by the platform. +type ChatCapabilities struct { + // Allowed Unicode reactions. Omitted means all emoji reactions are allowed. + AllowedReactions []string `json:"allowedReactions"` + // True if archive/unarchive is supported. + Archive bool `json:"archive"` + // Supported attachment message types and their per-type constraints, keyed by + // Matrix msgtype or pseudo-msgtype (for example m.image, m.video, + // org.matrix.msc3245.voice). Missing message types should be treated as rejected. + Attachments map[string]ChatCapabilitiesAttachment `json:"attachments"` + // True if custom emoji reactions are supported. + CustomEmojiReactions bool `json:"customEmojiReactions"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Delete int64 `json:"delete"` + // True if deleting chats for the authenticated user is supported. + DeleteChat bool `json:"deleteChat"` + // True if deleting chats for everyone is supported. + DeleteChatForEveryone bool `json:"deleteChatForEveryone"` + // True if deleting messages only for the authenticated user is supported. + DeleteForMe bool `json:"deleteForMe"` + // Maximum message age for delete-for-everyone, in seconds. + DeleteMaxAge int64 `json:"deleteMaxAge"` + // Disappearing-message timer capabilities. + DisappearingTimer ChatCapabilitiesDisappearingTimer `json:"disappearingTimer"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Edit int64 `json:"edit"` + // Maximum message age for edits, in seconds. + EditMaxAge int64 `json:"editMaxAge"` + // Maximum number of edits allowed for one message. + EditMaxCount int64 `json:"editMaxCount"` + // Supported rich-text formatting features keyed by feature name (for example bold, + // inline_code, code_block.syntax_highlighting). Omitted means no formatting + // support is advertised. + // + // Any of -2, -1, 0, 1, 2. + Formatting map[string]int64 `json:"formatting"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + LocationMessage int64 `json:"locationMessage"` + // True if marking chats unread is supported. + MarkAsUnread bool `json:"markAsUnread"` + // Maximum length of normal text messages. + MaxTextLength int64 `json:"maxTextLength"` + // Message request capabilities. + MessageRequest ChatCapabilitiesMessageRequest `json:"messageRequest"` + // Participant management capabilities. + ParticipantActions ChatCapabilitiesParticipantActions `json:"participantActions"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Poll int64 `json:"poll"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Reaction int64 `json:"reaction"` + // Maximum number of reactions allowed on a single message. + ReactionCount int64 `json:"reactionCount"` + // True if read receipts are supported. + ReadReceipts bool `json:"readReceipts"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Reply int64 `json:"reply"` + // Chat state update capabilities. + State ChatCapabilitiesState `json:"state"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Thread int64 `json:"thread"` + // True if typing notifications are supported. + TypingNotifications bool `json:"typingNotifications"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AllowedReactions respjson.Field + Archive respjson.Field + Attachments respjson.Field + CustomEmojiReactions respjson.Field + Delete respjson.Field + DeleteChat respjson.Field + DeleteChatForEveryone respjson.Field + DeleteForMe respjson.Field + DeleteMaxAge respjson.Field + DisappearingTimer respjson.Field + Edit respjson.Field + EditMaxAge respjson.Field + EditMaxCount respjson.Field + Formatting respjson.Field + LocationMessage respjson.Field + MarkAsUnread respjson.Field + MaxTextLength respjson.Field + MessageRequest respjson.Field + ParticipantActions respjson.Field + Poll respjson.Field + Reaction respjson.Field + ReactionCount respjson.Field + ReadReceipts respjson.Field + Reply respjson.Field + State respjson.Field + Thread respjson.Field + TypingNotifications respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilities) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilities) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Capabilities for one attachment message type. +type ChatCapabilitiesAttachment struct { + // Supported MIME types or MIME patterns for this file message type. Missing MIME + // types should be treated as rejected. + // + // Any of -2, -1, 0, 1, 2. + MimeTypes map[string]int64 `json:"mimeTypes" api:"required"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Caption int64 `json:"caption"` + // Maximum caption length when captions are supported. + MaxCaptionLength int64 `json:"maxCaptionLength"` + // Maximum audio or video duration in seconds. + MaxDuration int64 `json:"maxDuration"` + // Maximum image or video height in pixels. + MaxHeight int64 `json:"maxHeight"` + // Maximum file size in bytes. + MaxSize int64 `json:"maxSize"` + // Maximum image or video width in pixels. + MaxWidth int64 `json:"maxWidth"` + // True if this file type can be sent as view-once media. + ViewOnce bool `json:"viewOnce"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + MimeTypes respjson.Field + Caption respjson.Field + MaxCaptionLength respjson.Field + MaxDuration respjson.Field + MaxHeight respjson.Field + MaxSize respjson.Field + MaxWidth respjson.Field + ViewOnce respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesAttachment) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesAttachment) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Disappearing-message timer capabilities. +type ChatCapabilitiesDisappearingTimer struct { + // True if empty timer objects should be omitted from message content. + OmitEmptyTimer bool `json:"omitEmptyTimer"` + // Allowed disappearing timer values in milliseconds. Omitted means any timer is + // allowed. + Timers []int64 `json:"timers"` + // Supported disappearing timer types. + // + // Any of "afterRead", "afterSend". + Types []string `json:"types"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + OmitEmptyTimer respjson.Field + Timers respjson.Field + Types respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesDisappearingTimer) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesDisappearingTimer) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Message request capabilities. +type ChatCapabilitiesMessageRequest struct { + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + AcceptWithButton int64 `json:"acceptWithButton"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + AcceptWithMessage int64 `json:"acceptWithMessage"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + AcceptWithButton respjson.Field + AcceptWithMessage respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesMessageRequest) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesMessageRequest) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Participant management capabilities. +type ChatCapabilitiesParticipantActions struct { + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Ban int64 `json:"ban"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Invite int64 `json:"invite"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Kick int64 `json:"kick"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Leave int64 `json:"leave"` + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + RevokeInvite int64 `json:"revokeInvite"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Ban respjson.Field + Invite respjson.Field + Kick respjson.Field + Leave respjson.Field + RevokeInvite respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesParticipantActions) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesParticipantActions) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Chat state update capabilities. +type ChatCapabilitiesState struct { + // Chat avatar state capability. + Avatar ChatCapabilitiesStateAvatar `json:"avatar"` + // Chat description/topic state capability. + Description ChatCapabilitiesStateDescription `json:"description"` + // Disappearing-message timer state capability. + DisappearingTimer ChatCapabilitiesStateDisappearingTimer `json:"disappearingTimer"` + // Chat title state capability. + Title ChatCapabilitiesStateTitle `json:"title"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Avatar respjson.Field + Description respjson.Field + DisappearingTimer respjson.Field + Title respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesState) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesState) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Chat avatar state capability. +type ChatCapabilitiesStateAvatar struct { + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Level int64 `json:"level" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Level respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesStateAvatar) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesStateAvatar) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Chat description/topic state capability. +type ChatCapabilitiesStateDescription struct { + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Level int64 `json:"level" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Level respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesStateDescription) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesStateDescription) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Disappearing-message timer state capability. +type ChatCapabilitiesStateDisappearingTimer struct { + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Level int64 `json:"level" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Level respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesStateDisappearingTimer) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesStateDisappearingTimer) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Chat title state capability. +type ChatCapabilitiesStateTitle struct { + // -2: rejected, -1: dropped, 0: unsupported, 1: partially supported, 2: fully + // supported. + // + // Any of -2, -1, 0, 1, 2. + Level int64 `json:"level" api:"required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Level respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ChatCapabilitiesStateTitle) RawJSON() string { return r.JSON.raw } +func (r *ChatCapabilitiesStateTitle) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Current draft object for this chat, or null when no draft is set. type ChatDraft struct { + // Matrix HTML draft body. + Text string `json:"text" api:"required"` // Draft attachments keyed by attachment ID. Attachments map[string]ChatDraftAttachment `json:"attachments"` - // Rich-text draft content as Tiptap JSON. - Json ChatDraftJson `json:"json" api:"nullable"` - // Plain-text draft projection. - Text string `json:"text" api:"nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - Attachments respjson.Field - Json respjson.Field Text respjson.Field + Attachments respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -272,6 +737,10 @@ func (r *ChatDraft) UnmarshalJSON(data []byte) error { type ChatDraftAttachment struct { // Draft attachment identifier. ID string `json:"id" api:"required"` + // Draft attachment type. GIF and recorded audio are mutually exclusive types. + // + // Any of "file", "gif", "recorded_audio". + Type string `json:"type" api:"required"` // Audio duration in seconds if known. AudioDurationSeconds float64 `json:"audioDurationSeconds"` // Original filename if available. @@ -280,25 +749,23 @@ type ChatDraftAttachment struct { FilePath string `json:"filePath"` // File size in bytes if known. FileSize float64 `json:"fileSize"` - // True if the attachment is a GIF. - IsGif bool `json:"isGif"` - // True if the attachment is recorded audio. - IsRecordedAudio bool `json:"isRecordedAudio"` // MIME type if known. MimeType string `json:"mimeType"` // Pixel dimensions of the attachment. Size ChatDraftAttachmentSize `json:"size"` + // Sticker identifier if the draft attachment is a sticker. + StickerID string `json:"stickerID"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ID respjson.Field + Type respjson.Field AudioDurationSeconds respjson.Field FileName respjson.Field FilePath respjson.Field FileSize respjson.Field - IsGif respjson.Field - IsRecordedAudio respjson.Field MimeType respjson.Field Size respjson.Field + StickerID respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` @@ -329,99 +796,60 @@ func (r *ChatDraftAttachmentSize) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Rich-text draft content as Tiptap JSON. -type ChatDraftJson struct { - Attrs map[string]any `json:"attrs"` - Content []map[string]any `json:"content"` - Marks []ChatDraftJsonMark `json:"marks"` - Text string `json:"text"` - Type string `json:"type"` +// Current reminder for this chat, or null when no reminder is set. +type ChatReminder struct { + // Cancel reminder if someone messages in the chat. + DismissOnIncomingMessage bool `json:"dismissOnIncomingMessage"` + // Timestamp when the reminder should trigger. + RemindAt time.Time `json:"remindAt" format:"date-time"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - Attrs respjson.Field - Content respjson.Field - Marks respjson.Field - Text respjson.Field - Type respjson.Field - ExtraFields map[string]respjson.Field - raw string + DismissOnIncomingMessage respjson.Field + RemindAt respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } // Returns the unmodified JSON received from the API -func (r ChatDraftJson) RawJSON() string { return r.JSON.raw } -func (r *ChatDraftJson) UnmarshalJSON(data []byte) error { +func (r ChatReminder) RawJSON() string { return r.JSON.raw } +func (r *ChatReminder) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -type ChatDraftJsonMark struct { - Type string `json:"type" api:"required"` - Attrs map[string]any `json:"attrs"` +// Current snooze state for this chat, or null when no snooze is set. +type ChatSnooze struct { + // Timestamp when the snooze expires. + SnoozeUntil time.Time `json:"snoozeUntil" format:"date-time"` + // Timestamp when the user set the snooze. + UserSnoozedAt time.Time `json:"userSnoozedAt" format:"date-time"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - Type respjson.Field - Attrs respjson.Field - ExtraFields map[string]respjson.Field - raw string + SnoozeUntil respjson.Field + UserSnoozedAt respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } // Returns the unmodified JSON received from the API -func (r ChatDraftJsonMark) RawJSON() string { return r.JSON.raw } -func (r *ChatDraftJsonMark) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -// ChatMutedUntilUnion contains all possible properties and values from -// [time.Time], [string]. -// -// Use the methods beginning with 'As' to cast the union to one of its variants. -// -// If the underlying value is not a json object, one of the following properties -// will be valid: OfTime OfChatMutedUntilString] -type ChatMutedUntilUnion struct { - // This field will be present if the value is a [time.Time] instead of an object. - OfTime time.Time `json:",inline"` - // This field will be present if the value is a [string] instead of an object. - OfChatMutedUntilString string `json:",inline"` - JSON struct { - OfTime respjson.Field - OfChatMutedUntilString respjson.Field - raw string - } `json:"-"` -} - -func (u ChatMutedUntilUnion) AsTime() (v time.Time) { - apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) - return -} - -func (u ChatMutedUntilUnion) AsChatMutedUntilString() (v string) { - apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) - return -} - -// Returns the unmodified JSON received from the API -func (u ChatMutedUntilUnion) RawJSON() string { return u.JSON.raw } - -func (r *ChatMutedUntilUnion) UnmarshalJSON(data []byte) error { +func (r ChatSnooze) RawJSON() string { return r.JSON.raw } +func (r *ChatSnooze) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -type ChatMutedUntilString string - -const ( - ChatMutedUntilStringForever ChatMutedUntilString = "forever" -) - type ChatNewResponse struct { - // Newly created chat ID. + // DEPRECATED - use id instead. Compatibility alias for older clients. + // + // Deprecated: deprecated ChatID string `json:"chatID" api:"required"` - // Only returned in start mode. 'existing' means an existing chat was reused; - // 'created' means a new chat was created. + // DEPRECATED - legacy start-chat status for older clients. New clients should + // inspect the returned Chat instead. // // Any of "existing", "created". - Status ChatNewResponseStatus `json:"status"` + // + // Deprecated: deprecated + Status string `json:"status"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ChatID respjson.Field @@ -429,6 +857,7 @@ type ChatNewResponse struct { ExtraFields map[string]respjson.Field raw string } `json:"-"` + Chat } // Returns the unmodified JSON received from the API @@ -437,15 +866,7 @@ func (r *ChatNewResponse) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Only returned in start mode. 'existing' means an existing chat was reused; -// 'created' means a new chat was created. -type ChatNewResponseStatus string - -const ( - ChatNewResponseStatusExisting ChatNewResponseStatus = "existing" - ChatNewResponseStatusCreated ChatNewResponseStatus = "created" -) - +// Chat with optional last message preview. type ChatListResponse struct { // Last message preview for this chat, if available. Preview shared.Message `json:"preview"` @@ -465,13 +886,17 @@ func (r *ChatListResponse) UnmarshalJSON(data []byte) error { } type ChatStartResponse struct { - // Newly created chat ID. + // DEPRECATED - use id instead. Compatibility alias for older clients. + // + // Deprecated: deprecated ChatID string `json:"chatID" api:"required"` - // Only returned in start mode. 'existing' means an existing chat was reused; - // 'created' means a new chat was created. + // DEPRECATED - legacy start-chat status for older clients. New clients should + // inspect the returned Chat instead. // // Any of "existing", "created". - Status ChatStartResponseStatus `json:"status"` + // + // Deprecated: deprecated + Status string `json:"status"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ChatID respjson.Field @@ -479,6 +904,7 @@ type ChatStartResponse struct { ExtraFields map[string]respjson.Field raw string } `json:"-"` + Chat } // Returns the unmodified JSON received from the API @@ -487,15 +913,6 @@ func (r *ChatStartResponse) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } -// Only returned in start mode. 'existing' means an existing chat was reused; -// 'created' means a new chat was created. -type ChatStartResponseStatus string - -const ( - ChatStartResponseStatusExisting ChatStartResponseStatus = "existing" - ChatStartResponseStatusCreated ChatStartResponseStatus = "created" -) - type ChatNewParams struct { // Account to create or start the chat on. AccountID string `json:"accountID" api:"required"` @@ -531,8 +948,9 @@ const ( ) type ChatGetParams struct { - // Maximum number of participants to return. Use -1 for all; otherwise 0–500. - // Defaults to all (-1). + // Maximum number of participants to return. Use -1 for all; otherwise 0-500. + // Defaults to 100. List and search endpoints return up to 20 participants per + // chat. MaxParticipantCount param.Opt[int64] `query:"maxParticipantCount,omitzero" json:"-"` paramObj } @@ -545,6 +963,116 @@ func (r ChatGetParams) URLQuery() (v url.Values, err error) { }) } +type ChatUpdateParams struct { + // Group chat description/topic. Support depends on the chat account and chat + // permissions. + Description param.Opt[string] `json:"description,omitzero"` + // Local filesystem path to a group chat avatar image. Support depends on the chat + // account and chat permissions. + ImgURL param.Opt[string] `json:"imgURL,omitzero"` + // Disappearing-message timer in seconds, or null to clear when supported. + MessageExpirySeconds param.Opt[int64] `json:"messageExpirySeconds,omitzero"` + // Custom chat title. Support depends on the chat account and chat permissions. + Title param.Opt[string] `json:"title,omitzero"` + // Archive or unarchive the chat. + IsArchived param.Opt[bool] `json:"isArchived,omitzero"` + // Mark or unmark the chat as low priority when supported by the account. + IsLowPriority param.Opt[bool] `json:"isLowPriority,omitzero"` + // Mute or unmute the chat. + IsMuted param.Opt[bool] `json:"isMuted,omitzero"` + // Pin or unpin the chat when supported by the account. + IsPinned param.Opt[bool] `json:"isPinned,omitzero"` + // Draft object to set or clear. Non-empty drafts are only accepted when the + // current draft is empty. Send draft=null to clear text and attachments together + // before setting a new draft. + Draft ChatUpdateParamsDraft `json:"draft,omitzero"` + paramObj +} + +func (r ChatUpdateParams) MarshalJSON() (data []byte, err error) { + type shadow ChatUpdateParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatUpdateParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Draft object to set or clear. Non-empty drafts are only accepted when the +// current draft is empty. Send draft=null to clear text and attachments together +// before setting a new draft. +// +// The property Text is required. +type ChatUpdateParamsDraft struct { + // Draft text. Plain text and Markdown are converted to Matrix HTML with the same + // rules used by send and edit. + Text string `json:"text" api:"required"` + // Draft attachments keyed by attachment ID. Each attachment must reference an + // uploadID returned by the upload file endpoint. + Attachments map[string]ChatUpdateParamsDraftAttachment `json:"attachments,omitzero"` + paramObj +} + +func (r ChatUpdateParamsDraft) MarshalJSON() (data []byte, err error) { + type shadow ChatUpdateParamsDraft + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatUpdateParamsDraft) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property UploadID is required. +type ChatUpdateParamsDraftAttachment struct { + // Upload ID from uploadAsset endpoint. Required to reference uploaded files. + UploadID string `json:"uploadID" api:"required"` + // Optional draft attachment identifier. If omitted, a new identifier is generated. + ID param.Opt[string] `json:"id,omitzero"` + // Duration in seconds (optional override of cached value) + Duration param.Opt[float64] `json:"duration,omitzero"` + // Filename (optional override of cached value) + FileName param.Opt[string] `json:"fileName,omitzero"` + // MIME type (optional override of cached value) + MimeType param.Opt[string] `json:"mimeType,omitzero"` + // Dimensions (optional override of cached value) + Size ChatUpdateParamsDraftAttachmentSize `json:"size,omitzero"` + // Attachment type hint (image, video, audio, file, gif, voice-note, sticker). If + // omitted, auto-detected from mimeType + // + // Any of "image", "video", "audio", "file", "gif", "voice-note", "sticker". + Type string `json:"type,omitzero"` + paramObj +} + +func (r ChatUpdateParamsDraftAttachment) MarshalJSON() (data []byte, err error) { + type shadow ChatUpdateParamsDraftAttachment + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatUpdateParamsDraftAttachment) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func init() { + apijson.RegisterFieldValidator[ChatUpdateParamsDraftAttachment]( + "type", "image", "video", "audio", "file", "gif", "voice-note", "sticker", + ) +} + +// Dimensions (optional override of cached value) +// +// The properties Height, Width are required. +type ChatUpdateParamsDraftAttachmentSize struct { + Height float64 `json:"height" api:"required"` + Width float64 `json:"width" api:"required"` + paramObj +} + +func (r ChatUpdateParamsDraftAttachmentSize) MarshalJSON() (data []byte, err error) { + type shadow ChatUpdateParamsDraftAttachmentSize + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatUpdateParamsDraftAttachmentSize) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type ChatListParams struct { // Opaque pagination cursor; do not inspect. Use together with 'direction'. Cursor param.Opt[string] `query:"cursor,omitzero" json:"-"` @@ -589,6 +1117,46 @@ func (r *ChatArchiveParams) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type ChatMarkReadParams struct { + // Optional message ID to mark read through. + MessageID param.Opt[string] `json:"messageID,omitzero"` + paramObj +} + +func (r ChatMarkReadParams) MarshalJSON() (data []byte, err error) { + type shadow ChatMarkReadParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatMarkReadParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type ChatMarkUnreadParams struct { + // Optional message ID to mark unread from. + MessageID param.Opt[string] `json:"messageID,omitzero"` + paramObj +} + +func (r ChatMarkUnreadParams) MarshalJSON() (data []byte, err error) { + type shadow ChatMarkUnreadParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatMarkUnreadParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +type ChatNotifyAnywayParams struct { + paramObj +} + +func (r ChatNotifyAnywayParams) MarshalJSON() (data []byte, err error) { + type shadow ChatNotifyAnywayParams + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *ChatNotifyAnywayParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type ChatSearchParams struct { // Include chats marked as Muted by the user, which are usually less important. // Default: true. Set to false if the user wants a more refined search. diff --git a/chat_test.go b/chat_test.go index 4f509e3..b7e96e0 100644 --- a/chat_test.go +++ b/chat_test.go @@ -58,7 +58,59 @@ func TestChatGetWithOptionalParams(t *testing.T) { context.TODO(), "!NCdzlIaMjZUmvmvyHU:beeper.com", beeperdesktopapi.ChatGetParams{ - MaxParticipantCount: beeperdesktopapi.Int(50), + MaxParticipantCount: beeperdesktopapi.Int(100), + }, + ) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestChatUpdateWithOptionalParams(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := beeperdesktopapi.NewClient( + option.WithBaseURL(baseURL), + option.WithAccessToken("My Access Token"), + ) + _, err := client.Chats.Update( + context.TODO(), + "!NCdzlIaMjZUmvmvyHU:beeper.com", + beeperdesktopapi.ChatUpdateParams{ + Description: beeperdesktopapi.String("description"), + Draft: beeperdesktopapi.ChatUpdateParamsDraft{ + Text: "text", + Attachments: map[string]beeperdesktopapi.ChatUpdateParamsDraftAttachment{ + "foo": { + UploadID: "uploadID", + ID: beeperdesktopapi.String("id"), + Duration: beeperdesktopapi.Float(0), + FileName: beeperdesktopapi.String("fileName"), + MimeType: beeperdesktopapi.String("mimeType"), + Size: beeperdesktopapi.ChatUpdateParamsDraftAttachmentSize{ + Height: 0, + Width: 0, + }, + Type: "image", + }, + }, + }, + ImgURL: beeperdesktopapi.String("imgURL"), + IsArchived: beeperdesktopapi.Bool(true), + IsLowPriority: beeperdesktopapi.Bool(true), + IsMuted: beeperdesktopapi.Bool(true), + IsPinned: beeperdesktopapi.Bool(true), + MessageExpirySeconds: beeperdesktopapi.Int(0), + Title: beeperdesktopapi.String("title"), }, ) if err != nil { @@ -83,7 +135,7 @@ func TestChatListWithOptionalParams(t *testing.T) { option.WithAccessToken("My Access Token"), ) _, err := client.Chats.List(context.TODO(), beeperdesktopapi.ChatListParams{ - AccountIDs: []string{"local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU"}, + AccountIDs: []string{"matrix", "discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, Cursor: beeperdesktopapi.String("1725489123456|c29tZUltc2dQYWdl"), Direction: beeperdesktopapi.ChatListParamsDirectionBefore, }) @@ -124,6 +176,88 @@ func TestChatArchiveWithOptionalParams(t *testing.T) { } } +func TestChatMarkReadWithOptionalParams(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := beeperdesktopapi.NewClient( + option.WithBaseURL(baseURL), + option.WithAccessToken("My Access Token"), + ) + _, err := client.Chats.MarkRead( + context.TODO(), + "!NCdzlIaMjZUmvmvyHU:beeper.com", + beeperdesktopapi.ChatMarkReadParams{ + MessageID: beeperdesktopapi.String("1343993"), + }, + ) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestChatMarkUnreadWithOptionalParams(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := beeperdesktopapi.NewClient( + option.WithBaseURL(baseURL), + option.WithAccessToken("My Access Token"), + ) + _, err := client.Chats.MarkUnread( + context.TODO(), + "!NCdzlIaMjZUmvmvyHU:beeper.com", + beeperdesktopapi.ChatMarkUnreadParams{ + MessageID: beeperdesktopapi.String("1343993"), + }, + ) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestChatNotifyAnyway(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := beeperdesktopapi.NewClient( + option.WithBaseURL(baseURL), + option.WithAccessToken("My Access Token"), + ) + _, err := client.Chats.NotifyAnyway( + context.TODO(), + "!NCdzlIaMjZUmvmvyHU:beeper.com", + beeperdesktopapi.ChatNotifyAnywayParams{}, + ) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestChatSearchWithOptionalParams(t *testing.T) { baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -137,7 +271,7 @@ func TestChatSearchWithOptionalParams(t *testing.T) { option.WithAccessToken("My Access Token"), ) _, err := client.Chats.Search(context.TODO(), beeperdesktopapi.ChatSearchParams{ - AccountIDs: []string{"local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", "local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"}, + AccountIDs: []string{"matrix", "discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, Cursor: beeperdesktopapi.String("1725489123456|c29tZUltc2dQYWdl"), Direction: beeperdesktopapi.ChatSearchParamsDirectionBefore, Inbox: beeperdesktopapi.ChatSearchParamsInboxPrimary, @@ -160,7 +294,6 @@ func TestChatSearchWithOptionalParams(t *testing.T) { } func TestChatStartWithOptionalParams(t *testing.T) { - t.Skip("Stainless mock tests currently load the project-published OpenAPI spec URL, which may not include newly-added local-only endpoints during build checks.") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { baseURL = envURL diff --git a/chatmessagereaction.go b/chatmessagereaction.go index 25299ed..59b9ff0 100644 --- a/chatmessagereaction.go +++ b/chatmessagereaction.go @@ -7,11 +7,9 @@ import ( "errors" "fmt" "net/http" - "net/url" "slices" "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/apiquery" "github.com/beeper/desktop-api-go/internal/requestconfig" "github.com/beeper/desktop-api-go/option" "github.com/beeper/desktop-api-go/packages/param" @@ -40,18 +38,22 @@ func NewChatMessageReactionService(opts ...option.RequestOption) (r ChatMessageR } // Remove the reaction added by the authenticated user from an existing message. -func (r *ChatMessageReactionService) Delete(ctx context.Context, messageID string, params ChatMessageReactionDeleteParams, opts ...option.RequestOption) (res *ChatMessageReactionDeleteResponse, err error) { +func (r *ChatMessageReactionService) Delete(ctx context.Context, reactionKey string, body ChatMessageReactionDeleteParams, opts ...option.RequestOption) (res *ChatMessageReactionDeleteResponse, err error) { opts = slices.Concat(r.Options, opts) - if params.ChatID == "" { + if body.ChatID == "" { err = errors.New("missing required chatID parameter") return nil, err } - if messageID == "" { + if body.MessageID == "" { err = errors.New("missing required messageID parameter") return nil, err } - path := fmt.Sprintf("v1/chats/%s/messages/%s/reactions", params.ChatID, messageID) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, params, &res, opts...) + if reactionKey == "" { + err = errors.New("missing required reactionKey parameter") + return nil, err + } + path := fmt.Sprintf("v1/chats/%s/messages/%s/reactions/%s", body.ChatID, body.MessageID, reactionKey) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, &res, opts...) return res, err } @@ -72,13 +74,15 @@ func (r *ChatMessageReactionService) Add(ctx context.Context, messageID string, } type ChatMessageReactionDeleteResponse struct { - // Unique identifier of the chat. + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. ChatID string `json:"chatID" api:"required"` // Message ID. MessageID string `json:"messageID" api:"required"` - // Reaction key that was removed + // Reaction key that was removed. ReactionKey string `json:"reactionKey" api:"required"` - // Whether the reaction was successfully removed + // Always true. Indicates the reaction removal was queued; failures return an error + // response. Success bool `json:"success" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -98,15 +102,17 @@ func (r *ChatMessageReactionDeleteResponse) UnmarshalJSON(data []byte) error { } type ChatMessageReactionAddResponse struct { - // Unique identifier of the chat. + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. ChatID string `json:"chatID" api:"required"` // Message ID. MessageID string `json:"messageID" api:"required"` - // Reaction key that was added + // Reaction key that was added. ReactionKey string `json:"reactionKey" api:"required"` - // Whether the reaction was successfully added + // Always true. Indicates the reaction was queued; failures return an error + // response. Success bool `json:"success" api:"required"` - // Transaction ID used for the reaction event + // Transaction ID used for send tracking. TransactionID string `json:"transactionID" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -127,28 +133,21 @@ func (r *ChatMessageReactionAddResponse) UnmarshalJSON(data []byte) error { } type ChatMessageReactionDeleteParams struct { - // Unique identifier of the chat. + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. ChatID string `path:"chatID" api:"required" json:"-"` - // Reaction key to remove - ReactionKey string `query:"reactionKey" api:"required" json:"-"` + // Message ID. + MessageID string `path:"messageID" api:"required" json:"-"` paramObj } -// URLQuery serializes [ChatMessageReactionDeleteParams]'s query parameters as -// `url.Values`. -func (r ChatMessageReactionDeleteParams) URLQuery() (v url.Values, err error) { - return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ - ArrayFormat: apiquery.ArrayQueryFormatRepeat, - NestedFormat: apiquery.NestedQueryFormatBrackets, - }) -} - type ChatMessageReactionAddParams struct { - // Unique identifier of the chat. + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. ChatID string `path:"chatID" api:"required" json:"-"` // Reaction key to add (emoji, shortcode, or custom emoji key) ReactionKey string `json:"reactionKey" api:"required"` - // Optional transaction ID for deduplication and local echo tracking + // Optional transaction ID for deduplication and send tracking TransactionID param.Opt[string] `json:"transactionID,omitzero"` paramObj } diff --git a/chatmessagereaction_test.go b/chatmessagereaction_test.go index d67651f..2d31210 100644 --- a/chatmessagereaction_test.go +++ b/chatmessagereaction_test.go @@ -27,10 +27,10 @@ func TestChatMessageReactionDelete(t *testing.T) { ) _, err := client.Chats.Messages.Reactions.Delete( context.TODO(), - "messageID", + "x", beeperdesktopapi.ChatMessageReactionDeleteParams{ - ChatID: "!NCdzlIaMjZUmvmvyHU:beeper.com", - ReactionKey: "x", + ChatID: "!NCdzlIaMjZUmvmvyHU:beeper.com", + MessageID: "1343993", }, ) if err != nil { @@ -56,7 +56,7 @@ func TestChatMessageReactionAddWithOptionalParams(t *testing.T) { ) _, err := client.Chats.Messages.Reactions.Add( context.TODO(), - "messageID", + "1343993", beeperdesktopapi.ChatMessageReactionAddParams{ ChatID: "!NCdzlIaMjZUmvmvyHU:beeper.com", ReactionKey: "x", diff --git a/chatreminder.go b/chatreminder.go index 7d65d41..fa8b429 100644 --- a/chatreminder.go +++ b/chatreminder.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "slices" + "time" "github.com/beeper/desktop-api-go/internal/apijson" "github.com/beeper/desktop-api-go/internal/requestconfig" @@ -78,10 +79,10 @@ func (r *ChatReminderNewParams) UnmarshalJSON(data []byte) error { // Reminder configuration // -// The property RemindAtMs is required. +// The property RemindAt is required. type ChatReminderNewParamsReminder struct { - // Unix timestamp in milliseconds when reminder should trigger - RemindAtMs float64 `json:"remindAtMs" api:"required"` + // Timestamp when the reminder should trigger. + RemindAt time.Time `json:"remindAt" api:"required" format:"date-time"` // Cancel reminder if someone messages in the chat DismissOnIncomingMessage param.Opt[bool] `json:"dismissOnIncomingMessage,omitzero"` paramObj diff --git a/chatreminder_test.go b/chatreminder_test.go index cc2c7fd..54c8f8d 100644 --- a/chatreminder_test.go +++ b/chatreminder_test.go @@ -7,6 +7,7 @@ import ( "errors" "os" "testing" + "time" "github.com/beeper/desktop-api-go" "github.com/beeper/desktop-api-go/internal/testutil" @@ -30,7 +31,7 @@ func TestChatReminderNewWithOptionalParams(t *testing.T) { "!NCdzlIaMjZUmvmvyHU:beeper.com", beeperdesktopapi.ChatReminderNewParams{ Reminder: beeperdesktopapi.ChatReminderNewParamsReminder{ - RemindAtMs: 0, + RemindAt: time.Now(), DismissOnIncomingMessage: beeperdesktopapi.Bool(true), }, }, diff --git a/client.go b/client.go index 8f60a56..99a2e4b 100644 --- a/client.go +++ b/client.go @@ -26,7 +26,8 @@ type Client struct { Messages MessageService // Manage assets in Beeper Desktop, like message attachments Assets AssetService - // Control the Beeper Desktop application + // Server discovery and capability metadata. Use /v1/info before authentication + // setup. Info InfoService } @@ -139,7 +140,7 @@ func (r *Client) Delete(ctx context.Context, path string, params any, res any, o } // Focus Beeper Desktop and optionally navigate to a specific chat, message, or -// pre-fill draft text and attachment. +// pre-fill plain text and an image path. func (r *Client) Focus(ctx context.Context, body FocusParams, opts ...option.RequestOption) (res *FocusResponse, err error) { opts = slices.Concat(r.Options, opts) path := "v1/focus" diff --git a/info.go b/info.go index 2871536..60ac893 100644 --- a/info.go +++ b/info.go @@ -13,7 +13,8 @@ import ( "github.com/beeper/desktop-api-go/packages/respjson" ) -// Control the Beeper Desktop application +// Server discovery and capability metadata. Use /v1/info before authentication +// setup. // // InfoService contains methods and other services that help with interacting with // the beeperdesktop API. @@ -34,8 +35,8 @@ func NewInfoService(opts ...option.RequestOption) (r InfoService) { return } -// Returns app, platform, server, and endpoint discovery metadata for this Beeper -// Desktop instance. +// Returns app, platform, server, endpoint discovery, OAuth, and WebSocket metadata +// for this Beeper Desktop instance. func (r *InfoService) Get(ctx context.Context, opts ...option.RequestOption) (res *InfoGetResponse, err error) { var preClientOpts = []option.RequestOption{requestconfig.WithSecurity(requestconfig.Security{})} opts = slices.Concat(preClientOpts, r.Options, opts) @@ -170,7 +171,7 @@ func (r *InfoGetResponsePlatform) UnmarshalJSON(data []byte) error { } type InfoGetResponseServer struct { - // Base URL of the Connect server + // Base URL of the Beeper Desktop API server BaseURL string `json:"base_url" api:"required"` // Listening host Hostname string `json:"hostname" api:"required"` diff --git a/message.go b/message.go index 2ebbbb4..ca4b896 100644 --- a/message.go +++ b/message.go @@ -42,6 +42,23 @@ func NewMessageService(opts ...option.RequestOption) (r MessageService) { return } +// Retrieve a message by final message ID, pendingMessageID, or Matrix event ID. +// Chat ID may be a Beeper chat ID or local chat ID. +func (r *MessageService) Get(ctx context.Context, messageID string, query MessageGetParams, opts ...option.RequestOption) (res *shared.Message, err error) { + opts = slices.Concat(r.Options, opts) + if query.ChatID == "" { + err = errors.New("missing required chatID parameter") + return nil, err + } + if messageID == "" { + err = errors.New("missing required messageID parameter") + return nil, err + } + path := fmt.Sprintf("v1/chats/%s/messages/%s", query.ChatID, messageID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return res, err +} + // Edit the text content of an existing message. Messages with attachments cannot // be edited. func (r *MessageService) Update(ctx context.Context, messageID string, params MessageUpdateParams, opts ...option.RequestOption) (res *MessageUpdateResponse, err error) { @@ -86,6 +103,24 @@ func (r *MessageService) ListAutoPaging(ctx context.Context, chatID string, quer return pagination.NewCursorNoLimitAutoPager(r.List(ctx, chatID, query, opts...)) } +// Delete a message by final message ID. Pending message IDs are not accepted +// because messages cannot be deleted while sending. +func (r *MessageService) Delete(ctx context.Context, messageID string, params MessageDeleteParams, opts ...option.RequestOption) (err error) { + opts = slices.Concat(r.Options, opts) + opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) + if params.ChatID == "" { + err = errors.New("missing required chatID parameter") + return err + } + if messageID == "" { + err = errors.New("missing required messageID parameter") + return err + } + path := fmt.Sprintf("v1/chats/%s/messages/%s", params.ChatID, messageID) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, params, nil, opts...) + return err +} + // Search messages across chats. func (r *MessageService) Search(ctx context.Context, query MessageSearchParams, opts ...option.RequestOption) (res *pagination.CursorSearch[shared.Message], err error) { var raw *http.Response @@ -123,20 +158,25 @@ func (r *MessageService) Send(ctx context.Context, chatID string, body MessageSe } type MessageUpdateResponse struct { - // Unique identifier of the chat. - ChatID string `json:"chatID" api:"required"` - // Message ID. + // DEPRECATED - use id instead. Compatibility alias for older clients. + // + // Deprecated: deprecated MessageID string `json:"messageID" api:"required"` - // Whether the message was successfully edited + // DEPRECATED - compatibility field. Successful responses are already represented + // by the 200 status code. + // + // Any of true. + // + // Deprecated: deprecated Success bool `json:"success" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ChatID respjson.Field MessageID respjson.Field Success respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` + shared.Message } // Returns the unmodified JSON received from the API @@ -146,9 +186,12 @@ func (r *MessageUpdateResponse) UnmarshalJSON(data []byte) error { } type MessageSendResponse struct { - // Unique identifier of the chat. + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. ChatID string `json:"chatID" api:"required"` - // Pending message ID + // Pending ID assigned to the message before the network confirms the send. Pass it + // to GET /v1/chats/{chatID}/messages/{messageID} to resolve, or wait for the + // matching message.upserted over the WebSocket. PendingMessageID string `json:"pendingMessageID" api:"required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { @@ -165,8 +208,16 @@ func (r *MessageSendResponse) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type MessageGetParams struct { + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. + ChatID string `path:"chatID" api:"required" json:"-"` + paramObj +} + type MessageUpdateParams struct { - // Unique identifier of the chat. + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. ChatID string `path:"chatID" api:"required" json:"-"` // New text content for the message Text string `json:"text" api:"required"` @@ -209,6 +260,24 @@ const ( MessageListParamsDirectionBefore MessageListParamsDirection = "before" ) +type MessageDeleteParams struct { + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. + ChatID string `path:"chatID" api:"required" json:"-"` + // True to request deletion for everyone when the network supports it; false to + // delete only for the authenticated user when supported. + ForEveryone param.Opt[bool] `query:"forEveryone,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [MessageDeleteParams]'s query parameters as `url.Values`. +func (r MessageDeleteParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatRepeat, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} + type MessageSearchParams struct { // Exclude messages marked Low Priority by the user. Default: true. Set to false to // include all. @@ -283,7 +352,8 @@ const ( type MessageSendParams struct { // Provide a message ID to send this as a reply to an existing message ReplyToMessageID param.Opt[string] `json:"replyToMessageID,omitzero"` - // Text content of the message you want to send. You may use markdown. + // Draft text. Plain text and Markdown are converted to Matrix HTML with the same + // rules used by send and edit. Text param.Opt[string] `json:"text,omitzero"` // Single attachment to send with the message Attachment MessageSendParamsAttachment `json:"attachment,omitzero"` @@ -312,10 +382,10 @@ type MessageSendParamsAttachment struct { MimeType param.Opt[string] `json:"mimeType,omitzero"` // Dimensions (optional override of cached value) Size MessageSendParamsAttachmentSize `json:"size,omitzero"` - // Special attachment type (gif, voiceNote, sticker). If omitted, auto-detected - // from mimeType + // Attachment type hint (image, video, audio, file, gif, voice-note, sticker). If + // omitted, auto-detected from mimeType // - // Any of "gif", "voiceNote", "sticker". + // Any of "image", "video", "audio", "file", "gif", "voice-note", "sticker". Type string `json:"type,omitzero"` paramObj } @@ -330,7 +400,7 @@ func (r *MessageSendParamsAttachment) UnmarshalJSON(data []byte) error { func init() { apijson.RegisterFieldValidator[MessageSendParamsAttachment]( - "type", "gif", "voiceNote", "sticker", + "type", "image", "video", "audio", "file", "gif", "voice-note", "sticker", ) } diff --git a/message_test.go b/message_test.go index cb17cd7..c21916f 100644 --- a/message_test.go +++ b/message_test.go @@ -14,6 +14,34 @@ import ( "github.com/beeper/desktop-api-go/option" ) +func TestMessageGet(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := beeperdesktopapi.NewClient( + option.WithBaseURL(baseURL), + option.WithAccessToken("My Access Token"), + ) + _, err := client.Messages.Get( + context.TODO(), + "1343993", + beeperdesktopapi.MessageGetParams{ + ChatID: "!NCdzlIaMjZUmvmvyHU:beeper.com", + }, + ) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestMessageUpdate(t *testing.T) { baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -28,7 +56,7 @@ func TestMessageUpdate(t *testing.T) { ) _, err := client.Messages.Update( context.TODO(), - "messageID", + "1343993", beeperdesktopapi.MessageUpdateParams{ ChatID: "!NCdzlIaMjZUmvmvyHU:beeper.com", Text: "x", @@ -72,6 +100,35 @@ func TestMessageListWithOptionalParams(t *testing.T) { } } +func TestMessageDeleteWithOptionalParams(t *testing.T) { + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := beeperdesktopapi.NewClient( + option.WithBaseURL(baseURL), + option.WithAccessToken("My Access Token"), + ) + err := client.Messages.Delete( + context.TODO(), + "1343993", + beeperdesktopapi.MessageDeleteParams{ + ChatID: "!NCdzlIaMjZUmvmvyHU:beeper.com", + ForEveryone: beeperdesktopapi.Bool(true), + }, + ) + if err != nil { + var apierr *beeperdesktopapi.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestMessageSearchWithOptionalParams(t *testing.T) { baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -85,7 +142,7 @@ func TestMessageSearchWithOptionalParams(t *testing.T) { option.WithAccessToken("My Access Token"), ) _, err := client.Messages.Search(context.TODO(), beeperdesktopapi.MessageSearchParams{ - AccountIDs: []string{"local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc", "local-instagram_ba_eRfQMmnSNy_p7Ih7HL7RduRpKFU"}, + AccountIDs: []string{"matrix", "discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, ChatIDs: []string{"!NCdzlIaMjZUmvmvyHU:beeper.com", "1231073"}, ChatType: beeperdesktopapi.MessageSearchParamsChatTypeGroup, Cursor: beeperdesktopapi.String("1725489123456|c29tZUltc2dQYWdl"), @@ -133,7 +190,7 @@ func TestMessageSendWithOptionalParams(t *testing.T) { Height: 0, Width: 0, }, - Type: "gif", + Type: "image", }, ReplyToMessageID: beeperdesktopapi.String("replyToMessageID"), Text: beeperdesktopapi.String("text"), diff --git a/paginationauto_test.go b/paginationauto_test.go index d1b301a..3539201 100644 --- a/paginationauto_test.go +++ b/paginationauto_test.go @@ -25,9 +25,9 @@ func TestAutoPagination(t *testing.T) { option.WithAccessToken("My Access Token"), ) iter := client.Messages.SearchAutoPaging(context.TODO(), beeperdesktopapi.MessageSearchParams{ - AccountIDs: []string{"local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"}, + AccountIDs: []string{"discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, Limit: beeperdesktopapi.Int(10), - Query: beeperdesktopapi.String("deployment"), + Query: beeperdesktopapi.String("oauth"), }) // The mock server isn't going to give us real pagination for i := 0; i < 3 && iter.Next(); i++ { diff --git a/paginationmanual_test.go b/paginationmanual_test.go index 78b1ddc..821bf3d 100644 --- a/paginationmanual_test.go +++ b/paginationmanual_test.go @@ -25,9 +25,9 @@ func TestManualPagination(t *testing.T) { option.WithAccessToken("My Access Token"), ) page, err := client.Messages.Search(context.TODO(), beeperdesktopapi.MessageSearchParams{ - AccountIDs: []string{"local-telegram_ba_QFrb5lrLPhO3OT5MFBeTWv0x4BI"}, + AccountIDs: []string{"discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, Limit: beeperdesktopapi.Int(10), - Query: beeperdesktopapi.String("deployment"), + Query: beeperdesktopapi.String("oauth"), }) if err != nil { t.Fatalf("err should be nil: %s", err.Error()) diff --git a/shared/shared.go b/shared/shared.go index 3a7424c..4cb55e8 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -3,6 +3,7 @@ package shared import ( + "encoding/json" "time" "github.com/beeper/desktop-api-go/internal/apijson" @@ -21,8 +22,8 @@ type Attachment struct { // // Any of "unknown", "img", "video", "audio". Type AttachmentType `json:"type" api:"required"` - // Attachment identifier (typically an mxc:// URL). Use with /v1/assets/download to - // get a local file path. + // Attachment identifier (typically an mxc:// URL). Use the download file endpoint + // to get a local file path. ID string `json:"id"` // Duration in seconds (audio/video). Duration float64 `json:"duration"` @@ -43,25 +44,28 @@ type Attachment struct { PosterImg string `json:"posterImg"` // Pixel dimensions of the attachment: width/height in px. Size AttachmentSize `json:"size"` - // Public URL or local file path to fetch the asset. May be temporary or local-only + // Public URL or local file path to fetch the file. May be temporary or local-only // to this device; download promptly if durable access is needed. SrcURL string `json:"srcURL"` + // Attachment transcription if available. + Transcription AttachmentTranscription `json:"transcription"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - Type respjson.Field - ID respjson.Field - Duration respjson.Field - FileName respjson.Field - FileSize respjson.Field - IsGif respjson.Field - IsSticker respjson.Field - IsVoiceNote respjson.Field - MimeType respjson.Field - PosterImg respjson.Field - Size respjson.Field - SrcURL respjson.Field - ExtraFields map[string]respjson.Field - raw string + Type respjson.Field + ID respjson.Field + Duration respjson.Field + FileName respjson.Field + FileSize respjson.Field + IsGif respjson.Field + IsSticker respjson.Field + IsVoiceNote respjson.Field + MimeType respjson.Field + PosterImg respjson.Field + Size respjson.Field + SrcURL respjson.Field + Transcription respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -100,14 +104,40 @@ func (r *AttachmentSize) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Attachment transcription if available. +type AttachmentTranscription struct { + // Transcription engine. + Engine string `json:"engine" api:"required"` + // Transcribed text. + Transcription string `json:"transcription" api:"required"` + // Detected or selected language. + Language string `json:"language"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Engine respjson.Field + Transcription respjson.Field + Language respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r AttachmentTranscription) RawJSON() string { return r.JSON.raw } +func (r *AttachmentTranscription) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type Message struct { // Message ID. ID string `json:"id" api:"required"` // Beeper account ID the message belongs to. AccountID string `json:"accountID" api:"required"` - // Unique identifier of the chat. + // Chat ID. Input routes also accept the local chat ID from this Beeper Desktop + // installation when available. ChatID string `json:"chatID" api:"required"` - // Sender user ID. + // Matrix-style fully-qualified sender user ID, usually including a bridge prefix + // and homeserver. SenderID string `json:"senderID" api:"required"` // A unique, sortable key used to sort messages. SortKey string `json:"sortKey" api:"required"` @@ -115,18 +145,32 @@ type Message struct { Timestamp time.Time `json:"timestamp" api:"required" format:"date-time"` // Attachments included with this message, if any. Attachments []Attachment `json:"attachments"` + // Timestamp when the message was edited, if known. + EditedTimestamp time.Time `json:"editedTimestamp" format:"date-time"` + // True if the message has been deleted. + IsDeleted bool `json:"isDeleted"` + // True if the message is hidden from normal display. + IsHidden bool `json:"isHidden"` // True if the authenticated user sent the message. IsSender bool `json:"isSender"` // True if the message is unread for the authenticated user. May be omitted. IsUnread bool `json:"isUnread"` // ID of the message this is a reply to, if any. LinkedMessageID string `json:"linkedMessageID"` + // Link previews included with this message, if any. + Links []MessageLink `json:"links"` + // Mentioned user IDs, @room, or null for legacy messages that require text + // scanning. + Mentions []string `json:"mentions" api:"nullable"` // Reactions to the message, if any. Reactions []Reaction `json:"reactions"` + // Read receipt state for this message, when available. + Seen MessageSeenUnion `json:"seen" format:"date-time"` // Resolved sender display name (impersonator/full name/username/participant name). SenderName string `json:"senderName"` - // Plain-text body if present. May include a JSON fallback with text entities for - // rich messages. + // Message send status for this message, when reported by the bridge. + SendStatus MessageSendStatus `json:"sendStatus"` + // Matrix HTML body if present. Text string `json:"text"` // Message content type. Useful for distinguishing reactions, media messages, and // state events from regular text messages. @@ -143,11 +187,18 @@ type Message struct { SortKey respjson.Field Timestamp respjson.Field Attachments respjson.Field + EditedTimestamp respjson.Field + IsDeleted respjson.Field + IsHidden respjson.Field IsSender respjson.Field IsUnread respjson.Field LinkedMessageID respjson.Field + Links respjson.Field + Mentions respjson.Field Reactions respjson.Field + Seen respjson.Field SenderName respjson.Field + SendStatus respjson.Field Text respjson.Field Type respjson.Field ExtraFields map[string]respjson.Field @@ -161,6 +212,175 @@ func (r *Message) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Link preview included with a message. +type MessageLink struct { + // Link preview title. + Title string `json:"title" api:"required"` + // Resolved link URL. + URL string `json:"url" api:"required"` + // Favicon URL if available. May be temporary or local-only to this device; + // download promptly if durable access is needed. + Favicon string `json:"favicon"` + // Preview image URL if available. May be temporary or local-only to this device; + // download promptly if durable access is needed. + Img string `json:"img"` + // Preview image dimensions. + ImgSize MessageLinkImgSize `json:"imgSize"` + // Original URL when the displayed URL is shortened or redirected. + OriginalURL string `json:"originalURL"` + // Link preview summary. + Summary string `json:"summary"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Title respjson.Field + URL respjson.Field + Favicon respjson.Field + Img respjson.Field + ImgSize respjson.Field + OriginalURL respjson.Field + Summary respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MessageLink) RawJSON() string { return r.JSON.raw } +func (r *MessageLink) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Preview image dimensions. +type MessageLinkImgSize struct { + Height float64 `json:"height"` + Width float64 `json:"width"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Height respjson.Field + Width respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MessageLinkImgSize) RawJSON() string { return r.JSON.raw } +func (r *MessageLinkImgSize) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// MessageSeenUnion contains all possible properties and values from [bool], +// [time.Time], [map[string]MessageSeenByParticipantItemUnion]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +// +// If the underlying value is not a json object, one of the following properties +// will be valid: OfBoolean OfTimestamp] +type MessageSeenUnion struct { + // This field will be present if the value is a [bool] instead of an object. + OfBoolean bool `json:",inline"` + // This field will be present if the value is a [time.Time] instead of an object. + OfTimestamp time.Time `json:",inline"` + JSON struct { + OfBoolean respjson.Field + OfTimestamp respjson.Field + raw string + } `json:"-"` +} + +func (u MessageSeenUnion) AsBoolean() (v bool) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u MessageSeenUnion) AsTimestamp() (v time.Time) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u MessageSeenUnion) AsByParticipant() (v map[string]MessageSeenByParticipantItemUnion) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u MessageSeenUnion) RawJSON() string { return u.JSON.raw } + +func (r *MessageSeenUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// MessageSeenByParticipantItemUnion contains all possible properties and values +// from [bool], [time.Time]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +// +// If the underlying value is not a json object, one of the following properties +// will be valid: OfBoolean OfTimestamp] +type MessageSeenByParticipantItemUnion struct { + // This field will be present if the value is a [bool] instead of an object. + OfBoolean bool `json:",inline"` + // This field will be present if the value is a [time.Time] instead of an object. + OfTimestamp time.Time `json:",inline"` + JSON struct { + OfBoolean respjson.Field + OfTimestamp respjson.Field + raw string + } `json:"-"` +} + +func (u MessageSeenByParticipantItemUnion) AsBoolean() (v bool) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u MessageSeenByParticipantItemUnion) AsTimestamp() (v time.Time) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u MessageSeenByParticipantItemUnion) RawJSON() string { return u.JSON.raw } + +func (r *MessageSeenByParticipantItemUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Message send status for this message, when reported by the bridge. +type MessageSendStatus struct { + // Current status of the message send attempt. + // + // Any of "SUCCESS", "PENDING", "FAIL_RETRIABLE", "FAIL_PERMANENT". + Status string `json:"status" api:"required"` + // Timestamp for the send status event. + Timestamp time.Time `json:"timestamp" api:"required" format:"date-time"` + // User IDs the message was delivered to, when reported by the network. + DeliveredToUsers []string `json:"deliveredToUsers"` + // Internal bridge error detail. Intended for diagnostics, not end-user display. + InternalError string `json:"internalError"` + // Human-readable send status or failure message. + Message string `json:"message"` + // Machine-readable failure reason. Present when the send status is a failure. + Reason string `json:"reason"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Status respjson.Field + Timestamp respjson.Field + DeliveredToUsers respjson.Field + InternalError respjson.Field + Message respjson.Field + Reason respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r MessageSendStatus) RawJSON() string { return r.JSON.raw } +func (r *MessageSendStatus) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // Message content type. Useful for distinguishing reactions, media messages, and // state events from regular text messages. type MessageType string @@ -179,8 +399,9 @@ const ( ) type Reaction struct { - // Reaction ID, typically ${participantID}${reactionKey} if multiple reactions - // allowed, or just participantID otherwise. + // Reaction ID. When a participant can react more than once, the ID is the + // participant ID concatenated with the reaction key; otherwise it equals the + // participant ID. ID string `json:"id" api:"required"` // User ID of the participant who reacted. ParticipantID string `json:"participantID" api:"required"` @@ -221,8 +442,9 @@ type User struct { Email string `json:"email"` // Display name as shown in clients (e.g., 'Alice Example'). May include emojis. FullName string `json:"fullName"` - // Avatar image URL if available. May be temporary or local-only to this device; - // download promptly if durable access is needed. + // Avatar image URL if available. This may be a remote URL, Matrix media URL, data + // URL, or local filesystem URL depending on source and endpoint. May be temporary + // or local-only to this device; download promptly if durable access is needed. ImgURL string `json:"imgURL"` // True if this user represents the authenticated account's own identity. IsSelf bool `json:"isSelf"` diff --git a/usage_test.go b/usage_test.go index 1b24b2a..1f81d45 100644 --- a/usage_test.go +++ b/usage_test.go @@ -25,6 +25,7 @@ func TestUsage(t *testing.T) { option.WithAccessToken("My Access Token"), ) page, err := client.Chats.Search(context.TODO(), beeperdesktopapi.ChatSearchParams{ + AccountIDs: []string{"matrix", "discordgo", "local-whatsapp_ba_EvYDBBsZbRQAy3UOSWqG0LuTVkc"}, IncludeMuted: beeperdesktopapi.Bool(true), Limit: beeperdesktopapi.Int(3), Type: beeperdesktopapi.ChatSearchParamsTypeSingle, From f23c6027b62860e17329f92c4023d1d4708e6b24 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 21:18:20 +0000 Subject: [PATCH 42/42] release: 5.0.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 49 +++++++++ README.md | 10 +- account.go | 10 +- account_test.go | 6 +- accountcontact.go | 16 +-- accountcontact_test.go | 6 +- aliases.go | 6 +- api.md | 100 +++++++++--------- asset.go | 14 +-- asset_test.go | 6 +- beeperdesktopapi.go | 10 +- beeperdesktopapi_test.go | 6 +- chat.go | 16 +-- chat_test.go | 6 +- chatmessage.go | 2 +- chatmessagereaction.go | 10 +- chatmessagereaction_test.go | 6 +- chatreminder.go | 8 +- chatreminder_test.go | 6 +- client.go | 4 +- client_test.go | 6 +- field.go | 2 +- go.mod | 2 +- info.go | 8 +- info_test.go | 6 +- internal/apierror/apierror.go | 4 +- internal/apiform/encoder.go | 2 +- internal/apiform/form_test.go | 2 +- internal/apiform/richparam.go | 2 +- internal/apijson/decodeparam_test.go | 4 +- internal/apijson/decoder.go | 2 +- internal/apijson/decoderesp_test.go | 4 +- internal/apijson/encoder.go | 2 +- internal/apijson/subfield.go | 2 +- internal/apijson/union.go | 2 +- internal/apiquery/encoder.go | 2 +- internal/apiquery/query_test.go | 2 +- internal/apiquery/richparam.go | 2 +- internal/encoding/json/decode.go | 2 +- internal/encoding/json/encode.go | 4 +- internal/encoding/json/sentinel/null.go | 2 +- .../encoding/json/sentinel/sentinel_test.go | 4 +- internal/encoding/json/time.go | 2 +- internal/paramutil/field.go | 4 +- internal/paramutil/union.go | 2 +- internal/requestconfig/requestconfig.go | 8 +- internal/version.go | 2 +- message.go | 16 +-- message_test.go | 6 +- option/requestoption.go | 2 +- packages/pagination/pagination.go | 10 +- packages/param/encoder.go | 2 +- packages/param/encoder_test.go | 2 +- packages/param/null.go | 2 +- packages/param/null_test.go | 2 +- packages/param/option.go | 2 +- packages/param/param.go | 2 +- packages/respjson/decoder_test.go | 4 +- paginationauto_test.go | 6 +- paginationmanual_test.go | 6 +- shared/constant/constants.go | 2 +- shared/shared.go | 6 +- usage_test.go | 6 +- 64 files changed, 255 insertions(+), 206 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2aca35a..8e76abb 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.5.0" + ".": "5.0.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e49922..f774108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## 5.0.0 (2026-05-06) + +Full Changelog: [v0.5.0...v5.0.0](https://github.com/beeper/desktop-api-go/compare/v0.5.0...v5.0.0) + +### Features + +* **api:** add network, bridge fields to accounts ([5042271](https://github.com/beeper/desktop-api-go/commit/50422719fe1149b4a0a33cac7a7605e499fac9e4)) +* **api:** api update ([4681bd6](https://github.com/beeper/desktop-api-go/commit/4681bd6077032db3dfe8d8e6d4b4edaddcb7292c)) +* **api:** api update ([85ced8c](https://github.com/beeper/desktop-api-go/commit/85ced8c13f1d373cd36f5fb2c74ae3aad2a99016)) +* **api:** api update ([d2d6223](https://github.com/beeper/desktop-api-go/commit/d2d6223284dade370d2671176ff36811203d9981)) +* **api:** manual updates ([9fd07e4](https://github.com/beeper/desktop-api-go/commit/9fd07e44e68014b1ba88891a8e89d2994540df19)) +* **go:** add default http client with timeout ([7c34729](https://github.com/beeper/desktop-api-go/commit/7c347297b7df763e97e3554777ffebdecd37009c)) +* **internal:** support comma format in multipart form encoding ([d4f7e8e](https://github.com/beeper/desktop-api-go/commit/d4f7e8ef134cb9a535fa05458ee7af93532c8c2b)) +* support setting headers via env ([d4fdbe1](https://github.com/beeper/desktop-api-go/commit/d4fdbe157b2634c7e0c16be61583f9ebb99a8ed4)) + + +### Bug Fixes + +* prevent duplicate ? in query params ([8293eb7](https://github.com/beeper/desktop-api-go/commit/8293eb76584617003ec104770810028b3e4076b0)) + + +### Chores + +* avoid embedding reflect.Type for dead code elimination ([6a8ff6a](https://github.com/beeper/desktop-api-go/commit/6a8ff6a1189beb742794e0f7b11ce2aa5651c63e)) +* **ci:** skip lint on metadata-only changes ([2a83146](https://github.com/beeper/desktop-api-go/commit/2a83146c4d9b2f29ddce45b6f8484f7533f98c58)) +* **ci:** support opting out of skipping builds on metadata-only commits ([b4dec8f](https://github.com/beeper/desktop-api-go/commit/b4dec8f324d1c18699f81d6a060b33c1b0d4772a)) +* **client:** fix multipart serialisation of Default() fields ([029f5d6](https://github.com/beeper/desktop-api-go/commit/029f5d67da0158cf1ed913fc3f6dfd213681d363)) +* **internal:** minor cleanup ([9504a61](https://github.com/beeper/desktop-api-go/commit/9504a6100b1c23909976ac1473285cab5397aece)) +* **internal:** more robust bootstrap script ([25b94e4](https://github.com/beeper/desktop-api-go/commit/25b94e4324b7c4b7f0c8decbaed4cd8ed979a3b4)) +* **internal:** support default value struct tag ([3e39971](https://github.com/beeper/desktop-api-go/commit/3e39971ec0ee20accae2c2eae5e64004f6cfe810)) +* **internal:** tweak CI branches ([b7458ce](https://github.com/beeper/desktop-api-go/commit/b7458ceb902a6e768e54adc05d5532f63147e3db)) +* **internal:** update gitignore ([229284f](https://github.com/beeper/desktop-api-go/commit/229284fcf7e552241afaebbe24df184a5db693af)) +* **internal:** use explicit returns ([ce4f065](https://github.com/beeper/desktop-api-go/commit/ce4f065cc711a045df715b8c0a7c4338f7d114db)) +* **internal:** use explicit returns in more places ([f102b8e](https://github.com/beeper/desktop-api-go/commit/f102b8ecdfa469185a1ed3700f73952f7c3bfec5)) +* remove unnecessary error check for url parsing ([76dc981](https://github.com/beeper/desktop-api-go/commit/76dc981c3f95e4b7935aaefd95f1de5e119d9298)) +* **tests:** bump steady to v0.19.4 ([0260191](https://github.com/beeper/desktop-api-go/commit/026019176b91a6cc8a06f3e34cf6df2365f14e50)) +* **tests:** bump steady to v0.19.5 ([bb2044f](https://github.com/beeper/desktop-api-go/commit/bb2044f9230e575f610398a5f28a43bd7b7e8cf2)) +* **tests:** bump steady to v0.19.6 ([30b897c](https://github.com/beeper/desktop-api-go/commit/30b897ce73a2216204876ab5fb3132cf549228ee)) +* **tests:** bump steady to v0.19.7 ([b64d423](https://github.com/beeper/desktop-api-go/commit/b64d42348319e02f4cad0107d9848f57743d04c7)) +* **tests:** bump steady to v0.20.1 ([8e7e3a6](https://github.com/beeper/desktop-api-go/commit/8e7e3a6fcedaf7b4eaeb0f7e880e5347f5c2ac93)) +* **tests:** bump steady to v0.20.2 ([a5fbcc7](https://github.com/beeper/desktop-api-go/commit/a5fbcc7e74ba585849c839bffb71ee1b42f6af2d)) +* **tests:** bump steady to v0.22.1 ([423bb69](https://github.com/beeper/desktop-api-go/commit/423bb69d2de050be324c3389a759747693eb55ed)) +* update docs for api:"required" ([350f111](https://github.com/beeper/desktop-api-go/commit/350f111de7c4045065804f2425729f72a65a1388)) + + +### Refactors + +* **tests:** switch from prism to steady ([bd09ac3](https://github.com/beeper/desktop-api-go/commit/bd09ac321d342e5f197f8ceac6abf4ee5e766822)) + ## 0.5.0 (2026-03-06) Full Changelog: [v0.4.0...v0.5.0](https://github.com/beeper/desktop-api-go/compare/v0.4.0...v0.5.0) diff --git a/README.md b/README.md index 5f42211..858aded 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -Go Reference +Go Reference @@ -24,7 +24,7 @@ Use the Beeper Desktop MCP Server to enable AI assistants to interact with this ```go import ( - "github.com/beeper/desktop-api-go" // imported as beeperdesktopapi + "github.com/beeper/desktop-api-go/v5" // imported as beeperdesktopapi ) ``` @@ -35,7 +35,7 @@ Or to pin the version: ```sh -go get -u 'github.com/beeper/desktop-api-go@v0.5.0' +go get -u 'github.com/beeper/desktop-api-go@v5.0.0' ``` @@ -55,8 +55,8 @@ import ( "context" "fmt" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/option" ) func main() { diff --git a/account.go b/account.go index 5a0b582..2e8f335 100644 --- a/account.go +++ b/account.go @@ -7,11 +7,11 @@ import ( "net/http" "slices" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/respjson" - "github.com/beeper/desktop-api-go/shared" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/respjson" + "github.com/beeper/desktop-api-go/v5/shared" ) // Manage connected chat accounts diff --git a/account_test.go b/account_test.go index 884aff1..b8875e9 100644 --- a/account_test.go +++ b/account_test.go @@ -8,9 +8,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestAccountList(t *testing.T) { diff --git a/accountcontact.go b/accountcontact.go index f38a21c..75a41a1 100644 --- a/accountcontact.go +++ b/accountcontact.go @@ -10,14 +10,14 @@ import ( "net/url" "slices" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/apiquery" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/pagination" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" - "github.com/beeper/desktop-api-go/shared" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/apiquery" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/pagination" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" + "github.com/beeper/desktop-api-go/v5/shared" ) // Manage contacts on a specific account diff --git a/accountcontact_test.go b/accountcontact_test.go index ec38de1..cd195b8 100644 --- a/accountcontact_test.go +++ b/accountcontact_test.go @@ -8,9 +8,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestAccountContactListWithOptionalParams(t *testing.T) { diff --git a/aliases.go b/aliases.go index 6860625..e7c69e4 100644 --- a/aliases.go +++ b/aliases.go @@ -3,9 +3,9 @@ package beeperdesktopapi import ( - "github.com/beeper/desktop-api-go/internal/apierror" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/shared" + "github.com/beeper/desktop-api-go/v5/internal/apierror" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/shared" ) // aliased to make [param.APIUnion] private when embedding diff --git a/api.md b/api.md index 7f2a38f..d0d68af 100644 --- a/api.md +++ b/api.md @@ -1,71 +1,71 @@ # Shared Response Types -- shared.Attachment -- shared.Message -- shared.Reaction -- shared.User +- shared.Attachment +- shared.Message +- shared.Reaction +- shared.User # beeperdesktopapi Response Types: -- beeperdesktopapi.FocusResponse -- beeperdesktopapi.SearchResponse +- beeperdesktopapi.FocusResponse +- beeperdesktopapi.SearchResponse Methods: -- client.Focus(ctx context.Context, body beeperdesktopapi.FocusParams) (\*beeperdesktopapi.FocusResponse, error) -- client.Search(ctx context.Context, query beeperdesktopapi.SearchParams) (\*beeperdesktopapi.SearchResponse, error) +- client.Focus(ctx context.Context, body beeperdesktopapi.FocusParams) (\*beeperdesktopapi.FocusResponse, error) +- client.Search(ctx context.Context, query beeperdesktopapi.SearchParams) (\*beeperdesktopapi.SearchResponse, error) # Accounts Response Types: -- beeperdesktopapi.Account +- beeperdesktopapi.Account Methods: -- client.Accounts.List(ctx context.Context) (\*[]beeperdesktopapi.Account, error) +- client.Accounts.List(ctx context.Context) (\*[]beeperdesktopapi.Account, error) ## Contacts Response Types: -- beeperdesktopapi.AccountContactSearchResponse +- beeperdesktopapi.AccountContactSearchResponse Methods: -- client.Accounts.Contacts.List(ctx context.Context, accountID string, query beeperdesktopapi.AccountContactListParams) (\*pagination.CursorSearch[shared.User], error) -- client.Accounts.Contacts.Search(ctx context.Context, accountID string, query beeperdesktopapi.AccountContactSearchParams) (\*beeperdesktopapi.AccountContactSearchResponse, error) +- client.Accounts.Contacts.List(ctx context.Context, accountID string, query beeperdesktopapi.AccountContactListParams) (\*pagination.CursorSearch[shared.User], error) +- client.Accounts.Contacts.Search(ctx context.Context, accountID string, query beeperdesktopapi.AccountContactSearchParams) (\*beeperdesktopapi.AccountContactSearchResponse, error) # Chats Response Types: -- beeperdesktopapi.Chat -- beeperdesktopapi.ChatNewResponse -- beeperdesktopapi.ChatListResponse -- beeperdesktopapi.ChatStartResponse +- beeperdesktopapi.Chat +- beeperdesktopapi.ChatNewResponse +- beeperdesktopapi.ChatListResponse +- beeperdesktopapi.ChatStartResponse Methods: -- client.Chats.New(ctx context.Context, body beeperdesktopapi.ChatNewParams) (\*beeperdesktopapi.ChatNewResponse, error) -- client.Chats.Get(ctx context.Context, chatID string, query beeperdesktopapi.ChatGetParams) (\*beeperdesktopapi.Chat, error) -- client.Chats.Update(ctx context.Context, chatID string, body beeperdesktopapi.ChatUpdateParams) (\*beeperdesktopapi.Chat, error) -- client.Chats.List(ctx context.Context, query beeperdesktopapi.ChatListParams) (\*pagination.CursorNoLimit[beeperdesktopapi.ChatListResponse], error) -- client.Chats.Archive(ctx context.Context, chatID string, body beeperdesktopapi.ChatArchiveParams) error -- client.Chats.MarkRead(ctx context.Context, chatID string, body beeperdesktopapi.ChatMarkReadParams) (\*beeperdesktopapi.Chat, error) -- client.Chats.MarkUnread(ctx context.Context, chatID string, body beeperdesktopapi.ChatMarkUnreadParams) (\*beeperdesktopapi.Chat, error) -- client.Chats.NotifyAnyway(ctx context.Context, chatID string, body beeperdesktopapi.ChatNotifyAnywayParams) (\*beeperdesktopapi.Chat, error) -- client.Chats.Search(ctx context.Context, query beeperdesktopapi.ChatSearchParams) (\*pagination.CursorSearch[beeperdesktopapi.Chat], error) -- client.Chats.Start(ctx context.Context, body beeperdesktopapi.ChatStartParams) (\*beeperdesktopapi.ChatStartResponse, error) +- client.Chats.New(ctx context.Context, body beeperdesktopapi.ChatNewParams) (\*beeperdesktopapi.ChatNewResponse, error) +- client.Chats.Get(ctx context.Context, chatID string, query beeperdesktopapi.ChatGetParams) (\*beeperdesktopapi.Chat, error) +- client.Chats.Update(ctx context.Context, chatID string, body beeperdesktopapi.ChatUpdateParams) (\*beeperdesktopapi.Chat, error) +- client.Chats.List(ctx context.Context, query beeperdesktopapi.ChatListParams) (\*pagination.CursorNoLimit[beeperdesktopapi.ChatListResponse], error) +- client.Chats.Archive(ctx context.Context, chatID string, body beeperdesktopapi.ChatArchiveParams) error +- client.Chats.MarkRead(ctx context.Context, chatID string, body beeperdesktopapi.ChatMarkReadParams) (\*beeperdesktopapi.Chat, error) +- client.Chats.MarkUnread(ctx context.Context, chatID string, body beeperdesktopapi.ChatMarkUnreadParams) (\*beeperdesktopapi.Chat, error) +- client.Chats.NotifyAnyway(ctx context.Context, chatID string, body beeperdesktopapi.ChatNotifyAnywayParams) (\*beeperdesktopapi.Chat, error) +- client.Chats.Search(ctx context.Context, query beeperdesktopapi.ChatSearchParams) (\*pagination.CursorSearch[beeperdesktopapi.Chat], error) +- client.Chats.Start(ctx context.Context, body beeperdesktopapi.ChatStartParams) (\*beeperdesktopapi.ChatStartResponse, error) ## Reminders Methods: -- client.Chats.Reminders.New(ctx context.Context, chatID string, body beeperdesktopapi.ChatReminderNewParams) error -- client.Chats.Reminders.Delete(ctx context.Context, chatID string) error +- client.Chats.Reminders.New(ctx context.Context, chatID string, body beeperdesktopapi.ChatReminderNewParams) error +- client.Chats.Reminders.Delete(ctx context.Context, chatID string) error ## Messages @@ -73,51 +73,51 @@ Methods: Response Types: -- beeperdesktopapi.ChatMessageReactionDeleteResponse -- beeperdesktopapi.ChatMessageReactionAddResponse +- beeperdesktopapi.ChatMessageReactionDeleteResponse +- beeperdesktopapi.ChatMessageReactionAddResponse Methods: -- client.Chats.Messages.Reactions.Delete(ctx context.Context, reactionKey string, body beeperdesktopapi.ChatMessageReactionDeleteParams) (\*beeperdesktopapi.ChatMessageReactionDeleteResponse, error) -- client.Chats.Messages.Reactions.Add(ctx context.Context, messageID string, params beeperdesktopapi.ChatMessageReactionAddParams) (\*beeperdesktopapi.ChatMessageReactionAddResponse, error) +- client.Chats.Messages.Reactions.Delete(ctx context.Context, reactionKey string, body beeperdesktopapi.ChatMessageReactionDeleteParams) (\*beeperdesktopapi.ChatMessageReactionDeleteResponse, error) +- client.Chats.Messages.Reactions.Add(ctx context.Context, messageID string, params beeperdesktopapi.ChatMessageReactionAddParams) (\*beeperdesktopapi.ChatMessageReactionAddResponse, error) # Messages Response Types: -- beeperdesktopapi.MessageUpdateResponse -- beeperdesktopapi.MessageSendResponse +- beeperdesktopapi.MessageUpdateResponse +- beeperdesktopapi.MessageSendResponse Methods: -- client.Messages.Get(ctx context.Context, messageID string, query beeperdesktopapi.MessageGetParams) (\*shared.Message, error) -- client.Messages.Update(ctx context.Context, messageID string, params beeperdesktopapi.MessageUpdateParams) (\*beeperdesktopapi.MessageUpdateResponse, error) -- client.Messages.List(ctx context.Context, chatID string, query beeperdesktopapi.MessageListParams) (\*pagination.CursorNoLimit[shared.Message], error) -- client.Messages.Delete(ctx context.Context, messageID string, params beeperdesktopapi.MessageDeleteParams) error -- client.Messages.Search(ctx context.Context, query beeperdesktopapi.MessageSearchParams) (\*pagination.CursorSearch[shared.Message], error) -- client.Messages.Send(ctx context.Context, chatID string, body beeperdesktopapi.MessageSendParams) (\*beeperdesktopapi.MessageSendResponse, error) +- client.Messages.Get(ctx context.Context, messageID string, query beeperdesktopapi.MessageGetParams) (\*shared.Message, error) +- client.Messages.Update(ctx context.Context, messageID string, params beeperdesktopapi.MessageUpdateParams) (\*beeperdesktopapi.MessageUpdateResponse, error) +- client.Messages.List(ctx context.Context, chatID string, query beeperdesktopapi.MessageListParams) (\*pagination.CursorNoLimit[shared.Message], error) +- client.Messages.Delete(ctx context.Context, messageID string, params beeperdesktopapi.MessageDeleteParams) error +- client.Messages.Search(ctx context.Context, query beeperdesktopapi.MessageSearchParams) (\*pagination.CursorSearch[shared.Message], error) +- client.Messages.Send(ctx context.Context, chatID string, body beeperdesktopapi.MessageSendParams) (\*beeperdesktopapi.MessageSendResponse, error) # Assets Response Types: -- beeperdesktopapi.AssetDownloadResponse -- beeperdesktopapi.AssetUploadResponse -- beeperdesktopapi.AssetUploadBase64Response +- beeperdesktopapi.AssetDownloadResponse +- beeperdesktopapi.AssetUploadResponse +- beeperdesktopapi.AssetUploadBase64Response Methods: -- client.Assets.Download(ctx context.Context, body beeperdesktopapi.AssetDownloadParams) (\*beeperdesktopapi.AssetDownloadResponse, error) -- client.Assets.Serve(ctx context.Context, query beeperdesktopapi.AssetServeParams) (\*http.Response, error) -- client.Assets.Upload(ctx context.Context, body beeperdesktopapi.AssetUploadParams) (\*beeperdesktopapi.AssetUploadResponse, error) -- client.Assets.UploadBase64(ctx context.Context, body beeperdesktopapi.AssetUploadBase64Params) (\*beeperdesktopapi.AssetUploadBase64Response, error) +- client.Assets.Download(ctx context.Context, body beeperdesktopapi.AssetDownloadParams) (\*beeperdesktopapi.AssetDownloadResponse, error) +- client.Assets.Serve(ctx context.Context, query beeperdesktopapi.AssetServeParams) (\*http.Response, error) +- client.Assets.Upload(ctx context.Context, body beeperdesktopapi.AssetUploadParams) (\*beeperdesktopapi.AssetUploadResponse, error) +- client.Assets.UploadBase64(ctx context.Context, body beeperdesktopapi.AssetUploadBase64Params) (\*beeperdesktopapi.AssetUploadBase64Response, error) # Info Response Types: -- beeperdesktopapi.InfoGetResponse +- beeperdesktopapi.InfoGetResponse Methods: -- client.Info.Get(ctx context.Context) (\*beeperdesktopapi.InfoGetResponse, error) +- client.Info.Get(ctx context.Context) (\*beeperdesktopapi.InfoGetResponse, error) diff --git a/asset.go b/asset.go index 0b98119..c9379a4 100644 --- a/asset.go +++ b/asset.go @@ -11,13 +11,13 @@ import ( "net/url" "slices" - "github.com/beeper/desktop-api-go/internal/apiform" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/apiquery" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/internal/apiform" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/apiquery" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" ) // Manage assets in Beeper Desktop, like message attachments diff --git a/asset_test.go b/asset_test.go index 3b7face..d1c4b07 100644 --- a/asset_test.go +++ b/asset_test.go @@ -12,9 +12,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestAssetDownload(t *testing.T) { diff --git a/beeperdesktopapi.go b/beeperdesktopapi.go index 482dab9..d50d11f 100644 --- a/beeperdesktopapi.go +++ b/beeperdesktopapi.go @@ -5,11 +5,11 @@ package beeperdesktopapi import ( "net/url" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/apiquery" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" - "github.com/beeper/desktop-api-go/shared" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/apiquery" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" + "github.com/beeper/desktop-api-go/v5/shared" ) // Response indicating successful app focus action. diff --git a/beeperdesktopapi_test.go b/beeperdesktopapi_test.go index 3160381..b2fefa4 100644 --- a/beeperdesktopapi_test.go +++ b/beeperdesktopapi_test.go @@ -8,9 +8,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestFocusWithOptionalParams(t *testing.T) { diff --git a/chat.go b/chat.go index ab1b4f1..49060be 100644 --- a/chat.go +++ b/chat.go @@ -11,14 +11,14 @@ import ( "slices" "time" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/apiquery" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/pagination" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" - "github.com/beeper/desktop-api-go/shared" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/apiquery" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/pagination" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" + "github.com/beeper/desktop-api-go/v5/shared" ) // Manage chats diff --git a/chat_test.go b/chat_test.go index b7e96e0..61f7344 100644 --- a/chat_test.go +++ b/chat_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestChatNewWithOptionalParams(t *testing.T) { diff --git a/chatmessage.go b/chatmessage.go index 90c990e..86afd8a 100644 --- a/chatmessage.go +++ b/chatmessage.go @@ -3,7 +3,7 @@ package beeperdesktopapi import ( - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5/option" ) // Manage chat messages diff --git a/chatmessagereaction.go b/chatmessagereaction.go index 59b9ff0..141ed76 100644 --- a/chatmessagereaction.go +++ b/chatmessagereaction.go @@ -9,11 +9,11 @@ import ( "net/http" "slices" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" ) // Manage message reactions diff --git a/chatmessagereaction_test.go b/chatmessagereaction_test.go index 2d31210..71a25d6 100644 --- a/chatmessagereaction_test.go +++ b/chatmessagereaction_test.go @@ -8,9 +8,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestChatMessageReactionDelete(t *testing.T) { diff --git a/chatreminder.go b/chatreminder.go index fa8b429..afcc735 100644 --- a/chatreminder.go +++ b/chatreminder.go @@ -10,10 +10,10 @@ import ( "slices" "time" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/param" ) // Manage reminders for chats diff --git a/chatreminder_test.go b/chatreminder_test.go index 54c8f8d..76ed918 100644 --- a/chatreminder_test.go +++ b/chatreminder_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestChatReminderNewWithOptionalParams(t *testing.T) { diff --git a/client.go b/client.go index 99a2e4b..ec43f74 100644 --- a/client.go +++ b/client.go @@ -9,8 +9,8 @@ import ( "slices" "strings" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" ) // Client creates a struct with services and top level methods that help with diff --git a/client_test.go b/client_test.go index b427d46..64a3cdd 100644 --- a/client_test.go +++ b/client_test.go @@ -10,9 +10,9 @@ import ( "testing" "time" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal" + "github.com/beeper/desktop-api-go/v5/option" ) type closureTransport struct { diff --git a/field.go b/field.go index 8aba1fb..74fc1f8 100644 --- a/field.go +++ b/field.go @@ -1,7 +1,7 @@ package beeperdesktopapi import ( - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "io" "time" ) diff --git a/go.mod b/go.mod index 24af450..34fab02 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/beeper/desktop-api-go +module github.com/beeper/desktop-api-go/v5 go 1.22 diff --git a/info.go b/info.go index 60ac893..c230686 100644 --- a/info.go +++ b/info.go @@ -7,10 +7,10 @@ import ( "net/http" "slices" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/respjson" ) // Server discovery and capability metadata. Use /v1/info before authentication diff --git a/info_test.go b/info_test.go index 1c55d30..7a3d349 100644 --- a/info_test.go +++ b/info_test.go @@ -8,9 +8,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestInfoGet(t *testing.T) { diff --git a/internal/apierror/apierror.go b/internal/apierror/apierror.go index 0fa3a87..8b02aad 100644 --- a/internal/apierror/apierror.go +++ b/internal/apierror/apierror.go @@ -7,8 +7,8 @@ import ( "net/http" "net/http/httputil" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/packages/respjson" ) // Error represents an error that originates from the API, i.e. when a request is diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index b2ac6fe..e03f1f9 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -13,7 +13,7 @@ import ( "sync" "time" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" ) var encoders sync.Map // map[encoderEntry]encoderFunc diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index 286ea13..873b763 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -2,7 +2,7 @@ package apiform import ( "bytes" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "io" "mime/multipart" "strings" diff --git a/internal/apiform/richparam.go b/internal/apiform/richparam.go index 76206f9..547aabc 100644 --- a/internal/apiform/richparam.go +++ b/internal/apiform/richparam.go @@ -1,7 +1,7 @@ package apiform import ( - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "mime/multipart" "reflect" ) diff --git a/internal/apijson/decodeparam_test.go b/internal/apijson/decodeparam_test.go index 81dec85..8a0576f 100644 --- a/internal/apijson/decodeparam_test.go +++ b/internal/apijson/decodeparam_test.go @@ -3,8 +3,8 @@ package apijson_test import ( "encoding/json" "fmt" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/packages/param" "reflect" "testing" ) diff --git a/internal/apijson/decoder.go b/internal/apijson/decoder.go index e9eb7a9..bab1a38 100644 --- a/internal/apijson/decoder.go +++ b/internal/apijson/decoder.go @@ -7,7 +7,7 @@ package apijson import ( "encoding/json" "fmt" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "reflect" "strconv" "sync" diff --git a/internal/apijson/decoderesp_test.go b/internal/apijson/decoderesp_test.go index 4ccbb41..9d335b1 100644 --- a/internal/apijson/decoderesp_test.go +++ b/internal/apijson/decoderesp_test.go @@ -2,8 +2,8 @@ package apijson_test import ( "encoding/json" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/packages/respjson" "testing" ) diff --git a/internal/apijson/encoder.go b/internal/apijson/encoder.go index c903d90..ee17217 100644 --- a/internal/apijson/encoder.go +++ b/internal/apijson/encoder.go @@ -13,7 +13,7 @@ import ( "github.com/tidwall/sjson" - shimjson "github.com/beeper/desktop-api-go/internal/encoding/json" + shimjson "github.com/beeper/desktop-api-go/v5/internal/encoding/json" ) var encoders sync.Map // map[encoderEntry]encoderFunc diff --git a/internal/apijson/subfield.go b/internal/apijson/subfield.go index 45d128a..adffc27 100644 --- a/internal/apijson/subfield.go +++ b/internal/apijson/subfield.go @@ -1,7 +1,7 @@ package apijson import ( - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/packages/respjson" "reflect" ) diff --git a/internal/apijson/union.go b/internal/apijson/union.go index 6eeb391..d780af5 100644 --- a/internal/apijson/union.go +++ b/internal/apijson/union.go @@ -2,7 +2,7 @@ package apijson import ( "errors" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "reflect" "github.com/tidwall/gjson" diff --git a/internal/apiquery/encoder.go b/internal/apiquery/encoder.go index 3444566..79994ec 100644 --- a/internal/apiquery/encoder.go +++ b/internal/apiquery/encoder.go @@ -9,7 +9,7 @@ import ( "sync" "time" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" ) var encoders sync.Map // map[reflect.Type]encoderFunc diff --git a/internal/apiquery/query_test.go b/internal/apiquery/query_test.go index 6ccb35e..aa5af6c 100644 --- a/internal/apiquery/query_test.go +++ b/internal/apiquery/query_test.go @@ -1,7 +1,7 @@ package apiquery import ( - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "net/url" "testing" "time" diff --git a/internal/apiquery/richparam.go b/internal/apiquery/richparam.go index c542ee7..953395f 100644 --- a/internal/apiquery/richparam.go +++ b/internal/apiquery/richparam.go @@ -1,7 +1,7 @@ package apiquery import ( - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "reflect" ) diff --git a/internal/encoding/json/decode.go b/internal/encoding/json/decode.go index 2432c3b..9a317a1 100644 --- a/internal/encoding/json/decode.go +++ b/internal/encoding/json/decode.go @@ -14,7 +14,7 @@ import ( "encoding" "encoding/base64" "fmt" - "github.com/beeper/desktop-api-go/internal/encoding/json/shims" + "github.com/beeper/desktop-api-go/v5/internal/encoding/json/shims" "reflect" "strconv" "strings" diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go index a292df9..041ca3d 100644 --- a/internal/encoding/json/encode.go +++ b/internal/encoding/json/encode.go @@ -19,8 +19,8 @@ import ( "encoding" "encoding/base64" "fmt" - "github.com/beeper/desktop-api-go/internal/encoding/json/sentinel" - "github.com/beeper/desktop-api-go/internal/encoding/json/shims" + "github.com/beeper/desktop-api-go/v5/internal/encoding/json/sentinel" + "github.com/beeper/desktop-api-go/v5/internal/encoding/json/shims" "math" "reflect" "slices" diff --git a/internal/encoding/json/sentinel/null.go b/internal/encoding/json/sentinel/null.go index 48e0af0..ef387aa 100644 --- a/internal/encoding/json/sentinel/null.go +++ b/internal/encoding/json/sentinel/null.go @@ -1,7 +1,7 @@ package sentinel import ( - "github.com/beeper/desktop-api-go/internal/encoding/json/shims" + "github.com/beeper/desktop-api-go/v5/internal/encoding/json/shims" "reflect" "sync" ) diff --git a/internal/encoding/json/sentinel/sentinel_test.go b/internal/encoding/json/sentinel/sentinel_test.go index ddc509f..659b2df 100644 --- a/internal/encoding/json/sentinel/sentinel_test.go +++ b/internal/encoding/json/sentinel/sentinel_test.go @@ -1,8 +1,8 @@ package sentinel_test import ( - "github.com/beeper/desktop-api-go/internal/encoding/json/sentinel" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/internal/encoding/json/sentinel" + "github.com/beeper/desktop-api-go/v5/packages/param" "reflect" "slices" "testing" diff --git a/internal/encoding/json/time.go b/internal/encoding/json/time.go index c19b606..7ad4a61 100644 --- a/internal/encoding/json/time.go +++ b/internal/encoding/json/time.go @@ -2,7 +2,7 @@ package json import ( - "github.com/beeper/desktop-api-go/internal/encoding/json/shims" + "github.com/beeper/desktop-api-go/v5/internal/encoding/json/shims" "reflect" "time" ) diff --git a/internal/paramutil/field.go b/internal/paramutil/field.go index b7e301c..157f3fc 100644 --- a/internal/paramutil/field.go +++ b/internal/paramutil/field.go @@ -1,8 +1,8 @@ package paramutil import ( - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" ) func AddrIfPresent[T comparable](v param.Opt[T]) *T { diff --git a/internal/paramutil/union.go b/internal/paramutil/union.go index 8da3a48..b55f02b 100644 --- a/internal/paramutil/union.go +++ b/internal/paramutil/union.go @@ -2,7 +2,7 @@ package paramutil import ( "fmt" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "reflect" ) diff --git a/internal/requestconfig/requestconfig.go b/internal/requestconfig/requestconfig.go index ebeff43..115b152 100644 --- a/internal/requestconfig/requestconfig.go +++ b/internal/requestconfig/requestconfig.go @@ -18,10 +18,10 @@ import ( "strings" "time" - "github.com/beeper/desktop-api-go/internal" - "github.com/beeper/desktop-api-go/internal/apierror" - "github.com/beeper/desktop-api-go/internal/apiform" - "github.com/beeper/desktop-api-go/internal/apiquery" + "github.com/beeper/desktop-api-go/v5/internal" + "github.com/beeper/desktop-api-go/v5/internal/apierror" + "github.com/beeper/desktop-api-go/v5/internal/apiform" + "github.com/beeper/desktop-api-go/v5/internal/apiquery" ) func getDefaultHeaders() map[string]string { diff --git a/internal/version.go b/internal/version.go index 67c4d40..c87941c 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.5.0" // x-release-please-version +const PackageVersion = "5.0.0" // x-release-please-version diff --git a/message.go b/message.go index ca4b896..c25d60c 100644 --- a/message.go +++ b/message.go @@ -11,14 +11,14 @@ import ( "slices" "time" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/apiquery" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/pagination" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" - "github.com/beeper/desktop-api-go/shared" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/apiquery" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/pagination" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" + "github.com/beeper/desktop-api-go/v5/shared" ) // Manage messages in chats diff --git a/message_test.go b/message_test.go index c21916f..f26bdb1 100644 --- a/message_test.go +++ b/message_test.go @@ -9,9 +9,9 @@ import ( "testing" "time" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestMessageGet(t *testing.T) { diff --git a/option/requestoption.go b/option/requestoption.go index 6093527..101a747 100644 --- a/option/requestoption.go +++ b/option/requestoption.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/beeper/desktop-api-go/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" "github.com/tidwall/sjson" ) diff --git a/packages/pagination/pagination.go b/packages/pagination/pagination.go index 49b2e64..ee67f4a 100644 --- a/packages/pagination/pagination.go +++ b/packages/pagination/pagination.go @@ -5,11 +5,11 @@ package pagination import ( "net/http" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/internal/requestconfig" - "github.com/beeper/desktop-api-go/option" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/internal/requestconfig" + "github.com/beeper/desktop-api-go/v5/option" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" ) // aliased to make [param.APIUnion] private when embedding diff --git a/packages/param/encoder.go b/packages/param/encoder.go index 26c2ae3..a944e57 100644 --- a/packages/param/encoder.go +++ b/packages/param/encoder.go @@ -7,7 +7,7 @@ import ( "strings" "time" - shimjson "github.com/beeper/desktop-api-go/internal/encoding/json" + shimjson "github.com/beeper/desktop-api-go/v5/internal/encoding/json" "github.com/tidwall/sjson" ) diff --git a/packages/param/encoder_test.go b/packages/param/encoder_test.go index 230c916..63769fb 100644 --- a/packages/param/encoder_test.go +++ b/packages/param/encoder_test.go @@ -5,7 +5,7 @@ import ( "testing" "time" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" ) type Struct struct { diff --git a/packages/param/null.go b/packages/param/null.go index 1bf7c37..d740ca1 100644 --- a/packages/param/null.go +++ b/packages/param/null.go @@ -1,6 +1,6 @@ package param -import "github.com/beeper/desktop-api-go/internal/encoding/json/sentinel" +import "github.com/beeper/desktop-api-go/v5/internal/encoding/json/sentinel" // NullMap returns a non-nil map with a length of 0. // When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null. diff --git a/packages/param/null_test.go b/packages/param/null_test.go index 9c07938..7775700 100644 --- a/packages/param/null_test.go +++ b/packages/param/null_test.go @@ -2,7 +2,7 @@ package param_test import ( "encoding/json" - "github.com/beeper/desktop-api-go/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/param" "testing" ) diff --git a/packages/param/option.go b/packages/param/option.go index cdbe395..d3291b9 100644 --- a/packages/param/option.go +++ b/packages/param/option.go @@ -3,7 +3,7 @@ package param import ( "encoding/json" "fmt" - shimjson "github.com/beeper/desktop-api-go/internal/encoding/json" + shimjson "github.com/beeper/desktop-api-go/v5/internal/encoding/json" "time" ) diff --git a/packages/param/param.go b/packages/param/param.go index e68a5ef..1cdd474 100644 --- a/packages/param/param.go +++ b/packages/param/param.go @@ -2,7 +2,7 @@ package param import ( "encoding/json" - "github.com/beeper/desktop-api-go/internal/encoding/json/sentinel" + "github.com/beeper/desktop-api-go/v5/internal/encoding/json/sentinel" "reflect" ) diff --git a/packages/respjson/decoder_test.go b/packages/respjson/decoder_test.go index 65d6604..e08e97f 100644 --- a/packages/respjson/decoder_test.go +++ b/packages/respjson/decoder_test.go @@ -3,8 +3,8 @@ package respjson_test import ( "encoding/json" "fmt" - "github.com/beeper/desktop-api-go/internal/apijson" - rj "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + rj "github.com/beeper/desktop-api-go/v5/packages/respjson" "reflect" "testing" ) diff --git a/paginationauto_test.go b/paginationauto_test.go index 3539201..3b34ed7 100644 --- a/paginationauto_test.go +++ b/paginationauto_test.go @@ -7,9 +7,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestAutoPagination(t *testing.T) { diff --git a/paginationmanual_test.go b/paginationmanual_test.go index 821bf3d..a19a401 100644 --- a/paginationmanual_test.go +++ b/paginationmanual_test.go @@ -7,9 +7,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestManualPagination(t *testing.T) { diff --git a/shared/constant/constants.go b/shared/constant/constants.go index 87c5596..0f62c9f 100644 --- a/shared/constant/constants.go +++ b/shared/constant/constants.go @@ -3,7 +3,7 @@ package constant import ( - shimjson "github.com/beeper/desktop-api-go/internal/encoding/json" + shimjson "github.com/beeper/desktop-api-go/v5/internal/encoding/json" ) type Constant[T any] interface { diff --git a/shared/shared.go b/shared/shared.go index 4cb55e8..d658c39 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -6,9 +6,9 @@ import ( "encoding/json" "time" - "github.com/beeper/desktop-api-go/internal/apijson" - "github.com/beeper/desktop-api-go/packages/param" - "github.com/beeper/desktop-api-go/packages/respjson" + "github.com/beeper/desktop-api-go/v5/internal/apijson" + "github.com/beeper/desktop-api-go/v5/packages/param" + "github.com/beeper/desktop-api-go/v5/packages/respjson" ) // aliased to make [param.APIUnion] private when embedding diff --git a/usage_test.go b/usage_test.go index 1f81d45..a13b0e0 100644 --- a/usage_test.go +++ b/usage_test.go @@ -7,9 +7,9 @@ import ( "os" "testing" - "github.com/beeper/desktop-api-go" - "github.com/beeper/desktop-api-go/internal/testutil" - "github.com/beeper/desktop-api-go/option" + "github.com/beeper/desktop-api-go/v5" + "github.com/beeper/desktop-api-go/v5/internal/testutil" + "github.com/beeper/desktop-api-go/v5/option" ) func TestUsage(t *testing.T) {