webhookdb 1.2.2 → 1.3.1

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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/admin-dist/assets/index-6aebf805.js +264 -0
  3. data/admin-dist/favicon.ico +0 -0
  4. data/admin-dist/index.html +130 -0
  5. data/admin-dist/manifest.json +15 -0
  6. data/data/messages/replicators/url-recorder.liquid +20 -0
  7. data/data/messages/templates/errors/signalwire_send_sms.email.liquid +31 -0
  8. data/data/messages/web/install-customer-login.liquid +6 -5
  9. data/data/messages/web/install-error.liquid +1 -1
  10. data/data/messages/web/install-forbidden.liquid +25 -0
  11. data/data/messages/web/install-org-chooser.liquid +40 -0
  12. data/data/messages/web/install-success.liquid +2 -1
  13. data/data/messages/web/install.liquid +2 -1
  14. data/data/messages/web/partials/head.liquid +2 -0
  15. data/data/messages/web/styles.liquid +24 -0
  16. data/db/migrations/041_views.rb +20 -0
  17. data/db/migrations/042_sint_lock.rb +10 -0
  18. data/db/migrations/043_text_search.rb +28 -0
  19. data/db/migrations/044_oauth_session_token_cache.rb +21 -0
  20. data/integration/auth_spec.rb +2 -2
  21. data/lib/sequel/plugins/text_searchable.rb +165 -0
  22. data/lib/sequel/text_searchable.rb +42 -0
  23. data/lib/webhookdb/admin_api/auth.rb +24 -3
  24. data/lib/webhookdb/admin_api/data_provider.rb +196 -0
  25. data/lib/webhookdb/admin_api/entities.rb +143 -28
  26. data/lib/webhookdb/admin_api.rb +0 -2
  27. data/lib/webhookdb/api/auth.rb +5 -6
  28. data/lib/webhookdb/api/db.rb +31 -6
  29. data/lib/webhookdb/api/entities.rb +7 -1
  30. data/lib/webhookdb/api/helpers.rb +6 -25
  31. data/lib/webhookdb/api/install.rb +204 -79
  32. data/lib/webhookdb/api/organizations.rb +14 -12
  33. data/lib/webhookdb/api/saved_queries.rb +9 -3
  34. data/lib/webhookdb/api/saved_views.rb +99 -0
  35. data/lib/webhookdb/api/service_integrations.rb +15 -9
  36. data/lib/webhookdb/api/subscriptions.rb +3 -1
  37. data/lib/webhookdb/api/sync_targets.rb +9 -7
  38. data/lib/webhookdb/api/system.rb +1 -0
  39. data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
  40. data/lib/webhookdb/apps.rb +30 -7
  41. data/lib/webhookdb/async/audit_logger.rb +2 -0
  42. data/lib/webhookdb/async.rb +5 -0
  43. data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
  44. data/lib/webhookdb/backfill_job.rb +9 -0
  45. data/lib/webhookdb/customer.rb +5 -0
  46. data/lib/webhookdb/database_document.rb +1 -1
  47. data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
  48. data/lib/webhookdb/db_adapter.rb +20 -4
  49. data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
  50. data/lib/webhookdb/fixtures/organizations.rb +5 -0
  51. data/lib/webhookdb/fixtures/roles.rb +14 -0
  52. data/lib/webhookdb/fixtures/saved_views.rb +25 -0
  53. data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
  54. data/lib/webhookdb/http.rb +8 -2
  55. data/lib/webhookdb/icalendar.rb +3 -0
  56. data/lib/webhookdb/idempotency.rb +69 -22
  57. data/lib/webhookdb/increase.rb +69 -21
  58. data/lib/webhookdb/intercom.rb +10 -3
  59. data/lib/webhookdb/jobs/backfill.rb +3 -1
  60. data/lib/webhookdb/jobs/emailer.rb +0 -1
  61. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
  62. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
  63. data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
  64. data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
  65. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
  66. data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
  67. data/lib/webhookdb/message/body.rb +6 -4
  68. data/lib/webhookdb/message/delivery.rb +2 -0
  69. data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
  70. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
  71. data/lib/webhookdb/oauth/fake_provider.rb +44 -0
  72. data/lib/webhookdb/oauth/front_provider.rb +1 -2
  73. data/lib/webhookdb/oauth/increase_provider.rb +80 -0
  74. data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
  75. data/lib/webhookdb/oauth/session.rb +20 -0
  76. data/lib/webhookdb/oauth.rb +7 -21
  77. data/lib/webhookdb/organization/alerting.rb +2 -0
  78. data/lib/webhookdb/organization/database_migration.rb +3 -0
  79. data/lib/webhookdb/organization.rb +37 -6
  80. data/lib/webhookdb/organization_membership.rb +14 -7
  81. data/lib/webhookdb/postgres.rb +2 -0
  82. data/lib/webhookdb/replicator/base.rb +1 -0
  83. data/lib/webhookdb/replicator/docgen.rb +9 -1
  84. data/lib/webhookdb/replicator/fake.rb +2 -3
  85. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
  86. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
  87. data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
  88. data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
  89. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
  90. data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
  91. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
  92. data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
  93. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
  94. data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
  95. data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
  96. data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
  97. data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
  98. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
  99. data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
  100. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
  101. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
  102. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
  103. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
  104. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  105. data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
  106. data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
  107. data/lib/webhookdb/replicator/webhook_request.rb +4 -0
  108. data/lib/webhookdb/replicator.rb +8 -0
  109. data/lib/webhookdb/role.rb +5 -2
  110. data/lib/webhookdb/saved_query.rb +23 -0
  111. data/lib/webhookdb/saved_view.rb +73 -0
  112. data/lib/webhookdb/sentry.rb +2 -0
  113. data/lib/webhookdb/service/entities.rb +0 -4
  114. data/lib/webhookdb/service/helpers.rb +5 -0
  115. data/lib/webhookdb/service/middleware.rb +9 -0
  116. data/lib/webhookdb/service/types.rb +10 -8
  117. data/lib/webhookdb/service/validators.rb +1 -2
  118. data/lib/webhookdb/service/view_api.rb +1 -1
  119. data/lib/webhookdb/service_integration.rb +17 -15
  120. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
  121. data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
  122. data/lib/webhookdb/subscription.rb +2 -0
  123. data/lib/webhookdb/sync_target.rb +10 -2
  124. data/lib/webhookdb/tasks/message.rb +3 -1
  125. data/lib/webhookdb/version.rb +1 -1
  126. data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
  127. data/lib/webhookdb/webhook_subscription.rb +2 -0
  128. metadata +57 -9
  129. data/lib/webhookdb/admin_api/customers.rb +0 -63
  130. data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
  131. data/lib/webhookdb/admin_api/roles.rb +0 -15
