postproxy-sdk 1.0.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 +7 -0
- data/README.md +197 -0
- data/lib/postproxy/client.rb +155 -0
- data/lib/postproxy/constants.rb +28 -0
- data/lib/postproxy/errors.rb +16 -0
- data/lib/postproxy/resources/posts.rb +117 -0
- data/lib/postproxy/resources/profile_groups.rb +37 -0
- data/lib/postproxy/resources/profiles.rb +31 -0
- data/lib/postproxy/types.rb +197 -0
- data/lib/postproxy/version.rb +3 -0
- data/lib/postproxy.rb +8 -0
- metadata +110 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 822ce030b347645daad188e55da76ab9a3494b0f7f01d0b6a78a02b4c9e933d6
|
|
4
|
+
data.tar.gz: 33dc5d8cf8e97817b9f6f69d8f57dd4dfd207ff1b8add0ba58661cddfd88f64f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8460f568b415ec00973fa57d9065a85a141fe16aafb430765054ae51ca0ee16c514a55c02f59c54b7983def183c9131c07c65a5db448976f05d9db2139243915
|
|
7
|
+
data.tar.gz: 64c079b04bbd82dde18a2c612000ad42f8525351326714243c923dfde21bba6e51125006169a36675301b2fde2857f94c54e74cab08ae406345bf1dd8a26e711
|
data/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# PostProxy Ruby SDK
|
|
2
|
+
|
|
3
|
+
Ruby client for the [PostProxy API](https://postproxy.dev) — manage social media posts, profiles, and profile groups.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install postproxy-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or add to your Gemfile:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
gem "postproxy-sdk"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
require "postproxy"
|
|
21
|
+
|
|
22
|
+
client = PostProxy::Client.new("your-api-key", profile_group_id: "pg-abc")
|
|
23
|
+
|
|
24
|
+
profiles = client.profiles.list.data
|
|
25
|
+
post = client.posts.create(
|
|
26
|
+
"Hello from PostProxy!",
|
|
27
|
+
profiles: [profiles.first.id]
|
|
28
|
+
)
|
|
29
|
+
puts post.id, post.status
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Client
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
# Basic
|
|
36
|
+
client = PostProxy::Client.new("your-api-key")
|
|
37
|
+
|
|
38
|
+
# With default profile group
|
|
39
|
+
client = PostProxy::Client.new("your-api-key", profile_group_id: "pg-abc")
|
|
40
|
+
|
|
41
|
+
# With custom base URL
|
|
42
|
+
client = PostProxy::Client.new("your-api-key", base_url: "https://custom.api.dev")
|
|
43
|
+
|
|
44
|
+
# With custom Faraday client
|
|
45
|
+
faraday = Faraday.new(url: "https://api.postproxy.dev") do |f|
|
|
46
|
+
f.request :retry
|
|
47
|
+
f.headers["Authorization"] = "Bearer your-api-key"
|
|
48
|
+
f.adapter :net_http
|
|
49
|
+
end
|
|
50
|
+
client = PostProxy::Client.new("your-api-key", faraday_client: faraday)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Posts
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# List posts (paginated)
|
|
57
|
+
result = client.posts.list(page: 1, per_page: 10, status: "processed")
|
|
58
|
+
result.data # => [Post, ...]
|
|
59
|
+
result.total # => 42
|
|
60
|
+
result.page # => 1
|
|
61
|
+
|
|
62
|
+
# Get a single post
|
|
63
|
+
post = client.posts.get("post-id")
|
|
64
|
+
|
|
65
|
+
# Create a post
|
|
66
|
+
post = client.posts.create("Hello!", profiles: ["prof-1", "prof-2"])
|
|
67
|
+
|
|
68
|
+
# Create with media URLs
|
|
69
|
+
post = client.posts.create(
|
|
70
|
+
"Check this out!",
|
|
71
|
+
profiles: ["prof-1"],
|
|
72
|
+
media: ["https://example.com/image.jpg"]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Create with local file uploads
|
|
76
|
+
post = client.posts.create(
|
|
77
|
+
"Uploaded!",
|
|
78
|
+
profiles: ["prof-1"],
|
|
79
|
+
media_files: ["/path/to/photo.jpg"]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Create a draft
|
|
83
|
+
draft = client.posts.create("Draft", profiles: ["prof-1"], draft: true)
|
|
84
|
+
|
|
85
|
+
# Publish a draft
|
|
86
|
+
post = client.posts.publish_draft("post-id")
|
|
87
|
+
|
|
88
|
+
# Schedule a post
|
|
89
|
+
post = client.posts.create(
|
|
90
|
+
"Later!",
|
|
91
|
+
profiles: ["prof-1"],
|
|
92
|
+
scheduled_at: (Time.now + 3600).iso8601
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Delete a post
|
|
96
|
+
client.posts.delete("post-id")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Profiles
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# List profiles
|
|
103
|
+
profiles = client.profiles.list.data
|
|
104
|
+
|
|
105
|
+
# Get a profile
|
|
106
|
+
profile = client.profiles.get("prof-id")
|
|
107
|
+
|
|
108
|
+
# Get placements for a profile
|
|
109
|
+
placements = client.profiles.placements("prof-id").data
|
|
110
|
+
|
|
111
|
+
# Delete a profile
|
|
112
|
+
client.profiles.delete("prof-id")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Profile Groups
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# List groups
|
|
119
|
+
groups = client.profile_groups.list.data
|
|
120
|
+
|
|
121
|
+
# Get a group
|
|
122
|
+
group = client.profile_groups.get("pg-id")
|
|
123
|
+
|
|
124
|
+
# Create a group
|
|
125
|
+
group = client.profile_groups.create("My Group")
|
|
126
|
+
|
|
127
|
+
# Delete a group
|
|
128
|
+
client.profile_groups.delete("pg-id")
|
|
129
|
+
|
|
130
|
+
# Initialize OAuth connection
|
|
131
|
+
connection = client.profile_groups.initialize_connection(
|
|
132
|
+
"pg-id",
|
|
133
|
+
platform: "instagram",
|
|
134
|
+
redirect_url: "https://myapp.com/callback"
|
|
135
|
+
)
|
|
136
|
+
# Redirect user to connection.url
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Platform Parameters
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
platforms = PostProxy::PlatformParams.new(
|
|
143
|
+
facebook: PostProxy::FacebookParams.new(
|
|
144
|
+
format: "post",
|
|
145
|
+
first_comment: "First!"
|
|
146
|
+
),
|
|
147
|
+
instagram: PostProxy::InstagramParams.new(
|
|
148
|
+
format: "reel",
|
|
149
|
+
collaborators: ["@friend"],
|
|
150
|
+
cover_url: "https://example.com/cover.jpg"
|
|
151
|
+
),
|
|
152
|
+
tiktok: PostProxy::TikTokParams.new(
|
|
153
|
+
privacy_status: "PUBLIC_TO_EVERYONE",
|
|
154
|
+
auto_add_music: true
|
|
155
|
+
),
|
|
156
|
+
linkedin: PostProxy::LinkedInParams.new(format: "post"),
|
|
157
|
+
youtube: PostProxy::YouTubeParams.new(
|
|
158
|
+
title: "My Video",
|
|
159
|
+
privacy_status: "public"
|
|
160
|
+
),
|
|
161
|
+
pinterest: PostProxy::PinterestParams.new(
|
|
162
|
+
title: "My Pin",
|
|
163
|
+
board_id: "board-123"
|
|
164
|
+
),
|
|
165
|
+
threads: PostProxy::ThreadsParams.new(format: "post"),
|
|
166
|
+
twitter: PostProxy::TwitterParams.new(format: "post")
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
post = client.posts.create(
|
|
170
|
+
"Cross-platform!",
|
|
171
|
+
profiles: ["prof-1", "prof-2"],
|
|
172
|
+
platforms: platforms
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
begin
|
|
180
|
+
client.posts.get("bad-id")
|
|
181
|
+
rescue PostProxy::AuthenticationError => e
|
|
182
|
+
puts "Auth failed: #{e.message}" # 401
|
|
183
|
+
rescue PostProxy::NotFoundError => e
|
|
184
|
+
puts "Not found: #{e.message}" # 404
|
|
185
|
+
rescue PostProxy::ValidationError => e
|
|
186
|
+
puts "Invalid: #{e.message}" # 422
|
|
187
|
+
rescue PostProxy::BadRequestError => e
|
|
188
|
+
puts "Bad request: #{e.message}" # 400
|
|
189
|
+
rescue PostProxy::Error => e
|
|
190
|
+
puts "Error #{e.status_code}: #{e.message}"
|
|
191
|
+
puts e.response # parsed response body
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "faraday/multipart"
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "resources/posts"
|
|
5
|
+
require_relative "resources/profiles"
|
|
6
|
+
require_relative "resources/profile_groups"
|
|
7
|
+
|
|
8
|
+
module PostProxy
|
|
9
|
+
class Client
|
|
10
|
+
attr_reader :api_key, :base_url, :profile_group_id
|
|
11
|
+
|
|
12
|
+
def initialize(api_key, base_url: DEFAULT_BASE_URL, profile_group_id: nil, faraday_client: nil)
|
|
13
|
+
@api_key = api_key
|
|
14
|
+
@base_url = base_url
|
|
15
|
+
@profile_group_id = profile_group_id
|
|
16
|
+
@faraday_client = faraday_client
|
|
17
|
+
@posts = nil
|
|
18
|
+
@profiles = nil
|
|
19
|
+
@profile_groups = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def posts
|
|
23
|
+
@posts ||= Resources::Posts.new(self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def profiles
|
|
27
|
+
@profiles ||= Resources::Profiles.new(self)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def profile_groups
|
|
31
|
+
@profile_groups ||= Resources::ProfileGroups.new(self)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def request(method, path, params: nil, json: nil, data: nil, files: nil, profile_group_id: nil)
|
|
35
|
+
url = "/api#{path}"
|
|
36
|
+
|
|
37
|
+
query = {}
|
|
38
|
+
pgid = profile_group_id || @profile_group_id
|
|
39
|
+
query[:profile_group_id] = pgid if pgid
|
|
40
|
+
query.merge!(params) if params
|
|
41
|
+
|
|
42
|
+
response = if files
|
|
43
|
+
conn = multipart_connection
|
|
44
|
+
parts = []
|
|
45
|
+
data&.each { |k, v| parts << [k.to_s, v.to_s] }
|
|
46
|
+
files.each do |field, filename, io, content_type|
|
|
47
|
+
if io.nil?
|
|
48
|
+
# Plain text part (filename holds the string value)
|
|
49
|
+
parts << [field, filename]
|
|
50
|
+
elsif io.is_a?(String)
|
|
51
|
+
# Plain text part
|
|
52
|
+
parts << [field, io]
|
|
53
|
+
else
|
|
54
|
+
# File upload
|
|
55
|
+
parts << [field, Faraday::Multipart::FilePart.new(io, content_type, filename)]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
# Build payload preserving duplicate keys
|
|
59
|
+
payload = parts.each_with_object({}) do |(key, val), h|
|
|
60
|
+
if h.key?(key)
|
|
61
|
+
h[key] = [h[key]] unless h[key].is_a?(Array)
|
|
62
|
+
h[key] << val
|
|
63
|
+
else
|
|
64
|
+
h[key] = val
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
conn.send(method, url) do |req|
|
|
68
|
+
req.params = query unless query.empty?
|
|
69
|
+
req.body = payload
|
|
70
|
+
end
|
|
71
|
+
else
|
|
72
|
+
conn = json_connection
|
|
73
|
+
conn.send(method, url) do |req|
|
|
74
|
+
req.params = query unless query.empty?
|
|
75
|
+
req.body = json.to_json if json
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
handle_response(response)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def json_connection
|
|
85
|
+
@faraday_client || Faraday.new(url: @base_url) do |f|
|
|
86
|
+
f.request :url_encoded
|
|
87
|
+
f.headers["Authorization"] = "Bearer #{@api_key}"
|
|
88
|
+
f.headers["Content-Type"] = "application/json"
|
|
89
|
+
f.adapter Faraday.default_adapter
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def multipart_connection
|
|
94
|
+
@faraday_client || Faraday.new(url: @base_url) do |f|
|
|
95
|
+
f.request :multipart
|
|
96
|
+
f.headers["Authorization"] = "Bearer #{@api_key}"
|
|
97
|
+
f.adapter Faraday.default_adapter
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def handle_response(response)
|
|
102
|
+
case response.status
|
|
103
|
+
when 200..299
|
|
104
|
+
return nil if response.status == 204
|
|
105
|
+
return nil if response.body.nil? || response.body.empty?
|
|
106
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
107
|
+
when 401
|
|
108
|
+
body = parse_error_body(response)
|
|
109
|
+
raise AuthenticationError.new(
|
|
110
|
+
error_message(body),
|
|
111
|
+
status_code: response.status,
|
|
112
|
+
response: body
|
|
113
|
+
)
|
|
114
|
+
when 404
|
|
115
|
+
body = parse_error_body(response)
|
|
116
|
+
raise NotFoundError.new(
|
|
117
|
+
error_message(body),
|
|
118
|
+
status_code: response.status,
|
|
119
|
+
response: body
|
|
120
|
+
)
|
|
121
|
+
when 422
|
|
122
|
+
body = parse_error_body(response)
|
|
123
|
+
raise ValidationError.new(
|
|
124
|
+
error_message(body),
|
|
125
|
+
status_code: response.status,
|
|
126
|
+
response: body
|
|
127
|
+
)
|
|
128
|
+
when 400
|
|
129
|
+
body = parse_error_body(response)
|
|
130
|
+
raise BadRequestError.new(
|
|
131
|
+
error_message(body),
|
|
132
|
+
status_code: response.status,
|
|
133
|
+
response: body
|
|
134
|
+
)
|
|
135
|
+
else
|
|
136
|
+
body = parse_error_body(response)
|
|
137
|
+
raise Error.new(
|
|
138
|
+
error_message(body),
|
|
139
|
+
status_code: response.status,
|
|
140
|
+
response: body
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def parse_error_body(response)
|
|
146
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
147
|
+
rescue JSON::ParserError, TypeError
|
|
148
|
+
{ error: response.body }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def error_message(body)
|
|
152
|
+
body[:message] || body[:error]
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module PostProxy
|
|
2
|
+
DEFAULT_BASE_URL = "https://api.postproxy.dev"
|
|
3
|
+
|
|
4
|
+
PLATFORMS = %w[
|
|
5
|
+
facebook instagram tiktok linkedin youtube twitter threads pinterest
|
|
6
|
+
].freeze
|
|
7
|
+
|
|
8
|
+
PROFILE_STATUSES = %w[active expired inactive].freeze
|
|
9
|
+
|
|
10
|
+
POST_STATUSES = %w[pending draft processing processed scheduled].freeze
|
|
11
|
+
|
|
12
|
+
PLATFORM_POST_STATUSES = %w[pending processing published failed deleted].freeze
|
|
13
|
+
|
|
14
|
+
INSTAGRAM_FORMATS = %w[post reel story].freeze
|
|
15
|
+
FACEBOOK_FORMATS = %w[post story].freeze
|
|
16
|
+
TIKTOK_FORMATS = %w[video image].freeze
|
|
17
|
+
LINKEDIN_FORMATS = %w[post].freeze
|
|
18
|
+
YOUTUBE_FORMATS = %w[post].freeze
|
|
19
|
+
PINTEREST_FORMATS = %w[pin].freeze
|
|
20
|
+
THREADS_FORMATS = %w[post].freeze
|
|
21
|
+
TWITTER_FORMATS = %w[post].freeze
|
|
22
|
+
|
|
23
|
+
TIKTOK_PRIVACIES = %w[
|
|
24
|
+
PUBLIC_TO_EVERYONE MUTUAL_FOLLOW_FRIENDS FOLLOWER_OF_CREATOR SELF_ONLY
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
YOUTUBE_PRIVACIES = %w[public unlisted private].freeze
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module PostProxy
|
|
2
|
+
class Error < StandardError
|
|
3
|
+
attr_reader :status_code, :response
|
|
4
|
+
|
|
5
|
+
def initialize(message, status_code: nil, response: nil)
|
|
6
|
+
super(message)
|
|
7
|
+
@status_code = status_code
|
|
8
|
+
@response = response
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class AuthenticationError < Error; end
|
|
13
|
+
class NotFoundError < Error; end
|
|
14
|
+
class ValidationError < Error; end
|
|
15
|
+
class BadRequestError < Error; end
|
|
16
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
module PostProxy
|
|
2
|
+
module Resources
|
|
3
|
+
class Posts
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def list(page: nil, per_page: nil, status: nil, platforms: nil, scheduled_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[:status] = status if status
|
|
13
|
+
params[:platforms] = platforms.join(",") if platforms
|
|
14
|
+
params[:scheduled_after] = format_time(scheduled_after) if scheduled_after
|
|
15
|
+
|
|
16
|
+
result = @client.request(:get, "/posts", params: params, profile_group_id: profile_group_id)
|
|
17
|
+
posts = (result[:data] || []).map { |p| Post.new(**p) }
|
|
18
|
+
PaginatedResponse.new(
|
|
19
|
+
data: posts,
|
|
20
|
+
total: result[:total],
|
|
21
|
+
page: result[:page],
|
|
22
|
+
per_page: result[:per_page]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get(id, profile_group_id: nil)
|
|
27
|
+
result = @client.request(:get, "/posts/#{id}", profile_group_id: profile_group_id)
|
|
28
|
+
Post.new(**result)
|
|
29
|
+
end
|
|
30
|
+
|
|
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?
|
|
34
|
+
form_data = { "post[body]" => body }
|
|
35
|
+
form_data["post[scheduled_at]"] = format_time(scheduled_at) if scheduled_at
|
|
36
|
+
form_data["post[draft]"] = draft.to_s if !draft.nil?
|
|
37
|
+
|
|
38
|
+
files = []
|
|
39
|
+
|
|
40
|
+
profiles.each do |p|
|
|
41
|
+
files << ["profiles[]", nil, p, "text/plain"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
media&.each do |m|
|
|
45
|
+
files << ["media[]", nil, m, "text/plain"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if platforms
|
|
49
|
+
params_hash = platforms.is_a?(PlatformParams) ? platforms.to_h : platforms
|
|
50
|
+
params_hash.each do |platform, platform_params|
|
|
51
|
+
platform_params.each do |key, value|
|
|
52
|
+
files << ["platforms[#{platform}][#{key}]", nil, value.to_s, "text/plain"]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
media_files.each do |path|
|
|
58
|
+
path = path.to_s
|
|
59
|
+
filename = File.basename(path)
|
|
60
|
+
content_type = mime_type_for(filename)
|
|
61
|
+
io = File.open(path, "rb")
|
|
62
|
+
files << ["media[]", filename, io, content_type]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
result = @client.request(:post, "/posts",
|
|
66
|
+
data: form_data,
|
|
67
|
+
files: files,
|
|
68
|
+
profile_group_id: profile_group_id
|
|
69
|
+
)
|
|
70
|
+
else
|
|
71
|
+
post_payload = { body: body }
|
|
72
|
+
post_payload[:scheduled_at] = format_time(scheduled_at) if scheduled_at
|
|
73
|
+
post_payload[:draft] = draft unless draft.nil?
|
|
74
|
+
|
|
75
|
+
json_body = { post: post_payload, profiles: profiles }
|
|
76
|
+
json_body[:platforms] = platforms.is_a?(PlatformParams) ? platforms.to_h : platforms if platforms
|
|
77
|
+
json_body[:media] = media if media
|
|
78
|
+
|
|
79
|
+
result = @client.request(:post, "/posts", json: json_body, profile_group_id: profile_group_id)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Post.new(**result)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def publish_draft(id, profile_group_id: nil)
|
|
86
|
+
result = @client.request(:post, "/posts/#{id}/publish", profile_group_id: profile_group_id)
|
|
87
|
+
Post.new(**result)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def delete(id, profile_group_id: nil)
|
|
91
|
+
result = @client.request(:delete, "/posts/#{id}", profile_group_id: profile_group_id)
|
|
92
|
+
DeleteResponse.new(**result)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def format_time(value)
|
|
98
|
+
return value if value.is_a?(String)
|
|
99
|
+
value.iso8601
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def mime_type_for(filename)
|
|
103
|
+
case File.extname(filename).downcase
|
|
104
|
+
when ".jpg", ".jpeg" then "image/jpeg"
|
|
105
|
+
when ".png" then "image/png"
|
|
106
|
+
when ".gif" then "image/gif"
|
|
107
|
+
when ".webp" then "image/webp"
|
|
108
|
+
when ".mp4" then "video/mp4"
|
|
109
|
+
when ".mov" then "video/quicktime"
|
|
110
|
+
when ".avi" then "video/x-msvideo"
|
|
111
|
+
when ".webm" then "video/webm"
|
|
112
|
+
else "application/octet-stream"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module PostProxy
|
|
2
|
+
module Resources
|
|
3
|
+
class ProfileGroups
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def list
|
|
9
|
+
result = @client.request(:get, "/profile_groups")
|
|
10
|
+
groups = (result[:data] || []).map { |g| ProfileGroup.new(**g) }
|
|
11
|
+
ListResponse.new(data: groups)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(id)
|
|
15
|
+
result = @client.request(:get, "/profile_groups/#{id}")
|
|
16
|
+
ProfileGroup.new(**result)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create(name)
|
|
20
|
+
result = @client.request(:post, "/profile_groups", json: { name: name })
|
|
21
|
+
ProfileGroup.new(**result)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete(id)
|
|
25
|
+
result = @client.request(:delete, "/profile_groups/#{id}")
|
|
26
|
+
DeleteResponse.new(**result)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize_connection(id, platform:, redirect_url:)
|
|
30
|
+
result = @client.request(:post, "/profile_groups/#{id}/initialize_connection",
|
|
31
|
+
json: { platform: platform, redirect_url: redirect_url }
|
|
32
|
+
)
|
|
33
|
+
ConnectionResponse.new(**result)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module PostProxy
|
|
2
|
+
module Resources
|
|
3
|
+
class Profiles
|
|
4
|
+
def initialize(client)
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def list(profile_group_id: nil)
|
|
9
|
+
result = @client.request(:get, "/profiles", profile_group_id: profile_group_id)
|
|
10
|
+
profiles = (result[:data] || []).map { |p| Profile.new(**p) }
|
|
11
|
+
ListResponse.new(data: profiles)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(id, profile_group_id: nil)
|
|
15
|
+
result = @client.request(:get, "/profiles/#{id}", profile_group_id: profile_group_id)
|
|
16
|
+
Profile.new(**result)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def placements(id, profile_group_id: nil)
|
|
20
|
+
result = @client.request(:get, "/profiles/#{id}/placements", profile_group_id: profile_group_id)
|
|
21
|
+
items = (result[:data] || []).map { |p| Placement.new(**p) }
|
|
22
|
+
ListResponse.new(data: items)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delete(id, profile_group_id: nil)
|
|
26
|
+
result = @client.request(:delete, "/profiles/#{id}", profile_group_id: profile_group_id)
|
|
27
|
+
SuccessResponse.new(**result)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
require "time"
|
|
2
|
+
|
|
3
|
+
module PostProxy
|
|
4
|
+
class Model
|
|
5
|
+
def initialize(**attrs)
|
|
6
|
+
attrs.each do |key, value|
|
|
7
|
+
if respond_to?(:"#{key}=")
|
|
8
|
+
send(:"#{key}=", value)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
instance_variables.each_with_object({}) do |var, hash|
|
|
15
|
+
key = var.to_s.delete_prefix("@")
|
|
16
|
+
hash[key.to_sym] = instance_variable_get(var)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Profile < Model
|
|
22
|
+
attr_accessor :id, :name, :status, :platform, :profile_group_id, :expires_at, :post_count
|
|
23
|
+
|
|
24
|
+
def initialize(**attrs)
|
|
25
|
+
@expires_at = nil
|
|
26
|
+
@post_count = 0
|
|
27
|
+
super
|
|
28
|
+
@expires_at = parse_time(@expires_at)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_time(value)
|
|
34
|
+
return nil if value.nil?
|
|
35
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class ProfileGroup < Model
|
|
40
|
+
attr_accessor :id, :name, :profiles_count
|
|
41
|
+
|
|
42
|
+
def initialize(**attrs)
|
|
43
|
+
@profiles_count = 0
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class Insights < Model
|
|
49
|
+
attr_accessor :impressions, :on
|
|
50
|
+
|
|
51
|
+
def initialize(**attrs)
|
|
52
|
+
@impressions = nil
|
|
53
|
+
@on = nil
|
|
54
|
+
super
|
|
55
|
+
@on = parse_time(@on)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def parse_time(value)
|
|
61
|
+
return nil if value.nil?
|
|
62
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class PlatformResult < Model
|
|
67
|
+
attr_accessor :platform, :status, :params, :error, :attempted_at, :insights
|
|
68
|
+
|
|
69
|
+
def initialize(**attrs)
|
|
70
|
+
@params = nil
|
|
71
|
+
@error = nil
|
|
72
|
+
@attempted_at = nil
|
|
73
|
+
@insights = nil
|
|
74
|
+
super
|
|
75
|
+
@attempted_at = parse_time(@attempted_at)
|
|
76
|
+
@insights = Insights.new(**@insights) if @insights.is_a?(Hash)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def parse_time(value)
|
|
82
|
+
return nil if value.nil?
|
|
83
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class Post < Model
|
|
88
|
+
attr_accessor :id, :body, :status, :scheduled_at, :created_at, :platforms
|
|
89
|
+
|
|
90
|
+
def initialize(**attrs)
|
|
91
|
+
@scheduled_at = nil
|
|
92
|
+
@platforms = []
|
|
93
|
+
super
|
|
94
|
+
@scheduled_at = parse_time(@scheduled_at)
|
|
95
|
+
@created_at = parse_time(@created_at)
|
|
96
|
+
@platforms = (@platforms || []).map do |p|
|
|
97
|
+
p.is_a?(PlatformResult) ? p : PlatformResult.new(**p.transform_keys(&:to_sym))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def parse_time(value)
|
|
104
|
+
return nil if value.nil?
|
|
105
|
+
value.is_a?(Time) ? value : Time.parse(value.to_s)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class Placement < Model
|
|
110
|
+
attr_accessor :id, :name
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class ListResponse
|
|
114
|
+
attr_reader :data
|
|
115
|
+
|
|
116
|
+
def initialize(data:)
|
|
117
|
+
@data = data
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class PaginatedResponse < ListResponse
|
|
122
|
+
attr_reader :total, :page, :per_page
|
|
123
|
+
|
|
124
|
+
def initialize(data:, total:, page:, per_page:)
|
|
125
|
+
super(data: data)
|
|
126
|
+
@total = total
|
|
127
|
+
@page = page
|
|
128
|
+
@per_page = per_page
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
class DeleteResponse < Model
|
|
133
|
+
attr_accessor :deleted
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class SuccessResponse < Model
|
|
137
|
+
attr_accessor :success
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class ConnectionResponse < Model
|
|
141
|
+
attr_accessor :url, :success
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Platform-specific parameter structs
|
|
145
|
+
|
|
146
|
+
class FacebookParams < Model
|
|
147
|
+
attr_accessor :format, :first_comment, :page_id
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
class InstagramParams < Model
|
|
151
|
+
attr_accessor :format, :first_comment, :collaborators, :cover_url,
|
|
152
|
+
:audio_name, :trial_strategy, :thumb_offset
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class TikTokParams < Model
|
|
156
|
+
attr_accessor :format, :privacy_status, :photo_cover_index, :auto_add_music,
|
|
157
|
+
:made_with_ai, :disable_comment, :disable_duet, :disable_stitch,
|
|
158
|
+
:brand_content_toggle, :brand_organic_toggle
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class LinkedInParams < Model
|
|
162
|
+
attr_accessor :format, :organization_id
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class YouTubeParams < Model
|
|
166
|
+
attr_accessor :format, :title, :privacy_status, :cover_url
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
class PinterestParams < Model
|
|
170
|
+
attr_accessor :format, :title, :board_id, :destination_link, :cover_url, :thumb_offset
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class ThreadsParams < Model
|
|
174
|
+
attr_accessor :format
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
class TwitterParams < Model
|
|
178
|
+
attr_accessor :format
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class PlatformParams < Model
|
|
182
|
+
attr_accessor :facebook, :instagram, :tiktok, :linkedin, :youtube,
|
|
183
|
+
:pinterest, :threads, :twitter
|
|
184
|
+
|
|
185
|
+
def to_h
|
|
186
|
+
result = {}
|
|
187
|
+
%i[facebook instagram tiktok linkedin youtube pinterest threads twitter].each do |platform|
|
|
188
|
+
value = send(platform)
|
|
189
|
+
next if value.nil?
|
|
190
|
+
|
|
191
|
+
params = value.is_a?(Model) ? value.to_h : value
|
|
192
|
+
result[platform] = params.reject { |_, v| v.nil? }
|
|
193
|
+
end
|
|
194
|
+
result
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
data/lib/postproxy.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: postproxy-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- PostProxy
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-multipart
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: webmock
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
description: Ruby client for the PostProxy API — manage social media posts, profiles,
|
|
69
|
+
and profile groups.
|
|
70
|
+
email:
|
|
71
|
+
- support@postproxy.dev
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- README.md
|
|
77
|
+
- lib/postproxy.rb
|
|
78
|
+
- lib/postproxy/client.rb
|
|
79
|
+
- lib/postproxy/constants.rb
|
|
80
|
+
- lib/postproxy/errors.rb
|
|
81
|
+
- lib/postproxy/resources/posts.rb
|
|
82
|
+
- lib/postproxy/resources/profile_groups.rb
|
|
83
|
+
- lib/postproxy/resources/profiles.rb
|
|
84
|
+
- lib/postproxy/types.rb
|
|
85
|
+
- lib/postproxy/version.rb
|
|
86
|
+
homepage: https://postproxy.dev
|
|
87
|
+
licenses:
|
|
88
|
+
- MIT
|
|
89
|
+
metadata:
|
|
90
|
+
homepage_uri: https://postproxy.dev
|
|
91
|
+
source_code_uri: https://github.com/postproxy/postproxy-ruby
|
|
92
|
+
documentation_uri: https://postproxy.dev/getting-started/overview/
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '3.1'
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubygems_version: 4.0.3
|
|
108
|
+
specification_version: 4
|
|
109
|
+
summary: Ruby client for the PostProxy API
|
|
110
|
+
test_files: []
|