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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0aea2892ef3d3256dfd1cc9ce790da283df1d6edb83a2cfcaa76c0aa8a8194ae
4
- data.tar.gz: a9f008666d47832963cc7119d0db1b53d53c7ddd3dc690fa13acebc05c72f5f6
3
+ metadata.gz: 7b20d20d7dc1f6a386e74894a57e470c70a1edc3e95bcd61c3b25f02b3e457cd
4
+ data.tar.gz: 6fd6807bcbbd09fae222ccd07eee0d66cbb420bc04e83fd49ab98c4340a4ed82
5
5
  SHA512:
6
- metadata.gz: c9f6cf9ec0d398081c84c21c41824bd590fa9d57b7e8eea423512bcc899b82d3df93c4532edf307e2f949a0d72217e6677743b36434a4384e9b9d801030b4aa4
7
- data.tar.gz: 325923dc017d840f212edac1941f2f2aa8d8821594f2418e40dee1e1b56a8f103627ab1f8cb9adb07f576afcc8fe5a6b431fcc408a50c4ac6887948bf80426d6
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
@@ -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
@@ -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
- def initialize_connection(id, platform:, redirect_url:)
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: platform, redirect_url: redirect_url }
39
+ json: { platform: "bluesky", identifier: identifier, app_password: app_password }
32
40
  )
33
- ConnectionResponse.new(**result)
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)
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module PostProxy
2
- VERSION = "1.7.0"
2
+ VERSION = "1.9.0"
3
3
  end
@@ -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
@@ -4,6 +4,7 @@ require_relative "postproxy/errors"
4
4
  require_relative "postproxy/types"
5
5
  require_relative "postproxy/client"
6
6
  require_relative "postproxy/webhook_signature"
7
+ require_relative "postproxy/webhook_events"
7
8
 
8
9
  module PostProxy
9
10
  end
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.7.0
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: