Class: Basecamp::AccountClient

Inherits:
Object
  • Object
show all
Defined in:
lib/basecamp/client.rb

Overview

HTTP client bound to a specific Basecamp account.

Create an AccountClient using Client#for_account. All API operations that require an account context use this class.

Examples:

 = client.("12345")

# List projects
.projects.list.each do |project|
  puts project["name"]
end

# Create a todo
.todos.create(
  project_id: 123,
  todolist_id: 456,
  content: "New task"
)

Instance Attribute Summary collapse

Services collapse

Instance Method Summary collapse

Constructor Details

#initialize(parent:, account_id:) ⇒ AccountClient

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a new instance of AccountClient.

Parameters:

  • parent (Client)

    the parent client

  • account_id (String)

    the account ID



127
128
129
130
131
132
# File 'lib/basecamp/client.rb', line 127

def initialize(parent:, account_id:)
  @parent = parent
  @account_id = 
  @services = {}
  @mutex = Mutex.new
end

Instance Attribute Details

#account_idString (readonly)

Returns the account ID this client is bound to.

Returns:

  • (String)

    the account ID this client is bound to



122
123
124
# File 'lib/basecamp/client.rb', line 122

def 
  @account_id
end

Instance Method Details

#accountServices::AccountService



525
526
527
# File 'lib/basecamp/client.rb', line 525

def 
  service(:account) { Services::AccountService.new(self) }
end

#attachmentsServices::AttachmentsService



400
401
402
# File 'lib/basecamp/client.rb', line 400

def attachments
  service(:attachments) { Services::AttachmentsService.new(self) }
end

#automationServices::AutomationService



470
471
472
# File 'lib/basecamp/client.rb', line 470

def automation
  service(:automation) { Services::AutomationService.new(self) }
end

#boostsServices::BoostsService



520
521
522
# File 'lib/basecamp/client.rb', line 520

def boosts
  service(:boosts) { Services::BoostsService.new(self) }
end

#campfiresServices::CampfiresService



370
371
372
# File 'lib/basecamp/client.rb', line 370

def campfires
  service(:campfires) { Services::CampfiresService.new(self) }
end

#card_columnsServices::CardColumnsService



425
426
427
# File 'lib/basecamp/client.rb', line 425

def card_columns
  service(:card_columns) { Services::CardColumnsService.new(self) }
end

#card_stepsServices::CardStepsService



430
431
432
# File 'lib/basecamp/client.rb', line 430

def card_steps
  service(:card_steps) { Services::CardStepsService.new(self) }
end

#card_tablesServices::CardTablesService



415
416
417
# File 'lib/basecamp/client.rb', line 415

def card_tables
  service(:card_tables) { Services::CardTablesService.new(self) }
end

#cardsServices::CardsService



420
421
422
# File 'lib/basecamp/client.rb', line 420

def cards
  service(:cards) { Services::CardsService.new(self) }
end

#checkinsServices::CheckinsService



405
406
407
# File 'lib/basecamp/client.rb', line 405

def checkins
  service(:checkins) { Services::CheckinsService.new(self) }
end

#client_approvalsServices::ClientApprovalsService



450
451
452
# File 'lib/basecamp/client.rb', line 450

def client_approvals
  service(:client_approvals) { Services::ClientApprovalsService.new(self) }
end

#client_correspondencesServices::ClientCorrespondencesService



455
456
457
# File 'lib/basecamp/client.rb', line 455

def client_correspondences
  service(:client_correspondences) { Services::ClientCorrespondencesService.new(self) }
end

#client_repliesServices::ClientRepliesService



460
461
462
# File 'lib/basecamp/client.rb', line 460

def client_replies
  service(:client_replies) { Services::ClientRepliesService.new(self) }
end

#client_visibilityServices::ClientVisibilityService



510
511
512
# File 'lib/basecamp/client.rb', line 510

def client_visibility
  service(:client_visibility) { Services::ClientVisibilityService.new(self) }
end

#commentsServices::CommentsService



350
351
352
# File 'lib/basecamp/client.rb', line 350

def comments
  service(:comments) { Services::CommentsService.new(self) }
end

#configConfig

Returns client configuration.

Returns:

  • (Config)

    client configuration



135
136
137
# File 'lib/basecamp/client.rb', line 135

def config
  @parent.config
end

#delete(path) ⇒ Response

Performs a DELETE request scoped to this account.

Parameters:

  • path (String)

    URL path (without account prefix)

