webhookdb 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/data/messages/web/install-customer-login.liquid +1 -1
- data/data/messages/web/install-success.liquid +2 -2
- data/db/migrations/038_webhookdb_api_key.rb +13 -0
- data/db/migrations/039_saved_query.rb +17 -0
- data/lib/webhookdb/api/db.rb +2 -19
- data/lib/webhookdb/api/helpers.rb +27 -0
- data/lib/webhookdb/api/install.rb +23 -1
- data/lib/webhookdb/api/saved_queries.rb +219 -0
- data/lib/webhookdb/api/service_integrations.rb +17 -12
- data/lib/webhookdb/api/sync_targets.rb +2 -6
- data/lib/webhookdb/api/system.rb +13 -2
- data/lib/webhookdb/api/webhook_subscriptions.rb +12 -18
- data/lib/webhookdb/api.rb +11 -2
- data/lib/webhookdb/apps.rb +2 -0
- data/lib/webhookdb/email_octopus.rb +2 -0
- data/lib/webhookdb/envfixer.rb +29 -0
- data/lib/webhookdb/fixtures/saved_queries.rb +27 -0
- data/lib/webhookdb/fixtures/service_integrations.rb +4 -0
- data/lib/webhookdb/front.rb +23 -11
- data/lib/webhookdb/google_calendar.rb +6 -0
- data/lib/webhookdb/icalendar.rb +6 -0
- data/lib/webhookdb/idempotency.rb +94 -33
- data/lib/webhookdb/jobs/backfill.rb +24 -5
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +7 -1
- data/lib/webhookdb/jobs/prepare_database_connections.rb +2 -2
- data/lib/webhookdb/jobs/scheduled_backfills.rb +19 -13
- data/lib/webhookdb/oauth/{front.rb → front_provider.rb} +21 -4
- data/lib/webhookdb/oauth/{intercom.rb → intercom_provider.rb} +1 -1
- data/lib/webhookdb/oauth.rb +8 -7
- data/lib/webhookdb/organization/alerting.rb +11 -0
- data/lib/webhookdb/organization.rb +8 -2
- data/lib/webhookdb/postgres/model_utilities.rb +19 -0
- data/lib/webhookdb/postgres.rb +3 -0
- data/lib/webhookdb/replicator/column.rb +9 -1
- data/lib/webhookdb/replicator/front_conversation_v1.rb +5 -1
- data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +2 -5
- data/lib/webhookdb/replicator/front_message_v1.rb +5 -1
- data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +325 -0
- data/lib/webhookdb/replicator/front_v1_mixin.rb +9 -1
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +6 -3
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +28 -14
- data/lib/webhookdb/replicator/twilio_sms_v1.rb +3 -1
- data/lib/webhookdb/saved_query.rb +28 -0
- data/lib/webhookdb/service_integration.rb +36 -3
- data/lib/webhookdb/signalwire.rb +40 -0
- data/lib/webhookdb/spec_helpers/citest.rb +18 -9
- data/lib/webhookdb/spec_helpers/postgres.rb +9 -0
- data/lib/webhookdb/spec_helpers/service.rb +5 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +25 -13
- data/lib/webhookdb/spec_helpers/whdb.rb +7 -0
- data/lib/webhookdb/sync_target.rb +1 -1
- data/lib/webhookdb/tasks/specs.rb +4 -2
- data/lib/webhookdb/version.rb +1 -1
- data/lib/webhookdb.rb +24 -26
- metadata +39 -4
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faker"
|
4
|
+
|
5
|
+
require "webhookdb"
|
6
|
+
require "webhookdb/fixtures"
|
7
|
+
|
8
|
+
module Webhookdb::Fixtures::SavedQueries
|
9
|
+
extend Webhookdb::Fixtures
|
10
|
+
|
11
|
+
fixtured_class Webhookdb::SavedQuery
|
12
|
+
|
13
|
+
base :saved_query do
|
14
|
+
self.description ||= Faker::Lorem.sentence
|
15
|
+
self.sql ||= "SELECT * FROM mytable"
|
16
|
+
end
|
17
|
+
|
18
|
+
before_saving do |instance|
|
19
|
+
instance.organization ||= Webhookdb::Fixtures.organization.create
|
20
|
+
instance
|
21
|
+
end
|
22
|
+
|
23
|
+
decorator :created_by do |c={}|
|
24
|
+
c = Webhookdb::Fixtures.customer.create(c) unless c.is_a?(Webhookdb::Customer)
|
25
|
+
self.created_by = c
|
26
|
+
end
|
27
|
+
end
|
data/lib/webhookdb/front.rb
CHANGED
@@ -6,34 +6,44 @@ require "appydays/loggable"
|
|
6
6
|
module Webhookdb::Front
|
7
7
|
include Appydays::Configurable
|
8
8
|
|
9
|
+
CHANNEL_EVENT_TYPES = Set.new(["authorization", "delete", "message", "message_autoreply", "message_imported"])
|
10
|
+
|
9
11
|
configurable(:front) do
|
10
|
-
|
11
|
-
|
12
|
+
setting :http_timeout, 30
|
13
|
+
|
14
|
+
# WebhookDB App: App Secret from Basic Information tab in Front UI.
|
15
|
+
setting :app_secret, "front_api_secret", key: ["FRONT_APP_SECRET", "FRONT_API_SECRET"]
|
16
|
+
# WebhookDB App: Client ID from OAuth tab in Front UI.
|
12
17
|
setting :client_id, "front_client_id"
|
18
|
+
# WebhookDB App: Client Secret from OAuth tab in Front UI.
|
13
19
|
setting :client_secret, "front_client_secret"
|
14
|
-
setting :http_timeout, 30
|
15
|
-
end
|
16
20
|
|
17
|
-
|
21
|
+
setting :signalwire_channel_app_id, "front_swchan_app_id"
|
22
|
+
setting :signalwire_channel_app_secret, "front_swchan_app_secret"
|
23
|
+
setting :signalwire_channel_client_id, "front_swchan_client_id"
|
24
|
+
setting :signalwire_channel_client_secret, "front_swchan_client_secret"
|
25
|
+
|
26
|
+
setting :channel_sync_refreshness_cutoff, 48.hours.to_i
|
27
|
+
end
|
18
28
|
|
19
|
-
def self.verify_signature(request)
|
29
|
+
def self.verify_signature(request, secret)
|
20
30
|
request.body.rewind
|
21
31
|
body = request.body.read
|
22
32
|
base_string = "#{request.env['HTTP_X_FRONT_REQUEST_TIMESTAMP']}:#{body}"
|
23
|
-
calculated_signature = OpenSSL::HMAC.base64digest(OpenSSL::Digest.new("sha256"),
|
33
|
+
calculated_signature = OpenSSL::HMAC.base64digest(OpenSSL::Digest.new("sha256"), secret, base_string)
|
24
34
|
return calculated_signature == request.env["HTTP_X_FRONT_SIGNATURE"]
|
25
35
|
end
|
26
36
|
|
27
|
-
def self.webhook_response(request)
|
37
|
+
def self.webhook_response(request, secret)
|
28
38
|
return Webhookdb::WebhookResponse.error("missing signature") unless request.env["HTTP_X_FRONT_SIGNATURE"]
|
29
39
|
|
30
|
-
from_front =
|
40
|
+
from_front = self.verify_signature(request, secret)
|
31
41
|
return Webhookdb::WebhookResponse.ok(status: 200) if from_front
|
32
42
|
return Webhookdb::WebhookResponse.error("invalid signature")
|
33
43
|
end
|
34
44
|
|
35
|
-
def self.initial_verification_request_response(request)
|
36
|
-
from_front = self.verify_signature(request)
|
45
|
+
def self.initial_verification_request_response(request, secret)
|
46
|
+
from_front = self.verify_signature(request, secret)
|
37
47
|
if from_front
|
38
48
|
return Webhookdb::WebhookResponse.ok(
|
39
49
|
json: {challenge: request.env["HTTP_X_FRONT_CHALLENGE"]},
|
@@ -46,4 +56,6 @@ module Webhookdb::Front
|
|
46
56
|
def self.auth_headers(token)
|
47
57
|
return {"Authorization" => "Bearer #{token}"}
|
48
58
|
end
|
59
|
+
|
60
|
+
def self.channel_jwt_jti = SecureRandom.hex(4)
|
49
61
|
end
|
@@ -9,6 +9,12 @@ module Webhookdb::GoogleCalendar
|
|
9
9
|
# How many calendars/events should we fetch in a single page?
|
10
10
|
# Higher uses slightly more memory but fewer API calls.
|
11
11
|
# Max of 2500.
|
12
|
+
#
|
13
|
+
# **NOTE:** Changing this in production will
|
14
|
+
# INVALIDATE EXISTING RESOURCES CAUSING A FULL RESYNC.
|
15
|
+
# You should, in general, avoid modifying this number once there is
|
16
|
+
# much Google Calendar data stored. Instead, page sizes will be automatically reduced
|
17
|
+
# as requests time out.
|
12
18
|
setting :list_page_size, 2000
|
13
19
|
# How many rows should we upsert at a time?
|
14
20
|
# Higher is fewer upserts, but can create very large SQL strings,
|
data/lib/webhookdb/icalendar.rb
CHANGED
@@ -13,5 +13,11 @@ module Webhookdb::Icalendar
|
|
13
13
|
# Do not store events older then this when syncing recurring events.
|
14
14
|
# Many icalendar feeds are misconfigured and this prevents enumerating 2000+ years of recurrence.
|
15
15
|
setting :oldest_recurring_event, "1990-01-01", convert: ->(s) { Date.parse(s) }
|
16
|
+
# Sync icalendar calendars only this often.
|
17
|
+
# Most services only update every day or so. Assume it takes 5s to sync each feed (request, parse, upsert).
|
18
|
+
# If you have 10,000 feeds, that is 50,000 seconds, or almost 14 hours of processing time,
|
19
|
+
# or two threads for 7 hours. The resyncs are spread out across the sync period
|
20
|
+
# (ie, no thundering herd every 8 hours), but it is still a good idea to sync as infrequently as possible.
|
21
|
+
setting :sync_period_hours, 6
|
16
22
|
end
|
17
23
|
end
|
@@ -26,51 +26,112 @@ require "webhookdb/postgres/model"
|
|
26
26
|
# usually using the :no_transaction_check spec metadata.
|
27
27
|
#
|
28
28
|
class Webhookdb::Idempotency < Webhookdb::Postgres::Model(:idempotencies)
|
29
|
-
extend Webhookdb::MethodUtilities
|
30
|
-
|
31
29
|
NOOP = :skipped
|
32
30
|
|
33
|
-
|
34
|
-
|
31
|
+
class << self
|
32
|
+
# Skip the transaction check. Useful in unit tests. See class docs for details.
|
33
|
+
attr_accessor :skip_transaction_check
|
35
34
|
|
36
|
-
|
37
|
-
idem = self.new
|
38
|
-
idem.__once_ever = true
|
39
|
-
return idem
|
40
|
-
end
|
35
|
+
def skip_transaction_check? = self.skip_transaction_check
|
41
36
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
37
|
+
# @return [Builder]
|
38
|
+
def once_ever
|
39
|
+
b = self::Builder.new
|
40
|
+
b._once_ever = true
|
41
|
+
return b
|
42
|
+
end
|
47
43
|
|
48
|
-
|
44
|
+
# @return [Builder]
|
45
|
+
def every(interval)
|
46
|
+
b = self::Builder.new
|
47
|
+
b._every = interval
|
48
|
+
return b
|
49
|
+
end
|
49
50
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
51
|
+
def separate_connection
|
52
|
+
@connection ||= Sequel.connect(
|
53
|
+
uri,
|
54
|
+
logger: self.logger,
|
55
|
+
extensions: [
|
56
|
+
:connection_validator,
|
57
|
+
:pg_json, # Must have this to mirror the main model DB
|
58
|
+
],
|
59
|
+
**Webhookdb::Dbutil.configured_connection_options,
|
60
|
+
)
|
61
|
+
return @connection
|
62
|
+
end
|
54
63
|
end
|
55
64
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
65
|
+
class Builder
|
66
|
+
attr_accessor :_every, :_once_ever, :_stored, :_sepconn, :_key
|
67
|
+
|
68
|
+
# If set, the result of block is stored as JSON,
|
69
|
+
# and returned when an idempotent call is made.
|
70
|
+
# The JSON value (as_json) is returned from the block in all cases.
|
71
|
+
# @return [Builder]
|
72
|
+
def stored
|
73
|
+
self._stored = true
|
74
|
+
return self
|
75
|
+
end
|
61
76
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
77
|
+
# Run the idempotency on a separate connection.
|
78
|
+
# Allows use of idempotency within an existing transaction block,
|
79
|
+
# which is normally not allowed. Usually should be used with #stored,
|
80
|
+
# since otherwise the result of the idempotency will be lost.
|
81
|
+
#
|
82
|
+
# NOTE: When calling code with using_seperate_connection,
|
83
|
+
# you may want to use the spec metadata `truncate: Webhookdb::Idempotency`
|
84
|
+
# since the row won't be covered by the spec's transaction.
|
85
|
+
#
|
86
|
+
# @return [Builder]
|
87
|
+
def using_seperate_connection
|
88
|
+
self._sepconn = true
|
89
|
+
return self
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Builder]
|
93
|
+
def under_key(key, &block)
|
94
|
+
self._key = key
|
95
|
+
return self.execute(&block) if block
|
96
|
+
return self
|
97
|
+
end
|
98
|
+
|
99
|
+
def execute
|
100
|
+
if self._sepconn
|
101
|
+
db = Webhookdb::Idempotency.separate_connection
|
102
|
+
else
|
103
|
+
db = Webhookdb::Idempotency.db
|
104
|
+
Webhookdb::Postgres.check_transaction(
|
105
|
+
db,
|
106
|
+
"Cannot use idempotency while already in a transaction, since side effects may not be idempotent. " \
|
107
|
+
"You can chain withusing_seperate_connection to run the idempotency itself separately.",
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
db[:idempotencies].insert_conflict.insert(key: self._key)
|
112
|
+
db.transaction do
|
113
|
+
idem_row = db[:idempotencies].where(key: self._key).for_update.first
|
114
|
+
if idem_row.fetch(:last_run).nil?
|
115
|
+
result = yield()
|
116
|
+
result = self._update_row(db, result)
|
117
|
+
return result
|
118
|
+
end
|
119
|
+
noop_result = self._stored ? idem_row.fetch(:stored_result) : NOOP
|
120
|
+
return noop_result if self._once_ever
|
121
|
+
return noop_result if Time.now < (idem_row[:last_run] + self._every)
|
66
122
|
result = yield()
|
67
|
-
|
123
|
+
result = self._update_row(db, result)
|
68
124
|
return result
|
69
125
|
end
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
126
|
+
end
|
127
|
+
|
128
|
+
def _update_row(db, result)
|
129
|
+
updates = {last_run: Time.now}
|
130
|
+
if self._stored
|
131
|
+
result = result.as_json
|
132
|
+
updates[:stored_result] = Sequel.pg_jsonb_wrap(result)
|
133
|
+
end
|
134
|
+
db[:idempotencies].where(key: self._key).update(updates)
|
74
135
|
return result
|
75
136
|
end
|
76
137
|
end
|
@@ -19,13 +19,32 @@ class Webhookdb::Jobs::Backfill
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def _perform(event)
|
22
|
-
|
22
|
+
begin
|
23
|
+
bfjob = self.lookup_model(Webhookdb::BackfillJob, event.payload)
|
24
|
+
rescue RuntimeError => e
|
25
|
+
self.logger.info "skipping_missing_backfill_job", error: e
|
26
|
+
return
|
27
|
+
end
|
23
28
|
sint = bfjob.service_integration
|
24
29
|
self.with_log_tags(sint.log_tags.merge(backfill_job_id: bfjob.opaque_id)) do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
sint.db.transaction do
|
31
|
+
unless bfjob.lock?
|
32
|
+
self.logger.info "skipping_locked_backfill_job"
|
33
|
+
break
|
34
|
+
end
|
35
|
+
bfjob.refresh
|
36
|
+
if bfjob.finished?
|
37
|
+
self.logger.info "skipping_finished_backfill_job"
|
38
|
+
break
|
39
|
+
end
|
40
|
+
begin
|
41
|
+
sint.replicator.backfill(bfjob)
|
42
|
+
rescue Webhookdb::Replicator::CredentialsMissing
|
43
|
+
# The credentials could have been cleared out, so just finish this job.
|
44
|
+
self.logger.info "skipping_backfill_job_without_credentials"
|
45
|
+
bfjob.update(finished_at: Time.now)
|
46
|
+
break
|
47
|
+
end
|
29
48
|
end
|
30
49
|
end
|
31
50
|
end
|
@@ -3,6 +3,10 @@
|
|
3
3
|
require "webhookdb/async/scheduled_job"
|
4
4
|
require "webhookdb/jobs"
|
5
5
|
|
6
|
+
# For every IcalendarCalendar row needing a sync (across all service integrations),
|
7
|
+
# enqueue a +Webhookdb::Jobs::IcalendarSync+ job.
|
8
|
+
# Jobs are 'splayed' over 1/4 of the configured calendar sync period (see +Webhookdb::Icalendar+)
|
9
|
+
# to avoid a thundering herd.
|
6
10
|
class Webhookdb::Jobs::IcalendarEnqueueSyncs
|
7
11
|
extend Webhookdb::Async::ScheduledJob
|
8
12
|
|
@@ -10,12 +14,14 @@ class Webhookdb::Jobs::IcalendarEnqueueSyncs
|
|
10
14
|
splay 30
|
11
15
|
|
12
16
|
def _perform
|
17
|
+
max_splay = Webhookdb::Icalendar.sync_period_hours.hours.to_i / 4
|
13
18
|
Webhookdb::ServiceIntegration.dataset.where_each(service_name: "icalendar_calendar_v1") do |sint|
|
14
19
|
sint.replicator.admin_dataset do |ds|
|
15
20
|
sint.replicator.rows_needing_sync(ds).each do |row|
|
16
21
|
calendar_external_id = row.fetch(:external_id)
|
17
22
|
self.with_log_tags(sint.log_tags) do
|
18
|
-
|
23
|
+
splay = rand(1..max_splay)
|
24
|
+
enqueued_job_id = Webhookdb::Jobs::IcalendarSync.perform_in(splay, sint.id, calendar_external_id)
|
19
25
|
self.logger.info("enqueued_icalendar_sync", calendar_external_id:, enqueued_job_id:)
|
20
26
|
end
|
21
27
|
end
|
@@ -15,8 +15,8 @@ class Webhookdb::Jobs::PrepareDatabaseConnections
|
|
15
15
|
org.db.transaction do
|
16
16
|
# If creating the public host fails, we end up with an orphaned database,
|
17
17
|
# but that's not a big deal- we can eventually see it's empty/unlinked and drop it.
|
18
|
-
org.prepare_database_connections
|
19
|
-
org.create_public_host_cname
|
18
|
+
org.prepare_database_connections(safe: true)
|
19
|
+
org.create_public_host_cname(safe: true)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -1,10 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "webhookdb/email_octopus"
|
4
|
+
require "webhookdb/github"
|
5
|
+
require "webhookdb/sponsy"
|
6
|
+
require "webhookdb/transistor"
|
7
|
+
|
3
8
|
module Webhookdb::Jobs
|
4
9
|
# Create a single way to do the common task of automatic scheduled backfills.
|
5
10
|
# Each integration that needs automated backfills can add a specification here.
|
6
11
|
module ScheduledBackfills
|
7
|
-
Spec = Struct.new(:klass, :service_name, :cron_expr, :splay, :incremental)
|
12
|
+
Spec = Struct.new(:klass, :service_name, :cron_expr, :splay, :incremental, :recursive)
|
8
13
|
|
9
14
|
# @param spec [Spec]
|
10
15
|
def self.install(spec)
|
@@ -16,7 +21,8 @@ module Webhookdb::Jobs
|
|
16
21
|
|
17
22
|
define_method(:_perform) do
|
18
23
|
Webhookdb::ServiceIntegration.dataset.where_each(service_name: spec.service_name) do |sint|
|
19
|
-
|
24
|
+
m = spec.recursive ? :create_recursive : :create
|
25
|
+
Webhookdb::BackfillJob.send(m, service_integration: sint, incremental: spec.incremental).enqueue
|
20
26
|
end
|
21
27
|
end
|
22
28
|
end
|
@@ -26,19 +32,19 @@ module Webhookdb::Jobs
|
|
26
32
|
[
|
27
33
|
Spec.new(
|
28
34
|
"ConvertkitBroadcastBackfill", "convertkit_broadcast_v1",
|
29
|
-
"10 * * * *", 2.minutes, false,
|
35
|
+
"10 * * * *", 2.minutes, false, false,
|
30
36
|
),
|
31
37
|
Spec.new(
|
32
38
|
"ConvertkitSubscriberBackfill", "convertkit_subscriber_v1",
|
33
|
-
"20 * * * *", 2.minutes, true,
|
39
|
+
"20 * * * *", 2.minutes, true, false,
|
34
40
|
),
|
35
41
|
Spec.new(
|
36
42
|
"ConvertkitTagBackfill", "convertkit_tag_v1",
|
37
|
-
"30 * * * *", 2.minutes, false,
|
43
|
+
"30 * * * *", 2.minutes, false, false,
|
38
44
|
),
|
39
45
|
Spec.new(
|
40
46
|
"EmailOctopusScheduledBackfill", "email_octopus_list_v1",
|
41
|
-
Webhookdb::EmailOctopus.cron_expression, 2.minutes, false,
|
47
|
+
Webhookdb::EmailOctopus.cron_expression, 2.minutes, false, true,
|
42
48
|
),
|
43
49
|
Spec.new(
|
44
50
|
"GithubRepoActivityScheduledBackfill", "github_repository_event_v1",
|
@@ -46,31 +52,31 @@ module Webhookdb::Jobs
|
|
46
52
|
),
|
47
53
|
Spec.new(
|
48
54
|
"IntercomScheduledBackfill", "intercom_marketplace_root_v1",
|
49
|
-
"*/1 * * * *", 0, false,
|
55
|
+
"*/1 * * * *", 0, false, true,
|
50
56
|
),
|
51
57
|
Spec.new(
|
52
58
|
"AtomSingleFeedPoller", "atom_single_feed_v1",
|
53
|
-
"11 * * * *", 10.seconds, true,
|
59
|
+
"11 * * * *", 10.seconds, true, false,
|
54
60
|
),
|
55
61
|
Spec.new(
|
56
62
|
"SponsyScheduledBackfill", "sponsy_publication_v1",
|
57
|
-
Webhookdb::Sponsy.cron_expression, 30.seconds, true,
|
63
|
+
Webhookdb::Sponsy.cron_expression, 30.seconds, true, true,
|
58
64
|
),
|
59
65
|
Spec.new(
|
60
66
|
"TransistorEpisodeBackfill", "transistor_episode_v1",
|
61
|
-
Webhookdb::Transistor.episode_cron_expression, 2.minutes, true,
|
67
|
+
Webhookdb::Transistor.episode_cron_expression, 2.minutes, true, true,
|
62
68
|
),
|
63
69
|
Spec.new(
|
64
70
|
"TransistorShowBackfill", "transistor_show_v1",
|
65
|
-
Webhookdb::Transistor.show_cron_expression, 2.minutes, true,
|
71
|
+
Webhookdb::Transistor.show_cron_expression, 2.minutes, true, false,
|
66
72
|
),
|
67
73
|
Spec.new(
|
68
74
|
"TwilioSmsBackfill", "twilio_sms_v1",
|
69
|
-
"*/1 * * * *", 0, true,
|
75
|
+
"*/1 * * * *", 0, true, false,
|
70
76
|
),
|
71
77
|
Spec.new(
|
72
78
|
"SignalwireMessageBackfill", "signalwire_message_v1",
|
73
|
-
"*/1 * * * *", 0, true,
|
79
|
+
"*/1 * * * *", 0, true, false,
|
74
80
|
),
|
75
81
|
].each { |sp| self.install(sp) }
|
76
82
|
end
|
@@ -2,29 +2,35 @@
|
|
2
2
|
|
3
3
|
require "webhookdb/front"
|
4
4
|
|
5
|
-
class Webhookdb::Oauth::
|
5
|
+
class Webhookdb::Oauth::FrontProvider < Webhookdb::Oauth::Provider
|
6
6
|
include Appydays::Loggable
|
7
7
|
|
8
|
+
# Override these for custom OAuth of different apps
|
8
9
|
def key = "front"
|
9
10
|
def app_name = "Front"
|
11
|
+
def client_id = Webhookdb::Front.client_id
|
12
|
+
def client_secret = Webhookdb::Front.client_secret
|
13
|
+
|
10
14
|
def requires_webhookdb_auth? = true
|
11
15
|
def supports_webhooks? = true
|
12
16
|
|
13
17
|
def authorization_url(state:)
|
14
|
-
return "https://app.frontapp.com/oauth/authorize?response_type=code&redirect_uri=#{
|
18
|
+
return "https://app.frontapp.com/oauth/authorize?response_type=code&redirect_uri=#{self.callback_url}&state=#{state}&client_id=#{self.client_id}"
|
15
19
|
end
|
16
20
|
|
21
|
+
def callback_url = Webhookdb.api_url + "/v1/install/#{self.key}/callback"
|
22
|
+
|
17
23
|
def exchange_authorization_code(code:)
|
18
24
|
token = Webhookdb::Http.post(
|
19
25
|
"https://app.frontapp.com/oauth/token",
|
20
26
|
{
|
21
27
|
"code" => code,
|
22
|
-
"redirect_uri" =>
|
28
|
+
"redirect_uri" => self.callback_url,
|
23
29
|
"grant_type" => "authorization_code",
|
24
30
|
},
|
25
31
|
logger: self.logger,
|
26
32
|
timeout: Webhookdb::Front.http_timeout,
|
27
|
-
basic_auth: {username:
|
33
|
+
basic_auth: {username: self.client_id, password: self.client_secret},
|
28
34
|
)
|
29
35
|
return Webhookdb::Oauth::Tokens.new(
|
30
36
|
access_token: token.parsed_response["access_token"],
|
@@ -56,3 +62,14 @@ class Webhookdb::Oauth::Front < Webhookdb::Oauth::Provider
|
|
56
62
|
root_sint.replicator.build_dependents
|
57
63
|
end
|
58
64
|
end
|
65
|
+
|
66
|
+
class Webhookdb::Oauth::FrontSignalwireChannelProvider < Webhookdb::Oauth::FrontProvider
|
67
|
+
def key = "front_signalwire"
|
68
|
+
def app_name = "Front Signalwire Channel"
|
69
|
+
def client_id = Webhookdb::Front.signalwire_channel_client_id
|
70
|
+
def client_secret = Webhookdb::Front.signalwire_channel_client_secret
|
71
|
+
|
72
|
+
def build_marketplace_integrations(*)
|
73
|
+
raise NotImplementedError, "this should not be called, Front channels have a different setup"
|
74
|
+
end
|
75
|
+
end
|
data/lib/webhookdb/oauth.rb
CHANGED
@@ -56,9 +56,9 @@ module Webhookdb::Oauth
|
|
56
56
|
# rubocop:enable Lint/UnusedMethodArgument
|
57
57
|
|
58
58
|
class << self
|
59
|
-
|
60
|
-
|
61
|
-
raise "#{key} already registered to #{cls}" if self.registry.include?(key)
|
59
|
+
def register(cls)
|
60
|
+
key = cls.new.key
|
61
|
+
raise KeyError, "#{key} already registered to #{cls}" if self.registry.include?(key)
|
62
62
|
self.registry[key] = cls
|
63
63
|
end
|
64
64
|
|
@@ -74,7 +74,8 @@ module Webhookdb::Oauth
|
|
74
74
|
end
|
75
75
|
end
|
76
76
|
|
77
|
-
require "webhookdb/oauth/
|
78
|
-
Webhookdb::Oauth.register(Webhookdb::Oauth::
|
79
|
-
|
80
|
-
|
77
|
+
require "webhookdb/oauth/front_provider"
|
78
|
+
Webhookdb::Oauth.register(Webhookdb::Oauth::FrontProvider)
|
79
|
+
Webhookdb::Oauth.register(Webhookdb::Oauth::FrontSignalwireChannelProvider)
|
80
|
+
require "webhookdb/oauth/intercom_provider"
|
81
|
+
Webhookdb::Oauth.register(Webhookdb::Oauth::IntercomProvider)
|
@@ -7,7 +7,12 @@ class Webhookdb::Organization::Alerting
|
|
7
7
|
include Appydays::Configurable
|
8
8
|
|
9
9
|
configurable(:alerting) do
|
10
|
+
# Only send an alert with a given signature (replicator and error signature)
|
11
|
+
# to a given customer this often. Avoid spamming about any single replicator issue.
|
10
12
|
setting :interval, 24.hours.to_i
|
13
|
+
# Each customer can only receive this many alerts for a given template per day.
|
14
|
+
# Avoids spamming a customer when many rows of a replicator have problems.
|
15
|
+
setting :max_alerts_per_customer_per_day, 15
|
11
16
|
end
|
12
17
|
|
13
18
|
attr_reader :org
|
@@ -25,9 +30,15 @@ class Webhookdb::Organization::Alerting
|
|
25
30
|
"which is a unique identity for this error type, used for grouping and idempotency"
|
26
31
|
end
|
27
32
|
signature = message_template.signature
|
33
|
+
max_alerts_per_customer_per_day = Webhookdb::Organization::Alerting.max_alerts_per_customer_per_day
|
28
34
|
self.org.admin_customers.each do |c|
|
29
35
|
idemkey = "orgalert-#{signature}-#{c.id}"
|
30
36
|
Webhookdb::Idempotency.every(Webhookdb::Organization::Alerting.interval).under_key(idemkey) do
|
37
|
+
sent_last_day = Webhookdb::Message::Delivery.
|
38
|
+
where(template: message_template.full_template_name, recipient: c).
|
39
|
+
limit(max_alerts_per_customer_per_day).
|
40
|
+
count
|
41
|
+
next unless sent_last_day < max_alerts_per_customer_per_day
|
31
42
|
message_template.dispatch_email(c)
|
32
43
|
end
|
33
44
|
end
|
@@ -34,6 +34,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
34
34
|
adder: ->(om) { om.update(organization_id: id, verified: false) },
|
35
35
|
order: :id
|
36
36
|
one_to_many :service_integrations, class: "Webhookdb::ServiceIntegration", order: :id
|
37
|
+
one_to_many :saved_queries, class: "Webhookdb::SavedQuery", order: :id
|
37
38
|
one_to_many :webhook_subscriptions, class: "Webhookdb::WebhookSubscription", order: :id
|
38
39
|
many_to_many :feature_roles, class: "Webhookdb::Role", join_table: :feature_roles_organizations, right_key: :role_id
|
39
40
|
one_to_many :all_webhook_subscriptions,
|
@@ -204,6 +205,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
204
205
|
end
|
205
206
|
|
206
207
|
# Build the org-specific users, database, and set our connection URLs to it.
|
208
|
+
# @param safe [*] If true, noop if connection urls are set.
|
207
209
|
def prepare_database_connections(safe: false)
|
208
210
|
self.db.transaction do
|
209
211
|
self.lock!
|
@@ -220,13 +222,17 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
220
222
|
end
|
221
223
|
|
222
224
|
# Create a CNAME in Cloudflare for the currently configured connection urls.
|
223
|
-
|
225
|
+
# @param safe [*] If true, noop if the public host is set.
|
226
|
+
def create_public_host_cname(safe: false)
|
224
227
|
self.db.transaction do
|
225
228
|
self.lock!
|
226
229
|
# We must have a host to create a CNAME to.
|
227
230
|
raise Webhookdb::InvalidPrecondition, "connection urls must be set" if self.readonly_connection_url_raw.blank?
|
228
231
|
# Should only be used once when creating the org DBs.
|
229
|
-
|
232
|
+
if self.public_host.present?
|
233
|
+
return if safe
|
234
|
+
raise Webhookdb::InvalidPrecondition, "public_host must not be set"
|
235
|
+
end
|
230
236
|
# Use the raw URL, even though we know at this point
|
231
237
|
# public_host is empty so raw and public host urls are the same.
|
232
238
|
Webhookdb::Organization::DbBuilder.new(self).create_public_host_cname(self.readonly_connection_url_raw)
|
@@ -290,6 +290,25 @@ module Webhookdb::Postgres::ModelUtilities
|
|
290
290
|
end
|
291
291
|
end
|
292
292
|
|
293
|
+
# Run a SELECT FOR UPDATE SKIP LOCKED.
|
294
|
+
# If the model row is already locked, return false.
|
295
|
+
# Otherwise, acquire the lock and return true.
|
296
|
+
#
|
297
|
+
# If the lock is acquired, callers may want to refresh the receiver to make sure it has the newest values.
|
298
|
+
#
|
299
|
+
# @raise [Webhookdb::LockFailed] if the code is not currently in a transaction.
|
300
|
+
def lock?
|
301
|
+
raise Webhookdb::LockFailed, "must be in a transaction" unless self.db.in_transaction?
|
302
|
+
pk = self[self.class.primary_key]
|
303
|
+
got_lock = self.class.dataset.
|
304
|
+
select(1).
|
305
|
+
where(self.class.primary_key => pk).
|
306
|
+
for_update.
|
307
|
+
skip_locked.
|
308
|
+
first
|
309
|
+
return !got_lock.nil?
|
310
|
+
end
|
311
|
+
|
293
312
|
# Round +Time+ t to remove nanoseconds, since Postgres can only store microseconds.
|
294
313
|
protected def round_time(t)
|
295
314
|
return nil if t.nil?
|
data/lib/webhookdb/postgres.rb
CHANGED
@@ -60,6 +60,7 @@ module Webhookdb::Postgres
|
|
60
60
|
"webhookdb/organization/database_migration",
|
61
61
|
"webhookdb/organization_membership",
|
62
62
|
"webhookdb/role",
|
63
|
+
"webhookdb/saved_query",
|
63
64
|
"webhookdb/service_integration",
|
64
65
|
"webhookdb/subscription",
|
65
66
|
"webhookdb/sync_target",
|
@@ -120,11 +121,13 @@ module Webhookdb::Postgres
|
|
120
121
|
end
|
121
122
|
|
122
123
|
def self.run_all_migrations(target: nil)
|
124
|
+
# :nocov:
|
123
125
|
Sequel.extension :migration
|
124
126
|
Webhookdb::Postgres.each_model_superclass do |cls|
|
125
127
|
cls.install_all_extensions
|
126
128
|
Sequel::Migrator.run(cls.db, Pathname(__FILE__).dirname.parent.parent + "db/migrations", target:)
|
127
129
|
end
|
130
|
+
# :nocov:
|
128
131
|
end
|
129
132
|
|
130
133
|
# We can always register the models right away, since it does not have a side effect.
|