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.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/admin-dist/assets/index-6aebf805.js +264 -0
  3. data/admin-dist/favicon.ico +0 -0
  4. data/admin-dist/index.html +130 -0
  5. data/admin-dist/manifest.json +15 -0
  6. data/data/messages/replicators/url-recorder.liquid +20 -0
  7. data/data/messages/templates/errors/signalwire_send_sms.email.liquid +31 -0
  8. data/data/messages/web/install-customer-login.liquid +6 -5
  9. data/data/messages/web/install-error.liquid +1 -1
  10. data/data/messages/web/install-forbidden.liquid +25 -0
  11. data/data/messages/web/install-org-chooser.liquid +40 -0
  12. data/data/messages/web/install-success.liquid +2 -1
  13. data/data/messages/web/install.liquid +2 -1
  14. data/data/messages/web/partials/head.liquid +2 -0
  15. data/data/messages/web/styles.liquid +24 -0
  16. data/db/migrations/041_views.rb +20 -0
  17. data/db/migrations/042_sint_lock.rb +10 -0
  18. data/db/migrations/043_text_search.rb +28 -0
  19. data/db/migrations/044_oauth_session_token_cache.rb +21 -0
  20. data/integration/auth_spec.rb +2 -2
  21. data/lib/sequel/plugins/text_searchable.rb +165 -0
  22. data/lib/sequel/text_searchable.rb +42 -0
  23. data/lib/webhookdb/admin_api/auth.rb +24 -3
  24. data/lib/webhookdb/admin_api/data_provider.rb +196 -0
  25. data/lib/webhookdb/admin_api/entities.rb +143 -28
  26. data/lib/webhookdb/admin_api.rb +0 -2
  27. data/lib/webhookdb/api/auth.rb +5 -6
  28. data/lib/webhookdb/api/db.rb +31 -6
  29. data/lib/webhookdb/api/entities.rb +7 -1
  30. data/lib/webhookdb/api/helpers.rb +6 -25
  31. data/lib/webhookdb/api/install.rb +204 -79
  32. data/lib/webhookdb/api/organizations.rb +14 -12
  33. data/lib/webhookdb/api/saved_queries.rb +9 -3
  34. data/lib/webhookdb/api/saved_views.rb +99 -0
  35. data/lib/webhookdb/api/service_integrations.rb +15 -9
  36. data/lib/webhookdb/api/subscriptions.rb +3 -1
  37. data/lib/webhookdb/api/sync_targets.rb +9 -7
  38. data/lib/webhookdb/api/system.rb +1 -0
  39. data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
  40. data/lib/webhookdb/apps.rb +30 -7
  41. data/lib/webhookdb/async/audit_logger.rb +2 -0
  42. data/lib/webhookdb/async.rb +5 -0
  43. data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
  44. data/lib/webhookdb/backfill_job.rb +9 -0
  45. data/lib/webhookdb/customer.rb +5 -0
  46. data/lib/webhookdb/database_document.rb +1 -1
  47. data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
  48. data/lib/webhookdb/db_adapter.rb +20 -4
  49. data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
  50. data/lib/webhookdb/fixtures/organizations.rb +5 -0
  51. data/lib/webhookdb/fixtures/roles.rb +14 -0
  52. data/lib/webhookdb/fixtures/saved_views.rb +25 -0
  53. data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
  54. data/lib/webhookdb/http.rb +8 -2
  55. data/lib/webhookdb/icalendar.rb +3 -0
  56. data/lib/webhookdb/idempotency.rb +69 -22
  57. data/lib/webhookdb/increase.rb +69 -21
  58. data/lib/webhookdb/intercom.rb +10 -3
  59. data/lib/webhookdb/jobs/backfill.rb +3 -1
  60. data/lib/webhookdb/jobs/emailer.rb +0 -1
  61. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
  62. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
  63. data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
  64. data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
  65. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
  66. data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
  67. data/lib/webhookdb/message/body.rb +6 -4
  68. data/lib/webhookdb/message/delivery.rb +2 -0
  69. data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
  70. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
  71. data/lib/webhookdb/oauth/fake_provider.rb +44 -0
  72. data/lib/webhookdb/oauth/front_provider.rb +1 -2
  73. data/lib/webhookdb/oauth/increase_provider.rb +80 -0
  74. data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
  75. data/lib/webhookdb/oauth/session.rb +20 -0
  76. data/lib/webhookdb/oauth.rb +7 -21
  77. data/lib/webhookdb/organization/alerting.rb +2 -0
  78. data/lib/webhookdb/organization/database_migration.rb +3 -0
  79. data/lib/webhookdb/organization.rb +37 -6
  80. data/lib/webhookdb/organization_membership.rb +14 -7
  81. data/lib/webhookdb/postgres.rb +2 -0
  82. data/lib/webhookdb/replicator/base.rb +1 -0
  83. data/lib/webhookdb/replicator/docgen.rb +9 -1
  84. data/lib/webhookdb/replicator/fake.rb +2 -3
  85. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
  86. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
  87. data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
  88. data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
  89. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
  90. data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
  91. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
  92. data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
  93. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
  94. data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
  95. data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
  96. data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
  97. data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
  98. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
  99. data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
  100. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
  101. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
  102. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
  103. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
  104. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  105. data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
  106. data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
  107. data/lib/webhookdb/replicator/webhook_request.rb +4 -0
  108. data/lib/webhookdb/replicator.rb +8 -0
  109. data/lib/webhookdb/role.rb +5 -2
  110. data/lib/webhookdb/saved_query.rb +23 -0
  111. data/lib/webhookdb/saved_view.rb +73 -0
  112. data/lib/webhookdb/sentry.rb +2 -0
  113. data/lib/webhookdb/service/entities.rb +0 -4
  114. data/lib/webhookdb/service/helpers.rb +5 -0
  115. data/lib/webhookdb/service/middleware.rb +9 -0
  116. data/lib/webhookdb/service/types.rb +10 -8
  117. data/lib/webhookdb/service/validators.rb +1 -2
  118. data/lib/webhookdb/service/view_api.rb +1 -1
  119. data/lib/webhookdb/service_integration.rb +17 -15
  120. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
  121. data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
  122. data/lib/webhookdb/subscription.rb +2 -0
  123. data/lib/webhookdb/sync_target.rb +10 -2
  124. data/lib/webhookdb/tasks/message.rb +3 -1
  125. data/lib/webhookdb/version.rb +1 -1
  126. data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
  127. data/lib/webhookdb/webhook_subscription.rb +2 -0
  128. metadata +57 -9
  129. data/lib/webhookdb/admin_api/customers.rb +0 -63
  130. data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
  131. data/lib/webhookdb/admin_api/roles.rb +0 -15
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "webhookdb/increase"
4
3
  require "webhookdb/replicator/increase_v1_mixin"