@@ -34,6 +34,10 @@ class Webhookdb::Idempotency < Webhookdb::Postgres::Model(:idempotencies)
34
34
 
35
35
  def skip_transaction_check? = self.skip_transaction_check
36
36
 
37
+ # @return [Hash]
38
+ def memory_cache = @memory_cache ||= {}
39
+ def _memory_cache_results = @_memory_cache_results ||= {}
40
+
37
41
  # @return [Builder]
38
42
  def once_ever
39
43
  b = self::Builder.new
@@ -63,7 +67,7 @@ class Webhookdb::Idempotency < Webhookdb::Postgres::Model(:idempotencies)
63
67
  end
64
68
 
65
69
  class Builder
66
- attr_accessor :_every, :_once_ever, :_stored, :_sepconn, :_key
70
+ attr_accessor :_every, :_once_ever, :_stored, :_sepconn, :_key, :_in_memory
67
71
 
68
72
  # If set, the result of block is stored as JSON,
69
73
  # and returned when an idempotent call is made.
@@ -74,6 +78,16 @@ class Webhookdb::Idempotency < Webhookdb::Postgres::Model(:idempotencies)
74
78
  return self
75
79
  end
76
80
 
81
+ # If set, run a server-side idempotency (just for this process, across all threads)
82
+ # rather than in the database (for all processes).
83
+ # This is useful as a backoff mechanism for certain actions, like log sampling.
84
+ # Note, this uses an unbounded cache size for the sake of simplicity and performance,
85
+ # so be careful to use this with keys that will not grow infinitely.
86
+ def in_memory
87
+ self._in_memory = true
88
+ return self
89
+ end
90
+
77
91
  # Run the idempotency on a separate connection.
78
92
  # Allows use of idempotency within an existing transaction block,
79
93
  # which is normally not allowed. Usually should be used with #stored,
@@ -96,21 +110,24 @@ class Webhookdb::Idempotency < Webhookdb::Postgres::Model(:idempotencies)
96
110
  return self
97
111
  end
98
112
 
99
- def execute
100
- if self._sepconn
101
- db = Webhookdb::Idempotency.separate_connection
113
+ def execute(&)
114
+ if self._in_memory
115
+ db = InMemory.new(Webhookdb::Idempotency.memory_cache, Webhookdb::Idempotency._memory_cache_results)
116
+ elsif self._sepconn
117
+ db = InDatabase.new(Webhookdb::Idempotency.separate_connection)
102
118
  else
