Class: Basecamp::ApiClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/basecamp/api_client.rb

Overview

HTTP client for Basecamp 4 JSON API.

Features:

  • Auto-refreshes expired access tokens
  • Respects Basecamp rate limits (429 + Retry-After)
  • Retries transient failures (502, 503)
  • User-Agent required by Basecamp API

Reference: https://github.com/basecamp/api/blob/master/sections/authentication.md

Defined Under Namespace

Classes: ApiError, NotFoundError, RateLimitError

Constant Summary collapse

BASE_URL =
'https://3.basecampapi.com'
USER_AGENT =
'WarmlyYours CRM (support@warmlyyours.com)'
MAX_RETRIES =
3
RATE_LIMIT_SLEEP =

default wait if no Retry-After header

5

Instance Method Summary collapse

Constructor Details

#initialize(account: nil, oauth_service: nil) ⇒ ApiClient

Returns a new instance of ApiClient.

Parameters:

  • account (Account, nil) (defaults to: nil)

    the CRM account whose Basecamp token to use (nil for system-level)

  • oauth_service (Basecamp::OauthService, nil) (defaults to: nil)

    override for testing



35
36
37
# File 'app/services/basecamp/api_client.rb', line 35

def initialize(account: nil, oauth_service: nil)
  @oauth_service = oauth_service || Basecamp::OauthService.new(account: )
end

Instance Method Details

#campfire_lines(project_id, campfire_id) ⇒ Object



197
198
199
# File 'app/services/basecamp/api_client.rb', line 197

def campfire_lines(project_id, campfire_id)
  get("/buckets/#{project_id}/chats/#{campfire_id}/lines.json")
end

#card_tables(project_id) ⇒ Object

--- Card Table (Kanban) endpoints ---



203
204
205
# File 'app/services/basecamp/api_client.rb', line 203

def card_tables(project_id)
  get("/buckets/#{project_id}/card_tables.json")
end

#cards(project_id, column_id) ⇒ Object



211
212
213
# File 'app/services/basecamp/api_client.rb', line 211

def cards(project_id, column_id)
  get("/buckets/#{project_id}/card_tables/lists/#{column_id}/cards.json")
end

#columns(project_id, card_table_id) ⇒ Object



207
208
209
# File 'app/services/basecamp/api_client.rb', line 207

def columns(project_id, card_table_id)
  get("/buckets/#{project_id}/card_tables/#{card_table_id}/columns.json")
end

#comment(project_id, comment_id) ⇒ Object



254
255
256
# File 'app/services/basecamp/api_client.rb', line 254

def comment(project_id, comment_id)
  get("/buckets/#{project_id}/comments/#{comment_id}.json")
end

#comments(project_id, recording_id) ⇒ Object

--- Comments ---



250
251
252
# File 'app/services/basecamp/api_client.rb', line 250

def comments(project_id, recording_id)
  get_paginated("/buckets/#{project_id}/recordings/#{recording_id}/comments.json")
end

#complete_todo(project_id, todo_id) ⇒ Object



131
132
133
# File 'app/services/basecamp/api_client.rb', line 131

def complete_todo(project_id, todo_id)
  post("/buckets/#{project_id}/todos/#{todo_id}/completion.json")
end

#create_card(project_id, column_id, title:, content: nil, due_on: nil) ⇒ Object



215
216
217
218
219
220
# File 'app/services/basecamp/api_client.rb', line 215

def create_card(project_id, column_id, title:, content: nil, due_on: nil)
  body = { title: title }
  body[:content] = content if content
  body[:due_on] = due_on if due_on
  post("/buckets/#{project_id}/card_tables/lists/#{column_id}/cards.json", body)
end

#create_comment(project_id, recording_id, content:) ⇒ Object



258
259
260
# File 'app/services/basecamp/api_client.rb', line 258

def create_comment(project_id, recording_id, content:)
  post("/buckets/#{project_id}/recordings/#{recording_id}/comments.json", { content: content })
end

#create_document(project_id, vault_id, title:, content:, status: nil) ⇒ Object



348
349
350
351
352
# File 'app/services/basecamp/api_client.rb', line 348

