postproxy-sdk 1.1.0 → 1.4.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: a9a60fab0c383896df7be59f688fb8692ab12eb60725d031fac50e76d7acc507
4
- data.tar.gz: 38cc6ceda7bb03d4adb4e2bdfe7a27155ee0d4ef20b39d6d2b82d8e115284201
3
+ metadata.gz: 8aa3207765c712cdc5887c3c94ae33010ec0e01f8487c867641929f84d8f08e7
4
+ data.tar.gz: 1f40051ca925f4eda94ce0883af44f456c564666afd4f4e766ef9040ff48e6db
5
5
  SHA512:
6
- metadata.gz: b314e1009d278281897233d3059b3a04c29d2729a86d9f06299113d11cb973788b01a29bb9393e5ac8e9bf62a08f63e9de35eabcc8a033e943350d774236c24c
7
- data.tar.gz: c2142242f1ee28e96152c6de5869aca4821de27f6eb7b756c05034d2546d80a941fa0fd530ec823b6d6b1fde5e9b77f88aecc6503998d5bd143dbdc54e8fca09
6
+ metadata.gz: a035631e203fdefda661eda488dd7384b4690e8dd490b430d35ed73d421e49a11927a7282ee489e197a16975a0874e8d95ec0337b347ef5b141c30c570f69301
7
+ data.tar.gz: 36e967ff4467ee46f261489cfc770ac09e3e0d2a092dc8faa6e6186585fd8b4df3cc9f3abc7db5cd2607befd3603715f835da647c1aa5213eebe63ab920fcfec
data/README.md CHANGED
@@ -92,6 +92,17 @@ post = client.posts.create(
92
92
  scheduled_at: (Time.now + 3600).iso8601
93
93
  )
94
94
 
95
+ # Create a thread post
96
+ post = client.posts.create(
97
+ "Thread starts here",
98
+ profiles: ["prof-1"],
99
+ thread: [
100
+ { body: "Second post in the thread" },
101
+ { body: "Third with media", media: ["https://example.com/img.jpg"] },
102
+ ]
103
+ )
104
+ post.thread.each { |child| puts "#{child.id}: #{child.body}" }
105
+
95
106
  # Delete a post
96
107
  client.posts.delete("post-id")
97
108
  ```
@@ -146,6 +157,97 @@ Stats vary by platform:
146
157
  | TikTok | `impressions`, `likes`, `comments`, `shares` |
147
158
  | Pinterest | `impressions`, `likes`, `comments`, `saved`, `outbound_clicks` |
148
159
 
160
+ ## Queues
161
+
162
+ ```ruby
163
+ # List all queues
164
+ queues = client.queues.list.data
165
+
166
+ # Get a queue
167
+ queue = client.queues.get("queue-id")
168
+
169
+ # Get next available slot
170
+ next_slot = client.queues.next_slot("queue-id")
171
+ puts next_slot.next_slot
172
+
173
+ # Create a queue with timeslots
174
+ queue = client.queues.create(
175
+ "Morning Posts",
176
+ profile_group_id: "pg-abc",
177
+ description: "Weekday morning content",
178
+ timezone: "America/New_York",
179
+ jitter: 10,
180
+ timeslots: [
181
+ { day: 1, time: "09:00" },
182
+ { day: 2, time: "09:00" },
183
+ { day: 3, time: "09:00" },
184
+ ]
185
+ )
186
+
187
+ # Update a queue
188
+ queue = client.queues.update("queue-id",
189
+ jitter: 15,
190
+ timeslots: [
191
+ { day: 6, time: "10:00" }, # add new timeslot
192
+ { id: 1, _destroy: true }, # remove existing timeslot
193
+ ]
194
+ )
195
+
196
+ # Pause/unpause a queue
197
+ client.queues.update("queue-id", enabled: false)
198
+
199
+ # Delete a queue
200
+ client.queues.delete("queue-id")
201
+
202
+ # Add a post to a queue
203
+ post = client.posts.create(
204
+ "This post will be scheduled by the queue",
205
+ profiles: ["prof-1"],
206
+ queue_id: "queue-id",
207
+ queue_priority: "high"
208
+ )
209
+ ```
210
+
211
+ ## Webhooks
212
+
213
+ ```ruby
214
+ # List webhooks
215
+ webhooks = client.webhooks.list.data
216
+
217
+ # Get a webhook
218
+ webhook = client.webhooks.get("wh-id")
219
+
220
+ # Create a webhook
221
+ webhook = client.webhooks.create(
222
+ "https://example.com/webhook",
223
+ events: ["post.published", "post.failed"],
224
+ description: "My webhook"
225
+ )
226
+ puts webhook.id, webhook.secret
227
+
228
+ # Update a webhook
229
+ webhook = client.webhooks.update("wh-id", events: ["post.published"], enabled: false)
230
+
231
+ # Delete a webhook
232
+ client.webhooks.delete("wh-id")
233
+
234
+ # List deliveries
235
+ deliveries = client.webhooks.deliveries("wh-id", page: 1, per_page: 10)
236
+ deliveries.data.each { |d| puts "#{d.event_type}: #{d.success}" }
237
+ ```
238
+
239
+ ### Signature verification
240
+
241
+ Verify incoming webhook signatures using HMAC-SHA256:
242
+
243
+ ```ruby
244
+ PostProxy::WebhookSignature.verify(
245
+ payload: request.body.read,
246
+ signature_header: request.headers["X-PostProxy-Signature"],
247
+ secret: "whsec_..."
248
+ )
249
+ ```
250
+
149
251
  ## Profiles
150
252
 
151
253
  ```ruby