103
- db = Webhookdb::Idempotency.db
119
+ conn = Webhookdb::Idempotency.db
104
120
  Webhookdb::Postgres.check_transaction(
105
- db,
121
+ conn,
106
122
  "Cannot use idempotency while already in a transaction, since side effects may not be idempotent. " \
107
123
  "You can chain withusing_seperate_connection to run the idempotency itself separately.",
108
124
  )
125
+ db = InDatabase.new(conn)
109
126
  end
110
127
 
111
- db[:idempotencies].insert_conflict.insert(key: self._key)
128
+ db.ensure(self._key)
112
129
  db.transaction do
113
- idem_row = db[:idempotencies].where(key: self._key).for_update.first
130
+ idem_row = db.lock(self._key)
114
131
  if idem_row.fetch(:last_run).nil?
115
132
  result = yield()
116
133
  result = self._update_row(db, result)
@@ -126,26 +143,56 @@ class Webhookdb::Idempotency < Webhookdb::Postgres::Model(:idempotencies)
126
143
  end
127
144
 
128
145
  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)
135
- return result
146
+ jresult = self._stored ? result.as_json : result
147
+ db.finish(self._key, last_run: Time.now, stored: self._stored, result: jresult)
148
+ return jresult
149
+ end
150
+ end
151
+
152
+ class InMemory
153
+ def initialize(cache, store)
154
+ @cache = cache
155
+ @store = store
156
+ end
157
+
158
+ def ensure(_key) = nil
159
+ def lock(key) = {last_run: @cache[key], stored_result: @store[key]}
160
+ def transaction(&) = yield
161
+
162
+ def finish(key, last_run:, stored:, result:)
163
+ @cache[key] = last_run
164
+ @store[key] = result if stored
165
+ end
166
+ end
167
+
168
+ class InDatabase
169
+ def initialize(conn)
170
+ @conn = conn
171
+ end
172
+
173
+ def db = @conn
174
+ def transaction(&) = db.transaction(&)
175
+ def ensure(key) = db[:idempotencies].insert_conflict.insert(key:)
176
+ def lock(key) = db[:idempotencies].where(key:).for_update.first
177
+
178
+ def finish(key, last_run:, stored:, result:)
179
+ updates = {last_run:}
180
+ updates[:stored_result] = Sequel.pg_jsonb_wrap(result) if stored
181
+ db[:idempotencies].where(key:).update(updates)
136
182
  end
137
183
  end
138
184
  end
139
185
 
140
186
  # Table: idempotencies
141
- # -------------------------------------------------------------------------------------
187
+ # ----------------------------------------------------------------------------------------
142
188
  # Columns:
143
- # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
144
- # created_at | timestamp with time zone | NOT NULL DEFAULT now()
145
- # updated_at | timestamp with time zone |
146
- # last_run | timestamp with time zone |
147
- # key | text |
189
+ # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
190
+ # created_at | timestamp with time zone | NOT NULL DEFAULT now()
191
+ # updated_at | timestamp with time zone |
192
+ # last_run | timestamp with time zone |
193
+ # key | text |
194
+ # stored_result | jsonb |
148
195
  # Indexes:
149
196
  # idempotencies_pkey | PRIMARY KEY btree (id)
150
197
  # idempotencies_key_key | UNIQUE btree (key)
151
- # -------------------------------------------------------------------------------------
198
+ # ----------------------------------------------------------------------------------------
@@ -6,37 +6,85 @@ class Webhookdb::Increase
6
6
  include Appydays::Loggable
7
7
 
8
8
  configurable(:increase) do
9
+ # This is created in your 'platform' account, and is needed to call the OAuth endpoints.
10
+ # https://dashboard.increase.com/developers/api_keys
11
+ setting :api_key, "increase_api_key"
12
+ # This is created in your Platform account, and is used to sign webhooks,
13
+ # which we get for both platform events (which are ignored) and associated oauth app events
14
+ # (with an Increase-Group-Id header).
15
+ # https://dashboard.increase.com/developers/webhooks
16
+ setting :webhook_secret, "increase_webhook_secret"
17
+
18
+ # Id and secret for the WebhookDB Oauth app.
19
+ # https://dashboard.increase.com/developers/oauths
20
+ setting :oauth_client_id, "increase_oauth_fake_client"
21
+ setting :oauth_client_secret, "increase_oauth_fake_secret"
22
+
9
23
  setting :http_timeout, 30
10
24
  end
11
25
 
