omnisocials 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 +373 -0
- data/lib/omnisocials/client.rb +249 -0
- data/lib/omnisocials/errors.rb +77 -0
- data/lib/omnisocials/internal.rb +138 -0
- data/lib/omnisocials/resources/accounts.rb +25 -0
- data/lib/omnisocials/resources/analytics.rb +52 -0
- data/lib/omnisocials/resources/folders.rb +41 -0
- data/lib/omnisocials/resources/locations.rb +24 -0
- data/lib/omnisocials/resources/media.rb +128 -0
- data/lib/omnisocials/resources/posts.rb +173 -0
- data/lib/omnisocials/resources/webhooks.rb +61 -0
- data/lib/omnisocials/version.rb +5 -0
- data/lib/omnisocials/webhooks.rb +113 -0
- data/lib/omnisocials.rb +29 -0
- metadata +65 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OmniSocials
|
|
4
|
+
module Resources
|
|
5
|
+
# Webhooks resource: manage event subscriptions (post.scheduled,
|
|
6
|
+
# post.published, post.failed).
|
|
7
|
+
#
|
|
8
|
+
# For verifying incoming deliveries, see OmniSocials::Webhooks.verify.
|
|
9
|
+
class Webhooks
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# GET /webhooks - list webhook subscriptions.
|
|
15
|
+
def list
|
|
16
|
+
@client.request("GET", "/webhooks")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# GET /webhooks/{id} - fetch a single webhook subscription.
|
|
20
|
+
def get(webhook_id)
|
|
21
|
+
@client.request("GET", "/webhooks/#{webhook_id}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# POST /webhooks - create a webhook subscription.
|
|
25
|
+
#
|
|
26
|
+
# `url` must be HTTPS. `events` is a non-empty subset of
|
|
27
|
+
# post.scheduled, post.published, post.failed. The signing `secret` is
|
|
28
|
+
# only returned once, in this response - store it.
|
|
29
|
+
def create(url:, events:)
|
|
30
|
+
@client.request(
|
|
31
|
+
"POST", "/webhooks",
|
|
32
|
+
json: { "url" => url, "events" => Array(events) }
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# PATCH /webhooks/{id} - update url, events, or active state.
|
|
37
|
+
def update(webhook_id, url: nil, events: nil, is_active: nil)
|
|
38
|
+
body = Internal.drop_nil(
|
|
39
|
+
{
|
|
40
|
+
"url" => url,
|
|
41
|
+
"events" => events.nil? ? nil : Array(events),
|
|
42
|
+
"is_active" => is_active
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
@client.request("PATCH", "/webhooks/#{webhook_id}", json: body)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# DELETE /webhooks/{id} - delete a webhook. Returns nil (204).
|
|
49
|
+
def delete(webhook_id)
|
|
50
|
+
@client.request("DELETE", "/webhooks/#{webhook_id}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# POST /webhooks/{id}/rotate-secret - rotate the signing secret.
|
|
54
|
+
#
|
|
55
|
+
# The new secret is only returned once, in this response.
|
|
56
|
+
def rotate_secret(webhook_id)
|
|
57
|
+
@client.request("POST", "/webhooks/#{webhook_id}/rotate-secret")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module OmniSocials
|
|
7
|
+
# Webhook signature verification.
|
|
8
|
+
#
|
|
9
|
+
# OmniSocials signs every webhook delivery with HMAC-SHA256 (Stripe-style):
|
|
10
|
+
#
|
|
11
|
+
# - Header: X-OmniSocials-Signature: t=<unix>,v1=<hex>
|
|
12
|
+
# - Signed value: "{timestamp}.{raw_body}" (UTF-8), keyed with the
|
|
13
|
+
# webhook's secret, hex digest.
|
|
14
|
+
module Webhooks
|
|
15
|
+
class << self
|
|
16
|
+
# Verify an OmniSocials webhook delivery and return the parsed event.
|
|
17
|
+
#
|
|
18
|
+
# payload - the raw request body String, exactly as received. Do not
|
|
19
|
+
# parse and re-serialize it first; the signature is over the
|
|
20
|
+
# raw bytes.
|
|
21
|
+
# signature - the X-OmniSocials-Signature header value, of the form
|
|
22
|
+
# t=<unix>,v1=<hex>.
|
|
23
|
+
# secret - the webhook's signing secret (returned once when the
|
|
24
|
+
# webhook is created, or via rotate_secret).
|
|
25
|
+
# tolerance - maximum allowed age (and future skew) of the signed
|
|
26
|
+
# timestamp, in seconds. Defaults to 300. Pass nil to skip
|
|
27
|
+
# the timestamp check (not recommended).
|
|
28
|
+
#
|
|
29
|
+
# Returns the parsed event as a Hash (string keys).
|
|
30
|
+
#
|
|
31
|
+
# Raises OmniSocials::WebhookVerificationError if the header is
|
|
32
|
+
# malformed, the timestamp is outside the tolerance window, or the
|
|
33
|
+
# signature does not match. Uses a constant-time comparison.
|
|
34
|
+
def verify(payload:, signature:, secret:, tolerance: 300)
|
|
35
|
+
unless payload.is_a?(String)
|
|
36
|
+
raise WebhookVerificationError,
|
|
37
|
+
"payload must be a String (the raw request body), got #{payload.class}."
|
|
38
|
+
end
|
|
39
|
+
unless signature.is_a?(String) && !signature.empty?
|
|
40
|
+
raise WebhookVerificationError, "Missing signature header."
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
timestamp = nil
|
|
44
|
+
candidates = []
|
|
45
|
+
signature.split(",").each do |pair|
|
|
46
|
+
key, _, value = pair.strip.partition("=")
|
|
47
|
+
case key
|
|
48
|
+
when "t"
|
|
49
|
+
begin
|
|
50
|
+
timestamp = Integer(value, 10)
|
|
51
|
+
rescue ArgumentError, TypeError
|
|
52
|
+
raise WebhookVerificationError,
|
|
53
|
+
"Malformed signature header: non-integer timestamp."
|
|
54
|
+
end
|
|
55
|
+
when "v1"
|
|
56
|
+
candidates << value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if timestamp.nil?
|
|
61
|
+
raise WebhookVerificationError,
|
|
62
|
+
"Malformed signature header: missing 't=' timestamp."
|
|
63
|
+
end
|
|
64
|
+
if candidates.empty?
|
|
65
|
+
raise WebhookVerificationError,
|
|
66
|
+
"Malformed signature header: missing 'v1=' signature."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless tolerance.nil?
|
|
70
|
+
drift = (Time.now.to_i - timestamp).abs
|
|
71
|
+
if drift > tolerance
|
|
72
|
+
raise WebhookVerificationError,
|
|
73
|
+
"Timestamp outside the tolerance window (#{drift}s > #{tolerance}s). " \
|
|
74
|
+
"The delivery may be stale or replayed."
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
signed_value = "#{timestamp}.".b + payload.b
|
|
79
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_value)
|
|
80
|
+
|
|
81
|
+
unless candidates.any? { |candidate| secure_compare(expected, candidate) }
|
|
82
|
+
raise WebhookVerificationError,
|
|
83
|
+
"Signature mismatch: the payload was not signed with this secret, " \
|
|
84
|
+
"or the body was modified in transit."
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
event = JSON.parse(payload)
|
|
89
|
+
rescue JSON::ParserError
|
|
90
|
+
raise WebhookVerificationError, "Payload is not valid JSON."
|
|
91
|
+
end
|
|
92
|
+
unless event.is_a?(Hash)
|
|
93
|
+
raise WebhookVerificationError, "Payload is not a JSON object."
|
|
94
|
+
end
|
|
95
|
+
event
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
# Constant-time string comparison.
|
|
101
|
+
def secure_compare(expected, candidate)
|
|
102
|
+
return false unless candidate.is_a?(String)
|
|
103
|
+
|
|
104
|
+
if OpenSSL.respond_to?(:secure_compare)
|
|
105
|
+
OpenSSL.secure_compare(expected, candidate)
|
|
106
|
+
else
|
|
107
|
+
expected.bytesize == candidate.bytesize &&
|
|
108
|
+
OpenSSL.fixed_length_secure_compare(expected, candidate)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/omnisocials.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Official Ruby SDK for the OmniSocials Public API.
|
|
4
|
+
#
|
|
5
|
+
# Docs: https://docs.omnisocials.com
|
|
6
|
+
#
|
|
7
|
+
# require "omnisocials"
|
|
8
|
+
#
|
|
9
|
+
# client = OmniSocials::Client.new # reads OMNISOCIALS_API_KEY from env
|
|
10
|
+
# post = client.posts.create(
|
|
11
|
+
# content: "Hello from Ruby",
|
|
12
|
+
# channels: ["instagram", "linkedin"]
|
|
13
|
+
# )
|
|
14
|
+
|
|
15
|
+
require_relative "omnisocials/version"
|
|
16
|
+
require_relative "omnisocials/errors"
|
|
17
|
+
require_relative "omnisocials/internal"
|
|
18
|
+
require_relative "omnisocials/webhooks"
|
|
19
|
+
require_relative "omnisocials/resources/posts"
|
|
20
|
+
require_relative "omnisocials/resources/media"
|
|
21
|
+
require_relative "omnisocials/resources/folders"
|
|
22
|
+
require_relative "omnisocials/resources/accounts"
|
|
23
|
+
require_relative "omnisocials/resources/analytics"
|
|
24
|
+
require_relative "omnisocials/resources/locations"
|
|
25
|
+
require_relative "omnisocials/resources/webhooks"
|
|
26
|
+
require_relative "omnisocials/client"
|
|
27
|
+
|
|
28
|
+
module OmniSocials
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: omnisocials
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- OmniSocials
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Schedule and publish social media posts, upload media, and read analytics
|
|
14
|
+
across Instagram, Facebook, LinkedIn, YouTube, TikTok, X, Pinterest, Bluesky, Threads,
|
|
15
|
+
Mastodon, and Google Business via the OmniSocials Public API. Zero runtime dependencies
|
|
16
|
+
(Net::HTTP, OpenSSL, JSON).
|
|
17
|
+
email:
|
|
18
|
+
- hello@omnisocials.com
|
|
19
|
+
executables: []
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/omnisocials.rb
|
|
25
|
+
- lib/omnisocials/client.rb
|
|
26
|
+
- lib/omnisocials/errors.rb
|
|
27
|
+
- lib/omnisocials/internal.rb
|
|
28
|
+
- lib/omnisocials/resources/accounts.rb
|
|
29
|
+
- lib/omnisocials/resources/analytics.rb
|
|
30
|
+
- lib/omnisocials/resources/folders.rb
|
|
31
|
+
- lib/omnisocials/resources/locations.rb
|
|
32
|
+
- lib/omnisocials/resources/media.rb
|
|
33
|
+
- lib/omnisocials/resources/posts.rb
|
|
34
|
+
- lib/omnisocials/resources/webhooks.rb
|
|
35
|
+
- lib/omnisocials/version.rb
|
|
36
|
+
- lib/omnisocials/webhooks.rb
|
|
37
|
+
homepage: https://omnisocials.com
|
|
38
|
+
licenses:
|
|
39
|
+
- MIT
|
|
40
|
+
metadata:
|
|
41
|
+
homepage_uri: https://omnisocials.com
|
|
42
|
+
documentation_uri: https://docs.omnisocials.com
|
|
43
|
+
source_code_uri: https://github.com/OmniSocials/omnisocials-ruby
|
|
44
|
+
bug_tracker_uri: https://github.com/OmniSocials/omnisocials-ruby/issues
|
|
45
|
+
rubygems_mfa_required: 'true'
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.4.19
|
|
62
|
+
signing_key:
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Official Ruby SDK for the OmniSocials Public API
|
|
65
|
+
test_files: []
|