5
4
 
6
5
  class Webhookdb::Replicator::IncreaseTransactionV1 < Webhookdb::Replicator::Base
@@ -11,10 +10,10 @@ class Webhookdb::Replicator::IncreaseTransactionV1 < Webhookdb::Replicator::Base
11
10
  def self.descriptor
12
11
  return Webhookdb::Replicator::Descriptor.new(
13
12
  name: "increase_transaction_v1",
14
- ctor: ->(sint) { Webhookdb::Replicator::IncreaseTransactionV1.new(sint) },
13
+ ctor: self,
15
14
  feature_roles: [],
16
15
  resource_name_singular: "Increase Transaction",
17
- supports_webhooks: true,
16
+ dependency_descriptor: Webhookdb::Replicator::IncreaseAppV1.descriptor,
18
17
  supports_backfill: true,
19
18
  api_docs_url: "https://increase.com/documentation/api",
20
19
  )
@@ -28,13 +27,8 @@ class Webhookdb::Replicator::IncreaseTransactionV1 < Webhookdb::Replicator::Base
28
27
  return [
29
28
  Webhookdb::Replicator::Column.new(:account_id, TEXT, index: true),
30
29
  Webhookdb::Replicator::Column.new(:amount, INTEGER, index: true),
31
- Webhookdb::Replicator::Column.new(
32
- :created_at,
33
- TIMESTAMP,
34
- data_key: "created_at",
35
- optional: true,
36
- index: true,
37
- ),
30
+ Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, index: true),
31
+ Webhookdb::Replicator::Column.new(:updated_at, TIMESTAMP, index: true),
38
32
  # date is a legacy field that is not documented in the API,
39
33
  # but is still sent with transactions as of April 2022.
