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



576
577
578
# File 'lib/basecamp/client.rb', line 576

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

#attachmentsServices::AttachmentsService



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

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

#automationServices::AutomationService



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

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

#bookmarksServices::BookmarksService



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

def bookmarks
  service(:bookmarks) { Services::BookmarksService.new(self) }
end

#boostsServices::BoostsService



571
572
573
# File 'lib/basecamp/client.rb', line 571

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

#calendarsServices::CalendarsService



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

def calendars
  service(:calendars) { Services::CalendarsService.new(self) }
end

#campfiresServices::CampfiresService



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

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

#card_columnsServices::CardColumnsService



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

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

#card_stepsServices::CardStepsService



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

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

#card_tablesServices::CardTablesService



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

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

#cardsServices::CardsService



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

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

#checkinsServices::CheckinsService



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

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

#client_approvalsServices::ClientApprovalsService



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

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

#client_correspondencesServices::ClientCorrespondencesService



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

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

#client_repliesServices::ClientRepliesService



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

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

#client_visibilityServices::ClientVisibilityService



561
562
563
# File 'lib/basecamp/client.rb', line 561

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

#cloud_filesServices::CloudFilesService



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

def cloud_files
  service(:cloud_files) { Services::CloudFilesService.new(self) }
end

#commentsServices::CommentsService



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

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



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

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. Neither hop follows a redirect on its own: hop 1's is the dispatch to hop 2, and a redirect on hop 2 is an error (SPEC §14 "Hop-2 Redirect Policy").

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



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
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/basecamp/client.rb', line 252

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 under the SPEC §14 hop-1 retry
    # policy (captures redirect; every attempt is authenticated)
    response = http.get_download(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

#draftsServices::DraftsService



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

def drafts
  service(:drafts) { Services::DraftsService.new(self) }
end

#eventsServices::EventsService



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

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

#everythingServices::EverythingService



551
552
553
# File 'lib/basecamp/client.rb', line 551

def everything
  service(:everything) { Services::EverythingService.new(self) }
end

#foldersServices::FoldersService



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

def folders
  service(:folders) { Services::FoldersService.new(self) }
end

#forwardsServices::ForwardsService



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

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

#gaugesServices::GaugesService



581
582
583
# File 'lib/basecamp/client.rb', line 581

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

#get(path, params: {}, operation: nil) ⇒ 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: {}, operation: nil)
  @parent.http.get((path), params: params, operation: operation)
end

#google_documentsServices::GoogleDocumentsService



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

def google_documents
  service(:google_documents) { Services::GoogleDocumentsService.new(self) }
end

#hill_chartsServices::HillChartsService



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

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



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

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

#message_boardsServices::MessageBoardsService



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

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

#message_typesServices::MessageTypesService



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

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

#messagesServices::MessagesService



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

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

#my_assignmentsServices::MyAssignmentsService



586
587
588
# File 'lib/basecamp/client.rb', line 586

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

#my_notesServices::MyNotesService



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

def my_notes
  service(:my_notes) { Services::MyNotesService.new(self) }
end

#my_notificationsServices::MyNotificationsService



591
592
593
# File 'lib/basecamp/client.rb', line 591

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

#paginate(path, params: {}, operation: nil, max_items: nil) {|Hash| ... } ⇒ ListEnumerator

Fetches all pages of a paginated resource.

Parameters:

  • path (String)

    URL path (without account prefix)

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

    query parameters

  • max_items (Integer, nil) (defaults to: nil)

    cap on items yielded across pages; nil or non-positive means no cap

Yields:

  • (Hash)

    each item from the response

Returns:



209
210
211
# File 'lib/basecamp/client.rb', line 209

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

#paginate_key(path, key:, params: {}, operation: nil, max_items: nil) {|Hash| ... } ⇒ ListEnumerator

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

  • max_items (Integer, nil) (defaults to: nil)

    cap on items yielded across pages; nil or non-positive means no cap

Yields:

  • (Hash)

    each item from the response

Returns:



222
223
224
225
# File 'lib/basecamp/client.rb', line 222

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

#paginate_wrapped(path, key:, params: {}, operation: nil, max_items: nil) ⇒ 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

  • max_items (Integer, nil) (defaults to: nil)

    cap on items yielded across pages; nil or non-positive means no cap

Returns:

  • (Hash)

    wrapper fields merged with key => ListEnumerator of all items



234
235
236
237
# File 'lib/basecamp/client.rb', line 234

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

#peopleServices::PeopleService



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

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



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

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



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

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

#reportsServices::ReportsService



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

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

#schedulesServices::SchedulesService



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

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

#searchServices::SearchService



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

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

#subscriptionsServices::SubscriptionsService



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

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

#templatesServices::TemplatesService



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

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

#timelineServices::TimelineService



546
547
548
# File 'lib/basecamp/client.rb', line 546

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

#timesheetsServices::TimesheetsService



556
557
558
# File 'lib/basecamp/client.rb', line 556

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

#todolist_groupsServices::TodolistGroupsService



566
567
568
# File 'lib/basecamp/client.rb', line 566

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

#todolistsServices::TodolistsService



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

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

#todosServices::TodosService



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

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

#todosetsServices::TodosetsService



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

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

#toolsServices::ToolsService



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

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

#uploadsServices::UploadsService



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

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

#vaultsServices::VaultsService



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

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

#webhooksServices::WebhooksService



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

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

#wormholesServices::WormholesService



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

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