12
- def self.webhook_response(request, webhook_secret)
13
- http_signature = request.env["HTTP_X_BANK_WEBHOOK_SIGNATURE"]
14
-
15
- return Webhookdb::WebhookResponse.error("missing hmac") if http_signature.nil?
26
+ class WebhookSignature < Webhookdb::TypedStruct
27
+ attr_accessor :t, :v1
16
28
 
17
- request.body.rewind
18
- request_data = request.body.read
29
+ def _defaults = {t: nil, v1: []}
19
30
 
20
- computed_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), webhook_secret, request_data)
21
-
22
- if http_signature != "sha256=" + computed_signature
23
- # Invalid signature
24
- self.logger.warn "increase signature verification error"
25
- return Webhookdb::WebhookResponse.error("invalid hmac")
31
+ def format
32
+ parts = []
33
+ parts << "t=#{self.t.utc.iso8601}" if self.t
34
+ self.v1&.each { |v1| parts << "v1=#{v1}" }
35
+ return parts.join(",")
26
36
  end
37
+ end
27
38
 
28
- return Webhookdb::WebhookResponse.ok
39
+ # @param s [String,nil]
40
+ # @return [WebhookSignature]
41
+ def self.parse_signature(s)
42
+ sig = WebhookSignature.new
43
+ s&.split(",")&.each do |part|
44
+ key, val = part.split("=")
45
+ if key == "t"
46
+ begin
47
+ sig.t = Time.rfc3339(val)
48
+ rescue ArgumentError
49
+ nil
50
+ end
51
+ elsif key == "v1"
52
+ sig.v1 << val
53
+ end
54
+ end
55
+ return sig
29
56
  end
30
57
 
31
- # this helper function finds the relevant object data and helps us avoid repeated code
32
- def self.find_desired_object_data(body)
33
- return body.fetch("data", body)
58
+ # @param secret [String]
59
+ # @param data [String]
60
+ # @param t [Time]
61
+ # @return [WebhookSignature]
62
+ def self.compute_signature(secret:, data:, t:)
63
+ signed_payload = "#{t.utc.iso8601}.#{data}"
64
+ sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, signed_payload)
65
+ return WebhookSignature.new(v1: [sig], t:)
34
66
  end
35
67
 
36
- # this function interprets webhook contents to assist with filtering webhooks by object type in our increase services
37
- def self.contains_desired_object(webhook_body, desired_object_name)
38
- object_of_interest = self.find_desired_object_data(webhook_body)
39
- object_id = object_of_interest["id"]
40
- return object_id.include?(desired_object_name)
68
+ OLD_CUTOFF = 35.days
69
+ NEW_CUTOFF = 4.days
70
+
71
+ def self.webhook_response(request, webhook_secret, now: Time.now)
72
+ http_signature = request.env["HTTP_INCREASE_WEBHOOK_SIGNATURE"]
73
+ return Webhookdb::WebhookResponse.error("missing header") if http_signature.nil?
74
+
75
+ request.body.rewind
76
+ request_data = request.body.read
77
+
78
+ parsed_signature = self.parse_signature(http_signature)
79
+ return Webhookdb::WebhookResponse.error("missing timestamp") if parsed_signature.t.nil?
80
+ return Webhookdb::WebhookResponse.error("missing signatures") if parsed_signature.v1.empty?
81
+ return Webhookdb::WebhookResponse.error("too old") if parsed_signature.t < (now - OLD_CUTOFF)
82
+ return Webhookdb::WebhookResponse.error("too new") if parsed_signature.t > (now + NEW_CUTOFF)
83
+
84
+ computed_signature = self.compute_signature(secret: webhook_secret, data: request_data, t: parsed_signature.t)
85
+ return Webhookdb::WebhookResponse.error("invalid signature") unless
86
+ parsed_signature.v1.include?(computed_signature.v1.first)
87
+
88
+ return Webhookdb::WebhookResponse.ok
41
89
  end
42
90
  end
@@ -12,9 +12,16 @@ module Webhookdb::Intercom
12
12
  setting :page_size, 20
13
13
  end
14
14
 
15
- def self.verify_webhook(data, hmac_header)
16
- calculated_hmac = "sha1=#{OpenSSL::HMAC.hexdigest('SHA1', self.client_secret, data)}"
17
- return ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, hmac_header)
15
+ def self.webhook_response(request, webhook_secret)
16
+ header_value = request.env["HTTP_X_HUB_SIGNATURE"]
17
+ return Webhookdb::WebhookResponse.error("missing hmac") if header_value.nil?
18
+ request.body.rewind
19
+ request_data = request.body.read
20
+ hmac = OpenSSL::HMAC.hexdigest("SHA1", webhook_secret, request_data)
21
+ calculated_hmac = "sha1=#{hmac}"
22
+ verified = ActiveSupport::SecurityUtils.secure_compare(calculated_hmac, header_value)
23
+ return Webhookdb::WebhookResponse.ok if verified
24
+ return Webhookdb::WebhookResponse.error("invalid hmac")
18
25
  end
