fopost 0.1.0 → 0.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1845a9113bbfdb41688fd430794d43749707b835d82e2f0a4df826df43b4f1da
4
- data.tar.gz: '08c64a3e1385d13789b22b5924f8672bcffbcf89dd524da03e8f3b94806fefc3'
3
+ metadata.gz: 39dcfa4fd395633ab79afe8b23b7888c9c76ad6b07d7bd86c874c56335647f48
4
+ data.tar.gz: 49ebacabb32eb4054db27d6bf69e14eae7f828a4071207c921b6dd9b5ef529ad
5
5
  SHA512:
6
- metadata.gz: d151dc7b437cfeb9e3369c3455f5534c8a2a599a17084eb5b6cc5ba77d5bc2d53ffdf887abba1eb33836e04b34d9d60b04f9b7958c9263e2882056137d07d1d9
7
- data.tar.gz: 05f02730b6ab206ae44dbcaee5910253e10a28f8f6fd2eaa2f9f0d6ecae7c44e598716a4eff08e9f77565c528070dc9a8f68ea4075b59a7f5da93cf3af09487c
6
+ metadata.gz: c4e179a15c45093239bd90ec0664c5ce49a491e11a750b7152448dcfebac7edb7fd2538798789f9eb1859efa0bdf6e0d24bc35c9f1b782fdc68ee4dcc337cb4d
7
+ data.tar.gz: 82b98ae4e3eb9c9004454abf091c693dbe254a6d7983b990d57e9766bc760b169016f328284730b0292d0b6776d1b6e75da48204cd1e6da0fc16fc80d6e56164
data/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to this gem are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the gem follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.2.0] - 2026-09-19
8
+
9
+ ### Added
10
+
11
+ - `inbox` resource: list items, threads and conversations, unread count, accounts and
12
+ platforms, mark a thread read, refresh, update state, reply, hide, unhide, delete, and the
13
+ reply approvals (`inbox` scope).
14
+ - `ads` resource: ads, external ads, boostable posts, connections, sources, Meta authorization,
15
+ boost, create, refresh, set status, delete, audiences, targeting search, lead forms and leads
16
+ (`ads` scope; `boost`, `create`, `set_status` and `delete` also need `publish`).
17
+
7
18
  ## [0.1.0] - 2026-08-30
8
19
 
9
20
  Initial release.
@@ -15,4 +26,5 @@ Initial release.
15
26
  - Response models that accept either wire casing and keep unknown fields on `#raw`.
16
27
  - A pluggable transport, so the HTTP stack can be swapped or stubbed.
17
28
 
29
+ [0.2.0]: https://github.com/fopost/fopost-ruby/releases/tag/v0.2.0
18
30
  [0.1.0]: https://github.com/fopost/fopost-ruby/releases/tag/v0.1.0
data/README.md CHANGED
@@ -25,7 +25,7 @@ gem install fopost
25
25
 
26
26
  ## Get an API key
27
27
 
