basecamp-sdk 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6c1f02712a441b2b7772199b8029904b0c254be6c9b687a94a8d957f7d8a066
4
- data.tar.gz: f0750c8e75561aaa908630440f0b2c4276a54abb653418361cc4363883497d9a
3
+ metadata.gz: 6831e79b8c78f4ba54ee64287457227c5553fbd2837e71b09d022601084ac2af
4
+ data.tar.gz: 3dd5ac9aefd6f03e78fa9fa4c9833ac98d217a09a05ea129b8b2b6f3d7049939
5
5
  SHA512:
6
- metadata.gz: a7d6ecde9f18d17b9baadc7b827b6d396c0f6e63ab304d93800ee2e35f3803af9d668965b5d5885a408d926e780700f7fe0320d68375924a6647cce2462eee20
7
- data.tar.gz: a445a4123c2ebf596026035d347dbe99883b207a650003d2651f0dbc77eed9c0f4634703555e17af674aca87e015fdbe17f38a40e9370deff1b73a42cec6d042
6
+ metadata.gz: 007f77e92a111a5687b26740caedc75e71e640ddfc10a32057b39b1627ab7cedd2412faebc75647b26402551e7f235b4bf77743236e8ab9da5fc26e8b8286c0b
7
+ data.tar.gz: dff2430d3c5225c2ad563c0848d61d09a1da0620c58d93438947318f567b2f551548b5a899dfbadb55d14f84b495efb8982f786515c8fd934a885757b5f9db5e
@@ -17,11 +17,12 @@ module Basecamp
17
17
  # Creates an ApiError from an HTTP status code.
18
18
  # @param status [Integer] HTTP status code
19
19
  # @param message [String, nil] optional error message
20
+ # @param hint [String, nil] optional hint (SPEC section 6 step 3)
20
21
  # @return [ApiError]
21
- def self.from_status(status, message = nil)
22
+ def self.from_status(status, message = nil, hint: nil)
22
23
  message ||= "Request failed (HTTP #{status})"
23
24
  retryable = status >= 500 && status < 600
24
- new(message, http_status: status, retryable: retryable)
25
+ new(message, http_status: status, hint: hint, retryable: retryable)
25
26
  end
26
27
  end
27
28
  end
@@ -507,6 +507,11 @@ module Basecamp
507
507
  service(:bookmarks) { Services::BookmarksService.new(self) }
508
508
  end
509
509
 
510
+ # @return [Services::BubbleUpsService]
511
+ def bubble_ups
512
+ service(:bubble_ups) { Services::BubbleUpsService.new(self) }
513
+ end
514
+
510
515
  # @return [Services::FoldersService]
511
516
  def folders
512
517
  service(:folders) { Services::FoldersService.new(self) }
@@ -635,7 +640,16 @@ module Basecamp
635
640
  nil
636
641
  end
637
642
  unless uri.is_a?(URI::HTTP) && uri.host && !uri.host.empty?
638
- raise ApiError.new("redirect to undialable download URL: #{Security.truncate(url)}")
643
+ # SPEC §9: the signed URL is a credential — render its origin alone,
644
+ # projected from the parse, or the fixed token when no complete origin
645
+ # exists. Truncating the whole URL kept a short query intact.
646
+ origin = if uri&.scheme && uri&.host && !uri.host.empty?
647
+ port = uri.port && uri.port != uri.default_port ? ":#{uri.port}" : ""
648
+ "#{uri.scheme}://#{uri.host}#{port}"
649
+ else
650
+ "unparsable"
651
+ end
652
+ raise ApiError.new("redirect to undialable download URL: #{origin}")
639
653
  end
640
654
 
641
655
  http_client = Net::HTTP.new(uri.host, uri.port)
@@ -652,8 +666,12 @@ module Basecamp
652
666
  # Redirect Policy"). Stated here so a move to a following client
653
667
  # (Faraday, Net::HTTP.get_response's callers) has to argue with it.
654
668
  response = http_client.request(request)
655
- rescue StandardError => e
656
- raise NetworkError.new("Download failed: #{e.message}", cause: e)
669
+ rescue StandardError
670
+ # SPEC §9: the transport error renders the signed URL, so neither its
671
+ # message nor the exception itself survives. cause: nil at the raise
672
+ # site — MRI sets the built-in cause at raise time past the class's
673
+ # stored cause: keyword (same pattern as oauth/exchange.rb).
674
+ raise NetworkError.new("Download failed"), cause: nil
657
675
  end
658
676
 
659
677
  # The exact set hop 1 dispatches on, not Net::HTTPRedirection — that
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://basecamp.com/schemas/sdk-metadata.json",
3
3
  "version": "1.0.0",
