postproxy-sdk 1.13.0 → 1.14.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 +415 -11
- data/lib/postproxy/client.rb +72 -7
- data/lib/postproxy/constants.rb +37 -2
- data/lib/postproxy/errors.rb +15 -0
- data/lib/postproxy/resources/api_keys.rb +30 -0
- data/lib/postproxy/resources/chats.rb +30 -1
- data/lib/postproxy/resources/comments.rb +52 -4
- data/lib/postproxy/resources/google_business.rb +194 -0
- data/lib/postproxy/resources/messages.rb +25 -1
- data/lib/postproxy/resources/placements.rb +35 -0
- data/lib/postproxy/resources/posts.rb +86 -25
- data/lib/postproxy/resources/profile_groups.rb +8 -1
- data/lib/postproxy/resources/profiles.rb +131 -0
- data/lib/postproxy/resources/whatsapp.rb +278 -0
- data/lib/postproxy/resources/whatsapp_templates.rb +108 -0
- data/lib/postproxy/types.rb +408 -29
- data/lib/postproxy/version.rb +1 -1
- data/lib/postproxy/webhook_events.rb +90 -4
- metadata +9 -4
|
@@ -28,13 +28,20 @@ module PostProxy
|
|
|
28
28
|
Post.new(**result)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# `media` items are URL strings or `{ url: }` hashes. A local
|
|
32
|
+
# `cover_file` in `platforms` (Instagram, TikTok, YouTube, Pinterest) is
|
|
33
|
+
# uploaded with the post, switching the request to multipart; a base64
|
|
34
|
+
# data URL is sent as-is. `skip_duplicate_check: true` publishes even if
|
|
35
|
+
# an identical post was just submitted (no ConflictError).
|
|
31
36
|
def create(body, profiles:, media: nil, media_files: nil, platforms: nil,
|
|
32
37
|
thread: nil, scheduled_at: nil, draft: nil, queue_id: nil,
|
|
33
|
-
queue_priority: nil, profile_group_id: nil, idempotency_key: nil
|
|
38
|
+
queue_priority: nil, profile_group_id: nil, idempotency_key: nil,
|
|
39
|
+
skip_duplicate_check: nil)
|
|
34
40
|
has_files = media_files && !media_files.empty?
|
|
35
41
|
has_thread_files = thread&.any? { |t| t[:media_files]&.any? }
|
|
42
|
+
headers = skip_duplicate_check ? { "X-Skip-Duplicate-Check" => "true" } : nil
|
|
36
43
|
|
|
37
|
-
if has_files || has_thread_files
|
|
44
|
+
if has_files || has_thread_files || local_cover_file?(platforms)
|
|
38
45
|
form_data = { "post[body]" => body }
|
|
39
46
|
form_data["post[scheduled_at]"] = format_time(scheduled_at) if scheduled_at
|
|
40
47
|
form_data["post[draft]"] = draft.to_s if !draft.nil?
|
|
@@ -48,17 +55,10 @@ module PostProxy
|
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
media&.each do |m|
|
|
51
|
-
files << ["media[]", nil, m, "text/plain"]
|
|
58
|
+
files << ["media[]", nil, media_url(m), "text/plain"]
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
if platforms
|
|
55
|
-
params_hash = platforms.is_a?(PlatformParams) ? platforms.to_h : platforms
|
|
56
|
-
params_hash.each do |platform, platform_params|
|
|
57
|
-
platform_params.each do |key, value|
|
|
58
|
-
files << ["platforms[#{platform}][#{key}]", nil, value.to_s, "text/plain"]
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
61
|
+
append_platform_params(files, platforms) if platforms
|
|
62
62
|
|
|
63
63
|
media_files&.each do |path|
|
|
64
64
|
path = path.to_s
|
|
@@ -72,7 +72,7 @@ module PostProxy
|
|
|
72
72
|
form_data["thread[#{i}][body]"] = t[:body] if t[:body]
|
|
73
73
|
|
|
74
74
|
t[:media]&.each do |m|
|
|
75
|
-
files << ["thread[#{i}][media][]", nil, m, "text/plain"]
|
|
75
|
+
files << ["thread[#{i}][media][]", nil, media_url(m), "text/plain"]
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
t[:media_files]&.each do |path|
|
|
@@ -88,7 +88,8 @@ module PostProxy
|
|
|
88
88
|
data: form_data,
|
|
89
89
|
files: files,
|
|
90
90
|
profile_group_id: profile_group_id,
|
|
91
|
-
idempotency_key: idempotency_key
|
|
91
|
+
idempotency_key: idempotency_key,
|
|
92
|
+
headers: headers
|
|
92
93
|
)
|
|
93
94
|
else
|
|
94
95
|
post_payload = { body: body }
|
|
@@ -105,20 +106,25 @@ module PostProxy
|
|
|
105
106
|
result = @client.request(:post, "/posts",
|
|
106
107
|
json: json_body,
|
|
107
108
|
profile_group_id: profile_group_id,
|
|
108
|
-
idempotency_key: idempotency_key
|
|
109
|
+
idempotency_key: idempotency_key,
|
|
110
|
+
headers: headers
|
|
109
111
|
)
|
|
110
112
|
end
|
|
111
113
|
|
|
112
114
|
Post.new(**result)
|
|
113
115
|
end
|
|
114
116
|
|
|
117
|
+
# On a published post only `body`, `platforms.youtube.title`,
|
|
118
|
+
# `platforms.pinterest.title` and `platforms.pinterest.destination_link`
|
|
119
|
+
# can change; the edit is pushed to the platforms in the background
|
|
120
|
+
# (`platform_post.edited` / `platform_post.edit_failed` webhooks).
|
|
115
121
|
def update(id, body: nil, profiles: nil, media: nil, media_files: nil, platforms: nil,
|
|
116
122
|
thread: nil, scheduled_at: nil, draft: nil, queue_id: nil,
|
|
117
123
|
queue_priority: nil, profile_group_id: nil, idempotency_key: nil)
|
|
118
124
|
has_files = media_files && !media_files.empty?
|
|
119
125
|
has_thread_files = thread&.any? { |t| t[:media_files]&.any? }
|
|
120
126
|
|
|
121
|
-
if has_files || has_thread_files
|
|
127
|
+
if has_files || has_thread_files || local_cover_file?(platforms)
|
|
122
128
|
form_data = {}
|
|
123
129
|
form_data["post[body]"] = body if body
|
|
124
130
|
form_data["post[scheduled_at]"] = format_time(scheduled_at) if scheduled_at
|
|
@@ -133,17 +139,10 @@ module PostProxy
|
|
|
133
139
|
end
|
|
134
140
|
|
|
135
141
|
media&.each do |m|
|
|
136
|
-
files << ["media[]", nil, m, "text/plain"]
|
|
142
|
+
files << ["media[]", nil, media_url(m), "text/plain"]
|
|
137
143
|
end
|
|
138
144
|
|
|
139
|
-
if platforms
|
|
140
|
-
params_hash = platforms.is_a?(PlatformParams) ? platforms.to_h : platforms
|
|
141
|
-
params_hash.each do |platform, platform_params|
|
|
142
|
-
platform_params.each do |key, value|
|
|
143
|
-
files << ["platforms[#{platform}][#{key}]", nil, value.to_s, "text/plain"]
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
end
|
|
145
|
+
append_platform_params(files, platforms) if platforms
|
|
147
146
|
|
|
148
147
|
media_files&.each do |path|
|
|
149
148
|
path = path.to_s
|
|
@@ -157,7 +156,7 @@ module PostProxy
|
|
|
157
156
|
form_data["thread[#{i}][body]"] = t[:body] if t[:body]
|
|
158
157
|
|
|
159
158
|
t[:media]&.each do |m|
|
|
160
|
-
files << ["thread[#{i}][media][]", nil, m, "text/plain"]
|
|
159
|
+
files << ["thread[#{i}][media][]", nil, media_url(m), "text/plain"]
|
|
161
160
|
end
|
|
162
161
|
|
|
163
162
|
t[:media_files]&.each do |path|
|
|
@@ -249,6 +248,68 @@ module PostProxy
|
|
|
249
248
|
|
|
250
249
|
private
|
|
251
250
|
|
|
251
|
+
def media_url(item)
|
|
252
|
+
return item unless item.is_a?(Hash)
|
|
253
|
+
|
|
254
|
+
item[:url] || item["url"]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# `cover_file` is a local path unless it's a base64 data URL, which the
|
|
258
|
+
# API takes as a plain string.
|
|
259
|
+
def local_cover_file_value?(key, value)
|
|
260
|
+
key.to_s == "cover_file" && value.is_a?(String) && !value.start_with?("data:")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def local_cover_file?(platforms)
|
|
264
|
+
return false unless platforms
|
|
265
|
+
|
|
266
|
+
params_hash = platforms.is_a?(PlatformParams) ? platforms.to_h : platforms
|
|
267
|
+
params_hash.any? do |_, platform_params|
|
|
268
|
+
platform_params = platform_params.to_h if platform_params.is_a?(Model)
|
|
269
|
+
platform_params.is_a?(Hash) && platform_params.any? { |k, v| local_cover_file_value?(k, v) }
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Platform params as flat form fields; a local `cover_file` is attached
|
|
274
|
+
# as a file.
|
|
275
|
+
def append_platform_params(files, platforms)
|
|
276
|
+
params_hash = platforms.is_a?(PlatformParams) ? platforms.to_h : platforms
|
|
277
|
+
params_hash.each do |platform, platform_params|
|
|
278
|
+
next if platform_params.nil?
|
|
279
|
+
|
|
280
|
+
platform_params = platform_params.to_h if platform_params.is_a?(Model)
|
|
281
|
+
platform_params.each do |key, value|
|
|
282
|
+
next if value.nil?
|
|
283
|
+
|
|
284
|
+
field = "platforms[#{platform}][#{key}]"
|
|
285
|
+
if local_cover_file_value?(key, value)
|
|
286
|
+
filename = File.basename(value)
|
|
287
|
+
files << [field, filename, File.open(value, "rb"), mime_type_for(filename)]
|
|
288
|
+
else
|
|
289
|
+
append_form_value(files, field, value)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def append_form_value(files, field, value)
|
|
296
|
+
case value
|
|
297
|
+
when Array
|
|
298
|
+
# Hashes (e.g. Instagram user tags) get an index so their fields stay
|
|
299
|
+
# grouped; Rails reads numeric keys back as an array.
|
|
300
|
+
value.each_with_index do |item, i|
|
|
301
|
+
nested = item.is_a?(Hash) || item.is_a?(Model)
|
|
302
|
+
append_form_value(files, nested ? "#{field}[#{i}]" : "#{field}[]", item)
|
|
303
|
+
end
|
|
304
|
+
when Hash
|
|
305
|
+
value.each { |k, v| append_form_value(files, "#{field}[#{k}]", v) unless v.nil? }
|
|
306
|
+
when Model
|
|
307
|
+
append_form_value(files, field, value.to_h)
|
|
308
|
+
else
|
|
309
|
+
files << [field, nil, value.to_s, "text/plain"]
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
252
313
|
def format_time(value)
|
|
253
314
|
return value if value.is_a?(String)
|
|
254
315
|
value.iso8601
|
|
@@ -30,9 +30,16 @@ module PostProxy
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# OAuth flow. BlueSky and Telegram use their dedicated helpers below.
|
|
33
|
-
|
|
33
|
+
#
|
|
34
|
+
# `onboarding` picks the flow: Instagram "instagram" (default) or
|
|
35
|
+
# "facebook" (Facebook login — needed for reel music); WhatsApp "api" or
|
|
36
|
+
# "business_app". `refresh_connection: true` re-authorizes an existing
|
|
37
|
+
# profile.
|
|
38
|
+
def initialize_connection(id, platform:, redirect_url: nil, onboarding: nil, refresh_connection: nil)
|
|
34
39
|
body = { platform: platform }
|
|
35
40
|
body[:redirect_url] = redirect_url if redirect_url
|
|
41
|
+
body[:onboarding] = onboarding if onboarding
|
|
42
|
+
body[:refresh_connection] = refresh_connection unless refresh_connection.nil?
|
|
36
43
|
result = @client.request(:post, "/profile_groups/#{id}/initialize_connection", json: body)
|
|
37
44
|
ConnectionResponse.new(**result)
|
|
38
45
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "erb"
|
|
2
|
+
|
|
1
3
|
module PostProxy
|
|
2
4
|
module Resources
|
|
3
5
|
class Profiles
|
|
@@ -36,6 +38,97 @@ module PostProxy
|
|
|
36
38
|
ProfileStatsResponse.new(data: result[:data])
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
# Per-day series. `attribution: "publish"` attributes post metrics to the
|
|
42
|
+
# day the post was published instead of the day they were received.
|
|
43
|
+
def daily_stats(id, placement_id: nil, from: nil, to: nil, attribution: nil, profile_group_id: nil)
|
|
44
|
+
params = {}
|
|
45
|
+
params[:placement_id] = placement_id if placement_id
|
|
46
|
+
params[:from] = from if from
|
|
47
|
+
params[:to] = to if to
|
|
48
|
+
params[:attribution] = attribution if attribution
|
|
49
|
+
|
|
50
|
+
result = @client.request(:get, "/profiles/#{id}/daily_stats",
|
|
51
|
+
params: params.empty? ? nil : params,
|
|
52
|
+
profile_group_id: profile_group_id
|
|
53
|
+
)
|
|
54
|
+
DailyStatsResponse.new(data: result[:data])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def follower_history(id, placement_id: nil, from: nil, to: nil, profile_group_id: nil)
|
|
58
|
+
params = {}
|
|
59
|
+
params[:placement_id] = placement_id if placement_id
|
|
60
|
+
params[:from] = from if from
|
|
61
|
+
params[:to] = to if to
|
|
62
|
+
|
|
63
|
+
result = @client.request(:get, "/profiles/#{id}/follower_history",
|
|
64
|
+
params: params.empty? ? nil : params,
|
|
65
|
+
profile_group_id: profile_group_id
|
|
66
|
+
)
|
|
67
|
+
FollowerHistoryResponse.new(data: result[:data])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Instagram only: whether a DM participant is verified and follows you.
|
|
71
|
+
def participant_status(id, user_id, profile_group_id: nil)
|
|
72
|
+
result = @client.request(:get, "/profiles/#{id}/participants/#{ERB::Util.url_encode(user_id.to_s)}",
|
|
73
|
+
profile_group_id: profile_group_id
|
|
74
|
+
)
|
|
75
|
+
ParticipantStatus.new(**result)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# LinkedIn only: resolve a company page URL (or vanity name) to the URN
|
|
79
|
+
# used to @-mention it in a post.
|
|
80
|
+
def resolve_mention(id, url, profile_group_id: nil)
|
|
81
|
+
result = @client.request(:get, "/profiles/#{id}/resolve_mention",
|
|
82
|
+
params: { url: url },
|
|
83
|
+
profile_group_id: profile_group_id
|
|
84
|
+
)
|
|
85
|
+
ResolvedMention.new(**result)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Music and original sounds an Instagram reel can attach via
|
|
89
|
+
# `platforms.instagram.audio_id`. Profiles connected via Facebook login
|
|
90
|
+
# only. `audio_type` is "music" or "original_sound". Returns the
|
|
91
|
+
# response hash (`data` plus paging).
|
|
92
|
+
def instagram_available_audio(id, audio_type: nil, search_query: nil, profile_group_id: nil)
|
|
93
|
+
params = {}
|
|
94
|
+
params[:audio_type] = audio_type if audio_type
|
|
95
|
+
params[:search_query] = search_query if search_query
|
|
96
|
+
|
|
97
|
+
@client.request(:get, "/profiles/#{id}/instagram/available_audio",
|
|
98
|
+
params: params.empty? ? nil : params,
|
|
99
|
+
profile_group_id: profile_group_id
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Facebook only. `page_id` is the placement id from #placements. Returns
|
|
104
|
+
# the Page fields as a hash.
|
|
105
|
+
def facebook_page(id, page_id, fields: nil, profile_group_id: nil)
|
|
106
|
+
params = { page_id: page_id }
|
|
107
|
+
params[:fields] = fields if fields
|
|
108
|
+
|
|
109
|
+
@client.request(:get, "/profiles/#{id}/facebook/page", params: params, profile_group_id: profile_group_id)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Update the Page's about / contact fields, e.g. `about` (255 characters
|
|
113
|
+
# or fewer), `description`, `website`, `phone`, `emails`.
|
|
114
|
+
def update_facebook_page(id, page_id, fields, profile_group_id: nil, idempotency_key: nil)
|
|
115
|
+
@client.request(:patch, "/profiles/#{id}/facebook/update_page",
|
|
116
|
+
json: fields.merge(page_id: page_id),
|
|
117
|
+
profile_group_id: profile_group_id,
|
|
118
|
+
idempotency_key: idempotency_key
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Moves the profile (with its placements) to another profile group.
|
|
123
|
+
def assign_to_group(id, target_profile_group_id, profile_group_id: nil, idempotency_key: nil)
|
|
124
|
+
result = @client.request(:patch, "/profiles/#{id}/assign_to_group",
|
|
125
|
+
json: { target_profile_group_id: target_profile_group_id },
|
|
126
|
+
profile_group_id: profile_group_id,
|
|
127
|
+
idempotency_key: idempotency_key
|
|
128
|
+
)
|
|
129
|
+
Profile.new(**result)
|
|
130
|
+
end
|
|
131
|
+
|
|
39
132
|
# Moves a placement (e.g. a Facebook Page or Telegram channel) to another
|
|
40
133
|
# profile group. `placement_id` is the placement's external ID as
|
|
41
134
|
# returned by #placements.
|
|
@@ -97,6 +190,44 @@ module PostProxy
|
|
|
97
190
|
PostSync.new(**result)
|
|
98
191
|
end
|
|
99
192
|
|
|
193
|
+
# Imports DM history back to `from`. Like #backfill_posts, runs in the
|
|
194
|
+
# background and only one runs per profile.
|
|
195
|
+
def backfill_chats(id, from:, profile_group_id: nil, idempotency_key: nil)
|
|
196
|
+
result = @client.request(:post, "/profiles/#{id}/backfill_chats",
|
|
197
|
+
json: { from: from },
|
|
198
|
+
profile_group_id: profile_group_id,
|
|
199
|
+
idempotency_key: idempotency_key
|
|
200
|
+
)
|
|
201
|
+
ChatSync.new(**result)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def chat_syncs(id, trigger: nil, status: nil, page: nil, per_page: nil, profile_group_id: nil)
|
|
205
|
+
params = {}
|
|
206
|
+
params[:trigger] = trigger if trigger
|
|
207
|
+
params[:status] = status if status
|
|
208
|
+
params[:page] = page if page
|
|
209
|
+
params[:per_page] = per_page if per_page
|
|
210
|
+
|
|
211
|
+
result = @client.request(:get, "/profiles/#{id}/chat_syncs",
|
|
212
|
+
params: params.empty? ? nil : params,
|
|
213
|
+
profile_group_id: profile_group_id
|
|
214
|
+
)
|
|
215
|
+
syncs = (result[:data] || []).map { |s| ChatSync.new(**s) }
|
|
216
|
+
PaginatedResponse.new(
|
|
217
|
+
data: syncs,
|
|
218
|
+
total: result[:total],
|
|
219
|
+
page: result[:page],
|
|
220
|
+
per_page: result[:per_page]
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def chat_sync(id, chat_sync_id, profile_group_id: nil)
|
|
225
|
+
result = @client.request(:get, "/profiles/#{id}/chat_syncs/#{chat_sync_id}",
|
|
226
|
+
profile_group_id: profile_group_id
|
|
227
|
+
)
|
|
228
|
+
ChatSync.new(**result)
|
|
229
|
+
end
|
|
230
|
+
|
|
100
231
|
# Lists DM ice breakers. Supported for Instagram profiles only.
|
|
101
232
|
def ice_breakers(id, profile_group_id: nil)
|
|
102
233
|
result = @client.request(:get, "/profiles/#{id}/ice_breakers", profile_group_id: profile_group_id)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
module PostProxy
|
|
2
|
+
module Resources
|
|
3
|
+
# WhatsApp number administration: registration, business profile, display
|
|
4
|
+
# name, username, blocked users, groups and conversion events. Responses
|
|
5
|
+
# are Meta's payloads, returned as plain hashes.
|
|
6
|
+
#
|
|
7
|
+
# `phone_number_id` is the placement id from
|
|
8
|
+
# `profiles.placements(profile_id)`. Dataset and conversion calls are
|
|
9
|
+
# account-level and don't take one.
|
|
10
|
+
class WhatsApp
|
|
11
|
+
def initialize(client)
|
|
12
|
+
@client = client
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# --- Phone number ---
|
|
16
|
+
|
|
17
|
+
def number_info(profile_id, phone_number_id, profile_group_id: nil)
|
|
18
|
+
call(:get, profile_id, "number_info", { phone_number_id: phone_number_id }, profile_group_id)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# `pin` is the 6-digit two-step verification PIN.
|
|
22
|
+
def register_phone_number(profile_id, phone_number_id, pin, profile_group_id: nil, idempotency_key: nil)
|
|
23
|
+
call(:post, profile_id, "register_phone_number",
|
|
24
|
+
{ phone_number_id: phone_number_id, pin: pin }, profile_group_id, idempotency_key)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# `method` is "SMS" or "VOICE".
|
|
28
|
+
def request_verification_code(profile_id, phone_number_id, method: nil, language: nil, profile_group_id: nil,
|
|
29
|
+
idempotency_key: nil)
|
|
30
|
+
call(:post, profile_id, "request_verification_code",
|
|
31
|
+
{ phone_number_id: phone_number_id, method: method, language: language }, profile_group_id, idempotency_key)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def verify_phone_number(profile_id, phone_number_id, code, profile_group_id: nil, idempotency_key: nil)
|
|
35
|
+
call(:post, profile_id, "verify_phone_number",
|
|
36
|
+
{ phone_number_id: phone_number_id, code: code }, profile_group_id, idempotency_key)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# --- Business profile ---
|
|
40
|
+
|
|
41
|
+
def business_profile(profile_id, phone_number_id, profile_group_id: nil)
|
|
42
|
+
call(:get, profile_id, "business_profile", { phone_number_id: phone_number_id }, profile_group_id)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def update_business_profile(profile_id, phone_number_id, about: nil, address: nil, description: nil,
|
|
46
|
+
email: nil, websites: nil, vertical: nil, profile_group_id: nil, idempotency_key: nil)
|
|
47
|
+
call(:patch, profile_id, "update_business_profile", {
|
|
48
|
+
phone_number_id: phone_number_id,
|
|
49
|
+
about: about,
|
|
50
|
+
address: address,
|
|
51
|
+
description: description,
|
|
52
|
+
email: email,
|
|
53
|
+
websites: websites,
|
|
54
|
+
vertical: vertical
|
|
55
|
+
}, profile_group_id, idempotency_key)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Pass a public `url`, or base64 `data` with its `content_type`
|
|
59
|
+
# (image/jpeg or image/png).
|
|
60
|
+
def update_business_profile_photo(profile_id, phone_number_id, url: nil, data: nil, content_type: nil,
|
|
61
|
+
profile_group_id: nil, idempotency_key: nil)
|
|
62
|
+
call(:post, profile_id, "update_business_profile_photo", {
|
|
63
|
+
phone_number_id: phone_number_id,
|
|
64
|
+
url: url,
|
|
65
|
+
data: data,
|
|
66
|
+
content_type: content_type
|
|
67
|
+
}, profile_group_id, idempotency_key)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# --- Display name ---
|
|
71
|
+
|
|
72
|
+
def display_name(profile_id, phone_number_id, profile_group_id: nil)
|
|
73
|
+
call(:get, profile_id, "display_name", { phone_number_id: phone_number_id }, profile_group_id)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Meta reviews the new name; the outcome arrives as the
|
|
77
|
+
# `placement.name_status_updated` webhook.
|
|
78
|
+
def request_display_name_change(profile_id, phone_number_id, display_name, profile_group_id: nil,
|
|
79
|
+
idempotency_key: nil)
|
|
80
|
+
call(:post, profile_id, "request_display_name_change",
|
|
81
|
+
{ phone_number_id: phone_number_id, display_name: display_name }, profile_group_id, idempotency_key)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# --- Username ---
|
|
85
|
+
|
|
86
|
+
def username(profile_id, phone_number_id, profile_group_id: nil)
|
|
87
|
+
call(:get, profile_id, "username", { phone_number_id: phone_number_id }, profile_group_id)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# `transfer_action` is "none" or "force_transfer".
|
|
91
|
+
def set_username(profile_id, phone_number_id, username, transfer_action: nil, profile_group_id: nil,
|
|
92
|
+
idempotency_key: nil)
|
|
93
|
+
call(:put, profile_id, "set_username",
|
|
94
|
+
{ phone_number_id: phone_number_id, username: username, transfer_action: transfer_action },
|
|
95
|
+
profile_group_id, idempotency_key)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def delete_username(profile_id, phone_number_id, profile_group_id: nil, idempotency_key: nil)
|
|
99
|
+
call(:delete, profile_id, "delete_username", { phone_number_id: phone_number_id }, profile_group_id,
|
|
100
|
+
idempotency_key)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def username_suggestions(profile_id, phone_number_id, profile_group_id: nil)
|
|
104
|
+
call(:get, profile_id, "username_suggestions", { phone_number_id: phone_number_id }, profile_group_id)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# --- Blocked users ---
|
|
108
|
+
|
|
109
|
+
def blocked_users(profile_id, phone_number_id, limit: nil, after: nil, profile_group_id: nil)
|
|
110
|
+
call(:get, profile_id, "blocked_users",
|
|
111
|
+
{ phone_number_id: phone_number_id, limit: limit, after: after }, profile_group_id)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def blocked_user_status(profile_id, phone_number_id, user, profile_group_id: nil)
|
|
115
|
+
call(:get, profile_id, "blocked_user_status", { phone_number_id: phone_number_id, user: user },
|
|
116
|
+
profile_group_id)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# `users` are phone numbers or WhatsApp ids.
|
|
120
|
+
def block_users(profile_id, phone_number_id, users, profile_group_id: nil, idempotency_key: nil)
|
|
121
|
+
call(:post, profile_id, "block_users", { phone_number_id: phone_number_id, users: users },
|
|
122
|
+
profile_group_id, idempotency_key)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def unblock_users(profile_id, phone_number_id, users, profile_group_id: nil, idempotency_key: nil)
|
|
126
|
+
call(:post, profile_id, "unblock_users", { phone_number_id: phone_number_id, users: users },
|
|
127
|
+
profile_group_id, idempotency_key)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# --- Groups ---
|
|
131
|
+
|
|
132
|
+
def groups(profile_id, phone_number_id, limit: nil, after: nil, profile_group_id: nil)
|
|
133
|
+
call(:get, profile_id, "groups", { phone_number_id: phone_number_id, limit: limit, after: after },
|
|
134
|
+
profile_group_id)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# `join_approval_mode` is "approval_required" or "auto_approve".
|
|
138
|
+
def create_group(profile_id, phone_number_id, subject, description: nil, join_approval_mode: nil,
|
|
139
|
+
profile_group_id: nil, idempotency_key: nil)
|
|
140
|
+
call(:post, profile_id, "create_group", {
|
|
141
|
+
phone_number_id: phone_number_id,
|
|
142
|
+
subject: subject,
|
|
143
|
+
description: description,
|
|
144
|
+
join_approval_mode: join_approval_mode
|
|
145
|
+
}, profile_group_id, idempotency_key)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def group(profile_id, phone_number_id, group_id, profile_group_id: nil)
|
|
149
|
+
call(:get, profile_id, "group", { phone_number_id: phone_number_id, group_id: group_id }, profile_group_id)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def update_group(profile_id, phone_number_id, group_id, subject: nil, description: nil,
|
|
153
|
+
join_approval_mode: nil, profile_group_id: nil, idempotency_key: nil)
|
|
154
|
+
call(:patch, profile_id, "update_group", {
|
|
155
|
+
phone_number_id: phone_number_id,
|
|
156
|
+
group_id: group_id,
|
|
157
|
+
subject: subject,
|
|
158
|
+
description: description,
|
|
159
|
+
join_approval_mode: join_approval_mode
|
|
160
|
+
}, profile_group_id, idempotency_key)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def delete_group(profile_id, phone_number_id, group_id, profile_group_id: nil, idempotency_key: nil)
|
|
164
|
+
call(:delete, profile_id, "delete_group", { phone_number_id: phone_number_id, group_id: group_id },
|
|
165
|
+
profile_group_id, idempotency_key)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def add_group_participants(profile_id, phone_number_id, group_id, phone_numbers, profile_group_id: nil,
|
|
169
|
+
idempotency_key: nil)
|
|
170
|
+
group_members(:post, profile_id, "add_group_participants", phone_number_id, group_id, phone_numbers,
|
|
171
|
+
profile_group_id, idempotency_key)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def remove_group_participants(profile_id, phone_number_id, group_id, phone_numbers, profile_group_id: nil,
|
|
175
|
+
idempotency_key: nil)
|
|
176
|
+
group_members(:post, profile_id, "remove_group_participants", phone_number_id, group_id, phone_numbers,
|
|
177
|
+
profile_group_id, idempotency_key)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def create_group_invite_link(profile_id, phone_number_id, group_id, profile_group_id: nil,
|
|
181
|
+
idempotency_key: nil)
|
|
182
|
+
call(:post, profile_id, "create_group_invite_link",
|
|
183
|
+
{ phone_number_id: phone_number_id, group_id: group_id }, profile_group_id, idempotency_key)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def group_join_requests(profile_id, phone_number_id, group_id, profile_group_id: nil)
|
|
187
|
+
call(:get, profile_id, "group_join_requests", { phone_number_id: phone_number_id, group_id: group_id },
|
|
188
|
+
profile_group_id)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def approve_group_join_requests(profile_id, phone_number_id, group_id, phone_numbers, profile_group_id: nil,
|
|
192
|
+
idempotency_key: nil)
|
|
193
|
+
group_members(:post, profile_id, "approve_group_join_requests", phone_number_id, group_id, phone_numbers,
|
|
194
|
+
profile_group_id, idempotency_key)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def reject_group_join_requests(profile_id, phone_number_id, group_id, phone_numbers, profile_group_id: nil,
|
|
198
|
+
idempotency_key: nil)
|
|
199
|
+
group_members(:post, profile_id, "reject_group_join_requests", phone_number_id, group_id, phone_numbers,
|
|
200
|
+
profile_group_id, idempotency_key)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# --- Conversions (account-level) ---
|
|
204
|
+
|
|
205
|
+
def dataset(profile_id, profile_group_id: nil)
|
|
206
|
+
call(:get, profile_id, "dataset", {}, profile_group_id)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def create_dataset(profile_id, profile_group_id: nil, idempotency_key: nil)
|
|
210
|
+
call(:post, profile_id, "create_dataset", {}, profile_group_id, idempotency_key)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Report a conversion (e.g. Purchase, LeadSubmitted) back to Meta for a
|
|
214
|
+
# click-to-WhatsApp ad. Identify the person by `chat_id` or `phone`.
|
|
215
|
+
def send_conversion_event(profile_id, event_name:, event_id:, chat_id: nil, phone: nil, dataset_id: nil,
|
|
216
|
+
event_time: nil, email: nil, external_id: nil, currency: nil, value: nil, content_ids: nil,
|
|
217
|
+
test_code: nil, profile_group_id: nil, idempotency_key: nil)
|
|
218
|
+
call(:post, profile_id, "send_conversion_event", {
|
|
219
|
+
event_name: event_name,
|
|
220
|
+
event_id: event_id,
|
|
221
|
+
chat_id: chat_id,
|
|
222
|
+
phone: phone,
|
|
223
|
+
dataset_id: dataset_id,
|
|
224
|
+
event_time: event_time,
|
|
225
|
+
email: email,
|
|
226
|
+
external_id: external_id,
|
|
227
|
+
currency: currency,
|
|
228
|
+
value: value,
|
|
229
|
+
content_ids: content_ids,
|
|
230
|
+
test_code: test_code
|
|
231
|
+
}, profile_group_id, idempotency_key)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# --- Media ---
|
|
235
|
+
|
|
236
|
+
# Download an inbound media file (e.g. from a message attachment's
|
|
237
|
+
# `external_id`). Returns a RawResponse: `data` holds the raw bytes,
|
|
238
|
+
# `content_type` the MIME type.
|
|
239
|
+
def media(profile_id, phone_number_id, media_id, profile_group_id: nil)
|
|
240
|
+
@client.request(:get, "/profiles/#{profile_id}/whatsapp/media",
|
|
241
|
+
params: { phone_number_id: phone_number_id, media_id: media_id },
|
|
242
|
+
profile_group_id: profile_group_id,
|
|
243
|
+
raw: true
|
|
244
|
+
)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
private
|
|
248
|
+
|
|
249
|
+
# Reads and deletes send their arguments as query params; everything
|
|
250
|
+
# else as a JSON body. Nil values are dropped.
|
|
251
|
+
def call(http_method, profile_id, action, data, profile_group_id, idempotency_key = nil)
|
|
252
|
+
path = "/profiles/#{profile_id}/whatsapp/#{action}"
|
|
253
|
+
compact = data.compact
|
|
254
|
+
|
|
255
|
+
if %i[get delete].include?(http_method)
|
|
256
|
+
@client.request(http_method, path,
|
|
257
|
+
params: compact.empty? ? nil : compact,
|
|
258
|
+
profile_group_id: profile_group_id,
|
|
259
|
+
idempotency_key: idempotency_key
|
|
260
|
+
)
|
|
261
|
+
else
|
|
262
|
+
@client.request(http_method, path,
|
|
263
|
+
json: compact,
|
|
264
|
+
profile_group_id: profile_group_id,
|
|
265
|
+
idempotency_key: idempotency_key
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def group_members(http_method, profile_id, action, phone_number_id, group_id, phone_numbers,
|
|
271
|
+
profile_group_id, idempotency_key)
|
|
272
|
+
call(http_method, profile_id, action,
|
|
273
|
+
{ phone_number_id: phone_number_id, group_id: group_id, phone_numbers: phone_numbers },
|
|
274
|
+
profile_group_id, idempotency_key)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|