webhookdb 1.0.2 → 1.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 +4 -4
- data/data/messages/web/install-customer-login.liquid +1 -1
- data/data/messages/web/install-success.liquid +2 -2
- data/db/migrations/038_webhookdb_api_key.rb +13 -0
- data/lib/webhookdb/api/install.rb +23 -1
- data/lib/webhookdb/api/service_integrations.rb +17 -12
- data/lib/webhookdb/api/system.rb +13 -2
- data/lib/webhookdb/fixtures/service_integrations.rb +4 -0
- data/lib/webhookdb/front.rb +23 -11
- data/lib/webhookdb/idempotency.rb +94 -33
- data/lib/webhookdb/jobs/backfill.rb +24 -5
- data/lib/webhookdb/jobs/scheduled_backfills.rb +5 -0
- data/lib/webhookdb/oauth/{front.rb → front_provider.rb} +21 -4
- data/lib/webhookdb/oauth/{intercom.rb → intercom_provider.rb} +1 -1
- data/lib/webhookdb/oauth.rb +8 -7
- data/lib/webhookdb/organization/alerting.rb +11 -0
- data/lib/webhookdb/postgres/model_utilities.rb +19 -0
- data/lib/webhookdb/replicator/column.rb +9 -1
- data/lib/webhookdb/replicator/front_conversation_v1.rb +5 -1
- data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +2 -5
- data/lib/webhookdb/replicator/front_message_v1.rb +5 -1
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +325 -0
- data/lib/webhookdb/replicator/front_v1_mixin.rb +9 -1
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +25 -13
- data/lib/webhookdb/service_integration.rb +36 -3
- data/lib/webhookdb/signalwire.rb +40 -0
- data/lib/webhookdb/spec_helpers/citest.rb +18 -9
- data/lib/webhookdb/spec_helpers/postgres.rb +9 -0
- data/lib/webhookdb/spec_helpers/service.rb +5 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +25 -13
- data/lib/webhookdb/spec_helpers/whdb.rb +7 -0
- data/lib/webhookdb/sync_target.rb +1 -1
- data/lib/webhookdb/tasks/specs.rb +4 -2
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb.rb +14 -0
- metadata +34 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d22b47783f1aaabc29dcc0fa023c0fee5bc9088d303103d1cf0cf859f807df0f
|
4
|
+
data.tar.gz: 41f7fcafef5d6724060e99fd40c5cfd19e9cd8d58c5736ccdd1bbaef9ecca826
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d33bedf7341af3216f623a9cc4328615d7a383251fc6d6b7b21000ce0366c99ea53d31f15f92d4a0bc754084faa063b1feb81e0174a966b27c7fda538f65a9a
|
7
|
+
data.tar.gz: 57d0fe4657c4c54a2b36fc3f71f7ca14017473b06be658f87f813670d79d424d51692717e6c1e9558ce0c1314f068b1b78c967bbf49c5d3d8c43bb08c3af874f
|
@@ -11,7 +11,7 @@
|
|
11
11
|
<form method="POST" action="{{ action_url }}">
|
12
12
|
{% if view == "email" %}
|
13
13
|
<p>
|
14
|
-
Welcome to
|
14
|
+
Welcome to WebhookDB! In order to complete this integration, we need your email address so that
|
15
15
|
we can associate your information with an organization.
|
16
16
|
</p>
|
17
17
|
<p>Enter your email:</p>
|
@@ -19,9 +19,9 @@
|
|
19
19
|
</p>
|
20
20
|
{% endif %}
|
21
21
|
<p>
|
22
|
-
You can
|
22
|
+
You can connect to your replicated database with the following url:
|
23
23
|
</p>
|
24
|
-
<p>{{ database_url }}</p>
|
24
|
+
<p><code>{{ database_url }}</code></p>
|
25
25
|
<p>
|
26
26
|
To get more out of WebhookDB, like to replicate other APIs or set up Change Data Capture
|
27
27
|
to HTTP endpoints or databases, you can use the WebhookDB CLI.
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Sequel.migration do
|
4
|
+
change do
|
5
|
+
alter_table(:service_integrations) do
|
6
|
+
add_column :webhookdb_api_key, :text, null: true
|
7
|
+
end
|
8
|
+
|
9
|
+
alter_table(:idempotencies) do
|
10
|
+
add_column :stored_result, :jsonb, null: true
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -3,6 +3,7 @@
|
|
3
3
|
require "webhookdb/api"
|
4
4
|
require "webhookdb/oauth"
|
5
5
|
require "webhookdb/service/view_api"
|
6
|
+
require "webhookdb/front"
|
6
7
|
|
7
8
|
class Webhookdb::API::Install < Webhookdb::API::V1
|
8
9
|
include Webhookdb::Service::ViewApi
|
@@ -37,6 +38,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
|
|
37
38
|
|
38
39
|
def find_and_verify_user(email:, otp_token:)
|
39
40
|
(me = Webhookdb::Customer.with_email(email)) or forbidden!
|
41
|
+
return me if me.should_skip_authentication?
|
40
42
|
begin
|
41
43
|
Webhookdb::Customer::ResetCode.use_code_with_token(otp_token) do |code|
|
42
44
|
raise Webhookdb::Customer::ResetCode::Unusable unless code.customer === me
|
@@ -189,7 +191,7 @@ class Webhookdb::API::Install < Webhookdb::API::V1
|
|
189
191
|
post :webhook do
|
190
192
|
is_initial_request = request.headers["X-Front-Challenge"].present?
|
191
193
|
if is_initial_request
|
192
|
-
whresp = Webhookdb::Front.initial_verification_request_response(request)
|
194
|
+
whresp = Webhookdb::Front.initial_verification_request_response(request, Webhookdb::Front.app_secret)
|
193
195
|
s_status, s_headers, s_body = whresp.to_rack
|
194
196
|
s_headers.each { |k, v| header k, v }
|
195
197
|
if s_headers["Content-Type"] == "application/json"
|
@@ -236,6 +238,26 @@ class Webhookdb::API::Install < Webhookdb::API::V1
|
|
236
238
|
end
|
237
239
|
end
|
238
240
|
|
241
|
+
resource :front_signalwire do
|
242
|
+
params do
|
243
|
+
requires :type, type: String, values: Webhookdb::Front::CHANNEL_EVENT_TYPES
|
244
|
+
optional :payload, type: JSON
|
245
|
+
end
|
246
|
+
route [:post, :delete], :channel do
|
247
|
+
handle_webhook_request("front-signalwire-channel") do
|
248
|
+
auth_header = request.headers["Authorization"]
|
249
|
+
merror!(401, "Missing Authorization header", code: "unauthenticated") if
|
250
|
+
auth_header.nil?
|
251
|
+
merror!(401, "Expected Bearer authorization", code: "unauthenticated") unless
|
252
|
+
auth_header.start_with?("Bearer ")
|
253
|
+
apikey = auth_header[7..]
|
254
|
+
sint = Webhookdb::ServiceIntegration.for_api_key(apikey)
|
255
|
+
merror!(401, "Invalid API key", code: "unauthenticated") if sint.nil?
|
256
|
+
sint
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
239
261
|
resource :intercom do
|
240
262
|
post :webhook do
|
241
263
|
# Because the `_webhook_response` function is always the same here, I'm wondering if it's even
|
@@ -187,19 +187,14 @@ If the list does not look correct, you can contact support at #{Webhookdb.suppor
|
|
187
187
|
|
188
188
|
desc "Returns information about the integration."
|
189
189
|
params do
|
190
|
-
optional :field, type: String, values: Webhookdb::ServiceIntegration::INTEGRATION_INFO_FIELDS
|
190
|
+
optional :field, type: String, values: Webhookdb::ServiceIntegration::INTEGRATION_INFO_FIELDS.keys + [""]
|
191
191
|
end
|
192
192
|
post :info do
|
193
193
|
ensure_plan_supports!
|
194
194
|
org = lookup_org!
|
195
195
|
sint = lookup_service_integration!(org, params[:sint_identifier])
|
196
|
-
data =
|
197
|
-
|
198
|
-
service: sint.service_name,
|
199
|
-
table: sint.table_name,
|
200
|
-
url: sint.replicator.webhook_endpoint,
|
201
|
-
webhook_secret: sint.webhook_secret,
|
202
|
-
}
|
196
|
+
data = Webhookdb::ServiceIntegration::INTEGRATION_INFO_FIELDS.
|
197
|
+
to_h { |k, v| [k.to_sym, sint.send(v)] }
|
203
198
|
|
204
199
|
field_name = params[:field]
|
205
200
|
blocks = Webhookdb::Formatting.blocks
|
@@ -207,7 +202,7 @@ If the list does not look correct, you can contact support at #{Webhookdb.suppor
|
|
207
202
|
blocks.line(data.fetch(field_name.to_sym))
|
208
203
|
else
|
209
204
|
rows = data.map do |k, v|
|
210
|
-
[k.to_s.
|
205
|
+
[k.to_s.humanize, v]
|
211
206
|
end
|
212
207
|
blocks.table(["Field", "Value"], rows)
|
213
208
|
end
|
@@ -216,6 +211,16 @@ If the list does not look correct, you can contact support at #{Webhookdb.suppor
|
|
216
211
|
present r
|
217
212
|
end
|
218
213
|
|
214
|
+
post :roll_api_key do
|
215
|
+
ensure_plan_supports!
|
216
|
+
org = lookup_org!
|
217
|
+
sint = lookup_service_integration!(org, params[:sint_identifier])
|
218
|
+
sint.update(webhookdb_api_key: sint.new_api_key)
|
219
|
+
r = {webhookdb_api_key: sint.webhookdb_api_key}
|
220
|
+
status 200
|
221
|
+
present r
|
222
|
+
end
|
223
|
+
|
219
224
|
resource :backfill do
|
220
225
|
helpers do
|
221
226
|
def lookup_backfillable_replicator(customer:, allow_connstr_auth: false)
|
@@ -370,9 +375,9 @@ The tables and all data for this integration and its dependents will also be rem
|
|
370
375
|
if sint.dependents.empty?
|
371
376
|
confirmation_msg = "Great! We've deleted all secrets for #{sint.service_name}. " \
|
372
377
|
"The table #{sint.table_name} containing its data has been dropped."
|
373
|
-
|
374
|
-
|
375
|
-
|
378
|
+
else
|
379
|
+
confirmation_msg = "Great! We've deleted all secrets for #{sint.service_name} and its dependents. " \
|
380
|
+
"The following tables have been dropped:\n\n#{sint.table_name}\n#{dependents_lines}"
|
376
381
|
end
|
377
382
|
present sint, with: Webhookdb::API::ServiceIntegrationEntity, message: confirmation_msg
|
378
383
|
end
|
data/lib/webhookdb/api/system.rb
CHANGED
@@ -12,6 +12,10 @@ class Webhookdb::API::System < Webhookdb::Service
|
|
12
12
|
require "webhookdb/service/helpers"
|
13
13
|
helpers Webhookdb::Service::Helpers
|
14
14
|
|
15
|
+
get "/" do
|
16
|
+
redirect "/terminal/"
|
17
|
+
end
|
18
|
+
|
15
19
|
get :healthz do
|
16
20
|
# Do not bother looking at dependencies like databases.
|
17
21
|
# If the primary is down, we can still accept webhooks
|
@@ -34,8 +38,15 @@ class Webhookdb::API::System < Webhookdb::Service
|
|
34
38
|
|
35
39
|
if ["development", "test"].include?(Webhookdb::RACK_ENV)
|
36
40
|
resource :debug do
|
37
|
-
|
38
|
-
|
41
|
+
resource :echo do
|
42
|
+
[:get, :post, :patch, :put, :delete].each do |m|
|
43
|
+
self.send(m) do
|
44
|
+
pp params.to_h
|
45
|
+
pp request.headers
|
46
|
+
status 200
|
47
|
+
present({})
|
48
|
+
end
|
49
|
+
end
|
39
50
|
end
|
40
51
|
end
|
41
52
|
end
|
data/lib/webhookdb/front.rb
CHANGED
@@ -6,34 +6,44 @@ require "appydays/loggable"
|
|
6
6
|
module Webhookdb::Front
|
7
7
|
include Appydays::Configurable
|
8
8
|
|
9
|
+
CHANNEL_EVENT_TYPES = Set.new(["authorization", "delete", "message", "message_autoreply", "message_imported"])
|
10
|
+
|
9
11
|
configurable(:front) do
|
10
|
-
|
11
|
-
|
12
|
+
setting :http_timeout, 30
|
13
|
+
|
14
|
+
# WebhookDB App: App Secret from Basic Information tab in Front UI.
|
15
|
+
setting :app_secret, "front_api_secret", key: ["FRONT_APP_SECRET", "FRONT_API_SECRET"]
|
16
|
+
# WebhookDB App: Client ID from OAuth tab in Front UI.
|
12
17
|
setting :client_id, "front_client_id"
|
18
|
+
# WebhookDB App: Client Secret from OAuth tab in Front UI.
|
13
19
|
setting :client_secret, "front_client_secret"
|
14
|
-
setting :http_timeout, 30
|
15
|
-
end
|
16
20
|
|
17
|
-
|
21
|
+
setting :signalwire_channel_app_id, "front_swchan_app_id"
|
22
|
+
setting :signalwire_channel_app_secret, "front_swchan_app_secret"
|
23
|
+
setting :signalwire_channel_client_id, "front_swchan_client_id"
|
24
|
+
setting :signalwire_channel_client_secret, "front_swchan_client_secret"
|
25
|
+
|
26
|
+
setting :channel_sync_refreshness_cutoff, 48.hours.to_i
|
27
|
+
end
|
18
28
|
|
19
|
-
def self.verify_signature(request)
|
29
|
+
def self.verify_signature(request, secret)
|
20
30
|
request.body.rewind
|
21
31
|
body = request.body.read
|
22
32
|
base_string = "#{request.env['HTTP_X_FRONT_REQUEST_TIMESTAMP']}:#{body}"
|
23
|
-
calculated_signature = OpenSSL::HMAC.base64digest(OpenSSL::Digest.new("sha256"),
|
33
|
+
calculated_signature = OpenSSL::HMAC.base64digest(OpenSSL::Digest.new("sha256"), secret, base_string)
|
24
34
|
return calculated_signature == request.env["HTTP_X_FRONT_SIGNATURE"]
|
25
35
|
end
|
26
36
|
|
27
|
-
def self.webhook_response(request)
|
37
|
+
def self.webhook_response(request, secret)
|
28
38
|
return Webhookdb::WebhookResponse.error("missing signature") unless request.env["HTTP_X_FRONT_SIGNATURE"]
|
29
39
|
|
30
|
-
from_front =
|
40
|
+
from_front = self.verify_signature(request, secret)
|
31
41
|
return Webhookdb::WebhookResponse.ok(status: 200) if from_front
|
32
42
|
return Webhookdb::WebhookResponse.error("invalid signature")
|
33
43
|
end
|
34
44
|
|
35
|
-
def self.initial_verification_request_response(request)
|
36
|
-
from_front = self.verify_signature(request)
|
45
|
+
def self.initial_verification_request_response(request, secret)
|
46
|
+
from_front = self.verify_signature(request, secret)
|
37
47
|
if from_front
|
38
48
|
return Webhookdb::WebhookResponse.ok(
|
39
49
|
json: {challenge: request.env["HTTP_X_FRONT_CHALLENGE"]},
|
@@ -46,4 +56,6 @@ module Webhookdb::Front
|
|
46
56
|
def self.auth_headers(token)
|
47
57
|
return {"Authorization" => "Bearer #{token}"}
|
48
58
|
end
|
59
|
+
|
60
|
+
def self.channel_jwt_jti = SecureRandom.hex(4)
|
49
61
|
end
|
@@ -26,51 +26,112 @@ require "webhookdb/postgres/model"
|
|
26
26
|
# usually using the :no_transaction_check spec metadata.
|
27
27
|
#
|
28
28
|
class Webhookdb::Idempotency < Webhookdb::Postgres::Model(:idempotencies)
|
29
|
-
extend Webhookdb::MethodUtilities
|
30
|
-
|
31
29
|
NOOP = :skipped
|
32
30
|
|
33
|
-
|
34
|
-
|
31
|
+
class << self
|
32
|
+
# Skip the transaction check. Useful in unit tests. See class docs for details.
|
33
|
+
attr_accessor :skip_transaction_check
|
35
34
|
|
36
|
-
|
37
|
-
idem = self.new
|
38
|
-
idem.__once_ever = true
|
39
|
-
return idem
|
40
|
-
end
|
35
|
+
def skip_transaction_check? = self.skip_transaction_check
|
41
36
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
37
|
+
# @return [Builder]
|
38
|
+
def once_ever
|
39
|
+
b = self::Builder.new
|
40
|
+
b._once_ever = true
|
41
|
+
return b
|
42
|
+
end
|
47
43
|
|
48
|
-
|
44
|
+
# @return [Builder]
|
45
|
+
def every(interval)
|
46
|
+
b = self::Builder.new
|
47
|
+
b._every = interval
|
48
|
+
return b
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
def separate_connection
|
52
|
+
@connection ||= Sequel.connect(
|
53
|
+
uri,
|
54
|
+
logger: self.logger,
|
55
|
+
extensions: [
|
56
|
+
:connection_validator,
|
57
|
+
:pg_json, # Must have this to mirror the main model DB
|
58
|
+
],
|
59
|
+
**Webhookdb::Dbutil.configured_connection_options,
|
60
|
+
)
|
61
|
+
return @connection
|
62
|
+
end
|
54
63
|
end
|
55
64
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
65
|
+
class Builder
|
66
|
+
attr_accessor :_every, :_once_ever, :_stored, :_sepconn, :_key
|
67
|
+
|
68
|
+
# If set, the result of block is stored as JSON,
|
69
|
+
# and returned when an idempotent call is made.
|
70
|
+
# The JSON value (as_json) is returned from the block in all cases.
|
71
|
+
# @return [Builder]
|
72
|
+
def stored
|
73
|
+
self._stored = true
|
74
|
+
return self
|
75
|
+
end
|
61
76
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
77
|
+
# Run the idempotency on a separate connection.
|
78
|
+
# Allows use of idempotency within an existing transaction block,
|
79
|
+
# which is normally not allowed. Usually should be used with #stored,
|
80
|
+
# since otherwise the result of the idempotency will be lost.
|
81
|
+
#
|
82
|
+
# NOTE: When calling code with using_seperate_connection,
|
83
|
+
# you may want to use the spec metadata `truncate: Webhookdb::Idempotency`
|
84
|
+
# since the row won't be covered by the spec's transaction.
|
85
|
+
#
|
86
|
+
# @return [Builder]
|
87
|
+
def using_seperate_connection
|
88
|
+
self._sepconn = true
|
89
|
+
return self
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Builder]
|
93
|
+
def under_key(key, &block)
|
94
|
+
self._key = key
|
95
|
+
return self.execute(&block) if block
|
96
|
+
return self
|
97
|
+
end
|
98
|
+
|
99
|
+
def execute
|
100
|
+
if self._sepconn
|
101
|
+
db = Webhookdb::Idempotency.separate_connection
|
102
|
+
else
|
103
|
+
db = Webhookdb::Idempotency.db
|
104
|
+
Webhookdb::Postgres.check_transaction(
|
105
|
+
db,
|
106
|
+
"Cannot use idempotency while already in a transaction, since side effects may not be idempotent. " \
|
107
|
+
"You can chain withusing_seperate_connection to run the idempotency itself separately.",
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
db[:idempotencies].insert_conflict.insert(key: self._key)
|
112
|
+
db.transaction do
|
113
|
+
idem_row = db[:idempotencies].where(key: self._key).for_update.first
|
114
|
+
if idem_row.fetch(:last_run).nil?
|
115
|
+
result = yield()
|
116
|
+
result = self._update_row(db, result)
|
117
|
+
return result
|
118
|
+
end
|
119
|
+
noop_result = self._stored ? idem_row.fetch(:stored_result) : NOOP
|
120
|
+
return noop_result if self._once_ever
|
121
|
+
return noop_result if Time.now < (idem_row[:last_run] + self._every)
|
66
122
|
result = yield()
|
67
|
-
|
123
|
+
result = self._update_row(db, result)
|
68
124
|
return result
|
69
125
|
end
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
126
|
+
end
|
127
|
+
|
128
|
+
def _update_row(db, result)
|
129
|
+
updates = {last_run: Time.now}
|
130
|
+
if self._stored
|
131
|
+
result = result.as_json
|
132
|
+
updates[:stored_result] = Sequel.pg_jsonb_wrap(result)
|
133
|
+
end
|
134
|
+
db[:idempotencies].where(key: self._key).update(updates)
|
74
135
|
return result
|
75
136
|
end
|
76
137
|
end
|
@@ -19,13 +19,32 @@ class Webhookdb::Jobs::Backfill
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def _perform(event)
|
22
|
-
|
22
|
+
begin
|
23
|
+
bfjob = self.lookup_model(Webhookdb::BackfillJob, event.payload)
|
24
|
+
rescue RuntimeError => e
|
25
|
+
self.logger.info "skipping_missing_backfill_job", error: e
|
26
|
+
return
|
27
|
+
end
|
23
28
|
sint = bfjob.service_integration
|
24
29
|
self.with_log_tags(sint.log_tags.merge(backfill_job_id: bfjob.opaque_id)) do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
sint.db.transaction do
|
31
|
+
unless bfjob.lock?
|
32
|
+
self.logger.info "skipping_locked_backfill_job"
|
33
|
+
break
|
34
|
+
end
|
35
|
+
bfjob.refresh
|
36
|
+
if bfjob.finished?
|
37
|
+
self.logger.info "skipping_finished_backfill_job"
|
38
|
+
break
|
39
|
+
end
|
40
|
+
begin
|
41
|
+
sint.replicator.backfill(bfjob)
|
42
|
+
rescue Webhookdb::Replicator::CredentialsMissing
|
43
|
+
# The credentials could have been cleared out, so just finish this job.
|
44
|
+
self.logger.info "skipping_backfill_job_without_credentials"
|
45
|
+
bfjob.update(finished_at: Time.now)
|
46
|
+
break
|
47
|
+
end
|
29
48
|
end
|
30
49
|
end
|
31
50
|
end
|
@@ -1,5 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "webhookdb/email_octopus"
|
4
|
+
require "webhookdb/github"
|
5
|
+
require "webhookdb/sponsy"
|
6
|
+
require "webhookdb/transistor"
|
7
|
+
|
3
8
|
module Webhookdb::Jobs
|
4
9
|
# Create a single way to do the common task of automatic scheduled backfills.
|
5
10
|
# Each integration that needs automated backfills can add a specification here.
|
@@ -2,29 +2,35 @@
|
|
2
2
|
|
3
3
|
require "webhookdb/front"
|
4
4
|
|
5
|
-
class Webhookdb::Oauth::
|
5
|
+
class Webhookdb::Oauth::FrontProvider < Webhookdb::Oauth::Provider
|
6
6
|
include Appydays::Loggable
|
7
7
|
|
8
|
+
# Override these for custom OAuth of different apps
|
8
9
|
def key = "front"
|
9
10
|
def app_name = "Front"
|
11
|
+
def client_id = Webhookdb::Front.client_id
|
12
|
+
def client_secret = Webhookdb::Front.client_secret
|
13
|
+
|
10
14
|
def requires_webhookdb_auth? = true
|
11
15
|
def supports_webhooks? = true
|
12
16
|
|
13
17
|
def authorization_url(state:)
|
14
|
-
return "https://app.frontapp.com/oauth/authorize?response_type=code&redirect_uri=#{
|
18
|
+
return "https://app.frontapp.com/oauth/authorize?response_type=code&redirect_uri=#{self.callback_url}&state=#{state}&client_id=#{self.client_id}"
|
15
19
|
end
|
16
20
|
|
21
|
+
def callback_url = Webhookdb.api_url + "/v1/install/#{self.key}/callback"
|
22
|
+
|
17
23
|
def exchange_authorization_code(code:)
|
18
24
|
token = Webhookdb::Http.post(
|
19
25
|
"https://app.frontapp.com/oauth/token",
|
20
26
|
{
|
21
27
|
"code" => code,
|
22
|
-
"redirect_uri" =>
|
28
|
+
"redirect_uri" => self.callback_url,
|
23
29
|
"grant_type" => "authorization_code",
|
24
30
|
},
|
25
31
|
logger: self.logger,
|
26
32
|
timeout: Webhookdb::Front.http_timeout,
|
27
|
-
basic_auth: {username:
|
33
|
+
basic_auth: {username: self.client_id, password: self.client_secret},
|
28
34
|
)
|
29
35
|
return Webhookdb::Oauth::Tokens.new(
|
30
36
|
access_token: token.parsed_response["access_token"],
|
@@ -56,3 +62,14 @@ class Webhookdb::Oauth::Front < Webhookdb::Oauth::Provider
|
|
56
62
|
root_sint.replicator.build_dependents
|
57
63
|
end
|
58
64
|
end
|
65
|
+
|
66
|
+
class Webhookdb::Oauth::FrontSignalwireChannelProvider < Webhookdb::Oauth::FrontProvider
|
67
|
+
def key = "front_signalwire"
|
68
|
+
def app_name = "Front Signalwire Channel"
|
69
|
+
def client_id = Webhookdb::Front.signalwire_channel_client_id
|
70
|
+
def client_secret = Webhookdb::Front.signalwire_channel_client_secret
|
71
|
+
|
72
|
+
def build_marketplace_integrations(*)
|
73
|
+
raise NotImplementedError, "this should not be called, Front channels have a different setup"
|
74
|
+
end
|
75
|
+
end
|
data/lib/webhookdb/oauth.rb
CHANGED
@@ -56,9 +56,9 @@ module Webhookdb::Oauth
|
|
56
56
|
# rubocop:enable Lint/UnusedMethodArgument
|
57
57
|
|
58
58
|
class << self
|
59
|
-
|
60
|
-
|
61
|
-
raise "#{key} already registered to #{cls}" if self.registry.include?(key)
|
59
|
+
def register(cls)
|
60
|
+
key = cls.new.key
|
61
|
+
raise KeyError, "#{key} already registered to #{cls}" if self.registry.include?(key)
|
62
62
|
self.registry[key] = cls
|
63
63
|
end
|
64
64
|
|
@@ -74,7 +74,8 @@ module Webhookdb::Oauth
|
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
|
-
require "webhookdb/oauth/
|
78
|
-
Webhookdb::Oauth.register(Webhookdb::Oauth::
|
79
|
-
|
80
|
-
|
77
|
+
require "webhookdb/oauth/front_provider"
|
78
|
+
Webhookdb::Oauth.register(Webhookdb::Oauth::FrontProvider)
|
79
|
+
Webhookdb::Oauth.register(Webhookdb::Oauth::FrontSignalwireChannelProvider)
|
80
|
+
require "webhookdb/oauth/intercom_provider"
|
81
|
+
Webhookdb::Oauth.register(Webhookdb::Oauth::IntercomProvider)
|
@@ -7,7 +7,12 @@ class Webhookdb::Organization::Alerting
|
|
7
7
|
include Appydays::Configurable
|
8
8
|
|
9
9
|
configurable(:alerting) do
|
10
|
+
# Only send an alert with a given signature (replicator and error signature)
|
11
|
+
# to a given customer this often. Avoid spamming about any single replicator issue.
|
10
12
|
setting :interval, 24.hours.to_i
|
13
|
+
# Each customer can only receive this many alerts for a given template per day.
|
14
|
+
# Avoids spamming a customer when many rows of a replicator have problems.
|
15
|
+
setting :max_alerts_per_customer_per_day, 15
|
11
16
|
end
|
12
17
|
|
13
18
|
attr_reader :org
|
@@ -25,9 +30,15 @@ class Webhookdb::Organization::Alerting
|
|
25
30
|
"which is a unique identity for this error type, used for grouping and idempotency"
|
26
31
|
end
|
27
32
|
signature = message_template.signature
|
33
|
+
max_alerts_per_customer_per_day = Webhookdb::Organization::Alerting.max_alerts_per_customer_per_day
|
28
34
|
self.org.admin_customers.each do |c|
|
29
35
|
idemkey = "orgalert-#{signature}-#{c.id}"
|
30
36
|
Webhookdb::Idempotency.every(Webhookdb::Organization::Alerting.interval).under_key(idemkey) do
|
37
|
+
sent_last_day = Webhookdb::Message::Delivery.
|
38
|
+
where(template: message_template.full_template_name, recipient: c).
|
39
|
+
limit(max_alerts_per_customer_per_day).
|
40
|
+
count
|
41
|
+
next unless sent_last_day < max_alerts_per_customer_per_day
|
31
42
|
message_template.dispatch_email(c)
|
32
43
|
end
|
33
44
|
end
|
@@ -290,6 +290,25 @@ module Webhookdb::Postgres::ModelUtilities
|
|
290
290
|
end
|
291
291
|
end
|
292
292
|
|
293
|
+
# Run a SELECT FOR UPDATE SKIP LOCKED.
|
294
|
+
# If the model row is already locked, return false.
|
295
|
+
# Otherwise, acquire the lock and return true.
|
296
|
+
#
|
297
|
+
# If the lock is acquired, callers may want to refresh the receiver to make sure it has the newest values.
|
298
|
+
#
|
299
|
+
# @raise [Webhookdb::LockFailed] if the code is not currently in a transaction.
|
300
|
+
def lock?
|
301
|
+
raise Webhookdb::LockFailed, "must be in a transaction" unless self.db.in_transaction?
|
302
|
+
pk = self[self.class.primary_key]
|
303
|
+
got_lock = self.class.dataset.
|
304
|
+
select(1).
|
305
|
+
where(self.class.primary_key => pk).
|
306
|
+
for_update.
|
307
|
+
skip_locked.
|
308
|
+
first
|
309
|
+
return !got_lock.nil?
|
310
|
+
end
|
311
|
+
|
293
312
|
# Round +Time+ t to remove nanoseconds, since Postgres can only store microseconds.
|
294
313
|
protected def round_time(t)
|
295
314
|
return nil if t.nil?
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "uuidx"
|
3
4
|
require "webhookdb/db_adapter"
|
4
5
|
|
5
6
|
class Webhookdb::Replicator::Column
|
@@ -248,6 +249,8 @@ class Webhookdb::Replicator::Column
|
|
248
249
|
|
249
250
|
DEFAULTER_NOW = IsomorphicProc.new(ruby: ->(*) { Time.now }, sql: ->(*) { Sequel.function(:now) })
|
250
251
|
DEFAULTER_FALSE = IsomorphicProc.new(ruby: ->(*) { false }, sql: ->(*) { false })
|
252
|
+
DEFAULTER_UUID4 = IsomorphicProc.new(ruby: ->(*) { Uuidx.v4 }, sql: ->(*) { Sequel.function(:gen_random_uuid) })
|
253
|
+
DEFAULTER_UUID7 = IsomorphicProc.new(ruby: ->(*) { Uuidx.v7 }, sql: NOT_IMPLEMENTED)
|
251
254
|
DEFAULTER_FROM_INTEGRATION_SEQUENCE = IsomorphicProc.new(
|
252
255
|
ruby: ->(service_integration:, **_) { service_integration.sequence_nextval },
|
253
256
|
sql: ->(service_integration:) { Sequel.function(:nextval, service_integration.sequence_name) },
|
@@ -259,7 +262,12 @@ class Webhookdb::Replicator::Column
|
|
259
262
|
sql: ->(*) { key.to_sym },
|
260
263
|
)
|
261
264
|
end
|
262
|
-
KNOWN_DEFAULTERS = {
|
265
|
+
KNOWN_DEFAULTERS = {
|
266
|
+
now: DEFAULTER_NOW,
|
267
|
+
tofalse: DEFAULTER_FALSE,
|
268
|
+
uuid4: DEFAULTER_UUID4,
|
269
|
+
uuid7: DEFAULTER_UUID7,
|
270
|
+
}.freeze
|
263
271
|
|
264
272
|
# Use in data_key when a value is an array, and you want to map a value from the array.
|
265
273
|
EACH_ITEM = :_each_item
|
@@ -11,7 +11,7 @@ class Webhookdb::Replicator::FrontConversationV1 < Webhookdb::Replicator::Base
|
|
11
11
|
return Webhookdb::Replicator::Descriptor.new(
|
12
12
|
name: "front_conversation_v1",
|
13
13
|
ctor: self,
|
14
|
-
feature_roles: [
|
14
|
+
feature_roles: [],
|
15
15
|
resource_name_singular: "Front Conversation",
|
16
16
|
dependency_descriptor: Webhookdb::Replicator::FrontMarketplaceRootV1.descriptor,
|
17
17
|
supports_webhooks: true,
|
@@ -42,4 +42,8 @@ class Webhookdb::Replicator::FrontConversationV1 < Webhookdb::Replicator::Base
|
|
42
42
|
def _update_where_expr
|
43
43
|
return self.qualified_table_sequel_identifier[:data] !~ Sequel[:excluded][:data]
|
44
44
|
end
|
45
|
+
|
46
|
+
def calculate_webhook_state_machine
|
47
|
+
return Webhookdb::Replicator::FrontV1Mixin.marketplace_only_state_machine
|
48
|
+
end
|
45
49
|
end
|