postproxy-sdk 1.1.0 → 1.2.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: 665c9b0d7745b0da8ae0bd7864047997816bfa532725ee338588b94f1e6e9724
4
+ data.tar.gz: d9ebae2aa90d310886561d4ec4b2da268a2dcd6d341b59343c1661c3429b656c
5
5
  SHA512:
6
- metadata.gz: b314e1009d278281897233d3059b3a04c29d2729a86d9f06299113d11cb973788b01a29bb9393e5ac8e9bf62a08f63e9de35eabcc8a033e943350d774236c24c
7
- data.tar.gz: c2142242f1ee28e96152c6de5869aca4821de27f6eb7b756c05034d2546d80a941fa0fd530ec823b6d6b1fde5e9b77f88aecc6503998d5bd143dbdc54e8fca09
6
+ metadata.gz: 32384b1c8ca96c498094d59530fd2617a542a911342ef969c93243f31da53afeb06a5d9de458413bfd0ef584c09c53a207743d0a217d49fb1dc097972e2e697e
7
+ data.tar.gz: 15c9e8c42a588f99fe9ef0eb93536c7206f3a90b16d75cd1648bb1497ec5fb3b9e00fae71de3abad3b2a4c2ca442c88aea54ba129b58f6d4dc283d8c9a07de0d
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,46 @@ Stats vary by platform:
146
157
  | TikTok | `impressions`, `likes`, `comments`, `shares` |
147
158
  | Pinterest | `impressions`, `likes`, `comments`, `saved`, `outbound_clicks` |
148
159
 
160
+ ## Webhooks
161
+
162
+ ```ruby
163
+ # List webhooks
164
+ webhooks = client.webhooks.list.data
165
+
166
+ # Get a webhook
167
+ webhook = client.webhooks.get("wh-id")
168
+
169
+ # Create a webhook
170
+ webhook = client.webhooks.create(
171
+ "https://example.com/webhook",
172
+ events: ["post.published", "post.failed"],
173
+ description: "My webhook"
174
+ )
175
+ puts webhook.id, webhook.secret
176
+
177
+ # Update a webhook
178
+ webhook = client.webhooks.update("wh-id", events: ["post.published"], enabled: false)
179
+
180
+ # Delete a webhook
181
+ client.webhooks.delete("wh-id")
182
+
183
+ # List deliveries
184
+ deliveries = client.webhooks.deliveries("wh-id", page: 1, per_page: 10)
185
+ deliveries.data.each { |d| puts "#{d.event_type}: #{d.success}" }
186
+ ```
187
+
188
+ ### Signature verification
189
+
190
+ Verify incoming webhook signatures using HMAC-SHA256:
191
+
192
+ ```ruby
193
+ PostProxy::WebhookSignature.verify(
194
+ payload: request.body.read,
195
+ signature_header: request.headers["X-PostProxy-Signature"],
196
+ secret: "whsec_..."
197
+ )
198
+ ```
199
+
149
200
  ## Profiles
150
201
 
