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
|
@@ -22,3 +22,23 @@ class Webhookdb::Oauth::Session < Webhookdb::Postgres::Model(:oauth_sessions)
|
|
|
22
22
|
}
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
# Table: oauth_sessions
|
|
27
|
+
# -------------------------------------------------------------------------------------------------------
|
|
28
|
+
# Columns:
|
|
29
|
+
# id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
|
|
30
|
+
# created_at | timestamp with time zone | NOT NULL DEFAULT now()
|
|
31
|
+
# customer_id | integer |
|
|
32
|
+
# organization_id | integer |
|
|
33
|
+
# user_agent | text | NOT NULL
|
|
34
|
+
# peer_ip | inet | NOT NULL
|
|
35
|
+
# oauth_state | text | NOT NULL
|
|
36
|
+
# authorization_code | text |
|
|
37
|
+
# used_at | timestamp with time zone |
|
|
38
|
+
# Indexes:
|
|
39
|
+
# oauth_sessions_pkey | PRIMARY KEY btree (id)
|
|
40
|
+
# oauth_sessions_customer_id_index | btree (customer_id)
|
|
41
|
+
# Foreign key constraints:
|
|
42
|
+
# oauth_sessions_customer_id_fkey | (customer_id) REFERENCES customers(id) ON DELETE CASCADE
|
|
43
|
+
# oauth_sessions_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
|
44
|
+
# -------------------------------------------------------------------------------------------------------
|
data/lib/webhookdb/oauth.rb
CHANGED
|
@@ -11,7 +11,6 @@ module Webhookdb::Oauth
|
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
# rubocop:disable Lint/UnusedMethodArgument
|
|
15
14
|
class Provider
|
|
16
15
|
# @return [String] Unique key to identify the provider.
|
|
17
16
|
def key = raise NotImplementedError
|
|
@@ -19,13 +18,6 @@ module Webhookdb::Oauth
|
|
|
19
18
|
# @return [String] Name of the app to present to users.
|
|
20
19
|
def app_name = raise NotImplementedError
|
|
21
20
|
|
|
22
|
-
# True if auth with this provider requires the user auth in WebhookDB,
|
|
23
|
-
# false if we can get their email from the Oauth process.
|
|
24
|
-
# If the access token can be used to get the 'me' user,
|
|
25
|
-
# we can usually use their email for the customer,
|
|
26
|
-
# but this may not be possible for some integrations.
|
|
27
|
-
def requires_webhookdb_auth? = raise NotImplementedError
|
|
28
|
-
|
|
29
21
|
# This is similar to `supports_webhooks` in the Replicator descriptors,
|
|
30
22
|
# except that this is used to make the success page dynamic.
|
|
31
23
|
# True if this provider's integrations support webhooks
|
|
@@ -39,22 +31,12 @@ module Webhookdb::Oauth
|
|
|
39
31
|
# @return [Webhookdb::Oauth::Tokens]
|
|
40
32
|
def exchange_authorization_code(code:) = raise NotImplementedError
|
|
41
33
|
|
|
42
|
-
# @param tokens [Webhookdb::Oauth::Tokens]
|
|
43
|
-
# @param scope [Hash] Used to store data needed in later calls, like when building integrations.
|
|
44
|
-
# @return [Array{TrueClass, FalseClass, Webhookdb::Customer}]
|
|
45
|
-
def find_or_create_customer(tokens:, scope:)
|
|
46
|
-
raise RuntimeError("should not be called") if self.requires_webhookdb_auth?
|
|
47
|
-
raise NotImplementedError
|
|
48
|
-
end
|
|
49
|
-
|
|
50
34
|
# Create the actual service integrations for the given org.
|
|
51
35
|
# @param organization [Webhookdb::Organization]
|
|
52
36
|
# @param tokens [Webhookdb::Oauth::Tokens]
|
|
53
|
-
#
|
|
54
|
-
def build_marketplace_integrations(organization:, tokens
|
|
55
|
-
|
|
56
|
-
# rubocop:enable Lint/UnusedMethodArgument
|
|
57
|
-
|
|
37
|
+
# @return [Webhookdb::ServiceIntegration]
|
|
38
|
+
def build_marketplace_integrations(organization:, tokens:) = raise NotImplementedError
|
|
39
|
+
end
|
|
58
40
|
class << self
|
|
59
41
|
def register(cls)
|
|
60
42
|
key = cls.new.key
|
|
@@ -74,8 +56,12 @@ module Webhookdb::Oauth
|
|
|
74
56
|
end
|
|
75
57
|
end
|
|
76
58
|
|
|
59
|
+
require "webhookdb/oauth/fake_provider"
|
|
60
|
+
Webhookdb::Oauth.register(Webhookdb::Oauth::FakeProvider)
|
|
77
61
|
require "webhookdb/oauth/front_provider"
|
|
78
62
|
Webhookdb::Oauth.register(Webhookdb::Oauth::FrontProvider)
|
|
79
63
|
Webhookdb::Oauth.register(Webhookdb::Oauth::FrontSignalwireChannelProvider)
|
|
64
|
+
require "webhookdb/oauth/increase_provider"
|
|
65
|
+
Webhookdb::Oauth.register(Webhookdb::Oauth::IncreaseProvider)
|
|
80
66
|
require "webhookdb/oauth/intercom_provider"
|
|
81
67
|
Webhookdb::Oauth.register(Webhookdb::Oauth::IntercomProvider)
|
|
@@ -31,11 +31,13 @@ class Webhookdb::Organization::Alerting
|
|
|
31
31
|
end
|
|
32
32
|
signature = message_template.signature
|
|
33
33
|
max_alerts_per_customer_per_day = Webhookdb::Organization::Alerting.max_alerts_per_customer_per_day
|
|
34
|
+
yesterday = Time.now - 24.hours
|
|
34
35
|
self.org.admin_customers.each do |c|
|
|
35
36
|
idemkey = "orgalert-#{signature}-#{c.id}"
|
|
36
37
|
Webhookdb::Idempotency.every(Webhookdb::Organization::Alerting.interval).under_key(idemkey) do
|
|
37
38
|
sent_last_day = Webhookdb::Message::Delivery.
|
|
38
39
|
where(template: message_template.full_template_name, recipient: c).
|
|
40
|
+
where { created_at > yesterday }.
|
|
39
41
|
limit(max_alerts_per_customer_per_day).
|
|
40
42
|
count
|
|
41
43
|
next unless sent_last_day < max_alerts_per_customer_per_day
|
|
@@ -7,6 +7,7 @@ class Webhookdb::Organization::DatabaseMigration < Webhookdb::Postgres::Model(:o
|
|
|
7
7
|
class MigrationAlreadyFinished < StandardError; end
|
|
8
8
|
|
|
9
9
|
plugin :timestamps
|
|
10
|
+
plugin :text_searchable, terms: [:organization, :started_by]
|
|
10
11
|
plugin :column_encryption do |enc|
|
|
11
12
|
enc.column :source_admin_connection_url
|
|
12
13
|
enc.column :destination_admin_connection_url
|
|
@@ -14,6 +15,7 @@ class Webhookdb::Organization::DatabaseMigration < Webhookdb::Postgres::Model(:o
|
|
|
14
15
|
|
|
15
16
|
many_to_one :started_by, class: "Webhookdb::Customer"
|
|
16
17
|
many_to_one :organization, class: "Webhookdb::Organization"
|
|
18
|
+
many_to_one :last_migrated_service_integration, class: "Webhookdb::ServiceIntegration"
|
|
17
19
|
|
|
18
20
|
dataset_module do
|
|
19
21
|
def ongoing
|
|
@@ -141,6 +143,7 @@ end
|
|
|
141
143
|
# organization_schema | text |
|
|
142
144
|
# last_migrated_service_integration_id | integer | NOT NULL DEFAULT 0
|
|
143
145
|
# last_migrated_timestamp | timestamp with time zone |
|
|
146
|
+
# text_search | tsvector |
|
|
144
147
|
# Indexes:
|
|
145
148
|
# organization_database_migrations_pkey | PRIMARY KEY btree (id)
|
|
146
149
|
# one_inprogress_migration_per_org | UNIQUE btree (organization_id) WHERE finished_at IS NULL
|
|
@@ -11,6 +11,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
|
11
11
|
|
|
12
12
|
plugin :timestamps
|
|
13
13
|
plugin :soft_deletes
|
|
14
|
+
plugin :text_searchable, terms: [:name, :key, :billing_email]
|
|
14
15
|
plugin :column_encryption do |enc|
|
|
15
16
|
enc.column :readonly_connection_url_raw
|
|
16
17
|
enc.column :admin_connection_url_raw
|
|
@@ -35,6 +36,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
|
35
36
|
order: :id
|
|
36
37
|
one_to_many :service_integrations, class: "Webhookdb::ServiceIntegration", order: :id
|
|
37
38
|
one_to_many :saved_queries, class: "Webhookdb::SavedQuery", order: :id
|
|
39
|
+
one_to_many :saved_views, class: "Webhookdb::SavedView", order: :id
|
|
38
40
|
one_to_many :webhook_subscriptions, class: "Webhookdb::WebhookSubscription", order: :id
|
|
39
41
|
many_to_many :feature_roles, class: "Webhookdb::Role", join_table: :feature_roles_organizations, right_key: :role_id
|
|
40
42
|
one_to_many :all_webhook_subscriptions,
|
|
@@ -145,6 +147,33 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
|
145
147
|
end
|
|
146
148
|
end
|
|
147
149
|
|
|
150
|
+
# Run the given SQL inside the org, and use special error handling if it fails.
|
|
151
|
+
# @return [Array<Webhookdb::Organization::QueryResult,String,nil>] Tuple of query result, and optional message.
|
|
152
|
+
# On query success, return <QueryResult, nil>.
|
|
153
|
+
# On DatabaseError, return <nil, message>.
|
|
154
|
+
# On other types of errors, raise.
|
|
155
|
+
def execute_readonly_query_with_help(sql)
|
|
156
|
+
result = self.execute_readonly_query(sql)
|
|
157
|
+
return result, nil
|
|
158
|
+
rescue Sequel::DatabaseError => e
|
|
159
|
+
self.logger.error("db_query_database_error", error: e)
|
|
160
|
+
# We want to handle InsufficientPrivileges and UndefinedTable explicitly
|
|
161
|
+
# since we can hint the user at what to do.
|
|
162
|
+
# Otherwise, we should just return the Postgres exception.
|
|
163
|
+
msg = ""
|
|
164
|
+
case e.wrapped_exception
|
|
165
|
+
when PG::UndefinedTable
|
|
166
|
+
missing_table = e.wrapped_exception.message.match(/relation (.+) does not/)&.captures&.first
|
|
167
|
+
msg = "The table #{missing_table} does not exist. Run `webhookdb db tables` to see available tables." if
|
|
168
|
+
missing_table
|
|
169
|
+
when PG::InsufficientPrivilege
|
|
170
|
+
msg = "You do not have permission to perform this query. Queries must be read-only."
|
|
171
|
+
else
|
|
172
|
+
msg = e.wrapped_exception.message
|
|
173
|
+
end
|
|
174
|
+
return [nil, msg]
|
|
175
|
+
end
|
|
176
|
+
|
|
148
177
|
class QueryResult
|
|
149
178
|
attr_accessor :rows, :columns, :max_rows_reached
|
|
150
179
|
end
|
|
@@ -355,10 +384,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
|
355
384
|
end
|
|
356
385
|
|
|
357
386
|
def migrate_replication_schema(schema)
|
|
358
|
-
|
|
359
|
-
msg = "Sorry, this is not a valid schema name. " + Webhookdb::DBAdapter::INVALID_IDENTIFIER_MESSAGE
|
|
360
|
-
raise SchemaMigrationError, msg
|
|
361
|
-
end
|
|
387
|
+
Webhookdb::DBAdapter.validate_identifier!(schema, type: "schema")
|
|
362
388
|
Webhookdb::Organization::DatabaseMigration.guard_ongoing!(self)
|
|
363
389
|
raise SchemaMigrationError, "destination and target schema are the same" if schema == self.replication_schema
|
|
364
390
|
builder = Webhookdb::Organization::DbBuilder.new(self)
|
|
@@ -446,7 +472,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
|
446
472
|
return Webhookdb::ServiceIntegration.where(organization: self).count < limit
|
|
447
473
|
end
|
|
448
474
|
|
|
449
|
-
def
|
|
475
|
+
def available_replicators
|
|
450
476
|
available = Webhookdb::Replicator.registry.values.filter do |desc|
|
|
451
477
|
# The org must have any of the flags required for the service. In other words,
|
|
452
478
|
# the intersection of desc[:feature_roles] & org.feature_roles must
|
|
@@ -456,7 +482,7 @@ class Webhookdb::Organization < Webhookdb::Postgres::Model(:organizations)
|
|
|
456
482
|
org_has_access = (self.feature_roles.map(&:name) & desc.feature_roles).present?
|
|
457
483
|
org_has_access
|
|
458
484
|
end
|
|
459
|
-
return available
|
|
485
|
+
return available
|
|
460
486
|
end
|
|
461
487
|
|
|
462
488
|
#
|
|
@@ -498,6 +524,8 @@ require "webhookdb/organization/db_builder"
|
|
|
498
524
|
# minimum_sync_seconds | integer | NOT NULL
|
|
499
525
|
# sync_target_timeout | integer | NOT NULL DEFAULT 30
|
|
500
526
|
# max_query_rows | integer |
|
|
527
|
+
# priority_backfill | boolean | NOT NULL DEFAULT false
|
|
528
|
+
# text_search | tsvector |
|
|
501
529
|
# Indexes:
|
|
502
530
|
# organizations_pkey | PRIMARY KEY btree (id)
|
|
503
531
|
# organizations_key_key | UNIQUE btree (key)
|
|
@@ -505,8 +533,11 @@ require "webhookdb/organization/db_builder"
|
|
|
505
533
|
# Referenced By:
|
|
506
534
|
# feature_roles_organizations | feature_roles_organizations_organization_id_fkey | (organization_id) REFERENCES organizations(id)
|
|
507
535
|
# logged_webhooks | logged_webhooks_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
|
536
|
+
# oauth_sessions | oauth_sessions_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
|
508
537
|
# organization_database_migrations | organization_database_migrations_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
|
509
538
|
# organization_memberships | organization_memberships_organization_id_fkey | (organization_id) REFERENCES organizations(id)
|
|
539
|
+
# saved_queries | saved_queries_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
|
540
|
+
# saved_views | saved_views_organization_id_fkey | (organization_id) REFERENCES organizations(id) ON DELETE CASCADE
|
|
510
541
|
# service_integrations | service_integrations_organization_id_fkey | (organization_id) REFERENCES organizations(id)
|
|
511
542
|
# webhook_subscriptions | webhook_subscriptions_organization_id_fkey | (organization_id) REFERENCES organizations(id)
|
|
512
543
|
# ------------------------------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -5,10 +5,16 @@ require "webhookdb/postgres/model"
|
|
|
5
5
|
class Webhookdb::OrganizationMembership < Webhookdb::Postgres::Model(:organization_memberships)
|
|
6
6
|
VALID_ROLE_NAMES = ["admin", "member"].freeze
|
|
7
7
|
|
|
8
|
+
plugin :text_searchable, terms: [:customer, :organization, :membership_role]
|
|
9
|
+
|
|
8
10
|
many_to_one :organization, class: "Webhookdb::Organization"
|
|
9
11
|
many_to_one :customer, class: "Webhookdb::Customer"
|
|
10
12
|
many_to_one :membership_role, class: "Webhookdb::Role"
|
|
11
13
|
|
|
14
|
+
dataset_module do
|
|
15
|
+
def admin = self.where(membership_role: Webhookdb::Role.admin_role)
|
|
16
|
+
end
|
|
17
|
+
|
|
12
18
|
def verified?
|
|
13
19
|
return self.verified
|
|
14
20
|
end
|
|
@@ -38,13 +44,14 @@ end
|
|
|
38
44
|
# Table: organization_memberships
|
|
39
45
|
# ------------------------------------------------------------------------------------------------------------------------------
|
|
40
46
|
# Columns:
|
|
41
|
-
# id | integer
|
|
42
|
-
# customer_id | integer
|
|
43
|
-
# organization_id | integer
|
|
44
|
-
# verified | boolean
|
|
45
|
-
# invitation_code | text
|
|
46
|
-
# membership_role_id | integer
|
|
47
|
-
# is_default | boolean
|
|
47
|
+
# id | integer | PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY
|
|
48
|
+
# customer_id | integer | NOT NULL
|
|
49
|
+
# organization_id | integer | NOT NULL
|
|
50
|
+
# verified | boolean | NOT NULL
|
|
51
|
+
# invitation_code | text | NOT NULL DEFAULT ''::text
|
|
52
|
+
# membership_role_id | integer | NOT NULL
|
|
53
|
+
# is_default | boolean | NOT NULL DEFAULT false
|
|
54
|
+
# text_search | tsvector |
|
|
48
55
|
# Indexes:
|
|
49
56
|
# organization_memberships_pkey | PRIMARY KEY btree (id)
|
|
50
57
|
# one_default_per_customer | UNIQUE btree (customer_id, organization_id) WHERE is_default IS TRUE
|
data/lib/webhookdb/postgres.rb
CHANGED
|
@@ -48,6 +48,7 @@ module Webhookdb::Postgres
|
|
|
48
48
|
# Require paths for all Sequel models used by the app.
|
|
49
49
|
MODELS = [
|
|
50
50
|
"webhookdb/backfill_job",
|
|
51
|
+
"webhookdb/backfill_job/service_integration_lock",
|
|
51
52
|
"webhookdb/customer",
|
|
52
53
|
"webhookdb/customer/reset_code",
|
|
53
54
|
"webhookdb/database_document",
|
|
@@ -61,6 +62,7 @@ module Webhookdb::Postgres
|
|
|
61
62
|
"webhookdb/organization_membership",
|
|
62
63
|
"webhookdb/role",
|
|
63
64
|
"webhookdb/saved_query",
|
|
65
|
+
"webhookdb/saved_view",
|
|
64
66
|
"webhookdb/service_integration",
|
|
65
67
|
"webhookdb/subscription",
|
|
66
68
|
"webhookdb/sync_target",
|
|
@@ -159,6 +159,7 @@ class Webhookdb::Replicator::Base
|
|
|
159
159
|
def process_state_change(field, value, attr: nil)
|
|
160
160
|
attr ||= field
|
|
161
161
|
desc = self.descriptor
|
|
162
|
+
value = value.strip if value.respond_to?(:strip)
|
|
162
163
|
case field
|
|
163
164
|
when *self._webhook_state_change_fields
|
|
164
165
|
# If we don't support webhooks, then the backfill state machine may be using it.
|
|
@@ -55,13 +55,21 @@ class Webhookdb::Replicator::Docgen
|
|
|
55
55
|
|
|
56
56
|
def _intro
|
|
57
57
|
lines << "# #{desc.resource_name_singular} (`#{desc.name}`)"
|
|
58
|
+
if desc.enterprise?
|
|
59
|
+
lines << ""
|
|
60
|
+
lines << "{% include enterprise_integration_list.md %}"
|
|
61
|
+
lines << ""
|
|
62
|
+
end
|
|
58
63
|
if desc.description.present?
|
|
59
64
|
lines << ""
|
|
60
65
|
lines << desc.description
|
|
61
66
|
end
|
|
67
|
+
lines << ""
|
|
68
|
+
lines << "To get set up, run this code from the [WebhookDB CLI](https://webhookdb.com/terminal):"
|
|
69
|
+
lines << "```\nwebhookdb integrations create #{desc.name}\n```"
|
|
62
70
|
if desc.api_docs_url.present?
|
|
63
71
|
lines << ""
|
|
64
|
-
lines << "
|
|
72
|
+
lines << "Source documentation for this API: [#{desc.api_docs_url}](#{desc.api_docs_url})"
|
|
65
73
|
end
|
|
66
74
|
lines << ""
|
|
67
75
|
end
|
|
@@ -109,9 +109,8 @@ class Webhookdb::Replicator::Fake < Webhookdb::Replicator::Base
|
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
def _resource_and_event(request)
|
|
112
|
-
|
|
113
|
-
return
|
|
114
|
-
return body, nil
|
|
112
|
+
return self.class.resource_and_event_hook.call(request) if self.class.resource_and_event_hook
|
|
113
|
+
return request.body, nil
|
|
115
114
|
end
|
|
116
115
|
|
|
117
116
|
def _update_where_expr
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "jwt"
|
|
4
4
|
|
|
5
|
+
require "webhookdb/messages/error_signalwire_send_sms"
|
|
5
6
|
require "webhookdb/replicator/front_v1_mixin"
|
|
6
7
|
|
|
7
8
|
# Front has a system of 'channels' but it is a challenge to use.
|
|
@@ -148,7 +149,7 @@ All of this information can be found in the WebhookDB docs, at https://docs.webh
|
|
|
148
149
|
self.service_integration.save_changes
|
|
149
150
|
return {type: "success", webhook_url: "#{Webhookdb.api_url}/v1/install/front_signalwire/channel"}.to_json
|
|
150
151
|
when "delete"
|
|
151
|
-
self.service_integration.
|
|
152
|
+
self.service_integration.destroy_self_and_all_dependents
|
|
152
153
|
return "{}"
|
|
153
154
|
when "message", "message_autoreply"
|
|
154
155
|
return {
|
|
@@ -269,27 +270,61 @@ All of this information can be found in the WebhookDB docs, at https://docs.webh
|
|
|
269
270
|
item[:signalwire_sid] = "skipped_due_to_age"
|
|
270
271
|
else
|
|
271
272
|
# send the SMS via signalwire
|
|
272
|
-
signalwire_resp =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
api_key: @signalwire_sint.backfill_secret,
|
|
280
|
-
logger: @replicator.logger,
|
|
281
|
-
)
|
|
282
|
-
end
|
|
283
|
-
item[:signalwire_sid] = signalwire_resp.fetch("sid")
|
|
273
|
+
signalwire_resp = _send_sms(
|
|
274
|
+
idempotency,
|
|
275
|
+
from: sender,
|
|
276
|
+
to: recipient,
|
|
277
|
+
body:,
|
|
278
|
+
)
|
|
279
|
+
item[:signalwire_sid] = signalwire_resp.fetch("sid") if signalwire_resp
|
|
284
280
|
end
|
|
285
281
|
end
|
|
286
282
|
@replicator.upsert_webhook_body(item.deep_stringify_keys)
|
|
287
283
|
end
|
|
288
284
|
|
|
285
|
+
def _send_sms(idempotency, from:, to:, body:)
|
|
286
|
+
return idempotency.execute do
|
|
287
|
+
Webhookdb::Signalwire.send_sms(
|
|
288
|
+
from:,
|
|
289
|
+
to:,
|
|
290
|
+
body:,
|
|
291
|
+
space_url: @signalwire_sint.api_url,
|
|
292
|
+
project_id: @signalwire_sint.backfill_key,
|
|
293
|
+
api_key: @signalwire_sint.backfill_secret,
|
|
294
|
+
logger: @replicator.logger,
|
|
295
|
+
)
|
|
296
|
+
end
|
|
297
|
+
rescue Webhookdb::Http::Error => e
|
|
298
|
+
response_body = e.body
|
|
299
|
+
response_status = e.status
|
|
300
|
+
request_url = e.uri.to_s
|
|
301
|
+
@replicator.logger.warn("signalwire_send_sms_error",
|
|
302
|
+
response_body:, response_status:, request_url:, sms_from: from, sms_to: to,)
|
|
303
|
+
code = begin
|
|
304
|
+
# If this fails for whatever reason, or there is no 'code', re-raise the original error
|
|
305
|
+
e.response.parsed_response["code"]
|
|
306
|
+
rescue StandardError
|
|
307
|
+
nil
|
|
308
|
+
end
|
|
309
|
+
# All known codes are for the integrator, not on the webhookdb code side.
|
|
310
|
+
# https://developer.signalwire.com/guides/how-to-troubleshoot-common-messaging-issues
|
|
311
|
+
raise e if code.nil?
|
|
312
|
+
|
|
313
|
+
message = Webhookdb::Messages::ErrorSignalwireSendSms.new(
|
|
314
|
+
@replicator.service_integration,
|
|
315
|
+
response_status:,
|
|
316
|
+
response_body:,
|
|
317
|
+
request_url:,
|
|
318
|
+
request_method: "POST",
|
|
319
|
+
)
|
|
320
|
+
@replicator.service_integration.organization.alerting.dispatch_alert(message)
|
|
321
|
+
return nil
|
|
322
|
+
end
|
|
323
|
+
|
|
289
324
|
def _sync_front_inbound(sender:, texted_at:, item:, body:)
|
|
290
325
|
body = {
|
|
291
326
|
sender: {handle: sender},
|
|
292
|
-
body
|
|
327
|
+
body: body || "<no body>",
|
|
293
328
|
delivered_at: texted_at.to_i,
|
|
294
329
|
metadata: {
|
|
295
330
|
external_id: item.fetch(:external_id),
|
|
@@ -146,11 +146,12 @@ The secret to use for signing is:
|
|
|
146
146
|
|
|
147
147
|
class Upserter
|
|
148
148
|
include Webhookdb::Backfiller::Bulk
|
|
149
|
-
attr_reader :upserting_replicator, :calendar_external_id
|
|
149
|
+
attr_reader :upserting_replicator, :calendar_external_id, :now
|
|
150
150
|
|
|
151
|
-
def initialize(replicator, calendar_external_id)
|
|
151
|
+
def initialize(replicator, calendar_external_id, now:)
|
|
152
152
|
@upserting_replicator = replicator
|
|
153
153
|
@calendar_external_id = calendar_external_id
|
|
154
|
+
@now = now
|
|
154
155
|
end
|
|
155
156
|
|
|
156
157
|
def upsert_page_size = 500
|
|
@@ -158,31 +159,33 @@ The secret to use for signing is:
|
|
|
158
159
|
|
|
159
160
|
def prepare_body(body)
|
|
160
161
|
body["calendar_external_id"] = @calendar_external_id
|
|
162
|
+
body["row_updated_at"] = @now
|
|
161
163
|
end
|
|
162
164
|
end
|
|
163
165
|
|
|
164
166
|
def sync_row(row)
|
|
165
167
|
Appydays::Loggable.with_log_tags(icalendar_url: row.fetch(:ics_url)) do
|
|
166
168
|
self.with_advisory_lock(row.fetch(:pk)) do
|
|
169
|
+
now = Time.now
|
|
167
170
|
if (dep = self.find_dependent("icalendar_event_v1"))
|
|
168
|
-
self._sync_row(row, dep)
|
|
171
|
+
self._sync_row(row, dep, now:)
|
|
169
172
|
end
|
|
170
|
-
self.admin_dataset { |ds| ds.where(pk: row.fetch(:pk)).update(last_synced_at:
|
|
173
|
+
self.admin_dataset { |ds| ds.where(pk: row.fetch(:pk)).update(last_synced_at: now) }
|
|
171
174
|
end
|
|
172
175
|
end
|
|
173
176
|
end
|
|
174
177
|
|
|
175
|
-
def _sync_row(row, dep)
|
|
178
|
+
def _sync_row(row, dep, now:)
|
|
176
179
|
calendar_external_id = row.fetch(:external_id)
|
|
177
|
-
request_url = row.fetch(:ics_url)
|
|
178
180
|
begin
|
|
181
|
+
request_url = self._clean_ics_url(row.fetch(:ics_url))
|
|
179
182
|
io = Webhookdb::Http.chunked_download(request_url, rewindable: false)
|
|
180
|
-
rescue Down::Error => e
|
|
183
|
+
rescue Down::Error, URI::InvalidURIError => e
|
|
181
184
|
self._handle_down_error(e, request_url:, calendar_external_id:)
|
|
182
185
|
return
|
|
183
186
|
end
|
|
184
187
|
|
|
185
|
-
upserter = Upserter.new(dep.replicator, calendar_external_id)
|
|
188
|
+
upserter = Upserter.new(dep.replicator, calendar_external_id, now:)
|
|
186
189
|
processor = EventProcessor.new(io, upserter)
|
|
187
190
|
processor.process
|
|
188
191
|
# Delete all the extra replicator rows, and cancel all the rows that weren't upserted.
|
|
@@ -192,12 +195,23 @@ The secret to use for signing is:
|
|
|
192
195
|
ds.where(delete_condition).delete
|
|
193
196
|
end
|
|
194
197
|
# Update both the status, and set the data json to match.
|
|
195
|
-
|
|
198
|
+
# Only update rows not already CANCELLED.
|
|
199
|
+
ds = ds.exclude(Sequel[compound_identity: processor.upserted_identities])
|
|
200
|
+
ds = ds.where(Sequel[status: nil] | ~Sequel[status: "CANCELLED"])
|
|
201
|
+
ds.update(
|
|
196
202
|
status: "CANCELLED",
|
|
197
203
|
data: Sequel.lit('data || \'{"STATUS":{"v":"CANCELLED"}}\'::jsonb'),
|
|
204
|
+
row_updated_at: now,
|
|
198
205
|
)
|
|
199
206
|
end
|
|
200
|
-
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# We get all sorts of strange urls, fix up what we can.
|
|
210
|
+
def _clean_ics_url(url)
|
|
211
|
+
u = URI(url)
|
|
212
|
+
# https://xyz.com:80 is invalid, set it to 443 which yields https://xyz.com
|
|
213
|
+
u.port = 443 if u.scheme == "https" && u.port == 80
|
|
214
|
+
return u.to_s
|
|
201
215
|
end
|
|
202
216
|
|
|
203
217
|
def _handle_down_error(e, request_url:, calendar_external_id:)
|
|
@@ -205,22 +219,37 @@ The secret to use for signing is:
|
|
|
205
219
|
when Down::TooManyRedirects
|
|
206
220
|
response_status = 301
|
|
207
221
|
response_body = "<too many redirects>"
|
|
208
|
-
when Down::
|
|
222
|
+
when Down::NotModified
|
|
223
|
+
# Do not alert on 304, but do log
|
|
224
|
+
self.logger.info("icalendar_fetch_not_modified", response_status: 304, request_url:, calendar_external_id:)
|
|
225
|
+
return
|
|
226
|
+
when Down::SSLError
|
|
227
|
+
self._handle_retryable_down_error!(e, request_url:, calendar_external_id:)
|
|
228
|
+
when Down::TimeoutError, Down::ConnectionError, Down::InvalidUrl, URI::InvalidURIError
|
|
209
229
|
response_status = 0
|
|
210
230
|
response_body = e.to_s
|
|
211
231
|
when Down::ClientError
|
|
212
232
|
raise e if e.response.nil?
|
|
213
233
|
response_status = e.response.code.to_i
|
|
234
|
+
self._handle_retryable_down_error!(e, request_url:, calendar_external_id:) if
|
|
235
|
+
self._retryable_client_error?(e, request_url:)
|
|
236
|
+
# These are all the errors we've seen, we can't do anything about.
|
|
237
|
+
# In theory we should do this for ALL 4xx errors,
|
|
238
|
+
# but we'd rather error on the WebhookDB side until we're sure
|
|
239
|
+
# we want to ignore things.
|
|
214
240
|
expected_errors = [
|
|
241
|
+
400, 401, 402, 403, # Common access problems we can't do anything about
|
|
242
|
+
404, 405, # Fundamental issues with the URL given
|
|
243
|
+
409, 410, # More access problems
|
|
215
244
|
417, # If someone uses an Outlook HTML calendar, fetch gives us a 417
|
|
245
|
+
429, # Usually 429s are retried (as above), but in some cases they're not.
|
|
216
246
|
]
|
|
217
247
|
# For most client errors, we can't do anything about it. For example,
|
|
218
248
|
# and 'unshared' URL could result in a 401, 403, 404, or even a 405.
|
|
219
249
|
# For now, other client errors, we can raise on,
|
|
220
250
|
# in case it's something we can fix/work around.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
raise e if is_problem_error
|
|
251
|
+
# For example, it's possible something like a 415 is a WebhookDB issue.
|
|
252
|
+
raise e unless expected_errors.include?(response_status)
|
|
224
253
|
response_body = e.response.body.to_s
|
|
225
254
|
when Down::ServerError
|
|
226
255
|
response_status = e.response.code.to_i
|
|
@@ -243,6 +272,32 @@ The secret to use for signing is:
|
|
|
243
272
|
self.service_integration.organization.alerting.dispatch_alert(message)
|
|
244
273
|
end
|
|
245
274
|
|
|
275
|
+
def _retryable_client_error?(e, request_url:)
|
|
276
|
+
code = e.response.code.to_i
|
|
277
|
+
# This is a bad domain that returns 429 for most requests.
|
|
278
|
+
# Tell the org admins it won't sync.
|
|
279
|
+
return false if code == 429 && request_url.start_with?("https://ical.schedulestar.com")
|
|
280
|
+
# Other 429s can be retried.
|
|
281
|
+
return true if code == 429
|
|
282
|
+
# Otherwise, handle the client error normally, by telling the org admins, or raising.
|
|
283
|
+
return false
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def _handle_retryable_down_error!(e, request_url:, calendar_external_id:)
|
|
287
|
+
# Retry on these, which are hopefully transient.
|
|
288
|
+
# For now, if they aren't transient, die so we see the job.
|
|
289
|
+
# We will probably need to do an alert if the retries on exhausted instead.
|
|
290
|
+
retry_in = rand(4..60).minutes
|
|
291
|
+
self.logger.debug(
|
|
292
|
+
"icalendar_fetch_error_retry",
|
|
293
|
+
response_status: e.respond_to?(:response) ? e.response&.code : 0,
|
|
294
|
+
request_url:,
|
|
295
|
+
calendar_external_id:,
|
|
296
|
+
retry_at: Time.now + retry_in,
|
|
297
|
+
)
|
|
298
|
+
raise Amigo::Retry::OrDie.new(10, retry_in)
|
|
299
|
+
end
|
|
300
|
+
|
|
246
301
|
class EventProcessor
|
|
247
302
|
attr_reader :upserted_identities
|
|
248
303
|
|
|
@@ -378,13 +433,21 @@ The secret to use for signing is:
|
|
|
378
433
|
|
|
379
434
|
schedule = IceCube::Schedule.from_hash(ical_params)
|
|
380
435
|
dont_project_before = Webhookdb::Icalendar.oldest_recurring_event
|
|
381
|
-
dont_project_after =
|
|
436
|
+
dont_project_after = @upserter.now + RECURRENCE_PROJECTION
|
|
382
437
|
|
|
383
438
|
# Just like google, track the original event id.
|
|
384
439
|
h["recurring_event_id"] = uid
|
|
385
440
|
final_sequence = -1
|
|
386
441
|
begin
|
|
387
|
-
|
|
442
|
+
# Pass in a 'closing time' to avoid a denial of service for an impossible rrule.
|
|
443
|
+
# It is further into the future than the "don't project after"
|
|
444
|
+
# since using something too short causes the calculation to be short-circuited before it should
|
|
445
|
+
# (I'm unclear what the ideal value is, but tests will fail with much less than the number here).
|
|
446
|
+
# This still results in a slow calculation, but there's not much we can do for now.
|
|
447
|
+
# In the future perhaps we should try to pre-validate common problems.
|
|
448
|
+
# See spec for examples.
|
|
449
|
+
dos_cutoff = dont_project_after + 210.days
|
|
450
|
+
schedule.send(:enumerate_occurrences, schedule.start_time, dos_cutoff).each_with_index do |occ, idx|
|
|
388
451
|
next if occ.start_time < dont_project_before
|
|
389
452
|
# Given the original hash, we will modify some fields.
|
|
390
453
|
e = h.dup
|
|
@@ -424,6 +487,7 @@ The secret to use for signing is:
|
|
|
424
487
|
ical = ical.gsub(/BYMONTHDAY=[\d,]+/, "")
|
|
425
488
|
ical.delete_prefix! ";"
|
|
426
489
|
ical.delete_suffix! ";"
|
|
490
|
+
ical.squeeze!(";")
|
|
427
491
|
end
|
|
428
492
|
return IceCube::IcalParser.rule_from_ical(ical)
|
|
429
493
|
end
|
|
@@ -443,7 +507,23 @@ The secret to use for signing is:
|
|
|
443
507
|
vevent_lines = []
|
|
444
508
|
in_vevent = false
|
|
445
509
|
while (line = @io.gets)
|
|
446
|
-
|
|
510
|
+
begin
|
|
511
|
+
line.rstrip!
|
|
512
|
+
rescue Encoding::CompatibilityError
|
|
513
|
+
# We occassionally get incorrectly encoded files.
|
|
514
|
+
# For example, the response may have a header:
|
|
515
|
+
# Content-Type: text/calendar; charset=UTF-8
|
|
516
|
+
# but the actual encoding is not:
|
|
517
|
+
# file -I <filename>
|
|
518
|
+
# <filename>: text/calendar; charset=iso-8859-1
|
|
519
|
+
# In these cases, there's not much we can do.
|
|
520
|
+
# We can use chardet, but it's a big library and this issue
|
|
521
|
+
# isn't common enough. Instead, try to force the encoding to utf-8,
|
|
522
|
+
# which may break some things, but we'll see what happens.
|
|
523
|
+
line = line.force_encoding("utf-8")
|
|
524
|
+
line = line.scrub
|
|
525
|
+
line = line.rstrip
|
|
526
|
+
end
|
|
447
527
|
if line == "BEGIN:VEVENT"
|
|
448
528
|
in_vevent = true
|
|
449
529
|
vevent_lines << line
|