Returns:



178
179
180
# File 'lib/basecamp/client.rb', line 178

def delete(path)
  @parent.http.delete((path))
end

#documentsServices::DocumentsService



390
391
392
# File 'lib/basecamp/client.rb', line 390

def documents
  service(:documents) { Services::DocumentsService.new(self) }
end

#download_url(raw_url) ⇒ DownloadResult

Downloads file content from any API-routable download URL.

Handles the full download flow: URL rewriting to the configured API host, authenticated first hop (which typically 302s to a signed download URL), and unauthenticated second hop to fetch the actual file content.

Parameters:

  • raw_url (String)

    absolute download URL (e.g., from bc-attachment elements)

Returns:

  • (DownloadResult)

    the download result with body, content_type, content_length, filename

Raises:

  • (UsageError)

    if raw_url is empty or not absolute

  • (NetworkError)

    if a network error occurs

  • (ApiError)

    if the API or download returns an error



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/basecamp/client.rb', line 242

def download_url(raw_url)
  # Validation
  raise UsageError.new("download URL is required") if raw_url.nil? || raw_url.to_s.empty?

  begin
    parsed = URI.parse(raw_url)
  rescue URI::InvalidURIError
    raise UsageError.new("download URL must be an absolute URL")
  end
  raise UsageError.new("download URL must be an absolute URL") unless parsed.is_a?(URI::HTTP)

  # Operation hooks
  op = OperationInfo.new(
    service: "Account", operation: "DownloadURL",
    resource_type: "download", is_mutation: false
  )
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
  safe_hook { hooks.on_operation_start(op) }

  begin
    # URL rewriting: replace scheme+host with config.base_url origin, preserve path+query+fragment
    base = URI.parse(config.base_url)
    rewritten = parsed.dup
    rewritten.scheme = base.scheme
    rewritten.host = base.host
    rewritten.port = base.port
    rewritten_url = rewritten.to_s

    # Hop 1: Authenticated API request (no retry, captures redirect)
    response = http.get_no_retry(rewritten_url)

    result = case response.status
    when 301, 302, 303, 307, 308
      # Redirect — extract Location, proceed to hop 2
      location = response.headers["Location"] || response.headers["location"]
      raise ApiError.new("redirect #{response.status} with no Location header") if location.nil? || location.empty?

      # Resolve relative Location against the rewritten API URL
      resolved_url = Security.resolve_url(rewritten_url, location)

      # Hop 2: fetch from signed URL (no auth, no hooks)
      signed_response = fetch_signed_download(resolved_url)

      DownloadResult.new(
        body: signed_response.body,
        content_type: signed_response["Content-Type"] || "",
        content_length: parse_content_length(signed_response["Content-Length"]),
        filename: Basecamp.filename_from_url(raw_url)
      )

    when 200..299
      # Direct download — no second hop
      DownloadResult.new(
        body: response.body,
        content_type: response.headers["Content-Type"] || response.headers["content-type"] || "",
        content_length: parse_content_length(response.headers["Content-Length"] || response.headers["content-length"]),
        filename: Basecamp.filename_from_url(raw_url)
      )

    else
      # This shouldn't happen because Faraday's raise_error middleware
      # handles 4xx/5xx, but handle it defensively
      raise Basecamp.error_from_response(response.status, response.body)
    end
  rescue => e
    duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
    safe_hook { hooks.on_operation_end(op, OperationResult.new(duration_ms: duration, error: e)) }
    raise
  else
    duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
    safe_hook { hooks.on_operation_end(op, OperationResult.new(duration_ms: duration, error: nil)) }
    result
  end
end

#eventsServices::EventsService



445
446
447
# File 'lib/basecamp/client.rb', line 445

def events
  service(:events) { Services::EventsService.new(self) }
end

#forwardsServices::ForwardsService



410
411
412
# File 'lib/basecamp/client.rb', line 410

def forwards
  service(:forwards) { Services::ForwardsService.new(self) }
end

#gaugesServices::GaugesService



530
531
532
# File 'lib/basecamp/client.rb', line 530

def gauges
  service(:gauges) { Services::GaugesService.new(self) }
end

#get(path, params: {}) ⇒ Response

Performs a GET request scoped to this account.

Parameters:

  • path (String)

    URL path (without account prefix)

  • params (Hash) (defaults to: {})

    query parameters

Returns:



155
156
157
# File 'lib/basecamp/client.rb', line 155