def create_document(project_id, vault_id, title:, content:, status: nil)
  body = { title: title, content: content }
  body[:status] = status if status
  post("/buckets/#{project_id}/vaults/#{vault_id}/documents.json", body)
end

#create_message(project_id, message_board_id, subject:, status:, content: nil, category_id: nil, subscriptions: nil) ⇒ Object



173
174
175
176
177
178
179
# File 'app/services/basecamp/api_client.rb', line 173

def create_message(project_id, message_board_id, subject:, status:, content: nil, category_id: nil, subscriptions: nil)
  body = { subject: subject, status: status }
  body[:content] = content if content
  body[:category_id] = category_id if category_id
  body[:subscriptions] = subscriptions if subscriptions
  post("/buckets/#{project_id}/message_boards/#{message_board_id}/messages.json", body)
end

#create_project(name:, description: nil) ⇒ Object



51
52
53
54
55
# File 'app/services/basecamp/api_client.rb', line 51

def create_project(name:, description: nil)
  body = { name: name }
  body[:description] = description if description
  post('/projects.json', body)
end

#create_todo(project_id, todolist_id, content:, description: nil, assignee_ids: nil, due_on: nil, starts_on: nil) ⇒ Object



112
113
114
115
116
117
118
119
# File 'app/services/basecamp/api_client.rb', line 112

def create_todo(project_id, todolist_id, content:, description: nil, assignee_ids: nil, due_on: nil, starts_on: nil)
  body = { content: content }
  body[:description] = description if description
  body[:assignee_ids] = assignee_ids if assignee_ids
  body[:due_on] = due_on if due_on
  body[:starts_on] = starts_on if starts_on
  post("/buckets/#{project_id}/todolists/#{todolist_id}/todos.json", body)
end

#create_todo_group(project_id, todolist_id, name:, color: nil) ⇒ Object



145
146
147
148
149
# File 'app/services/basecamp/api_client.rb', line 145

def create_todo_group(project_id, todolist_id, name:, color: nil)
  body = { name: name }
  body[:color] = color if color
  post("/buckets/#{project_id}/todolists/#{todolist_id}/groups.json", body)
end

#create_todolist(project_id, todoset_id, name:, description: nil) ⇒ Object



89
90
91
92
93
# File 'app/services/basecamp/api_client.rb', line 89

def create_todolist(project_id, todoset_id, name:, description: nil)
  body = { name: name }
  body[:description] = description if description
  post("/buckets/#{project_id}/todosets/#{todoset_id}/todolists.json", body)
end

#create_upload(project_id, vault_id, attachable_sgid:, description: nil, base_name: nil) ⇒ Object



368
369
370
371
372
373
# File 'app/services/basecamp/api_client.rb', line 368

def create_upload(project_id, vault_id, attachable_sgid:, description: nil, base_name: nil)
  body = { attachable_sgid: attachable_sgid }
  body[:description] = description if description
  body[:base_name] = base_name if base_name
  post("/buckets/#{project_id}/vaults/#{vault_id}/uploads.json", body)
end

#document(project_id, document_id) ⇒ Object



344
345
346
# File 'app/services/basecamp/api_client.rb', line 344

def document(project_id, document_id)
  get("/buckets/#{project_id}/documents/#{document_id}.json")
end

#documents(project_id, vault_id) ⇒ Object

--- Documents ---



340
341
342
# File 'app/services/basecamp/api_client.rb', line 340

def documents(project_id, vault_id)
  get_paginated("/buckets/#{project_id}/vaults/#{vault_id}/documents.json")
end

#message(project_id, message_id) ⇒ Object



169
170
171
# File 'app/services/basecamp/api_client.rb', line 169

def message(project_id, message_id)
  get("/buckets/#{project_id}/messages/#{message_id}.json")
end

#message_board(project_id, message_board_id = nil) ⇒ Object

--- Message Board / Campfire ---



157
158
159
160
161
162
163
# File 'app/services/basecamp/api_client.rb', line 157

def message_board(project_id, message_board_id = nil)
  if message_board_id
    get("/buckets/#{project_id}/message_boards/#{message_board_id}.json")
  else
    get("/buckets/#{project_id}/message_board.json")
  end
