webhookdb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. checksums.yaml +7 -0
  2. data/data/messages/layouts/blank.email.liquid +10 -0
  3. data/data/messages/layouts/minimal.email.liquid +28 -0
  4. data/data/messages/layouts/standard.email.liquid +28 -0
  5. data/data/messages/partials/button.liquid +15 -0
  6. data/data/messages/partials/environment_banner.liquid +9 -0
  7. data/data/messages/partials/footer.liquid +22 -0
  8. data/data/messages/partials/greeting.liquid +3 -0
  9. data/data/messages/partials/logo_header.liquid +18 -0
  10. data/data/messages/partials/signoff.liquid +1 -0
  11. data/data/messages/styles/v1.liquid +346 -0
  12. data/data/messages/templates/errors/icalendar_fetch.email.liquid +29 -0
  13. data/data/messages/templates/invite.email.liquid +15 -0
  14. data/data/messages/templates/new_customer.email.liquid +24 -0
  15. data/data/messages/templates/org_database_migration_finished.email.liquid +7 -0
  16. data/data/messages/templates/org_database_migration_started.email.liquid +9 -0
  17. data/data/messages/templates/specs/_field_partial.liquid +1 -0
  18. data/data/messages/templates/specs/basic.email.liquid +2 -0
  19. data/data/messages/templates/specs/basic.fake.liquid +1 -0
  20. data/data/messages/templates/specs/with_field.email.liquid +2 -0
  21. data/data/messages/templates/specs/with_field.fake.liquid +1 -0
  22. data/data/messages/templates/specs/with_include.email.liquid +2 -0
  23. data/data/messages/templates/specs/with_partial.email.liquid +1 -0
  24. data/data/messages/templates/verification.email.liquid +14 -0
  25. data/data/messages/templates/verification.sms.liquid +1 -0
  26. data/data/messages/web/install-customer-login.liquid +48 -0
  27. data/data/messages/web/install-error.liquid +17 -0
  28. data/data/messages/web/install-success.liquid +35 -0
  29. data/data/messages/web/install.liquid +20 -0
  30. data/data/messages/web/partials/footer.liquid +4 -0
  31. data/data/messages/web/partials/form_error.liquid +1 -0
  32. data/data/messages/web/partials/header.liquid +3 -0
  33. data/data/messages/web/styles.liquid +134 -0
  34. data/data/windows_tz.txt +461 -0
  35. data/db/migrations/001_testing_pixies.rb +13 -0
  36. data/db/migrations/002_initial.rb +132 -0
  37. data/db/migrations/003_ux_overhaul.rb +20 -0
  38. data/db/migrations/004_incremental_backfill.rb +9 -0
  39. data/db/migrations/005_log_webhooks.rb +24 -0
  40. data/db/migrations/006_generalize_roles.rb +29 -0
  41. data/db/migrations/007_org_dns.rb +12 -0
  42. data/db/migrations/008_webhook_subscriptions.rb +19 -0
  43. data/db/migrations/009_nonunique_stripe_subscription_customer.rb +16 -0
  44. data/db/migrations/010_drop_integration_soft_delete.rb +14 -0
  45. data/db/migrations/011_webhook_subscriptions_created_at.rb +10 -0
  46. data/db/migrations/012_webhook_subscriptions_created_by.rb +9 -0
  47. data/db/migrations/013_default_org_membership.rb +30 -0
  48. data/db/migrations/014_webhook_subscription_deliveries.rb +26 -0
  49. data/db/migrations/015_dependent_integrations.rb +9 -0
  50. data/db/migrations/016_encrypted_columns.rb +9 -0
  51. data/db/migrations/017_skip_verification.rb +9 -0
  52. data/db/migrations/018_sync_targets.rb +25 -0
  53. data/db/migrations/019_org_schema.rb +9 -0
  54. data/db/migrations/020_org_database_migrations.rb +25 -0
  55. data/db/migrations/021_no_default_org_schema.rb +14 -0
  56. data/db/migrations/022_database_document.rb +15 -0
  57. data/db/migrations/023_sync_target_schema.rb +9 -0
  58. data/db/migrations/024_org_semaphore_jobs.rb +9 -0
  59. data/db/migrations/025_integration_backfill_cursor.rb +9 -0
  60. data/db/migrations/026_undo_integration_backfill_cursor.rb +9 -0
  61. data/db/migrations/027_sync_target_http_sync.rb +12 -0
  62. data/db/migrations/028_logged_webhook_path.rb +24 -0
  63. data/db/migrations/029_encrypt_columns.rb +97 -0
  64. data/db/migrations/030_org_sync_target_timeout.rb +9 -0
  65. data/db/migrations/031_org_max_query_rows.rb +9 -0
  66. data/db/migrations/032_remove_db_defaults.rb +12 -0
  67. data/db/migrations/033_backfill_jobs.rb +26 -0
  68. data/db/migrations/034_backfill_job_criteria.rb +9 -0
  69. data/db/migrations/035_synchronous_backfill.rb +9 -0
  70. data/db/migrations/036_oauth.rb +26 -0
  71. data/db/migrations/037_oauth_used.rb +9 -0
  72. data/lib/amigo/durable_job.rb +416 -0
  73. data/lib/pry/clipboard.rb +111 -0
  74. data/lib/sequel/advisory_lock.rb +65 -0
  75. data/lib/webhookdb/admin.rb +4 -0
  76. data/lib/webhookdb/admin_api/auth.rb +36 -0
  77. data/lib/webhookdb/admin_api/customers.rb +63 -0
  78. data/lib/webhookdb/admin_api/database_documents.rb +20 -0
  79. data/lib/webhookdb/admin_api/entities.rb +66 -0
  80. data/lib/webhookdb/admin_api/message_deliveries.rb +61 -0
  81. data/lib/webhookdb/admin_api/roles.rb +15 -0
  82. data/lib/webhookdb/admin_api.rb +34 -0
  83. data/lib/webhookdb/aggregate_result.rb +63 -0
  84. data/lib/webhookdb/api/auth.rb +122 -0
  85. data/lib/webhookdb/api/connstr_auth.rb +36 -0
  86. data/lib/webhookdb/api/db.rb +188 -0
  87. data/lib/webhookdb/api/demo.rb +14 -0
  88. data/lib/webhookdb/api/entities.rb +198 -0
  89. data/lib/webhookdb/api/helpers.rb +253 -0
  90. data/lib/webhookdb/api/install.rb +296 -0
  91. data/lib/webhookdb/api/me.rb +53 -0
  92. data/lib/webhookdb/api/organizations.rb +254 -0
  93. data/lib/webhookdb/api/replay.rb +64 -0
  94. data/lib/webhookdb/api/service_integrations.rb +402 -0
  95. data/lib/webhookdb/api/services.rb +27 -0
  96. data/lib/webhookdb/api/stripe.rb +22 -0
  97. data/lib/webhookdb/api/subscriptions.rb +67 -0
  98. data/lib/webhookdb/api/sync_targets.rb +232 -0
  99. data/lib/webhookdb/api/system.rb +37 -0
  100. data/lib/webhookdb/api/webhook_subscriptions.rb +96 -0
  101. data/lib/webhookdb/api.rb +92 -0
  102. data/lib/webhookdb/apps.rb +93 -0
  103. data/lib/webhookdb/async/audit_logger.rb +38 -0
  104. data/lib/webhookdb/async/autoscaler.rb +84 -0
  105. data/lib/webhookdb/async/job.rb +18 -0
  106. data/lib/webhookdb/async/job_logger.rb +45 -0
  107. data/lib/webhookdb/async/scheduled_job.rb +18 -0
  108. data/lib/webhookdb/async.rb +142 -0
  109. data/lib/webhookdb/aws.rb +98 -0
  110. data/lib/webhookdb/backfill_job.rb +107 -0
  111. data/lib/webhookdb/backfiller.rb +107 -0
  112. data/lib/webhookdb/cloudflare.rb +39 -0
  113. data/lib/webhookdb/connection_cache.rb +177 -0
  114. data/lib/webhookdb/console.rb +71 -0
  115. data/lib/webhookdb/convertkit.rb +14 -0
  116. data/lib/webhookdb/crypto.rb +66 -0
  117. data/lib/webhookdb/customer/reset_code.rb +94 -0
  118. data/lib/webhookdb/customer.rb +347 -0
  119. data/lib/webhookdb/database_document.rb +72 -0
  120. data/lib/webhookdb/db_adapter/column_types.rb +37 -0
  121. data/lib/webhookdb/db_adapter/default_sql.rb +187 -0
  122. data/lib/webhookdb/db_adapter/pg.rb +96 -0
  123. data/lib/webhookdb/db_adapter/snowflake.rb +137 -0
  124. data/lib/webhookdb/db_adapter.rb +208 -0
  125. data/lib/webhookdb/dbutil.rb +92 -0
  126. data/lib/webhookdb/demo_mode.rb +100 -0
  127. data/lib/webhookdb/developer_alert.rb +51 -0
  128. data/lib/webhookdb/email_octopus.rb +21 -0
  129. data/lib/webhookdb/enumerable.rb +18 -0
  130. data/lib/webhookdb/fixtures/backfill_jobs.rb +72 -0
  131. data/lib/webhookdb/fixtures/customers.rb +65 -0
  132. data/lib/webhookdb/fixtures/database_documents.rb +27 -0
  133. data/lib/webhookdb/fixtures/faker.rb +41 -0
  134. data/lib/webhookdb/fixtures/logged_webhooks.rb +56 -0
  135. data/lib/webhookdb/fixtures/message_deliveries.rb +59 -0
  136. data/lib/webhookdb/fixtures/oauth_sessions.rb +24 -0
  137. data/lib/webhookdb/fixtures/organization_database_migrations.rb +37 -0
  138. data/lib/webhookdb/fixtures/organization_memberships.rb +54 -0
  139. data/lib/webhookdb/fixtures/organizations.rb +32 -0
  140. data/lib/webhookdb/fixtures/reset_codes.rb +23 -0
  141. data/lib/webhookdb/fixtures/service_integrations.rb +42 -0
  142. data/lib/webhookdb/fixtures/subscriptions.rb +33 -0
  143. data/lib/webhookdb/fixtures/sync_targets.rb +32 -0
  144. data/lib/webhookdb/fixtures/webhook_subscriptions.rb +35 -0
  145. data/lib/webhookdb/fixtures.rb +15 -0
  146. data/lib/webhookdb/formatting.rb +56 -0
  147. data/lib/webhookdb/front.rb +49 -0
  148. data/lib/webhookdb/github.rb +22 -0
  149. data/lib/webhookdb/google_calendar.rb +29 -0
  150. data/lib/webhookdb/heroku.rb +21 -0
  151. data/lib/webhookdb/http.rb +114 -0
  152. data/lib/webhookdb/icalendar.rb +17 -0
  153. data/lib/webhookdb/id.rb +17 -0
  154. data/lib/webhookdb/idempotency.rb +90 -0
  155. data/lib/webhookdb/increase.rb +42 -0
  156. data/lib/webhookdb/intercom.rb +23 -0
  157. data/lib/webhookdb/jobs/amigo_test_jobs.rb +118 -0
  158. data/lib/webhookdb/jobs/backfill.rb +32 -0
  159. data/lib/webhookdb/jobs/create_mirror_table.rb +18 -0
  160. data/lib/webhookdb/jobs/create_stripe_customer.rb +17 -0
  161. data/lib/webhookdb/jobs/customer_created_notify_internal.rb +22 -0
  162. data/lib/webhookdb/jobs/demo_mode_sync_data.rb +19 -0
  163. data/lib/webhookdb/jobs/deprecated_jobs.rb +19 -0
  164. data/lib/webhookdb/jobs/developer_alert_handle.rb +14 -0
  165. data/lib/webhookdb/jobs/durable_job_recheck_poller.rb +17 -0
  166. data/lib/webhookdb/jobs/emailer.rb +15 -0
  167. data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +25 -0
  168. data/lib/webhookdb/jobs/icalendar_sync.rb +23 -0
  169. data/lib/webhookdb/jobs/logged_webhook_replay.rb +17 -0
  170. data/lib/webhookdb/jobs/logged_webhook_resilient_replay.rb +15 -0
  171. data/lib/webhookdb/jobs/message_dispatched.rb +16 -0
  172. data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +21 -0
  173. data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +21 -0
  174. data/lib/webhookdb/jobs/organization_database_migration_run.rb +24 -0
  175. data/lib/webhookdb/jobs/prepare_database_connections.rb +22 -0
  176. data/lib/webhookdb/jobs/process_webhook.rb +47 -0
  177. data/lib/webhookdb/jobs/renew_watch_channel.rb +24 -0
  178. data/lib/webhookdb/jobs/replication_migration.rb +24 -0
  179. data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +23 -0
  180. data/lib/webhookdb/jobs/scheduled_backfills.rb +77 -0
  181. data/lib/webhookdb/jobs/send_invite.rb +15 -0
  182. data/lib/webhookdb/jobs/send_test_webhook.rb +25 -0
  183. data/lib/webhookdb/jobs/send_webhook.rb +20 -0
  184. data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +16 -0
  185. data/lib/webhookdb/jobs/sync_target_run_sync.rb +38 -0
  186. data/lib/webhookdb/jobs/trim_logged_webhooks.rb +15 -0
  187. data/lib/webhookdb/jobs/webhook_resource_notify_integrations.rb +30 -0
  188. data/lib/webhookdb/jobs/webhook_subscription_delivery_attempt.rb +29 -0
  189. data/lib/webhookdb/jobs.rb +4 -0
  190. data/lib/webhookdb/json.rb +113 -0
  191. data/lib/webhookdb/liquid/expose.rb +27 -0
  192. data/lib/webhookdb/liquid/filters.rb +16 -0
  193. data/lib/webhookdb/liquid/liquification.rb +26 -0
  194. data/lib/webhookdb/liquid/partial.rb +12 -0
  195. data/lib/webhookdb/logged_webhook/resilient.rb +95 -0
  196. data/lib/webhookdb/logged_webhook.rb +194 -0
  197. data/lib/webhookdb/message/body.rb +25 -0
  198. data/lib/webhookdb/message/delivery.rb +127 -0
  199. data/lib/webhookdb/message/email_transport.rb +133 -0
  200. data/lib/webhookdb/message/fake_transport.rb +54 -0
  201. data/lib/webhookdb/message/liquid_drops.rb +29 -0
  202. data/lib/webhookdb/message/template.rb +89 -0
  203. data/lib/webhookdb/message/transport.rb +43 -0
  204. data/lib/webhookdb/message.rb +150 -0
  205. data/lib/webhookdb/messages/error_icalendar_fetch.rb +42 -0
  206. data/lib/webhookdb/messages/invite.rb +23 -0
  207. data/lib/webhookdb/messages/new_customer.rb +14 -0
  208. data/lib/webhookdb/messages/org_database_migration_finished.rb +23 -0
  209. data/lib/webhookdb/messages/org_database_migration_started.rb +24 -0
  210. data/lib/webhookdb/messages/specs.rb +57 -0
  211. data/lib/webhookdb/messages/verification.rb +23 -0
  212. data/lib/webhookdb/method_utilities.rb +82 -0
  213. data/lib/webhookdb/microsoft_calendar.rb +36 -0
  214. data/lib/webhookdb/nextpax.rb +14 -0
  215. data/lib/webhookdb/oauth/front.rb +58 -0
  216. data/lib/webhookdb/oauth/intercom.rb +58 -0
  217. data/lib/webhookdb/oauth/session.rb +24 -0
  218. data/lib/webhookdb/oauth.rb +80 -0
  219. data/lib/webhookdb/organization/alerting.rb +35 -0
  220. data/lib/webhookdb/organization/database_migration.rb +151 -0
  221. data/lib/webhookdb/organization/db_builder.rb +429 -0
  222. data/lib/webhookdb/organization.rb +506 -0
  223. data/lib/webhookdb/organization_membership.rb +58 -0
  224. data/lib/webhookdb/phone_number.rb +38 -0
  225. data/lib/webhookdb/plaid.rb +23 -0
  226. data/lib/webhookdb/platform.rb +27 -0
  227. data/lib/webhookdb/plivo.rb +52 -0
  228. data/lib/webhookdb/postgres/maintenance.rb +166 -0
  229. data/lib/webhookdb/postgres/model.rb +82 -0
  230. data/lib/webhookdb/postgres/model_utilities.rb +382 -0
  231. data/lib/webhookdb/postgres/testing_pixie.rb +16 -0
  232. data/lib/webhookdb/postgres/validations.rb +46 -0
  233. data/lib/webhookdb/postgres.rb +176 -0
  234. data/lib/webhookdb/postmark.rb +20 -0
  235. data/lib/webhookdb/redis.rb +35 -0
  236. data/lib/webhookdb/replicator/atom_single_feed_v1.rb +116 -0
  237. data/lib/webhookdb/replicator/aws_pricing_v1.rb +488 -0
  238. data/lib/webhookdb/replicator/base.rb +1185 -0
  239. data/lib/webhookdb/replicator/column.rb +482 -0
  240. data/lib/webhookdb/replicator/convertkit_broadcast_v1.rb +69 -0
  241. data/lib/webhookdb/replicator/convertkit_subscriber_v1.rb +200 -0
  242. data/lib/webhookdb/replicator/convertkit_tag_v1.rb +66 -0
  243. data/lib/webhookdb/replicator/convertkit_v1_mixin.rb +65 -0
  244. data/lib/webhookdb/replicator/docgen.rb +167 -0
  245. data/lib/webhookdb/replicator/email_octopus_campaign_v1.rb +84 -0
  246. data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +159 -0
  247. data/lib/webhookdb/replicator/email_octopus_event_v1.rb +244 -0
  248. data/lib/webhookdb/replicator/email_octopus_list_v1.rb +101 -0
  249. data/lib/webhookdb/replicator/fake.rb +453 -0
  250. data/lib/webhookdb/replicator/front_conversation_v1.rb +45 -0
  251. data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +55 -0
  252. data/lib/webhookdb/replicator/front_message_v1.rb +45 -0
  253. data/lib/webhookdb/replicator/front_v1_mixin.rb +22 -0
  254. data/lib/webhookdb/replicator/github_issue_comment_v1.rb +58 -0
  255. data/lib/webhookdb/replicator/github_issue_v1.rb +83 -0
  256. data/lib/webhookdb/replicator/github_pull_v1.rb +84 -0
  257. data/lib/webhookdb/replicator/github_release_v1.rb +47 -0
  258. data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +250 -0
  259. data/lib/webhookdb/replicator/github_repository_event_v1.rb +45 -0
  260. data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +465 -0
  261. data/lib/webhookdb/replicator/icalendar_event_v1.rb +334 -0
  262. data/lib/webhookdb/replicator/increase_account_number_v1.rb +77 -0
  263. data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +61 -0
  264. data/lib/webhookdb/replicator/increase_account_v1.rb +63 -0
  265. data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +78 -0
  266. data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +64 -0
  267. data/lib/webhookdb/replicator/increase_limit_v1.rb +78 -0
  268. data/lib/webhookdb/replicator/increase_transaction_v1.rb +74 -0
  269. data/lib/webhookdb/replicator/increase_v1_mixin.rb +121 -0
  270. data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +61 -0
  271. data/lib/webhookdb/replicator/intercom_contact_v1.rb +36 -0
  272. data/lib/webhookdb/replicator/intercom_conversation_v1.rb +38 -0
  273. data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +69 -0
  274. data/lib/webhookdb/replicator/intercom_v1_mixin.rb +105 -0
  275. data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +65 -0
  276. data/lib/webhookdb/replicator/plivo_sms_inbound_v1.rb +102 -0
  277. data/lib/webhookdb/replicator/postmark_inbound_message_v1.rb +94 -0
  278. data/lib/webhookdb/replicator/postmark_outbound_message_event_v1.rb +107 -0
  279. data/lib/webhookdb/replicator/schema_modification.rb +42 -0
  280. data/lib/webhookdb/replicator/shopify_customer_v1.rb +58 -0
  281. data/lib/webhookdb/replicator/shopify_order_v1.rb +64 -0
  282. data/lib/webhookdb/replicator/shopify_v1_mixin.rb +161 -0
  283. data/lib/webhookdb/replicator/signalwire_message_v1.rb +169 -0
  284. data/lib/webhookdb/replicator/sponsy_customer_v1.rb +54 -0
  285. data/lib/webhookdb/replicator/sponsy_placement_v1.rb +34 -0
  286. data/lib/webhookdb/replicator/sponsy_publication_v1.rb +125 -0
  287. data/lib/webhookdb/replicator/sponsy_slot_v1.rb +41 -0
  288. data/lib/webhookdb/replicator/sponsy_status_v1.rb +35 -0
  289. data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +165 -0
  290. data/lib/webhookdb/replicator/state_machine_step.rb +69 -0
  291. data/lib/webhookdb/replicator/stripe_charge_v1.rb +77 -0
  292. data/lib/webhookdb/replicator/stripe_coupon_v1.rb +62 -0
  293. data/lib/webhookdb/replicator/stripe_customer_v1.rb +60 -0
  294. data/lib/webhookdb/replicator/stripe_dispute_v1.rb +77 -0
  295. data/lib/webhookdb/replicator/stripe_invoice_item_v1.rb +82 -0
  296. data/lib/webhookdb/replicator/stripe_invoice_v1.rb +116 -0
  297. data/lib/webhookdb/replicator/stripe_payout_v1.rb +67 -0
  298. data/lib/webhookdb/replicator/stripe_price_v1.rb +60 -0
  299. data/lib/webhookdb/replicator/stripe_product_v1.rb +60 -0
  300. data/lib/webhookdb/replicator/stripe_refund_v1.rb +101 -0
  301. data/lib/webhookdb/replicator/stripe_subscription_item_v1.rb +56 -0
  302. data/lib/webhookdb/replicator/stripe_subscription_v1.rb +75 -0
  303. data/lib/webhookdb/replicator/stripe_v1_mixin.rb +116 -0
  304. data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +141 -0
  305. data/lib/webhookdb/replicator/transistor_episode_v1.rb +169 -0
  306. data/lib/webhookdb/replicator/transistor_show_v1.rb +68 -0
  307. data/lib/webhookdb/replicator/transistor_v1_mixin.rb +65 -0
  308. data/lib/webhookdb/replicator/twilio_sms_v1.rb +156 -0
  309. data/lib/webhookdb/replicator/webhook_request.rb +5 -0
  310. data/lib/webhookdb/replicator/webhookdb_customer_v1.rb +74 -0
  311. data/lib/webhookdb/replicator.rb +224 -0
  312. data/lib/webhookdb/role.rb +42 -0
  313. data/lib/webhookdb/sentry.rb +35 -0
  314. data/lib/webhookdb/service/auth.rb +138 -0
  315. data/lib/webhookdb/service/collection.rb +91 -0
  316. data/lib/webhookdb/service/entities.rb +97 -0
  317. data/lib/webhookdb/service/helpers.rb +270 -0
  318. data/lib/webhookdb/service/middleware.rb +124 -0
  319. data/lib/webhookdb/service/types.rb +30 -0
  320. data/lib/webhookdb/service/validators.rb +32 -0
  321. data/lib/webhookdb/service/view_api.rb +63 -0
  322. data/lib/webhookdb/service.rb +219 -0
  323. data/lib/webhookdb/service_integration.rb +332 -0
  324. data/lib/webhookdb/shopify.rb +35 -0
  325. data/lib/webhookdb/signalwire.rb +13 -0
  326. data/lib/webhookdb/slack.rb +68 -0
  327. data/lib/webhookdb/snowflake.rb +90 -0
  328. data/lib/webhookdb/spec_helpers/async.rb +122 -0
  329. data/lib/webhookdb/spec_helpers/citest.rb +88 -0
  330. data/lib/webhookdb/spec_helpers/integration.rb +121 -0
  331. data/lib/webhookdb/spec_helpers/message.rb +41 -0
  332. data/lib/webhookdb/spec_helpers/postgres.rb +220 -0
  333. data/lib/webhookdb/spec_helpers/service.rb +432 -0
  334. data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +56 -0
  335. data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +915 -0
  336. data/lib/webhookdb/spec_helpers/whdb.rb +139 -0
  337. data/lib/webhookdb/spec_helpers.rb +63 -0
  338. data/lib/webhookdb/sponsy.rb +14 -0
  339. data/lib/webhookdb/stripe.rb +37 -0
  340. data/lib/webhookdb/subscription.rb +203 -0
  341. data/lib/webhookdb/sync_target.rb +491 -0
  342. data/lib/webhookdb/tasks/admin.rb +49 -0
  343. data/lib/webhookdb/tasks/annotate.rb +36 -0
  344. data/lib/webhookdb/tasks/db.rb +82 -0
  345. data/lib/webhookdb/tasks/docs.rb +42 -0
  346. data/lib/webhookdb/tasks/fixture.rb +35 -0
  347. data/lib/webhookdb/tasks/message.rb +50 -0
  348. data/lib/webhookdb/tasks/regress.rb +87 -0
  349. data/lib/webhookdb/tasks/release.rb +27 -0
  350. data/lib/webhookdb/tasks/sidekiq.rb +23 -0
  351. data/lib/webhookdb/tasks/specs.rb +64 -0
  352. data/lib/webhookdb/theranest.rb +15 -0
  353. data/lib/webhookdb/transistor.rb +13 -0
  354. data/lib/webhookdb/twilio.rb +13 -0
  355. data/lib/webhookdb/typed_struct.rb +44 -0
  356. data/lib/webhookdb/version.rb +5 -0
  357. data/lib/webhookdb/webhook_response.rb +50 -0
  358. data/lib/webhookdb/webhook_subscription/delivery.rb +82 -0
  359. data/lib/webhookdb/webhook_subscription.rb +226 -0
  360. data/lib/webhookdb/windows_tz.rb +32 -0
  361. data/lib/webhookdb/xml.rb +92 -0
  362. data/lib/webhookdb.rb +224 -0
  363. data/lib/webterm/apps.rb +45 -0
  364. metadata +1129 -0
