webhookdb 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) 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/040_saved_query_fix_unique.rb +17 -0
  17. data/db/migrations/041_views.rb +20 -0
  18. data/db/migrations/042_sint_lock.rb +10 -0
  19. data/db/migrations/043_text_search.rb +28 -0
  20. data/db/migrations/044_oauth_session_token_cache.rb +21 -0
  21. data/integration/auth_spec.rb +2 -2
  22. data/lib/sequel/plugins/text_searchable.rb +165 -0
  23. data/lib/sequel/text_searchable.rb +42 -0
  24. data/lib/webhookdb/admin_api/auth.rb +24 -3
  25. data/lib/webhookdb/admin_api/data_provider.rb +196 -0
  26. data/lib/webhookdb/admin_api/entities.rb +143 -28
  27. data/lib/webhookdb/admin_api.rb +0 -2
  28. data/lib/webhookdb/api/auth.rb +5 -6
  29. data/lib/webhookdb/api/db.rb +31 -6
  30. data/lib/webhookdb/api/entities.rb +7 -1
  31. data/lib/webhookdb/api/helpers.rb +6 -25
  32. data/lib/webhookdb/api/install.rb +204 -79
  33. data/lib/webhookdb/api/organizations.rb +14 -12
  34. data/lib/webhookdb/api/saved_queries.rb +10 -3
  35. data/lib/webhookdb/api/saved_views.rb +99 -0
  36. data/lib/webhookdb/api/service_integrations.rb +15 -9
  37. data/lib/webhookdb/api/subscriptions.rb +3 -1
  38. data/lib/webhookdb/api/sync_targets.rb +9 -7
  39. data/lib/webhookdb/api/system.rb +1 -0
  40. data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
  41. data/lib/webhookdb/apps.rb +30 -7
  42. data/lib/webhookdb/async/audit_logger.rb +2 -0
  43. data/lib/webhookdb/async.rb +5 -0
  44. data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
  45. data/lib/webhookdb/backfill_job.rb +9 -0
  46. data/lib/webhookdb/customer.rb +5 -0
  47. data/lib/webhookdb/database_document.rb +1 -1
  48. data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
  49. data/lib/webhookdb/db_adapter.rb +20 -4
  50. data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
  51. data/lib/webhookdb/fixtures/organizations.rb +5 -0
  52. data/lib/webhookdb/fixtures/roles.rb +14 -0
  53. data/lib/webhookdb/fixtures/saved_views.rb +25 -0
  54. data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
  55. data/lib/webhookdb/http.rb +8 -2
  56. data/lib/webhookdb/icalendar.rb +3 -0
  57. data/lib/webhookdb/idempotency.rb +69 -22
  58. data/lib/webhookdb/increase.rb +69 -21
  59. data/lib/webhookdb/intercom.rb +10 -3
  60. data/lib/webhookdb/jobs/backfill.rb +3 -1
  61. data/lib/webhookdb/jobs/emailer.rb +0 -1
  62. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
  63. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
  64. data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
  65. data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
  66. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
  67. data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
  68. data/lib/webhookdb/message/body.rb +6 -4
  69. data/lib/webhookdb/message/delivery.rb +2 -0
  70. data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
  71. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
  72. data/lib/webhookdb/oauth/fake_provider.rb +44 -0
  73. data/lib/webhookdb/oauth/front_provider.rb +1 -2
  74. data/lib/webhookdb/oauth/increase_provider.rb +80 -0
  75. data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
  76. data/lib/webhookdb/oauth/session.rb +20 -0
  77. data/lib/webhookdb/oauth.rb +7 -21
  78. data/lib/webhookdb/organization/alerting.rb +2 -0
  79. data/lib/webhookdb/organization/database_migration.rb +3 -0
  80. data/lib/webhookdb/organization.rb +37 -6
  81. data/lib/webhookdb/organization_membership.rb +14 -7
  82. data/lib/webhookdb/postgres.rb +2 -0
  83. data/lib/webhookdb/replicator/base.rb +1 -0
  84. data/lib/webhookdb/replicator/docgen.rb +9 -1
  85. data/lib/webhookdb/replicator/fake.rb +2 -3
  86. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
  87. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
  88. data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
  89. data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
  90. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
  91. data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
  92. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
  93. data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
  94. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
  95. data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
  96. data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
  97. data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
  98. data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
  99. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
  100. data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
  101. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
  102. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
  103. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
  104. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
  105. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  106. data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
  107. data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
  108. data/lib/webhookdb/replicator/webhook_request.rb +4 -0
  109. data/lib/webhookdb/replicator.rb +8 -0
  110. data/lib/webhookdb/role.rb +5 -2
  111. data/lib/webhookdb/saved_query.rb +23 -0
  112. data/lib/webhookdb/saved_view.rb +73 -0
  113. data/lib/webhookdb/sentry.rb +2 -0
  114. data/lib/webhookdb/service/entities.rb +0 -4
  115. data/lib/webhookdb/service/helpers.rb +5 -0
  116. data/lib/webhookdb/service/middleware.rb +17 -0
  117. data/lib/webhookdb/service/types.rb +10 -8
  118. data/lib/webhookdb/service/validators.rb +1 -2
  119. data/lib/webhookdb/service/view_api.rb +1 -1
  120. data/lib/webhookdb/service_integration.rb +17 -15
  121. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
  122. data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
  123. data/lib/webhookdb/subscription.rb +2 -0
  124. data/lib/webhookdb/sync_target.rb +10 -2
  125. data/lib/webhookdb/tasks/message.rb +3 -1
  126. data/lib/webhookdb/version.rb +1 -1
  127. data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
  128. data/lib/webhookdb/webhook_subscription.rb +2 -0
  129. metadata +58 -9
  130. data/lib/webhookdb/admin_api/customers.rb +0 -63
  131. data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
  132. 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