151
202
  ```ruby
@@ -4,6 +4,7 @@ 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"
7
8
 
8
9
  module PostProxy
9
10
  class Client
@@ -17,6 +18,7 @@ module PostProxy
17
18
  @posts = nil
18
19
  @profiles = nil
19
20
  @profile_groups = nil
21
+ @webhooks = nil
20
22
  end
21
23
 
22
24
  def posts
@@ -31,6 +33,10 @@ module PostProxy
31
33
  @profile_groups ||= Resources::ProfileGroups.new(self)
32
34
  end
33
35
 
36
+ def webhooks
37
+ @webhooks ||= Resources::Webhooks.new(self)
38
+ end
39
+
34
40
  def request(method, path, params: nil, json: nil, data: nil, files: nil, profile_group_id: nil)
35
41
  url = "/api#{path}"
36
42
 
@@ -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,11 @@ 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, profile_group_id: nil)
33
+ has_files = media_files && !media_files.empty?
34
+ has_thread_files = thread&.any? { |t| t[:media_files]&.any? }
35
+
36
+ if has_files || has_thread_files
34
37
  form_data = { "post[body]" => body }
35
38
  form_data["post[scheduled_at]"] = format_time(scheduled_at) if scheduled_at
36
39
  form_data["post[draft]"] = draft.to_s if !draft.nil?
@@ -54,7 +57,7 @@ module PostProxy
54
57
  end
55
58
  end
56
59
 
57
- media_files.each do |path|
60
+ media_files&.each do |path|
58
61
  path = path.to_s
59
62
  filename = File.basename(path)
60
63
  content_type = mime_type_for(filename)
@@ -62,6 +65,22 @@ module PostProxy
62
65
  files << ["media[]", filename, io, content_type]
63
66
  end
64
67
 
68
+ thread&.each_with_index do |t, i|
69
+ form_data["thread[#{i}][body]"] = t[:body] if t[:body]
70
+
71
+ t[:media]&.each do |m|
72
+ files << ["thread[#{i}][media][]", nil, m, "text/plain"]
73
+ end
74
+
75
+ t[:media_files]&.each do |path|
76
+ path = path.to_s
77
+ filename = File.basename(path)
78
+ content_type = mime_type_for(filename)
79
+ io = File.open(path, "rb")
80
+ files << ["thread[#{i}][media][]", filename, io, content_type]
81
+ end
82
+ end
83
+
65
84
  result = @client.request(:post, "/posts",
66
85
  data: form_data,
67
86
  files: files,
@@ -75,6 +94,7 @@ module PostProxy
75
94
  json_body = { post: post_payload, profiles: profiles }
76
95
  json_body[:platforms] = platforms.is_a?(PlatformParams) ? platforms.to_h : platforms if platforms
77
96
  json_body[:media] = media if media
97
+ json_body[:thread] = thread if thread
78
98
 
79
99
  result = @client.request(:post, "/posts", json: json_body, profile_group_id: profile_group_id)
80
100
  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,89 @@ 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
+
87
110
  class Post < Model
88
- attr_accessor :id, :body, :status, :scheduled_at, :created_at, :platforms
111
+ attr_accessor :id, :body, :status, :scheduled_at, :created_at, :media, :platforms, :thread
89
112
 
90
113
  def initialize(**attrs)
91
114
  @scheduled_at = nil
115
+ @media = []
92
116
  @platforms = []
117
+ @thread = []
93
118
  super
94
119
  @scheduled_at = parse_time(@scheduled_at)
95
120
  @created_at = parse_time(@created_at)
121
+ @media = (@media || []).map do |m|
122
+ m.is_a?(Media) ? m : Media.new(**m.transform_keys(&:to_sym))
123
+ end
96
124
  @platforms = (@platforms || []).map do |p|
97
125
  p.is_a?(PlatformResult) ? p : PlatformResult.new(**p.transform_keys(&:to_sym))
98
126
  end
127
+ @thread = (@thread || []).map do |t|
128
+ t.is_a?(ThreadChild) ? t : ThreadChild.new(**t.transform_keys(&:to_sym))
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ def parse_time(value)
135
+ return nil if value.nil?
136
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
137
+ end
138
+ end
139
+
140
+ class Webhook < Model
141
+ attr_accessor :id, :url, :events, :enabled, :description, :secret,
142
+ :created_at, :updated_at
143
+
144
+ def initialize(**attrs)
145
+ @events = []
146
+ @description = nil
147
+ @secret = nil
148
+ super
149
+ @created_at = parse_time(@created_at)
150
+ @updated_at = parse_time(@updated_at)
151
+ end
152
+
153
+ private
154
+
155
+ def parse_time(value)
156
+ return nil if value.nil?
157
+ value.is_a?(Time) ? value : Time.parse(value.to_s)
158
+ end
159
+ end
160
+
161
+ class WebhookDelivery < Model
162
+ attr_accessor :id, :event_id, :event_type, :response_status,
163
+ :attempt_number, :success, :attempted_at, :created_at
164
+
165
+ def initialize(**attrs)
166
+ @response_status = nil
167
+ super
168
+ @attempted_at = parse_time(@attempted_at)
169
+ @created_at = parse_time(@created_at)
99
170
  end
100
171
 
101
172
  private
@@ -213,7 +284,7 @@ module PostProxy
213
284
  end
214
285
 
215
286
  class YouTubeParams < Model
216
- attr_accessor :format, :title, :privacy_status, :cover_url
287
+ attr_accessor :format, :title, :privacy_status, :cover_url, :made_for_kids
217
288
  end
218
289
 
219
290
  class PinterestParams < Model
@@ -1,3 +1,3 @@
1
1
  module PostProxy
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - PostProxy
@@ -81,8 +81,10 @@ 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/webhooks.rb
84
85
  - lib/postproxy/types.rb
85
86
  - lib/postproxy/version.rb
87
+ - lib/postproxy/webhook_signature.rb
86
88
  homepage: https://postproxy.dev
87
89
  licenses:
88
90
  - MIT