webhookdb 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/data/messages/web/install-customer-login.liquid +1 -1
  3. data/data/messages/web/install-success.liquid +2 -2
  4. data/db/migrations/038_webhookdb_api_key.rb +13 -0
  5. data/lib/webhookdb/api/install.rb +23 -1
  6. data/lib/webhookdb/api/service_integrations.rb +17 -12
  7. data/lib/webhookdb/api/system.rb +13 -2
  8. data/lib/webhookdb/fixtures/service_integrations.rb +4 -0
  9. data/lib/webhookdb/front.rb +23 -11
  10. data/lib/webhookdb/idempotency.rb +94 -33
  11. data/lib/webhookdb/jobs/backfill.rb +24 -5
  12. data/lib/webhookdb/jobs/scheduled_backfills.rb +5 -0
  13. data/lib/webhookdb/oauth/{front.rb → front_provider.rb} +21 -4
  14. data/lib/webhookdb/oauth/{intercom.rb → intercom_provider.rb} +1 -1
  15. data/lib/webhookdb/oauth.rb +8 -7
  16. data/lib/webhookdb/organization/alerting.rb +11 -0
  17. data/lib/webhookdb/postgres/model_utilities.rb +19 -0
  18. data/lib/webhookdb/replicator/column.rb +9 -1
  19. data/lib/webhookdb/replicator/front_conversation_v1.rb +5 -1
  20. data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +2 -5
  21. data/lib/webhookdb/replicator/front_message_v1.rb +5 -1
  22. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +325 -0
  23. data/lib/webhookdb/replicator/front_v1_mixin.rb +9 -1
  24. data/lib/webhookdb/replicator/signalwire_message_v1.rb +25 -13
  25. data/lib/webhookdb/service_integration.rb +36 -3
  26. data/lib/webhookdb/signalwire.rb +40 -0
  27. data/lib/webhookdb/spec_helpers/citest.rb +18 -9
  28. data/lib/webhookdb/spec_helpers/postgres.rb +9 -0
  29. data/lib/webhookdb/spec_helpers/service.rb +5 -0
  30. data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +25 -13
  31. data/lib/webhookdb/spec_helpers/whdb.rb +7 -0
  32. data/lib/webhookdb/sync_target.rb +1 -1
  33. data/lib/webhookdb/tasks/specs.rb +4 -2
  34. data/lib/webhookdb/version.rb +1 -1
  35. data/lib/webhookdb.rb +14 -0
  36. metadata +34 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd104a25c13b1c3d4a9500bdb618c2f704692c26a09a307eb926cf87bcb2befe
4
- data.tar.gz: e61ed8994471efa0ab7ab448cf12b68cf4cbb44152df3e98024da3ffa1985c5c
3
+ metadata.gz: d22b47783f1aaabc29dcc0fa023c0fee5bc9088d303103d1cf0cf859f807df0f
4
+ data.tar.gz: 41f7fcafef5d6724060e99fd40c5cfd19e9cd8d58c5736ccdd1bbaef9ecca826
5
5
  SHA512:
6
- metadata.gz: 7a0839a974acd4cc643501fe544461e0848f3ee01e1e2b47bcb325dba10636556d15bb597532adb7039caa2dd4b8cf9be6fe487a4cfa211bdd82505dda1c3cc2
7
- data.tar.gz: 4b619d15175fefaea0eafe39a57aa127c2a1b41beadf5f87fb7586e233170f30c51c02b4e20aa4701e695a42031459d9b1f11fb2ddae4ff1aa8f3d94a98aa4ee
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 Webhookdb! In order to complete this integration, we need your email address so that
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 access the information using the following url:
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
- id: sint.opaque_id,
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.capitalize, v]
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
- else
374
- confirmation_msg = "Great! We've deleted all secrets for #{sint.service_name} and its dependents. " \
375
- "The following tables have been dropped: \n\n #{sint.table_name} \n #{dependents_lines}"
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
@@ -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
- get :echo do
38
- pp params.to_h
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
@@ -39,4 +39,8 @@ module Webhookdb::Fixtures::ServiceIntegrations
39
39
  self.backfill_key = "fake_bfkey"
40
40
  self.backfill_secret = "fake_bfsecret"
41
41
  end
42
+
43
+ decorator :with_api_key do
44
+ self.webhookdb_api_key ||= self.new_api_key
45
+ end
42
46
  end
@@ -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
- # The api secret is used for webhook verification, the client id and secret are used for OAuth
11
- setting :api_secret, "front_api_secret"
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
- def self.oauth_callback_url = Webhookdb.api_url + "/v1/install/front/callback"
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"), self.api_secret, base_string)
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 = Webhookdb::Front.verify_signature(request)
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
- # Skip the transaction check. Useful in unit tests. See class docs for details.
34
- singleton_predicate_accessor :skip_transaction_check
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
- def self.once_ever
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
- def self.every(interval)
43
- idem = self.new
44
- idem.__every = interval
45
- return idem
46
- end
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
- attr_accessor :__every, :__once_ever
44
+ # @return [Builder]
45
+ def every(interval)
46
+ b = self::Builder.new
47
+ b._every = interval
48
+ return b
49
+ end
49
50
 
50
- def under_key(key, &block)
51
- self.key = key
52
- return self.execute(&block) if block
53
- return self
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
- def execute
57
- Webhookdb::Postgres.check_transaction(
58
- self.db,
59
- "Cannot use idempotency while already in a transaction, since side effects may not be idempotent",
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
- self.class.dataset.insert_conflict.insert(key: self.key)
63
- self.db.transaction do
64
- idem = Webhookdb::Idempotency[key: self.key].lock!
65
- if idem.last_run.nil?
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
- idem.update(last_run: Time.now)
123
+ result = self._update_row(db, result)
68
124
  return result
69
125
  end
70
- return NOOP if self.__once_ever
71
- return NOOP if Time.now < (idem.last_run + self.__every)
72
- result = yield()
73
- idem.update(last_run: Time.now)
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
- bfjob = self.lookup_model(Webhookdb::BackfillJob, event.payload)
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
- if bfjob.finished?
26
- self.logger.info "skipping_finished_backfill_job"
27
- else
28
- sint.replicator.backfill(bfjob)
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::Front < Webhookdb::Oauth::Provider
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=#{Webhookdb::Front.oauth_callback_url}&state=#{state}&client_id=#{Webhookdb::Front.client_id}"
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" => Webhookdb::Front.oauth_callback_url,
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: Webhookdb::Front.client_id, password: Webhookdb::Front.client_secret},
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "webhookdb/intercom"
4
4
 
5
- class Webhookdb::Oauth::Intercom < Webhookdb::Oauth::Provider
5
+ class Webhookdb::Oauth::IntercomProvider < Webhookdb::Oauth::Provider
6
6
  include Appydays::Loggable
7
7
 
8
8
  def key = "intercom"
@@ -56,9 +56,9 @@ module Webhookdb::Oauth
56
56
  # rubocop:enable Lint/UnusedMethodArgument
57
57
 
58
58
  class << self
59
- # @return [String, Class]
60
- def register(key, cls)
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/front"
78
- Webhookdb::Oauth.register(Webhookdb::Oauth::Front.new.key, Webhookdb::Oauth::Front)
79
- require "webhookdb/oauth/intercom"
80
- Webhookdb::Oauth.register(Webhookdb::Oauth::Intercom.new.key, Webhookdb::Oauth::Intercom)
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 = {now: DEFAULTER_NOW, tofalse: DEFAULTER_FALSE}.freeze
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: ["front"],
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