@@ -0,0 +1,465 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "down"
4
+ require "ice_cube"
5
+
6
+ require "webhookdb/icalendar"
7
+ require "webhookdb/jobs/icalendar_sync"
8
+ require "webhookdb/messages/error_icalendar_fetch"
9
+
10
+ class Webhookdb::Replicator::IcalendarCalendarV1 < Webhookdb::Replicator::Base
11
+ include Appydays::Loggable
12
+
13
+ RECURRENCE_PROJECTION = 5.years
14
+
15
+ def documentation_url = Webhookdb::Icalendar::DOCUMENTATION_URL
16
+
17
+ # @return [Webhookdb::Replicator::Descriptor]
18
+ def self.descriptor
19
+ return Webhookdb::Replicator::Descriptor.new(
20
+ name: "icalendar_calendar_v1",
21
+ ctor: ->(sint) { Webhookdb::Replicator::IcalendarCalendarV1.new(sint) },
22
+ feature_roles: [],
23
+ resource_name_singular: "iCalendar Calendar",
24
+ supports_webhooks: true,
25
+ description: "Fetch and convert an icalendar format file into a schematized and queryable database table.",
26
+ api_docs_url: "https://icalendar.org/",
27
+ )
28
+ end
29
+
30
+ def upsert_has_deps? = true
31
+
32
+ def _webhook_response(request)
33
+ return Webhookdb::WebhookResponse.for_standard_secret(request, self.service_integration.webhook_secret)
34
+ end
35
+
36
+ def calculate_webhook_state_machine
37
+ step = Webhookdb::Replicator::StateMachineStep.new
38
+ if self.service_integration.webhook_secret.blank?
39
+ self.service_integration.save_changes
40
+ step.output = %(You are about to add support for replicating iCalendar (.ics) URLs into WebhookDB.
41
+
42
+ We have detailed instructions on this process
43
+ at https://docs.webhookdb.com/guides/icalendar/.
44
+
45
+ The first step is to generate a secret you will use for signing
46
+ API requests you send to WebhookDB. You can use '#{Webhookdb::Id.rand_enc(16)}'
47
+ or generate your own value.
48
+ Copy and paste or enter a new value, and press enter.)
49
+ return step.secret_prompt("secret").webhook_secret(self.service_integration)
50
+ end
51
+ step.output = %(
52
+ All set! Here is the endpoint to send requests to
53
+ from your backend. Refer to https://docs.webhookdb.com/guides/icalendar/
54
+ for details on the format of the request:
55
+
56
+ #{self.webhook_endpoint}
57
+
58
+ The secret to use for signing is:
59
+
60
+ #{self.service_integration.webhook_secret}
61
+
62
+ #{self._query_help_output})
63
+ return step.completed
64
+ end
65
+
66
+ def _remote_key_column
67
+ return Webhookdb::Replicator::Column.new(:external_id, TEXT)
68
+ end
69
+
70
+ def _denormalized_columns
71
+ col = Webhookdb::Replicator::Column
72
+ return [
73
+ col.new(:row_created_at, TIMESTAMP, index: true, optional: true, defaulter: :now),
74
+ col.new(:row_updated_at, TIMESTAMP, index: true, optional: true, defaulter: :now),
75
+ col.new(:last_synced_at, TIMESTAMP, index: true, optional: true),
76
+ col.new(:ics_url, TEXT, converter: col.converter_gsub("^webcal", "https")),
77
+ ]
78
+ end
79
+
80
+ def _timestamp_column_name
81
+ return :row_updated_at
82
+ end
83
+
84
+ def _resource_and_event(request)
85
+ return request.body, nil
86
+ end
87
+
88
+ def _update_where_expr
89
+ return self.qualified_table_sequel_identifier[:row_updated_at] < Sequel[:excluded][:row_updated_at]
90
+ end
91
+
92
+ def _upsert_update_expr(inserting, **_kwargs)
93
+ update = super
94
+ # Only set created_at if it's not set so the initial insert isn't modified.
95
+ self._coalesce_excluded_on_update(update, [:row_created_at])
96
+ return update
97
+ end
98
+
99
+ def _resource_to_data(resource, *)
100
+ data = resource.dup
101
+ # Remove the client-provided webhook fields.
102
+ data.clear
103
+ return data
104
+ end
105
+
106
+ def upsert_webhook(request)
107
+ request_type = request.body.fetch("type")
108
+ external_id = request.body.fetch("external_id")
109
+ case request_type
110
+ when "SYNC"
111
+ super(request)
112
+ Webhookdb::Jobs::IcalendarSync.perform_async(self.service_integration.id, external_id)
113
+ return
114
+ when "DELETE"
115
+ self.delete_data_for_external_id(external_id)
116
+ return
117
+ when "__WHDB_UNIT_TEST"
118
+ unless Webhookdb::RACK_ENV == "test"
119
+ raise "someone tried to use the special unit test google event type outside of unit tests"
120
+ end
121
+ return super(request)
122
+ else
123
+ raise ArgumentError, "Unknown request type: #{request_type}"
124
+ end
125
+ end
126
+
127
+ CLEANUP_SERVICE_NAMES = ["icalendar_event_v1"].freeze
128
+ SYNC_PERIOD = 4.hours
129
+
130
+ def rows_needing_sync(dataset, now: Time.now)
131
+ cutoff = now - SYNC_PERIOD
132
+ return dataset.where(Sequel[last_synced_at: nil] | Sequel.expr { last_synced_at < cutoff })
133
+ end
134
+
135
+ def delete_data_for_external_id(external_id)
136
+ relevant_integrations = self.service_integration.recursive_dependents.
137
+ filter { |d| CLEANUP_SERVICE_NAMES.include?(d.service_name) }
138
+ self.admin_dataset do |ds|
139
+ ds.db.transaction do
140
+ ds.where(external_id:).delete
141
+ relevant_integrations.each do |sint|
142
+ ds.db[sint.replicator.qualified_table_sequel_identifier].where(calendar_external_id: external_id).delete
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ class Upserter
149
+ include Webhookdb::Backfiller::Bulk
150
+ attr_reader :upserting_replicator, :calendar_external_id
151
+
152
+ def initialize(replicator, calendar_external_id)
153
+ @upserting_replicator = replicator
154
+ @calendar_external_id = calendar_external_id
155
+ end
156
+
157
+ def upsert_page_size = 500
158
+ def conditional_upsert? = true
159
+
160
+ def prepare_body(body)
161
+ body["calendar_external_id"] = @calendar_external_id
162
+ end
163
+ end
164
+
165
+ def sync_row(row)
166
+ Appydays::Loggable.with_log_tags(icalendar_url: row.fetch(:ics_url)) do
167
+ self.with_advisory_lock(row.fetch(:pk)) do
168
+ if (dep = self.find_dependent("icalendar_event_v1"))
169
+ self._sync_row(row, dep)
170
+ end
171
+ self.admin_dataset { |ds| ds.where(pk: row.fetch(:pk)).update(last_synced_at: Time.now) }
172
+ end
173
+ end
174
+ end
175
+
176
+ def _sync_row(row, dep)
177
+ calendar_external_id = row.fetch(:external_id)
178
+ request_url = row.fetch(:ics_url)
179
+ begin
180
+ io = Webhookdb::Http.chunked_download(request_url, rewindable: false)
181
+ rescue Down::Error => e
182
+ self._handle_down_error(e, request_url:, calendar_external_id:)
183
+ return
184
+ end
185
+
186
+ upserter = Upserter.new(dep.replicator, calendar_external_id)
187
+ processor = EventProcessor.new(io, upserter)
188
+ processor.process
189
+ # Delete all the extra replicator rows, and cancel all the rows that weren't upserted.
190
+ dep.replicator.admin_dataset do |ds|
191
+ ds = ds.where(calendar_external_id:)
192
+ if (delete_condition = processor.delete_condition)
193
+ ds.where(delete_condition).delete
194
+ end
195
+ # Update both the status, and set the data json to match.
196
+ ds.exclude(compound_identity: processor.upserted_identities).update(
197
+ status: "CANCELLED",
198
+ data: Sequel.lit('data || \'{"STATUS":{"v":"CANCELLED"}}\'::jsonb'),
199
+ )
200
+ end
201
+ self.admin_dataset { |ds| ds.where(pk: row.fetch(:pk)).update(last_synced_at: Time.now) }
202
+ end
203
+
204
+ def _handle_down_error(e, request_url:, calendar_external_id:)
205
+ case e
206
+ when Down::TooManyRedirects
207
+ response_status = 301
208
+ response_body = "<too many redirects>"
209
+ when Down::TimeoutError, Down::SSLError, Down::ConnectionError, Down::InvalidUrl
210
+ response_status = 0
211
+ response_body = e.to_s
212
+ when Down::ClientError
213
+ raise e if e.response.nil?
214
+ response_status = e.response.code.to_i
215
+ expected_errors = [
216
+ 417, # If someone uses an Outlook HTML calendar, fetch gives us a 417
217
+ ]
218
+ is_problem_error = (response_status > 404 || response_status < 400) &&
219
+ !expected_errors.include?(response_status)
220
+ raise e if is_problem_error
221
+ response_body = e.response.body.to_s
222
+ when Down::ServerError
223
+ response_status = e.response.code.to_i
224
+ response_body = e.response.body.to_s
225
+ else
226
+ response_body = nil
227
+ response_status = nil
228
+ end
229
+ raise e if response_status.nil?
230
+ self.logger.warn("icalendar_fetch_error",
231
+ response_body:, response_status:, request_url:, calendar_external_id:,)
232
+ message = Webhookdb::Messages::ErrorIcalendarFetch.new(
233
+ self.service_integration,
234
+ calendar_external_id,
235
+ response_status:,
236
+ response_body:,
237
+ request_url:,
238
+ request_method: "GET",
239
+ )
240
+ self.service_integration.organization.alerting.dispatch_alert(message)
241
+ end
242
+
243
+ class EventProcessor
244
+ attr_reader :upserted_identities
245
+
246
+ def initialize(io, upserter)
247
+ @io = io
248
+ @upserter = upserter
249
+ # Keep track of everything we upsert. For any rows we aren't upserting,
250
+ # delete them if they're recurring, or cancel them if they're not recurring.
251
+ # If doing it this way is slow, we could invert this (pull down all IDs and pop from the set).
252
+ @upserted_identities = []
253
+ # Keep track of all upserted recurring items.
254
+ # If we find a RECURRENCE-ID on a later item,
255
+ # we need to modify the item from the sequence by stealing its compound identity.
256
+ @expanded_events_by_uid = {}
257
+ # Delete 'extra' recurring event rows.
258
+ # We need to keep track of how many events each UID spawns,
259
+ # so we can delete any with a higher count.
260
+ @max_sequence_num_by_uid = {}
261
+ end
262
+
263
+ def delete_condition
264
+ return nil if @max_sequence_num_by_uid.empty?
265
+ return @max_sequence_num_by_uid.map do |uid, n|
266
+ Sequel[recurring_event_id: uid] & (Sequel[:recurring_event_sequence] > n)
267
+ end.inject(&:|)
268
+ end
269
+
270
+ def process
271
+ self.each_feed_event do |feed_event|
272
+ self.each_projected_event(feed_event) do |ev|
273
+ ident, upserted = @upserter.handle_item(ev)
274
+ @upserted_identities << ident
275
+ if (recurring_uid = upserted.fetch(:recurring_event_id))
276
+ @expanded_events_by_uid[recurring_uid] ||= []
277
+ @expanded_events_by_uid[recurring_uid] << upserted
278
+ end
279
+ end
280
+ end
281
+ @upserter.flush_pending_inserts
282
+ end
283
+
284
+ def each_projected_event(h)
285
+ raise LocalJumpError unless block_given?
286
+
287
+ uid = h.fetch("UID").fetch("v")
288
+
289
+ if (recurrence_id = h["RECURRENCE-ID"])
290
+ # Track down the original item in the projected sequence, so we can update it.
291
+ if Webhookdb::Replicator::IcalendarEventV1.value_is_date_str?(recurrence_id.fetch("v"))
292
+ start = Webhookdb::Replicator::IcalendarEventV1.entry_to_date(recurrence_id)
293
+ startfield = :start_date
294
+ else
295
+ startfield = :start_at
296
+ start = Webhookdb::Replicator::IcalendarEventV1.entry_to_datetime(recurrence_id).first
297
+ end
298
+ candidates = @expanded_events_by_uid[uid]
299
+ if candidates.nil?
300
+ # We can have no recurring events, even with the exclusion date.
301
+ # Not much we can do here- just treat it as a standalone event.
302
+ yield h
303
+ return
304
+ end
305
+ unless (match = candidates.find { |c| c[startfield] == start })
306
+ # There are some providers (like Apple) where an excluded event
307
+ # will be outside the bounds of the RRULE of its owner.
308
+ # Usually the RRULE has an UNTIL that is before the RECURRENCE-ID datetime.
309
+ #
310
+ # In these cases, we can use the event as-is, but we need to
311
+ # make sure it is treated as part of the sequence.
312
+ # So increment the last-seen sequence number for the UID and use that.
313
+ max_seq_num = @max_sequence_num_by_uid[uid] += 1
314
+ h["UID"] = {"v" => "#{uid}-#{max_seq_num}"}
315
+ h["recurring_event_id"] = uid
316
+ h["recurring_event_sequence"] = max_seq_num
317
+ yield h
318
+ return
319
+ end
320
+
321
+ # Steal the UID to overwrite the original, and record where it came from.
322
+ # Note that all other fields, like categories, will be overwritten with the fields in this exclusion.
323
+ # This seems to be correct, but we should keep an eye open in case we need to merge
324
+ # these exclusion events into the originals.
325
+ h["UID"] = {"v" => match[:uid]}
326
+ h["recurring_event_sequence"] = match[:recurring_event_sequence]
327
+ # Usually the recurrent event and exclusion have the same last-modified.
328
+ # But we need to set the last-modified to AFTER the original,
329
+ # to make sure it replaces what's in the database (the original un-excluded event
330
+ # may already be present in the database).
331
+ h["LAST-MODIFIED"] = match.fetch(:last_modified_at) + 1.second
332
+ yield h
333
+ return
334
+ end
335
+
336
+ unless h["RRULE"]
337
+ yield h
338
+ return
339
+ end
340
+
341
+ # We need to convert relevant parsed ical lines back to a string for use in ice_cube.
342
+ # There are other ways to handle this, but this is fine for now.
343
+ ical_params = {}
344
+ if (exdates = h["RDATE"])
345
+ ical_params[:rtimes] = exdates.map { |d| self._time_array(d) }.flatten
346
+ end
347
+ if (exdates = h["EXDATE"])
348
+ ical_params[:extimes] = exdates.map { |d| self._time_array(d) }.flatten
349
+ end
350
+ ical_params[:rrules] = [self._icecube_rule_from_ical(h["RRULE"]["v"])] if h["RRULE"]
351
+ # DURATION is not supported
352
+
353
+ start_entry = h.fetch("DTSTART")
354
+ ev_replicator = Webhookdb::Replicator::IcalendarEventV1
355
+ is_date = ev_replicator.entry_is_date_str?(start_entry)
356
+ # Use actual Times for start/end since ice_cube doesn't parse them well
357
+ ical_params[:start_time] = ev_replicator.entry_to_date_or_datetime(start_entry).first
358
+ if ical_params[:start_time].year < 1000
359
+ # This is almost definitely a misconfiguration. Yield it as non-recurring and move on.
360
+ yield h
361
+ return
362
+ end
363
+ has_end_time = false
364
+ if (end_entry = h["DTEND"])
365
+ # the end date is optional. If we don't have one, we should never store one.
366
+ has_end_time = true
367
+ ical_params[:end_time] = ev_replicator.entry_to_date_or_datetime(end_entry).first
368
+ if ical_params[:end_time] < ical_params[:start_time]
369
+ # This is an invalid event. Not sure what it'll do to IceCube so don't send it there.
370
+ # Yield it as a non-recurring event and move on.
371
+ yield h
372
+ return
373
+ end
374
+ end
375
+
376
+ schedule = IceCube::Schedule.from_hash(ical_params)
377
+ dont_project_before = Webhookdb::Icalendar.oldest_recurring_event
378
+ dont_project_after = Time.now + RECURRENCE_PROJECTION
379
+
380
+ # Just like google, track the original event id.
381
+ h["recurring_event_id"] = uid
382
+ final_sequence = -1
383
+ begin
384
+ schedule.send(:enumerate_occurrences, schedule.start_time).each_with_index do |occ, idx|
385
+ next if occ.start_time < dont_project_before
386
+ # Given the original hash, we will modify some fields.
387
+ e = h.dup
388
+ # Keep track of how many events we're managing.
389
+ e["recurring_event_sequence"] = idx
390
+ # The new UID has the sequence number.
391
+ e["UID"] = {"v" => "#{uid}-#{idx}"}
392
+ e["DTSTART"] = self._ical_entry_from_ruby(occ.start_time, start_entry, is_date)
393
+ e["DTEND"] = self._ical_entry_from_ruby(occ.end_time, end_entry, is_date) if has_end_time
394
+ yield e
395
+ final_sequence = idx
396
+ break if occ.start_time > dont_project_after
397
+ end
398
+ rescue Date::Error
399
+ # It's possible we yielded some recurring events too, in that case, treat them as normal,
400
+ # in addition to yielding the event as non-recurring.
401
+ yield h
402
+ end
403
+ @max_sequence_num_by_uid[uid] = final_sequence
404
+ return
405
+ end
406
+
407
+ # We need is_date because the recurrence/IceCube schedule may be using times, not date.
408
+ def _ical_entry_from_ruby(r, entry, is_date)
409
+ return {"v" => r.strftime("%Y%m%d")} if is_date
410
+ return {"v" => r.strftime("%Y%m%dT%H%M%SZ")} if r.zone == "UTC"
411
+ return {"v" => r.strftime("%Y%m%dT%H%M%S"), "TZID" => entry.fetch("TZID")}
412
+ end
413
+
414
+ def _icecube_rule_from_ical(ical)
415
+ # We have seen certain ambiguous rules, like FREQ=WEEKLY with BYMONTHDAY=4.
416
+ # Apple interprets this as every 2 weeks; rrule.js interprets it as on the 4th of the month.
417
+ # IceCube errors, because `day_of_month` isn't valid on a WeeklyRule.
418
+ # In this case, we need to sanitize the string to remove the offending rule piece.
419
+ # There are probably many other offending formats, but we'll add them here as needed.
420
+ if ical.include?("FREQ=WEEKLY") && ical.include?("BYMONTHDAY=")
421
+ ical = ical.gsub(/BYMONTHDAY=[\d,]+/, "")
422
+ ical.delete_prefix! ";"
423
+ ical.delete_suffix! ";"
424
+ end
425
+ return IceCube::IcalParser.rule_from_ical(ical)
426
+ end
427
+
428
+ def _time_array(h)
429
+ expanded_entries = h["v"].split(",").map { |v| h.merge("v" => v) }
430
+ return expanded_entries.map do |e|
431
+ parsed_val, _got_tz = Webhookdb::Replicator::IcalendarEventV1.entry_to_date_or_datetime(e)
432
+ next parsed_val if parsed_val.is_a?(Date)
433
+ # Convert to UTC. We don't work with ActiveSupport timezones in the icalendar code for the most part.
434
+ parsed_val.utc
435
+ end
436
+ end
437
+
438
+ def each_feed_event
439
+ bad_event_uids = Set.new
440
+ vevent_lines = []
441
+ in_vevent = false
442
+ while (line = @io.gets)
443
+ line.rstrip!
444
+ if line == "BEGIN:VEVENT"
445
+ in_vevent = true
446
+ vevent_lines << line
447
+ elsif line == "END:VEVENT"
448
+ in_vevent = false
449
+ vevent_lines << line
450
+ h = Webhookdb::Replicator::IcalendarEventV1.vevent_to_hash(vevent_lines)
451
+ vevent_lines.clear
452
+ if h.key?("DTSTART") && h.key?("UID")
453
+ yield h
454
+ else
455
+ bad_event_uids << h.fetch("UID", {}).fetch("v", "[missing]")
456
+ end
457
+ elsif in_vevent
458
+ vevent_lines << line
459
+ end
460
+ end
461
+ return if bad_event_uids.empty?
462
+ @upserter.upserting_replicator.logger.warn("invalid_vevent_hash", vevent_uids: bad_event_uids.sort)
463
+ end
464
+ end
465
+ end