def get(path, params: {})
  @parent.http.get((path), params: params)
end

#hill_chartsServices::HillChartsService



335
336
337
# File 'lib/basecamp/client.rb', line 335

def hill_charts
  service(:hill_charts) { Services::HillChartsService.new(self) }
end

#hooksHooks

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the observability hooks.

Returns:

  • (Hooks)

    the observability hooks



147
148
149
# File 'lib/basecamp/client.rb', line 147

def hooks
  @parent.hooks
end

#httpHttp

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the HTTP client.

Returns:

  • (Http)

    the HTTP client



141
142
143
# File 'lib/basecamp/client.rb', line 141

def http
  @parent.http
end

#lineupServices::LineupService



465
466
467
# File 'lib/basecamp/client.rb', line 465

def lineup
  service(:lineup) { Services::LineupService.new(self) }
end

#message_boardsServices::MessageBoardsService



360
361
362
# File 'lib/basecamp/client.rb', line 360

def message_boards
  service(:message_boards) { Services::MessageBoardsService.new(self) }
end

#message_typesServices::MessageTypesService



475
476
477
# File 'lib/basecamp/client.rb', line 475

def message_types
  service(:message_types) { Services::MessageTypesService.new(self) }
end

#messagesServices::MessagesService



355
356
357
# File 'lib/basecamp/client.rb', line 355

def messages
  service(:messages) { Services::MessagesService.new(self) }
end

#my_assignmentsServices::MyAssignmentsService



535
536
537
# File 'lib/basecamp/client.rb', line 535

def my_assignments
  service(:my_assignments) { Services::MyAssignmentsService.new(self) }
end

#my_notificationsServices::MyNotificationsService



540
541
542
# File 'lib/basecamp/client.rb', line 540

def my_notifications
  service(:my_notifications) { Services::MyNotificationsService.new(self) }
end

#paginate(path, params: {}) {|Hash| ... } ⇒ Enumerator

Fetches all pages of a paginated resource.

Parameters:

  • path (String)

    URL path (without account prefix)

  • params (Hash) (defaults to: {})

    query parameters

Yields:

  • (Hash)

    each item from the response

Returns:

  • (Enumerator)

    if no block given



207
208
209
# File 'lib/basecamp/client.rb', line 207

def paginate(path, params: {}, &)
  @parent.http.paginate((path), params: params, &)
end

#paginate_key(path, key:, params: {}) {|Hash| ... } ⇒ Enumerator

Fetches all pages of a paginated resource, extracting items from a key. Use this for endpoints that return objects like { "events": [...] }.

Parameters:

  • path (String)

    URL path (without account prefix)

  • key (String)

    the key containing the array of items

  • params (Hash) (defaults to: {})

    query parameters

Yields:

  • (Hash)

    each item from the response

Returns:

  • (Enumerator)

    if no block given



218
219
220
# File 'lib/basecamp/client.rb', line 218

def paginate_key(path, key:, params: {}, &)
  @parent.http.paginate_key((path), key: key, params: params, &)
end

#paginate_wrapped(path, key:, params: {}) ⇒ Hash

Fetches a wrapped paginated resource, returning wrapper fields + lazy paginated items.

Parameters:

  • path (String)

    URL path (without account prefix)

  • key (String)

    the key containing the array of paginated items

  • params (Hash) (defaults to: {})

    query parameters

Returns:

  • (Hash)

    wrapper fields merged with key => Enumerator of all items



227
228
229
# File 'lib/basecamp/client.rb', line 227

def paginate_wrapped(path, key:, params: {})
  @parent.http.paginate_wrapped((path), key: key, params: params)
end

#peopleServices::PeopleService



345
346
347
# File 'lib/basecamp/client.rb', line 345

def people
  service(:people) { Services::PeopleService.new(self) }
end

#post(path, body: nil) ⇒ Response

Performs a POST request scoped to this account.

Parameters:

  • path (String)

    URL path (without account prefix)

  • body (Hash, nil) (defaults to: nil)

    request body

Returns:



163
164
165
# File 'lib/basecamp/client.rb', line 163

def post(path, body: nil)
  @parent.http.post((path), body: body)
end

#post_raw(path, body:, content_type:) ⇒ Response

Performs a POST request with raw binary data scoped to this account. Used for file uploads (attachments).

Parameters:

  • path (String)

    URL path (without account prefix)

  • body (String, IO)

    raw binary data

  • content_type (String)

    MIME content type

