fopost-rails 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/CHANGELOG.md +20 -0
- data/LICENSE +21 -0
- data/README.md +233 -0
- data/app/controllers/fopost/rails/webhooks_controller.rb +59 -0
- data/config/routes.rb +9 -0
- data/lib/fopost/rails/application_job.rb +47 -0
- data/lib/fopost/rails/configuration.rb +119 -0
- data/lib/fopost/rails/create_post_job.rb +44 -0
- data/lib/fopost/rails/engine.rb +19 -0
- data/lib/fopost/rails/publish_job.rb +19 -0
- data/lib/fopost/rails/railtie.rb +22 -0
- data/lib/fopost/rails/version.rb +7 -0
- data/lib/fopost/rails/webhook_signature.rb +33 -0
- data/lib/fopost/rails.rb +85 -0
- data/lib/fopost-rails.rb +4 -0
- data/lib/generators/fopost/install/install_generator.rb +30 -0
- data/lib/generators/fopost/install/templates/fopost.rb.tt +35 -0
- metadata +118 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5959178d911d67f15c611ae03d778c338acb8c90e0f0d40b5b6484ba5b0608c6
|
|
4
|
+
data.tar.gz: 4ed0650b489d85ad122a10ee30787456eca0606846c8bfcdab4aeb42ccf8fe23
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: dc18b76fcd3d73197793b2803bd7b427b86d8d9a4e194528c70009b47712841ff8317eeaecb957920506ddea4b9b7200afafe23588a178e7a4e93f997007ad34
|
|
7
|
+
data.tar.gz: f89fa93ca20e7f221a0e3e7fd90c18dfd9853a4e964b20bfe9ddfdc47a90831212e74232fa130976ca4c2f10610094afae7f8f27d9b76b6078b0cf695607ed8f
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the gem follows
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-08-30
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
- `Fopost::Rails.configure` and `config.fopost`, resolving each setting from an explicit value,
|
|
12
|
+
then Rails credentials under `fopost:`, then the environment.
|
|
13
|
+
- A memoized, thread-safe `Fopost::Rails.client`.
|
|
14
|
+
- `rails generate fopost:install`, writing `config/initializers/fopost.rb`.
|
|
15
|
+
- `PublishJob` and `CreatePostJob`, re-enqueueing a rate-limited call for the interval the API
|
|
16
|
+
asked for.
|
|
17
|
+
- A mountable engine that verifies incoming webhook signatures and republishes them as
|
|
18
|
+
`ActiveSupport::Notifications` events.
|
|
19
|
+
|
|
20
|
+
[0.1.0]: https://github.com/fopost/fopost-rails/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Porter Bridge, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# FoPost for Rails
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/fopost-rails)
|
|
4
|
+
[](https://rubygems.org/gems/fopost-rails)
|
|
5
|
+
[](https://github.com/fopost/fopost-rails/actions)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
The official Rails integration for the [FoPost](https://fopost.com) API. Connect social accounts
|
|
9
|
+
once, then compose, schedule, and publish to +30 platforms from your own application.
|
|
10
|
+
|
|
11
|
+
This gem is a thin wrapper around the [`fopost`](https://github.com/fopost/fopost-ruby) gem. Every
|
|
12
|
+
request, retry, model, and error class lives there; what is added here is Rails wiring:
|
|
13
|
+
|
|
14
|
+
- `config.fopost` and Rails credentials, with a sensible fallback order
|
|
15
|
+
- a memoized, thread-safe `Fopost::Rails.client`
|
|
16
|
+
- `rails generate fopost:install`
|
|
17
|
+
- ActiveJob jobs, so publishing never blocks a request
|
|
18
|
+
- a mountable endpoint that verifies and republishes incoming FoPost webhooks
|
|
19
|
+
|
|
20
|
+
Requires Ruby 3.1+ and Rails 7.0+.
|
|
21
|
+
|
|
22
|
+
> **0.x release.** The public API is still settling and minor versions may contain breaking
|
|
23
|
+
> changes. Pin an exact version if that matters to you.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle add fopost-rails
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then write the initializer:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bin/rails generate fopost:install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configure
|
|
38
|
+
|
|
39
|
+
Create an API key at [app.fopost.com/api-keys](https://app.fopost.com/api-keys) and put it
|
|
40
|
+
somewhere the app can read it:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bin/rails credentials:edit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
fopost:
|
|
48
|
+
api_key: fp_your_key_here
|
|
49
|
+
default_workspace_id: ws_...
|
|
50
|
+
webhook_secret: whsec_...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or in the environment:
|
|
54
|
+
|
|
55
|
+
```dotenv
|
|
56
|
+
FOPOST_API_KEY=fp_your_key_here
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every setting resolves the same way — **what you set explicitly wins, then Rails credentials under
|
|
60
|
+
`fopost:`, then the environment, then the default**:
|
|
61
|
+
|
|
62
|
+
| Setting | Credentials key | Environment | Default |
|
|
63
|
+
| --- | --- | --- | --- |
|
|
64
|
+
| `api_key` | `fopost: api_key:` | `FOPOST_API_KEY` | none, required |
|
|
65
|
+
| `base_url` | `base_url` | `FOPOST_BASE_URL` | `https://api.fopost.com/v1` |
|
|
66
|
+
| `timeout` | `timeout` | `FOPOST_TIMEOUT` | `30.0` |
|
|
67
|
+
| `max_retries` | `max_retries` | `FOPOST_MAX_RETRIES` | `3` |
|
|
68
|
+
| `default_workspace_id` | `default_workspace_id` | `FOPOST_WORKSPACE_ID` | none |
|
|
69
|
+
| `webhook_secret` | `webhook_secret` | `FOPOST_WEBHOOK_SECRET` | none |
|
|
70
|
+
| `queue_name` | `queue_name` | `FOPOST_QUEUE` | `default` |
|
|
71
|
+
|
|
72
|
+
Set them in the initializer:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
Fopost::Rails.configure do |config|
|
|
76
|
+
config.api_key = Rails.application.credentials.dig(:fopost, :api_key)
|
|
77
|
+
config.queue_name = 'social'
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
or from `config/application.rb`:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
config.fopost.default_workspace_id = 'ws_...'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Publishing from a controller
|
|
88
|
+
|
|
89
|
+
`Fopost::Rails.client` is a configured `Fopost::Client`, memoized and safe to call from any
|
|
90
|
+
thread. The [`fopost` gem README](https://github.com/fopost/fopost-ruby) documents the full
|
|
91
|
+
resource surface — `posts`, `accounts`, `workspaces`, `labels`, `ai`.
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
class PostsController < ApplicationController
|
|
95
|
+
def index
|
|
96
|
+
@posts = Fopost::Rails.client.posts.list(status: 'scheduled')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def create
|
|
100
|
+
post = Fopost::Rails.client.posts.create(
|
|
101
|
+
workspace_id: Fopost::Rails.config.default_workspace_id,
|
|
102
|
+
content: params.require(:text),
|
|
103
|
+
accounts: params.require(:account_ids)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
Fopost::Rails::PublishJob.perform_later(post.id)
|
|
107
|
+
redirect_to posts_path, notice: 'Queued for publishing.'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Errors are the SDK's, so one `rescue_from` covers the lot:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
rescue_from Fopost::PaymentRequiredError do |error|
|
|
116
|
+
redirect_to error.upgrade_url, alert: error.message
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
rescue_from Fopost::Error do |error|
|
|
120
|
+
Rails.logger.error("FoPost: #{error}") # "[404 (not_found)] Post not found"
|
|
121
|
+
head :bad_gateway
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Background jobs
|
|
126
|
+
|
|
127
|
+
Publishing reaches a third-party network, so it belongs off the request cycle.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# Publish something that already exists.
|
|
131
|
+
Fopost::Rails::PublishJob.perform_later(post.id)
|
|
132
|
+
|
|
133
|
+
# Compose and, optionally, send in one job.
|
|
134
|
+
Fopost::Rails::CreatePostJob.perform_later(
|
|
135
|
+
content: 'Shipping today.',
|
|
136
|
+
accounts: account_ids,
|
|
137
|
+
publish: true
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Or schedule it, and pass anything else the SDK takes through `options`.
|
|
141
|
+
Fopost::Rails::CreatePostJob.perform_later(
|
|
142
|
+
workspace_id: 'ws_...',
|
|
143
|
+
content: ['First post in the thread', 'And the reply'],
|
|
144
|
+
accounts: account_ids,
|
|
145
|
+
status: 'scheduled',
|
|
146
|
+
schedule_at: 1.hour.from_now,
|
|
147
|
+
options: { labels: ['launch'], title: 'Launch week' }
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`workspace_id` falls back to `config.default_workspace_id`. Both jobs run on `config.queue_name`.
|
|
152
|
+
|
|
153
|
+
When the API answers `429`, the job is re-enqueued for exactly the interval the API asked for in
|
|
154
|
+
`Retry-After` (capped at a minute), up to five attempts. Every other `Fopost::Error` is left to
|
|
155
|
+
your queue's own error handling.
|
|
156
|
+
|
|
157
|
+
Publishing returns once delivery is **queued**, not once it is live. Subscribe to
|
|
158
|
+
`fopost.post.published` for that.
|
|
159
|
+
|
|
160
|
+
## Receiving webhooks
|
|
161
|
+
|
|
162
|
+
Mount the engine:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# config/routes.rb
|
|
166
|
+
mount Fopost::Rails::Engine => '/fopost'
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
That serves `POST /fopost/webhooks`. Create a webhook pointing at it, copy the secret it shows you
|
|
170
|
+
once into `config.webhook_secret`, and subscribe:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# config/initializers/fopost_webhooks.rb
|
|
174
|
+
ActiveSupport::Notifications.subscribe('fopost.post.published') do |*, payload|
|
|
175
|
+
payload[:event] # "post.published"
|
|
176
|
+
payload[:data] # the event body FoPost sent
|
|
177
|
+
payload[:timestamp] # ISO 8601, when FoPost sent it
|
|
178
|
+
payload[:delivery_id] # X-FoPost-Delivery, unique per attempt
|
|
179
|
+
payload[:payload] # the whole parsed body
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Two notifications fire per verified delivery: `fopost.<event>` and `fopost.webhook` for a
|
|
184
|
+
catch-all. The events FoPost sends are `post.published`, `post.failed`, `post.partially_failed`,
|
|
185
|
+
`delivery.published`, `delivery.failed`, `delivery.delayed`, and `account.health_changed`.
|
|
186
|
+
|
|
187
|
+
Verification is not optional and not yours to write. FoPost signs the exact bytes of the request
|
|
188
|
+
body with HMAC-SHA256, keyed by the webhook secret, and sends the hex digest as
|
|
189
|
+
`X-FoPost-Signature: sha256=<digest>`. The controller recomputes it over the raw body and compares
|
|
190
|
+
in constant time; a mismatch is a `401` and publishes nothing, and an unconfigured secret is a
|
|
191
|
+
`503` rather than a pretended success.
|
|
192
|
+
|
|
193
|
+
To sign a request yourself — in a request spec, say:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
body = { event: 'post.published', data: { postId: 'post_1' } }.to_json
|
|
197
|
+
|
|
198
|
+
post '/fopost/webhooks',
|
|
199
|
+
params: body,
|
|
200
|
+
headers: {
|
|
201
|
+
'CONTENT_TYPE' => 'application/json',
|
|
202
|
+
'X-FoPost-Signature' => Fopost::Rails::WebhookSignature.sign(body, secret)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Testing your app
|
|
207
|
+
|
|
208
|
+
Swap the client for one wired to your own transport and nothing touches the network:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
Fopost::Rails.client = Fopost::Client.new(api_key: 'fp_test', transport: my_stub)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`Fopost::Rails.reset!` puts config and client back to their defaults between tests.
|
|
215
|
+
|
|
216
|
+
## Looking for the free self-hosted toolkit?
|
|
217
|
+
|
|
218
|
+
This gem talks to the FoPost Cloud API with a FoPost API key. To publish straight to the social
|
|
219
|
+
platforms using your own app credentials, with no FoPost account involved, use
|
|
220
|
+
[`fopost-social-core`](https://github.com/fopost/fopost-social-core) instead. The two families are
|
|
221
|
+
separate on purpose and never depend on each other.
|
|
222
|
+
|
|
223
|
+
## Links
|
|
224
|
+
|
|
225
|
+
- Documentation: [fopost.com/docs](https://fopost.com/docs)
|
|
226
|
+
- API keys: [app.fopost.com/api-keys](https://app.fopost.com/api-keys)
|
|
227
|
+
- The SDK this wraps: [`fopost`](https://github.com/fopost/fopost-ruby)
|
|
228
|
+
- Issues: [github.com/fopost/fopost-rails/issues](https://github.com/fopost/fopost-rails/issues)
|
|
229
|
+
- Support: [fopost.com/contact](https://fopost.com/contact)
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT. Copyright (c) 2026 Porter Bridge, LLC. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'active_support/notifications'
|
|
5
|
+
require 'fopost/rails/webhook_signature'
|
|
6
|
+
|
|
7
|
+
module Fopost
|
|
8
|
+
module Rails
|
|
9
|
+
# Receives FoPost webhooks and republishes them as
|
|
10
|
+
# ActiveSupport::Notifications events, so subscribing costs no route,
|
|
11
|
+
# controller, or queue of your own.
|
|
12
|
+
#
|
|
13
|
+
# ActiveSupport::Notifications.subscribe('fopost.post.published') do |*, payload|
|
|
14
|
+
# Rails.logger.info(payload[:data])
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Two events fire per delivery: `fopost.<event>` (`fopost.post.published`,
|
|
18
|
+
# `fopost.delivery.failed`, …) and `fopost.webhook` for a catch-all.
|
|
19
|
+
class WebhooksController < ActionController::API
|
|
20
|
+
def create
|
|
21
|
+
secret = Fopost::Rails.config.webhook_secret
|
|
22
|
+
return head :service_unavailable if secret.nil? || secret.to_s.empty?
|
|
23
|
+
|
|
24
|
+
payload = request.raw_post
|
|
25
|
+
signature = request.headers[WebhookSignature::SIGNATURE_HEADER]
|
|
26
|
+
return head :unauthorized unless WebhookSignature.valid?(payload, signature, secret)
|
|
27
|
+
|
|
28
|
+
body = parse(payload)
|
|
29
|
+
event = body && body['event']
|
|
30
|
+
return head :bad_request unless event.is_a?(String) && !event.empty?
|
|
31
|
+
|
|
32
|
+
publish(event, body)
|
|
33
|
+
head :ok
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def publish(event, body)
|
|
39
|
+
notification = {
|
|
40
|
+
event: event,
|
|
41
|
+
data: body['data'],
|
|
42
|
+
timestamp: body['timestamp'],
|
|
43
|
+
delivery_id: request.headers[WebhookSignature::DELIVERY_HEADER],
|
|
44
|
+
payload: body
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ActiveSupport::Notifications.instrument("fopost.#{event}", notification)
|
|
48
|
+
ActiveSupport::Notifications.instrument('fopost.webhook', notification)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse(payload)
|
|
52
|
+
parsed = JSON.parse(payload.to_s)
|
|
53
|
+
parsed.is_a?(Hash) ? parsed : nil
|
|
54
|
+
rescue JSON::ParserError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Fopost::Rails::Engine.routes.draw do
|
|
4
|
+
post '/webhooks', to: 'webhooks#create', as: :webhooks
|
|
5
|
+
|
|
6
|
+
# So mounting straight onto the endpoint path also works:
|
|
7
|
+
# mount Fopost::Rails::Engine => '/fopost/webhooks'
|
|
8
|
+
post '/', to: 'webhooks#create', as: :root
|
|
9
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_job'
|
|
4
|
+
require 'fopost/rails'
|
|
5
|
+
|
|
6
|
+
module Fopost
|
|
7
|
+
module Rails
|
|
8
|
+
# Base for the jobs this gem ships.
|
|
9
|
+
#
|
|
10
|
+
# A rate-limited call is re-enqueued for exactly as long as the API asked
|
|
11
|
+
# for in `Retry-After`, instead of a guessed backoff.
|
|
12
|
+
class ApplicationJob < ActiveJob::Base
|
|
13
|
+
# The API never asks for longer than a minute; ignore it if it does.
|
|
14
|
+
MAX_RETRY_WAIT = 60
|
|
15
|
+
|
|
16
|
+
# ActiveJob hands the `wait:` proc the attempt count and nothing else, so
|
|
17
|
+
# the seconds the API asked for ride along on the thread that raised.
|
|
18
|
+
# `rescue_from` runs on that same thread, right after `perform`.
|
|
19
|
+
RETRY_AFTER_KEY = :fopost_rails_retry_after
|
|
20
|
+
|
|
21
|
+
queue_as { Fopost::Rails.config.queue_name }
|
|
22
|
+
|
|
23
|
+
retry_on Fopost::RateLimitError,
|
|
24
|
+
attempts: 5,
|
|
25
|
+
wait: lambda { |executions|
|
|
26
|
+
asked = Thread.current[RETRY_AFTER_KEY]
|
|
27
|
+
Thread.current[RETRY_AFTER_KEY] = nil
|
|
28
|
+
seconds = asked.to_f
|
|
29
|
+
seconds.positive? ? [seconds, MAX_RETRY_WAIT].min.ceil : 2**executions
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def client
|
|
35
|
+
Fopost::Rails.client
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Wrap every API call, so a 429 carries its interval into the retry.
|
|
39
|
+
def with_retry_after
|
|
40
|
+
yield
|
|
41
|
+
rescue Fopost::RateLimitError => e
|
|
42
|
+
Thread.current[RETRY_AFTER_KEY] = e.retry_after
|
|
43
|
+
raise
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fopost'
|
|
4
|
+
|
|
5
|
+
module Fopost
|
|
6
|
+
module Rails
|
|
7
|
+
# Settings for the API client, the jobs, and the webhook endpoint.
|
|
8
|
+
#
|
|
9
|
+
# Every setting resolves the same way: what you set explicitly wins, then
|
|
10
|
+
# Rails credentials under `fopost:`, then the environment, then a default.
|
|
11
|
+
#
|
|
12
|
+
# Fopost::Rails.configure do |config|
|
|
13
|
+
# config.api_key = 'fp_...'
|
|
14
|
+
# config.default_workspace_id = 'ws_...'
|
|
15
|
+
# end
|
|
16
|
+
class Configuration
|
|
17
|
+
SETTINGS = %i[
|
|
18
|
+
api_key base_url timeout max_retries default_workspace_id webhook_secret queue_name
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
ENV_KEYS = {
|
|
22
|
+
api_key: 'FOPOST_API_KEY',
|
|
23
|
+
base_url: 'FOPOST_BASE_URL',
|
|
24
|
+
timeout: 'FOPOST_TIMEOUT',
|
|
25
|
+
max_retries: 'FOPOST_MAX_RETRIES',
|
|
26
|
+
default_workspace_id: 'FOPOST_WORKSPACE_ID',
|
|
27
|
+
webhook_secret: 'FOPOST_WEBHOOK_SECRET',
|
|
28
|
+
queue_name: 'FOPOST_QUEUE'
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
DEFAULTS = {
|
|
32
|
+
base_url: Fopost::Client::DEFAULT_BASE_URL,
|
|
33
|
+
timeout: 30.0,
|
|
34
|
+
max_retries: 3,
|
|
35
|
+
queue_name: 'default'
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
attr_writer(*SETTINGS)
|
|
39
|
+
|
|
40
|
+
def api_key
|
|
41
|
+
resolve(:api_key)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def base_url
|
|
45
|
+
resolve(:base_url)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def timeout
|
|
49
|
+
value = resolve(:timeout)
|
|
50
|
+
value&.to_f
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def max_retries
|
|
54
|
+
value = resolve(:max_retries)
|
|
55
|
+
value&.to_i
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def default_workspace_id
|
|
59
|
+
resolve(:default_workspace_id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Shown once, when the webhook is created. The mounted endpoint needs it
|
|
63
|
+
# to verify the signature FoPost sends.
|
|
64
|
+
def webhook_secret
|
|
65
|
+
resolve(:webhook_secret)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def queue_name
|
|
69
|
+
resolve(:queue_name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The `fopost:` section of Rails credentials, or an empty hash outside a
|
|
73
|
+
# booted app. A missing master key is treated as "no credentials" rather
|
|
74
|
+
# than an error, so a machine without the key still boots.
|
|
75
|
+
def credentials
|
|
76
|
+
app = defined?(::Rails) && ::Rails.respond_to?(:application) ? ::Rails.application : nil
|
|
77
|
+
section = app&.credentials&.fopost
|
|
78
|
+
section.respond_to?(:[]) ? section : {}
|
|
79
|
+
rescue StandardError
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Drop every explicit value, so the next read falls back again.
|
|
84
|
+
def reset!
|
|
85
|
+
SETTINGS.each { |key| instance_variable_set(:"@#{key}", nil) }
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_h
|
|
90
|
+
SETTINGS.to_h { |key| [key, public_send(key)] }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Keeps the key out of logs and consoles.
|
|
94
|
+
def inspect
|
|
95
|
+
redacted = to_h.merge(api_key: mask(api_key), webhook_secret: mask(webhook_secret))
|
|
96
|
+
"#<Fopost::Rails::Configuration #{redacted.map { |k, v| "#{k}=#{v.inspect}" }.join(' ')}>"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def resolve(key)
|
|
102
|
+
explicit = instance_variable_get(:"@#{key}")
|
|
103
|
+
return explicit unless explicit.nil?
|
|
104
|
+
|
|
105
|
+
from_credentials = credentials[key]
|
|
106
|
+
return from_credentials unless from_credentials.nil?
|
|
107
|
+
|
|
108
|
+
from_env = ENV[ENV_KEYS.fetch(key)]
|
|
109
|
+
return from_env unless from_env.nil? || from_env.empty?
|
|
110
|
+
|
|
111
|
+
DEFAULTS[key]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def mask(value)
|
|
115
|
+
value.nil? || value.empty? ? value : "#{value[0, 4]}…"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fopost/rails/application_job'
|
|
4
|
+
|
|
5
|
+
module Fopost
|
|
6
|
+
module Rails
|
|
7
|
+
# Create a post — and optionally send it — off the request cycle.
|
|
8
|
+
#
|
|
9
|
+
# Fopost::Rails::CreatePostJob.perform_later(
|
|
10
|
+
# content: 'Shipping today.',
|
|
11
|
+
# accounts: account_ids,
|
|
12
|
+
# publish: true
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# `workspace_id` falls back to `config.default_workspace_id`. `options` is
|
|
16
|
+
# merged into the create call, so anything the SDK takes (`labels`,
|
|
17
|
+
# `title`, `settings`, …) passes straight through.
|
|
18
|
+
class CreatePostJob < ApplicationJob
|
|
19
|
+
def perform(content:, accounts:, workspace_id: nil, status: 'draft', schedule_at: nil,
|
|
20
|
+
publish: false, options: {})
|
|
21
|
+
workspace = workspace_id || Fopost::Rails.config.default_workspace_id
|
|
22
|
+
if workspace.nil? || workspace.to_s.empty?
|
|
23
|
+
raise ArgumentError,
|
|
24
|
+
'fopost: pass workspace_id: or set config.default_workspace_id'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
post = with_retry_after do
|
|
28
|
+
client.posts.create(
|
|
29
|
+
workspace_id: workspace,
|
|
30
|
+
content: content,
|
|
31
|
+
accounts: accounts,
|
|
32
|
+
status: status,
|
|
33
|
+
schedule_at: schedule_at,
|
|
34
|
+
**(options || {}).transform_keys(&:to_sym)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
with_retry_after { client.posts.publish(post.id) } if publish
|
|
39
|
+
|
|
40
|
+
post.id
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/engine'
|
|
4
|
+
|
|
5
|
+
module Fopost
|
|
6
|
+
module Rails
|
|
7
|
+
# Mount to receive FoPost webhooks:
|
|
8
|
+
#
|
|
9
|
+
# # config/routes.rb
|
|
10
|
+
# mount Fopost::Rails::Engine => '/fopost'
|
|
11
|
+
#
|
|
12
|
+
# That serves `POST /fopost/webhooks`. Point a webhook at it, set
|
|
13
|
+
# `config.webhook_secret` to the secret FoPost showed you once, and
|
|
14
|
+
# subscribe to the events it publishes.
|
|
15
|
+
class Engine < ::Rails::Engine
|
|
16
|
+
isolate_namespace Fopost::Rails
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fopost/rails/application_job'
|
|
4
|
+
|
|
5
|
+
module Fopost
|
|
6
|
+
module Rails
|
|
7
|
+
# Publish an existing post without blocking the request that asked for it.
|
|
8
|
+
#
|
|
9
|
+
# Fopost::Rails::PublishJob.perform_later(post_id)
|
|
10
|
+
#
|
|
11
|
+
# Returns once the API has queued delivery, which is not the same as live:
|
|
12
|
+
# subscribe to `fopost.post.published` for that.
|
|
13
|
+
class PublishJob < ApplicationJob
|
|
14
|
+
def perform(post_id)
|
|
15
|
+
with_retry_after { client.posts.publish(post_id) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/railtie'
|
|
4
|
+
|
|
5
|
+
module Fopost
|
|
6
|
+
module Rails
|
|
7
|
+
# Registers `config.fopost`, so an app can set the SDK up from
|
|
8
|
+
# `config/application.rb`:
|
|
9
|
+
#
|
|
10
|
+
# config.fopost.api_key = Rails.application.credentials.dig(:fopost, :api_key)
|
|
11
|
+
#
|
|
12
|
+
# Nothing is loaded eagerly: the client is built on first use and the jobs
|
|
13
|
+
# are autoloaded, so booting this gem costs a require of the SDK.
|
|
14
|
+
class Railtie < ::Rails::Railtie
|
|
15
|
+
config.fopost = Fopost::Rails.config
|
|
16
|
+
|
|
17
|
+
# An initializer may have written settings after something already built a
|
|
18
|
+
# client, so drop it once boot is done.
|
|
19
|
+
config.after_initialize { Fopost::Rails.reset_client! }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'openssl'
|
|
4
|
+
require 'active_support/security_utils'
|
|
5
|
+
|
|
6
|
+
module Fopost
|
|
7
|
+
module Rails
|
|
8
|
+
# FoPost signs every webhook body with HMAC-SHA256 over the exact bytes it
|
|
9
|
+
# sent, keyed by the webhook secret, and puts the hex digest in
|
|
10
|
+
# `X-FoPost-Signature` behind a `sha256=` prefix.
|
|
11
|
+
#
|
|
12
|
+
# Verify against the raw request body — a parsed-and-re-serialized hash will
|
|
13
|
+
# not match.
|
|
14
|
+
module WebhookSignature
|
|
15
|
+
SIGNATURE_HEADER = 'X-FoPost-Signature'
|
|
16
|
+
EVENT_HEADER = 'X-FoPost-Event'
|
|
17
|
+
DELIVERY_HEADER = 'X-FoPost-Delivery'
|
|
18
|
+
PREFIX = 'sha256='
|
|
19
|
+
|
|
20
|
+
# The header value FoPost would send for this body and secret.
|
|
21
|
+
def self.sign(payload, secret)
|
|
22
|
+
"#{PREFIX}#{OpenSSL::HMAC.hexdigest('SHA256', secret.to_s, payload.to_s)}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Constant-time comparison, so a wrong signature leaks no timing.
|
|
26
|
+
def self.valid?(payload, signature, secret)
|
|
27
|
+
return false if signature.nil? || secret.nil? || secret.to_s.empty?
|
|
28
|
+
|
|
29
|
+
ActiveSupport::SecurityUtils.secure_compare(sign(payload, secret), signature.to_s)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/fopost/rails.rb
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fopost'
|
|
4
|
+
|
|
5
|
+
require 'fopost/rails/version'
|
|
6
|
+
require 'fopost/rails/configuration'
|
|
7
|
+
|
|
8
|
+
# Official Rails integration for the FoPost API.
|
|
9
|
+
#
|
|
10
|
+
# This gem is a thin wrapper: every request, retry, model, and error class
|
|
11
|
+
# lives in the `fopost` gem. What is added here is Rails wiring — configuration
|
|
12
|
+
# and credentials, a memoized client, an install generator, ActiveJob jobs, and
|
|
13
|
+
# a mountable endpoint for receiving webhooks.
|
|
14
|
+
#
|
|
15
|
+
# Fopost::Rails.client.posts.list(status: 'scheduled')
|
|
16
|
+
# Fopost::Rails::PublishJob.perform_later(post.id)
|
|
17
|
+
module Fopost
|
|
18
|
+
module Rails
|
|
19
|
+
# Loaded on first use, so an app that never enqueues a job never pulls
|
|
20
|
+
# ActiveJob in on our account, and requiring this gem stays cheap.
|
|
21
|
+
autoload :ApplicationJob, 'fopost/rails/application_job'
|
|
22
|
+
autoload :CreatePostJob, 'fopost/rails/create_post_job'
|
|
23
|
+
autoload :PublishJob, 'fopost/rails/publish_job'
|
|
24
|
+
autoload :WebhookSignature, 'fopost/rails/webhook_signature'
|
|
25
|
+
|
|
26
|
+
@mutex = Mutex.new
|
|
27
|
+
@client = nil
|
|
28
|
+
@config = nil
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# The settings object. Also reachable as `config.fopost` inside
|
|
32
|
+
# `config/application.rb` and any Rails initializer.
|
|
33
|
+
def config
|
|
34
|
+
@config ||= Configuration.new
|
|
35
|
+
end
|
|
36
|
+
alias configuration config
|
|
37
|
+
|
|
38
|
+
# Fopost::Rails.configure do |c|
|
|
39
|
+
# c.api_key = ENV['FOPOST_API_KEY']
|
|
40
|
+
# end
|
|
41
|
+
def configure
|
|
42
|
+
yield config
|
|
43
|
+
reset_client!
|
|
44
|
+
config
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A memoized, configured Fopost::Client. Safe to call from any thread.
|
|
48
|
+
def client
|
|
49
|
+
@client || @mutex.synchronize { @client ||= build_client }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Swap in your own client — a stubbed transport in tests, say.
|
|
53
|
+
def client=(client)
|
|
54
|
+
@mutex.synchronize { @client = client }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Forget the memoized client, so the next call rebuilds it from config.
|
|
58
|
+
def reset_client!
|
|
59
|
+
@mutex.synchronize { @client = nil }
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Config and client back to their defaults. Meant for test suites.
|
|
64
|
+
def reset!
|
|
65
|
+
reset_client!
|
|
66
|
+
config.reset!
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def build_client
|
|
73
|
+
Fopost::Client.new(
|
|
74
|
+
api_key: config.api_key,
|
|
75
|
+
base_url: config.base_url,
|
|
76
|
+
timeout: config.timeout,
|
|
77
|
+
max_retries: config.max_retries
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
require 'fopost/rails/railtie' if defined?(::Rails::Railtie)
|
|
85
|
+
require 'fopost/rails/engine' if defined?(::Rails::Engine)
|
data/lib/fopost-rails.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/base'
|
|
4
|
+
|
|
5
|
+
module Fopost
|
|
6
|
+
module Generators
|
|
7
|
+
# `rails generate fopost:install`
|
|
8
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
|
10
|
+
|
|
11
|
+
desc 'Writes config/initializers/fopost.rb.'
|
|
12
|
+
|
|
13
|
+
def copy_initializer
|
|
14
|
+
template 'fopost.rb.tt', 'config/initializers/fopost.rb'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def print_next_steps
|
|
18
|
+
say <<~TEXT
|
|
19
|
+
|
|
20
|
+
Next: put your API key somewhere the app can read it.
|
|
21
|
+
|
|
22
|
+
bin/rails credentials:edit # fopost: { api_key: fp_... }
|
|
23
|
+
# or export FOPOST_API_KEY=fp_...
|
|
24
|
+
|
|
25
|
+
Create a key at https://app.fopost.com/api-keys.
|
|
26
|
+
TEXT
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# FoPost — https://fopost.com/docs
|
|
4
|
+
#
|
|
5
|
+
# Every setting below falls back on its own, in this order: what you set here,
|
|
6
|
+
# then Rails credentials under `fopost:`, then the environment. Leave a line
|
|
7
|
+
# commented to take the fallback.
|
|
8
|
+
Fopost::Rails.configure do |config|
|
|
9
|
+
# Create a key at https://app.fopost.com/api-keys.
|
|
10
|
+
# Falls back to credentials `fopost: api_key:`, then ENV['FOPOST_API_KEY'].
|
|
11
|
+
# config.api_key = nil
|
|
12
|
+
|
|
13
|
+
# config.base_url = 'https://api.fopost.com/v1'
|
|
14
|
+
# config.timeout = 30.0
|
|
15
|
+
# config.max_retries = 3
|
|
16
|
+
|
|
17
|
+
# Used by CreatePostJob when no workspace is passed.
|
|
18
|
+
# config.default_workspace_id = nil
|
|
19
|
+
|
|
20
|
+
# Shown once, when you create a webhook. Required by the mounted endpoint.
|
|
21
|
+
# config.webhook_secret = nil
|
|
22
|
+
|
|
23
|
+
# ActiveJob queue for PublishJob and CreatePostJob.
|
|
24
|
+
# config.queue_name = 'default'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# To receive webhooks, mount the engine in config/routes.rb:
|
|
28
|
+
#
|
|
29
|
+
# mount Fopost::Rails::Engine => '/fopost'
|
|
30
|
+
#
|
|
31
|
+
# then point a webhook at https://your-app.example/fopost/webhooks and subscribe:
|
|
32
|
+
#
|
|
33
|
+
# ActiveSupport::Notifications.subscribe('fopost.post.published') do |*, payload|
|
|
34
|
+
# Rails.logger.info("FoPost published: #{payload[:data]}")
|
|
35
|
+
# end
|
metadata
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fopost-rails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- FoPost
|
|
8
|
+
- Porter Bridge, LLC
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.0'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '9'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.0'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '9'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: fopost
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
40
|
+
type: :runtime
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.1'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: railties
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '7.0'
|
|
54
|
+
- - "<"
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '9'
|
|
57
|
+
type: :runtime
|
|
58
|
+
prerelease: false
|
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '7.0'
|
|
64
|
+
- - "<"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '9'
|
|
67
|
+
description: 'Rails integration for the FoPost API: configuration and credentials,
|
|
68
|
+
a memoized client, ActiveJob jobs, and a mountable webhook endpoint. A thin wrapper
|
|
69
|
+
around the fopost gem.'
|
|
70
|
+
executables: []
|
|
71
|
+
extensions: []
|
|
72
|
+
extra_rdoc_files: []
|
|
73
|
+
files:
|
|
74
|
+
- CHANGELOG.md
|
|
75
|
+
- LICENSE
|
|
76
|
+
- README.md
|
|
77
|
+
- app/controllers/fopost/rails/webhooks_controller.rb
|
|
78
|
+
- config/routes.rb
|
|
79
|
+
- lib/fopost-rails.rb
|
|
80
|
+
- lib/fopost/rails.rb
|
|
81
|
+
- lib/fopost/rails/application_job.rb
|
|
82
|
+
- lib/fopost/rails/configuration.rb
|
|
83
|
+
- lib/fopost/rails/create_post_job.rb
|
|
84
|
+
- lib/fopost/rails/engine.rb
|
|
85
|
+
- lib/fopost/rails/publish_job.rb
|
|
86
|
+
- lib/fopost/rails/railtie.rb
|
|
87
|
+
- lib/fopost/rails/version.rb
|
|
88
|
+
- lib/fopost/rails/webhook_signature.rb
|
|
89
|
+
- lib/generators/fopost/install/install_generator.rb
|
|
90
|
+
- lib/generators/fopost/install/templates/fopost.rb.tt
|
|
91
|
+
homepage: https://fopost.com
|
|
92
|
+
licenses:
|
|
93
|
+
- MIT
|
|
94
|
+
metadata:
|
|
95
|
+
homepage_uri: https://fopost.com
|
|
96
|
+
source_code_uri: https://github.com/fopost/fopost-rails
|
|
97
|
+
bug_tracker_uri: https://github.com/fopost/fopost-rails/issues
|
|
98
|
+
documentation_uri: https://fopost.com/docs
|
|
99
|
+
changelog_uri: https://github.com/fopost/fopost-rails/blob/main/CHANGELOG.md
|
|
100
|
+
rubygems_mfa_required: 'true'
|
|
101
|
+
rdoc_options: []
|
|
102
|
+
require_paths:
|
|
103
|
+
- lib
|
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '3.1'
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
requirements: []
|
|
115
|
+
rubygems_version: 3.6.9
|
|
116
|
+
specification_version: 4
|
|
117
|
+
summary: Official Rails integration for the FoPost API.
|
|
118
|
+
test_files: []
|