postproxy-sdk 1.8.0 → 1.10.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: 1d2f9ae8cbbc2ca3cd324b5dc522226c9d7a77cd93430abb309b31efc210a293
4
- data.tar.gz: 26c58e5f9b7ea1af4eccb3935ef8c71bf202bf5bff93898d0b30a5450114bb1a
3
+ metadata.gz: 2852d366a306a698457955ea9e631fb4cd05fbc0f55949a772f0b872b19ce0af
4
+ data.tar.gz: 74845607657bcea29116fe1a3d509daea04f0afba3e0ded079068f03b17d3506
5
5
  SHA512:
6
- metadata.gz: 906800719560c2e37351c958fa72449d1411b351c1cad4deb0d312e9b6fcaa3af3be3dcb504eeef045a3d24737815afb4e7f772edae4687e102e2f9d4bd605cc
7
- data.tar.gz: b6ee51b608b91a9d31ed2891b7bba87df996006be1795504f540e275dd54c8ff3b7ef7e5f1994a7aa495a02f67ec8f3921e9017baadb3e1e8d3c5550d6959b06
6
+ metadata.gz: 0650f24d42af376d663521ced5c78be0fe97dc99d117534a132479a02f8290a18404426d5791ea5f90f3ed786041ccb2d3a15486533b149e7ab0c94324839238
7
+ data.tar.gz: 39699c4a07a238f45fc1e12545dfc97b88d85299f03cd3eb1ba97380a1f7bfa79da004abd170c3da87aa707d486a7a103c298c0b34fb9371d9d0c4ada45e3014
data/README.md CHANGED
@@ -264,7 +264,7 @@ PostProxy::WebhookSignature.verify(
264
264
 
265
265
  Subscribe to any of these events (or pass `["*"]` for all):
266
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`.
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`, `profile_comment.created`, `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.edited`, `message.deleted`, `message.failed_waiting_for_retry`, `message.failed`, `reaction.received`.
268
268
 
269
269
  `PostProxy::WebhookEvents.parse` validates the envelope and returns a typed `Event` — `event.data` is the right model for the event:
270
270
 
@@ -277,9 +277,20 @@ when "platform_post.published"
277
277
  puts "Published: #{event.data.platform_id}"
278
278
  when "comment.created"
279
279
  puts "#{event.data.author_username}: #{event.data.body}"
280
+ when "message.received"
281
+ # MessageEventData — event.data.message is a full Message
282
+ puts "DM from #{event.data.message.chat_id}: #{event.data.message.body}"
283
+ when "reaction.received"
284
+ # ReactionEventData
285
+ puts "#{event.data.action}: #{event.data.reaction} on #{event.data.message.id}"
286
+ when "profile_comment.created"
287
+ # ProfileCommentCreatedData
288
+ puts "#{event.data.author_username}: #{event.data.body}"
280
289
  end
281
290
  ```
282
291
 
292
+ The direct-message events (`message.*`) carry a `MessageEventData` (`data.message` is a full `Message`); `reaction.received` carries a `ReactionEventData`; `profile_comment.created` carries a `ProfileCommentCreatedData`.
293
+
283
294
  ## Comments
284
295
 
285
296
  ```ruby
@@ -315,6 +326,97 @@ client.comments.unhide("post-id", "comment-id", profile_id: "profile-id")
315
326
  # Like / unlike a comment
316
327
  client.comments.like("post-id", "comment-id", profile_id: "profile-id")
317
328
  client.comments.unlike("post-id", "comment-id", profile_id: "profile-id")
329
+
330
+ # Synced comments may carry media attachments and author metadata
331
+ comment = client.comments.get("post-id", "comment-id", profile_id: "profile-id")
332
+ comment.attachments.each { |att| puts "#{att.type} #{att.url} (#{att.status})" }
333
+ puts comment.metadata[:follower_count] if comment.metadata
334
+
335
+ # Private reply to a comment's author (Instagram/Facebook) — returns a Message
336
+ message = client.comments.private_reply("post-id", "comment-id", profile_id: "profile-id", text: "DM-ing you the details.")
337
+ puts message.chat_id, message.status
338
+ ```
339
+
340
+ ## Direct Messages
341
+
342
+ Read and send 1:1 messages on DM-capable profiles (Facebook Messenger, Instagram, Telegram, Bluesky). A conversation is a **Chat**; it holds **Messages**. Outbound sends are processed asynchronously (`status` starts as `pending`).
343
+
344
+ ```ruby
345
+ # List chats for a profile (paginated, most recent first)
346
+ chats = client.chats.list("profile-id", per_page: 20)
347
+ chats.data.each { |chat| puts "#{chat.participant_username}: #{chat.last_message_at}" }
348
+
349
+ # Find or create a chat with a participant (idempotent)
350
+ chat = client.chats.create("profile-id", "igsid_8675309", participant_username: "jane_doe")
351
+
352
+ # Get a single chat
353
+ chat = client.chats.get(chat.id)
354
+
355
+ # List messages in a chat (filter by direction/status)
356
+ messages = client.messages.list(chat.id, direction: "inbound")
357
+ messages.data.each do |msg|
358
+ puts "#{msg.direction}: #{msg.body}"
359
+ msg.attachments.each { |att| puts " #{att.url}" }
360
+ end
361
+
362
+ # Send a text message (within the 24h window)
363
+ sent = client.messages.send(chat.id, body: "Yes, we ship worldwide!")
364
+
365
+ # Send outside the 24h window with a tag (Facebook/Instagram)
366
+ client.messages.send(chat.id, body: "Following up.", tag: "HUMAN_AGENT")
367
+
368
+ # Send media — by hosted URL or local file
369
+ client.messages.send(chat.id, media: ["https://cdn.example.com/photo.png"])
370
+ client.messages.send(chat.id, media_files: ["./photo.png"])
371
+
372
+ # Telegram: reply threading + inline keyboard
373
+ client.messages.send(
374
+ chat.id,
375
+ body: "Pick one:",
376
+ reply_markup: { inline_keyboard: [[{ text: "Track order", callback_data: "track:1" }]] }
377
+ )
378
+
379
+ # Get / edit (Telegram only) a message
380
+ msg = client.messages.get(sent.id)
381
+ client.messages.edit(sent.id, body: "Updated answer.")
382
+
383
+ # React / unreact (Facebook & Instagram)
384
+ client.messages.react(sent.id, reaction: "love", emoji: "❤️")
385
+ client.messages.unreact(sent.id)
386
+
387
+ # Archive / unarchive a chat (Bluesky only)
388
+ client.chats.archive(chat.id)
389
+ client.chats.unarchive(chat.id)
390
+
391
+ # Private reply to a comment's author (Instagram/Facebook) — returns a Message
392
+ message = client.comments.private_reply("post-id", "comment-id", profile_id: "profile-id", text: "DM-ing you the details.")
393
+ puts message.chat_id, message.status
394
+ ```
395
+
396
+ ## Profile comments (Google Business reviews)
397
+
398
+ 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.
399
+
400
+ ```ruby
401
+ # List reviews for a profile (paginated)
402
+ reviews = client.profile_comments.list("profile-id")
403
+ reviews.data.each do |review|
404
+ rating = (review.platform_data || {})[:star_rating]
405
+ puts "#{review.author_username} #{rating}: #{review.body}"
406
+ review.replies.each { |r| puts " reply: #{r.body}" }
407
+ end
408
+
409
+ # Filter by placement (location)
410
+ reviews = client.profile_comments.list("profile-id", placement_id: "accounts/123/locations/456")
411
+
412
+ # Get a single review
413
+ review = client.profile_comments.get("profile-id", "review-id")
414
+
415
+ # Reply to a review (parent_id is the review id)
416
+ reply = client.profile_comments.create("profile-id", parent_id: "review-id", text: "Thanks for visiting!")
417
+
418
+ # Delete your reply
419
+ client.profile_comments.delete("profile-id", "reply-id")
318
420
  ```
319
421
 
320
422
  ## Profiles
@@ -435,7 +537,27 @@ post = client.posts.create(
435
537
  )
436
538
  ```
437
539
 
438
- Supported platforms: `facebook`, `instagram`, `tiktok`, `linkedin`, `youtube`, `twitter`, `threads`, `pinterest`, `bluesky`, `telegram`. Telegram requires a `chat_id` per post — list channels with `client.profiles.placements(profile_id)`.
540
+ 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)`.
541
+
542
+ ### Google Business
543
+
544
+ 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).
545
+
546
+ ```ruby
547
+ client.posts.create(
548
+ "Now open weekends!",
549
+ ["gbp-profile-id"],
550
+ media: ["https://example.com/store.jpg"],
551
+ platforms: {
552
+ google_business: {
553
+ format: "standard",
554
+ location_id: "accounts/123/locations/456",
555
+ cta_action_type: "LEARN_MORE",
556
+ cta_url: "https://example.com"
557
+ }
558
+ }
559
+ )
560
+ ```
439
561
 
440
562
  ## Error Handling
441
563
 
@@ -7,6 +7,9 @@ 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"
11
+ require_relative "resources/chats"
12
+ require_relative "resources/messages"
10
13
 
11
14
  module PostProxy
12
15
  class Client
@@ -23,6 +26,9 @@ module PostProxy
23
26
  @webhooks = nil
24
27
  @queues = nil
25
28
  @comments = nil
29
+ @profile_comments = nil
30
+ @chats = nil
31
+ @messages = nil
26
32
  end
27
33
 
28
34
  def posts
@@ -49,6 +55,18 @@ module PostProxy
49
55
  @comments ||= Resources::Comments.new(self)
50
56
  end
51
57
 
58
+ def profile_comments
59
+ @profile_comments ||= Resources::ProfileComments.new(self)
60
+ end
61
+
62
+ def chats
63
+ @chats ||= Resources::Chats.new(self)
64
+ end
65
+
66
+ def messages
67
+ @messages ||= Resources::Messages.new(self)
68
+ end
69
+
52
70
  def request(method, path, params: nil, json: nil, data: nil, files: nil, profile_group_id: nil)
53
71
  url = "/api#{path}"
54
72
 
@@ -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 bluesky telegram
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
@@ -32,6 +32,10 @@ module PostProxy
32
32
 
33
33
  TELEGRAM_PARSE_MODES = %w[HTML MarkdownV2].freeze
34
34
 
35
+ MESSAGE_DIRECTIONS = %w[inbound outbound].freeze
36
+
37
+ MESSAGE_STATUSES = %w[pending published failed_waiting_for_retry failed received].freeze
38
+
35
39
  WEBHOOK_EVENT_TYPES = %w[
36
40
  post.processed
37
41
  post.imported
@@ -44,5 +48,15 @@ module PostProxy
44
48
  profile.stats
45
49
  media.failed
46
50
  comment.created
51
+ profile_comment.created
52
+ message.received
53
+ message.sent
54
+ message.delivered
55
+ message.read
56
+ message.edited
57
+ message.deleted
58
+ message.failed_waiting_for_retry
59
+ message.failed
60
+ reaction.received
47
61
  ].freeze
48
62
  end
@@ -0,0 +1,57 @@
1
+ module PostProxy
2
+ module Resources
3
+ class Chats
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list(profile_id, page: nil, per_page: nil, before: nil, after: nil, profile_group_id: nil)
9
+ params = {}
10
+ params[:page] = page if page
11
+ params[:per_page] = per_page if per_page
12
+ params[:before] = format_time(before) if before
13
+ params[:after] = format_time(after) if after
14
+
15
+ result = @client.request(:get, "/profiles/#{profile_id}/chats", params: params, profile_group_id: profile_group_id)
16
+ chats = (result[:data] || []).map { |c| Chat.new(**c) }
17
+ PaginatedResponse.new(
18
+ data: chats,
19
+ total: result[:total],
20
+ page: result[:page],
21
+ per_page: result[:per_page]
22
+ )
23
+ end
24
+
25
+ def create(profile_id, participant_external_id, participant_username: nil, participant_name: nil, profile_group_id: nil)
26
+ json_body = { participant_external_id: participant_external_id }
27
+ json_body[:participant_username] = participant_username if participant_username
28
+ json_body[:participant_name] = participant_name if participant_name
29
+
30
+ result = @client.request(:post, "/profiles/#{profile_id}/chats", json: json_body, profile_group_id: profile_group_id)
31
+ Chat.new(**result)
32
+ end
33
+
34
+ def get(chat_id, profile_group_id: nil)
35
+ result = @client.request(:get, "/chats/#{chat_id}", profile_group_id: profile_group_id)
36
+ Chat.new(**result)
37
+ end
38
+
39
+ def archive(chat_id, profile_group_id: nil)
40
+ result = @client.request(:post, "/chats/#{chat_id}/archive", profile_group_id: profile_group_id)
41
+ Chat.new(**result)
42
+ end
43
+
44
+ def unarchive(chat_id, profile_group_id: nil)
45
+ result = @client.request(:delete, "/chats/#{chat_id}/archive", profile_group_id: profile_group_id)
46
+ Chat.new(**result)
47
+ end
48
+
49
+ private
50
+
51
+ def format_time(value)
52
+ return value if value.is_a?(String)
53
+ value.iso8601
54
+ end
55
+ end
56
+ end
57
+ end
@@ -57,6 +57,14 @@ module PostProxy
57
57
  result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/unlike", params: { profile_id: profile_id })
58
58
  AcceptedResponse.new(**result)
59
59
  end
60
+
61
+ def private_reply(post_id, comment_id, profile_id:, text:)
62
+ result = @client.request(:post, "/posts/#{post_id}/comments/#{comment_id}/private_reply",
63
+ params: { profile_id: profile_id },
64
+ json: { text: text }
65
+ )
66
+ Message.new(**result)
67
+ end
60
68
  end
61
69
  end
62
70
  end
@@ -0,0 +1,118 @@
1
+ module PostProxy
2
+ module Resources
3
+ class Messages
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list(chat_id, page: nil, per_page: nil, direction: nil, status: nil, profile_group_id: nil)
9
+ params = {}
10
+ params[:page] = page if page
11
+ params[:per_page] = per_page if per_page
12
+ params[:direction] = direction if direction
13
+ params[:status] = status if status
14
+
15
+ result = @client.request(:get, "/chats/#{chat_id}/messages", params: params, profile_group_id: profile_group_id)
16
+ messages = (result[:data] || []).map { |m| Message.new(**m) }
17
+ PaginatedResponse.new(
18
+ data: messages,
19
+ total: result[:total],
20
+ page: result[:page],
21
+ per_page: result[:per_page]
22
+ )
23
+ end
24
+
25
+ def send_message(chat_id, body: nil, media: nil, media_files: nil, tag: nil,
26
+ reply_to_external_id: nil, reply_markup: nil, profile_group_id: nil)
27
+ has_files = media_files && !media_files.empty?
28
+
29
+ if has_files
30
+ form_data = {}
31
+ form_data["body"] = body if body
32
+ form_data["tag"] = tag if tag
33
+ form_data["reply_to_external_id"] = reply_to_external_id if reply_to_external_id
34
+
35
+ files = []
36
+ media&.each do |m|
37
+ files << ["media[]", nil, m, "text/plain"]
38
+ end
39
+ media_files.each do |path|
40
+ path = path.to_s
41
+ filename = File.basename(path)
42
+ content_type = mime_type_for(filename)
43
+ io = File.open(path, "rb")
44
+ files << ["media[]", filename, io, content_type]
45
+ end
46
+
47
+ result = @client.request(:post, "/chats/#{chat_id}/messages",
48
+ data: form_data,
49
+ files: files,
50
+ profile_group_id: profile_group_id
51
+ )
52
+ else
53
+ json_body = {}
54
+ json_body[:body] = body if body
55
+ json_body[:media] = media if media
56
+ json_body[:tag] = tag if tag
57
+ json_body[:reply_to_external_id] = reply_to_external_id if reply_to_external_id
58
+ json_body[:reply_markup] = reply_markup if reply_markup
59
+
60
+ result = @client.request(:post, "/chats/#{chat_id}/messages", json: json_body, profile_group_id: profile_group_id)
61
+ end
62
+
63
+ Message.new(**result)
64
+ end
65
+ alias_method :send, :send_message
66
+
67
+ def get(message_id, profile_group_id: nil)
68
+ result = @client.request(:get, "/messages/#{message_id}", profile_group_id: profile_group_id)
69
+ Message.new(**result)
70
+ end
71
+
72
+ def edit(message_id, body: nil, reply_markup: nil, profile_group_id: nil)
73
+ json_body = {}
74
+ json_body[:body] = body if body
75
+ json_body[:reply_markup] = reply_markup if reply_markup
76
+
77
+ result = @client.request(:patch, "/messages/#{message_id}", json: json_body, profile_group_id: profile_group_id)
78
+ Message.new(**result)
79
+ end
80
+
81
+ def react(message_id, reaction: nil, emoji: nil, profile_group_id: nil)
82
+ json_body = {}
83
+ json_body[:reaction] = reaction if reaction
84
+ json_body[:emoji] = emoji if emoji
85
+
86
+ result = @client.request(:post, "/messages/#{message_id}/react",
87
+ json: json_body.empty? ? nil : json_body,
88
+ profile_group_id: profile_group_id
89
+ )
90
+ Message.new(**result)
91
+ end
92
+
93
+ def unreact(message_id, profile_group_id: nil)
94
+ result = @client.request(:delete, "/messages/#{message_id}/unreact", profile_group_id: profile_group_id)
95
+ Message.new(**result)
96
+ end
97
+
98
+ private
99
+
100
+ def mime_type_for(filename)
101
+ case File.extname(filename).downcase
102
+ when ".jpg", ".jpeg" then "image/jpeg"
103
+ when ".png" then "image/png"
104
+ when ".gif" then "image/gif"
105
+ when ".webp" then "image/webp"
106
+ when ".mp4" then "video/mp4"
107
+ when ".mov" then "video/quicktime"
108
+ when ".avi" then "video/x-msvideo"
109
+ when ".webm" then "video/webm"
110
+ when ".mp3" then "audio/mpeg"
111
+ when ".ogg" then "audio/ogg"
112
+ when ".m4a" then "audio/mp4"
113
+ else "application/octet-stream"
114
+ end
115
+ end
116
+ end
117
+ end
118
+ 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
@@ -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
 
@@ -290,25 +307,59 @@ module PostProxy
290
307
  end
291
308
  end
292
309
 
310
+ class Attachment < Model
311
+ attr_accessor :id, :type, :url, :status, :external_id
312
+
313
+ def initialize(**attrs)
314
+ @url = nil
315
+ @external_id = nil
316
+ super
317
+ end
318
+ end
319
+
320
+ class Reaction < Model
321
+ attr_accessor :sender_external_id, :emoji, :reaction, :at
322
+
323
+ def initialize(**attrs)
324
+ @emoji = nil
325
+ @reaction = nil
326
+ @at = nil
327
+ super
328
+ @at = parse_time(@at)
329
+ end
330
+
331
+ private
332
+
333
+ def parse_time(value)
334
+ return nil if value.nil?
335
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
336
+ end
337
+ end
338
+
293
339
  class Comment < Model
294
340
  attr_accessor :id, :external_id, :body, :status, :author_username,
295
- :author_avatar_url, :author_external_id, :parent_external_id,
296
- :like_count, :is_hidden, :permalink, :platform_data,
297
- :posted_at, :created_at, :replies
341
+ :author_avatar_url, :author_external_id, :metadata,
342
+ :parent_external_id, :like_count, :is_hidden, :permalink,
343
+ :platform_data, :attachments, :posted_at, :created_at, :replies
298
344
 
299
345
  def initialize(**attrs)
300
346
  @external_id = nil
301
347
  @author_avatar_url = nil
302
348
  @author_external_id = nil
349
+ @metadata = nil
303
350
  @parent_external_id = nil
304
351
  @like_count = 0
305
352
  @is_hidden = false
306
353
  @permalink = nil
307
354
  @platform_data = nil
355
+ @attachments = []
308
356
  @replies = []
309
357
  super
310
358
  @posted_at = parse_time(@posted_at)
311
359
  @created_at = parse_time(@created_at)
360
+ @attachments = (@attachments || []).map do |a|
361
+ a.is_a?(Attachment) ? a : Attachment.new(**a.transform_keys(&:to_sym))
362
+ end
312
363
  @replies = (@replies || []).map do |r|
313
364
  r.is_a?(Comment) ? r : Comment.new(**r.transform_keys(&:to_sym))
314
365
  end
@@ -322,6 +373,112 @@ module PostProxy
322
373
  end
323
374
  end
324
375
 
376
+ class Chat < Model
377
+ attr_accessor :id, :profile_id, :platform, :participant_external_id,
378
+ :participant_username, :participant_name, :participant_avatar_url,
379
+ :external_conversation_id, :last_inbound_at, :last_outbound_at,
380
+ :last_message_at, :metadata, :archived, :created_at
381
+
382
+ def initialize(**attrs)
383
+ @participant_username = nil
384
+ @participant_name = nil
385
+ @participant_avatar_url = nil
386
+ @external_conversation_id = nil
387
+ @last_inbound_at = nil
388
+ @last_outbound_at = nil
389
+ @last_message_at = nil
390
+ @metadata = nil
391
+ @archived = nil
392
+ super
393
+ @last_inbound_at = parse_time(@last_inbound_at)
394
+ @last_outbound_at = parse_time(@last_outbound_at)
395
+ @last_message_at = parse_time(@last_message_at)
396
+ @created_at = parse_time(@created_at)
397
+ end
398
+
399
+ private
400
+
401
+ def parse_time(value)
402
+ return nil if value.nil?
403
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
404
+ end
405
+ end
406
+
407
+ class Message < Model
408
+ attr_accessor :id, :chat_id, :external_id, :direction, :body, :status,
409
+ :tag, :external_comment_id, :error_message, :platform_data,
410
+ :external_posted_at, :external_delivered_at, :external_read_at,
411
+ :external_edited_at, :reply_to_external_id, :reply_markup,
412
+ :external_deleted_at, :reactions, :attachments,
413
+ :is_unsupported, :created_at
414
+
415
+ def initialize(**attrs)
416
+ @external_id = nil
417
+ @body = nil
418
+ @tag = nil
419
+ @external_comment_id = nil
420
+ @error_message = nil
421
+ @platform_data = nil
422
+ @external_posted_at = nil
423
+ @external_delivered_at = nil
424
+ @external_read_at = nil
425
+ @external_edited_at = nil
426
+ @reply_to_external_id = nil
427
+ @reply_markup = nil
428
+ @external_deleted_at = nil
429
+ @reactions = []
430
+ @attachments = []
431
+ @is_unsupported = false
432
+ super
433
+ @external_posted_at = parse_time(@external_posted_at)
434
+ @external_delivered_at = parse_time(@external_delivered_at)
435
+ @external_read_at = parse_time(@external_read_at)
436
+ @external_edited_at = parse_time(@external_edited_at)
437
+ @external_deleted_at = parse_time(@external_deleted_at)
438
+ @created_at = parse_time(@created_at)
439
+ @reactions = (@reactions || []).map do |r|
440
+ r.is_a?(Reaction) ? r : Reaction.new(**r.transform_keys(&:to_sym))
441
+ end
442
+ @attachments = (@attachments || []).map do |a|
443
+ a.is_a?(Attachment) ? a : Attachment.new(**a.transform_keys(&:to_sym))
444
+ end
445
+ end
446
+
447
+ private
448
+
449
+ def parse_time(value)
450
+ return nil if value.nil?
451
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
452
+ end
453
+ end
454
+
455
+ class ProfileComment < Model
456
+ attr_accessor :id, :external_id, :parent_external_id, :placement_id,
457
+ :body, :status, :author_username, :author_avatar_url,
458
+ :platform_data, :posted_at, :created_at, :replies
459
+
460
+ def initialize(**attrs)
461
+ @parent_external_id = nil
462
+ @author_username = nil
463
+ @author_avatar_url = nil
464
+ @platform_data = nil
465
+ @replies = []
466
+ super
467
+ @posted_at = parse_time(@posted_at)
468
+ @created_at = parse_time(@created_at)
469
+ @replies = (@replies || []).map do |r|
470
+ r.is_a?(ProfileComment) ? r : ProfileComment.new(**r.transform_keys(&:to_sym))
471
+ end
472
+ end
473
+
474
+ private
475
+
476
+ def parse_time(value)
477
+ return nil if value.nil?
478
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
479
+ end
480
+ end
481
+
325
482
  class AcceptedResponse < Model
326
483
  attr_accessor :accepted
327
484
  end
@@ -452,11 +609,11 @@ module PostProxy
452
609
 
453
610
  class PlatformParams < Model
454
611
  attr_accessor :facebook, :instagram, :tiktok, :linkedin, :youtube,
455
- :pinterest, :threads, :twitter, :bluesky, :telegram
612
+ :pinterest, :threads, :twitter, :bluesky, :telegram, :google_business
456
613
 
457
614
  def to_h
458
615
  result = {}
459
- %i[facebook instagram tiktok linkedin youtube pinterest threads twitter bluesky telegram].each do |platform|
616
+ %i[facebook instagram tiktok linkedin youtube pinterest threads twitter bluesky telegram google_business].each do |platform|
460
617
  value = send(platform)
461
618
  next if value.nil?
462
619
 
@@ -1,3 +1,3 @@
1
1
  module PostProxy
2
- VERSION = "1.8.0"
2
+ VERSION = "1.10.0"
3
3
  end
@@ -71,6 +71,35 @@ module PostProxy
71
71
  :platform_data, :posted_at, :created_at
72
72
  end
73
73
 
74
+ class MessageEventData < Model
75
+ attr_accessor :message
76
+
77
+ def initialize(**attrs)
78
+ @message = nil
79
+ super
80
+ @message = Message.new(**@message.transform_keys(&:to_sym)) if @message.is_a?(Hash)
81
+ end
82
+ end
83
+
84
+ class ReactionEventData < Model
85
+ attr_accessor :message, :sender_external_id, :action, :reaction, :emoji, :occurred_at
86
+
87
+ def initialize(**attrs)
88
+ @message = nil
89
+ @reaction = nil
90
+ @emoji = nil
91
+ @occurred_at = nil
92
+ super
93
+ @message = Message.new(**@message.transform_keys(&:to_sym)) if @message.is_a?(Hash)
94
+ end
95
+ end
96
+
97
+ class ProfileCommentCreatedData < Model
98
+ attr_accessor :id, :profile_id, :platform, :placement_id, :external_id,
99
+ :parent_external_id, :body, :status, :author_username,
100
+ :author_avatar_url, :platform_data, :posted_at, :created_at
101
+ end
102
+
74
103
  DATA_CLASSES = {
75
104
  "post.processed" => PostProcessedData,
76
105
  "post.imported" => PostImportedData,
@@ -82,7 +111,17 @@ module PostProxy
82
111
  "profile.disconnected" => ProfileEventData,
83
112
  "profile.stats" => ProfileStatsData,
84
113
  "media.failed" => MediaFailedData,
85
- "comment.created" => CommentCreatedData
114
+ "comment.created" => CommentCreatedData,
115
+ "profile_comment.created" => ProfileCommentCreatedData,
116
+ "message.received" => MessageEventData,
117
+ "message.sent" => MessageEventData,
118
+ "message.delivered" => MessageEventData,
119
+ "message.read" => MessageEventData,
120
+ "message.edited" => MessageEventData,
121
+ "message.deleted" => MessageEventData,
122
+ "message.failed_waiting_for_retry" => MessageEventData,
123
+ "message.failed" => MessageEventData,
124
+ "reaction.received" => ReactionEventData
86
125
  }.freeze
87
126
 
88
127
  # Parse a webhook body and return a typed Event. `data` is parsed into the
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.8.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - PostProxy
@@ -78,8 +78,11 @@ files:
78
78
  - lib/postproxy/client.rb
79
79
  - lib/postproxy/constants.rb
80
80
  - lib/postproxy/errors.rb
81
+ - lib/postproxy/resources/chats.rb
81
82
  - lib/postproxy/resources/comments.rb
83
+ - lib/postproxy/resources/messages.rb
82
84
  - lib/postproxy/resources/posts.rb
85
+ - lib/postproxy/resources/profile_comments.rb
83
86
  - lib/postproxy/resources/profile_groups.rb
84
87
  - lib/postproxy/resources/profiles.rb
85
88
  - lib/postproxy/resources/queues.rb