40
34
  # We need to support the v1 schema, but do not want to depend
@@ -48,27 +42,8 @@ class Webhookdb::Replicator::IncreaseTransactionV1 < Webhookdb::Replicator::Base
48
42
  converter: Webhookdb::Replicator::Column::CONV_TO_UTC_DATE,
49
43
  ),
50
44
  Webhookdb::Replicator::Column.new(:route_id, TEXT, index: true),
51
- Webhookdb::Replicator::Column.new(
52
- :updated_at,
53
- TIMESTAMP,
54
- data_key: "created_at",
55
- event_key: "created_at",
56
- defaulter: :now,
57
- optional: true,
58
- index: true,
59
- ),
60
45
  ]
61
46
  end
62
47
 
63
- def _update_where_expr
64
- return self.qualified_table_sequel_identifier[:updated_at] < Sequel[:excluded][:updated_at]
65
- end
66
-
67
- def _resource_and_event(request)
68
- return self._find_resource_and_event(request.body, "transaction")
69
- end
70
-
71
- def _mixin_backfill_url
72
- return "#{self.service_integration.api_url}/transactions"
73
- end
48
+ def _mixin_object_type = "transaction"
74
49
  end
@@ -3,119 +3,99 @@
3
3
  require "webhookdb/increase"
4
4
 
5
5
  module Webhookdb::Replicator::IncreaseV1Mixin
6
- def _mixin_backfill_url
7
- raise NotImplementedError
8
- end
9
-
10
6
  def _webhook_response(request)
11
7
  return Webhookdb::Increase.webhook_response(request, self.service_integration.webhook_secret)
12
8
  end
13
9
 
10
+ def _resource_and_event(request) = request.body
11
+
14
12
  def _timestamp_column_name
13
+ # We derive updated_at from the event, or use 'now'
15
14
  return :updated_at
16
15
  end
17
16
 
18
- def _find_resource_and_event(body, desired_object_name)
19
- return nil unless Webhookdb::Increase.contains_desired_object(body, desired_object_name)
20
- return body.fetch("data"), body if body.key?("event") && body.key?("event_id")
21
- return body, nil
17
+ def _update_where_expr
18
+ ts = self._timestamp_column_name
19
+ return self.qualified_table_sequel_identifier[ts] < Sequel[:excluded][ts]
22
20
  end
23
21
 
24
- def process_state_change(field, value)
25
- # special handling for having a default value for api url
26
- value = "https://api.increase.com" if field == "api_url" && value == ""
27
- return super(field, value)
22
+ def on_dependency_webhook_upsert(_replicator, payload, **)
23
+ self.upsert_webhook_body(payload)
28
24
  end
29
25
 