19
26
 
20
27
  def self.auth_headers(token)
@@ -26,10 +26,12 @@ class Webhookdb::Jobs::Backfill
26
26
  return
27
27
  end
28
28
  sint = bfjob.service_integration
29
+ bflock = bfjob.ensure_service_integration_lock
29
30
  self.with_log_tags(sint.log_tags.merge(backfill_job_id: bfjob.opaque_id)) do
30
31
  sint.db.transaction do
31
- unless bfjob.lock?
32
+ unless bflock.lock?
32
33
  self.logger.info "skipping_locked_backfill_job"
34
+ bfjob.update(finished_at: Time.now)
33
35
  break
34
36
  end
35
37
  bfjob.refresh
@@ -9,7 +9,6 @@ class Webhookdb::Jobs::Emailer
9
9
  splay 5.seconds
10
10
 
11
11
  def _perform
12
- self.logger.info "Sending pending emails"
13
12
  Webhookdb::Message.send_unsent
14
13
  end
15
14
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/async/scheduled_job"
4
+ require "webhookdb/jobs"
5
+
6
+ class Webhookdb::Jobs::IcalendarDeleteStaleCancelledEvents
7
+ extend Webhookdb::Async::ScheduledJob
8
+
9
+ cron "37 7 * * *" # Once a day
10
+ splay 120
11
+
12
+ def _perform
13
+ Webhookdb::ServiceIntegration.where(service_name: "icalendar_event_v1").each do |sint|
14
+ self.with_log_tags(sint.log_tags) do
15
+ sint.replicator.delete_stale_cancelled_events
16
+ end
17
+ end
18
+ end
19
+ end
@@ -22,7 +22,7 @@ class Webhookdb::Jobs::IcalendarEnqueueSyncs
22
22
  self.with_log_tags(sint.log_tags) do
23
23
  splay = rand(1..max_splay)
24
24
  enqueued_job_id = Webhookdb::Jobs::IcalendarSync.perform_in(splay, sint.id, calendar_external_id)
25
- self.logger.info("enqueued_icalendar_sync", calendar_external_id:, enqueued_job_id:)
25
+ self.logger.debug("enqueued_icalendar_sync", calendar_external_id:, enqueued_job_id:)
26
26
  end
27
27
  end
28
28
  end
@@ -16,7 +16,7 @@ class Webhookdb::Jobs::IcalendarSync
16
16
  self.logger.warn("icalendar_sync_row_miss", calendar_external_id:)
17
17
  return
18
18
  end
19
- self.logger.info("icalendar_sync_start")
19
+ self.logger.debug("icalendar_sync_start")
20
20
  sint.replicator.sync_row(row)
21
21
  end
22
22
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/async/job"
4
+
5
+ class Webhookdb::Jobs::IncreaseEventHandler
6
+ extend Webhookdb::Async::Job
7
+
8
+ on "increase.*"
9
+
10
+ def _perform(event)
11
+ case event.name
12
+ when "increase.oauth_connection.deactivated"
13
+ conn_id = event.payload[0].fetch("associated_object_id")
14
+ self.logger.info("increase_oauth_disconnect", oauth_connection_id: conn_id)
15
+ Webhookdb::Oauth::IncreaseProvider.disconnect_oauth(conn_id)
16
+ else
17
+ self.logger.info("increase_event_noop")
18
+ end
19
+ end
20
+ end
@@ -51,8 +51,9 @@ module Webhookdb::Jobs
51
51
  Webhookdb::Github.activity_cron_expression, 30.seconds, false,
52
52
  ),
53
53
  Spec.new(
54
+ # I think we can get rid of this once we're more confident webhooks are working reliably.
54
55
  "IntercomScheduledBackfill", "intercom_marketplace_root_v1",
55
- "*/1 * * * *", 0, false, true,
56
+ "46 4 * * *", 0, false, true,
56
57
  ),
57
58
  Spec.new(
58
59
  "AtomSingleFeedPoller", "atom_single_feed_v1",
@@ -30,7 +30,9 @@ class Webhookdb::Jobs::SyncTargetRunSync
30
30
  ) do
31
31
  stgt.run_sync(now: Time.now)
