postproxy-sdk 1.9.0 → 1.11.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: 7b20d20d7dc1f6a386e74894a57e470c70a1edc3e95bcd61c3b25f02b3e457cd
4
- data.tar.gz: 6fd6807bcbbd09fae222ccd07eee0d66cbb420bc04e83fd49ab98c4340a4ed82
3
+ metadata.gz: c459486395f3eed2f4199f1946ed7a71c7b6b5de5ae270e46b088e779c772459
4
+ data.tar.gz: 38788f6fd45363f0c229af3cd7fd28a79a21511d1a7b8c4c417bb2d0c5c93f48
5
5
  SHA512:
6
- metadata.gz: c5e18b59805d1aff449eb7e3b8227bd2290b85994ba8e14f38b1c945381facd37a1b77e218d9eaf3c3e8c3183eade0da5268eb86b79762ad1aa9606b015a8e6a
7
- data.tar.gz: ec5a37899a8c3622212e5d7d2b43a540df1e7aff64f9ea99a17a3fd1b0ed1e332a44a7c568eef24882f70fba267fe18302581d9af999ed997651df368dca59cc
6
+ metadata.gz: 8458cc1ca2c4bd873fe980d9b18db5d81b2871c3d027f914a0555ce686cbca1daba5400464f2a950ceb2ba3602657d6e2bf1daa5d5d23c891addbdc0c5b2c54b
7
+ data.tar.gz: 3b9fa99981232895852351741ebdaf2cf5675d585b298100bb29ae1fdca054f67b6e16d9e909ba1db6a15f2fda1b0b06a69f37f6443eceb2680af253e0ca27a3
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,71 @@ 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
318
394
  ```
319
395
 
320
396
  ## Profile comments (Google Business reviews)
@@ -355,6 +431,24 @@ profile = client.profiles.get("prof-id")
355
431
  # Get placements for a profile
356
432
  placements = client.profiles.placements("prof-id").data
357
433
 
434
+ # Move a placement (e.g. a Facebook Page or Telegram channel) to another group
435
+ placement = client.profiles.assign_placement_to_group("prof-id",
436
+ placement_id: "placement-external-id",
437
+ target_profile_group_id: "pg-other"
438
+ )
439
+ puts placement.profile_group_id # => "pg-other"
440
+
441
+ # Ice breakers (Instagram DMs): FAQ prompts shown when a user opens a chat
442
+ result = client.profiles.ice_breakers("prof-id")
443
+ puts result.ice_breakers.map(&:question)
444
+
445
+ client.profiles.set_ice_breakers("prof-id", [
446
+ { question: "What services do you offer?", payload: "services" },
447
+ { question: "What are your hours?", payload: "hours" }
448
+ ]) # 1-4 items
449
+
450
+ client.profiles.delete_ice_breakers("prof-id")
451
+
358
452
  # Delete a profile
359
453
  client.profiles.delete("prof-id")
360
454
 
@@ -445,6 +539,9 @@ platforms = PostProxy::PlatformParams.new(
445
539
  board_id: "board-123"
446
540
  ),
447
541
  threads: PostProxy::ThreadsParams.new(format: "post"),
542
+ # Twitter also supports polls:
543
+ # twitter: PostProxy::TwitterParams.new(format: "poll",
544
+ # poll_options: ["Yes", "No"], poll_duration_minutes: 1440),
448
545
  twitter: PostProxy::TwitterParams.new(format: "post"),
449
546
  bluesky: PostProxy::BlueskyParams.new(format: "post"),
450
547
  telegram: PostProxy::TelegramParams.new(
@@ -8,6 +8,8 @@ require_relative "resources/webhooks"
8
8
  require_relative "resources/queues"
9
9
  require_relative "resources/comments"
10
10
  require_relative "resources/profile_comments"
11
+ require_relative "resources/chats"
12
+ require_relative "resources/messages"
11
13
 
12
14
  module PostProxy
13
15
  class Client
@@ -25,6 +27,8 @@ module PostProxy
25
27
  @queues = nil
26
28
  @comments = nil
27
29
  @profile_comments = nil
30
+ @chats = nil
31
+ @messages = nil
28
32
  end
29
33
 
30
34
  def posts
@@ -55,6 +59,14 @@ module PostProxy
55
59
  @profile_comments ||= Resources::ProfileComments.new(self)
56
60
  end
57
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
+
58
70
  def request(method, path, params: nil, json: nil, data: nil, files: nil, profile_group_id: nil)
59
71
  url = "/api#{path}"
60
72
 
@@ -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
@@ -36,6 +36,41 @@ module PostProxy
36
36
  ProfileStatsResponse.new(data: result[:data])
37
37
  end
38
38
 
39
+ # Moves a placement (e.g. a Facebook Page or Telegram channel) to another
40
+ # profile group. `placement_id` is the placement's external ID as
41
+ # returned by #placements.
42
+ def assign_placement_to_group(id, placement_id:, target_profile_group_id:, profile_group_id: nil)
43
+ result = @client.request(:patch, "/profiles/#{id}/assign_placement_to_group",
44
+ json: {
45
+ placement_id: placement_id,
46
+ target_profile_group_id: target_profile_group_id
47
+ },
48
+ profile_group_id: profile_group_id
49
+ )
50
+ Placement.new(**result)
51
+ end
52
+
53
+ # Lists DM ice breakers. Supported for Instagram profiles only.
54
+ def ice_breakers(id, profile_group_id: nil)
55
+ result = @client.request(:get, "/profiles/#{id}/ice_breakers", profile_group_id: profile_group_id)
56
+ IceBreakersResponse.new(**result)
57
+ end
58
+
59
+ # Replaces the DM ice breakers for a profile (1-4 items).
60
+ def set_ice_breakers(id, ice_breakers, profile_group_id: nil)
61
+ items = ice_breakers.map { |ib| ib.is_a?(IceBreaker) ? ib.to_h : ib }
62
+ result = @client.request(:post, "/profiles/#{id}/ice_breakers",
63
+ json: { ice_breakers: items },
64
+ profile_group_id: profile_group_id
65
+ )
66
+ SuccessResponse.new(**result)
67
+ end
68
+
69
+ def delete_ice_breakers(id, profile_group_id: nil)
70
+ result = @client.request(:delete, "/profiles/#{id}/ice_breakers", profile_group_id: profile_group_id)
71
+ SuccessResponse.new(**result)
72
+ end
73
+
39
74
  def delete(id, profile_group_id: nil)
40
75
  result = @client.request(:delete, "/profiles/#{id}", profile_group_id: profile_group_id)
41
76
  SuccessResponse.new(**result)
@@ -235,7 +235,29 @@ module PostProxy
235
235
  end
236
236
 
237
237
  class Placement < Model
238
- attr_accessor :id, :name
238
+ # `metadata` and `profile_group_id` are present in placements and
239
+ # assign_placement_to_group responses.
240
+ attr_accessor :id, :name, :metadata, :profile_group_id
241
+ end
242
+
243
+ # Instagram DM ice breaker (FAQ prompt). May carry extra platform fields.
244
+ class IceBreaker < Model
245
+ attr_accessor :question, :payload
246
+
247
+ def to_h
248
+ { question: question, payload: payload }
249
+ end
250
+ end
251
+
252
+ class IceBreakersResponse < Model
253
+ attr_accessor :ice_breakers
254
+
255
+ def initialize(**attrs)
256
+ @ice_breakers = (attrs.delete(:ice_breakers) || []).map do |ib|
257
+ ib.is_a?(IceBreaker) ? ib : IceBreaker.new(**ib)
258
+ end
259
+ super
260
+ end
239
261
  end
240
262
 
241
263
  class StatsRecord < Model
@@ -307,25 +329,59 @@ module PostProxy
307
329
  end
308
330
  end
309
331
 
332
+ class Attachment < Model
333
+ attr_accessor :id, :type, :url, :status, :external_id
334
+
335
+ def initialize(**attrs)
336
+ @url = nil
337
+ @external_id = nil
338
+ super
339
+ end
340
+ end
341
+
342
+ class Reaction < Model
343
+ attr_accessor :sender_external_id, :emoji, :reaction, :at
344
+
345
+ def initialize(**attrs)
346
+ @emoji = nil
347
+ @reaction = nil
348
+ @at = nil
349
+ super
350
+ @at = parse_time(@at)
351
+ end
352
+
353
+ private
354
+
355
+ def parse_time(value)
356
+ return nil if value.nil?
357
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
358
+ end
359
+ end
360
+
310
361
  class Comment < Model
311
362
  attr_accessor :id, :external_id, :body, :status, :author_username,
312
- :author_avatar_url, :author_external_id, :parent_external_id,
313
- :like_count, :is_hidden, :permalink, :platform_data,
314
- :posted_at, :created_at, :replies
363
+ :author_avatar_url, :author_external_id, :metadata,
364
+ :parent_external_id, :like_count, :is_hidden, :permalink,
365
+ :platform_data, :attachments, :posted_at, :created_at, :replies
315
366
 
316
367
  def initialize(**attrs)
317
368
  @external_id = nil
318
369
  @author_avatar_url = nil
319
370
  @author_external_id = nil
371
+ @metadata = nil
320
372
  @parent_external_id = nil
321
373
  @like_count = 0
322
374
  @is_hidden = false
323
375
  @permalink = nil
324
376
  @platform_data = nil
377
+ @attachments = []
325
378
  @replies = []
326
379
  super
327
380
  @posted_at = parse_time(@posted_at)
328
381
  @created_at = parse_time(@created_at)
382
+ @attachments = (@attachments || []).map do |a|
383
+ a.is_a?(Attachment) ? a : Attachment.new(**a.transform_keys(&:to_sym))
384
+ end
329
385
  @replies = (@replies || []).map do |r|
330
386
  r.is_a?(Comment) ? r : Comment.new(**r.transform_keys(&:to_sym))
331
387
  end
@@ -339,6 +395,85 @@ module PostProxy
339
395
  end
340
396
  end
341
397
 
398
+ class Chat < Model
399
+ attr_accessor :id, :profile_id, :platform, :participant_external_id,
400
+ :participant_username, :participant_name, :participant_avatar_url,
401
+ :external_conversation_id, :last_inbound_at, :last_outbound_at,
402
+ :last_message_at, :metadata, :archived, :created_at
403
+
404
+ def initialize(**attrs)
405
+ @participant_username = nil
406
+ @participant_name = nil
407
+ @participant_avatar_url = nil
408
+ @external_conversation_id = nil
409
+ @last_inbound_at = nil
410
+ @last_outbound_at = nil
411
+ @last_message_at = nil
412
+ @metadata = nil
413
+ @archived = nil
414
+ super
415
+ @last_inbound_at = parse_time(@last_inbound_at)
416
+ @last_outbound_at = parse_time(@last_outbound_at)
417
+ @last_message_at = parse_time(@last_message_at)
418
+ @created_at = parse_time(@created_at)
419
+ end
420
+
421
+ private
422
+
423
+ def parse_time(value)
424
+ return nil if value.nil?
425
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
426
+ end
427
+ end
428
+
429
+ class Message < Model
430
+ attr_accessor :id, :chat_id, :external_id, :direction, :body, :status,
431
+ :tag, :external_comment_id, :error_message, :platform_data,
432
+ :external_posted_at, :external_delivered_at, :external_read_at,
433
+ :external_edited_at, :reply_to_external_id, :reply_markup,
434
+ :external_deleted_at, :reactions, :attachments,
435
+ :is_unsupported, :created_at
436
+
437
+ def initialize(**attrs)
438
+ @external_id = nil
439
+ @body = nil
440
+ @tag = nil
441
+ @external_comment_id = nil
442
+ @error_message = nil
443
+ @platform_data = nil
444
+ @external_posted_at = nil
445
+ @external_delivered_at = nil
446
+ @external_read_at = nil
447
+ @external_edited_at = nil
448
+ @reply_to_external_id = nil
449
+ @reply_markup = nil
450
+ @external_deleted_at = nil
451
+ @reactions = []
452
+ @attachments = []
453
+ @is_unsupported = false
454
+ super
455
+ @external_posted_at = parse_time(@external_posted_at)
456
+ @external_delivered_at = parse_time(@external_delivered_at)
457
+ @external_read_at = parse_time(@external_read_at)
458
+ @external_edited_at = parse_time(@external_edited_at)
459
+ @external_deleted_at = parse_time(@external_deleted_at)
460
+ @created_at = parse_time(@created_at)
461
+ @reactions = (@reactions || []).map do |r|
462
+ r.is_a?(Reaction) ? r : Reaction.new(**r.transform_keys(&:to_sym))
463
+ end
464
+ @attachments = (@attachments || []).map do |a|
465
+ a.is_a?(Attachment) ? a : Attachment.new(**a.transform_keys(&:to_sym))
466
+ end
467
+ end
468
+
469
+ private
470
+
471
+ def parse_time(value)
472
+ return nil if value.nil?
473
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
474
+ end
475
+ end
476
+
342
477
  class ProfileComment < Model
343
478
  attr_accessor :id, :external_id, :parent_external_id, :placement_id,
344
479
  :body, :status, :author_username, :author_avatar_url,
@@ -483,7 +618,9 @@ module PostProxy
483
618
  end
484
619
 
485
620
  class TwitterParams < Model
486
- attr_accessor :format
621
+ # poll_options and poll_duration_minutes are required when format is
622
+ # "poll": 2-4 options (max 25 chars each), duration 5 to 10080 minutes.
623
+ attr_accessor :format, :poll_options, :poll_duration_minutes
487
624
  end
488
625
 
489
626
  class BlueskyParams < Model
@@ -1,3 +1,3 @@
1
1
  module PostProxy
2
- VERSION = "1.9.0"
2
+ VERSION = "1.11.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.9.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - PostProxy
@@ -78,7 +78,9 @@ 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
83
85
  - lib/postproxy/resources/profile_comments.rb
84
86
  - lib/postproxy/resources/profile_groups.rb