30
- def calculate_webhook_state_machine
31
- step = Webhookdb::Replicator::StateMachineStep.new
32
- # if the service integration doesn't exist, create it with some standard values
33
- unless self.service_integration.webhook_secret.present?
34
- step.output = %(You are about to start replicating #{self.resource_name_plural} info into WebhookDB.
35
- We've made an endpoint available for #{self.resource_name_singular} webhooks:
36
-
37
- #{self._webhook_endpoint}
38
-
39
- From your Increase admin dashboard, go to Applications -> Create Webhook.
40
- In the "Webhook endpoint URL" field you can enter the URL above.
41
- For the shared secret, you'll have to generate a strong password
42
- (you can use '#{Webhookdb::Id.rand_enc(16)}')
43
- and then enter it into the textbox.
44
-
45
- Copy that shared secret value.
46
- )
47
- return step.secret_prompt("secret").webhook_secret(self.service_integration)
48
- end
49
-
50
- step.output = %(Great! WebhookDB is now listening for #{self.resource_name_singular} webhooks.
51
- #{self._query_help_output}
52
- In order to backfill existing #{self.resource_name_plural}, run this from a shell:
53
-
54
- #{self._backfill_command}
26
+ def _mixin_object_type = raise NotImplementedError
27
+ def _mixin_backfill_path = "/#{self._mixin_object_type}s"
28
+ def _mixin_backfill_url = "#{self._api_url}#{self._mixin_backfill_path}"
29
+ def _api_url = "https://api.increase.com"
30
+
31
+ def handle_event?(event) = event.fetch("associated_object_type") == self._mixin_object_type
32
+
33
+ def _fetch_enrichment(resource, _event, _request)
34
+ # If the resource type isn't what we expect, it must be an event.
35
+ # In that case, we need to fetch the resource from the API,
36
+ # and replace the event body in prepare_for_insert.
37
+ # The updated_at becomes the event's created_at,
38
+ # which should be fine- it's better than setting updated_at to 'now'
39
+ # since that will be confusing as it looks like a resource was recently updated.
40
+ rtype = resource.fetch("type")
41
+ return nil if rtype == self._mixin_object_type
42
+ raise Webhookdb::InvalidPrecondition, "unexpected resource: #{resource}" unless
43
+ rtype == "event" && resource.fetch("associated_object_type") == self._mixin_object_type
44
+ response = Webhookdb::Http.get(
45
+ self._mixin_backfill_url + "/#{resource.fetch('associated_object_id')}",
46
+ {},
47
+ headers: self._auth_headers,
48
+ logger: self.logger,
49
+ timeout: Webhookdb::Increase.http_timeout,
55
50
  )
56
- return step.completed
51
+ return response.parsed_response.merge("updated_at" => resource.fetch("created_at"))
57
52
  end
58
53
 
59
- def calculate_backfill_state_machine
60
- step = Webhookdb::Replicator::StateMachineStep.new
61
- unless self.service_integration.backfill_key.present?
62
- step.output = %(In order to backfill #{self.resource_name_plural}, we need an API key.
63
- From your Increase admin dashboard, go to Settings -> Development -> API Keys.
64
- We'll need the Production key--copy that value to your clipboard.
65
- )
66
- return step.secret_prompt("API Key").backfill_key(self.service_integration)
67
- end
68
-
69
- unless self.service_integration.api_url.present?
70
- step.output = %(Great. Now we want to make sure we're sending API requests to the right place.
71
- For Increase, the API url is different when you are in Sandbox mode and when you are in Production mode.
72
- For Sandbox mode, the API root url is:
73
-
74
- https://sandbox.increase.com
75
-
76
- For Production mode, which is our default, it is:
54
+ def _prepare_for_insert(resource, event, request, enrichment)
55
+ resource = enrichment if enrichment
56
+ return super(resource, event, request, nil)
57
+ end
77
58
 
78
- https://api.increase.com
59
+ def _app_sint = Webhookdb::Replicator.find_at_root!(self.service_integration, service_name: "increase_app_v1")
79
60
 
80
- Leave blank to use the default or paste the answer into this prompt.
81
- )
82
- return step.prompting("API url").api_url(self.service_integration)
83
- end
61
+ def _auth_headers
62
+ return {"Authorization" => ("Bearer " + self._app_sint.backfill_key)}
63
+ end
84
64
 
85
- unless (result = self.verify_backfill_credentials).verified
86
- self.service_integration.replicator.clear_backfill_information
87
- step.output = result.message
88
- return step.secret_prompt("API Key").backfill_key(self.service_integration)
65
+ def calculate_backfill_state_machine
66
+ if (step = self.calculate_dependency_state_machine_step(dependency_help: ""))
67
+ step.output = %(This replicator is managed automatically using OAuth through Increase.
68
+ Head over to #{Webhookdb::Replicator::IncreaseAppV1.descriptor.install_url} to learn more.)
69
+ return step
89
70
  end
90
-
71
+ step = Webhookdb::Replicator::StateMachineStep.new
91
72
  step.needs_input = false
92
73
  step.output = %(Great! We are going to start backfilling your #{self.resource_name_plural}.
93
- #{self._query_help_output}
94
- )
74
+ #{self._query_help_output})
95
75
  step.complete = true
96
76
  return step
97
77
  end
98
78
 
99
- def _verify_backfill_401_err_msg
100
- return "It looks like that API Key is invalid. Please reenter the API Key you just created:"
101
- end
102
-
103
- def _verify_backfill_err_msg
104
- return "An error occurred. Please reenter the API Key you just created:"
105
- end
106
-
107
79
  def _fetch_backfill_page(pagination_token, **_kwargs)
108
80
  query = {}
109
81
  (query[:cursor] = pagination_token) if pagination_token.present?
82
+ fetched_at = Time.now
110
83
  response = Webhookdb::Http.get(
111
84
  self._mixin_backfill_url,
112
85
  query,
113
- headers: {"Authorization" => ("Bearer " + self.service_integration.backfill_key)},
86
+ headers: self._auth_headers,
114
87
  logger: self.logger,
115
88
  timeout: Webhookdb::Increase.http_timeout,
116
89
  )
117
90
  data = response.parsed_response
118
91
  next_page_param = data.dig("response_metadata", "next_cursor")
119
- return data["data"], next_page_param
92
+ rows = data["data"]
93
+ # In general, we want to use webhooks/events to keep rows updated.
94
+ # But if we are backfilling, touch the 'updated at' timestamp to make sure
95
+ # these rows get inserted.
96
+ # It does mess up history, but we can't get that history to be accurate
97
+ # in the case of a backfill anyway.
98
+ rows.each { |r| r["updated_at"] = fetched_at }
99
+ return rows, next_page_param
120
100
  end
121
101
  end
@@ -11,10 +11,10 @@ class Webhookdb::Replicator::IncreaseWireTransferV1 < Webhookdb::Replicator::Bas
11
11
  def self.descriptor
12
12
  return Webhookdb::Replicator::Descriptor.new(
13
13
  name: "increase_wire_transfer_v1",
14
- ctor: ->(sint) { Webhookdb::Replicator::IncreaseWireTransferV1.new(sint) },
14
+ ctor: self,
15
15
  feature_roles: [],
16
16
  resource_name_singular: "Increase Wire Transfer",
17
- supports_webhooks: true,
17
+ dependency_descriptor: Webhookdb::Replicator::IncreaseAppV1.descriptor,
18
18
  supports_backfill: true,
19
19
  api_docs_url: "https://increase.com/documentation/api",
20
20
  )
@@ -30,32 +30,13 @@ class Webhookdb::Replicator::IncreaseWireTransferV1 < Webhookdb::Replicator::Bas
30
30
  Webhookdb::Replicator::Column.new(:account_id, TEXT, index: true),
31
31
  Webhookdb::Replicator::Column.new(:amount, INTEGER, index: true),
32
32
  Webhookdb::Replicator::Column.new(:approved_at, TIMESTAMP, data_key: ["approval", "approved_at"]),
33
- Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, optional: true, index: true),
33
+ Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, index: true),
34
34
  Webhookdb::Replicator::Column.new(:routing_number, TEXT, index: true),