Returns:



188
189
190
# File 'lib/basecamp/client.rb', line 188

def post_raw(path, body:, content_type:)
  @parent.http.post_raw((path), body: body, content_type: content_type)
end

#projectsServices::ProjectsService



320
321
322
# File 'lib/basecamp/client.rb', line 320

def projects
  service(:projects) { Services::ProjectsService.new(self) }
end

#put(path, body: nil) ⇒ Response

Performs a PUT request scoped to this account.

Parameters:

  • path (String)

    URL path (without account prefix)

  • body (Hash, nil) (defaults to: nil)

    request body

Returns:



171
172
173
# File 'lib/basecamp/client.rb', line 171

def put(path, body: nil)
  @parent.http.put((path), body: body)
end

#put_raw(path, body:, content_type:) ⇒ Response

Performs a PUT request with raw binary data scoped to this account. Used for multipart uploads (e.g., account logo).

Parameters:

  • path (String)

    URL path (without account prefix)

  • body (String, IO)

    raw binary data

  • content_type (String)

    MIME content type

Returns:



198
199
200
# File 'lib/basecamp/client.rb', line 198

def put_raw(path, body:, content_type:)
  @parent.http.put_raw((path), body: body, content_type: content_type)
end

#recordingsServices::RecordingsService



385
386
387
# File 'lib/basecamp/client.rb', line 385

def recordings
  service(:recordings) { Services::RecordingsService.new(self) }
end

#reportsServices::ReportsService



495
496
497
# File 'lib/basecamp/client.rb', line 495

def reports
  service(:reports) { Services::ReportsService.new(self) }
end

#schedulesServices::SchedulesService



375
376
377
# File 'lib/basecamp/client.rb', line 375

def schedules
  service(:schedules) { Services::SchedulesService.new(self) }
end

#searchServices::SearchService



490
491
492
# File 'lib/basecamp/client.rb', line 490

def search
  service(:search) { Services::SearchService.new(self) }
end

#subscriptionsServices::SubscriptionsService



485
486
487
# File 'lib/basecamp/client.rb', line 485

def subscriptions
  service(:subscriptions) { Services::SubscriptionsService.new(self) }
end

#templatesServices::TemplatesService



440
441
442
# File 'lib/basecamp/client.rb', line 440

def templates
  service(:templates) { Services::TemplatesService.new(self) }
end

#timelineServices::TimelineService



500
501
502
# File 'lib/basecamp/client.rb', line 500

def timeline
  service(:timeline) { Services::TimelineService.new(self) }
end

#timesheetsServices::TimesheetsService



505
506
507
# File 'lib/basecamp/client.rb', line 505

def timesheets
  service(:timesheets) { Services::TimesheetsService.new(self) }
end

#todolist_groupsServices::TodolistGroupsService



515
516
517
# File 'lib/basecamp/client.rb', line 515

def todolist_groups
  service(:todolist_groups) { Services::TodolistGroupsService.new(self) }
end

#todolistsServices::TodolistsService



340
341
342
# File 'lib/basecamp/client.rb', line 340

def todolists
  service(:todolists) { Services::TodolistsService.new(self) }
end

#todosServices::TodosService



325
326
327
# File 'lib/basecamp/client.rb', line 325

def todos
  service(:todos) { Services::TodosService.new(self) }
end

#todosetsServices::TodosetsService



330
331
332
# File 'lib/basecamp/client.rb', line 330

def todosets
  service(:todosets) { Services::TodosetsService.new(self) }
end

#toolsServices::ToolsService



480
481
482
# File 'lib/basecamp/client.rb', line 480

def tools
  service(:tools) { Services::ToolsService.new(self) }
end

#uploadsServices::UploadsService



395
396
397
# File 'lib/basecamp/client.rb', line 395

def uploads
  service(:uploads) { Services::UploadsService.new(self) }
end

#vaultsServices::VaultsService



380
381
382
# File 'lib/basecamp/client.rb', line 380

def vaults
  service(:vaults) { Services::VaultsService.new(self) }
end

#webhooksServices::WebhooksService



365
366
367
# File 'lib/basecamp/client.rb', line 365

def webhooks
  service(:webhooks) { Services::WebhooksService.new(self) }
end

#wormholesServices::WormholesService



435
436
437
# File 'lib/basecamp/client.rb', line 435

def wormholes
  service(:wormholes) { Services::WormholesService.new(self) }
end