end

#messages(project_id, message_board_id) ⇒ Object



165
166
167
# File 'app/services/basecamp/api_client.rb', line 165

def messages(project_id, message_board_id)
  get_paginated("/buckets/#{project_id}/message_boards/#{message_board_id}/messages.json")
end

#my_assignments(completed: false, group_by: 'bucket') ⇒ Object

Query tasks assigned to the current authenticated user.

For open assignments we use the reports endpoint, which returns pending to-dos
assigned to a person and matches Basecamp's assignment report behavior.
For completed assignments we fall back to /my/assignments.json.



299
300
301
302
303
304
305
306
# File 'app/services/basecamp/api_client.rb', line 299

def my_assignments(completed: false, group_by: 'bucket')
  if completed
    return get_paginated('/my/assignments.json', params: { completed: true })
  end

  person_id = my_profile['id']
  todo_assignments_for_person(person_id, group_by: group_by)
end

#my_profileObject



334
335
336
# File 'app/services/basecamp/api_client.rb', line 334

def my_profile
  get('/my/profile.json')
end

#overdue_todosObject



320
321
322
# File 'app/services/basecamp/api_client.rb', line 320

def overdue_todos
  get('/reports/todos/overdue.json')
end

#peopleObject

--- People ---



224
225
226
# File 'app/services/basecamp/api_client.rb', line 224

def people
  get_paginated('/people.json')
end

#person(person_id) ⇒ Object



228
229
230
# File 'app/services/basecamp/api_client.rb', line 228

def person(person_id)
  get("/people/#{person_id}.json")
end

#pin_recording(project_id, recording_id) ⇒ Object



189
190
191
# File 'app/services/basecamp/api_client.rb', line 189

def pin_recording(project_id, recording_id)
  post("/buckets/#{project_id}/recordings/#{recording_id}/pin.json")
end

#pingable_peopleObject



244
245
246
# File 'app/services/basecamp/api_client.rb', line 244

def pingable_people
  get('/circles/people.json')
end

#project(project_id) ⇒ Object



47
48
49
# File 'app/services/basecamp/api_client.rb', line 47

def project(project_id)
  get("/projects/#{project_id}.json")
end

#project_people(project_id) ⇒ Object



232
233
234
# File 'app/services/basecamp/api_client.rb', line 232

def project_people(project_id)
  get_paginated("/projects/#{project_id}/people.json")
end

#projects(status: nil) ⇒ Object

--- Project endpoints ---



41
42
43
44
45
# File 'app/services/basecamp/api_client.rb', line 41

def projects(status: nil)
  params = {}
  params[:status] = status if status.present?
  get_paginated('/projects.json', params: params.presence)
end

#reposition_todo(project_id, todo_id, position:, parent_id: nil) ⇒ Object



125
126
127
128
129
# File 'app/services/basecamp/api_client.rb', line 125

def reposition_todo(project_id, todo_id, position:, parent_id: nil)
  body = { position: position }
  body[:parent_id] = parent_id if parent_id
  put("/buckets/#{project_id}/todos/#{todo_id}/position.json", body)
end

#reposition_todo_group(project_id, group_id, position:) ⇒ Object



151
152
153
# File 'app/services/basecamp/api_client.rb', line 151

def reposition_todo_group(project_id, group_id, position:)
  put("/buckets/#{project_id}/todolists/groups/#{group_id}/position.json", { position: position })
end

#search(q:, type: nil, bucket_id: nil, creator_id: nil, file_type: nil, exclude_chat: nil, page: nil, per_page: nil, fetch_all: false) ⇒ Object



276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'app/services/basecamp/api_client.rb', line 276

def search(q:, type: nil, bucket_id: nil, creator_id: nil, file_type: nil, exclude_chat: nil, page: nil, per_page: nil, fetch_all: false)
  params = { q: q }
  params[:type] = type if type.present?
  params[:bucket_id] = bucket_id if bucket_id.present?
  params[:creator_id] = creator_id if creator_id.present?
  params[:file_type] = file_type if file_type.present?
  params[:exclude_chat] = exclude_chat unless exclude_chat.nil?
  params[:page] = page if page.present?
  params[:per_page] = per_page if per_page.present?
  if fetch_all
    get_paginated('/search.json', params: params)
  else
    get('/search.json', params: params)
  end
