webhookdb 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) 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/040_saved_query_fix_unique.rb +17 -0
  17. data/db/migrations/041_views.rb +20 -0
  18. data/db/migrations/042_sint_lock.rb +10 -0
  19. data/db/migrations/043_text_search.rb +28 -0
  20. data/db/migrations/044_oauth_session_token_cache.rb +21 -0
  21. data/integration/auth_spec.rb +2 -2
  22. data/lib/sequel/plugins/text_searchable.rb +165 -0
  23. data/lib/sequel/text_searchable.rb +42 -0
  24. data/lib/webhookdb/admin_api/auth.rb +24 -3
  25. data/lib/webhookdb/admin_api/data_provider.rb +196 -0
  26. data/lib/webhookdb/admin_api/entities.rb +143 -28
  27. data/lib/webhookdb/admin_api.rb +0 -2
  28. data/lib/webhookdb/api/auth.rb +5 -6
  29. data/lib/webhookdb/api/db.rb +31 -6
  30. data/lib/webhookdb/api/entities.rb +7 -1
  31. data/lib/webhookdb/api/helpers.rb +6 -25
  32. data/lib/webhookdb/api/install.rb +204 -79
  33. data/lib/webhookdb/api/organizations.rb +14 -12
  34. data/lib/webhookdb/api/saved_queries.rb +10 -3
  35. data/lib/webhookdb/api/saved_views.rb +99 -0
  36. data/lib/webhookdb/api/service_integrations.rb +15 -9
  37. data/lib/webhookdb/api/subscriptions.rb +3 -1
  38. data/lib/webhookdb/api/sync_targets.rb +9 -7
  39. data/lib/webhookdb/api/system.rb +1 -0
  40. data/lib/webhookdb/api/webhook_subscriptions.rb +3 -1
  41. data/lib/webhookdb/apps.rb +30 -7
  42. data/lib/webhookdb/async/audit_logger.rb +2 -0
  43. data/lib/webhookdb/async.rb +5 -0
  44. data/lib/webhookdb/backfill_job/service_integration_lock.rb +22 -0
  45. data/lib/webhookdb/backfill_job.rb +9 -0
  46. data/lib/webhookdb/customer.rb +5 -0
  47. data/lib/webhookdb/database_document.rb +1 -1
  48. data/lib/webhookdb/db_adapter/default_sql.rb +1 -1
  49. data/lib/webhookdb/db_adapter.rb +20 -4
  50. data/lib/webhookdb/fixtures/message_bodies.rb +34 -0
  51. data/lib/webhookdb/fixtures/organizations.rb +5 -0
  52. data/lib/webhookdb/fixtures/roles.rb +14 -0
  53. data/lib/webhookdb/fixtures/saved_views.rb +25 -0
  54. data/lib/webhookdb/fixtures/webhook_subscription_deliveries.rb +18 -0
  55. data/lib/webhookdb/http.rb +8 -2
  56. data/lib/webhookdb/icalendar.rb +3 -0
  57. data/lib/webhookdb/idempotency.rb +69 -22
  58. data/lib/webhookdb/increase.rb +69 -21
  59. data/lib/webhookdb/intercom.rb +10 -3
  60. data/lib/webhookdb/jobs/backfill.rb +3 -1
  61. data/lib/webhookdb/jobs/emailer.rb +0 -1
  62. data/lib/webhookdb/jobs/icalendar_delete_stale_cancelled_events.rb +19 -0
  63. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +1 -1
  64. data/lib/webhookdb/jobs/icalendar_sync.rb +1 -1
  65. data/lib/webhookdb/jobs/increase_event_handler.rb +20 -0
  66. data/lib/webhookdb/jobs/scheduled_backfills.rb +2 -1
  67. data/lib/webhookdb/jobs/sync_target_run_sync.rb +3 -1
  68. data/lib/webhookdb/message/body.rb +6 -4
  69. data/lib/webhookdb/message/delivery.rb +2 -0
  70. data/lib/webhookdb/messages/error_icalendar_fetch.rb +1 -2
  71. data/lib/webhookdb/messages/error_signalwire_send_sms.rb +48 -0
  72. data/lib/webhookdb/oauth/fake_provider.rb +44 -0
  73. data/lib/webhookdb/oauth/front_provider.rb +1 -2
  74. data/lib/webhookdb/oauth/increase_provider.rb +80 -0
  75. data/lib/webhookdb/oauth/intercom_provider.rb +3 -11
  76. data/lib/webhookdb/oauth/session.rb +20 -0
  77. data/lib/webhookdb/oauth.rb +7 -21
  78. data/lib/webhookdb/organization/alerting.rb +2 -0
  79. data/lib/webhookdb/organization/database_migration.rb +3 -0
  80. data/lib/webhookdb/organization.rb +37 -6
  81. data/lib/webhookdb/organization_membership.rb +14 -7
  82. data/lib/webhookdb/postgres.rb +2 -0
  83. data/lib/webhookdb/replicator/base.rb +1 -0
  84. data/lib/webhookdb/replicator/docgen.rb +9 -1
  85. data/lib/webhookdb/replicator/fake.rb +2 -3
  86. data/lib/webhookdb/replicator/front_signalwire_message_channel_app_v1.rb +49 -14
  87. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +97 -17
  88. data/lib/webhookdb/replicator/icalendar_event_v1.rb +104 -2
  89. data/lib/webhookdb/replicator/increase_account_number_v1.rb +6 -43
  90. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +7 -24
  91. data/lib/webhookdb/replicator/increase_account_v1.rb +7 -31
  92. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +5 -43
  93. data/lib/webhookdb/replicator/increase_app_v1.rb +78 -0
  94. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +23 -29
  95. data/lib/webhookdb/replicator/increase_event_v1.rb +41 -0
  96. data/lib/webhookdb/replicator/increase_limit_v1.rb +9 -34
  97. data/lib/webhookdb/replicator/increase_transaction_v1.rb +5 -30
  98. data/lib/webhookdb/replicator/increase_v1_mixin.rb +58 -78
  99. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +5 -24
  100. data/lib/webhookdb/replicator/intercom_contact_v1.rb +51 -4
  101. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +42 -6
  102. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +2 -13
  103. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +20 -16
  104. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +1 -1
  105. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +1 -1
  106. data/lib/webhookdb/replicator/transistor_episode_v1.rb +17 -0
  107. data/lib/webhookdb/replicator/url_recorder_v1.rb +137 -0
  108. data/lib/webhookdb/replicator/webhook_request.rb +4 -0
  109. data/lib/webhookdb/replicator.rb +8 -0
  110. data/lib/webhookdb/role.rb +5 -2
  111. data/lib/webhookdb/saved_query.rb +23 -0
  112. data/lib/webhookdb/saved_view.rb +73 -0
  113. data/lib/webhookdb/sentry.rb +2 -0
  114. data/lib/webhookdb/service/entities.rb +0 -4
  115. data/lib/webhookdb/service/helpers.rb +5 -0
  116. data/lib/webhookdb/service/middleware.rb +17 -0
  117. data/lib/webhookdb/service/types.rb +10 -8
  118. data/lib/webhookdb/service/validators.rb +1 -2
  119. data/lib/webhookdb/service/view_api.rb +1 -1
  120. data/lib/webhookdb/service_integration.rb +17 -15
  121. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +8 -8
  122. data/lib/webhookdb/spec_helpers/whdb.rb +3 -2
  123. data/lib/webhookdb/subscription.rb +2 -0
  124. data/lib/webhookdb/sync_target.rb +10 -2
  125. data/lib/webhookdb/tasks/message.rb +3 -1
  126. data/lib/webhookdb/version.rb +1 -1
  127. data/lib/webhookdb/webhook_subscription/delivery.rb +2 -0
  128. data/lib/webhookdb/webhook_subscription.rb +2 -0
  129. metadata +58 -9
  130. data/lib/webhookdb/admin_api/customers.rb +0 -63
  131. data/lib/webhookdb/admin_api/message_deliveries.rb +0 -61
  132. 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