postproxy-sdk 1.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0aea2892ef3d3256dfd1cc9ce790da283df1d6edb83a2cfcaa76c0aa8a8194ae
4
- data.tar.gz: a9f008666d47832963cc7119d0db1b53d53c7ddd3dc690fa13acebc05c72f5f6
3
+ metadata.gz: 1d2f9ae8cbbc2ca3cd324b5dc522226c9d7a77cd93430abb309b31efc210a293
4
+ data.tar.gz: 26c58e5f9b7ea1af4eccb3935ef8c71bf202bf5bff93898d0b30a5450114bb1a
5
5
  SHA512:
6
- metadata.gz: c9f6cf9ec0d398081c84c21c41824bd590fa9d57b7e8eea423512bcc899b82d3df93c4532edf307e2f949a0d72217e6677743b36434a4384e9b9d801030b4aa4
7
- data.tar.gz: 325923dc017d840f212edac1941f2f2aa8d8821594f2418e40dee1e1b56a8f103627ab1f8cb9adb07f576afcc8fe5a6b431fcc408a50c4ac6887948bf80426d6
6
+ metadata.gz: 906800719560c2e37351c958fa72449d1411b351c1cad4deb0d312e9b6fcaa3af3be3dcb504eeef045a3d24737815afb4e7f772edae4687e102e2f9d4bd605cc
7
+ data.tar.gz: b6ee51b608b91a9d31ed2891b7bba87df996006be1795504f540e275dd54c8ff3b7ef7e5f1994a7aa495a02f67ec8f3921e9017baadb3e1e8d3c5550d6959b06
data/README.md CHANGED
@@ -260,6 +260,26 @@ PostProxy::WebhookSignature.verify(
260
260
  )
261
261
  ```
262
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
+
263
283
  ## Comments
264
284
 
265
285
  ```ruby
@@ -311,6 +331,19 @@ placements = client.profiles.placements("prof-id").data
311
331
 
312
332
  # Delete a profile
313
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]
314
347
  ```
315
348
 
316
349
  ## Profile Groups
@@ -335,6 +368,28 @@ connection = client.profile_groups.initialize_connection(
335
368
  redirect_url: "https://myapp.com/callback"
336
369
  )
337
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] }}"
338
393
  ```
339
394
 
340
395
  ## Platform Parameters
@@ -364,7 +419,13 @@ platforms = PostProxy::PlatformParams.new(
364
419
  board_id: "board-123"
365
420
  ),
366
421
  threads: PostProxy::ThreadsParams.new(format: "post"),
367
- 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
+ )
368
429
  )
369
430
 
370
431
  post = client.posts.create(
@@ -374,6 +435,8 @@ post = client.posts.create(
374
435
  )
375
436
  ```
376
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
+
377
440
  ## Error Handling
378
441
 
379
442
  ```ruby
@@ -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
@@ -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
@@ -26,11 +26,28 @@ module PostProxy
26
26
  DeleteResponse.new(**result)
27
27
  end
28
28
 
29
- def initialize_connection(id, platform:, redirect_url:)
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: platform, redirect_url: redirect_url }
39
+ json: { platform: "bluesky", identifier: identifier, app_password: app_password }
32
40
  )
33
- ConnectionResponse.new(**result)
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)
@@ -350,10 +350,60 @@ module PostProxy
350
350
  attr_accessor :success
351
351
  end
352
352
 
353
+ # OAuth-style connection response (facebook, instagram, twitter, etc.).
353
354
  class ConnectionResponse < Model
354
355
  attr_accessor :url, :success
355
356
  end
356
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
+
357
407
  # Platform-specific parameter structs
358
408
 
359
409
  class FacebookParams < Model
@@ -392,13 +442,21 @@ module PostProxy
392
442
  attr_accessor :format
393
443
  end
394
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
+
395
453
  class PlatformParams < Model
396
454
  attr_accessor :facebook, :instagram, :tiktok, :linkedin, :youtube,
397
- :pinterest, :threads, :twitter
455
+ :pinterest, :threads, :twitter, :bluesky, :telegram
398
456
 
399
457
  def to_h
400
458
  result = {}
401
- %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|
402
460
  value = send(platform)
403
461
  next if value.nil?
404
462
 
@@ -1,3 +1,3 @@
1
1
  module PostProxy
2
- VERSION = "1.7.0"
2
+ VERSION = "1.8.0"
3
3
  end
@@ -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
@@ -4,6 +4,7 @@ require_relative "postproxy/errors"
4
4
  require_relative "postproxy/types"
5
5
  require_relative "postproxy/client"
6
6
  require_relative "postproxy/webhook_signature"
7
+ require_relative "postproxy/webhook_events"
7
8
 
8
9
  module PostProxy
9
10
  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.7.0
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: