postproxy-sdk 1.6.0 → 1.8.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 +4 -4
- data/README.md +76 -1
- data/lib/postproxy/client.rb +6 -0
- data/lib/postproxy/constants.rb +19 -1
- data/lib/postproxy/resources/posts.rb +23 -2
- data/lib/postproxy/resources/profile_groups.rb +20 -3
- data/lib/postproxy/resources/profiles.rb +14 -0
- data/lib/postproxy/types.rb +93 -4
- data/lib/postproxy/version.rb +1 -1
- data/lib/postproxy/webhook_events.rb +113 -0
- data/lib/postproxy.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d2f9ae8cbbc2ca3cd324b5dc522226c9d7a77cd93430abb309b31efc210a293
|
|
4
|
+
data.tar.gz: 26c58e5f9b7ea1af4eccb3935ef8c71bf202bf5bff93898d0b30a5450114bb1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 906800719560c2e37351c958fa72449d1411b351c1cad4deb0d312e9b6fcaa3af3be3dcb504eeef045a3d24737815afb4e7f772edae4687e102e2f9d4bd605cc
|
|
7
|
+
data.tar.gz: b6ee51b608b91a9d31ed2891b7bba87df996006be1795504f540e275dd54c8ff3b7ef7e5f1994a7aa495a02f67ec8f3921e9017baadb3e1e8d3c5550d6959b06
|
data/README.md
CHANGED
|
@@ -105,6 +105,18 @@ post.thread.each { |child| puts "#{child.id}: #{child.body}" }
|
|
|
105
105
|
|
|
106
106
|
# Delete a post
|
|
107
107
|
client.posts.delete("post-id")
|
|
108
|
+
|
|
109
|
+
# Delete a post and also remove it from social platforms
|
|
110
|
+
client.posts.delete("post-id", delete_on_platform: true)
|
|
111
|
+
|
|
112
|
+
# Delete from platforms only (keeps DB record). Defaults to all platforms.
|
|
113
|
+
client.posts.delete_on_platform("post-id")
|
|
114
|
+
# Target a single network
|
|
115
|
+
client.posts.delete_on_platform("post-id", network: "twitter")
|
|
116
|
+
# Target a specific profile
|
|
117
|
+
client.posts.delete_on_platform("post-id", profile_id: "prof-abc")
|
|
118
|
+
# Target a specific post profile (covers entire thread for that profile)
|
|
119
|
+
client.posts.delete_on_platform("post-id", post_profile_id: "pp-abc")
|
|
108
120
|
```
|
|
109
121
|
|
|
110
122
|
## Post Stats
|
|
@@ -248,6 +260,26 @@ PostProxy::WebhookSignature.verify(
|
|
|
248
260
|
)
|
|
249
261
|
```
|
|
250
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
|
+
|
|
251
283
|
## Comments
|
|
252
284
|
|
|
253
285
|
```ruby
|
|
@@ -299,6 +331,19 @@ placements = client.profiles.placements("prof-id").data
|
|
|
299
331
|
|
|
300
332
|
# Delete a profile
|
|
301
333
|
client.profiles.delete("prof-id")
|
|
334
|
+
|
|
335
|
+
# Profile stats timeseries — placement_id required for facebook, linkedin, telegram
|
|
336
|
+
stats = client.profiles.get_profile_stats("prof_li_001",
|
|
337
|
+
placement_id: "108520199",
|
|
338
|
+
from: "2026-04-01T00:00:00Z"
|
|
339
|
+
)
|
|
340
|
+
stats.data.records.each do |r|
|
|
341
|
+
puts "#{r.recorded_at}: #{r.stats[:followerCount]}"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Bluesky — no placements
|
|
345
|
+
bsky = client.profiles.get_profile_stats("prof_bsky_001")
|
|
346
|
+
puts bsky.data.records.last.stats[:followersCount]
|
|
302
347
|
```
|
|
303
348
|
|
|
304
349
|
## Profile Groups
|
|
@@ -323,6 +368,28 @@ connection = client.profile_groups.initialize_connection(
|
|
|
323
368
|
redirect_url: "https://myapp.com/callback"
|
|
324
369
|
)
|
|
325
370
|
# Redirect user to connection.url
|
|
371
|
+
|
|
372
|
+
# BlueSky — app password (synchronous, no OAuth)
|
|
373
|
+
bsky = client.profile_groups.connect_bluesky("pg-id",
|
|
374
|
+
identifier: "yourname.bsky.social",
|
|
375
|
+
app_password: "xxxx-xxxx-xxxx-xxxx"
|
|
376
|
+
)
|
|
377
|
+
puts bsky.profile.id
|
|
378
|
+
|
|
379
|
+
# Telegram — bring-your-own-bot. Channels populate asynchronously; poll
|
|
380
|
+
# placements until non-empty.
|
|
381
|
+
tg = client.profile_groups.connect_telegram("pg-id",
|
|
382
|
+
bot_token: "123456789:ABCdef-GhIJklMnOpQrStUvWxYz"
|
|
383
|
+
)
|
|
384
|
+
puts tg.next_step
|
|
385
|
+
|
|
386
|
+
placements = []
|
|
387
|
+
loop do
|
|
388
|
+
placements = client.profiles.placements(tg.profile.id).data
|
|
389
|
+
break unless placements.empty?
|
|
390
|
+
sleep 3
|
|
391
|
+
end
|
|
392
|
+
puts "Channels: #{placements.map { |p| [p.id, p.name] }}"
|
|
326
393
|
```
|
|
327
394
|
|
|
328
395
|
## Platform Parameters
|
|
@@ -352,7 +419,13 @@ platforms = PostProxy::PlatformParams.new(
|
|
|
352
419
|
board_id: "board-123"
|
|
353
420
|
),
|
|
354
421
|
threads: PostProxy::ThreadsParams.new(format: "post"),
|
|
355
|
-
twitter: PostProxy::TwitterParams.new(format: "post")
|
|
422
|
+
twitter: PostProxy::TwitterParams.new(format: "post"),
|
|
423
|
+
bluesky: PostProxy::BlueskyParams.new(format: "post"),
|
|
424
|
+
telegram: PostProxy::TelegramParams.new(
|
|
425
|
+
chat_id: "-1001234567890",
|
|
426
|
+
parse_mode: "HTML",
|
|
427
|
+
disable_link_preview: true
|
|
428
|
+
)
|
|
356
429
|
)
|
|
357
430
|
|
|
358
431
|
post = client.posts.create(
|
|
@@ -362,6 +435,8 @@ post = client.posts.create(
|
|
|
362
435
|
)
|
|
363
436
|
```
|
|
364
437
|
|
|
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)`.
|
|
439
|
+
|
|
365
440
|
## Error Handling
|
|
366
441
|
|
|
367
442
|
```ruby
|
data/lib/postproxy/client.rb
CHANGED
|
@@ -99,11 +99,16 @@ module PostProxy
|
|
|
99
99
|
|
|
100
100
|
private
|
|
101
101
|
|
|
102
|
+
def user_agent
|
|
103
|
+
@user_agent ||= "postproxy-ruby/#{PostProxy::VERSION} (ruby/#{RUBY_VERSION})"
|
|
104
|
+
end
|
|
105
|
+
|
|
102
106
|
def json_connection
|
|
103
107
|
@faraday_client || Faraday.new(url: @base_url) do |f|
|
|
104
108
|
f.request :url_encoded
|
|
105
109
|
f.headers["Authorization"] = "Bearer #{@api_key}"
|
|
106
110
|
f.headers["Content-Type"] = "application/json"
|
|
111
|
+
f.headers["User-Agent"] = user_agent
|
|
107
112
|
f.adapter Faraday.default_adapter
|
|
108
113
|
end
|
|
109
114
|
end
|
|
@@ -112,6 +117,7 @@ module PostProxy
|
|
|
112
117
|
@faraday_client || Faraday.new(url: @base_url) do |f|
|
|
113
118
|
f.request :multipart
|
|
114
119
|
f.headers["Authorization"] = "Bearer #{@api_key}"
|
|
120
|
+
f.headers["User-Agent"] = user_agent
|
|
115
121
|
f.adapter Faraday.default_adapter
|
|
116
122
|
end
|
|
117
123
|
end
|
data/lib/postproxy/constants.rb
CHANGED
|
@@ -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
|
|
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
|
|
@@ -38,6 +38,8 @@ module PostProxy
|
|
|
38
38
|
form_data = { "post[body]" => body }
|
|
39
39
|
form_data["post[scheduled_at]"] = format_time(scheduled_at) if scheduled_at
|
|
40
40
|
form_data["post[draft]"] = draft.to_s if !draft.nil?
|
|
41
|
+
form_data["queue_id"] = queue_id if queue_id
|
|
42
|
+
form_data["queue_priority"] = queue_priority if queue_priority
|
|
41
43
|
|
|
42
44
|
files = []
|
|
43
45
|
|
|
@@ -116,6 +118,8 @@ module PostProxy
|
|
|
116
118
|
form_data["post[body]"] = body if body
|
|
117
119
|
form_data["post[scheduled_at]"] = format_time(scheduled_at) if scheduled_at
|
|
118
120
|
form_data["post[draft]"] = draft.to_s if !draft.nil?
|
|
121
|
+
form_data["queue_id"] = queue_id if queue_id
|
|
122
|
+
form_data["queue_priority"] = queue_priority if queue_priority
|
|
119
123
|
|
|
120
124
|
files = []
|
|
121
125
|
|
|
@@ -205,11 +209,28 @@ module PostProxy
|
|
|
205
209
|
StatsResponse.new(data: posts)
|
|
206
210
|
end
|
|
207
211
|
|
|
208
|
-
def delete(id, profile_group_id: nil)
|
|
209
|
-
|
|
212
|
+
def delete(id, delete_on_platform: nil, profile_group_id: nil)
|
|
213
|
+
params = {}
|
|
214
|
+
params[:delete_on_platform] = delete_on_platform unless delete_on_platform.nil?
|
|
215
|
+
result = @client.request(:delete, "/posts/#{id}",
|
|
216
|
+
params: params.empty? ? nil : params,
|
|
217
|
+
profile_group_id: profile_group_id
|
|
218
|
+
)
|
|
210
219
|
DeleteResponse.new(**result)
|
|
211
220
|
end
|
|
212
221
|
|
|
222
|
+
def delete_on_platform(id, post_profile_id: nil, profile_id: nil, network: nil, profile_group_id: nil)
|
|
223
|
+
json_body = {}
|
|
224
|
+
json_body[:post_profile_id] = post_profile_id if post_profile_id
|
|
225
|
+
json_body[:profile_id] = profile_id if profile_id
|
|
226
|
+
json_body[:network] = network if network
|
|
227
|
+
result = @client.request(:post, "/posts/#{id}/delete_on_platform",
|
|
228
|
+
json: json_body.empty? ? nil : json_body,
|
|
229
|
+
profile_group_id: profile_group_id
|
|
230
|
+
)
|
|
231
|
+
DeleteOnPlatformResponse.new(**result)
|
|
232
|
+
end
|
|
233
|
+
|
|
213
234
|
private
|
|
214
235
|
|
|
215
236
|
def format_time(value)
|
|
@@ -26,11 +26,28 @@ module PostProxy
|
|
|
26
26
|
DeleteResponse.new(**result)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
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:
|
|
39
|
+
json: { platform: "bluesky", identifier: identifier, app_password: app_password }
|
|
32
40
|
)
|
|
33
|
-
|
|
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)
|
data/lib/postproxy/types.rb
CHANGED
|
@@ -63,17 +63,31 @@ module PostProxy
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
class ErrorDetails < Model
|
|
67
|
+
attr_accessor :platform_error_code, :platform_error_subcode, :platform_error_message, :postproxy_note
|
|
68
|
+
|
|
69
|
+
def initialize(**attrs)
|
|
70
|
+
@platform_error_code = nil
|
|
71
|
+
@platform_error_subcode = nil
|
|
72
|
+
@platform_error_message = nil
|
|
73
|
+
@postproxy_note = nil
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
66
78
|
class PlatformResult < Model
|
|
67
|
-
attr_accessor :platform, :status, :params, :error, :attempted_at, :insights
|
|
79
|
+
attr_accessor :platform, :status, :params, :error, :error_details, :attempted_at, :insights
|
|
68
80
|
|
|
69
81
|
def initialize(**attrs)
|
|
70
82
|
@params = nil
|
|
71
83
|
@error = nil
|
|
84
|
+
@error_details = nil
|
|
72
85
|
@attempted_at = nil
|
|
73
86
|
@insights = nil
|
|
74
87
|
super
|
|
75
88
|
@attempted_at = parse_time(@attempted_at)
|
|
76
89
|
@insights = Insights.new(**@insights) if @insights.is_a?(Hash)
|
|
90
|
+
@error_details = ErrorDetails.new(**@error_details.transform_keys(&:to_sym)) if @error_details.is_a?(Hash)
|
|
77
91
|
end
|
|
78
92
|
|
|
79
93
|
private
|
|
@@ -316,14 +330,80 @@ module PostProxy
|
|
|
316
330
|
attr_accessor :deleted
|
|
317
331
|
end
|
|
318
332
|
|
|
333
|
+
class DeletingPlatform < Model
|
|
334
|
+
attr_accessor :post_profile_id, :platform
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
class DeleteOnPlatformResponse < Model
|
|
338
|
+
attr_accessor :success, :deleting
|
|
339
|
+
|
|
340
|
+
def initialize(**attrs)
|
|
341
|
+
@deleting = []
|
|
342
|
+
super
|
|
343
|
+
@deleting = (@deleting || []).map do |entry|
|
|
344
|
+
entry.is_a?(DeletingPlatform) ? entry : DeletingPlatform.new(**entry)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
319
349
|
class SuccessResponse < Model
|
|
320
350
|
attr_accessor :success
|
|
321
351
|
end
|
|
322
352
|
|
|
353
|
+
# OAuth-style connection response (facebook, instagram, twitter, etc.).
|
|
323
354
|
class ConnectionResponse < Model
|
|
324
355
|
attr_accessor :url, :success
|
|
325
356
|
end
|
|
326
357
|
|
|
358
|
+
# Alias for clarity at call sites; same shape as ConnectionResponse.
|
|
359
|
+
OAuthConnectionResponse = ConnectionResponse
|
|
360
|
+
|
|
361
|
+
class SyncProfile < Model
|
|
362
|
+
attr_accessor :id, :network, :name, :external_username
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
class BlueskyConnectionResponse < Model
|
|
366
|
+
attr_accessor :success, :profile
|
|
367
|
+
|
|
368
|
+
def initialize(**attrs)
|
|
369
|
+
@profile = nil
|
|
370
|
+
super
|
|
371
|
+
@profile = SyncProfile.new(**@profile.transform_keys(&:to_sym)) if @profile.is_a?(Hash)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
class TelegramConnectionResponse < Model
|
|
376
|
+
attr_accessor :success, :profile, :next_step
|
|
377
|
+
|
|
378
|
+
def initialize(**attrs)
|
|
379
|
+
@profile = nil
|
|
380
|
+
@next_step = nil
|
|
381
|
+
super
|
|
382
|
+
@profile = SyncProfile.new(**@profile.transform_keys(&:to_sym)) if @profile.is_a?(Hash)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
class ProfileStats < Model
|
|
387
|
+
attr_accessor :profile_id, :platform, :placement_id, :records
|
|
388
|
+
|
|
389
|
+
def initialize(**attrs)
|
|
390
|
+
@placement_id = nil
|
|
391
|
+
@records = []
|
|
392
|
+
super
|
|
393
|
+
@records = (@records || []).map do |r|
|
|
394
|
+
r.is_a?(StatsRecord) ? r : StatsRecord.new(**r.transform_keys(&:to_sym))
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
class ProfileStatsResponse
|
|
400
|
+
attr_reader :data
|
|
401
|
+
|
|
402
|
+
def initialize(data:)
|
|
403
|
+
@data = data.is_a?(ProfileStats) ? data : ProfileStats.new(**data.transform_keys(&:to_sym))
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
327
407
|
# Platform-specific parameter structs
|
|
328
408
|
|
|
329
409
|
class FacebookParams < Model
|
|
@@ -346,7 +426,8 @@ module PostProxy
|
|
|
346
426
|
end
|
|
347
427
|
|
|
348
428
|
class YouTubeParams < Model
|
|
349
|
-
attr_accessor :format, :title, :privacy_status, :cover_url, :made_for_kids
|
|
429
|
+
attr_accessor :format, :title, :privacy_status, :cover_url, :made_for_kids,
|
|
430
|
+
:tags, :category_id, :contains_synthetic_media
|
|
350
431
|
end
|
|
351
432
|
|
|
352
433
|
class PinterestParams < Model
|
|
@@ -361,13 +442,21 @@ module PostProxy
|
|
|
361
442
|
attr_accessor :format
|
|
362
443
|
end
|
|
363
444
|
|
|
445
|
+
class BlueskyParams < Model
|
|
446
|
+
attr_accessor :format
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
class TelegramParams < Model
|
|
450
|
+
attr_accessor :format, :chat_id, :parse_mode, :disable_link_preview, :disable_notification
|
|
451
|
+
end
|
|
452
|
+
|
|
364
453
|
class PlatformParams < Model
|
|
365
454
|
attr_accessor :facebook, :instagram, :tiktok, :linkedin, :youtube,
|
|
366
|
-
:pinterest, :threads, :twitter
|
|
455
|
+
:pinterest, :threads, :twitter, :bluesky, :telegram
|
|
367
456
|
|
|
368
457
|
def to_h
|
|
369
458
|
result = {}
|
|
370
|
-
%i[facebook instagram tiktok linkedin youtube pinterest threads twitter].each do |platform|
|
|
459
|
+
%i[facebook instagram tiktok linkedin youtube pinterest threads twitter bluesky telegram].each do |platform|
|
|
371
460
|
value = send(platform)
|
|
372
461
|
next if value.nil?
|
|
373
462
|
|
data/lib/postproxy/version.rb
CHANGED
|
@@ -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
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.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- PostProxy
|
|
@@ -86,6 +86,7 @@ files:
|
|
|
86
86
|
- lib/postproxy/resources/webhooks.rb
|
|
87
87
|
- lib/postproxy/types.rb
|
|
88
88
|
- lib/postproxy/version.rb
|
|
89
|
+
- lib/postproxy/webhook_events.rb
|
|
89
90
|
- lib/postproxy/webhook_signature.rb
|
|
90
91
|
homepage: https://postproxy.dev
|
|
91
92
|
licenses:
|