35
35
  Webhookdb::Replicator::Column.new(:status, TEXT),
36
- Webhookdb::Replicator::Column.new(:template_id, TEXT),
37
36
  Webhookdb::Replicator::Column.new(:transaction_id, TEXT, index: true),
38
- Webhookdb::Replicator::Column.new(
39
- :updated_at,
40
- TIMESTAMP,
41
- data_key: "created_at",
42
- event_key: "created_at",
43
- defaulter: :now,
44
- optional: true,
45
- index: true,
46
- ),
37
+ Webhookdb::Replicator::Column.new(:updated_at, TIMESTAMP, index: true),
47
38
  ]
48
39
  end
49
40
 
50
- def _resource_and_event(request)
51
- return self._find_resource_and_event(request.body, "wire_transfer")
52
- end
53
-
54
- def _update_where_expr
55
- return self.qualified_table_sequel_identifier[:updated_at] < Sequel[:excluded][:updated_at]
56
- end
57
-
58
- def _mixin_backfill_url
59
- return "#{self.service_integration.api_url}/wire_transfers"
60
- end
41
+ def _mixin_object_type = "wire_transfer"
61
42
  end
@@ -25,12 +25,59 @@ class Webhookdb::Replicator::IntercomContactV1 < Webhookdb::Replicator::Base
25
25
 
26
26
  def _denormalized_columns
