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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +54 -1
- data/lib/fopost/client.rb +5 -1
- data/lib/fopost/models.rb +277 -0
- data/lib/fopost/resources/ads.rb +194 -0
- data/lib/fopost/resources/inbox.rb +158 -0
- data/lib/fopost/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 39dcfa4fd395633ab79afe8b23b7888c9c76ad6b07d7bd86c874c56335647f48
|
|
4
|
+
data.tar.gz: 49ebacabb32eb4054db27d6bf69e14eae7f828a4071207c921b6dd9b5ef529ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 [
|
|
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
|
data/lib/fopost/version.rb
CHANGED
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.
|
|
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
|