end

#search_metadataObject

--- Search ---



272
273
274
# File 'app/services/basecamp/api_client.rb', line 272

def 
  get('/searches/metadata.json')
end

#todo(project_id, todo_id) ⇒ Object



108
109
110
# File 'app/services/basecamp/api_client.rb', line 108

def todo(project_id, todo_id)
  get("/buckets/#{project_id}/todos/#{todo_id}.json")
end

#todo_assigneesObject

--- Reports ---



310
311
312
# File 'app/services/basecamp/api_client.rb', line 310

def todo_assignees
  get_paginated('/reports/todos/assigned.json')
end

#todo_assignments_for_person(person_id, group_by: 'bucket') ⇒ Object



314
315
316
317
318
# File 'app/services/basecamp/api_client.rb', line 314

def todo_assignments_for_person(person_id, group_by: 'bucket')
  params = {}
  params[:group_by] = group_by if group_by.present?
  get("/reports/todos/assigned/#{person_id}.json", params: params.presence)
end

#todo_groups(project_id, todolist_id, status: nil) ⇒ Object



139
140
141
142
143
# File 'app/services/basecamp/api_client.rb', line 139

def todo_groups(project_id, todolist_id, status: nil)
  params = {}
  params[:status] = status if status.present?
  get_paginated("/buckets/#{project_id}/todolists/#{todolist_id}/groups.json", params: params.presence)
end

#todolist(project_id, todolist_id) ⇒ Object



85
86
87
# File 'app/services/basecamp/api_client.rb', line 85

def todolist(project_id, todolist_id)
  get("/buckets/#{project_id}/todolists/#{todolist_id}.json")
end

#todolists(project_id) ⇒ Object



75
76
77
# File 'app/services/basecamp/api_client.rb', line 75

def todolists(project_id)
  get_paginated("/buckets/#{project_id}/todolists.json")
end

#todolists_in_set(project_id, todoset_id, status: nil) ⇒ Object



79
80
81
82
83
# File 'app/services/basecamp/api_client.rb', line 79

def todolists_in_set(project_id, todoset_id, status: nil)
  params = {}
  params[:status] = status if status.present?
  get_paginated("/buckets/#{project_id}/todosets/#{todoset_id}/todolists.json", params: params.presence)
end

#todos(project_id, todolist_id, status: nil, completed: nil) ⇒ Object



101
102
103
104
105
106
# File 'app/services/basecamp/api_client.rb', line 101

def todos(project_id, todolist_id, status: nil, completed: nil)
  params = {}
  params[:status] = status if status.present?
  params[:completed] = completed unless completed.nil?
  get_paginated("/buckets/#{project_id}/todolists/#{todolist_id}/todos.json", params: params.presence)
end

#todoset(project_id, todoset_id) ⇒ Object

--- Todo endpoints ---



71
72
73
# File 'app/services/basecamp/api_client.rb', line 71

def todoset(project_id, todoset_id)
  get("/buckets/#{project_id}/todosets/#{todoset_id}.json")
end

#trash_project(project_id) ⇒ Object



65
66
67
# File 'app/services/basecamp/api_client.rb', line 65

def trash_project(project_id)
  delete("/projects/#{project_id}.json")
end

#trash_recording(project_id, recording_id) ⇒ Object



266
267
268
# File 'app/services/basecamp/api_client.rb', line 266

def trash_recording(project_id, recording_id)
  post("/buckets/#{project_id}/recordings/#{recording_id}/status/trashed.json")
end

#uncomplete_todo(project_id, todo_id) ⇒ Object



135
136
137
# File 'app/services/basecamp/api_client.rb', line 135

def uncomplete_todo(project_id, todo_id)
  delete("/buckets/#{project_id}/todos/#{todo_id}/completion.json")
end

#unpin_recording(project_id, recording_id) ⇒ Object



193
194
195
# File 'app/services/basecamp/api_client.rb', line 193

def unpin_recording(project_id, recording_id)
  delete("/buckets/#{project_id}/recordings/#{recording_id}/pin.json")