27
27
  return [
28
- Webhookdb::Replicator::Column.new(:external_id, TEXT),
29
- Webhookdb::Replicator::Column.new(:email, TEXT),
30
- Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, converter: :tsat),
31
- Webhookdb::Replicator::Column.new(:updated_at, TIMESTAMP, converter: :tsat),
28
+ # All of these fields are missing on delete.
29
+ # We merge the deleted info into an existing one when handling the upsert.
30
+ Webhookdb::Replicator::Column.new(:external_id, TEXT, optional: true, index: true),
31
+ Webhookdb::Replicator::Column.new(:email, TEXT, optional: true, index: true),
32
+ Webhookdb::Replicator::Column.new(
33
+ :created_at, TIMESTAMP, converter: QUESTIONABLE_TIMESTAMP, optional: true, index: true,
34
+ ),
35
+ Webhookdb::Replicator::Column.new(
36
+ :updated_at, TIMESTAMP, converter: QUESTIONABLE_TIMESTAMP, optional: true, index: true,
37
+ ),
38
+ # This is set in the contact.deleted webhook
39
+ Webhookdb::Replicator::Column.new(:deleted_at, TIMESTAMP, optional: true),
40
+ # This is set in the contact.archived webhook
41
+ Webhookdb::Replicator::Column.new(:archived_at, TIMESTAMP, optional: true),
32
42
  ]
33
43
  end
34
44
 
35
45
  def _mixin_backfill_url = "https://api.intercom.io/contacts"
46
+
47
+ def _resource_and_event(request)
48
+ resource, event = super
49
+ return resource, nil if event.nil?
50
+ # noinspection RubyCaseWithoutElseBlockInspection
51
+ case event.fetch("topic")
52
+ when "contact.deleted"
53
+ resource["updated_at"] = Time.now
54
+ resource["deleted_at"] = Time.now
55
+ when "contact.archived"
56
+ resource["updated_at"] = Time.now
57
+ resource["archived_at"] = Time.now
58
+ when "contact.unsubscribed"
59
+ resource = resource.fetch("contact")
60
+ end
61
+ return resource, event
62
+ end
63
+
64
+ def _upsert_update_expr(inserting, enrichment: nil)
65
+ full_update = super
66
+ # In the case of a delete or archive, update the deleted_at/archived_at field,
67
+ # and merge 'deleted' or 'archived' into the :data field.
68
+ if inserting[:deleted_at]
69
+ status_key = :deleted_at
70
+ status_field = "deleted"
71
+ elsif inserting[:archived_at]
72
+ status_key = :archived_at
73
+ status_field = "archived"
74
+ else
75
+ return full_update
76
+ end
77
+ result = {updated_at: full_update.fetch(:updated_at)}
78
+ result[status_key] = full_update.fetch(status_key)
79
+ data_col = Sequel[self.service_integration.table_name.to_sym][:data]
80
+ result[:data] = Sequel.join([data_col, Sequel.lit("'{\"#{status_field}\":true}'::jsonb")])
81
+ return result
82
+ end
36
83
  end
@@ -25,14 +25,50 @@ class Webhookdb::Replicator::IntercomConversationV1 < Webhookdb::Replicator::Bas
25
25
 
26
26
  def _denormalized_columns
27
27
  return [
28
- Webhookdb::Replicator::Column.new(:title, TEXT),
29
- Webhookdb::Replicator::Column.new(:state, TEXT),
30
- Webhookdb::Replicator::Column.new(:open, BOOLEAN),
31
- Webhookdb::Replicator::Column.new(:read, BOOLEAN),
32
- Webhookdb::Replicator::Column.new(:created_at, TIMESTAMP, converter: :tsat),
33
- Webhookdb::Replicator::Column.new(:updated_at, TIMESTAMP, converter: :tsat),
28
+ Webhookdb::Replicator::Column.new(:title, TEXT, optional: true),
29
+ Webhookdb::Replicator::Column.new(:state, TEXT, optional: true),
30
+ Webhookdb::Replicator::Column.new(:open, BOOLEAN, optional: true),
31
+ Webhookdb::Replicator::Column.new(:read, BOOLEAN, optional: true),
32
+ Webhookdb::Replicator::Column.new(
33
+ :created_at, TIMESTAMP, converter: QUESTIONABLE_TIMESTAMP, optional: true, index: true,
34
+ ),
35
+ Webhookdb::Replicator::Column.new(
36
+ :updated_at, TIMESTAMP, converter: QUESTIONABLE_TIMESTAMP, optional: true, index: true,
37
+ ),
38
+ Webhookdb::Replicator::Column.new(:deleted_at, TIMESTAMP, optional: true, index: true),
34
39
  ]
35
40
  end
36
41
 
37
42
  def _mixin_backfill_url = "https://api.intercom.io/conversations"
