postproxy-sdk 1.7.0 → 1.9.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/README.md +110 -1
- data/lib/postproxy/client.rb +12 -0
- data/lib/postproxy/constants.rb +19 -1
- data/lib/postproxy/resources/profile_comments.rb +41 -0
- data/lib/postproxy/resources/profile_groups.rb +20 -3
- data/lib/postproxy/resources/profiles.rb +14 -0
- data/lib/postproxy/types.rb +105 -3
- data/lib/postproxy/version.rb +1 -1
- data/lib/postproxy/webhook_events.rb +113 -0
- data/lib/postproxy.rb +1 -0
- 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: 7b20d20d7dc1f6a386e74894a57e470c70a1edc3e95bcd61c3b25f02b3e457cd
|
|
4
|
+
data.tar.gz: 6fd6807bcbbd09fae222ccd07eee0d66cbb420bc04e83fd49ab98c4340a4ed82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5e18b59805d1aff449eb7e3b8227bd2290b85994ba8e14f38b1c945381facd37a1b77e218d9eaf3c3e8c3183eade0da5268eb86b79762ad1aa9606b015a8e6a
|
|
7
|
+
data.tar.gz: ec5a37899a8c3622212e5d7d2b43a540df1e7aff64f9ea99a17a3fd1b0ed1e332a44a7c568eef24882f70fba267fe18302581d9af999ed997651df368dca59cc
|
data/README.md
CHANGED
|
@@ -260,6 +260,26 @@ PostProxy::WebhookSignature.verify(
|
|
|
260
260
|
)
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
+
### Event types and typed payloads
|
|
264
|
+
|
|
265
|
+
Subscribe to any of these events (or pass `["*"]` for all):
|
|
266
|
+
|
|
267
|
+
`post.processed`, `post.imported`, `platform_post.published`, `platform_post.failed`, `platform_post.failed_waiting_for_retry`, `platform_post.insights`, `profile.connected`, `profile.disconnected`, `profile.stats`, `media.failed`, `comment.created`.
|
|
268
|
+
|
|
269
|
+
`PostProxy::WebhookEvents.parse` validates the envelope and returns a typed `Event` — `event.data` is the right model for the event:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
event = PostProxy::WebhookEvents.parse(request.body.read)
|
|
273
|
+
case event.type
|
|
274
|
+
when "profile.stats"
|
|
275
|
+
puts "#{event.data.profile_id}: #{event.data.stats}"
|
|
276
|
+
when "platform_post.published"
|
|
277
|
+
puts "Published: #{event.data.platform_id}"
|
|
278
|
+
when "comment.created"
|
|
279
|
+
puts "#{event.data.author_username}: #{event.data.body}"
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
263
283
|
## Comments
|
|
264
284
|
|
|
265
285
|
```ruby
|
|
@@ -297,6 +317,32 @@ client.comments.like("post-id", "comment-id", profile_id: "profile-id")
|
|
|
297
317
|
client.comments.unlike("post-id", "comment-id", profile_id: "profile-id")
|
|
298
318
|
```
|
|
299
319
|
|
|
320
|
+
## Profile comments (Google Business reviews)
|
|
321
|
+
|
|
322
|
+
Profile-level comments expose Google Business reviews and replies. Reviews are user-generated — the SDK lets you list/get them and reply to or delete your own replies. Reviews sync twice daily.
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
# List reviews for a profile (paginated)
|
|
326
|
+
reviews = client.profile_comments.list("profile-id")
|
|
327
|
+
reviews.data.each do |review|
|
|
328
|
+
rating = (review.platform_data || {})[:star_rating]
|
|
329
|
+
puts "#{review.author_username} #{rating}: #{review.body}"
|
|
330
|
+
review.replies.each { |r| puts " reply: #{r.body}" }
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Filter by placement (location)
|
|
334
|
+
reviews = client.profile_comments.list("profile-id", placement_id: "accounts/123/locations/456")
|
|
335
|
+
|
|
336
|
+
# Get a single review
|
|
337
|
+
review = client.profile_comments.get("profile-id", "review-id")
|
|
338
|
+
|
|
339
|
+
# Reply to a review (parent_id is the review id)
|
|
340
|
+
reply = client.profile_comments.create("profile-id", parent_id: "review-id", text: "Thanks for visiting!")
|
|
341
|
+
|
|
342
|
+
# Delete your reply
|
|
343
|
+
client.profile_comments.delete("profile-id", "reply-id")
|
|
344
|
+
```
|
|
345
|
+
|
|
300
346
|
## Profiles
|
|
301
347
|
|
|
302
348
|
```ruby
|
|
@@ -311,6 +357,19 @@ placements = client.profiles.placements("prof-id").data
|
|
|
311
357
|
|
|
312
358
|
# Delete a profile
|
|
313
359
|
client.profiles.delete("prof-id")
|
|
360
|
+
|
|
361
|
+
# Profile stats timeseries — placement_id required for facebook, linkedin, telegram
|
|
362
|
+
stats = client.profiles.get_profile_stats("prof_li_001",
|
|
363
|
+
placement_id: "108520199",
|
|
364
|
+
from: "2026-04-01T00:00:00Z"
|
|
365
|
+
)
|
|
366
|
+
stats.data.records.each do |r|
|
|
367
|
+
puts "#{r.recorded_at}: #{r.stats[:followerCount]}"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Bluesky — no placements
|
|
371
|
+
bsky = client.profiles.get_profile_stats("prof_bsky_001")
|
|
372
|
+
puts bsky.data.records.last.stats[:followersCount]
|
|
314
373
|
```
|
|
315
374
|
|
|
316
375
|
## Profile Groups
|
|
@@ -335,6 +394,28 @@ connection = client.profile_groups.initialize_connection(
|
|
|
335
394
|
redirect_url: "https://myapp.com/callback"
|
|
336
395
|
)
|
|
337
396
|
# Redirect user to connection.url
|
|
397
|
+
|
|
398
|
+
# BlueSky — app password (synchronous, no OAuth)
|
|
399
|
+
bsky = client.profile_groups.connect_bluesky("pg-id",
|
|
400
|
+
identifier: "yourname.bsky.social",
|
|
401
|
+
app_password: "xxxx-xxxx-xxxx-xxxx"
|
|
402
|
+
)
|
|
403
|
+
puts bsky.profile.id
|
|
404
|
+
|
|
405
|
+
# Telegram — bring-your-own-bot. Channels populate asynchronously; poll
|
|
406
|
+
# placements until non-empty.
|
|
407
|
+
tg = client.profile_groups.connect_telegram("pg-id",
|
|
408
|
+
bot_token: "123456789:ABCdef-GhIJklMnOpQrStUvWxYz"
|
|
409
|
+
)
|
|
410
|
+
puts tg.next_step
|
|
411
|
+
|
|
412
|
+
placements = []
|
|
413
|
+
loop do
|
|
414
|
+
placements = client.profiles.placements(tg.profile.id).data
|
|
415
|
+
break unless placements.empty?
|
|
416
|
+
sleep 3
|
|
417
|
+
end
|
|
418
|
+
puts "Channels: #{placements.map { |p| [p.id, p.name] }}"
|
|
338
419
|
```
|
|
339
420
|
|
|
340
421
|
## Platform Parameters
|
|
@@ -364,7 +445,13 @@ platforms = PostProxy::PlatformParams.new(
|
|
|
364
445
|
board_id: "board-123"
|
|
365
446
|
),
|
|
366
447
|
threads: PostProxy::ThreadsParams.new(format: "post"),
|
|
367
|
-
twitter: PostProxy::TwitterParams.new(format: "post")
|
|
448
|
+
twitter: PostProxy::TwitterParams.new(format: "post"),
|
|
449
|
+
bluesky: PostProxy::BlueskyParams.new(format: "post"),
|
|
450
|
+
telegram: PostProxy::TelegramParams.new(
|
|
451
|
+
chat_id: "-1001234567890",
|
|
452
|
+
parse_mode: "HTML",
|
|
453
|
+
disable_link_preview: true
|
|
454
|
+
)
|
|
368
455
|
)
|
|
369
456
|
|
|
370
457
|
post = client.posts.create(
|
|
@@ -374,6 +461,28 @@ post = client.posts.create(
|
|
|
374
461
|
)
|
|
375
462
|
```
|
|
376
463
|
|
|
464
|
+
Supported platforms: `facebook`, `instagram`, `tiktok`, `linkedin`, `youtube`, `twitter`, `threads`, `pinterest`, `bluesky`, `telegram`, `google_business`. Telegram requires a `chat_id` per post — list channels with `client.profiles.placements(profile_id)`.
|
|
465
|
+
|
|
466
|
+
### Google Business
|
|
467
|
+
|
|
468
|
+
Google Business posts use a `google_business` entry in `PlatformParams` (a plain hash; no typed struct). The `location_id` is the location resource path returned by `client.profiles.placements()`. Supported formats: `standard`, `event`, `offer`. CTA actions: `LEARN_MORE`, `BOOK`, `ORDER`, `SHOP`, `SIGN_UP`, `CALL`. Media is limited to one image (≤5 MB).
|
|
469
|
+
|
|
470
|
+
```ruby
|
|
471
|
+
client.posts.create(
|
|
472
|
+
"Now open weekends!",
|
|
473
|
+
["gbp-profile-id"],
|
|
474
|
+
media: ["https://example.com/store.jpg"],
|
|
475
|
+
platforms: {
|
|
476
|
+
google_business: {
|
|
477
|
+
format: "standard",
|
|
478
|
+
location_id: "accounts/123/locations/456",
|
|
479
|
+
cta_action_type: "LEARN_MORE",
|
|
480
|
+
cta_url: "https://example.com"
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
)
|
|
484
|
+
```
|
|
485
|
+
|
|
377
486
|
## Error Handling
|
|
378
487
|
|
|
379
488
|
```ruby
|
data/lib/postproxy/client.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "resources/profile_groups"
|
|
|
7
7
|
require_relative "resources/webhooks"
|
|
8
8
|
require_relative "resources/queues"
|
|
9
9
|
require_relative "resources/comments"
|
|
10
|
+
require_relative "resources/profile_comments"
|
|
10
11
|
|
|
11
12
|
module PostProxy
|
|
12
13
|
class Client
|
|
@@ -23,6 +24,7 @@ module PostProxy
|
|
|
23
24
|
@webhooks = nil
|
|
24
25
|
@queues = nil
|
|
25
26
|
@comments = nil
|
|
27
|
+
@profile_comments = nil
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
def posts
|
|
@@ -49,6 +51,10 @@ module PostProxy
|
|
|
49
51
|
@comments ||= Resources::Comments.new(self)
|
|
50
52
|
end
|
|
51
53
|
|
|
54
|
+
def profile_comments
|
|
55
|
+
@profile_comments ||= Resources::ProfileComments.new(self)
|
|
56
|
+
end
|
|
57
|
+
|
|
52
58
|
def request(method, path, params: nil, json: nil, data: nil, files: nil, profile_group_id: nil)
|
|
53
59
|
url = "/api#{path}"
|
|
54
60
|
|
|
@@ -99,11 +105,16 @@ module PostProxy
|
|
|
99
105
|
|
|
100
106
|
private
|
|
101
107
|
|
|
108
|
+
def user_agent
|
|
109
|
+
@user_agent ||= "postproxy-ruby/#{PostProxy::VERSION} (ruby/#{RUBY_VERSION})"
|
|
110
|
+
end
|
|
111
|
+
|
|
102
112
|
def json_connection
|
|
103
113
|
@faraday_client || Faraday.new(url: @base_url) do |f|
|
|
104
114
|
f.request :url_encoded
|
|
105
115
|
f.headers["Authorization"] = "Bearer #{@api_key}"
|
|
106
116
|
f.headers["Content-Type"] = "application/json"
|
|
117
|
+
f.headers["User-Agent"] = user_agent
|
|
107
118
|
f.adapter Faraday.default_adapter
|
|
108
119
|
end
|
|
109
120
|
end
|
|
@@ -112,6 +123,7 @@ module PostProxy
|
|
|
112
123
|
@faraday_client || Faraday.new(url: @base_url) do |f|
|
|
113
124
|
f.request :multipart
|
|
114
125
|
f.headers["Authorization"] = "Bearer #{@api_key}"
|
|
126
|
+
f.headers["User-Agent"] = user_agent
|
|
115
127
|
f.adapter Faraday.default_adapter
|
|
116
128
|
end
|
|
117
129
|
end
|
data/lib/postproxy/constants.rb
CHANGED
|
@@ -2,7 +2,7 @@ module PostProxy
|
|
|
2
2
|
DEFAULT_BASE_URL = "https://api.postproxy.dev"
|
|
3
3
|
|
|
4
4
|
PLATFORMS = %w[
|
|
5
|
-
facebook instagram tiktok linkedin youtube twitter threads pinterest
|
|
5
|
+
facebook instagram tiktok linkedin youtube twitter threads pinterest bluesky telegram google_business
|
|
6
6
|
].freeze
|
|
7
7
|
|
|
8
8
|
PROFILE_STATUSES = %w[active expired inactive].freeze
|
|
@@ -21,10 +21,28 @@ module PostProxy
|
|
|
21
21
|
PINTEREST_FORMATS = %w[pin].freeze
|
|
22
22
|
THREADS_FORMATS = %w[post].freeze
|
|
23
23
|
TWITTER_FORMATS = %w[post].freeze
|
|
24
|
+
BLUESKY_FORMATS = %w[post].freeze
|
|
25
|
+
TELEGRAM_FORMATS = %w[post].freeze
|
|
24
26
|
|
|
25
27
|
TIKTOK_PRIVACIES = %w[
|
|
26
28
|
PUBLIC_TO_EVERYONE MUTUAL_FOLLOW_FRIENDS FOLLOWER_OF_CREATOR SELF_ONLY
|
|
27
29
|
].freeze
|
|
28
30
|
|
|
29
31
|
YOUTUBE_PRIVACIES = %w[public unlisted private].freeze
|
|
32
|
+
|
|
33
|
+
TELEGRAM_PARSE_MODES = %w[HTML MarkdownV2].freeze
|
|
34
|
+
|
|
35
|
+
WEBHOOK_EVENT_TYPES = %w[
|
|
36
|
+
post.processed
|
|
37
|
+
post.imported
|
|
38
|
+
platform_post.published
|
|
39
|
+
platform_post.failed
|
|
40
|
+
platform_post.failed_waiting_for_retry
|
|
41
|
+
platform_post.insights
|
|
42
|
+
profile.connected
|
|
43
|
+
profile.disconnected
|
|
44
|
+
profile.stats
|
|
45
|
+
media.failed
|
|
46
|
+
comment.created
|
|
47
|
+
].freeze
|
|
30
48
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module PostProxy
|
|
2
|
+
module Resources
|
|
3
|
+
class ProfileComments
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def list(profile_id, placement_id: nil, page: nil, per_page: nil)
|
|
9
|
+
params = {}
|
|
10
|
+
params[:placement_id] = placement_id if placement_id
|
|
11
|
+
params[:page] = page if page
|
|
12
|
+
params[:per_page] = per_page if per_page
|
|
13
|
+
|
|
14
|
+
result = @client.request(:get, "/profiles/#{profile_id}/comments", params: params)
|
|
15
|
+
comments = (result[:data] || []).map { |c| ProfileComment.new(**c) }
|
|
16
|
+
PaginatedResponse.new(
|
|
17
|
+
data: comments,
|
|
18
|
+
total: result[:total],
|
|
19
|
+
page: result[:page],
|
|
20
|
+
per_page: result[:per_page]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get(profile_id, comment_id)
|
|
25
|
+
result = @client.request(:get, "/profiles/#{profile_id}/comments/#{comment_id}")
|
|
26
|
+
ProfileComment.new(**result)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create(profile_id, parent_id:, text:)
|
|
30
|
+
result = @client.request(:post, "/profiles/#{profile_id}/comments",
|
|
31
|
+
json: { parent_id: parent_id, text: text })
|
|
32
|
+
ProfileComment.new(**result)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(profile_id, comment_id)
|
|
36
|
+
result = @client.request(:delete, "/profiles/#{profile_id}/comments/#{comment_id}")
|
|
37
|
+
AcceptedResponse.new(**result)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -26,11 +26,28 @@ module PostProxy
|
|
|
26
26
|
DeleteResponse.new(**result)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# OAuth flow. BlueSky and Telegram use their dedicated helpers below.
|
|
30
|
+
def initialize_connection(id, platform:, redirect_url: nil)
|
|
31
|
+
body = { platform: platform }
|
|
32
|
+
body[:redirect_url] = redirect_url if redirect_url
|
|
33
|
+
result = @client.request(:post, "/profile_groups/#{id}/initialize_connection", json: body)
|
|
34
|
+
ConnectionResponse.new(**result)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def connect_bluesky(id, identifier:, app_password:)
|
|
30
38
|
result = @client.request(:post, "/profile_groups/#{id}/initialize_connection",
|
|
31
|
-
json: { platform:
|
|
39
|
+
json: { platform: "bluesky", identifier: identifier, app_password: app_password }
|
|
32
40
|
)
|
|
33
|
-
|
|
41
|
+
BlueskyConnectionResponse.new(**result)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# After this call, poll `client.profiles.placements(profile.id)` until non-empty —
|
|
45
|
+
# the bot must be added as administrator to a channel in Telegram first.
|
|
46
|
+
def connect_telegram(id, bot_token:)
|
|
47
|
+
result = @client.request(:post, "/profile_groups/#{id}/initialize_connection",
|
|
48
|
+
json: { platform: "telegram", bot_token: bot_token }
|
|
49
|
+
)
|
|
50
|
+
TelegramConnectionResponse.new(**result)
|
|
34
51
|
end
|
|
35
52
|
end
|
|
36
53
|
end
|
|
@@ -22,6 +22,20 @@ module PostProxy
|
|
|
22
22
|
ListResponse.new(data: items)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# `placement_id` is required for facebook, linkedin, and telegram profiles.
|
|
26
|
+
def get_profile_stats(id, placement_id: nil, from: nil, to: nil, profile_group_id: nil)
|
|
27
|
+
params = {}
|
|
28
|
+
params[:placement_id] = placement_id if placement_id
|
|
29
|
+
params[:from] = from if from
|
|
30
|
+
params[:to] = to if to
|
|
31
|
+
|
|
32
|
+
result = @client.request(:get, "/profiles/#{id}/stats",
|
|
33
|
+
params: params.empty? ? nil : params,
|
|
34
|
+
profile_group_id: profile_group_id
|
|
35
|
+
)
|
|
36
|
+
ProfileStatsResponse.new(data: result[:data])
|
|
37
|
+
end
|
|
38
|
+
|
|
25
39
|
def delete(id, profile_group_id: nil)
|
|
26
40
|
result = @client.request(:delete, "/profiles/#{id}", profile_group_id: profile_group_id)
|
|
27
41
|
SuccessResponse.new(**result)
|
data/lib/postproxy/types.rb
CHANGED
|
@@ -98,14 +98,31 @@ module PostProxy
|
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
class MediaPlatformError < Model
|
|
102
|
+
attr_accessor :platform, :status, :error, :error_details
|
|
103
|
+
|
|
104
|
+
def initialize(**attrs)
|
|
105
|
+
@error = nil
|
|
106
|
+
@error_details = nil
|
|
107
|
+
super
|
|
108
|
+
@error_details = ErrorDetails.new(**@error_details.transform_keys(&:to_sym)) if @error_details.is_a?(Hash)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
101
112
|
class Media < Model
|
|
102
|
-
attr_accessor :id, :status, :error_message, :content_type, :source_url, :url
|
|
113
|
+
attr_accessor :id, :status, :error_message, :content_type, :source_url, :url, :platforms
|
|
103
114
|
|
|
104
115
|
def initialize(**attrs)
|
|
105
116
|
@error_message = nil
|
|
106
117
|
@source_url = nil
|
|
107
118
|
@url = nil
|
|
119
|
+
@platforms = nil
|
|
108
120
|
super
|
|
121
|
+
if @platforms.is_a?(Array)
|
|
122
|
+
@platforms = @platforms.map do |p|
|
|
123
|
+
p.is_a?(MediaPlatformError) ? p : MediaPlatformError.new(**p.transform_keys(&:to_sym))
|
|
124
|
+
end
|
|
125
|
+
end
|
|
109
126
|
end
|
|
110
127
|
end
|
|
111
128
|
|
|
@@ -322,6 +339,33 @@ module PostProxy
|
|
|
322
339
|
end
|
|
323
340
|
end
|
|
324
341
|
|
|
342
|
+
class ProfileComment < Model
|
|
343
|
+
attr_accessor :id, :external_id, :parent_external_id, :placement_id,
|
|
344
|
+
:body, :status, :author_username, :author_avatar_url,
|
|
345
|
+
:platform_data, :posted_at, :created_at, :replies
|
|
346
|
+
|
|
347
|
+
def initialize(**attrs)
|
|
348
|
+
@parent_external_id = nil
|
|
349
|
+
@author_username = nil
|
|
350
|
+
@author_avatar_url = nil
|
|
351
|
+
@platform_data = nil
|
|
352
|
+
@replies = []
|
|
353
|
+
super
|
|
354
|
+
@posted_at = parse_time(@posted_at)
|
|
355
|
+
@created_at = parse_time(@created_at)
|
|
356
|
+
@replies = (@replies || []).map do |r|
|
|
357
|
+
r.is_a?(ProfileComment) ? r : ProfileComment.new(**r.transform_keys(&:to_sym))
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private
|
|
362
|
+
|
|
363
|
+
def parse_time(value)
|
|
364
|
+
return nil if value.nil?
|
|
365
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
325
369
|
class AcceptedResponse < Model
|
|
326
370
|
attr_accessor :accepted
|
|
327
371
|
end
|
|
@@ -350,10 +394,60 @@ module PostProxy
|
|
|
350
394
|
attr_accessor :success
|
|
351
395
|
end
|
|
352
396
|
|
|
397
|
+
# OAuth-style connection response (facebook, instagram, twitter, etc.).
|
|
353
398
|
class ConnectionResponse < Model
|
|
354
399
|
attr_accessor :url, :success
|
|
355
400
|
end
|
|
356
401
|
|
|
402
|
+
# Alias for clarity at call sites; same shape as ConnectionResponse.
|
|
403
|
+
OAuthConnectionResponse = ConnectionResponse
|
|
404
|
+
|
|
405
|
+
class SyncProfile < Model
|
|
406
|
+
attr_accessor :id, :network, :name, :external_username
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
class BlueskyConnectionResponse < Model
|
|
410
|
+
attr_accessor :success, :profile
|
|
411
|
+
|
|
412
|
+
def initialize(**attrs)
|
|
413
|
+
@profile = nil
|
|
414
|
+
super
|
|
415
|
+
@profile = SyncProfile.new(**@profile.transform_keys(&:to_sym)) if @profile.is_a?(Hash)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
class TelegramConnectionResponse < Model
|
|
420
|
+
attr_accessor :success, :profile, :next_step
|
|
421
|
+
|
|
422
|
+
def initialize(**attrs)
|
|
423
|
+
@profile = nil
|
|
424
|
+
@next_step = nil
|
|
425
|
+
super
|
|
426
|
+
@profile = SyncProfile.new(**@profile.transform_keys(&:to_sym)) if @profile.is_a?(Hash)
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
class ProfileStats < Model
|
|
431
|
+
attr_accessor :profile_id, :platform, :placement_id, :records
|
|
432
|
+
|
|
433
|
+
def initialize(**attrs)
|
|
434
|
+
@placement_id = nil
|
|
435
|
+
@records = []
|
|
436
|
+
super
|
|
437
|
+
@records = (@records || []).map do |r|
|
|
438
|
+
r.is_a?(StatsRecord) ? r : StatsRecord.new(**r.transform_keys(&:to_sym))
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
class ProfileStatsResponse
|
|
444
|
+
attr_reader :data
|
|
445
|
+
|
|
446
|
+
def initialize(data:)
|
|
447
|
+
@data = data.is_a?(ProfileStats) ? data : ProfileStats.new(**data.transform_keys(&:to_sym))
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
357
451
|
# Platform-specific parameter structs
|
|
358
452
|
|
|
359
453
|
class FacebookParams < Model
|
|
@@ -392,13 +486,21 @@ module PostProxy
|
|
|
392
486
|
attr_accessor :format
|
|
393
487
|
end
|
|
394
488
|
|
|
489
|
+
class BlueskyParams < Model
|
|
490
|
+
attr_accessor :format
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
class TelegramParams < Model
|
|
494
|
+
attr_accessor :format, :chat_id, :parse_mode, :disable_link_preview, :disable_notification
|
|
495
|
+
end
|
|
496
|
+
|
|
395
497
|
class PlatformParams < Model
|
|
396
498
|
attr_accessor :facebook, :instagram, :tiktok, :linkedin, :youtube,
|
|
397
|
-
:pinterest, :threads, :twitter
|
|
499
|
+
:pinterest, :threads, :twitter, :bluesky, :telegram, :google_business
|
|
398
500
|
|
|
399
501
|
def to_h
|
|
400
502
|
result = {}
|
|
401
|
-
%i[facebook instagram tiktok linkedin youtube pinterest threads twitter].each do |platform|
|
|
503
|
+
%i[facebook instagram tiktok linkedin youtube pinterest threads twitter bluesky telegram google_business].each do |platform|
|
|
402
504
|
value = send(platform)
|
|
403
505
|
next if value.nil?
|
|
404
506
|
|
data/lib/postproxy/version.rb
CHANGED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module PostProxy
|
|
4
|
+
class WebhookParseError < StandardError; end
|
|
5
|
+
|
|
6
|
+
module WebhookEvents
|
|
7
|
+
class Event < Model
|
|
8
|
+
attr_accessor :id, :type, :created_at, :data
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class PostProcessedPlatform < Model
|
|
12
|
+
attr_accessor :id, :platform, :name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class PostProcessedData < Model
|
|
16
|
+
attr_accessor :id, :body, :status, :scheduled_at, :created_at, :platforms
|
|
17
|
+
|
|
18
|
+
def initialize(**attrs)
|
|
19
|
+
@platforms = []
|
|
20
|
+
super
|
|
21
|
+
@platforms = (@platforms || []).map do |p|
|
|
22
|
+
p.is_a?(PostProcessedPlatform) ? p : PostProcessedPlatform.new(**p.transform_keys(&:to_sym))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class ImportedProfile < Model
|
|
28
|
+
attr_accessor :id, :name, :platform
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class PostImportedData < Model
|
|
32
|
+
attr_accessor :id, :body, :source, :posted_at, :created_at,
|
|
33
|
+
:platform, :profile, :platform_post_id, :public_id
|
|
34
|
+
|
|
35
|
+
def initialize(**attrs)
|
|
36
|
+
@profile = nil
|
|
37
|
+
super
|
|
38
|
+
@profile = ImportedProfile.new(**@profile.transform_keys(&:to_sym)) if @profile.is_a?(Hash)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class PlatformPostData < Model
|
|
43
|
+
attr_accessor :id, :post_id, :platform, :profile_id, :profile_name,
|
|
44
|
+
:status, :error, :error_details, :platform_id, :insights
|
|
45
|
+
|
|
46
|
+
def initialize(**attrs)
|
|
47
|
+
@error_details = nil
|
|
48
|
+
@insights = nil
|
|
49
|
+
super
|
|
50
|
+
@error_details = ErrorDetails.new(**@error_details.transform_keys(&:to_sym)) if @error_details.is_a?(Hash)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class ProfileEventData < Model
|
|
55
|
+
attr_accessor :id, :name, :platform, :profile_group_id, :status, :uid, :username
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class ProfileStatsData < Model
|
|
59
|
+
attr_accessor :profile_id, :platform, :placement_id, :stats, :recorded_at
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class MediaFailedData < Model
|
|
63
|
+
attr_accessor :id, :post_id, :content_type, :status, :error_message
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class CommentCreatedData < Model
|
|
67
|
+
attr_accessor :id, :post_id, :platform_post_id, :platform, :external_id,
|
|
68
|
+
:parent_external_id, :body, :status, :author_external_id,
|
|
69
|
+
:author_name, :author_username, :author_avatar_url,
|
|
70
|
+
:like_count, :reply_count, :is_hidden, :permalink,
|
|
71
|
+
:platform_data, :posted_at, :created_at
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
DATA_CLASSES = {
|
|
75
|
+
"post.processed" => PostProcessedData,
|
|
76
|
+
"post.imported" => PostImportedData,
|
|
77
|
+
"platform_post.published" => PlatformPostData,
|
|
78
|
+
"platform_post.failed" => PlatformPostData,
|
|
79
|
+
"platform_post.failed_waiting_for_retry" => PlatformPostData,
|
|
80
|
+
"platform_post.insights" => PlatformPostData,
|
|
81
|
+
"profile.connected" => ProfileEventData,
|
|
82
|
+
"profile.disconnected" => ProfileEventData,
|
|
83
|
+
"profile.stats" => ProfileStatsData,
|
|
84
|
+
"media.failed" => MediaFailedData,
|
|
85
|
+
"comment.created" => CommentCreatedData
|
|
86
|
+
}.freeze
|
|
87
|
+
|
|
88
|
+
# Parse a webhook body and return a typed Event. `data` is parsed into the
|
|
89
|
+
# appropriate model based on `type`. Raises WebhookParseError on bad input.
|
|
90
|
+
def self.parse(body)
|
|
91
|
+
parsed =
|
|
92
|
+
case body
|
|
93
|
+
when String then JSON.parse(body, symbolize_names: true)
|
|
94
|
+
when Hash then body.transform_keys(&:to_sym)
|
|
95
|
+
else raise WebhookParseError, "Webhook body must be a String or Hash"
|
|
96
|
+
end
|
|
97
|
+
rescue JSON::ParserError => e
|
|
98
|
+
raise WebhookParseError, "Invalid JSON: #{e.message}"
|
|
99
|
+
else
|
|
100
|
+
type = parsed[:type].to_s
|
|
101
|
+
raise WebhookParseError, "Unknown webhook event type: #{type.inspect}" unless DATA_CLASSES.key?(type)
|
|
102
|
+
|
|
103
|
+
data_class = DATA_CLASSES[type]
|
|
104
|
+
data_hash = (parsed[:data] || {}).transform_keys(&:to_sym)
|
|
105
|
+
Event.new(
|
|
106
|
+
id: parsed[:id],
|
|
107
|
+
type: type,
|
|
108
|
+
created_at: parsed[:created_at],
|
|
109
|
+
data: data_class.new(**data_hash)
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/postproxy.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: postproxy-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- PostProxy
|
|
@@ -80,12 +80,14 @@ files:
|
|
|
80
80
|
- lib/postproxy/errors.rb
|
|
81
81
|
- lib/postproxy/resources/comments.rb
|
|
82
82
|
- lib/postproxy/resources/posts.rb
|
|
83
|
+
- lib/postproxy/resources/profile_comments.rb
|
|
83
84
|
- lib/postproxy/resources/profile_groups.rb
|
|
84
85
|
- lib/postproxy/resources/profiles.rb
|
|
85
86
|
- lib/postproxy/resources/queues.rb
|
|
86
87
|
- lib/postproxy/resources/webhooks.rb
|
|
87
88
|
- lib/postproxy/types.rb
|
|
88
89
|
- lib/postproxy/version.rb
|
|
90
|
+
- lib/postproxy/webhook_events.rb
|
|
89
91
|
- lib/postproxy/webhook_signature.rb
|
|
90
92
|
homepage: https://postproxy.dev
|
|
91
93
|
licenses:
|