32
32
  rescue Webhookdb::SyncTarget::SyncInProgress
33
- self.logger.info("sync_target_already_in_progress")
33
+ Webhookdb::Idempotency.every(30.seconds).in_memory.under_key("sync_target_in_progress-#{stgt.id}") do
34
+ self.logger.info("sync_target_already_in_progress")
35
+ end
34
36
  rescue Webhookdb::SyncTarget::Deleted
35
37
  self.logger.info("sync_target_deleted")
36
38
  end
@@ -6,6 +6,7 @@ require "webhookdb/message"
6
6
 
7
7
  class Webhookdb::Message::Body < Webhookdb::Postgres::Model(:message_bodies)
8
8
  plugin :timestamps
9
+ plugin :text_searchable, terms: [:content]
9
10
 
10
11
  many_to_one :delivery, class: "Webhookdb::Message::Delivery"
11
12
  end
@@ -13,10 +14,11 @@ end
13
14
  # Table: message_bodies
14
15
  # ----------------------------------------------------------------------------------------------------
15
16
  # Columns:
16
- # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
17
- # content | text | NOT NULL
18
- # mediatype | text | NOT NULL
19
- # delivery_id | integer | NOT NULL
17
+ # id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
18
+ # content | text | NOT NULL
19
+ # mediatype | text | NOT NULL
20
+ # delivery_id | integer | NOT NULL
21
+ # text_search | tsvector |
20
22
  # Indexes:
21
23
  # message_bodies_pkey | PRIMARY KEY btree (id)
22
24
  # message_bodies_delivery_id_index | btree (delivery_id)
@@ -7,6 +7,7 @@ require "webhookdb/message"
7
7
  class Webhookdb::Message::Delivery < Webhookdb::Postgres::Model(:message_deliveries)
8
8
  plugin :timestamps
9
9
  plugin :soft_deletes
10
+ plugin :text_searchable, terms: [:template, :to, :recipient]
10
11
 
11
12
  many_to_one :recipient, class: "Webhookdb::Customer"
12
13
  one_to_many :bodies, class: "Webhookdb::Message::Body"
@@ -115,6 +116,7 @@ end
115
116
  # recipient_id | integer |
116
117
  # extra_fields | jsonb | NOT NULL DEFAULT '{}'::jsonb
117
118
  # soft_deleted_at | timestamp with time zone |
119
+ # text_search | tsvector |
118
120
  # Indexes:
119
121
  # message_deliveries_pkey | PRIMARY KEY btree (id)
120
122
  # message_deliveries_transport_message_id_key | UNIQUE btree (transport_message_id)
@@ -21,8 +21,7 @@ class Webhookdb::Messages::ErrorIcalendarFetch < Webhookdb::Message::Template
21
21
  end
22
22
 
23
23
  def signature
24
- ehash = Digest::MD5.hexdigest(@kw.to_s)
25
- return "msg-#{self.full_template_name}-sint:#{@service_integration.id}-eid:#{@external_calendar_id}-err:#{ehash}"
24
+ return "msg-#{self.full_template_name}-sint:#{@service_integration.id}-eid:#{@external_calendar_id}"
26
25
  end
27
26
 