43
+
44
+ def _resource_and_event(request)
45
+ resource, event = super
46
+ return resource, nil if event.nil?
47
+ # noinspection RubyCaseWithoutElseBlockInspection
48
+ case event.fetch("topic")
49
+ when "conversation.deleted"
50
+ resource["id"] = resource.fetch("conversation_id")
51
+ resource["updated_at"] = Time.now
52
+ resource["deleted_at"] = Time.now
53
+ when "conversation.contact.attached", "conversation.contact.detached"
54
+ # The convo is in resource['conversation']['model'], and doesn't have a number of fields.
55
+ # This doesn't seem like an important enough event to track for now,
56
+ # unless we start to do it relationally.
57
+ return nil, nil
58
+ end
59
+ return resource, event
60
+ end
61
+
62
+ def _upsert_update_expr(inserting, enrichment: nil)
63
+ full_update = super
64
+ # In the case of a delete, update the deleted_at field and merge 'deleted' into the :data field.
65
+ return full_update unless inserting[:deleted_at]
66
+ data_col = Sequel[self.service_integration.table_name.to_sym][:data]
67
+ result = {
68
+ updated_at: full_update.fetch(:updated_at),
69
+ deleted_at: full_update.fetch(:deleted_at),
70
+ data: Sequel.join([data_col, Sequel.lit("'{\"deleted\":true}'::jsonb")]),
71
+ }
72
+ return result
73
+ end
38
74
  end
@@ -25,17 +25,13 @@ class Webhookdb::Replicator::IntercomMarketplaceRootV1 < Webhookdb::Replicator::
25
25
  return []
26
26
  end
27
27
 
28
- def _upsert_webhook(**_kwargs)
29
- raise NotImplementedError("This is a stub integration only for auth purposes.")
30
- end
28
+ def _upsert_webhook(**_kwargs) = raise NotImplementedError("This is a stub integration only for auth purposes.")
31
29
 
32
30
  def _fetch_backfill_page(*)
33
31
  return [], nil
34
32
  end
35
33
 
36
- def webhook_response(_request)
37
- raise NotImplementedError("This is a stub integration only for auth purposes.")
38
- end
34
+ def webhook_response(_request) = raise NotImplementedError("This is a stub integration only for auth purposes.")
39
35
 
40
36
  def calculate_backfill_state_machine
41
37
  step = Webhookdb::Replicator::StateMachineStep.new
@@ -44,13 +40,6 @@ class Webhookdb::Replicator::IntercomMarketplaceRootV1 < Webhookdb::Replicator::
44
40
  return step
45
41
  end
46
42
 
47
- def get_auth_headers
48
- return {
49
- "Authorization" => "Bearer #{self.service_integration.backfill_key}",
50
- "Accept" => "application/json",
51
- }
52
- end
53
-
54
43
  def build_dependents
55
44
  org = self.service_integration.organization
