webhookdb 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|