4
- "generated": "2026-08-12T19:58:36Z",
4
+ "generated": "2026-09-08T23:57:38Z",
5
5
  "operations": {
6
6
  "GetAccount": {
7
7
  "retry": {
@@ -1576,6 +1576,17 @@
1576
1576
  "maxPageSize": 50
1577
1577
  }
1578
1578
  },
1579
+ "ListRecentProjects": {
1580
+ "retry": {
1581
+ "maxAttempts": 3,
1582
+ "baseDelayMs": 1000,
1583
+ "backoff": "exponential",
1584
+ "retryOn": [
1585
+ 429,
1586
+ 503
1587
+ ]
1588
+ }
1589
+ },
1579
1590
  "MarkAsRead": {
1580
1591
  "retry": {
1581
1592
  "maxAttempts": 2,
@@ -1735,6 +1746,34 @@
1735
1746
  "natural": true
1736
1747
  }
1737
1748
  },
1749
+ "EnableProjectClients": {
1750
+ "retry": {
1751
+ "maxAttempts": 3,
1752
+ "baseDelayMs": 1000,
1753
+ "backoff": "exponential",
1754
+ "retryOn": [
1755
+ 429,
1756
+ 503
1757
+ ]
1758
+ },
1759
+ "idempotent": {
1760
+ "natural": true
1761
+ }
1762
+ },
1763
+ "DisableProjectClients": {
1764
+ "retry": {
1765
+ "maxAttempts": 3,
1766
+ "baseDelayMs": 1000,
1767
+ "backoff": "exponential",
1768
+ "retryOn": [
1769
+ 429,
1770
+ 503
1771
+ ]
1772
+ },
1773
+ "idempotent": {
1774
+ "natural": true
1775
+ }
1776
+ },
1738
1777
  "ToggleGauge": {
1739
1778
  "retry": {
1740
1779
  "maxAttempts": 2,
@@ -1792,6 +1831,19 @@
1792
1831
  "maxPageSize": 50
1793
1832
  }
1794
1833
  },
1834
+ "UpdateProjectClientAccess": {
1835
+ "retry": {
1836
+ "maxAttempts": 3,
1837
+ "baseDelayMs": 1000,
1838
+ "backoff": "exponential",
1839
+ "retryOn": [
1840
+ 503
1841
+ ]
1842
+ },
1843
+ "idempotent": {
1844
+ "natural": true
1845
+ }
1846
+ },
1795
1847
  "UpdateProjectAccess": {
1796
1848
  "retry": {
1797
1849
  "maxAttempts": 3,
@@ -1806,6 +1858,20 @@
1806
1858
  "natural": true
1807
1859
  }
1808
1860
  },
1861
+ "RecordProjectVisit": {
1862
+ "retry": {
1863
+ "maxAttempts": 3,
1864
+ "baseDelayMs": 1000,
1865
+ "backoff": "exponential",
1866
+ "retryOn": [
1867
+ 429,
1868
+ 503
1869
+ ]
1870
+ },
1871
+ "idempotent": {
1872
+ "natural": true
1873
+ }
1874
+ },
1809
1875
  "UnarchiveProject": {
1810
1876
  "retry": {
1811
1877
  "maxAttempts": 3,
@@ -2144,6 +2210,34 @@
2144
2210
  ]
2145
2211
  }
2146
2212
  },
2213
+ "CreateBubbleUp": {
2214
+ "retry": {
2215
+ "maxAttempts": 3,
2216
+ "baseDelayMs": 1000,
2217
+ "backoff": "exponential",
2218
+ "retryOn": [
2219
+ 429,
2220
+ 503
2221
+ ]
2222
+ },
2223
+ "idempotent": {
2224
+ "natural": true
2225
+ }
2226
+ },
2227
+ "DeleteBubbleUp": {
2228
+ "retry": {
2229
+ "maxAttempts": 3,
2230
+ "baseDelayMs": 1000,
2231
+ "backoff": "exponential",
2232
+ "retryOn": [
2233
+ 429,
2234
+ 503
2235
+ ]
2236
+ },
2237
+ "idempotent": {
2238
+ "natural": true
2239
+ }
2240
+ },
2147
2241
  "SetClientVisibility": {
2148
2242
  "retry": {
2149
2243
  "maxAttempts": 3,
@@ -2228,6 +2322,34 @@
2228
2322
  ]
2229
2323
  }
2230
2324
  },
2325
+ "SpotlightRecording": {
2326
+ "retry": {
2327
+ "maxAttempts": 3,
2328
+ "baseDelayMs": 1000,
2329
+ "backoff": "exponential",
2330
+ "retryOn": [
2331
+ 429,
2332
+ 503
2333
+ ]
2334
+ },
2335
+ "idempotent": {
2336
+ "natural": true
2337
+ }
2338
+ },
2339
+ "UnspotlightRecording": {
2340
+ "retry": {
2341
+ "maxAttempts": 3,
2342
+ "baseDelayMs": 1000,
2343
+ "backoff": "exponential",
2344
+ "retryOn": [
2345
+ 429,
2346
+ 503
2347
+ ]
2348
+ },
2349
+ "idempotent": {
2350
+ "natural": true
2351
+ }
2352
+ },
2231
2353
  "UnarchiveRecording": {
2232
2354
  "retry": {
2233
2355
  "maxAttempts": 3,
@@ -2668,6 +2790,39 @@
2668
2790
  "natural": true
2669
2791
  }
2670
2792
  },
