posthubify 0.1.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.
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Posthubify
4
+ # Ads lifecycle (Node sdk .ads): accounts, campaigns, audiences,
5
+ # lead-gen, conversions (CAPI), targeting, ad-set/ad hierarchy, comments, catalogs.
6
+ class AdsResource
7
+ def initialize(http)
8
+ @http = http
9
+ end
10
+
11
+ # Connected ad accounts.
12
+ def accounts
13
+ @http.data('GET', '/ads/accounts')
14
+ end
15
+
16
+ # ── Headless ADS OAuth connect (D8 cp4) — tempToken start/status. ──
17
+ # Start headless ADS OAuth → {tempToken, authUrl}: open authUrl in a browser, then poll status.
18
+ def connect_oauth_start(platform, profile_id: nil, label: nil)
19
+ @http.data('POST', '/ads/connect/oauth/start', body: { 'platform' => platform, 'profileId' => profile_id, 'label' => label })
20
+ end
21
+
22
+ # Headless ADS OAuth status (poll): pending | connected (+account) | error.
23
+ def connect_oauth_status(temp_token)
24
+ @http.data('GET', '/ads/connect/oauth/status', query: { 'tempToken' => temp_token })
25
+ end
26
+
27
+ # This ad account's platform capabilities (pre-discovery instead of trial-and-error).
28
+ def capabilities(id)
29
+ @http.data('GET', "/ads/accounts/#{id}/capabilities")
30
+ end
31
+
32
+ # Platform-side ad accounts (one login → multiple accounts; for select).
33
+ def customers(id)
34
+ @http.data('GET', "/ads/accounts/#{id}/customers")
35
+ end
36
+
37
+ # Select the active ad account for this login.
38
+ def select(id, ad_account_id)
39
+ @http.data('POST', "/ads/accounts/#{id}/select", body: { 'adAccountId' => ad_account_id })
40
+ end
41
+
42
+ # Billing/publishing readiness — check before promote.
43
+ def readiness(id)
44
+ @http.data('GET', "/ads/accounts/#{id}/readiness")
45
+ end
46
+
47
+ # List campaigns (optional status filter).
48
+ def campaigns(id, status: nil)
49
+ @http.data('GET', "/ads/accounts/#{id}/campaigns", query: { 'status' => status })
50
+ end
51
+
52
+ # Create a campaign. input is a camelCase-keyed Hash (name/budget/budgetType/objective/status).
53
+ def create_campaign(id, input, idempotency_key: nil)
54
+ @http.data('POST', "/ads/accounts/#{id}/campaigns", body: input, idempotency_key: idempotency_key)
55
+ end
56
+
57
+ # Single campaign status (enabled/paused).
58
+ def set_campaign_status(id, campaign_id, status)
59
+ @http.data('POST', "/ads/accounts/#{id}/campaigns/#{campaign_id}/status", body: { 'status' => status })
60
+ end
61
+
62
+ # Bulk status (≤50 campaigns) — returns a per-campaign ok/error result.
63
+ def bulk_campaign_status(id, campaign_ids, status)
64
+ @http.data('POST', "/ads/accounts/#{id}/campaigns/bulk-status", body: { 'campaignIds' => campaign_ids, 'status' => status })
65
+ end
66
+
67
+ # Duplicate a campaign — the copy starts PAUSED (to avoid accidental spend).
68
+ def duplicate_campaign(id, campaign_id, name = nil)
69
+ body = name ? { 'name' => name } : {}
70
+ @http.data('POST', "/ads/accounts/#{id}/campaigns/#{campaign_id}/duplicate", body: body)
71
+ end
72
+
73
+ # Delete a campaign.
74
+ def delete_campaign(id, campaign_id)
75
+ @http.data('DELETE', "/ads/accounts/#{id}/campaigns/#{campaign_id}")
76
+ end
77
+
78
+ # Campaign's daily performance timeline (from report rows).
79
+ def campaign_timeline(id, campaign_id, since: nil, until_: nil)
80
+ @http.data('GET', "/ads/accounts/#{id}/campaigns/#{campaign_id}/timeline", query: { 'since' => since, 'until' => until_ })
81
+ end
82
+
83
+ # Account report rows (optional date range).
84
+ def report(id, since: nil, until_: nil)
85
+ @http.data('GET', "/ads/accounts/#{id}/report", query: { 'since' => since, 'until' => until_ })
86
+ end
87
+
88
+ # Creative targets (pages/boards).
89
+ def creative_targets(id)
90
+ @http.data('GET', "/ads/accounts/#{id}/creative-targets")
91
+ end
92
+
93
+ # Create an ad with a creative. mediaUrl may only come from your own media store (SSRF protection).
94
+ def promote(id, input, idempotency_key: nil)
95
+ @http.data('POST', "/ads/accounts/#{id}/promote", body: input, idempotency_key: idempotency_key)
96
+ end
97
+
98
+ # ---- Audiences / Lead-gen / Conversions / Pixels / Targeting (Meta ilk) ----
99
+
100
+ # Custom audiences.
101
+ def audiences(id)
102
+ @http.data('GET', "/ads/accounts/#{id}/audiences")
103
+ end
104
+
105
+ # Create an audience (name/description).
106
+ def create_audience(id, input)
107
+ @http.data('POST', "/ads/accounts/#{id}/audiences", body: input)
108
+ end
109
+
110
+ # Delete an audience.
111
+ def delete_audience(id, audience_id)
112
+ @http.data('DELETE', "/ads/accounts/#{id}/audiences/#{audience_id}")
113
+ end
114
+
115
+ # Upload members to an audience (≤10k/request). Raw emails/phones → server SHA-256; if pre-hashed, use the hashed* fields.
116
+ def upload_audience_users(id, audience_id, input)
117
+ @http.data('POST', "/ads/accounts/#{id}/audiences/#{audience_id}/users", body: input)
118
+ end
119
+
120
+ # Lead-gen forms.
121
+ def lead_forms(id)
122
+ @http.data('GET', "/ads/accounts/#{id}/lead-forms")
123
+ end
124
+
125
+ # A form's lead records (optional limit).
126
+ def leads(id, form_id, limit: nil)
127
+ @http.data('GET', "/ads/accounts/#{id}/lead-forms/#{form_id}/leads", query: { 'limit' => limit })
128
+ end
129
+
130
+ # Conversion pixels.
131
+ def pixels(id)
132
+ @http.data('GET', "/ads/accounts/#{id}/pixels")
133
+ end
134
+
135
+ # CAPI relay (≤1000 events) — raw email/phone is hashed on the server; use testEventCode for a safe trial.
136
+ def send_conversions(id, input)
137
+ @http.data('POST', "/ads/accounts/#{id}/conversions", body: input)
138
+ end
139
+
140
+ # Interest targeting search.
141
+ def search_interests(id, q)
142
+ @http.data('GET', "/ads/accounts/#{id}/targeting/interests", query: { 'q' => q })
143
+ end
144
+
145
+ # Reach estimate (targeting is a camelCase-keyed Hash).
146
+ def reach_estimate(id, targeting)
147
+ @http.data('POST', "/ads/accounts/#{id}/reach-estimate", body: targeting)
148
+ end
149
+
150
+ # ---- Campaign update + ad-set/ad hierarchy (C-T3) ----
151
+
152
+ # Update a campaign (patch: name/budget/budgetType/bidAmount/status).
153
+ def update_campaign(id, campaign_id, patch)
154
+ @http.data('PATCH', "/ads/accounts/#{id}/campaigns/#{campaign_id}", body: patch)
155
+ end
156
+
157
+ # A campaign's ad sets.
158
+ def ad_sets(id, campaign_id)
159
+ @http.data('GET', "/ads/accounts/#{id}/campaigns/#{campaign_id}/adsets")
160
+ end
161
+
162
+ # Campaign → ad sets → ads hierarchy (single call).
163
+ def campaign_tree(id, campaign_id)
164
+ @http.data('GET', "/ads/accounts/#{id}/campaigns/#{campaign_id}/tree")
165
+ end
166
+
167
+ # An ad set's ads.
168
+ def ads(id, ad_set_id)
169
+ @http.data('GET', "/ads/accounts/#{id}/adsets/#{ad_set_id}/ads")
170
+ end
171
+
172
+ # Ad set status (enabled/paused).
173
+ def set_ad_set_status(id, ad_set_id, status)
174
+ @http.data('POST', "/ads/accounts/#{id}/adsets/#{ad_set_id}/status", body: { 'status' => status })
175
+ end
176
+
177
+ # Delete an ad set.
178
+ def delete_ad_set(id, ad_set_id)
179
+ @http.data('DELETE', "/ads/accounts/#{id}/adsets/#{ad_set_id}")
180
+ end
181
+
182
+ # Ad status (enabled/paused).
183
+ def set_ad_status(id, ad_id, status)
184
+ @http.data('POST', "/ads/accounts/#{id}/ads/#{ad_id}/status", body: { 'status' => status })
185
+ end
186
+
187
+ # Delete an ad.
188
+ def delete_ad(id, ad_id)
189
+ @http.data('DELETE', "/ads/accounts/#{id}/ads/#{ad_id}")
190
+ end
191
+
192
+ # ---- Ads-comment moderation (C-T4) ----
193
+
194
+ # An ad's comments.
195
+ def ad_comments(id, ad_id)
196
+ @http.data('GET', "/ads/accounts/#{id}/ads/#{ad_id}/comments")
197
+ end
198
+
199
+ # Reply to an ad comment.
200
+ def reply_ad_comment(id, comment_id, message)
201
+ @http.data('POST', "/ads/accounts/#{id}/comments/#{comment_id}/reply", body: { 'message' => message })
202
+ end
203
+
204
+ # Hide/show an ad comment (default hide).
205
+ def hide_ad_comment(id, comment_id, hidden = true)
206
+ @http.data('POST', "/ads/accounts/#{id}/comments/#{comment_id}/hide", body: { 'hidden' => hidden })
207
+ end
208
+
209
+ # Delete an ad comment.
210
+ def delete_ad_comment(id, comment_id)
211
+ @http.data('DELETE', "/ads/accounts/#{id}/comments/#{comment_id}")
212
+ end
213
+
214
+ # ---- Catalogs (C-T5) ----
215
+
216
+ # Product catalogs.
217
+ def catalogs(id)
218
+ @http.data('GET', "/ads/accounts/#{id}/catalogs")
219
+ end
220
+
221
+ # A catalog's product sets.
222
+ def product_sets(id, catalog_id)
223
+ @http.data('GET', "/ads/accounts/#{id}/catalogs/#{catalog_id}/product-sets")
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Posthubify
4
+ # Derived insights (Node sdk .insights) — from stored history, no live platform call.
5
+ class InsightsResource
6
+ def initialize(http)
7
+ @http = http
8
+ end
9
+
10
+ # Best posting times (slots sorted by average engagement; dayOfWeek 1=Mon..7=Sun, UTC).
11
+ def best_time(days: nil, platform: nil, account_id: nil)
12
+ @http.data('GET', '/analytics/best-time',
13
+ query: { 'days' => days, 'platform' => platform, 'accountId' => account_id })
14
+ end
15
+
16
+ # Content lifetime curve (day buckets; avgPctOfFinal = average % reached by end of bucket).
17
+ def content_decay(days: nil, platform: nil, account_id: nil)
18
+ @http.data('GET', '/analytics/content-decay',
19
+ query: { 'days' => days, 'platform' => platform, 'accountId' => account_id })
20
+ end
21
+
22
+ # Weekly posting frequency ↔ engagement correlation (Pearson; <2 weeks → null).
23
+ def posting_frequency(days: nil, platform: nil, account_id: nil)
24
+ @http.data('GET', '/analytics/posting-frequency',
25
+ query: { 'days' => days, 'platform' => platform, 'accountId' => account_id })
26
+ end
27
+
28
+ # Daily metric timeline for a post (cumulative + delta; post_id = platform post id).
29
+ def post_timeline(post_id, days: nil)
30
+ @http.data('GET', "/analytics/posts/#{post_id}/timeline", query: { 'days' => days })
31
+ end
32
+ end
33
+
34
+ # Platform-specific analytics (Node sdk .platformAnalytics) — live data via the account connector.
35
+ class PlatformAnalyticsResource
36
+ def initialize(http)
37
+ @http = http
38
+ end
39
+
40
+ # Instagram daily follower history (from our own snapshot store; Meta does not provide a historical series).
41
+ def instagram_follower_history(account_id:, days: nil)
42
+ @http.data('GET', '/analytics/instagram/follower-history',
43
+ query: { 'accountId' => account_id, 'days' => days })
44
+ end
45
+
46
+ # Facebook Page insights (current metric names as of November 2025).
47
+ def facebook_page_insights(account_id)
48
+ @http.data('GET', '/analytics/facebook/page-insights', query: { 'accountId' => account_id })
49
+ end
50
+
51
+ # Instagram account insights (reach/views/accounts_engaged/total_interactions…).
52
+ def instagram_insights(account_id)
53
+ @http.data('GET', '/analytics/instagram/insights', query: { 'accountId' => account_id })
54
+ end
55
+
56
+ # Instagram audience demographics (age/gender/country/city; Meta requires min 100 followers).
57
+ def instagram_demographics(account_id)
58
+ @http.data('GET', '/analytics/instagram/demographics', query: { 'accountId' => account_id })
59
+ end
60
+
61
+ # Active Instagram stories (24-hour window).
62
+ def instagram_stories(account_id)
63
+ @http.data('GET', '/analytics/instagram/stories', query: { 'accountId' => account_id })
64
+ end
65
+
66
+ # Story metrics + navigation breakdown (tapsForward/tapsBack/exits/swipesForward).
67
+ def instagram_story_insights(account_id, story_id)
68
+ @http.data('GET', "/analytics/instagram/stories/#{story_id}/insights",
69
+ query: { 'accountId' => account_id })
70
+ end
71
+
72
+ # TikTok account insights (follower/like/video counts — current totals).
73
+ def tiktok_account_insights(account_id)
74
+ @http.data('GET', '/analytics/tiktok/account-insights', query: { 'accountId' => account_id })
75
+ end
76
+
77
+ # YouTube channel insights (cumulative + 28-day Analytics metrics).
78
+ def youtube_channel_insights(account_id)
79
+ @http.data('GET', '/analytics/youtube/channel-insights', query: { 'accountId' => account_id })
80
+ end
81
+
82
+ # YouTube viewer demographics (age×gender percentages + country breakdown).
83
+ def youtube_demographics(account_id)
84
+ @http.data('GET', '/analytics/youtube/demographics', query: { 'accountId' => account_id })
85
+ end
86
+
87
+ # LinkedIn organization page statistics (12-month window; engagement=rate).
88
+ def linkedin_org_aggregate(account_id)
89
+ @http.data('GET', '/analytics/linkedin/org-aggregate', query: { 'accountId' => account_id })
90
+ end
91
+
92
+ # Pinterest account insights (last 30 days: impression/save/pin-click/outbound-click/engagement + followers).
93
+ def pinterest_account_insights(account_id)
94
+ @http.data('GET', '/analytics/pinterest/account-insights', query: { 'accountId' => account_id })
95
+ end
96
+
97
+ # LinkedIn post reaction breakdown.
98
+ def linkedin_post_reactions(account_id, post_id)
99
+ @http.data('GET', "/analytics/linkedin/post-reactions/#{post_id}",
100
+ query: { 'accountId' => account_id })
101
+ end
102
+ end
103
+
104
+ # Inbox analytics (Node sdk .inboxAnalytics) — dm-based volume/heatmap/response-time/source/leaderboard.
105
+ # from_date is required (YYYY-MM-DD); common filters to_date/platform/account_id/source.
106
+ class InboxAnalyticsResource
107
+ def initialize(http)
108
+ @http = http
109
+ end
110
+
111
+ # DM volume (summary + daily + platform breakdown).
112
+ def volume(from_date:, to_date: nil, platform: nil, account_id: nil, source: nil)
113
+ @http.data('GET', '/analytics/inbox/volume',
114
+ query: inbox_query(from_date, to_date, platform, account_id, source))
115
+ end
116
+
117
+ # Activity heatmap (dow 1=Mon..7=Sun, hour 0-23 UTC; only non-empty buckets).
118
+ def heatmap(from_date:, to_date: nil, platform: nil, account_id: nil, source: nil)
119
+ @http.data('GET', '/analytics/inbox/heatmap',
120
+ query: inbox_query(from_date, to_date, platform, account_id, source))
121
+ end
122
+
123
+ # Response time metrics.
124
+ def response_time(from_date:, to_date: nil, platform: nil, account_id: nil, source: nil)
125
+ @http.data('GET', '/analytics/inbox/response-time',
126
+ query: inbox_query(from_date, to_date, platform, account_id, source))
127
+ end
128
+
129
+ # Source breakdown.
130
+ def source_breakdown(from_date:, to_date: nil, platform: nil, account_id: nil, source: nil)
131
+ @http.data('GET', '/analytics/inbox/source-breakdown',
132
+ query: inbox_query(from_date, to_date, platform, account_id, source))
133
+ end
134
+
135
+ # Leaderboard of the most active accounts.
136
+ def top_accounts(from_date:, to_date: nil, platform: nil, account_id: nil, source: nil)
137
+ @http.data('GET', '/analytics/inbox/top-accounts',
138
+ query: inbox_query(from_date, to_date, platform, account_id, source))
139
+ end
140
+
141
+ # Per-conversation statistics — paginated (Paged envelope; @http.req returns the raw body).
142
+ def conversations(from_date:, to_date: nil, platform: nil, account_id: nil, source: nil,
143
+ limit: nil, cursor: nil)
144
+ query = inbox_query(from_date, to_date, platform, account_id, source)
145
+ query['limit'] = limit
146
+ query['cursor'] = cursor
147
+ @http.req('GET', '/analytics/inbox/conversations', query: query)
148
+ end
149
+
150
+ # Detail for a single conversation's analytics.
151
+ def conversation(conversation_id, from_date:, to_date: nil, platform: nil, account_id: nil,
152
+ source: nil)
153
+ @http.data('GET', "/analytics/inbox/conversations/#{conversation_id}",
154
+ query: inbox_query(from_date, to_date, platform, account_id, source))
155
+ end
156
+
157
+ private
158
+
159
+ # Common inbox query Hash (wire-camelCase keys; nil values are skipped in transport).
160
+ def inbox_query(from_date, to_date, platform, account_id, source)
161
+ {
162
+ 'fromDate' => from_date,
163
+ 'toDate' => to_date,
164
+ 'platform' => platform,
165
+ 'accountId' => account_id,
166
+ 'source' => source
167
+ }
168
+ end
169
+ end
170
+ end