28
27
  def template_folder = "errors"
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/message/template"
4
+
5
+ class Webhookdb::Messages::ErrorSignalwireSendSms < Webhookdb::Message::Template
6
+ def self.fixtured(_recipient)
7
+ sint = Webhookdb::Fixtures.service_integration.create
8
+ return self.new(
9
+ sint,
10
+ response_status: 422,
11
+ request_url: "https://whdbtest.signalwire.com/2010-04-01/Accounts/projid/Messages.json",
12
+ request_method: "POST",
13
+ response_body: {
14
+ code: "21717",
15
+ message: "From must belong to an active campaign.",
16
+ more_info: "https://developer.signalwire.com/compatibility-api/reference/error-codes",
17
+ status: 400,
18
+ }.to_json,
19
+ )
20
+ end
21
+
22
+ def initialize(service_integration, request_url:, request_method:, response_status:, response_body:)
23
+ @service_integration = service_integration
24
+ @request_url = request_url
25
+ @request_method = request_method
26
+ @response_status = response_status
27
+ @response_body = response_body
28
+ super()
29
+ end
30
+
31
+ def signature
32
+ return "msg-#{self.full_template_name}-sint:#{@service_integration.id}-req:#{@request_url}"
33
+ end
34
+
35
+ def template_folder = "errors"
36
+ def template_name = "signalwire_send_sms"
37
+
38
+ def liquid_drops
39
+ return super.merge(
40
+ service_name: @service_integration.service_name,
41
+ opaque_id: @service_integration.opaque_id,
42
+ request_method: @request_method,
43
+ request_url: @request_url,
44
+ response_status: @response_status,
45
+ response_body: @response_body,
46
+ )
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/increase"
4
+
5
+ class Webhookdb::Oauth::FakeProvider < Webhookdb::Oauth::Provider
6
+ class << self
7
+ # If any of these are non-nil, they're called instead of the instance method.
8
+ attr_accessor :supports_webhooks, :exchange_authorization_code
9
+
10
+ def reset
11
+ self.supports_webhooks = nil
12
+ self.exchange_authorization_code = nil
13
+ end
14
+ end
15
+
16
+ def key = "fake"
17
+ def app_name = "Fake"
18
+ def supports_webhooks? = _call_or_do(:supports_webhooks) { true }
19
+
20
+ def authorization_url(state:)
21
+ return "#{Webhookdb.api_url}/v1/install/fake_oauth_authorization?client_id=fakeclient&state=#{state}"
22
+ end
23
+
24
+ def exchange_authorization_code(code:)
25
+ return _call_or_do(:exchange_authorization_code) do
26
+ Webhookdb::Oauth::Tokens.new(access_token: "access-#{code}", refresh_token: "refresh-#{code}")
27
+ end
28
+ end
29
+
30
+ def build_marketplace_integrations(organization:, tokens:, **)
31
+ return Webhookdb::ServiceIntegration.create_disambiguated(
32
+ "fake_v1",
33
+ organization:,
34
+ webhook_secret: tokens.access_token,
35
+ backfill_key: tokens.refresh_token,
36
+ )
37
+ end
38
+
39
+ protected def _call_or_do(m)
40
+ d = Webhookdb::Oauth::FakeProvider.send(m)
41
+ return yield if d.nil?
42
+ return d.call
43
+ end
44
+ end
@@ -5,13 +5,11 @@ require "webhookdb/front"
5
5
  class Webhookdb::Oauth::FrontProvider < Webhookdb::Oauth::Provider
6
6
  include Appydays::Loggable
7
7
 
8
- # Override these for custom OAuth of different apps
9
8
  def key = "front"
10
9
  def app_name = "Front"
11
10
  def client_id = Webhookdb::Front.client_id
12
11
  def client_secret = Webhookdb::Front.client_secret
13
12
 
14
- def requires_webhookdb_auth? = true
15
13
  def supports_webhooks? = true
16
14
 
17
15
  def authorization_url(state:)
@@ -60,6 +58,7 @@ class Webhookdb::Oauth::FrontProvider < Webhookdb::Oauth::Provider
60
58
  backfill_key: tokens.refresh_token,
61
59
  )
62
60
  root_sint.replicator.build_dependents
61
+ return root_sint
63
62
  end
64
63
  end