2793
+ "GetTemplateLibrary": {
2794
+ "retry": {
2795
+ "maxAttempts": 3,
2796
+ "baseDelayMs": 1000,
2797
+ "backoff": "exponential",
2798
+ "retryOn": [
2799
+ 429,
2800
+ 503
2801
+ ]
2802
+ }
2803
+ },
2804
+ "CreateTemplateLibraryCopy": {
2805
+ "retry": {
2806
+ "maxAttempts": 2,
2807
+ "baseDelayMs": 1000,
2808
+ "backoff": "exponential",
2809
+ "retryOn": [
2810
+ 429,
2811
+ 503
2812
+ ]
2813
+ }
2814
+ },
2815
+ "GetTemplateLibraryCopy": {
2816
+ "retry": {
2817
+ "maxAttempts": 3,
2818
+ "baseDelayMs": 1000,
2819
+ "backoff": "exponential",
2820
+ "retryOn": [
2821
+ 429,
2822
+ 503
2823
+ ]
2824
+ }
2825
+ },
2671
2826
  "ListTemplates": {
2672
2827
  "retry": {
2673
2828
  "maxAttempts": 3,
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for BubbleUps operations
6
+ #
7
+ # @generated from OpenAPI spec
8
+ class BubbleUpsService < BaseService
9
+
10
+ # Bubble up a recording for the current user, resurfacing it in the current
11
+ # @param recording_id [Integer] recording id ID
12
+ # @param at [String, nil] Timing for the bubble-up. `"now"` bubbles up immediately; a scheduling
13
+ # keyword (`"today"`, `"tomorrow"`, `"weekend"`, `"next_week"`) or an ISO8601
14
+ # date (e.g. `"2026-09-10"`) schedules it to resurface later. bc3 requires a
15
+ # value — omitting `at` errors server-side (`Date.iso8601(nil)`) — so send
16
+ # `"now"` for the immediate case.
17
+ # @return [void]
18
+ def create_bubble_up(recording_id:, at: nil)
19
+ with_operation(service: "bubbleups", operation: "create_bubble_up", is_mutation: true, resource_id: recording_id) do
20
+ http_post("/recordings/#{recording_id}/bubble_up.json", body: compact_params(at: at))
21
+ nil
22
+ end
23
+ end
24
+
25
+ # Remove the current user's bubble-up from a recording (returns 204 No Content).
26
+ # @param recording_id [Integer] recording id ID
27
+ # @return [void]
28
+ def delete_bubble_up(recording_id:)
29
+ with_operation(service: "bubbleups", operation: "delete_bubble_up", is_mutation: true, resource_id: recording_id) do
30
+ http_delete("/recordings/#{recording_id}/bubble_up.json")
31
+ nil
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -107,6 +107,24 @@ module Basecamp
107
107
  end
108
108
  end
109
109
 
110
+ # Enable clients on a project so client users can be added to it
111
+ # @param project_id [Integer] project id ID
112
+ # @return [Hash] response data
113
+ def enable_project_clients(project_id:)
114
+ with_operation(service: "people", operation: "enable_project_clients", is_mutation: true, project_id: project_id) do
115
+ http_post("/projects/#{project_id}/client_enablement.json").json
116
+ end
117
+ end
118
+
119
+ # Disable clients on a project
120
+ # @param project_id [Integer] project id ID
121
+ # @return [Hash] response data
122
+ def disable_project_clients(project_id:)
123
+ with_operation(service: "people", operation: "disable_project_clients", is_mutation: true, project_id: project_id) do
124
+ http_delete("/projects/#{project_id}/client_enablement.json").json
125
+ end
126
+ end
127
+
110
128
  # List all active people on a project
111
129
  # @param project_id [Integer] project id ID
112
130
  # @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
@@ -119,6 +137,18 @@ module Basecamp
119
137
  end
120
138
  end
121
139
 
140
+ # Update project client access (grant/revoke/create client users)
141
+ # @param project_id [Integer] project id ID
142
+ # @param grant [Array, nil] Existing client people IDs to add to the project.
143
+ # @param revoke [Array, nil] Client people IDs to remove from the project.
144
+ # @param create [Array, nil] New clients to invite by email.
145
+ # @return [Hash] response data
146
+ def update_project_client_access(project_id:, grant: nil, revoke: nil, create: nil)
147
+ with_operation(service: "people", operation: "update_project_client_access", is_mutation: true, project_id: project_id) do
148
+ http_put("/projects/#{project_id}/people/client_users.json", body: compact_params(grant: grant, revoke: revoke, create: create)).json
149
+ end
150
+ end
151
+
122
152
  # Update project access (grant/revoke/create people)
123
153
  # @param project_id [Integer] project id ID
124
154
  # @param grant [Array, nil] grant
@@ -7,6 +7,14 @@ module Basecamp
7
7
  # @generated from OpenAPI spec
8
8
  class ProjectsService < BaseService
9
9
 
10
+ # List the projects the current user has most recently visited, most recent visit first.
11
+ # @return [Array<Hash>] response data
12
+ def list_recent_projects()
13
+ with_operation(service: "projects", operation: "list_recent_projects", is_mutation: false) do
14
+ http_get("/my/recent_projects.json", operation: "ListRecentProjects").json
15
+ end
16
+ end
17
+
10
18
  # List projects (active by default; optionally archived/trashed)
11
19
  # @param status [String, nil] active|archived|trashed
12
20
  # @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
@@ -61,6 +69,16 @@ module Basecamp
61
69
  end
62
70
  end
63
71
 
72
+ # Record that the current user visited a project, moving it to the front of ListRecentProjects (returns 204 No Content).
73
+ # @param project_id [Integer] project id ID
74
+ # @return [void]
75
+ def record_project_visit(project_id:)
76
+ with_operation(service: "projects", operation: "record_project_visit", is_mutation: true, project_id: project_id) do
77
+ http_post("/projects/#{project_id}/recent_visit.json")
78
+ nil
79
+ end
80
+ end
81
+
64
82
  # Restore a project to active status from trash as well as from the archive (returns 204 No Content).
65
83
  # @param project_id [Integer] project id ID
66
84
  # @return [void]
@@ -23,6 +23,25 @@ module Basecamp
23
23
  end
24
24
  end
25
25
 
26
+ # Put a recording's card in the spotlight area on its project or template home page.
27
+ # @param recording_id [Integer] recording id ID
28
+ # @return [Hash] response data
29
+ def spotlight(recording_id:)
30
+ with_operation(service: "recordings", operation: "spotlight", is_mutation: true, resource_id: recording_id) do
31
+ http_post("/recordings/#{recording_id}/spotlight.json").json
32
+ end
33
+ end
34
+
35
+ # Remove a recording from the spotlight area.
36
+ # @param recording_id [Integer] recording id ID
37
+ # @return [void]
38
+ def unspotlight(recording_id:)
39
+ with_operation(service: "recordings", operation: "unspotlight", is_mutation: true, resource_id: recording_id) do
40
+ http_delete("/recordings/#{recording_id}/spotlight.json")
41
+ nil
42
+ end
43
+ end
44
+
26
45
  # Unarchive a recording (restore to active status)
27
46
  # @param recording_id [Integer] recording id ID
28
47
  # @return [void]
@@ -7,6 +7,34 @@ module Basecamp
7
7
  # @generated from OpenAPI spec
8
8
  class TemplatesService < BaseService
9
9
 
10
+ # Get the account's to-do list template library
11
+ # @return [Hash] response data
12
+ def get_library()
13
+ with_operation(service: "templates", operation: "get_library", is_mutation: false) do
14
+ http_get("/template_library.json", operation: "GetTemplateLibrary").json
15
+ end
16
+ end
17
+
18
+ # Start copying a to-do list template into a project
19
+ # @param template_recording_id [Integer] template recording id
20
+ # @param destination_parent_id [Integer] destination parent id
21
+ # @param adding_people_confirmed [Boolean, nil] Confirm granting destination-project access to people referenced by the template.
22
+ # @return [Hash] response data
23
+ def create_library_copy(template_recording_id:, destination_parent_id:, adding_people_confirmed: nil)
24
+ with_operation(service: "templates", operation: "create_library_copy", is_mutation: true) do
25
+ http_post("/template_library/copies.json", body: compact_params(template_recording_id: template_recording_id, destination_parent_id: destination_parent_id, adding_people_confirmed: adding_people_confirmed)).json
26
+ end
27
+ end
28
+
29
+ # Get the current status of a to-do list template copy
30
+ # @param copy_id [Integer] copy id ID
31
+ # @return [Hash] response data
32
+ def get_library_copy(copy_id:)
33
+ with_operation(service: "templates", operation: "get_library_copy", is_mutation: false, resource_id: copy_id) do
34
+ http_get("/template_library/copies/#{copy_id}", operation: "GetTemplateLibraryCopy").json
35
+ end
36
+ end
37
+
10
38
  # List all templates visible to the current user
11
39
  # @param status [String, nil] active|archived|trashed
12
40
  # @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.