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.
- 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/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 +9 -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 +9 -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 +57 -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
|