@@ -4,6 +4,8 @@ require "json"
4
4
  require_relative "resources/posts"
5
5
  require_relative "resources/profiles"
6
6
  require_relative "resources/profile_groups"
7
+ require_relative "resources/webhooks"
8
+ require_relative "resources/queues"
7
9
 
8
10
  module PostProxy
9
11
  class Client
@@ -17,6 +19,8 @@ module PostProxy
17
19
  @posts = nil
18
20
  @profiles = nil
19
21
  @profile_groups = nil
22
+ @webhooks = nil
23
+ @queues = nil
20
24
  end
21
25
 
22
26
  def posts
@@ -31,6 +35,14 @@ module PostProxy
31
35
  @profile_groups ||= Resources::ProfileGroups.new(self)
32
36
  end
33
37
 
38
+ def webhooks
39
+ @webhooks ||= Resources::Webhooks.new(self)
40
+ end
41
+
42
+ def queues
43
+ @queues ||= Resources::Queues.new(self)
44
+ end
45
+
34
46
  def request(method, path, params: nil, json: nil, data: nil, files: nil, profile_group_id: nil)
35
47
  url = "/api#{path}"
36
48
 
@@ -7,7 +7,9 @@ module PostProxy
7
7
 
8
8
  PROFILE_STATUSES = %w[active expired inactive].freeze
9
9
 
10
- POST_STATUSES = %w[pending draft processing processed scheduled].freeze
10
+ POST_STATUSES = %w[pending draft processing processed scheduled media_processing_failed].freeze
11
+
12
+ MEDIA_STATUSES = %w[pending processed failed].freeze
11
13
 
12
14
  PLATFORM_POST_STATUSES = %w[pending processing published failed deleted].freeze
13
15
 
@@ -29,8 +29,12 @@ module PostProxy
29
29
  end
30
30
 
31
31
  def create(body, profiles:, media: nil, media_files: nil, platforms: nil,
32
- scheduled_at: nil, draft: nil, profile_group_id: nil)
33
- if media_files && !media_files.empty?
32
+ thread: nil, scheduled_at: nil, draft: nil, queue_id: nil,
33
+ queue_priority: nil, profile_group_id: nil)
34
+ has_files = media_files && !media_files.empty?
35
+ has_thread_files = thread&.any? { |t| t[:media_files]&.any? }
36
+
37
+ if has_files || has_thread_files
34
38
  form_data = { "post[body]" => body }
35
39
  form_data["post[scheduled_at]"] = format_time(scheduled_at) if scheduled_at
36
40
  form_data["post[draft]"] = draft.to_s if !draft.nil?
@@ -54,7 +58,7 @@ module PostProxy
54
58
  end
55
59
  end
56
60
 