end

#upcoming_schedule(window_starts_on:, window_ends_on:) ⇒ Object



324
325
326
327
328
329
330
331
332
# File 'app/services/basecamp/api_client.rb', line 324

def upcoming_schedule(window_starts_on:, window_ends_on:)
  get(
    '/reports/schedules/upcoming.json',
    params: {
      window_starts_on: window_starts_on,
      window_ends_on: window_ends_on
    }
  )
end

#update_comment(project_id, comment_id, content:) ⇒ Object



262
263
264
# File 'app/services/basecamp/api_client.rb', line 262

def update_comment(project_id, comment_id, content:)
  put("/buckets/#{project_id}/comments/#{comment_id}.json", { content: content })
end

#update_document(project_id, document_id, title:, content:) ⇒ Object



354
355
356
# File 'app/services/basecamp/api_client.rb', line 354

def update_document(project_id, document_id, title:, content:)
  put("/buckets/#{project_id}/documents/#{document_id}.json", { title: title, content: content })
end

#update_message(project_id, message_id, subject: nil, content: nil, category_id: nil) ⇒ Object



181
182
183
184
185
186
187
# File 'app/services/basecamp/api_client.rb', line 181

def update_message(project_id, message_id, subject: nil, content: nil, category_id: nil)
  body = {}
  body[:subject] = subject if subject
  body[:content] = content if content
  body[:category_id] = category_id if category_id
  put("/buckets/#{project_id}/messages/#{message_id}.json", body)
end

#update_project(project_id, name:, description: nil, admissions: nil, schedule_attributes: nil) ⇒ Object



57
58
59
60
61
62
63
# File 'app/services/basecamp/api_client.rb', line 57

def update_project(project_id, name:, description: nil, admissions: nil, schedule_attributes: nil)
  body = { name: name }
  body[:description] = description if description
  body[:admissions] = admissions if admissions
  body[:schedule_attributes] = schedule_attributes if schedule_attributes
  put("/projects/#{project_id}.json", body)
end

#update_project_people(project_id, grant: nil, revoke: nil, create: nil) ⇒ Object



236
237
238
239
240
241
242
# File 'app/services/basecamp/api_client.rb', line 236

def update_project_people(project_id, grant: nil, revoke: nil, create: nil)
  body = {}
  body[:grant] = grant if grant.present?
  body[:revoke] = revoke if revoke.present?
  body[:create] = create if create.present?
  put("/projects/#{project_id}/people/users.json", body)
end

#update_todo(project_id, todo_id, **attrs) ⇒ Object



121
122
123
# File 'app/services/basecamp/api_client.rb', line 121

def update_todo(project_id, todo_id, **attrs)
  put("/buckets/#{project_id}/todos/#{todo_id}.json", attrs.compact)
end

#update_todolist(project_id, todolist_id, name:, description: nil) ⇒ Object



95
96
97
98
99
# File 'app/services/basecamp/api_client.rb', line 95

def update_todolist(project_id, todolist_id, name:, description: nil)
  body = { name: name }
  body[:description] = description if description
  put("/buckets/#{project_id}/todolists/#{todolist_id}.json", body)
end

#update_upload(project_id, upload_id, description: nil, base_name: nil) ⇒ Object



375
376
377
378
379
380
# File 'app/services/basecamp/api_client.rb', line 375

def update_upload(project_id, upload_id, description: nil, base_name: nil)
  body = {}
  body[:description] = description if description
  body[:base_name] = base_name if base_name
  put("/buckets/#{project_id}/uploads/#{upload_id}.json", body)
end

#upload(project_id, upload_id) ⇒ Object



364
365
366
# File 'app/services/basecamp/api_client.rb', line 364

def upload(project_id, upload_id)
  get("/buckets/#{project_id}/uploads/#{upload_id}.json")
end

#uploads(project_id, vault_id) ⇒ Object

--- Uploads ---



360
361
362
# File 'app/services/basecamp/api_client.rb', line 360

def uploads(project_id, vault_id)
  get_paginated("/buckets/#{project_id}/vaults/#{vault_id}/uploads.json")
end