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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ # Information about a service operation for observability hooks.
5
+ OperationInfo = Data.define(:service, :operation, :resource_type, :is_mutation, :project_id, :resource_id) do
6
+ def initialize(service:, operation:, resource_type: nil, is_mutation: false, project_id: nil, resource_id: nil)
7
+ super
8
+ end
9
+ end
10
+
11
+ # Result information for completed service operations.
12
+ OperationResult = Data.define(:duration_ms, :error) do
13
+ def initialize(duration_ms: 0, error: nil)
14
+ super
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ # Information about an HTTP request for observability hooks.
5
+ RequestInfo = Data.define(:method, :url, :attempt) do
6
+ def initialize(method:, url:, attempt: 1)
7
+ super
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ # Result information for completed HTTP requests.
5
+ RequestResult = Data.define(:status_code, :duration, :error, :retry_after, :from_cache) do
6
+ def initialize(status_code: nil, duration: 0.0, error: nil, retry_after: nil, from_cache: false)
7
+ super
8
+ end
9
+
10
+ def success?
11
+ status_code && status_code >= 200 && status_code < 300
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Basecamp
6
+ # Security helpers for URL validation, message truncation, and header redaction.
7
+ # Used across the SDK to enforce HTTPS, prevent SSRF, and protect sensitive data.
8
+ module Security
9
+ MAX_ERROR_MESSAGE_BYTES = 500
10
+ MAX_RESPONSE_BODY_BYTES = 50 * 1024 * 1024 # 50 MB
11
+ MAX_ERROR_BODY_BYTES = 1 * 1024 * 1024 # 1 MB
12
+
13
+ def self.truncate(str, max = MAX_ERROR_MESSAGE_BYTES)
14
+ return str if str.nil? || str.bytesize <= max
15
+
16
+ max <= 3 ? str.byteslice(0, max) : str.byteslice(0, max - 3) + "..."
17
+ end
18
+
19
+ def self.require_https!(url, label = "URL")
20
+ uri = URI.parse(url.to_s)
21
+ raise UsageError.new("#{label} must use HTTPS: #{url}") unless uri.scheme&.downcase == "https"
22
+ rescue URI::InvalidURIError
23
+ raise UsageError.new("Invalid #{label}: #{url}")
24
+ end
25
+
26
+ def self.same_origin?(a, b)
27
+ ua = URI.parse(a)
28
+ ub = URI.parse(b)
29
+ return false if ua.scheme.nil? || ub.scheme.nil?
30
+
31
+ ua.scheme.downcase == ub.scheme.downcase &&
32
+ normalize_host(ua) == normalize_host(ub)
33
+ rescue URI::InvalidURIError
34
+ false
35
+ end
36
+
37
+ def self.resolve_url(base, target)
38
+ URI.join(base, target).to_s
39
+ rescue URI::InvalidURIError
40
+ target
41
+ end
42
+
43
+ def self.normalize_host(uri)
44
+ host = uri.host&.downcase
45
+ port = uri.port
46
+ return host if port.nil?
47
+ return host if uri.scheme&.downcase == "https" && port == 443
48
+ return host if uri.scheme&.downcase == "http" && port == 80
49
+
50
+ "#{host}:#{port}"
51
+ end
52
+
53
+ def self.check_body_size!(body, max, label = "Response")
54
+ return if body.nil?
55
+
56
+ if body.bytesize > max
57
+ raise Basecamp::APIError.new(
58
+ "#{label} body too large (#{body.bytesize} bytes, max #{max})"
59
+ )
60
+ end
61
+ end
62
+
63
+ def self.localhost?(url)
64
+ uri = URI.parse(url.to_s)
65
+ host = uri.host&.downcase
66
+ return false if host.nil?
67
+
68
+ host == "localhost" ||
69
+ host == "127.0.0.1" ||
70
+ host == "::1" ||
71
+ host == "[::1]" ||
72
+ host.end_with?(".localhost")
73
+ rescue URI::InvalidURIError
74
+ false
75
+ end
76
+
77
+ def self.require_https_unless_localhost!(url, label = "URL")
78
+ return if localhost?(url)
79
+
80
+ require_https!(url, label)
81
+ end
82
+
83
+ # Headers that contain sensitive values and should be redacted.
84
+ SENSITIVE_HEADERS = %w[
85
+ authorization
86
+ cookie
87
+ set-cookie
88
+ x-csrf-token
89
+ ].freeze
90
+
91
+ # Returns a copy of the headers with sensitive values replaced by "[REDACTED]".
92
+ #
93
+ # This is useful for safely logging HTTP requests and responses without
94
+ # exposing tokens, cookies, or other credentials.
95
+ #
96
+ # @param headers [Hash] the headers hash (case-insensitive keys)
97
+ # @return [Hash] a new hash with sensitive values redacted
98
+ #
99
+ # @example
100
+ # headers = { "Authorization" => "Bearer token", "Content-Type" => "application/json" }
101
+ # safe = Basecamp::Security.redact_headers(headers)
102
+ # # => { "Authorization" => "[REDACTED]", "Content-Type" => "application/json" }
103
+ #
104
+ def self.redact_headers(headers)
105
+ result = {}
106
+ headers.each do |key, value|
107
+ result[key] = SENSITIVE_HEADERS.include?(key.to_s.downcase) ? "[REDACTED]" : value
108
+ end
109
+ result
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Basecamp
6
+ module Services
7
+ # Service for attachment operations.
8
+ #
9
+ # Attachments are used to upload files that can be embedded in rich text
10
+ # content like messages, comments, and documents. After uploading, you
11
+ # receive an attachable_sgid that can be used to embed the file in HTML.
12
+ #
13
+ # @example Upload a file
14
+ # attachment = account.attachments.create(
15
+ # filename: "report.pdf",
16
+ # content_type: "application/pdf",
17
+ # data: file_content
18
+ # )
19
+ # # Use in HTML: <bc-attachment sgid="#{attachment["attachable_sgid"]}"></bc-attachment>
20
+ class AttachmentsService < BaseService
21
+ # Creates an attachment by uploading a file.
22
+ # Returns an attachable_sgid for embedding the file in rich text content.
23
+ #
24
+ # @param filename [String] filename for the uploaded file
25
+ # @param content_type [String] MIME content type (e.g., "image/png", "application/pdf")
26
+ # @param data [String] file data
27
+ # @return [Hash] attachment response with attachable_sgid
28
+ def create(filename:, content_type:, data:)
29
+ http_post_raw("/attachments.json?name=#{URI.encode_www_form_component(filename)}", body: data, content_type: content_type).json
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for authorization operations.
6
+ # This is the only service that doesn't require an account context.
7
+ #
8
+ # @example Get authorization info
9
+ # auth = client.authorization.get
10
+ # puts "Identity: #{auth["identity"]["email_address"]}"
11
+ # auth["accounts"].each do |account|
12
+ # puts "Account: #{account["name"]} (#{account["id"]})"
13
+ # end
14
+ class AuthorizationService < BaseService
15
+ # Fallback Launchpad endpoint for authorization
16
+ LAUNCHPAD_AUTHORIZATION_URL = "https://launchpad.37signals.com/authorization.json"
17
+
18
+ # Gets authorization information for the current user.
19
+ #
20
+ # Attempts to use the authorization endpoint discovered via OAuth discovery
21
+ # on the configured base URL. Falls back to Launchpad if discovery fails.
22
+ #
23
+ # Returns the authenticated user's identity and list of accounts
24
+ # they have access to.
25
+ #
26
+ # @return [Hash] authorization info with :identity and :accounts
27
+ # @see https://github.com/basecamp/bc3-api/blob/master/sections/authentication.md
28
+ def get
29
+ url = discover_authorization_url
30
+ response = http.get_absolute(url)
31
+ response.json
32
+ end
33
+
34
+ private
35
+
36
+ def discover_authorization_url
37
+ # Try OAuth discovery on the configured base URL
38
+ config = Oauth.discover(http.base_url)
39
+ # Use issuer as base for authorization.json
40
+ "#{config.issuer.chomp("/")}/authorization.json"
41
+ rescue Oauth::OAuthError
42
+ # Fall back to Launchpad
43
+ LAUNCHPAD_AUTHORIZATION_URL
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi/escape"
4
+ require_relative "../errors"
5
+
6
+ module Basecamp
7
+ module Services
8
+ # Base service class for Basecamp API services.
9
+ #
10
+ # Provides shared functionality for all service classes including:
11
+ # - HTTP method delegation (http_get, http_post, etc.)
12
+ # - Path building helpers
13
+ # - Pagination support
14
+ #
15
+ # @example
16
+ # class TodosService < BaseService
17
+ # def list(project_id:, todolist_id:)
18
+ # paginate(bucket_path(project_id, "/todolists/#{todolist_id}/todos.json"))
19
+ # end
20
+ # end
21
+ class BaseService
22
+ # @return [String] the account ID for API requests
23
+ attr_reader :account_id
24
+
25
+ # @param client [Object] the parent client (AccountClient or Client)
26
+ def initialize(client)
27
+ @client = client
28
+ @account_id = client.account_id
29
+ @hooks = client.hooks
30
+ end
31
+
32
+ protected
33
+
34
+ # Wraps a service operation with hooks for observability.
35
+ # @param service [String] service name (e.g., "projects")
36
+ # @param operation [String] operation name (e.g., "list")
37
+ # @param resource_type [String, nil] resource type (e.g., "Project")
38
+ # @param is_mutation [Boolean] whether this is a write operation
39
+ # @param project_id [Integer, String, nil] project/bucket ID
40
+ # @param resource_id [Integer, String, nil] resource ID
41
+ # @yield the operation to execute
42
+ # @return the result of the block
43
+ def with_operation(service:, operation:, resource_type: nil, is_mutation: false, project_id: nil, resource_id: nil)
44
+ info = OperationInfo.new(
45
+ service: service, operation: operation, resource_type: resource_type,
46
+ is_mutation: is_mutation, project_id: project_id, resource_id: resource_id
47
+ )
48
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+ safe_hook { @hooks.on_operation_start(info) }
50
+ result = yield
51
+ duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
52
+ safe_hook { @hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: nil)) }
53
+ result
54
+ rescue => e
55
+ duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
56
+ safe_hook { @hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: e)) }
57
+ raise
58
+ end
59
+
60
+ # Wraps a lazy Enumerator so operation hooks fire around actual iteration,
61
+ # not at enumerator creation time. Hooks fire when the consumer begins
62
+ # iterating (.each, .to_a, .first, etc.) and end fires via ensure when
63
+ # iteration completes, errors, or is cut short by break/take.
64
+ def wrap_paginated(service:, operation:, is_mutation: false, project_id: nil, resource_id: nil)
65
+ info = OperationInfo.new(
66
+ service: service, operation: operation,
67
+ is_mutation: is_mutation, project_id: project_id, resource_id: resource_id
68
+ )
69
+ enum = yield
70
+
71
+ hooks = @hooks
72
+ Enumerator.new do |yielder|
73
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
74
+ error = nil
75
+ begin
76
+ safe_hook { hooks.on_operation_start(info) }
77
+ enum.each { |item| yielder.yield(item) }
78
+ rescue => e
79
+ error = e
80
+ raise
81
+ ensure
82
+ duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
83
+ safe_hook { hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: error)) }
84
+ end
85
+ end
86
+ end
87
+
88
+ # Invoke a hook callback, swallowing exceptions so hooks never break SDK behavior.
89
+ def safe_hook
90
+ yield
91
+ rescue => e
92
+ warn "Basecamp hook error: #{e.class}: #{e.message}"
93
+ end
94
+
95
+ # @return [HTTP] the HTTP client for direct access
96
+ def http
97
+ @client.http
98
+ end
99
+
100
+ # Helper to remove nil values from a hash.
101
+ # @param hash [Hash] the input hash
102
+ # @return [Hash] hash with nil values removed
103
+ def compact_params(**kwargs)
104
+ kwargs.compact
105
+ end
106
+
107
+ # Build a bucket (project) path.
108
+ # @param project_id [Integer, String] the project/bucket ID
109
+ # @param path [String] the path suffix
110
+ # @return [String] the full bucket path
111
+ def bucket_path(project_id, path)
112
+ "/buckets/#{project_id}#{path}"
113
+ end
114
+
115
+ # Delegate HTTP methods to the client with http_ prefix to avoid conflicts
116
+ # with service method names (e.g., service.get vs http_get)
117
+ # @!method http_get(path, params: {})
118
+ # @see AccountClient#get
119
+ # @!method http_post(path, body: nil)
120
+ # @see AccountClient#post
121
+ # @!method http_put(path, body: nil)
122
+ # @see AccountClient#put
123
+ # @!method http_delete(path)
124
+ # @see AccountClient#delete
125
+ # @!method http_post_raw(path, body:, content_type:)
126
+ # @see AccountClient#post_raw
127
+ # @!method paginate(path, params: {}, &block)
128
+ # @see AccountClient#paginate
129
+ %i[get post put delete post_raw].each do |method|
130
+ define_method(:"http_#{method}") do |*args, **kwargs, &block|
131
+ @client.public_send(method, *args, **kwargs, &block)
132
+ end
133
+ end
134
+
135
+ # Paginate doesn't conflict with service methods, keep as-is
136
+ def paginate(...)
137
+ @client.paginate(...)
138
+ end
139
+
140
+ # Paginate extracting items from a specific key (for object responses)
141
+ def paginate_key(...)
142
+ @client.paginate_key(...)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for campfire (chat) operations.
6
+ #
7
+ # Campfires are real-time chat rooms within Basecamp projects.
8
+ # They contain lines (messages) and can have chatbot integrations.
9
+ #
10
+ # @example List all campfires
11
+ # account.campfires.list.each do |campfire|
12
+ # puts campfire["title"]
13
+ # end
14
+ #
15
+ # @example Send a message
16
+ # line = account.campfires.create_line(
17
+ # project_id: 123,
18
+ # campfire_id: 456,
19
+ # content: "Hello team!"
20
+ # )
21
+ class CampfiresService < BaseService
22
+ # Lists all campfires across the account.
23
+ #
24
+ # @return [Enumerator<Hash>] campfires
25
+ def list
26
+ paginate("/chats.json")
27
+ end
28
+
29
+ # Gets a specific campfire.
30
+ #
31
+ # @param project_id [Integer, String] project (bucket) ID
32
+ # @param campfire_id [Integer, String] campfire ID
33
+ # @return [Hash] campfire data
34
+ def get(project_id:, campfire_id:)
35
+ http_get(bucket_path(project_id, "/chats/#{campfire_id}.json")).json
36
+ end
37
+
38
+ # Lists all lines (messages) in a campfire.
39
+ #
40
+ # @param project_id [Integer, String] project (bucket) ID
41
+ # @param campfire_id [Integer, String] campfire ID
42
+ # @return [Enumerator<Hash>] campfire lines
43
+ def list_lines(project_id:, campfire_id:)
44
+ paginate(bucket_path(project_id, "/chats/#{campfire_id}/lines.json"))
45
+ end
46
+
47
+ # Gets a specific line (message) from a campfire.
48
+ #
49
+ # @param project_id [Integer, String] project (bucket) ID
50
+ # @param campfire_id [Integer, String] campfire ID
51
+ # @param line_id [Integer, String] line ID
52
+ # @return [Hash] campfire line
53
+ def get_line(project_id:, campfire_id:, line_id:)
54
+ http_get(bucket_path(project_id, "/chats/#{campfire_id}/lines/#{line_id}.json")).json
55
+ end
56
+
57
+ # Creates a new line (message) in a campfire.
58
+ #
59
+ # @param project_id [Integer, String] project (bucket) ID
60
+ # @param campfire_id [Integer, String] campfire ID
61
+ # @param content [String] plain text message content
62
+ # @return [Hash] created line
63
+ def create_line(project_id:, campfire_id:, content:)
64
+ body = { content: content }
65
+ http_post(bucket_path(project_id, "/chats/#{campfire_id}/lines.json"), body: body).json
66
+ end
67
+
68
+ # Deletes a line (message) from a campfire.
69
+ #
70
+ # @param project_id [Integer, String] project (bucket) ID
71
+ # @param campfire_id [Integer, String] campfire ID
72
+ # @param line_id [Integer, String] line ID
73
+ # @return [void]
74
+ def delete_line(project_id:, campfire_id:, line_id:)
75
+ http_delete(bucket_path(project_id, "/chats/#{campfire_id}/lines/#{line_id}.json"))
76
+ nil
77
+ end
78
+
79
+ # Lists all chatbots for a campfire.
80
+ #
81
+ # @param project_id [Integer, String] project (bucket) ID
82
+ # @param campfire_id [Integer, String] campfire ID
83
+ # @return [Enumerator<Hash>] chatbots
84
+ def list_chatbots(project_id:, campfire_id:)
85
+ paginate(bucket_path(project_id, "/chats/#{campfire_id}/integrations.json"))
86
+ end
87
+
88
+ # Gets a specific chatbot.
89
+ #
90
+ # @param project_id [Integer, String] project (bucket) ID
91
+ # @param campfire_id [Integer, String] campfire ID
92
+ # @param chatbot_id [Integer, String] chatbot ID
93
+ # @return [Hash] chatbot data
94
+ def get_chatbot(project_id:, campfire_id:, chatbot_id:)
95
+ http_get(bucket_path(project_id, "/chats/#{campfire_id}/integrations/#{chatbot_id}.json")).json
96
+ end
97
+
98
+ # Creates a new chatbot for a campfire.
99
+ #
100
+ # @param project_id [Integer, String] project (bucket) ID
101
+ # @param campfire_id [Integer, String] campfire ID
102
+ # @param service_name [String] chatbot name (no spaces, emoji, or non-word characters)
103
+ # @param command_url [String, nil] HTTPS URL for bot callbacks
104
+ # @return [Hash] created chatbot with lines_url for posting
105
+ def create_chatbot(project_id:, campfire_id:, service_name:, command_url: nil)
106
+ body = compact_params(
107
+ service_name: service_name,
108
+ command_url: command_url
109
+ )
110
+ http_post(bucket_path(project_id, "/chats/#{campfire_id}/integrations.json"), body: body).json
111
+ end
112
+
113
+ # Updates an existing chatbot.
114
+ #
115
+ # @param project_id [Integer, String] project (bucket) ID
116
+ # @param campfire_id [Integer, String] campfire ID
117
+ # @param chatbot_id [Integer, String] chatbot ID
118
+ # @param service_name [String] new chatbot name
119
+ # @param command_url [String, nil] new callback URL
120
+ # @return [Hash] updated chatbot
121
+ def update_chatbot(project_id:, campfire_id:, chatbot_id:, service_name:, command_url: nil)
122
+ body = compact_params(
123
+ service_name: service_name,
124
+ command_url: command_url
125
+ )
126
+ http_put(bucket_path(project_id, "/chats/#{campfire_id}/integrations/#{chatbot_id}.json"), body: body).json
127
+ end
128
+
129
+ # Deletes a chatbot.
130
+ #
131
+ # @param project_id [Integer, String] project (bucket) ID
132
+ # @param campfire_id [Integer, String] campfire ID
133
+ # @param chatbot_id [Integer, String] chatbot ID
134
+ # @return [void]
135
+ def delete_chatbot(project_id:, campfire_id:, chatbot_id:)
136
+ http_delete(bucket_path(project_id, "/chats/#{campfire_id}/integrations/#{chatbot_id}.json"))
137
+ nil
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for card column operations.
6
+ #
7
+ # Columns are lists within a card table that contain cards.
8
+ #
9
+ # @example Create a column
10
+ # column = account.card_columns.create(
11
+ # project_id: 123,
12
+ # card_table_id: 456,
13
+ # title: "In Review"
14
+ # )
15
+ #
16
+ # @example Set column color
17
+ # account.card_columns.set_color(project_id: 123, column_id: 456, color: "blue")
18
+ class CardColumnsService < BaseService
19
+ # Gets a column by ID.
20
+ #
21
+ # @param project_id [Integer, String] project (bucket) ID
22
+ # @param column_id [Integer, String] column ID
23
+ # @return [Hash] column data
24
+ def get(project_id:, column_id:)
25
+ http_get(bucket_path(project_id, "/card_tables/columns/#{column_id}.json")).json
26
+ end
27
+
28
+ # Creates a new column in a card table.
29
+ #
30
+ # @param project_id [Integer, String] project (bucket) ID
31
+ # @param card_table_id [Integer, String] card table ID
32
+ # @param title [String] column title
33
+ # @param description [String, nil] column description
34
+ # @return [Hash] created column
35
+ def create(project_id:, card_table_id:, title:, description: nil)
36
+ body = compact_params(
37
+ title: title,
38
+ description: description
39
+ )
40
+ http_post(bucket_path(project_id, "/card_tables/#{card_table_id}/columns.json"), body: body).json
41
+ end
42
+
43
+ # Updates an existing column.
44
+ #
45
+ # @param project_id [Integer, String] project (bucket) ID
46
+ # @param column_id [Integer, String] column ID
47
+ # @param title [String, nil] new title
48
+ # @param description [String, nil] new description
49
+ # @return [Hash] updated column
50
+ def update(project_id:, column_id:, title: nil, description: nil)
51
+ body = compact_params(
52
+ title: title,
53
+ description: description
54
+ )
55
+ http_put(bucket_path(project_id, "/card_tables/columns/#{column_id}.json"), body: body).json
56
+ end
57
+
58
+ # Moves a column within a card table.
59
+ #
60
+ # @param project_id [Integer, String] project (bucket) ID
61
+ # @param card_table_id [Integer, String] card table ID
62
+ # @param source_id [Integer, String] column ID to move
63
+ # @param target_id [Integer, String] column ID to move relative to
64
+ # @param position [Integer, nil] position relative to target
65
+ # @return [void]
66
+ def move(project_id:, card_table_id:, source_id:, target_id:, position: nil)
67
+ body = compact_params(
68
+ source_id: source_id,
69
+ target_id: target_id,
70
+ position: position
71
+ )
72
+ http_post(bucket_path(project_id, "/card_tables/#{card_table_id}/moves.json"), body: body)
73
+ nil
74
+ end
75
+
76
+ # Sets the color of a column.
77
+ #
78
+ # @param project_id [Integer, String] project (bucket) ID
79
+ # @param column_id [Integer, String] column ID
80
+ # @param color [String] color name (white, red, orange, yellow, green, blue, aqua, purple, gray, pink, brown)
81
+ # @return [Hash] updated column
82
+ def set_color(project_id:, column_id:, color:)
83
+ http_put(bucket_path(project_id, "/card_tables/columns/#{column_id}/color.json"),
84
+ body: { color: color }).json
85
+ end
86
+
87
+ # Adds an on-hold section to a column.
88
+ #
89
+ # @param project_id [Integer, String] project (bucket) ID
90
+ # @param column_id [Integer, String] column ID
91
+ # @return [Hash] updated column
92
+ def enable_on_hold(project_id:, column_id:)
93
+ http_post(bucket_path(project_id, "/card_tables/columns/#{column_id}/on_hold.json")).json
94
+ end
95
+
96
+ # Removes the on-hold section from a column.
97
+ #
98
+ # @param project_id [Integer, String] project (bucket) ID
99
+ # @param column_id [Integer, String] column ID
100
+ # @return [Hash] updated column
101
+ def disable_on_hold(project_id:, column_id:)
102
+ http_delete(bucket_path(project_id, "/card_tables/columns/#{column_id}/on_hold.json")).json
103
+ end
104
+ end
105
+ end
106
+ end