basecamp-sdk 0.2.1

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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +14 -0
  3. data/.yardopts +6 -0
  4. data/README.md +293 -0
  5. data/Rakefile +26 -0
  6. data/basecamp-sdk.gemspec +46 -0
  7. data/lib/basecamp/auth_strategy.rb +38 -0
  8. data/lib/basecamp/chain_hooks.rb +45 -0
  9. data/lib/basecamp/client.rb +428 -0
  10. data/lib/basecamp/config.rb +143 -0
  11. data/lib/basecamp/errors.rb +289 -0
  12. data/lib/basecamp/generated/metadata.json +2281 -0
  13. data/lib/basecamp/generated/services/attachments_service.rb +24 -0
  14. data/lib/basecamp/generated/services/boosts_service.rb +70 -0
  15. data/lib/basecamp/generated/services/campfires_service.rb +122 -0
  16. data/lib/basecamp/generated/services/card_columns_service.rb +103 -0
  17. data/lib/basecamp/generated/services/card_steps_service.rb +57 -0
  18. data/lib/basecamp/generated/services/card_tables_service.rb +20 -0
  19. data/lib/basecamp/generated/services/cards_service.rb +66 -0
  20. data/lib/basecamp/generated/services/checkins_service.rb +157 -0
  21. data/lib/basecamp/generated/services/client_approvals_service.rb +28 -0
  22. data/lib/basecamp/generated/services/client_correspondences_service.rb +28 -0
  23. data/lib/basecamp/generated/services/client_replies_service.rb +30 -0
  24. data/lib/basecamp/generated/services/client_visibility_service.rb +21 -0
  25. data/lib/basecamp/generated/services/comments_service.rb +49 -0
  26. data/lib/basecamp/generated/services/documents_service.rb +52 -0
  27. data/lib/basecamp/generated/services/events_service.rb +20 -0
  28. data/lib/basecamp/generated/services/forwards_service.rb +67 -0
  29. data/lib/basecamp/generated/services/lineup_service.rb +44 -0
  30. data/lib/basecamp/generated/services/message_boards_service.rb +20 -0
  31. data/lib/basecamp/generated/services/message_types_service.rb +59 -0
  32. data/lib/basecamp/generated/services/messages_service.rb +75 -0
  33. data/lib/basecamp/generated/services/people_service.rb +73 -0
  34. data/lib/basecamp/generated/services/projects_service.rb +63 -0
  35. data/lib/basecamp/generated/services/recordings_service.rb +64 -0
  36. data/lib/basecamp/generated/services/reports_service.rb +56 -0
  37. data/lib/basecamp/generated/services/schedules_service.rb +92 -0
  38. data/lib/basecamp/generated/services/search_service.rb +31 -0
  39. data/lib/basecamp/generated/services/subscriptions_service.rb +50 -0
  40. data/lib/basecamp/generated/services/templates_service.rb +82 -0
  41. data/lib/basecamp/generated/services/timeline_service.rb +20 -0
  42. data/lib/basecamp/generated/services/timesheets_service.rb +81 -0
  43. data/lib/basecamp/generated/services/todolist_groups_service.rb +41 -0
  44. data/lib/basecamp/generated/services/todolists_service.rb +53 -0
  45. data/lib/basecamp/generated/services/todos_service.rb +106 -0
  46. data/lib/basecamp/generated/services/todosets_service.rb +20 -0
  47. data/lib/basecamp/generated/services/tools_service.rb +80 -0
  48. data/lib/basecamp/generated/services/uploads_service.rb +61 -0
  49. data/lib/basecamp/generated/services/vaults_service.rb +49 -0
  50. data/lib/basecamp/generated/services/webhooks_service.rb +63 -0
  51. data/lib/basecamp/generated/types.rb +3196 -0
  52. data/lib/basecamp/hooks.rb +70 -0
  53. data/lib/basecamp/http.rb +440 -0
  54. data/lib/basecamp/logger_hooks.rb +46 -0
  55. data/lib/basecamp/noop_hooks.rb +9 -0
  56. data/lib/basecamp/oauth/discovery.rb +123 -0
  57. data/lib/basecamp/oauth/errors.rb +35 -0
  58. data/lib/basecamp/oauth/exchange.rb +291 -0
  59. data/lib/basecamp/oauth/pkce.rb +68 -0
  60. data/lib/basecamp/oauth/types.rb +133 -0
  61. data/lib/basecamp/oauth.rb +56 -0
  62. data/lib/basecamp/oauth_token_provider.rb +108 -0
  63. data/lib/basecamp/operation_info.rb +17 -0
  64. data/lib/basecamp/request_info.rb +10 -0
  65. data/lib/basecamp/request_result.rb +14 -0
  66. data/lib/basecamp/security.rb +112 -0
  67. data/lib/basecamp/services/attachments_service.rb +33 -0
  68. data/lib/basecamp/services/authorization_service.rb +47 -0
  69. data/lib/basecamp/services/base_service.rb +146 -0
  70. data/lib/basecamp/services/campfires_service.rb +141 -0
  71. data/lib/basecamp/services/card_columns_service.rb +106 -0
  72. data/lib/basecamp/services/card_steps_service.rb +86 -0
  73. data/lib/basecamp/services/card_tables_service.rb +23 -0
  74. data/lib/basecamp/services/cards_service.rb +93 -0
  75. data/lib/basecamp/services/checkins_service.rb +127 -0
  76. data/lib/basecamp/services/client_approvals_service.rb +33 -0
  77. data/lib/basecamp/services/client_correspondences_service.rb +33 -0
  78. data/lib/basecamp/services/client_replies_service.rb +35 -0
  79. data/lib/basecamp/services/comments_service.rb +63 -0
  80. data/lib/basecamp/services/documents_service.rb +74 -0
  81. data/lib/basecamp/services/events_service.rb +27 -0
  82. data/lib/basecamp/services/forwards_service.rb +80 -0
  83. data/lib/basecamp/services/lineup_service.rb +67 -0
  84. data/lib/basecamp/services/message_boards_service.rb +24 -0
  85. data/lib/basecamp/services/message_types_service.rb +79 -0
  86. data/lib/basecamp/services/messages_service.rb +133 -0
  87. data/lib/basecamp/services/people_service.rb +73 -0
  88. data/lib/basecamp/services/projects_service.rb +67 -0
  89. data/lib/basecamp/services/recordings_service.rb +127 -0
  90. data/lib/basecamp/services/reports_service.rb +80 -0
  91. data/lib/basecamp/services/schedules_service.rb +156 -0
  92. data/lib/basecamp/services/search_service.rb +36 -0
  93. data/lib/basecamp/services/subscriptions_service.rb +67 -0
  94. data/lib/basecamp/services/templates_service.rb +96 -0
  95. data/lib/basecamp/services/timeline_service.rb +62 -0
  96. data/lib/basecamp/services/timesheet_service.rb +68 -0
  97. data/lib/basecamp/services/todolist_groups_service.rb +100 -0
  98. data/lib/basecamp/services/todolists_service.rb +104 -0
  99. data/lib/basecamp/services/todos_service.rb +156 -0
  100. data/lib/basecamp/services/todosets_service.rb +23 -0
  101. data/lib/basecamp/services/tools_service.rb +89 -0
  102. data/lib/basecamp/services/uploads_service.rb +84 -0
  103. data/lib/basecamp/services/vaults_service.rb +84 -0
  104. data/lib/basecamp/services/webhooks_service.rb +88 -0
  105. data/lib/basecamp/static_token_provider.rb +24 -0
  106. data/lib/basecamp/token_provider.rb +42 -0
  107. data/lib/basecamp/version.rb +6 -0
  108. data/lib/basecamp/webhooks/event.rb +52 -0
  109. data/lib/basecamp/webhooks/rack_middleware.rb +49 -0
  110. data/lib/basecamp/webhooks/receiver.rb +161 -0
  111. data/lib/basecamp/webhooks/verify.rb +36 -0
  112. data/lib/basecamp.rb +107 -0
  113. data/scripts/generate-metadata.rb +106 -0
  114. data/scripts/generate-services.rb +778 -0
  115. data/scripts/generate-types.rb +191 -0
  116. metadata +316 -0
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for todo operations.
6
+ #
7
+ # @example List todos in a todolist
8
+ # account.todos.list(project_id: 123, todolist_id: 456).each do |todo|
9
+ # puts "#{todo["content"]} (#{todo["completed"] ? "done" : "pending"})"
10
+ # end
11
+ #
12
+ # @example Create a todo
13
+ # todo = account.todos.create(
14
+ # project_id: 123,
15
+ # todolist_id: 456,
16
+ # content: "Write documentation",
17
+ # assignee_ids: [789]
18
+ # )
19
+ #
20
+ # @example Complete a todo
21
+ # account.todos.complete(project_id: 123, todo_id: 456)
22
+ class TodosService < BaseService
23
+ # Lists todos in a todolist.
24
+ #
25
+ # @param project_id [Integer, String] project (bucket) ID
26
+ # @param todolist_id [Integer, String] todolist ID
27
+ # @param status [String, nil] filter by status ("archived", "trashed")
28
+ # @param completed [Boolean, nil] filter by completion status
29
+ # @return [Enumerator<Hash>] todos
30
+ def list(project_id:, todolist_id:, status: nil, completed: nil)
31
+ params = compact_params(status: status, completed: completed)
32
+ paginate(bucket_path(project_id, "/todolists/#{todolist_id}/todos.json"), params: params)
33
+ end
34
+
35
+ # Gets a specific todo.
36
+ #
37
+ # @param project_id [Integer, String] project (bucket) ID
38
+ # @param todo_id [Integer, String] todo ID
39
+ # @return [Hash] todo data
40
+ def get(project_id:, todo_id:)
41
+ http_get(bucket_path(project_id, "/todos/#{todo_id}.json")).json
42
+ end
43
+
44
+ # Creates a new todo.
45
+ #
46
+ # @param project_id [Integer, String] project (bucket) ID
47
+ # @param todolist_id [Integer, String] todolist ID
48
+ # @param content [String] todo content (can include HTML)
49
+ # @param description [String, nil] extended description (HTML)
50
+ # @param assignee_ids [Array<Integer>, nil] user IDs to assign
51
+ # @param completion_subscriber_ids [Array<Integer>, nil] user IDs to notify on completion
52
+ # @param notify [Boolean] whether to notify assignees
53
+ # @param due_on [String, nil] due date (YYYY-MM-DD)
54
+ # @param starts_on [String, nil] start date (YYYY-MM-DD)
55
+ # @return [Hash] created todo
56
+ def create(
57
+ project_id:,
58
+ todolist_id:,
59
+ content:,
60
+ description: nil,
61
+ assignee_ids: nil,
62
+ completion_subscriber_ids: nil,
63
+ notify: true,
64
+ due_on: nil,
65
+ starts_on: nil
66
+ )
67
+ body = compact_params(
68
+ content: content,
69
+ description: description,
70
+ assignee_ids: assignee_ids,
71
+ completion_subscriber_ids: completion_subscriber_ids,
72
+ notify: notify,
73
+ due_on: due_on,
74
+ starts_on: starts_on
75
+ )
76
+ http_post(bucket_path(project_id, "/todolists/#{todolist_id}/todos.json"), body: body).json
77
+ end
78
+
79
+ # Updates a todo.
80
+ #
81
+ # @param project_id [Integer, String] project (bucket) ID
82
+ # @param todo_id [Integer, String] todo ID
83
+ # @param content [String, nil] new content
84
+ # @param description [String, nil] new description
85
+ # @param assignee_ids [Array<Integer>, nil] new assignee IDs
86
+ # @param completion_subscriber_ids [Array<Integer>, nil] new completion subscriber IDs
87
+ # @param notify [Boolean, nil] whether to notify
88
+ # @param due_on [String, nil] new due date
89
+ # @param starts_on [String, nil] new start date
90
+ # @return [Hash] updated todo
91
+ def update(
92
+ project_id:,
93
+ todo_id:,
94
+ content: nil,
95
+ description: nil,
96
+ assignee_ids: nil,
97
+ completion_subscriber_ids: nil,
98
+ notify: nil,
99
+ due_on: nil,
100
+ starts_on: nil
101
+ )
102
+ body = compact_params(
103
+ content: content,
104
+ description: description,
105
+ assignee_ids: assignee_ids,
106
+ completion_subscriber_ids: completion_subscriber_ids,
107
+ notify: notify,
108
+ due_on: due_on,
109
+ starts_on: starts_on
110
+ )
111
+ http_put(bucket_path(project_id, "/todos/#{todo_id}.json"), body: body).json
112
+ end
113
+
114
+ # Completes a todo.
115
+ #
116
+ # @param project_id [Integer, String] project (bucket) ID
117
+ # @param todo_id [Integer, String] todo ID
118
+ # @return [void]
119
+ def complete(project_id:, todo_id:)
120
+ http_post(bucket_path(project_id, "/todos/#{todo_id}/completion.json"))
121
+ nil
122
+ end
123
+
124
+ # Uncompletes a todo.
125
+ #
126
+ # @param project_id [Integer, String] project (bucket) ID
127
+ # @param todo_id [Integer, String] todo ID
128
+ # @return [void]
129
+ def uncomplete(project_id:, todo_id:)
130
+ http_delete(bucket_path(project_id, "/todos/#{todo_id}/completion.json"))
131
+ nil
132
+ end
133
+
134
+ # Repositions a todo within its todolist.
135
+ #
136
+ # @param project_id [Integer, String] project (bucket) ID
137
+ # @param todo_id [Integer, String] todo ID
138
+ # @param position [Integer] new position (1-based)
139
+ # @return [void]
140
+ def reposition(project_id:, todo_id:, position:)
141
+ http_put(bucket_path(project_id, "/todos/#{todo_id}/position.json"), body: { position: position })
142
+ nil
143
+ end
144
+
145
+ # Trashes a todo.
146
+ #
147
+ # @param project_id [Integer, String] project (bucket) ID
148
+ # @param todo_id [Integer, String] todo ID
149
+ # @return [void]
150
+ def trash(project_id:, todo_id:)
151
+ http_delete(bucket_path(project_id, "/todos/#{todo_id}.json"))
152
+ nil
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for todoset operations.
6
+ #
7
+ # Each project has one todoset which is the container for all todolists.
8
+ #
9
+ # @example Get a todoset
10
+ # todoset = account.todosets.get(project_id: 123, todoset_id: 456)
11
+ # puts "#{todoset["name"]} - #{todoset["todolists_count"]} lists"
12
+ class TodosetsService < BaseService
13
+ # Gets a specific todoset.
14
+ #
15
+ # @param project_id [Integer, String] project (bucket) ID
16
+ # @param todoset_id [Integer, String] todoset ID
17
+ # @return [Hash] todoset data
18
+ def get(project_id:, todoset_id:)
19
+ http_get(bucket_path(project_id, "/todosets/#{todoset_id}")).json
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for dock tool operations.
6
+ #
7
+ # Tools are dock items in a Basecamp project (e.g., Message Board,
8
+ # Todos, Schedule, etc.). This service allows you to manage these tools.
9
+ #
10
+ # @example Get a tool
11
+ # tool = account.tools.get(project_id: 123, tool_id: 456)
12
+ # puts "#{tool["name"]} - #{tool["enabled"] ? "enabled" : "disabled"}"
13
+ #
14
+ # @example Enable and reposition a tool
15
+ # account.tools.enable(project_id: 123, tool_id: 456)
16
+ # account.tools.reposition(project_id: 123, tool_id: 456, position: 1)
17
+ class ToolsService < BaseService
18
+ # Gets a tool by ID.
19
+ #
20
+ # @param project_id [Integer, String] project (bucket) ID
21
+ # @param tool_id [Integer, String] tool ID
22
+ # @return [Hash] tool data
23
+ def get(project_id:, tool_id:)
24
+ http_get(bucket_path(project_id, "/dock/tools/#{tool_id}")).json
25
+ end
26
+
27
+ # Clones an existing tool to create a new one.
28
+ #
29
+ # @param project_id [Integer, String] project (bucket) ID
30
+ # @param source_tool_id [Integer, String] ID of the tool to clone
31
+ # @return [Hash] newly created tool
32
+ def clone(project_id:, source_tool_id:)
33
+ http_post(bucket_path(project_id, "/dock/tools/#{source_tool_id}/clone.json")).json
34
+ end
35
+
36
+ # Updates (renames) an existing tool.
37
+ #
38
+ # @param project_id [Integer, String] project (bucket) ID
39
+ # @param tool_id [Integer, String] tool ID
40
+ # @param title [String] new title for the tool
41
+ # @return [Hash] updated tool
42
+ def update(project_id:, tool_id:, title:)
43
+ http_put(bucket_path(project_id, "/dock/tools/#{tool_id}"), body: { title: title }).json
44
+ end
45
+
46
+ # Deletes a tool (moves it to trash).
47
+ #
48
+ # @param project_id [Integer, String] project (bucket) ID
49
+ # @param tool_id [Integer, String] tool ID
50
+ # @return [void]
51
+ def delete(project_id:, tool_id:)
52
+ http_delete(bucket_path(project_id, "/dock/tools/#{tool_id}"))
53
+ nil
54
+ end
55
+
56
+ # Enables a tool (shows it on the project dock).
57
+ #
58
+ # @param project_id [Integer, String] project (bucket) ID
59
+ # @param tool_id [Integer, String] tool ID
60
+ # @return [void]
61
+ def enable(project_id:, tool_id:)
62
+ http_post(bucket_path(project_id, "/dock/tools/#{tool_id}/position.json"))
63
+ nil
64
+ end
65
+
66
+ # Disables a tool (hides it from the project dock).
67
+ #
68
+ # @param project_id [Integer, String] project (bucket) ID
69
+ # @param tool_id [Integer, String] tool ID
70
+ # @return [void]
71
+ def disable(project_id:, tool_id:)
72
+ http_delete(bucket_path(project_id, "/dock/tools/#{tool_id}/position.json"))
73
+ nil
74
+ end
75
+
76
+ # Changes the position of a tool on the project dock.
77
+ #
78
+ # @param project_id [Integer, String] project (bucket) ID
79
+ # @param tool_id [Integer, String] tool ID
80
+ # @param position [Integer] new position (1-based, 1 = first on dock)
81
+ # @return [void]
82
+ def reposition(project_id:, tool_id:, position:)
83
+ http_put(bucket_path(project_id, "/dock/tools/#{tool_id}/position.json"),
84
+ body: { position: position })
85
+ nil
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for upload operations.
6
+ #
7
+ # Uploads are files stored within vaults. They are created from
8
+ # attachments (via attachable_sgid) and can have descriptions
9
+ # and version history.
10
+ #
11
+ # @example List uploads in a vault
12
+ # account.uploads.list(project_id: 123, vault_id: 456).each do |upload|
13
+ # puts "#{upload["filename"]} - #{upload["byte_size"]} bytes"
14
+ # end
15
+ #
16
+ # @example Create an upload
17
+ # upload = account.uploads.create(
18
+ # project_id: 123,
19
+ # vault_id: 456,
20
+ # attachable_sgid: attachment_sgid,
21
+ # description: "Q4 financial report"
22
+ # )
23
+ class UploadsService < BaseService
24
+ # Lists all uploads in a vault.
25
+ #
26
+ # @param project_id [Integer, String] project (bucket) ID
27
+ # @param vault_id [Integer, String] vault ID
28
+ # @return [Enumerator<Hash>] uploads
29
+ def list(project_id:, vault_id:)
30
+ paginate(bucket_path(project_id, "/vaults/#{vault_id}/uploads.json"))
31
+ end
32
+
33
+ # Gets a specific upload.
34
+ #
35
+ # @param project_id [Integer, String] project (bucket) ID
36
+ # @param upload_id [Integer, String] upload ID
37
+ # @return [Hash] upload data
38
+ def get(project_id:, upload_id:)
39
+ http_get(bucket_path(project_id, "/uploads/#{upload_id}")).json
40
+ end
41
+
42
+ # Creates a new upload in a vault.
43
+ #
44
+ # @param project_id [Integer, String] project (bucket) ID
45
+ # @param vault_id [Integer, String] vault ID
46
+ # @param attachable_sgid [String] signed global ID from attachment upload
47
+ # @param description [String, nil] upload description in HTML
48
+ # @param base_name [String, nil] filename without extension
49
+ # @return [Hash] created upload
50
+ def create(project_id:, vault_id:, attachable_sgid:, description: nil, base_name: nil)
51
+ body = compact_params(
52
+ attachable_sgid: attachable_sgid,
53
+ description: description,
54
+ base_name: base_name
55
+ )
56
+ http_post(bucket_path(project_id, "/vaults/#{vault_id}/uploads.json"), body: body).json
57
+ end
58
+
59
+ # Updates an existing upload.
60
+ #
61
+ # @param project_id [Integer, String] project (bucket) ID
62
+ # @param upload_id [Integer, String] upload ID
63
+ # @param description [String, nil] new description
64
+ # @param base_name [String, nil] new filename without extension
65
+ # @return [Hash] updated upload
66
+ def update(project_id:, upload_id:, description: nil, base_name: nil)
67
+ body = compact_params(
68
+ description: description,
69
+ base_name: base_name
70
+ )
71
+ http_put(bucket_path(project_id, "/uploads/#{upload_id}"), body: body).json
72
+ end
73
+
74
+ # Lists all versions of an upload.
75
+ #
76
+ # @param project_id [Integer, String] project (bucket) ID
77
+ # @param upload_id [Integer, String] upload ID
78
+ # @return [Enumerator<Hash>] upload versions
79
+ def list_versions(project_id:, upload_id:)
80
+ paginate(bucket_path(project_id, "/uploads/#{upload_id}/versions.json"))
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for vault (folder) operations.
6
+ #
7
+ # Vaults are folders in the Files & Documents tool. They can contain
8
+ # documents, uploads (files), and nested vaults (subfolders).
9
+ #
10
+ # @example Get a vault
11
+ # vault = account.vaults.get(project_id: 123, vault_id: 456)
12
+ #
13
+ # @example List subfolders
14
+ # account.vaults.list(project_id: 123, vault_id: 456).each do |folder|
15
+ # puts folder["title"]
16
+ # end
17
+ #
18
+ # @example Create a subfolder
19
+ # vault = account.vaults.create(
20
+ # project_id: 123,
21
+ # vault_id: 456,
22
+ # title: "2024 Reports"
23
+ # )
24
+ class VaultsService < BaseService
25
+ # Gets a specific vault.
26
+ #
27
+ # @param project_id [Integer, String] project (bucket) ID
28
+ # @param vault_id [Integer, String] vault ID
29
+ # @return [Hash] vault data
30
+ def get(project_id:, vault_id:)
31
+ http_get(bucket_path(project_id, "/vaults/#{vault_id}")).json
32
+ end
33
+
34
+ # Lists all child vaults (subfolders) in a vault.
35
+ #
36
+ # @param project_id [Integer, String] project (bucket) ID
37
+ # @param vault_id [Integer, String] parent vault ID
38
+ # @return [Enumerator<Hash>] child vaults
39
+ def list(project_id:, vault_id:)
40
+ paginate(bucket_path(project_id, "/vaults/#{vault_id}/vaults.json"))
41
+ end
42
+
43
+ # Creates a new child vault (subfolder).
44
+ #
45
+ # @param project_id [Integer, String] project (bucket) ID
46
+ # @param vault_id [Integer, String] parent vault ID
47
+ # @param title [String] vault name
48
+ # @return [Hash] created vault
49
+ def create(project_id:, vault_id:, title:)
50
+ body = { title: title }
51
+ http_post(bucket_path(project_id, "/vaults/#{vault_id}/vaults.json"), body: body).json
52
+ end
53
+
54
+ # Updates an existing vault.
55
+ #
56
+ # @param project_id [Integer, String] project (bucket) ID
57
+ # @param vault_id [Integer, String] vault ID
58
+ # @param title [String, nil] new title
59
+ # @return [Hash] updated vault
60
+ def update(project_id:, vault_id:, title: nil)
61
+ body = compact_params(title: title)
62
+ http_put(bucket_path(project_id, "/vaults/#{vault_id}"), body: body).json
63
+ end
64
+
65
+ # Lists all documents in a vault.
66
+ #
67
+ # @param project_id [Integer, String] project (bucket) ID
68
+ # @param vault_id [Integer, String] vault ID
69
+ # @return [Enumerator<Hash>] documents
70
+ def list_documents(project_id:, vault_id:)
71
+ paginate(bucket_path(project_id, "/vaults/#{vault_id}/documents.json"))
72
+ end
73
+
74
+ # Lists all uploads (files) in a vault.
75
+ #
76
+ # @param project_id [Integer, String] project (bucket) ID
77
+ # @param vault_id [Integer, String] vault ID
78
+ # @return [Enumerator<Hash>] uploads
79
+ def list_uploads(project_id:, vault_id:)
80
+ paginate(bucket_path(project_id, "/vaults/#{vault_id}/uploads.json"))
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for webhook operations.
6
+ #
7
+ # Webhooks allow external services to receive notifications when events
8
+ # occur in a Basecamp project.
9
+ #
10
+ # @example List webhooks
11
+ # account.webhooks.list(project_id: 123).each do |webhook|
12
+ # puts "#{webhook["payload_url"]} - #{webhook["active"]}"
13
+ # end
14
+ #
15
+ # @example Create a webhook
16
+ # webhook = account.webhooks.create(
17
+ # project_id: 123,
18
+ # payload_url: "https://example.com/webhooks/basecamp",
19
+ # types: ["Todo", "Message"]
20
+ # )
21
+ class WebhooksService < BaseService
22
+ # Lists all webhooks in a project.
23
+ #
24
+ # @param project_id [Integer, String] project (bucket) ID
25
+ # @return [Enumerator<Hash>] webhooks
26
+ def list(project_id:)
27
+ paginate(bucket_path(project_id, "/webhooks.json"))
28
+ end
29
+
30
+ # Gets a specific webhook.
31
+ #
32
+ # @param project_id [Integer, String] project (bucket) ID
33
+ # @param webhook_id [Integer, String] webhook ID
34
+ # @return [Hash] webhook data
35
+ def get(project_id:, webhook_id:)
36
+ http_get(bucket_path(project_id, "/webhooks/#{webhook_id}")).json
37
+ end
38
+
39
+ # Creates a new webhook.
40
+ #
41
+ # @param project_id [Integer, String] project (bucket) ID
42
+ # @param payload_url [String] URL to receive webhook payloads
43
+ # @param types [Array<String>, nil] recording types to subscribe to
44
+ # @return [Hash] created webhook
45
+ def create(project_id:, payload_url:, types: nil)
46
+ raise Basecamp::UsageError.new("Webhook payload URL is required") if payload_url.to_s.empty?
47
+ Basecamp::Security.require_https!(payload_url, "webhook payload URL")
48
+
49
+ body = compact_params(
50
+ payload_url: payload_url,
51
+ types: types
52
+ )
53
+ http_post(bucket_path(project_id, "/webhooks.json"), body: body).json
54
+ end
55
+
56
+ # Updates an existing webhook.
57
+ #
58
+ # @param project_id [Integer, String] project (bucket) ID
59
+ # @param webhook_id [Integer, String] webhook ID
60
+ # @param payload_url [String, nil] new URL
61
+ # @param types [Array<String>, nil] new recording types
62
+ # @param active [Boolean, nil] whether the webhook is active
63
+ # @return [Hash] updated webhook
64
+ def update(project_id:, webhook_id:, payload_url: nil, types: nil, active: nil)
65
+ if payload_url
66
+ Basecamp::Security.require_https!(payload_url, "webhook payload URL")
67
+ end
68
+
69
+ body = compact_params(
70
+ payload_url: payload_url,
71
+ types: types,
72
+ active: active
73
+ )
74
+ http_put(bucket_path(project_id, "/webhooks/#{webhook_id}"), body: body).json
75
+ end
76
+
77
+ # Deletes a webhook.
78
+ #
79
+ # @param project_id [Integer, String] project (bucket) ID
80
+ # @param webhook_id [Integer, String] webhook ID
81
+ # @return [void]
82
+ def delete(project_id:, webhook_id:)
83
+ http_delete(bucket_path(project_id, "/webhooks/#{webhook_id}"))
84
+ nil
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ # A simple token provider that returns a static access token.
5
+ # Useful for testing or when you manage token refresh externally.
6
+ #
7
+ # @example
8
+ # provider = Basecamp::StaticTokenProvider.new(ENV["BASECAMP_ACCESS_TOKEN"])
9
+ class StaticTokenProvider
10
+ include TokenProvider
11
+
12
+ # @param token [String] the static access token
13
+ def initialize(token)
14
+ raise ArgumentError, "token cannot be nil or empty" if token.nil? || token.empty?
15
+
16
+ @token = token
17
+ end
18
+
19
+ # @return [String] the access token
20
+ def access_token
21
+ @token
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ # Interface for providing OAuth access tokens.
5
+ # Implement this to provide custom token management (e.g., refresh tokens).
6
+ #
7
+ # @example Static token provider
8
+ # token_provider = Basecamp::StaticTokenProvider.new("your-access-token")
9
+ # client = Basecamp::Client.new(config: config, token_provider: token_provider)
10
+ #
11
+ # @example Custom token provider with refresh
12
+ # class MyTokenProvider
13
+ # include Basecamp::TokenProvider
14
+ #
15
+ # def access_token
16
+ # # Return current token, refreshing if needed
17
+ # end
18
+ #
19
+ # def refresh
20
+ # # Refresh the token
21
+ # end
22
+ # end
23
+ module TokenProvider
24
+ # Returns the current access token.
25
+ # @return [String] the OAuth access token
26
+ def access_token
27
+ raise NotImplementedError, "#{self.class} must implement #access_token"
28
+ end
29
+
30
+ # Refreshes the access token.
31
+ # @return [Boolean] true if refresh succeeded
32
+ def refresh
33
+ false
34
+ end
35
+
36
+ # Returns whether token refresh is supported.
37
+ # @return [Boolean]
38
+ def refreshable?
39
+ false
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ VERSION = "0.2.1"
5
+ API_VERSION = "2026-01-26"
6
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Webhooks
5
+ # Structured wrapper around webhook event payloads.
6
+ # Accepts any hash - does not reject unknown fields or event kinds.
7
+ class Event
8
+ attr_reader :id, :kind, :details, :created_at, :recording, :creator, :copy, :raw
9
+
10
+ def initialize(hash)
11
+ @raw = hash
12
+ @id = hash["id"]
13
+ @kind = hash["kind"]
14
+ @details = hash["details"] || {}
15
+ @created_at = hash["created_at"]
16
+ @recording = hash["recording"] || {}
17
+ @creator = hash["creator"] || {}
18
+ @copy = hash["copy"]
19
+ end
20
+
21
+ # Parse "todo_created" -> { type: "todo", action: "created" }
22
+ def parsed_kind
23
+ return { type: kind, action: "" } unless kind&.include?("_")
24
+ last_underscore = kind.rindex("_")
25
+ {
26
+ type: kind[0...last_underscore],
27
+ action: kind[(last_underscore + 1)..]
28
+ }
29
+ end
30
+ end
31
+
32
+ # Known webhook recording types (convenience constants, not exhaustive).
33
+ module RecordingType
34
+ CHECKIN_REPLY = "Checkin::Reply"
35
+ CLOUD_FILE = "CloudFile"
36
+ COMMENT = "Comment"
37
+ DOCUMENT = "Document"
38
+ FORWARD_REPLY = "Forward::Reply"
39
+ GOOGLE_DOCUMENT = "GoogleDocument"
40
+ INBOX_FORWARD = "Inbox::Forward"
41
+ MESSAGE = "Message"
42
+ QUESTION = "Question"
43
+ QUESTION_ANSWER = "Question::Answer"
44
+ SCHEDULE_ENTRY = "Schedule::Entry"
45
+ TODO = "Todo"
46
+ TODOLIST = "Todolist"
47
+ TODOLIST_GROUP = "Todolist::Group"
48
+ UPLOAD = "Upload"
49
+ VAULT = "Vault"
50
+ end
51
+ end
52
+ end