28
- Create a key at [app.fopost.com/api-keys](https://app.fopost.com/api-keys). The full API reference lives at [fopost.com/docs](https://fopost.com/docs).
28
+ Create a key at [fopost.com/dashboard/api-keys](https://fopost.com/dashboard/api-keys). The full API reference lives at [fopost.com/docs](https://fopost.com/docs).
29
29
 
30
30
  ## Quick start
31
31
 
@@ -152,6 +152,59 @@ repurposed = client.ai.repurpose_url(
152
152
 
153
153
  > **API keys reach `credits` and `generate_caption`.** `rewrite` and `repurpose_url` currently require a signed-in dashboard session and answer `401` to an API key. They are here so the surface is complete once the server opens them up.
154
154
 
155
+ ## Inbox
156
+
157
+ Comments, mentions and direct messages on connected accounts. Needs the `inbox` scope.
158
+
159
+ ```ruby
160
+ page = client.inbox.list(workspace_id: workspace.id, state: 'unread', sort: 'unanswered')
161
+ page.each { |item| puts "#{item.platform} #{item.author_handle}: #{item.text}" }
162
+
163
+ client.inbox.threads(workspace_id: workspace.id) # one row per post with comments
164
+ client.inbox.conversations(workspace_id: workspace.id) # one row per DM thread
165
+ client.inbox.unread_count(workspace_id: workspace.id)
166
+
167
+ client.inbox.reply(item.id, text: 'Thanks!')
168
+ client.inbox.update(item.id, state: 'snoozed', snoozed_until: Time.utc(2026, 9, 20, 9))
169
+ client.inbox.hide(item.id)
170
+ client.inbox.mark_thread_read(workspace_id: workspace.id, account_id: item.account.id,
171
+ post_external_id: item.post_external_id)
172
+ client.inbox.refresh(workspace_id: workspace.id)
173
+
174
+ client.inbox.approvals(workspace_id: workspace.id).each { |a| client.inbox.approve_reply(a.id) }
175
+ ```
176
+
177
+ ## Ads
178
+
179
+ Meta ads, audiences and lead forms. Every call needs the `ads` scope; `boost`, `create`, `set_status` and `delete` spend money and also need `publish`. A boost or ad starts paused unless you pass `paused: false`.
180
+
181
+ ```ruby
182
+ url = client.ads.authorize_meta(workspace_id: workspace.id) # finish the Meta login in a browser
183
+ source = client.ads.sources(workspace_id: workspace.id).first
184
+
185
+ ad = client.ads.boost(
186
+ workspace_id: workspace.id,
187
+ connection_id: source.connection_id,
188
+ ad_account_id: source.ad_accounts.first['id'],
189
+ post_id: post.id,
190
+ account_id: accounts.first.id,
191
+ name: 'Launch boost',
192
+ goal: 'engagement',
193
+ budget: { minor: 5000, type: 'daily' },
194
+ targeting: { countries: ['US'] }
195
+ )
196
+
197
+ client.ads.set_status(ad.id, workspace_id: workspace.id, status: 'active')
198
+ client.ads.refresh(ad.id, workspace_id: workspace.id).insights.impressions
199
+ client.ads.list(workspace_id: workspace.id)
200
+ client.ads.external(workspace_id: workspace.id) # ads made outside FoPost, read live
201
+
202
+ client.ads.audiences(connection_id: source.connection_id, ad_account_id: 'act_123')
203
+ client.ads.search_targeting(connection_id: source.connection_id, type: 'interest', q: 'coffee')
204
+ client.ads.lead_forms(workspace_id: workspace.id)
205
+ client.ads.leads('form_1', connection_id: source.connection_id, page_id: '42')
206
+ ```
207
+
155
208
  ## Errors
156
209
 
157
210
  Every non-2xx response raises. All of them are rescuable as `Fopost::Error`.
data/lib/fopost/client.rb CHANGED
@@ -4,7 +4,9 @@ require 'fopost/http/client'
4
4
  require 'fopost/models'
5
5
  require 'fopost/resources/base'
6
6
  require 'fopost/resources/accounts'
7
+ require 'fopost/resources/ads'
7
8
  require 'fopost/resources/ai'
9
+ require 'fopost/resources/inbox'
8
10
  require 'fopost/resources/labels'
9
11
  require 'fopost/resources/posts'
10
12
  require 'fopost/resources/workspaces'
@@ -21,7 +23,7 @@ module Fopost
21
23
  class Client
22
24
  DEFAULT_BASE_URL = HTTP::Client::DEFAULT_BASE_URL
23
25
 
24
- attr_reader :posts, :accounts, :workspaces, :labels, :ai
26
+ attr_reader :posts, :accounts, :workspaces, :labels, :ai, :inbox, :ads
25
27
 
26
28
  def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, timeout: HTTP::Client::DEFAULT_TIMEOUT,
27
29
  max_retries: HTTP::Client::DEFAULT_MAX_RETRIES, transport: nil, sleeper: nil)
@@ -45,6 +47,8 @@ module Fopost
45
47
  @workspaces = Resources::Workspaces.new(@http)
46
48
  @labels = Resources::Labels.new(@http)
47
49
  @ai = Resources::Ai.new(@http)
50
+ @inbox = Resources::Inbox.new(@http)
51
+ @ads = Resources::Ads.new(@http)
48
52
  end
49
53
 
50
54
  def base_url
data/lib/fopost/models.rb CHANGED
@@ -205,4 +205,281 @@ module Fopost
205
205
  attribute :posts, :hash
206
206
  attribute :credits, AiCredits
207
207
  end
208
+
209
+ # `page`, `per_page`, `total` on the inbox list endpoints.
210
+ class InboxPageMeta < Model
211
+ attribute :page
212
+ attribute :per_page
213
+ attribute :total
214
+ end
215
+
216
+ class InboxAccountRef < Model
217
+ attribute :id
218
+ attribute :platform
219
+ attribute :username
220
+ attribute :name
221
+ attribute :avatar
222
+ end
223
+
224
+ class InboxAttachment < Model
225
+ attribute :kind
226
+ attribute :name
227
+ attribute :width
228
+ attribute :height
229
+ attribute :link
230
+ attribute :url
231
+ attribute :preview_url
232
+ end
233
+
234
+ # The platform post an item sits under, whoever published it.
235
+ class InboxPostContext < Model
236
+ attribute :external_id
237
+ attribute :is_own
238
+ attribute :text
239
+ attribute :author_name
240
+ attribute :author_handle
241
+ attribute :author_avatar_url
242
+ attribute :thumbnail_url
243
+ attribute :permalink
244
+ attribute :published_at, :time
245
+ attribute :published, :hash
246
+ end
247
+
248
+ # A comment, mention or direct message on a connected account.
249
+ class InboxItem < Model
250
+ attribute :id
251
+ attribute :workspace_id
252
+ attribute :platform
253
+ attribute :type
254
+ attribute :state
255
+ attribute :direction
256
+ attribute :conversation_id
257
+ attribute :author_name
258
+ attribute :author_handle
259
+ attribute :author_avatar_url
260
+ attribute :text
261
+ attribute :attachments, [InboxAttachment]
262
+ attribute :permalink
263
+ attribute :post_external_id
264
+ attribute :parent_external_id
265
+ attribute :platform_created_at, :time
266
+ attribute :snoozed_until, :time
267
+ attribute :replied_at, :time
268
+ attribute :created_at, :time
269
+ attribute :can_reply
270
+ attribute :hidden
271
+ attribute :can_hide
272
+ attribute :can_delete
273
+ attribute :post, :hash
274
+ attribute :post_context, InboxPostContext
275
+ attribute :account, InboxAccountRef
276
+ end
277
+
278
+ # One platform post and the comments it has collected.
279
+ class InboxThread < Model
280
+ attribute :workspace_id
281
+ attribute :account_id
282
+ attribute :post_external_id
283
+ attribute :comment_count
284
+ attribute :unread_count
285
+ attribute :last_comment_at, :time
286
+ attribute :last_comment_text
287
+ attribute :last_comment_author
288
+ attribute :post, InboxPostContext
289
+ attribute :account, InboxAccountRef
290
+ end
291
+
292
+ # One direct-message thread.
293
+ class InboxConversation < Model
294
+ attribute :workspace_id
295
+ attribute :account_id
296
+ attribute :conversation_id
297
+ attribute :message_count
298
+ attribute :unread_count
299
+ attribute :last_message_at, :time
300
+ attribute :last_message_text
301
+ attribute :last_message_outbound
302
+ attribute :participant, :hash
303
+ attribute :account, InboxAccountRef
304
+ end
305
+
306
+ class InboxAccount < Model
307
+ attribute :id
308
+ attribute :workspace_id
309
+ attribute :platform
310
+ attribute :username
311
+ attribute :name
312
+ attribute :avatar
313
+ attribute :inbox_supported
314
+ attribute :pending_reason
315
+ attribute :dm_supported
316
+ attribute :dm_pending_reason
317
+ end
318
+
319
+ class InboxPlatform < Model
320
+ attribute :platform
321
+ attribute :comments
322
+ attribute :dms
323
+ end
324
+
325
+ # A drafted reply a person still has to send.
326
+ class InboxApproval < Model
327
+ attribute :id
328
+ attribute :workspace_id
329
+ attribute :source
330
+ attribute :reply
331
+ attribute :created_at, :time
332
+ attribute :item, :hash
333
+ end
334
+
335
+ class InboxReplyResult < Model
336
+ attribute :item, InboxItem
337
+ attribute :reply, :hash
338
+ end
339
+
340
+ class InboxRefreshResult < Model
341
+ attribute :accounts_polled
342
+ attribute :new_items
343
+ attribute :rate_limited
344
+ attribute :dm_reconnect
345
+ end
346
+
347
+ # Lifetime numbers from the last refresh. `spend_minor` is in the ad
348
+ # account currency, minor units.
349
+ class AdInsights < Model
350
+ attribute :impressions
351
+ attribute :reach
352
+ attribute :clicks
353
+ attribute :spend_minor
354
+ end
355
+
356
+ # A boost or standalone ad created through FoPost.
357
+ class Ad < Model
358
+ attribute :id
359
+ attribute :workspace_id
360
+ attribute :kind
361
+ attribute :name
362
+ attribute :goal
363
+ attribute :status
364
+ attribute :effective_status
365
+ attribute :connection_id
366
+ attribute :account_id
367
+ attribute :platform
368
+ attribute :ad_account_id
369
+ attribute :source_post_id
370
+ attribute :budget_minor
371
+ attribute :budget_type
372
+ attribute :currency
373
+ attribute :end_at, :time
374
+ attribute :targeting, :hash
375
+ attribute :creative, :hash
376
+ attribute :insights, AdInsights
377
+ attribute :insights_at, :time
378
+ attribute :last_error
379
+ attribute :created_at, :time
380
+ end
381
+
382
+ # An ad on a connected ad account that was made outside FoPost.
383
+ class ExternalAd < Model
384
+ attribute :id
385
+ attribute :name
386
+ attribute :effective_status
387
+ attribute :campaign_id
388
+ attribute :campaign_name
389
+ attribute :objective
390
+ attribute :budget_minor
391
+ attribute :budget_type
392
+ attribute :end_at, :time
393
+ attribute :created_at, :time
394
+ attribute :connection_id
395
+ attribute :ad_account_id
396
+ attribute :currency
397
+ attribute :workspace_id
398
+ end
399
+
400
+ class AdConnection < Model
401
+ attribute :id
402
+ attribute :provider
403
+ attribute :auth_type
404
+ attribute :name
405
+ attribute :business_id
406
+ attribute :created_at, :time
407
+ attribute :workspace_id
408
+ end
409
+
410
+ # A connection with the ad accounts and Pages its grant reaches.
411
+ class AdSource < Model
412
+ attribute :connection_id
413
+ attribute :name
414
+ attribute :workspace_id
415
+ attribute :ad_accounts
416
+ attribute :pages
417
+ attribute :error
418
+ end
419
+
420
+ class BoostablePost < Model
421
+ attribute :id
422
+ attribute :workspace_id
423
+ attribute :text
424
+ attribute :thumbnail_url
425
+ attribute :deliveries
426
+ end
427
+
428
+ class Audience < Model
429
+ attribute :id
430
+ attribute :name
431
+ attribute :subtype
432
+ attribute :description
433
+ attribute :size_lower
434
+ attribute :size_upper
435
+ attribute :delivery_status
436
+ attribute :created_at
437
+ end
438
+
439
+ class AudiencesResult < Model
440
+ attribute :audiences, [Audience]
441
+ attribute :pixels
442
+ attribute :workspace_id
443
+ end
444
+
445
+ class TargetingOption < Model
446
+ attribute :id
447
+ attribute :name
448
+ attribute :detail
449
+ end
450
+
451
+ class LeadForm < Model
452
+ attribute :id
453
+ attribute :name
454
+ attribute :status
455
+ attribute :leads_count
456
+ attribute :created_at
457
+ attribute :questions
458
+ end
459
+
460
+ class LeadFormSource < Model
461
+ attribute :connection_id
462
+ attribute :connection_name
463
+ attribute :page_id
464
+ attribute :page_name
465
+ attribute :forms, [LeadForm]
466
+ attribute :error
467
+ attribute :workspace_id
468
+ end
469
+
470
+ class Lead < Model
471
+ attribute :id
472
+ attribute :created_at
473
+ attribute :fields
474
+ attribute :ad_name
475
+ attribute :campaign_name
476
+ attribute :platform
477
+ attribute :is_organic
478
+ end
479
+
480
+ # One page of leads; pass `next_cursor` back as `after:` for the next.
481
+ class LeadsPage < Model
482
+ attribute :leads, [Lead]
483
+ attribute :next_cursor
484
+ end
208
485
  end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fopost
4
+ module Resources
5
+ # `client.ads` — Meta ads, audiences and lead forms.
6
+ #
7
+ # Every method needs the `ads` scope. {#boost}, {#create}, {#set_status}
8
+ # and {#delete} spend money and also need `publish`.
9
+ class Ads < Base
10
+ # Boosts and ads created through FoPost, with insights from their last refresh.
11
+ def list(workspace_id: nil)
12
+ parse_list(Ad, unwrap(http.get('/ads', { 'workspace_id' => workspace_id })))
13
+ end
14
+
15
+ # Ads on the connected ad accounts that were made elsewhere. Read live, never stored.
16
+ def external(workspace_id: nil)
17
+ parse_list(ExternalAd, unwrap(http.get('/ads/external', { 'workspace_id' => workspace_id })))
18
+ end
19
+
20
+ def boostable(workspace_id: nil)
21
+ parse_list(BoostablePost, unwrap(http.get('/ads/boostable', { 'workspace_id' => workspace_id })))
22
+ end
23
+
24
+ def connections(workspace_id: nil)
25
+ parse_list(AdConnection, unwrap(http.get('/ads/connections', { 'workspace_id' => workspace_id })))
26
+ end
27
+
28
+ # Each connection with the ad accounts and Pages its grant reaches.
29
+ def sources(workspace_id: nil)
30
+ parse_list(AdSource, unwrap(http.get('/ads/sources', { 'workspace_id' => workspace_id })))
31
+ end
32
+
33
+ # The Meta login URL; the caller finishes it in their own browser.
34
+ # `method` is "business" (default) or "user".
35
+ def authorize_meta(workspace_id:, method: nil, return_to: nil)
36
+ body = compact_nil('workspaceId' => workspace_id, 'method' => method, 'returnTo' => return_to)
37
+ result = unwrap(http.post('/ads/connections/meta/authorize', body))
38
+ url = result.is_a?(Hash) ? result['url'] : nil
39
+ url.nil? ? '' : url.to_s
40
+ end
41
+
42
+ # Also deletes every ad record created through the connection.
43
+ def delete_connection(connection_id, workspace_id:)
44
+ http.request(:delete, "/ads/connections/#{connection_id}", params: { 'workspace_id' => workspace_id })
45
+ nil
46
+ end
47
+
48
+ # Promote a post FoPost already published. Needs `ads` and `publish`.
49
+ # The boost starts paused unless `paused: false`.
50
+ def boost(workspace_id:, connection_id:, ad_account_id:, post_id:, account_id:, name:, goal:,
51
+ budget:, targeting:, paused: nil)
52
+ body = {
53
+ 'workspaceId' => workspace_id,
54
+ 'connectionId' => connection_id,
55
+ 'adAccountId' => ad_account_id,
56
+ 'postId' => post_id,
57
+ 'accountId' => account_id,
58
+ 'name' => name,
59
+ 'goal' => goal,
60
+ 'budget' => stringify(budget),
61
+ 'targeting' => stringify(targeting)
62
+ }
63
+ body['paused'] = paused unless paused.nil?
64
+ Ad.new(unwrap(http.post('/ads/boost', body)))
65
+ end
66
+
67
+ # Create a standalone ad from a creative. Needs `ads` and `publish`.
68
+ # The ad starts paused unless `paused: false`.
69
+ def create(workspace_id:, connection_id:, ad_account_id:, page_id:, name:, goal:, budget:, targeting:,
70
+ text:, headline: nil, destination_url: nil, media_url: nil, paused: nil)
71
+ body = {
72
+ 'workspaceId' => workspace_id,
73
+ 'connectionId' => connection_id,
74
+ 'adAccountId' => ad_account_id,
75
+ 'pageId' => page_id,
76
+ 'name' => name,
77
+ 'goal' => goal,
78
+ 'budget' => stringify(budget),
79
+ 'targeting' => stringify(targeting),
80
+ 'text' => text
81
+ }
82
+ body.merge!(
83
+ compact_nil(
84
+ 'headline' => headline,
85
+ 'destinationUrl' => destination_url,
86
+ 'mediaUrl' => media_url,
87
+ 'paused' => paused
88
+ )
89
+ )
90
+ Ad.new(unwrap(http.post('/ads', body)))
91
+ end
92
+
93
+ # Read the delivery status and lifetime insights from Meta.
94
+ def refresh(ad_id, workspace_id:)
95
+ Ad.new(unwrap(http.request(:post, "/ads/#{ad_id}/refresh", params: { 'workspace_id' => workspace_id })))
96
+ end
97
+
98
+ # `status` is "active" or "paused". Needs `ads` and `publish`.
99
+ def set_status(ad_id, workspace_id:, status:)
100
+ Ad.new(
101
+ unwrap(
102
+ http.request(:patch, "/ads/#{ad_id}", json: { 'status' => status },
103
+ params: { 'workspace_id' => workspace_id })
104
+ )
105
+ )
106
+ end
107
+
108
+ # End delivery and delete the ad on Meta as well as here. Needs `ads` and `publish`.
109
+ def delete(ad_id, workspace_id:)
110
+ http.request(:delete, "/ads/#{ad_id}", params: { 'workspace_id' => workspace_id })
111
+ nil
112
+ end
113
+
114
+ def audiences(connection_id:, ad_account_id:, workspace_id: nil)
115
+ AudiencesResult.new(
116
+ unwrap(
117
+ http.get(
118
+ '/ads/audiences',
119
+ { 'workspace_id' => workspace_id, 'connection_id' => connection_id, 'ad_account_id' => ad_account_id }
120
+ )
121
+ )
122
+ )
123
+ end
124
+
125
+ # `spec` carries a `subtype` of CUSTOM, LOOKALIKE or WEBSITE. Answers `{ id, added }`.
126
+ def create_audience(workspace_id:, connection_id:, ad_account_id:, name:, spec:, description: nil)
127
+ body = {
128
+ 'workspaceId' => workspace_id,
129
+ 'connectionId' => connection_id,
130
+ 'adAccountId' => ad_account_id,
131
+ 'name' => name,
132
+ 'spec' => stringify(spec)
133
+ }
134
+ body['description'] = description unless description.nil?
135
+ as_hash(unwrap(http.post('/ads/audiences', body)))
136
+ end
137
+
138
+ # Locations, interests, behaviours and income brackets as Meta names them.
139
+ # `type` is country, region, city, zip, metro, interest, behavior or income.
140
+ def search_targeting(connection_id:, type:, q: nil, workspace_id: nil)
141
+ parse_list(
142
+ TargetingOption,
143
+ unwrap(
144
+ http.get(
145
+ '/ads/targeting/search',
146
+ { 'workspace_id' => workspace_id, 'connection_id' => connection_id, 'type' => type, 'q' => q }
147
+ )
148
+ )
149
+ )
150
+ end
151
+
152
+ def lead_forms(workspace_id: nil)
153
+ parse_list(LeadFormSource, unwrap(http.get('/ads/lead-forms', { 'workspace_id' => workspace_id })))
154
+ end
155
+
156
+ # Create an Instant Form on the Page. Returns its id.
157
+ def create_lead_form(workspace_id:, connection_id:, page_id:, name:, questions:, privacy_policy_url:,
158
+ thank_you_message:, follow_up_url: nil)
159
+ body = {
160
+ 'workspaceId' => workspace_id,
161
+ 'connectionId' => connection_id,
162
+ 'pageId' => page_id,
163
+ 'name' => name,
164
+ 'questions' => questions.to_a,
165
+ 'privacyPolicyUrl' => privacy_policy_url,
166
+ 'thankYouMessage' => thank_you_message
167
+ }
168
+ body['followUpUrl'] = follow_up_url unless follow_up_url.nil?
169
+ result = unwrap(http.post('/ads/lead-forms', body))
170
+ id = result.is_a?(Hash) ? result['id'] : nil
171
+ id.nil? ? '' : id.to_s
172
+ end
173
+
174
+ # One page of leads; pass `next_cursor` back as `after:` for the next.
175
+ def leads(form_id, connection_id:, page_id:, after: nil, workspace_id: nil)
176
+ LeadsPage.new(
177
+ unwrap(
178
+ http.get(
179
+ "/ads/lead-forms/#{form_id}/leads",
180
+ { 'workspace_id' => workspace_id, 'connection_id' => connection_id, 'page_id' => page_id,
181
+ 'after' => after }
182
+ )
183
+ )
184
+ )
185
+ end
186
+
187
+ private
188
+
189
+ def stringify(hash)
190
+ hash.to_h.transform_keys(&:to_s)
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fopost
4
+ module Resources
5
+ # `client.inbox` — comments, mentions and direct messages on connected
6
+ # accounts. Every method needs the `inbox` scope.
7
+ class Inbox < Base
8
+ # One page of items, newest first. The result is Enumerable over its items.
9
+ #
10
+ # `type` is comment, mention or dm; `state` is unread, read, resolved or
11
+ # snoozed; `sort` is newest, oldest or unanswered.
12
+ def list(workspace_id: nil, type: nil, state: nil, platform: nil, account_id: nil, post_id: nil,
13
+ post_external_id: nil, conversation_id: nil, direction: nil, q: nil, sort: nil,
14
+ page: 1, per_page: 25)
15
+ body = http.get(
16
+ '/inbox',
17
+ {
18
+ 'workspace_id' => workspace_id,
19
+ 'type' => type,
20
+ 'state' => state,
21
+ 'platform' => platform,
22
+ 'account_id' => account_id,
23
+ 'post_id' => post_id,
24
+ 'post_external_id' => post_external_id,
25
+ 'conversation_id' => conversation_id,
26
+ 'direction' => direction,
27
+ 'q' => q,
28
+ 'sort' => sort,
29
+ 'page' => page,
30
+ 'per_page' => per_page
31
+ }
32
+ )
33
+ page_of(InboxItem, body)
34
+ end
35
+
36
+ # One row per post with comments; `kind: 'mentions'` for posts the
37
+ # account was tagged in.
38
+ def threads(workspace_id: nil, kind: nil, platform: nil, account_id: nil, state: nil, q: nil,
39
+ sort: nil, page: 1, per_page: 25)
40
+ body = http.get(
41
+ '/inbox/posts',
42
+ {
43
+ 'workspace_id' => workspace_id,
44
+ 'kind' => kind,
45
+ 'platform' => platform,
46
+ 'account_id' => account_id,
47
+ 'state' => state,
48
+ 'q' => q,
49
+ 'sort' => sort,
50
+ 'page' => page,
51
+ 'per_page' => per_page
52
+ }
53
+ )
54
+ page_of(InboxThread, body)
55
+ end
56
+
57
+ # One row per DM thread, latest first.
58
+ def conversations(workspace_id: nil, platform: nil, account_id: nil, state: nil, q: nil, sort: nil,
59
+ page: 1, per_page: 25)
60
+ body = http.get(
61
+ '/inbox/conversations',
62
+ {
63
+ 'workspace_id' => workspace_id,
64
+ 'platform' => platform,
65
+ 'account_id' => account_id,
66
+ 'state' => state,
67
+ 'q' => q,
68
+ 'sort' => sort,
69
+ 'page' => page,
70
+ 'per_page' => per_page
71
+ }
72
+ )
73
+ page_of(InboxConversation, body)
74
+ end
75
+
76
+ def unread_count(workspace_id: nil)
77
+ body = http.get('/inbox/unread-count', { 'workspace_id' => workspace_id })
78
+ count = body.is_a?(Hash) ? body['count'] : nil
79
+ count.is_a?(Integer) ? count : 0
80
+ end
81
+
82
+ # Every active account, flagged with whether comments and DMs can be read for it.
83
+ def accounts(workspace_id: nil)
84
+ parse_list(InboxAccount, unwrap(http.get('/inbox/accounts', { 'workspace_id' => workspace_id })))
85
+ end
86
+
87
+ def platforms
88
+ parse_list(InboxPlatform, unwrap(http.get('/inbox/platforms')))
89
+ end
90
+
91
+ # Mark a whole comment thread or DM thread read. Returns how many items changed.
92
+ def mark_thread_read(workspace_id:, account_id:, post_external_id: nil, conversation_id: nil)
93
+ body = compact_nil(
94
+ 'workspace_id' => workspace_id,
95
+ 'account_id' => account_id,
96
+ 'post_external_id' => post_external_id,
97
+ 'conversation_id' => conversation_id
98
+ )
99
+ result = unwrap(http.post('/inbox/read', body))
100
+ updated = result.is_a?(Hash) ? result['updated'] : nil
101
+ updated.is_a?(Integer) ? updated : 0
102
+ end
103
+
104
+ # Poll every inbox-capable account in the workspace now.
105
+ def refresh(workspace_id:)
106
+ InboxRefreshResult.new(unwrap(http.post('/inbox/refresh', { 'workspace_id' => workspace_id })))
107
+ end
108
+
109
+ # Set the item's state to unread, read, resolved or snoozed. A snooze
110
+ # needs `snoozed_until` in the future.
111
+ def update(item_id, state:, snoozed_until: nil)
112
+ body = compact_nil('state' => state, 'snoozedUntil' => iso8601(snoozed_until))
113
+ InboxItem.new(unwrap(http.request(:patch, "/inbox/#{item_id}", json: body)))
114
+ end
115
+
116
+ # Send the reply on the platform as the connected account.
117
+ def reply(item_id, text:)
118
+ InboxReplyResult.new(unwrap(http.post("/inbox/#{item_id}/reply", { 'text' => text })))
119
+ end
120
+
121
+ def hide(item_id)
122
+ InboxItem.new(unwrap(http.post("/inbox/#{item_id}/hide")))
123
+ end
124
+
125
+ def unhide(item_id)
126
+ InboxItem.new(unwrap(http.post("/inbox/#{item_id}/unhide")))
127
+ end
128
+
129
+ # Delete the comment on the platform.
130
+ def delete(item_id)
131
+ http.delete("/inbox/#{item_id}")
132
+ nil
133
+ end
134
+
135
+ # Replies an automation or the agent drafted that a person still has to send.
136
+ def approvals(workspace_id: nil)
137
+ parse_list(InboxApproval, unwrap(http.get('/inbox/approvals', { 'workspace_id' => workspace_id })))
138
+ end
139
+
140
+ # Send the draft, or `text` in its place.
141
+ def approve_reply(approval_id, text: nil)
142
+ as_hash(unwrap(http.post("/inbox/approvals/#{approval_id}/approve", compact_nil('text' => text))))
143
+ end
144
+
145
+ def reject_reply(approval_id)
146
+ as_hash(unwrap(http.post("/inbox/approvals/#{approval_id}/reject")))
147
+ end
148
+
149
+ private
150
+
151
+ def page_of(model, body)
152
+ items = parse_list(model, body.is_a?(Hash) ? body['data'] : body)
153
+ raw_meta = body.is_a?(Hash) ? body['meta'] : nil
154
+ Page.new(items: items, meta: InboxPageMeta.new(raw_meta.is_a?(Hash) ? raw_meta : {}))
155
+ end
156
+ end
157
+ end
158
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fopost
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fopost
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - FoPost
@@ -30,8 +30,10 @@ files:
30
30
  - lib/fopost/models.rb
31
31
  - lib/fopost/platforms.rb
32
32
  - lib/fopost/resources/accounts.rb
33
+ - lib/fopost/resources/ads.rb
33
34
  - lib/fopost/resources/ai.rb
34
35
  - lib/fopost/resources/base.rb
36
+ - lib/fopost/resources/inbox.rb
35
37
  - lib/fopost/resources/labels.rb
36
38
  - lib/fopost/resources/posts.rb
37
39
  - lib/fopost/resources/workspaces.rb