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.
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: eaccd930255b228e07aec00b36d43b6bfc47d0d57fca0ff7fe721e2bee3e09d1
4
- data.tar.gz: e51b55c7dd624833ba56d11ecd666e6d3977c8c4e3d92f4b8146c3f8c9c2a47a
3
+ metadata.gz: d22b47783f1aaabc29dcc0fa023c0fee5bc9088d303103d1cf0cf859f807df0f
4
+ data.tar.gz: 41f7fcafef5d6724060e99fd40c5cfd19e9cd8d58c5736ccdd1bbaef9ecca826
5
5
  SHA512:
6
- metadata.gz: 1b83e8bd2da2e8857e4fbfc4fce5ec2b92ebf22438d047014a337540bfab293412d3327acf13d470cd2063b6f2b712f568e7e225355af8b978a631befc17eea0
7
- data.tar.gz: b5ef0bb3313894aab5b8c939905cbbd0f19196a96255ca3346a139b4f9338cedf55ad94ee55ac418395651c6d9a0d9d6949614de5965e651f1ea232354abd33b
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