webhookdb 1.2.1 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/admin-dist/assets/index-6aebf805.js +264 -0
- data/admin-dist/favicon.ico +0 -0
- data/admin-dist/index.html +130 -0
- data/admin-dist/manifest.json +15 -0
- data/data/messages/replicators/url-recorder.liquid +20 -0
- data/data/messages/templates/errors/signalwire_send_sms.email.liquid +31 -0
- data/data/messages/web/install-customer-login.liquid +6 -5
- data/data/messages/web/install-error.liquid +1 -1
- data/data/messages/web/install-forbidden.liquid +25 -0
- data/data/messages/web/install-org-chooser.liquid +40 -0
- data/data/messages/web/install-success.liquid +2 -1
- data/data/messages/web/install.liquid +2 -1
- data/data/messages/web/partials/head.liquid +2 -0
- data/data/messages/web/styles.liquid +24 -0
- data/db/migrations/040_saved_query_fix_unique.rb +17 -0
- data/db/migrations/041_views.rb +20 -0
- data/db/migrations/042_sint_lock.rb +10 -0
- data/db/migrations/043_text_search.rb +28 -0
- data/db/migrations/044_oauth_session_token_cache.rb +21 -0
- data/integration/auth_spec.rb +2 -2
- data/lib/sequel/plugins/text_searchable.rb +165 -0
- data/lib/sequel/text_searchable.rb +42 -0
- data/lib/webhookdb/admin_api/auth.rb +24 -3
- data/lib/webhookdb/admin_api/data_provider.rb +196 -0
- data/lib/webhookdb/admin_api/entities.rb +143 -28
- data/lib/webhookdb/admin_api.rb +0 -2
- data/lib/webhookdb/api/auth.rb +5 -6
- data/lib/webhookdb/api/db.rb +31 -6
- data/lib/webhookdb/api/entities.rb +7 -1
- data/lib/webhookdb/api/helpers.rb +6 -25
- data/lib/webhookdb/api/install.rb +204 -79
- data/lib/webhookdb/api/organizations.rb +14 -12
- data/lib/webhookdb/api/saved_queries.rb +10 -3
- data/lib/webhookdb/api/saved_views.rb +99 -0
- data/lib/webhookdb/api/service_integrations.rb +15 -9
- data/lib/webhookdb/api/subscriptions.rb +3 -1
- data/lib/webhookdb/api/sync_targets.rb +9 -7
- data/lib/webhookdb/api/system.rb +1 -0
- data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
- data/lib/webhookdb/apps.rb +30 -7
- data/lib/webhookdb/async/audit_logger.rb +2 -0
- data/lib/webhookdb/async.rb +5 -0
- data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
- data/lib/webhookdb/backfill_job.rb +9 -0
- data/lib/webhookdb/customer.rb +5 -0
- data/lib/webhookdb/database_document.rb +1 -1
- data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
- data/lib/webhookdb/db_adapter.rb +20 -4
- data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
- data/lib/webhookdb/fixtures/organizations.rb +5 -0
- data/lib/webhookdb/fixtures/roles.rb +14 -0
- data/lib/webhookdb/fixtures/saved_views.rb +25 -0
- data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
- data/lib/webhookdb/http.rb +8 -2
- data/lib/webhookdb/icalendar.rb +3 -0
- data/lib/webhookdb/idempotency.rb +69 -22
- data/lib/webhookdb/increase.rb +69 -21
- data/lib/webhookdb/intercom.rb +10 -3
- data/lib/webhookdb/jobs/backfill.rb +3 -1
- data/lib/webhookdb/jobs/emailer.rb +0 -1
- data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
- data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
- data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
- data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
- data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
- data/lib/webhookdb/message/body.rb +6 -4
- data/lib/webhookdb/message/delivery.rb +2 -0
- data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
- data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
- data/lib/webhookdb/oauth/fake_provider.rb +44 -0
- data/lib/webhookdb/oauth/front_provider.rb +1 -2
- data/lib/webhookdb/oauth/increase_provider.rb +80 -0
- data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
- data/lib/webhookdb/oauth/session.rb +20 -0
- data/lib/webhookdb/oauth.rb +7 -21
- data/lib/webhookdb/organization/alerting.rb +2 -0
- data/lib/webhookdb/organization/database_migration.rb +3 -0
- data/lib/webhookdb/organization.rb +37 -6
- data/lib/webhookdb/organization_membership.rb +14 -7
- data/lib/webhookdb/postgres.rb +2 -0
- data/lib/webhookdb/replicator/base.rb +1 -0
- data/lib/webhookdb/replicator/docgen.rb +9 -1
- data/lib/webhookdb/replicator/fake.rb +2 -3
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
- data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
- data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
- data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
- data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
- data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
- data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
- data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
- data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
- data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
- data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
- data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
- data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
- data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
- data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
- data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
- data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
- data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
- data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
- data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
- data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
- data/lib/webhookdb/replicator/webhook_request.rb +4 -0
- data/lib/webhookdb/replicator.rb +8 -0
- data/lib/webhookdb/role.rb +5 -2
- data/lib/webhookdb/saved_query.rb +23 -0
- data/lib/webhookdb/saved_view.rb +73 -0
- data/lib/webhookdb/sentry.rb +2 -0
- data/lib/webhookdb/service/entities.rb +0 -4
- data/lib/webhookdb/service/helpers.rb +5 -0
- data/lib/webhookdb/service/middleware.rb +17 -0
- data/lib/webhookdb/service/types.rb +10 -8
- data/lib/webhookdb/service/validators.rb +1 -2
- data/lib/webhookdb/service/view_api.rb +1 -1
- data/lib/webhookdb/service_integration.rb +17 -15
- data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
- data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
- data/lib/webhookdb/subscription.rb +2 -0
- data/lib/webhookdb/sync_target.rb +10 -2
- data/lib/webhookdb/tasks/message.rb +3 -1
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
- data/lib/webhookdb/webhook_subscription.rb +2 -0
- metadata +58 -9
- data/lib/webhookdb/admin_api/customers.rb +0 -63
- data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
- 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.
|
101
|
-
db = Webhookdb::Idempotency.
|
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
|
-
|
119
|
+
conn = Webhookdb::Idempotency.db
|
104
120
|
Webhookdb::Postgres.check_transaction(
|
105
|
-
|
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
|
128
|
+
db.ensure(self._key)
|
112
129
|
db.transaction do
|
113
|
-
idem_row = db
|
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
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
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
|
144
|
-
# created_at
|
145
|
-
# updated_at
|
146
|
-
# last_run
|
147
|
-
# key
|
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
|
+
# ----------------------------------------------------------------------------------------
|
data/lib/webhookdb/increase.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
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
|
-
|
18
|
-
request_data = request.body.read
|
29
|
+
def _defaults = {t: nil, v1: []}
|
19
30
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
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
|
-
#
|
32
|
-
|
33
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
data/lib/webhookdb/intercom.rb
CHANGED
@@ -12,9 +12,16 @@ module Webhookdb::Intercom
|
|
12
12
|
setting :page_size, 20
|
13
13
|
end
|
14
14
|
|
15
|
-
def self.
|
16
|
-
|
17
|
-
return
|
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
|
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
|
@@ -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.
|
25
|
+
self.logger.debug("enqueued_icalendar_sync", calendar_external_id:, enqueued_job_id:)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
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
|
-
"
|
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
|
-
|
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
|
17
|
-
# content | text
|
18
|
-
# mediatype | text
|
19
|
-
# delivery_id | integer
|
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
|
-
|
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
|
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 =
|
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
|