webhookdb 1.2.2 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/admin-dist/assets/index-6aebf805.js +264 -0
- data/admin-dist/favicon.ico +0 -0
- data/admin-dist/index.html +130 -0
- data/admin-dist/manifest.json +15 -0
- data/data/messages/replicators/url-recorder.liquid +20 -0
- data/data/messages/templates/errors/signalwire_send_sms.email.liquid +31 -0
- data/data/messages/web/install-customer-login.liquid +6 -5
- data/data/messages/web/install-error.liquid +1 -1
- data/data/messages/web/install-forbidden.liquid +25 -0
- data/data/messages/web/install-org-chooser.liquid +40 -0
- data/data/messages/web/install-success.liquid +2 -1
- data/data/messages/web/install.liquid +2 -1
- data/data/messages/web/partials/head.liquid +2 -0
- data/data/messages/web/styles.liquid +24 -0
- data/db/migrations/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
|