posthubify 0.1.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 +76 -0
- data/lib/posthubify/client.rb +138 -0
- data/lib/posthubify/errors.rb +21 -0
- data/lib/posthubify/http.rb +102 -0
- data/lib/posthubify/resources/accounts.rb +185 -0
- data/lib/posthubify/resources/ads.rb +226 -0
- data/lib/posthubify/resources/analytics.rb +170 -0
- data/lib/posthubify/resources/messaging.rb +882 -0
- data/lib/posthubify/resources/platform.rb +227 -0
- data/lib/posthubify/resources/posts.rb +102 -0
- data/lib/posthubify/resources/telecom.rb +86 -0
- data/lib/posthubify/version.rb +5 -0
- data/lib/posthubify.rb +16 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6aba91ab279c1953076139305c63fbf1b74818368796f6f9131d0179a745b0c3
|
|
4
|
+
data.tar.gz: 041db11a40a45c60ccedd45033e2d6c0f398de01f77c99143444c7223d08d521
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1f6f687df94c5a45fd36b5d0018dea79d8c14e3e78edf08a6729e3481e67c0e6cf34541d76a345493f1cc9a905ad83724dd05998365bffd6f17007ce8fa13226
|
|
7
|
+
data.tar.gz: 44c0b1e60be2554d52c9d9ec8e4003aed83e5556a5aa62354ecdd19b8479f8c4e9d7e9e29bc794fd04b2921f7b72ea95d4b13b4d33d8afee065ff08e549801b0
|
data/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# PostHubify Ruby SDK
|
|
2
|
+
|
|
3
|
+
The official Ruby client for the PostHubify `/v1` API — a single gem for social media
|
|
4
|
+
publishing, inbox, contacts, analytics, ads, and telecom (SMS/OTP).
|
|
5
|
+
|
|
6
|
+
- **Zero runtime dependencies** — Ruby standard library only (`net/http`).
|
|
7
|
+
- **Full surface** — covers all 190+ operations of the `/v1` spec one-to-one (verified by a contract test).
|
|
8
|
+
- Ruby 2.6+
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem install posthubify
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
require 'posthubify'
|
|
20
|
+
|
|
21
|
+
ph = Posthubify::Client.new(api_key: 'sk_...', base_url: 'https://api.posthubify.com/v1')
|
|
22
|
+
|
|
23
|
+
# Connection test
|
|
24
|
+
ph.ping # => {"pong"=>true, "version"=>"v1", ...}
|
|
25
|
+
|
|
26
|
+
# Create a post
|
|
27
|
+
ph.posts.create({ 'content' => 'Hello world!', 'accountIds' => ['acc_1'] })
|
|
28
|
+
|
|
29
|
+
# Safe retry (Idempotency-Key — posts/sms/otp/ads/broadcasts)
|
|
30
|
+
require 'securerandom'
|
|
31
|
+
key = SecureRandom.uuid
|
|
32
|
+
ph.posts.create({ 'content' => 'Launch!', 'accountIds' => ['acc_1'] }, idempotency_key: key)
|
|
33
|
+
|
|
34
|
+
# Inbox analytics
|
|
35
|
+
ph.inbox_analytics.volume(from_date: '2026-06-01', to_date: '2026-06-14')
|
|
36
|
+
|
|
37
|
+
# Error handling
|
|
38
|
+
begin
|
|
39
|
+
ph.posts.get('missing')
|
|
40
|
+
rescue Posthubify::Error => e
|
|
41
|
+
puts [e.status, e.code, e.message].inspect # [404, nil, "..."]
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Resource groups
|
|
46
|
+
|
|
47
|
+
The exact same surface as the Node/Python SDKs, with snake_case accessors:
|
|
48
|
+
|
|
49
|
+
`profiles` · `accounts` · `posts` · `media` · `tools` · `ads` · `insights` ·
|
|
50
|
+
`platform_analytics` · `inbox_analytics` · `inbox` · `comments` · `reviews` ·
|
|
51
|
+
`contacts` · `automations` · `broadcasts` · `sequences` · `webhooks` · `api_keys` ·
|
|
52
|
+
`queue` (`.schedules`) · `account_groups` · `engagement` (`.x`) · `users` ·
|
|
53
|
+
`invite_tokens` · `numbers` · `senders` · `sms` · `otp`
|
|
54
|
+
|
|
55
|
+
Top level: `ph.openapi`, `ph.ping`, `ph.me`, `ph.analytics`, `ph.analytics_posts`,
|
|
56
|
+
`ph.analytics_timeseries`, `ph.logs`, `ph.usage`.
|
|
57
|
+
|
|
58
|
+
## Media upload
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
content = File.binread('logo.png')
|
|
62
|
+
asset = ph.media.upload(content, 'logo.png') # => {"assetId"=>..., "url"=>...}
|
|
63
|
+
ph.posts.create({ 'content' => 'New!', 'accountIds' => ['acc_1'], 'mediaAssetId' => asset['assetId'] })
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
ruby packages/ruby-sdk/test/test_contract.rb # SDK ↔ spec contract test
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The contract test verifies that the endpoint path produced by every SDK method is defined in the OpenAPI spec.
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Posthubify
|
|
4
|
+
# PostHubify /v1 API client. All resource groups are exposed as snake_case readers.
|
|
5
|
+
class Client
|
|
6
|
+
attr_reader :profiles, :accounts, :posts, :media, :tools, :ads, :insights,
|
|
7
|
+
:platform_analytics, :inbox_analytics, :inbox, :comments, :reviews,
|
|
8
|
+
:contacts, :automations, :comment_automations, :workflows, :broadcasts, :sequences, :webhooks, :api_keys,
|
|
9
|
+
:queue, :account_groups, :engagement, :users, :invite_tokens,
|
|
10
|
+
:numbers, :senders, :sms, :otp,
|
|
11
|
+
:custom_fields, :whatsapp, :gmb, :discord, :discovery
|
|
12
|
+
|
|
13
|
+
# transport: injectable for tests (Node opts.fetch equivalent).
|
|
14
|
+
def initialize(api_key: nil, base_url: Transport::DEFAULT_BASE, timeout: 30, transport: nil)
|
|
15
|
+
@http = transport || Transport.new(api_key: api_key, base_url: base_url, timeout: timeout)
|
|
16
|
+
|
|
17
|
+
@profiles = ProfilesResource.new(@http)
|
|
18
|
+
@accounts = AccountsResource.new(@http)
|
|
19
|
+
@posts = PostsResource.new(@http)
|
|
20
|
+
@media = MediaResource.new(@http)
|
|
21
|
+
@tools = ToolsResource.new(@http)
|
|
22
|
+
@ads = AdsResource.new(@http)
|
|
23
|
+
@insights = InsightsResource.new(@http)
|
|
24
|
+
@platform_analytics = PlatformAnalyticsResource.new(@http)
|
|
25
|
+
@inbox_analytics = InboxAnalyticsResource.new(@http)
|
|
26
|
+
@inbox = InboxResource.new(@http)
|
|
27
|
+
@comments = CommentsResource.new(@http)
|
|
28
|
+
@reviews = ReviewsResource.new(@http)
|
|
29
|
+
@contacts = ContactsResource.new(@http)
|
|
30
|
+
@automations = AutomationsResource.new(@http)
|
|
31
|
+
@comment_automations = CommentAutomationsResource.new(@http)
|
|
32
|
+
@workflows = WorkflowsResource.new(@http)
|
|
33
|
+
@broadcasts = BroadcastsResource.new(@http)
|
|
34
|
+
@sequences = SequencesResource.new(@http)
|
|
35
|
+
@webhooks = WebhooksResource.new(@http)
|
|
36
|
+
@api_keys = ApiKeysResource.new(@http)
|
|
37
|
+
@queue = QueueResource.new(@http)
|
|
38
|
+
@account_groups = AccountGroupsResource.new(@http)
|
|
39
|
+
@engagement = EngagementResource.new(@http)
|
|
40
|
+
@users = UsersResource.new(@http)
|
|
41
|
+
@invite_tokens = InviteTokensResource.new(@http)
|
|
42
|
+
@numbers = NumbersResource.new(@http)
|
|
43
|
+
@senders = SendersResource.new(@http)
|
|
44
|
+
@sms = SmsResource.new(@http)
|
|
45
|
+
@otp = OtpResource.new(@http)
|
|
46
|
+
@custom_fields = CustomFieldsResource.new(@http)
|
|
47
|
+
@whatsapp = WhatsAppResource.new(@http)
|
|
48
|
+
@gmb = GmbResource.new(@http)
|
|
49
|
+
@discord = DiscordResource.new(@http)
|
|
50
|
+
@discovery = DiscoveryResource.new(@http)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# --- top-level endpoints ---
|
|
54
|
+
def openapi
|
|
55
|
+
@http.req('GET', '/openapi.json')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ping
|
|
59
|
+
@http.data('GET', '/ping')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def me
|
|
63
|
+
@http.data('GET', '/me')
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# AI content generation (F1) — command → per-platform draft + variants (multi-language).
|
|
67
|
+
def generate(command, platforms, variants: nil, context: nil, max_chars: nil)
|
|
68
|
+
@http.data('POST', '/generate', body: {
|
|
69
|
+
'command' => command, 'platforms' => platforms, 'variants' => variants,
|
|
70
|
+
'context' => context, 'maxChars' => max_chars,
|
|
71
|
+
})
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Subtitle generation (F4) — text → timed SRT/VTT (pure conversion, no AI/charge; a read key is enough).
|
|
75
|
+
# @param text [String] text to turn into subtitles (≤20000 characters)
|
|
76
|
+
# @param format [String] 'srt' or 'vtt' (default 'srt')
|
|
77
|
+
# @param duration_sec [Float, nil] if given, cues are distributed proportionally over this duration (audio-synced; ≤86400)
|
|
78
|
+
# @param max_chars_per_line [Integer, nil] max characters per line (10–120, default 42)
|
|
79
|
+
# @param max_lines_per_cue [Integer, nil] max lines per cue (1–4, default 2)
|
|
80
|
+
# @param chars_per_sec [Float, nil] reading speed when no duration is given (may be fractional, e.g. 15.5)
|
|
81
|
+
# @param gap_ms [Integer, nil] gap between cues (ms, default 80)
|
|
82
|
+
def subtitles(text, format: 'srt', duration_sec: nil, max_chars_per_line: nil,
|
|
83
|
+
max_lines_per_cue: nil, chars_per_sec: nil, gap_ms: nil)
|
|
84
|
+
@http.data('POST', '/subtitles', body: {
|
|
85
|
+
'text' => text, 'format' => format, 'durationSec' => duration_sec,
|
|
86
|
+
'maxCharsPerLine' => max_chars_per_line, 'maxLinesPerCue' => max_lines_per_cue,
|
|
87
|
+
'charsPerSec' => chars_per_sec, 'gapMs' => gap_ms,
|
|
88
|
+
})
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Promo video render (F4) — remote image (+optional audio/subtitle) → branded video (ffmpeg) → R2 URL.
|
|
92
|
+
# @param image_url [String] publicly-accessible https image URL
|
|
93
|
+
# @param format [String] video ratio: '9:16' | '1:1' | '16:9'
|
|
94
|
+
# @param audio_url [String, nil] voiceover (mp3) URL; if given, the video duration is matched to the audio
|
|
95
|
+
# @param subtitle_text [String, nil] subtitle text to burn into the video
|
|
96
|
+
# @param subtitle_rtl [Boolean, nil] whether the subtitle is right-to-left (Arabic, etc.)
|
|
97
|
+
# HEAVY operation (write permission + low rate limit). Media is downloaded SSRF-protected (https + type + ≤25MB).
|
|
98
|
+
def videos(image_url, format, audio_url: nil, subtitle_text: nil, subtitle_rtl: nil,
|
|
99
|
+
bumper_duration_sec: nil, main_duration_sec: nil)
|
|
100
|
+
@http.data('POST', '/videos', body: {
|
|
101
|
+
'imageUrl' => image_url, 'format' => format, 'audioUrl' => audio_url,
|
|
102
|
+
'subtitleText' => subtitle_text, 'subtitleRtl' => subtitle_rtl,
|
|
103
|
+
'bumperDurationSec' => bumper_duration_sec, 'mainDurationSec' => main_duration_sec,
|
|
104
|
+
})
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def analytics
|
|
108
|
+
@http.data('GET', '/analytics')
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Post analytics list — source: 'external' is synced from the platform, not PostHubify (B5).
|
|
112
|
+
def analytics_posts(platform: nil, account_id: nil, source: nil, limit: nil, cursor: nil)
|
|
113
|
+
@http.req('GET', '/analytics/posts', query: {
|
|
114
|
+
'platform' => platform, 'accountId' => account_id, 'source' => source,
|
|
115
|
+
'limit' => limit, 'cursor' => cursor
|
|
116
|
+
})
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Stored daily metric time series (account/post/inbox aggregates).
|
|
120
|
+
def analytics_timeseries(days: nil, platform: nil, account_id: nil, from_date: nil, to_date: nil, source: nil)
|
|
121
|
+
@http.data('GET', '/analytics/timeseries', query: {
|
|
122
|
+
'days' => days, 'platform' => platform, 'accountId' => account_id,
|
|
123
|
+
'fromDate' => from_date, 'toDate' => to_date, 'source' => source
|
|
124
|
+
})
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def usage
|
|
128
|
+
@http.data('GET', '/usage')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def logs(limit: nil, category: nil, status: nil, platform: nil, days: nil)
|
|
132
|
+
@http.data('GET', '/logs', query: {
|
|
133
|
+
'limit' => limit, 'category' => category, 'status' => status,
|
|
134
|
+
'platform' => platform, 'days' => days
|
|
135
|
+
})
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Posthubify
|
|
4
|
+
# Raised when an API request returns a non-2xx status.
|
|
5
|
+
#
|
|
6
|
+
# begin
|
|
7
|
+
# ph.posts.get("missing")
|
|
8
|
+
# rescue Posthubify::Error => e
|
|
9
|
+
# puts [e.status, e.code, e.message].inspect
|
|
10
|
+
# end
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
attr_reader :status, :code, :body
|
|
13
|
+
|
|
14
|
+
def initialize(status, message, code = nil, body = nil)
|
|
15
|
+
super(message)
|
|
16
|
+
@status = status
|
|
17
|
+
@code = code
|
|
18
|
+
@body = body
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'securerandom'
|
|
7
|
+
|
|
8
|
+
module Posthubify
|
|
9
|
+
# HTTP transport layer — standard library only (net/http). Bearer auth, JSON body,
|
|
10
|
+
# multipart upload, `{ "data" => ... }` envelope unwrapping, non-2xx → Posthubify::Error.
|
|
11
|
+
class Transport
|
|
12
|
+
DEFAULT_BASE = 'http://localhost:8787/v1'
|
|
13
|
+
USER_AGENT = 'posthubify-ruby/0.1.0'
|
|
14
|
+
|
|
15
|
+
def initialize(api_key:, base_url: DEFAULT_BASE, timeout: 30)
|
|
16
|
+
raise ArgumentError, 'api_key required (sk_…)' if api_key.nil? || api_key.empty?
|
|
17
|
+
|
|
18
|
+
@key = api_key
|
|
19
|
+
@base = base_url.sub(%r{/\z}, '')
|
|
20
|
+
@timeout = timeout
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Raw request: 2xx → parsed body, otherwise Posthubify::Error.
|
|
24
|
+
# files: [field_name, bytes, file_name] — multipart upload (media.upload).
|
|
25
|
+
def req(method, path, query: nil, body: nil, files: nil, idempotency_key: nil)
|
|
26
|
+
uri = URI("#{@base}#{path}")
|
|
27
|
+
unless query.nil?
|
|
28
|
+
pairs = query.reject { |_k, v| v.nil? }
|
|
29
|
+
uri.query = URI.encode_www_form(pairs) unless pairs.empty?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
request = build_request(method, uri, body, files, idempotency_key)
|
|
33
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
34
|
+
http.use_ssl = uri.scheme == 'https'
|
|
35
|
+
http.read_timeout = @timeout
|
|
36
|
+
http.open_timeout = @timeout
|
|
37
|
+
|
|
38
|
+
response = http.request(request)
|
|
39
|
+
parsed = parse(response.body)
|
|
40
|
+
status = response.code.to_i
|
|
41
|
+
unless (200..299).cover?(status)
|
|
42
|
+
message = (parsed.is_a?(Hash) && parsed['error']) || "HTTP #{status}"
|
|
43
|
+
code = parsed.is_a?(Hash) ? parsed['code'] : nil
|
|
44
|
+
raise Posthubify::Error.new(status, message, code, parsed)
|
|
45
|
+
end
|
|
46
|
+
parsed
|
|
47
|
+
rescue SocketError, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
|
|
48
|
+
raise Posthubify::Error.new(0, "Network error: #{e.message}")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Unwraps the `{ "data" => ... }` envelope.
|
|
52
|
+
def data(method, path, query: nil, body: nil, files: nil, idempotency_key: nil)
|
|
53
|
+
result = req(method, path, query: query, body: body, files: files, idempotency_key: idempotency_key)
|
|
54
|
+
result.is_a?(Hash) && result.key?('data') ? result['data'] : result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def build_request(method, uri, body, files, idempotency_key)
|
|
60
|
+
klass = {
|
|
61
|
+
'GET' => Net::HTTP::Get, 'POST' => Net::HTTP::Post, 'PUT' => Net::HTTP::Put,
|
|
62
|
+
'PATCH' => Net::HTTP::Patch, 'DELETE' => Net::HTTP::Delete
|
|
63
|
+
}.fetch(method.to_s.upcase)
|
|
64
|
+
request = klass.new(uri)
|
|
65
|
+
request['Authorization'] = "Bearer #{@key}"
|
|
66
|
+
request['User-Agent'] = USER_AGENT
|
|
67
|
+
request['Accept'] = 'application/json'
|
|
68
|
+
request['Idempotency-Key'] = idempotency_key if idempotency_key
|
|
69
|
+
|
|
70
|
+
if files
|
|
71
|
+
field, content, filename = files
|
|
72
|
+
boundary = SecureRandom.hex(16)
|
|
73
|
+
request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
|
74
|
+
request.body = multipart_body(field, content, filename, boundary)
|
|
75
|
+
elsif !body.nil?
|
|
76
|
+
request['Content-Type'] = 'application/json'
|
|
77
|
+
request.body = JSON.generate(body)
|
|
78
|
+
end
|
|
79
|
+
request
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def multipart_body(field, content, filename, boundary)
|
|
83
|
+
mime = case File.extname(filename).downcase
|
|
84
|
+
when '.png' then 'image/png'
|
|
85
|
+
when '.jpg', '.jpeg' then 'image/jpeg'
|
|
86
|
+
when '.mp4' then 'video/mp4'
|
|
87
|
+
else 'application/octet-stream'
|
|
88
|
+
end
|
|
89
|
+
"--#{boundary}\r\n" \
|
|
90
|
+
"Content-Disposition: form-data; name=\"#{field}\"; filename=\"#{filename}\"\r\n" \
|
|
91
|
+
"Content-Type: #{mime}\r\n\r\n" + content + "\r\n--#{boundary}--\r\n"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse(raw)
|
|
95
|
+
return nil if raw.nil? || raw.empty?
|
|
96
|
+
|
|
97
|
+
JSON.parse(raw)
|
|
98
|
+
rescue JSON::ParserError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Posthubify
|
|
4
|
+
# Profiles (Node sdk .profiles) — workspace/brand separation.
|
|
5
|
+
class ProfilesResource
|
|
6
|
+
def initialize(http)
|
|
7
|
+
@http = http
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# List all profiles.
|
|
11
|
+
def list
|
|
12
|
+
@http.data('GET', '/profiles')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Get a single profile.
|
|
16
|
+
def get(id)
|
|
17
|
+
@http.data('GET', "/profiles/#{id}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Create a profile (input: { name }).
|
|
21
|
+
def create(input)
|
|
22
|
+
@http.data('POST', '/profiles', body: input)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Update a profile (input: name/description/color/isDefault).
|
|
26
|
+
def update(id, input)
|
|
27
|
+
@http.data('PATCH', "/profiles/#{id}", body: input)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Delete a profile.
|
|
31
|
+
def delete(id)
|
|
32
|
+
@http.data('DELETE', "/profiles/#{id}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Connected platform accounts (Node sdk .accounts) — health, follower stats, OAuth linking.
|
|
37
|
+
class AccountsResource
|
|
38
|
+
def initialize(http)
|
|
39
|
+
@http = http
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# List accounts (filter: profile_id/platform/is_active).
|
|
43
|
+
def list(profile_id: nil, platform: nil, is_active: nil)
|
|
44
|
+
@http.data('GET', '/accounts', query: {
|
|
45
|
+
'profileId' => profile_id,
|
|
46
|
+
'platform' => platform,
|
|
47
|
+
'isActive' => is_active
|
|
48
|
+
})
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Update an account (input: label/profileId/isActive).
|
|
52
|
+
def update(id, input)
|
|
53
|
+
@http.data('PATCH', "/accounts/#{id}", body: input)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Health of all accounts + summary (token expiry, reconnection needs).
|
|
57
|
+
def health
|
|
58
|
+
@http.data('GET', '/accounts/health')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Health report for a single account.
|
|
62
|
+
def health_of(id)
|
|
63
|
+
@http.data('GET', "/accounts/#{id}/health")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Pinterest boards (for posts.create options.board). 400 on an unsupported platform.
|
|
67
|
+
def boards(id)
|
|
68
|
+
@http.data('GET', "/accounts/#{id}/boards")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Reddit subreddit flairs (for posts.create options.flairId). 400 on an unsupported platform.
|
|
72
|
+
def flairs(id)
|
|
73
|
+
@http.data('GET', "/accounts/#{id}/flairs")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# ── Entity pickers + publishing defaults (D8) ──
|
|
77
|
+
def reddit_subreddits(id)
|
|
78
|
+
@http.data('GET', "/accounts/#{id}/reddit-subreddits")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def set_default_subreddit(id, subreddit)
|
|
82
|
+
@http.data('PUT', "/accounts/#{id}/reddit-subreddits", body: { 'subreddit' => subreddit })
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def youtube_playlists(id)
|
|
86
|
+
@http.data('GET', "/accounts/#{id}/youtube-playlists")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def set_default_playlist(id, playlist_id)
|
|
90
|
+
@http.data('PUT', "/accounts/#{id}/youtube-playlists", body: { 'playlistId' => playlist_id })
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def linkedin_organizations(id)
|
|
94
|
+
@http.data('GET', "/accounts/#{id}/linkedin-organizations")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def pinterest_boards(id)
|
|
98
|
+
@http.data('GET', "/accounts/#{id}/pinterest-boards")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def set_default_board(id, board_id)
|
|
102
|
+
@http.data('PUT', "/accounts/#{id}/pinterest-boards", body: { 'boardId' => board_id })
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# ── Bot command menu (A1) — Telegram bot "/" commands (per account). ──
|
|
106
|
+
# Get the bot command menu (Telegram getMyCommands). scope/language_code optional.
|
|
107
|
+
def bot_commands(id, scope: nil, language_code: nil)
|
|
108
|
+
@http.data('GET', "/accounts/#{id}/bot-commands", query: {
|
|
109
|
+
'scope' => scope,
|
|
110
|
+
'languageCode' => language_code
|
|
111
|
+
})
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Set the bot command menu (Telegram setMyCommands).
|
|
115
|
+
def set_bot_commands(id, commands, scope: nil, language_code: nil)
|
|
116
|
+
@http.data('PUT', "/accounts/#{id}/bot-commands", body: {
|
|
117
|
+
'commands' => commands,
|
|
118
|
+
'scope' => scope,
|
|
119
|
+
'languageCode' => language_code
|
|
120
|
+
})
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Clear the bot command menu (Telegram deleteMyCommands).
|
|
124
|
+
def delete_bot_commands(id, scope: nil, language_code: nil)
|
|
125
|
+
@http.data('DELETE', "/accounts/#{id}/bot-commands", query: {
|
|
126
|
+
'scope' => scope,
|
|
127
|
+
'languageCode' => language_code
|
|
128
|
+
})
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Follower history + growth across all accounts (granularity: daily|weekly|monthly).
|
|
132
|
+
def follower_stats_all(days: nil, granularity: nil)
|
|
133
|
+
@http.data('GET', '/accounts/follower-stats', query: {
|
|
134
|
+
'days' => days,
|
|
135
|
+
'granularity' => granularity
|
|
136
|
+
})
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Follower history + change for a single account (granularity: daily|weekly|monthly).
|
|
140
|
+
def follower_stats(id, days: nil, granularity: nil)
|
|
141
|
+
@http.data('GET', "/accounts/#{id}/follower-stats", query: {
|
|
142
|
+
'days' => days,
|
|
143
|
+
'granularity' => granularity
|
|
144
|
+
})
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Delete an account.
|
|
148
|
+
def delete(id)
|
|
149
|
+
@http.data('DELETE', "/accounts/#{id}")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# OAuth connect URL — the user visits it in a browser (query: profile_id).
|
|
153
|
+
def connect_url(platform, profile_id: nil)
|
|
154
|
+
@http.data('GET', "/connect/#{platform}", query: { 'profileId' => profile_id })
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# ── Headless credential connect (D8 cp2) — linking without OAuth. ──
|
|
158
|
+
def connect_bluesky(handle, app_password, profile_id: nil, label: nil)
|
|
159
|
+
@http.data('POST', '/connect/bluesky/credentials', body: { 'handle' => handle, 'appPassword' => app_password, 'profileId' => profile_id, 'label' => label })
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def connect_telegram(bot_token, chat_id, profile_id: nil, label: nil)
|
|
163
|
+
@http.data('POST', '/connect/telegram', body: { 'botToken' => bot_token, 'chatId' => chat_id, 'profileId' => profile_id, 'label' => label })
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def connect_whatsapp(access_token, phone_number_id, recipients, waba_id: nil, profile_id: nil, label: nil)
|
|
167
|
+
@http.data('POST', '/connect/whatsapp/credentials', body: { 'accessToken' => access_token, 'phoneNumberId' => phone_number_id, 'recipients' => recipients, 'wabaId' => waba_id, 'profileId' => profile_id, 'label' => label })
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def connect_line(channel_access_token, channel_secret, profile_id: nil, label: nil)
|
|
171
|
+
@http.data('POST', '/connect/line/credentials', body: { 'channelAccessToken' => channel_access_token, 'channelSecret' => channel_secret, 'profileId' => profile_id, 'label' => label })
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# ── Headless OAuth connect (D8 cp3) — tempToken start/status. ──
|
|
175
|
+
# Start headless OAuth → {tempToken, authUrl}: open authUrl in a browser, then poll status.
|
|
176
|
+
def connect_oauth_start(platform, profile_id: nil, label: nil)
|
|
177
|
+
@http.data('POST', '/connect/oauth/start', body: { 'platform' => platform, 'profileId' => profile_id, 'label' => label })
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Headless OAuth status (poll): pending | connected (+account) | error.
|
|
181
|
+
def connect_oauth_status(temp_token)
|
|
182
|
+
@http.data('GET', '/connect/oauth/status', query: { 'tempToken' => temp_token })
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|