57
- media_files.each do |path|
61
+ media_files&.each do |path|
58
62
  path = path.to_s
59
63
  filename = File.basename(path)
60
64
  content_type = mime_type_for(filename)
@@ -62,6 +66,22 @@ module PostProxy
62
66
  files << ["media[]", filename, io, content_type]
63
67
  end
64
68
 
69
+ thread&.each_with_index do |t, i|
70
+ form_data["thread[#{i}][body]"] = t[:body] if t[:body]
71
+
72
+ t[:media]&.each do |m|
73
+ files << ["thread[#{i}][media][]", nil, m, "text/plain"]
74
+ end
75
+
76
+ t[:media_files]&.each do |path|
77
+ path = path.to_s
78
+ filename = File.basename(path)
79
+ content_type = mime_type_for(filename)
80
+ io = File.open(path, "rb")
81
+ files << ["thread[#{i}][media][]", filename, io, content_type]
82
+ end
83
+ end
84
+
65
85
  result = @client.request(:post, "/posts",
66
86
  data: form_data,
67
87
  files: files,
@@ -75,6 +95,9 @@ module PostProxy
75
95
  json_body = { post: post_payload, profiles: profiles }
76
96
  json_body[:platforms] = platforms.is_a?(PlatformParams) ? platforms.to_h : platforms if platforms
77
97
  json_body[:media] = media if media
98
+ json_body[:thread] = thread if thread
99
+ json_body[:queue_id] = queue_id if queue_id
100
+ json_body[:queue_priority] = queue_priority if queue_priority
78
101
 
79
102
  result = @client.request(:post, "/posts", json: json_body, profile_group_id: profile_group_id)
80
103
  end
@@ -0,0 +1,61 @@
1
+ module PostProxy
2
+ module Resources
3
+ class Queues
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list(profile_group_id: nil)
9
+ result = @client.request(:get, "/post_queues", profile_group_id: profile_group_id)
10
+ queues = (result[:data] || []).map { |q| Queue.new(**q) }
11
+ ListResponse.new(data: queues)
12
+ end
13
+
14
+ def get(id)
15
+ result = @client.request(:get, "/post_queues/#{id}")
16
+ Queue.new(**result)
17
+ end
18
+
19
+ def next_slot(id)
20
+ result = @client.request(:get, "/post_queues/#{id}/next_slot")
21
+ NextSlotResponse.new(**result)
22
+ end
23
+
24
+ def create(name, profile_group_id:, description: nil, timezone: nil, jitter: nil, timeslots: nil)
25
+ post_queue = { name: name }
26
+ post_queue[:description] = description if description
27
+ post_queue[:timezone] = timezone if timezone
28
+ post_queue[:jitter] = jitter unless jitter.nil?
29
+ post_queue[:queue_timeslots_attributes] = timeslots if timeslots
30
+
31
+ json_body = {
32
+ profile_group_id: profile_group_id,
33
+ post_queue: post_queue,
34
+ }
35
+
36
+ result = @client.request(:post, "/post_queues", json: json_body)
37
+ Queue.new(**result)
38
+ end
39
+
40
+ def update(id, name: nil, description: nil, timezone: nil, enabled: nil, jitter: nil, timeslots: nil)
41
+ post_queue = {}
42
+ post_queue[:name] = name unless name.nil?
43
+ post_queue[:description] = description unless description.nil?
44
+ post_queue[:timezone] = timezone unless timezone.nil?
45
+ post_queue[:enabled] = enabled unless enabled.nil?
46
+ post_queue[:jitter] = jitter unless jitter.nil?
47
+ post_queue[:queue_timeslots_attributes] = timeslots if timeslots
48
+
49
+ json_body = { post_queue: post_queue }
50
+
51
+ result = @client.request(:patch, "/post_queues/#{id}", json: json_body)
52
+ Queue.new(**result)
53
+ end
54
+
55
+ def delete(id)
56
+ result = @client.request(:delete, "/post_queues/#{id}")
57
+ DeleteResponse.new(**result)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,59 @@
1
+ module PostProxy
2
+ module Resources
3
+ class Webhooks
4
+ def initialize(client)
5
+ @client = client
6
+ end
7
+
8
+ def list
9
+ result = @client.request(:get, "/webhooks")
10
+ webhooks = (result[:data] || []).map { |w| Webhook.new(**w) }
11
+ ListResponse.new(data: webhooks)
12
+ end
13
+
14
+ def get(id)
15
+ result = @client.request(:get, "/webhooks/#{id}")
16
+ Webhook.new(**result)
17
+ end
18
+
19
+ def create(url, events:, description: nil)
20
+ json_body = { url: url, events: events }
21
+ json_body[:description] = description if description
22
+
23
+ result = @client.request(:post, "/webhooks", json: json_body)
24
+ Webhook.new(**result)
25
+ end
26
+
27
+ def update(id, url: nil, events: nil, enabled: nil, description: nil)
28
+ json_body = {}
29
+ json_body[:url] = url unless url.nil?
30
+ json_body[:events] = events unless events.nil?
31
+ json_body[:enabled] = enabled unless enabled.nil?
32
+ json_body[:description] = description unless description.nil?
33
+
34
+ result = @client.request(:patch, "/webhooks/#{id}", json: json_body)
35
+ Webhook.new(**result)
36
+ end
37
+
38
+ def delete(id)
39
+ result = @client.request(:delete, "/webhooks/#{id}")
40
+ DeleteResponse.new(**result)
41
+ end
42
+
43
+ def deliveries(id, page: nil, per_page: nil)
44
+ params = {}
45
+ params[:page] = page if page
46
+ params[:per_page] = per_page if per_page
47
+
48
+ result = @client.request(:get, "/webhooks/#{id}/deliveries", params: params)
49
+ deliveries = (result[:data] || []).map { |d| WebhookDelivery.new(**d) }
50
+ PaginatedResponse.new(
51
+ data: deliveries,
52
+ total: result[:total],
53
+ page: result[:page],
54
+ per_page: result[:per_page]
55
+ )
56
+ end
57
+ end
58
+ end
59
+ end
@@ -84,18 +84,115 @@ module PostProxy
84
84
  end
85
85
  end
86
86
 
87
+ class Media < Model
88
+ attr_accessor :id, :status, :error_message, :content_type, :source_url, :url
89
+
90
+ def initialize(**attrs)
91
+ @error_message = nil
92
+ @source_url = nil
93
+ @url = nil
94
+ super
95
+ end
96
+ end
97
+
98
+ class ThreadChild < Model
99
+ attr_accessor :id, :body, :media
100
+
101
+ def initialize(**attrs)
102
+ @media = []
103
+ super
104
+ @media = (@media || []).map do |m|
105
+ m.is_a?(Media) ? m : Media.new(**m.transform_keys(&:to_sym))
106
+ end
107
+ end
108
+ end
109
+
110
+ class Timeslot < Model
111
+ attr_accessor :id, :day, :time
112
+ end
113
+
114
+ class Queue < Model
115
+ attr_accessor :id, :name, :description, :timezone, :enabled, :jitter,
116
+ :profile_group_id, :timeslots, :posts_count
117
+
118
+ def initialize(**attrs)
119
+ @description = nil
120
+ @timeslots = []
121
+ @posts_count = 0
122
+ super
123
+ @timeslots = (@timeslots || []).map do |t|
124
+ t.is_a?(Timeslot) ? t : Timeslot.new(**t.transform_keys(&:to_sym))
125
+ end
126
+ end
127
+ end
128
+
129
+ class NextSlotResponse < Model
130
+ attr_accessor :next_slot
131
+ end
132
+
87
133
  class Post < Model
88
- attr_accessor :id, :body, :status, :scheduled_at, :created_at, :platforms
134
+ attr_accessor :id, :body, :status, :scheduled_at, :created_at, :media, :platforms, :thread,
135
+ :queue_id, :queue_priority
89
136
 
90
137
  def initialize(**attrs)
91
138
  @scheduled_at = nil
139
+ @media = []
92
140
  @platforms = []
141
+ @thread = []
142
+ @queue_id = nil
143
+ @queue_priority = nil
93
144
  super
94
145
  @scheduled_at = parse_time(@scheduled_at)
95
146
  @created_at = parse_time(@created_at)
147
+ @media = (@media || []).map do |m|
148
+ m.is_a?(Media) ? m : Media.new(**m.transform_keys(&:to_sym))
149
+ end
96
150
  @platforms = (@platforms || []).map do |p|
97
151
  p.is_a?(PlatformResult) ? p : PlatformResult.new(**p.transform_keys(&:to_sym))
98
152
  end
153
+ @thread = (@thread || []).map do |t|
154
+ t.is_a?(ThreadChild) ? t : ThreadChild.new(**t.transform_keys(&:to_sym))
155
+ end
156
+ end
157
+
158
+ private
159
+
160
+ def parse_time(value)
161
+ return nil if value.nil?
162
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
163
+ end
164
+ end
165
+
166
+ class Webhook < Model
167
+ attr_accessor :id, :url, :events, :enabled, :description, :secret,
168
+ :created_at, :updated_at
169
+
170
+ def initialize(**attrs)
171
+ @events = []
172
+ @description = nil
173
+ @secret = nil
174
+ super
175
+ @created_at = parse_time(@created_at)
176
+ @updated_at = parse_time(@updated_at)
177
+ end
178
+
179
+ private
180
+
181
+ def parse_time(value)
182
+ return nil if value.nil?
183
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
184
+ end
185
+ end
186
+
187
+ class WebhookDelivery < Model
188
+ attr_accessor :id, :event_id, :event_type, :response_status,
189
+ :attempt_number, :success, :attempted_at, :created_at
190
+
191
+ def initialize(**attrs)
192
+ @response_status = nil
193
+ super
194
+ @attempted_at = parse_time(@attempted_at)
195
+ @created_at = parse_time(@created_at)
99
196
  end
100
197
 
101
198
  private
@@ -213,7 +310,7 @@ module PostProxy
213
310
  end
214
311
 
215
312
  class YouTubeParams < Model
216
- attr_accessor :format, :title, :privacy_status, :cover_url
313
+ attr_accessor :format, :title, :privacy_status, :cover_url, :made_for_kids
217
314
  end
218
315
 
219
316
  class PinterestParams < Model
@@ -1,3 +1,3 @@
1
1
  module PostProxy
2
- VERSION = "1.1.0"
2
+ VERSION = "1.4.0"
3
3
  end
@@ -0,0 +1,28 @@
1
+ require "openssl"
2
+
3
+ module PostProxy
4
+ module WebhookSignature
5
+ def self.verify(payload, signature_header, secret)
6
+ parts = signature_header.split(",").map { |p| p.split("=", 2) }.to_h
7
+ timestamp = parts["t"]
8
+ expected = parts["v1"]
9
+
10
+ return false if timestamp.nil? || expected.nil?
11
+
12
+ signed_payload = "#{timestamp}.#{payload}"
13
+ computed = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
14
+
15
+ secure_compare(computed, expected)
16
+ end
17
+
18
+ private_class_method def self.secure_compare(a, b)
19
+ return false unless a.bytesize == b.bytesize
20
+
21
+ l = a.unpack("C*")
22
+ r = b.unpack("C*")
23
+ result = 0
24
+ l.zip(r) { |x, y| result |= x ^ y }
25
+ result.zero?
26
+ end
27
+ end
28
+ end
data/lib/postproxy.rb CHANGED
@@ -3,6 +3,7 @@ require_relative "postproxy/constants"
3
3
  require_relative "postproxy/errors"
4
4
  require_relative "postproxy/types"
5
5
  require_relative "postproxy/client"
6
+ require_relative "postproxy/webhook_signature"
6
7
 
7
8
  module PostProxy
8
9
  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.1.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - PostProxy
@@ -81,8 +81,11 @@ files:
81
81
  - lib/postproxy/resources/posts.rb
82
82
  - lib/postproxy/resources/profile_groups.rb
83
83
  - lib/postproxy/resources/profiles.rb
84
+ - lib/postproxy/resources/queues.rb
85
+ - lib/postproxy/resources/webhooks.rb
84
86
  - lib/postproxy/types.rb
85
87
  - lib/postproxy/version.rb
88
+ - lib/postproxy/webhook_signature.rb
86
89
  homepage: https://postproxy.dev
87
90
  licenses:
88
91
  - MIT