56
45
  contact_sint = Webhookdb::ServiceIntegration.create_disambiguated(
@@ -1,7 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Webhookdb::Replicator::IntercomV1Mixin
4
- # @return [Webhookdb::ServiceIntegration]
4
+ # Timestamps can be unix timestamps when listing a resource,
5
+ # or strings in other cases, like webhooks. This may have to do with API versions.
6
+ # Handle both.
7
+ QUESTIONABLE_TIMESTAMP = Webhookdb::Replicator::Column::IsomorphicProc.new(
8
+ ruby: lambda do |i, **_|
9
+ return nil if i.nil?
10
+ return Time.at(i)
11
+ rescue TypeError
12
+ return Time.parse(i)
13
+ end,
14
+ sql: lambda do |*|
15
+ # We would have to check the type of the data, which is a pain, so don't worry about this for now.
16
+ raise NotImplementedError
17
+ end,
18
+ )
5
19
 
6
20
  # Quick note on these Intercom integrations: although we will technically be bringing in information from webhooks,
7
21
  # all webhooks for the WebhookDB app will use a single endpoint and we use the WebhookDB app's Client Secret for
@@ -34,20 +48,12 @@ module Webhookdb::Replicator::IntercomV1Mixin
34
48
  return self.qualified_table_sequel_identifier[:updated_at] < Sequel[:excluded][:updated_at]
35
49
  end
36
50
 
37
- def _timestamp_column_name
38
- return :updated_at
39
- end
51
+ def _timestamp_column_name = :updated_at
40
52
 
41
53
  def _webhook_response(request)
42
- # info for debugging
43
- intercom_auth = request.env["HTTP_X_HUB_SIGNATURE"]
44
-
45
- return Webhookdb::WebhookResponse.error("missing hmac") if intercom_auth.nil?
46
- request.body.rewind
47
- request_data = request.body.read
48
- verified = Webhookdb::Intercom.verify_webhook(request_data, intercom_auth)
49
- return Webhookdb::WebhookResponse.ok if verified
50
- return Webhookdb::WebhookResponse.error("invalid hmac")
54
+ # Intercom webhooks are done through a centralized oauth replicator,
55
+ # so the secret is for the app, not the individual replicator.
56
+ return Webhookdb::Intercom.webhook_response(request, Webhookdb::Intercom.client_secret)
51
57
  end
52
58
 
53
59
  # @return [Webhookdb::Replicator::StateMachineStep]
@@ -67,9 +73,7 @@ module Webhookdb::Replicator::IntercomV1Mixin
67
73
  return
68
74
  end
69
75
 
70
- def _mixin_backfill_url
71
- raise NotImplementedError
72
- end
76
+ def _mixin_backfill_url = raise NotImplementedError
73
77
 
74
78
  def _fetch_backfill_page(pagination_token, **_kwargs)
75
79
  unless self.auth_credentials?
@@ -29,7 +29,7 @@ module Webhookdb::Replicator::OAuthRefreshAccessTokenMixin
29
29
  if got
30
30
  yield got
31
31
  else
32
- self.logger.info "creating_access_token"
32
+ self.logger.debug "creating_access_token", access_token_cache_key: key
33
33
  form_body = URI.encode_www_form(
34
34
  {
35
35
  client_id: self.service_integration.backfill_key,
@@ -117,7 +117,7 @@ module Webhookdb::Replicator::SponsyV1Mixin
117
117
  self.find_api_key.blank?
118
118
 
119
119
  publications_svc = self.service_integration.depends_on.replicator
120
- backfillers = publications_svc.readonly_dataset(timeout: :fast) do |pub_ds|
120
+ backfillers = publications_svc.admin_dataset(timeout: :fast) do |pub_ds|
121
121
  pub_ds = Webhookdb::Dbutil.reduce_expr(
122
122
  pub_ds,
123
123
  :|,
@@ -54,6 +54,9 @@ class Webhookdb::Replicator::TransistorEpisodeV1 < Webhookdb::Replicator::Base
54
54
  index: true,
55
55
  data_key: ["attributes", "updated_at"],
56
56
  ),
57
+
58
+ Webhookdb::Replicator::Column.new(:transcript_text, TEXT, optional: true),
59
+
57
60
  # Ideally these would have converters, but they'd be very confusing, and when this was built
58
61
  # we only had one transistor user, so we truncated the table instead.
59
62
  Webhookdb::Replicator::Column.new(:api_format, INTEGER, optional: true),
@@ -93,6 +96,7 @@ class Webhookdb::Replicator::TransistorEpisodeV1 < Webhookdb::Replicator::Base
93
96
  h[:logical_description] = description
94
97
  h[:api_format] = 1
95
98
  end
99
+ h.merge!(enrichment) if enrichment
96
100
  return h
97
101
  end
98
102
 
@@ -133,6 +137,19 @@ class Webhookdb::Replicator::TransistorEpisodeV1 < Webhookdb::Replicator::Base
133
137
 
134
138
  BLOCK_ELEMENT_TAGS = ["p", "div"].freeze
135
139
 
140
+ def _fetch_enrichment(resource, *)
141
+ transcript_url = resource.fetch("attributes").fetch("transcript_url", nil)
142
+ return nil if transcript_url.blank?
143
+ (transcript_url += ".txt") unless transcript_url.end_with?(".txt")
144
+ resp = Webhookdb::Http.get(
145
+ transcript_url,
146
+ logger: self.logger,
147
+ timeout: Webhookdb::Transistor.http_timeout,
148
+ )
149
+ transcript_text = resp.body
150
+ return {transcript_text:}
151
+ end
152
+
136
153
  def upsert_has_deps?
137
154
  return true
138
155
  end