65
64
 
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/increase"
4
+
5
+ class Webhookdb::Oauth::IncreaseProvider < Webhookdb::Oauth::Provider
6
+ include Appydays::Loggable
7
+
8
+ def key = "increase"
9
+ def app_name = "Increase"
10
+ # Increase POSTs all Oauth app webhooks to the same place
11
+ def supports_webhooks? = true
12
+
13
+ def authorization_url(state:)
14
+ return "https://increase.com/oauth/authorization?client_id=#{Webhookdb::Increase.oauth_client_id}&state=#{state}&scope=read_only"
15
+ end
16
+
17
+ def exchange_authorization_code(code:)
18
+ token_resp = Webhookdb::Http.post(
19
+ "https://api.increase.com/oauth/tokens",
20
+ {
21
+ "client_id" => Webhookdb::Increase.oauth_client_id,
22
+ "client_secret" => Webhookdb::Increase.oauth_client_secret,
23
+ "code" => code,
24
+ grant_type: "authorization_code",
25
+ },
26
+ headers: {"Authorization" => "Bearer #{Webhookdb::Increase.api_key}"},
27
+ logger: self.logger,
28
+ timeout: Webhookdb::Increase.http_timeout,
29
+ )
30
+ return Webhookdb::Oauth::Tokens.new(access_token: token_resp.parsed_response["access_token"])
31
+ end
32
+
33
+ def build_marketplace_integrations(organization:, tokens:, **)
34
+ group_resp = Webhookdb::Http.get(
35
+ "https://api.increase.com/groups/current",
36
+ headers: {"Authorization" => "Bearer #{tokens.access_token}"},
37
+ logger: self.logger,
38
+ timeout: Webhookdb::Increase.http_timeout,
39
+ )
40
+ group_id = group_resp.parsed_response.fetch("id")
41
+ # We need to store the Oauth Connection ID for the group, so when there is an oauth disconnect,
42
+ # we know which replicator to destroy.
43
+ #
44
+ # The deactivation comes through as an event on the Platform account,
45
+ # and does not tell us the Group ID. And we cannot fetch the OAuth Connection
46
+ # on the Platform account, since once a Connection is deactivated,
47
+ # the Increase API will not return it.
48
+ #
49
+ # This seems like a bug, and has been reported to Increase.
50
+ # If their /oauth_connections/:id endpoint starts working for 'inactive' connections,
51
+ # this code can be removed, and we can look up the group ID when we handle the deactivate webhook.
52
+ oauth_conns_resp = Webhookdb::Http.get(
53
+ "https://api.increase.com/oauth_connections",
54
+ headers: {"Authorization" => "Bearer #{Webhookdb::Increase.api_key}"},
55
+ logger: self.logger,
56
+ timeout: Webhookdb::Increase.http_timeout,
57
+ )
58
+ group_conn = oauth_conns_resp.parsed_response["data"].find { |c| c.fetch("group_id") == group_id }
59
+ raise Webhookdb::InvariantViolation, "no OAuth Connection for Group #{group_id}/Org #{organization.key}" if
60
+ group_conn.nil?
61
+ root_sint = Webhookdb::ServiceIntegration.create_disambiguated(
62
+ "increase_app_v1",
63
+ organization:,
64
+ api_url: group_id,
65
+ backfill_key: tokens.access_token,
66
+ webhookdb_api_key: group_conn.fetch("id"),
67
+ )
68
+ root_sint.replicator.build_dependents
69
+ return root_sint
70
+ end
71
+
72
+ def self.disconnect_oauth(connection_id)
73
+ # It may have already been deleted, make this idempotent.
74
+ # See above for why we store the connection id directly.
75
+ Webhookdb::ServiceIntegration.where(service_name: "increase_app_v1").
76
+ with_encrypted_value(:webhookdb_api_key, connection_id).
77
+ all.
78
+ each(&:destroy_self_and_all_dependents)
79
+ end
80
+ end
@@ -7,7 +7,6 @@ class Webhookdb::Oauth::IntercomProvider < Webhookdb::Oauth::Provider
7
7
 
8
8
  def key = "intercom"
9
9
  def app_name = "Intercom"
10
- def requires_webhookdb_auth? = false
11
10
  def supports_webhooks? = false
12
11
 
13
12
  def authorization_url(state:)
@@ -28,7 +27,7 @@ class Webhookdb::Oauth::IntercomProvider < Webhookdb::Oauth::Provider
28
27
  return Webhookdb::Oauth::Tokens.new(access_token: token_resp.parsed_response["token"])
29
28
  end
30
29
 
31
- def find_or_create_customer(tokens:, scope:)
30
+ def build_marketplace_integrations(organization:, tokens:)
32
31
  intercom_user_resp = Webhookdb::Http.get(
33
32
  "https://api.intercom.io/me",
34
33
  headers: Webhookdb::Intercom.auth_headers(tokens.access_token),
@@ -36,17 +35,9 @@ class Webhookdb::Oauth::IntercomProvider < Webhookdb::Oauth::Provider
36
35
  timeout: Webhookdb::Intercom.http_timeout,
37
36
  )
38
37
 
39
- intercom_user = intercom_user_resp.parsed_response
40
- scope[:me_response] = intercom_user
41
- intercom_email = intercom_user.fetch("email")
42
- return Webhookdb::Customer.find_or_create_for_email(intercom_email)
43
- end
44
-
45
- def build_marketplace_integrations(organization:, tokens:, scope:)
46
- intercom_user = scope.fetch(:me_response)
47
38
  # The intercom workspace id is used in the intercom webhook endpoint to identify which
48
39
  # service integration to delegate requests to.
49
- intercom_workspace_id = intercom_user.dig("app", "id_code")
40
+ intercom_workspace_id = intercom_user_resp.parsed_response.dig("app", "id_code")
50
41
  root_sint = Webhookdb::ServiceIntegration.create_disambiguated(
51
42
  "intercom_marketplace_root_v1",
52
43
  organization:,
@@ -54,5 +45,6 @@ class Webhookdb::Oauth::IntercomProvider < Webhookdb::Oauth::Provider
54
45
  backfill_key: tokens.access_token,
55
46
  )
56
47
  root_sint.replicator.build_dependents
48
+ return root_sint
57
49
  end
58
50
  end