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,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/api"
4
+ require "webhookdb/oauth"
5
+ require "webhookdb/service/view_api"
6
+
7
+ class Webhookdb::API::Install < Webhookdb::API::V1
8
+ include Webhookdb::Service::ViewApi
9
+
10
+ namespace :install do
11
+ helpers do
12
+ def lookup_session!
13
+ session = Webhookdb::Oauth::Session.usable.where(oauth_state: params[:state]).first
14
+ forbidden! unless session
15
+ return session
16
+ end
17
+
18
+ def handle_login(email:, session:, action_url:)
19
+ new_customer, me = Webhookdb::Customer.find_or_create_for_email(email)
20
+ me.reset_codes_dataset.usable.each(&:expire!)
21
+ me.add_reset_code(transport: "email")
22
+ session.update(customer: me)
23
+ rendered = render_liquid(
24
+ "messages/web/install-customer-login.liquid",
25
+ serialize_view_params: true,
26
+ vars: {
27
+ view: "otp",
28
+ action_url:,
29
+ oauth_state: session.oauth_state,
30
+ new_customer:,
31
+ email:,
32
+ },
33
+ )
34
+ status 200
35
+ body rendered
36
+ end
37
+
38
+ def find_and_verify_user(email:, otp_token:)
39
+ (me = Webhookdb::Customer.with_email(email)) or forbidden!
40
+ begin
41
+ Webhookdb::Customer::ResetCode.use_code_with_token(otp_token) do |code|
42
+ raise Webhookdb::Customer::ResetCode::Unusable unless code.customer === me
43
+ end
44
+ rescue Webhookdb::Customer::ResetCode::Unusable
45
+ raise FormError.new("Sorry, that token is invalid. Please try again.", 403)
46
+ end
47
+ return me
48
+ end
49
+ end
50
+
51
+ route_param :oauth_provider, type: String, values: Webhookdb::Oauth.registry.keys do
52
+ helpers do
53
+ def oauth_provider
54
+ return @oauth_provider ||= Webhookdb::Oauth.provider(params[:oauth_provider])
55
+ end
56
+
57
+ def finish_org_setup(organization:, tokens:, scope:)
58
+ organization.prepare_database_connections?
59
+ oauth_provider.build_marketplace_integrations(organization:, tokens:, scope:)
60
+ rendered = render_liquid(
61
+ "messages/web/install-success.liquid",
62
+ serialize_view_params: true,
63
+ vars: {
64
+ app_name: oauth_provider.app_name,
65
+ database_url: organization.readonly_connection_url,
66
+ supports_webhooks: oauth_provider.supports_webhooks?,
67
+ },
68
+ )
69
+ status 200
70
+ body rendered
71
+ end
72
+
73
+ def exchange_authorization_code(code)
74
+ return oauth_provider.exchange_authorization_code(code:)
75
+ rescue Webhookdb::Http::Error => e
76
+ logger.warn "oauth_exchange_error", exception: e
77
+ raise FormError.new(
78
+ "Something went wrong getting your access token from #{oauth_provider.app_name}. Please start over",
79
+ 400,
80
+ )
81
+ end
82
+
83
+ def find_admin_membership(customer)
84
+ _created, membership = Webhookdb::Customer.find_or_create_default_organization(customer)
85
+ membership = customer.verified_memberships.find(&:admin?) unless membership.admin?
86
+ return membership if membership
87
+ raise FormError.new(
88
+ "You must be an administrator of your WebhookDB organization to set up this app.",
89
+ 403,
90
+ )
91
+ end
92
+ end
93
+
94
+ get do
95
+ rendered = render_liquid(
96
+ "messages/web/install.liquid",
97
+ serialize_view_params: true,
98
+ vars: {app_name: oauth_provider.app_name, action_url: "/v1/install/#{oauth_provider.key}"},
99
+ )
100
+ status 200
101
+ body rendered
102
+ end
103
+
104
+ post do
105
+ oauth_state = SecureRandom.hex(16)
106
+ Webhookdb::Oauth::Session.create(
107
+ oauth_state:,
108
+ **Webhookdb::Oauth::Session.params_for_request(request),
109
+ )
110
+ auth_url = oauth_provider.authorization_url(state: oauth_state)
111
+ redirect auth_url
112
+ end
113
+
114
+ params do
115
+ requires :code, type: String, desc: "Authorization code that we exchange for tokens."
116
+ requires :state, type: String, desc: "The user session info string that we provided to the oauth flow."
117
+ end
118
+ get :callback do
119
+ session = lookup_session!
120
+ code = params[:code]
121
+ if oauth_provider.requires_webhookdb_auth?
122
+ session.update(authorization_code: code)
123
+ redirect "/v1/install/#{oauth_provider.key}/login?state=#{session.oauth_state}"
124
+ else
125
+ scope = {}
126
+ # Order of operations here is:
127
+ # - Exchange token. We need the token to create the customer so it comes first.
128
+ # - Create the customer, find the membership, and create replicators.
129
+ # - If this last step fails, the code becomes invalid, which is annoying,
130
+ # but should be rare and not a big deal to start over.
131
+ tokens = exchange_authorization_code(code)
132
+ session.db.transaction do
133
+ _created, customer = oauth_provider.find_or_create_customer(tokens:, scope:)
134
+ membership = find_admin_membership(customer)
135
+ finish_org_setup(organization: membership.organization, tokens:, scope:)
136
+ session.update(customer:, used_at: Time.now, authorization_code: code)
137
+ end
138
+ end
139
+ end
140
+
141
+ params do
142
+ requires :state, type: String
143
+ end
144
+ get :login do
145
+ session = lookup_session!
146
+ rendered = render_liquid(
147
+ "messages/web/install-customer-login.liquid",
148
+ serialize_view_params: true,
149
+ vars: {
150
+ view: "email",
151
+ action_url: "/v1/install/#{oauth_provider.key}/login",
152
+ oauth_state: session.oauth_state,
153
+ },
154
+ )
155
+ status 200
156
+ body rendered
157
+ end
158
+
159
+ params do
160
+ requires :state, type: String, desc: "the user session info string that we provided to Front"
161
+ optional :email, type: String
162
+ optional :otp_token, type: String
163
+ end
164
+ post :login do
165
+ session = lookup_session!
166
+ email = params[:email]
167
+ raise FormError.new("Email is required", 400) unless email.present?
168
+ otp_token = params[:otp_token]
169
+ if otp_token
170
+ # Order of operations here is:
171
+ # - Verify the OTP
172
+ # - Make sure we can find a valid admin membership
173
+ # - Only then do we exchange the token.
174
+ # - Setup replicators.
175
+ customer = find_and_verify_user(email:, otp_token:)
176
+ membership = find_admin_membership(customer)
177
+ tokens = exchange_authorization_code(session.authorization_code)
178
+ session.db.transaction do
179
+ finish_org_setup(organization: membership.organization, tokens:, scope: {})
180
+ session.update(used_at: Time.now)
181
+ end
182
+ else
183
+ handle_login(email:, session:, action_url: "/v1/install/#{oauth_provider.key}/login")
184
+ end
185
+ end
186
+ end
187
+
188
+ resource :front do
189
+ post :webhook do
190
+ is_initial_request = request.headers["X-Front-Challenge"].present?
191
+ if is_initial_request
192
+ whresp = Webhookdb::Front.initial_verification_request_response(request)
193
+ s_status, s_headers, s_body = whresp.to_rack
194
+ s_headers.each { |k, v| header k, v }
195
+ if s_headers["Content-Type"] == "application/json"
196
+ body Oj.load(s_body)
197
+ else
198
+ env["api.format"] = :binary
199
+ body s_body
200
+ end
201
+ status s_status
202
+ break
203
+ end
204
+
205
+ resource_url = params.dig(:payload, :_links, :self)
206
+ handle_webhook_request("front_marketplace_host-#{resource_url || '?'}") do
207
+ if resource_url.nil?
208
+ logger.warn "front_webhook_empty_resource_url"
209
+ status 200
210
+ present({message: "unregistered/empty app"})
211
+ next :pass
212
+ end
213
+
214
+ # In cases where there is a change to a message, the event payload will have a "target" object and that object
215
+ # will have a "type" of "message". In cases where there is a change to a conversation, there will be no
216
+ # "target" object. In these cases the conversation resource is in the event as the "conversation" object.
217
+ target_type = params.dig(:payload, :target, :_meta, :type) || "conversation"
218
+ service_name = "front_#{target_type}_v1"
219
+ api_url = URI.parse(resource_url).host
220
+ unless (root_sint = Webhookdb::ServiceIntegration[service_name: "front_marketplace_root_v1", api_url:])
221
+ logger.warn "front_webhook_unregistered_app", front_api_url: api_url
222
+ status 200
223
+ present({message: "unregistered app"})
224
+ next :pass
225
+ end
226
+
227
+ handling_sint = root_sint.recursive_dependents.find { |d| d.service_name == service_name }
228
+ if handling_sint.nil?
229
+ logger.warn "front_webhook_invalid_topic", front_api_url: api_url, front_topic: target_type
230
+ status 200
231
+ present({message: "invalid topic"})
232
+ next :pass
233
+ end
234
+ next handling_sint
235
+ end
236
+ end
237
+ end
238
+
239
+ resource :intercom do
240
+ post :webhook do
241
+ # Because the `_webhook_response` function is always the same here, I'm wondering if it's even
242
+ # advisable to do the integration lookup before performing a webhook verification when we don't
243
+ # need that info. Something to consider upon refactor
244
+
245
+ handle_webhook_request("intercom_marketplace_appid-#{params[:app_id] || '?'}") do
246
+ app_id = params[:app_id]
247
+ root_sint = Webhookdb::ServiceIntegration[service_name: "intercom_marketplace_root_v1", api_url: app_id]
248
+ if root_sint.nil?
249
+ logger.warn "intercom_webhook_unregistered_app", intercom_app_id: app_id
250
+ status 200
251
+ present({message: "unregistered app"})
252
+ next :pass
253
+ end
254
+ # Notification topics are formatted like "{model}.{thing that happened}" (e.g. "contact.created")
255
+ # to get the model type of the notification, for our purposes we can just grab that first chunk
256
+ type = params[:topic].split(".")[0]
257
+ handling_type = "intercom_#{type}_v1"
258
+ unless (handling_sint = root_sint.recursive_dependents.find { |d| d.service_name == handling_type })
259
+ logger.warn "intercom_webhook_invalid_topic", intercom_app_id: app_id, intercom_topic: params[:topic]
260
+ status 200
261
+ present({message: "invalid topic"})
262
+ next :pass
263
+ end
264
+ next handling_sint
265
+ end
266
+ end
267
+
268
+ params do
269
+ requires :app_id
270
+ end
271
+ post :uninstall do
272
+ # TODO: Verify the headers are valid
273
+ # We want to delete all the integrations associated with the app_id.
274
+ root_sint = Webhookdb::ServiceIntegration[service_name: "intercom_marketplace_root_v1",
275
+ api_url: params["app_id"]]
276
+ root_sint.destroy_self_and_all_dependents
277
+ status 200
278
+ present({o: "k"})
279
+ end
280
+
281
+ params do
282
+ requires :workspace_id
283
+ end
284
+ post :health do
285
+ # TODO: Verify the headers are valid
286
+ # For now we are just returning "OK" per the specification:
287
+ # https://developers.intercom.com/docs/build-an-integration/learn-more/installation-health-check
288
+ # An interesting point is that this endpoint recieves a value called "workspace_id" but it is
289
+ # identical to the "app_id" value we get from the `/me` endpoint. It just has a different name here, for
290
+ # some reason.
291
+ status 200
292
+ present({state: "OK"})
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grape"
4
+
5
+ require "webhookdb/api"
6
+
7
+ class Webhookdb::API::Me < Webhookdb::API::V1
8
+ resource :me do
9
+ desc "Return the current customer"
10
+ get do
11
+ customer = current_customer
12
+ present customer, with: Webhookdb::API::CurrentCustomerEntity, env:
13
+ end
14
+
15
+ resource :organization_memberships do
16
+ desc "Return all organizations the customer is part of."
17
+ params do
18
+ optional :active_org_identifier
19
+ end
20
+ get do
21
+ customer = current_customer
22
+ active_org_ids = Webhookdb::Organization.with_identifier(params[:active_org_identifier]).select_map(:id)
23
+ memberships, invited = Webhookdb::OrganizationMembership.where(customer:).all.partition(&:verified)
24
+ blocks = Webhookdb::Formatting.blocks
25
+ unless memberships.empty?
26
+ blocks.line("You are a member of the following organizations:")
27
+ blocks.blank
28
+ # the word "Status" here is referring to whether the org is "active" in the CLI. This designation
29
+ # is added by the client
30
+ rows = memberships.map do |m|
31
+ cli_status = active_org_ids.include?(m.organization_id) ? "active" : ""
32
+ [m.organization.name, m.organization.key, m.status, cli_status]
33
+ end
34
+ blocks.table(["Name", "Key", "Role", "Status"], rows)
35
+ end
36
+ blocks.blank if memberships.present? && invited.present?
37
+ unless invited.empty?
38
+ blocks.line("You have been invited to the following organizations:")
39
+ blocks.blank
40
+ rows = invited.map do |m|
41
+ [m.organization.name, m.organization.key, m.invitation_code]
42
+ end
43
+ blocks.table(["Name", "Key", "Join Code"], rows)
44
+ blocks.blank
45
+ blocks.line("To join an invited org, use: webhookdb org join [join code]")
46
+ end
47
+ blocks.line("You aren't affiliated with any organizations yet.") if memberships.empty? && invited.empty?
48
+ r = {blocks: blocks.as_json}
49
+ present r
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "grape"
4
+
5
+ require "webhookdb/api"
6
+ require "webhookdb/admin_api"
7
+
8
+ class Webhookdb::API::Organizations < Webhookdb::API::V1
9
+ include Webhookdb::Service::Types
10
+
11
+ resource :organizations do
12
+ route_param :org_identifier, type: String do
13
+ helpers do
14
+ def check_self_role_modification!(email)
15
+ return unless email == current_customer.email
16
+ return if params.key?(:guard_confirm)
17
+ Webhookdb::API::Helpers.prompt_for_required_param!(
18
+ request,
19
+ :guard_confirm,
20
+ "WARNING: You are modifying your own permissions. Enter to proceed, or Ctrl+C to quit:",
21
+ )
22
+ end
23
+
24
+ # If the current customer is the last admin of the org, we cannot allow them to remove themselves,
25
+ # since the org would have no admins.
26
+ def roll_back_if_no_admins!(org)
27
+ has_admins = !org.verified_memberships_dataset.
28
+ where(membership_role: Webhookdb::Role.admin_role).
29
+ empty?
30
+ return if has_admins
31
+ msg = "Sorry, you are the last admin in #{org.name} and cannot remove yourself. " \
32
+ "If you want to close down this org, Use 'webhookdb org close'"
33
+ merror!(409, msg, code: "remove_last_admin", rollback_db: org.db)
34
+ end
35
+ end
36
+
37
+ desc "Return organization with the given identifier."
38
+ get do
39
+ _customer = current_customer
40
+ org = lookup_org!
41
+ present org, with: Webhookdb::API::OrganizationEntity
42
+ end
43
+
44
+ resource :members do
45
+ desc "Return all customers associated with the organization"
46
+ get do
47
+ org_memberships = lookup_org!.all_memberships
48
+ present_collection org_memberships, with: Webhookdb::API::OrganizationMembershipEntity
49
+ end
50
+ end
51
+
52
+ resource :services do
53
+ desc "Returns a list of all available services."
54
+ get do
55
+ _customer = current_customer
56
+ org = lookup_org!
57
+ fake_entities = org.available_replicator_names.sort.map { |name| {name:} }
58
+ message = "Run `webhookdb integrations create [service name]` to start replicating data to your database."
59
+ present_collection fake_entities, with: Webhookdb::API::ServiceEntity, message:
60
+ end
61
+ end
62
+
63
+ desc "Generates an invitation code for a user, adds pending membership in the organization."
64
+ params do
65
+ optional :email, type: String, coerce_with: NormalizedEmail,
66
+ prompt: "Enter the email to send the invitation to:"
67
+ optional :role_name,
68
+ type: String,
69
+ values: Webhookdb::OrganizationMembership::VALID_ROLE_NAMES,
70
+ default: "member"
71
+ end
72
+ post :invite do
73
+ customer = current_customer
74
+ org = lookup_org!
75
+ ensure_admin!
76
+ customer.db.transaction do
77
+ email = params[:email]
78
+ # cannot use find_or_create_or_find here because all customers must be created with a random password,
79
+ # which can't be included in find parameters
80
+ invitee = Webhookdb::Customer[email:] ||
81
+ Webhookdb::Customer.create(email:, password: SecureRandom.hex(8))
82
+
83
+ membership = org.all_memberships_dataset[customer: invitee]
84
+ merror!(400, "That person is already a member of the organization.") if membership&.verified?
85
+
86
+ membership ||= Webhookdb::OrganizationMembership.new(
87
+ verified: false,
88
+ organization: org,
89
+ customer: invitee,
90
+ )
91
+ membership.membership_role = Webhookdb::Role.find_or_create_or_find(name: params[:role_name])
92
+ membership.invitation_code = "join-" + SecureRandom.hex(4)
93
+ membership.save_changes
94
+
95
+ membership.publish_deferred("invite", membership.id)
96
+ message = "An invitation to organization #{org.name} has been sent to #{email}.\n" \
97
+ "Their invite code is:\n #{membership.invitation_code}"
98
+ status 200
99
+ present membership, with: Webhookdb::API::OrganizationMembershipEntity, message:
100
+ end
101
+ end
102
+
103
+ desc "Allows organization admin to remove customer from an organization"
104
+ params do
105
+ optional :email, type: String, coerce_with: NormalizedEmail,
106
+ prompt: "Enter the email of the member you are removing permissions from:"
107
+ optional :guard_confirm
108
+ end
109
+ post :remove_member do
110
+ customer = current_customer
111
+ org = lookup_org!
112
+ ensure_admin!
113
+ email = params[:email]
114
+ check_self_role_modification!(params[:email])
115
+ customer.db.transaction do
116
+ to_delete = org.all_memberships_dataset.where(customer: Webhookdb::Customer[email:])
117
+ merror!(400, "That user is not a member of #{org.name}.") if to_delete.empty?
118
+ to_delete.delete
119
+ roll_back_if_no_admins!(org)
120
+ status 200
121
+ present({}, with: Webhookdb::AdminAPI::BaseEntity,
122
+ message: "#{email} is no longer a part of #{org.name}.",)
123
+ end
124
+ end
125
+
126
+ desc "Updates the field on an org."
127
+ params do
128
+ requires :field, type: String
129
+ requires :value, type: String
130
+ end
131
+ post :update do
132
+ customer = current_customer
133
+ org = lookup_org!
134
+ ensure_admin!
135
+ field_name = params[:field].downcase
136
+ unless org.cli_editable_fields.include?(field_name)
137
+ merror!(403, "That field is not editable from the command line")
138
+ end
139
+ customer.db.transaction do
140
+ org.send(:"#{field_name}=", params[:value])
141
+ org.save_changes
142
+ status 200
143
+ present org, with: Webhookdb::API::OrganizationEntity,
144
+ message: "You have successfully updated the organization #{org.name}."
145
+ end
146
+ end
147
+
148
+ desc "Allows organization admin to change customer's role in an organization"
149
+ params do
150
+ optional :emails, type: [String], coerce_with: CommaSepArray,
151
+ prompt: "Enter the emails to modify the roles of as a comma-separated list:"
152
+ optional :role_name, type: String, values: Webhookdb::OrganizationMembership::VALID_ROLE_NAMES,
153
+ prompt: "Enter the name of the role to assign " \
154
+ "(#{Webhookdb::OrganizationMembership::VALID_ROLE_NAMES.join(', ')}): "
155
+ optional :guard_confirm
156
+ end
157
+ post :change_roles do
158
+ customer = current_customer
159
+ org = lookup_org!
160
+ ensure_admin!
161
+ params[:emails].each { |e| check_self_role_modification!(e) }
162
+ customer.db.transaction do
163
+ new_role = Webhookdb::Role.find_or_create_or_find(name: params[:role_name])
164
+ memberships = org.all_memberships_dataset.where(customer: Webhookdb::Customer.where(email: params[:emails]))
165
+ merror!(400, "Those emails do not belong to members of #{org.name}.") if memberships.empty?
166
+ memberships.update(membership_role_id: new_role.id)
167
+ roll_back_if_no_admins!(org)
168
+ message = "Success! These users have now been assigned the role of #{new_role.name} in #{org.name}."
169
+ status 200
170
+ present_collection memberships, with: Webhookdb::API::OrganizationMembershipEntity, message:
171
+ end
172
+ end
173
+
174
+ desc "Allow organization admin to change the name of the organization"
175
+ params do
176
+ optional :name, type: String, prompt: "Enter the new organization name:"
177
+ end
178
+ post :rename do
179
+ customer = current_customer
180
+ org = lookup_org!
181
+ ensure_admin!
182
+ customer.db.transaction do
183
+ prev_name = org.name
184
+ org.name = params[:name]
185
+ org.save_changes
186
+ status 200
187
+ present org, with: Webhookdb::API::OrganizationEntity,
188
+ message: "The organization '#{org.key}' has been renamed from '#{prev_name}' to '#{org.name}'."
189
+ end
190
+ end
191
+
192
+ desc "Request closure of an organization"
193
+ post :close do
194
+ org = lookup_org!
195
+ c = current_customer
196
+ ensure_admin!(org, customer: c)
197
+ Webhookdb::DeveloperAlert.new(
198
+ subsystem: "Close Account",
199
+ emoji: ":no_pedestrians:",
200
+ fallback: "Org #{org.key} requested removal",
201
+ fields: [
202
+ {title: "Org Key", value: org.key, short: true},
203
+ {title: "Org Name", value: org.name, short: true},
204
+ {title: "Customer", value: "(#{c.id}) #{c.email}", short: false},
205
+ ],
206
+ ).emit
207
+ step = Webhookdb::Replicator::StateMachineStep.new.completed
208
+ step.output = "Thanks! We've received the request to close your #{org.name} organization. " \
209
+ "We'll be in touch within 2 business days confirming removal."
210
+ status 200
211
+ present step, with: Webhookdb::API::StateMachineEntity
212
+ end
213
+ end
214
+
215
+ desc "Creates a new organization and adds current customer as a member."
216
+ params do
217
+ optional :name, type: String, prompt: "Enter the name of the organization:"
218
+ end
219
+ post :create do
220
+ customer = current_customer
221
+ customer.db.transaction do
222
+ new_org = Webhookdb::Organization.create_if_unique(name: params[:name])
223
+ merror!(400, "An organization with that name already exists.") if new_org.nil?
224
+ new_org.billing_email = customer.email
225
+ new_org.save_changes
226
+ mem = new_org.add_membership(customer:, membership_role: Webhookdb::Role.admin_role, verified: true)
227
+ customer.replace_default_membership(mem)
228
+ message = "Organization created with identifier '#{new_org.key}'.\n" \
229
+ "Use `webhookdb org invite` to invite members to #{new_org.name}."
230
+ status 200
231
+ present new_org, with: Webhookdb::API::OrganizationEntity, message:
232
+ end
233
+ end
234
+
235
+ desc "Allows user to verify membership in an organization with an invitation code."
236
+ params do
237
+ optional :invitation_code, type: String, prompt: "Enter the invitation code:"
238
+ end
239
+ post :join do
240
+ customer = current_customer
241
+ customer.db.transaction do
242
+ membership = customer.invited_memberships_dataset[invitation_code: params[:invitation_code]]
243
+ merror!(400, "Looks like that invite code is invalid. Please try again.", alert: true) if membership.nil?
244
+ membership.verified = true
245
+ membership.invitation_code = ""
246
+ membership.save_changes
247
+ customer.replace_default_membership(membership)
248
+ message = "Congratulations! You are now a member of #{membership.organization_name}."
249
+ status 200
250
+ present membership, with: Webhookdb::API::OrganizationMembershipEntity, message:
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webhookdb/api"
4
+
5
+ class Webhookdb::API::Replay < Webhookdb::API::V1
6
+ DEFAULT_INTERVAL = 1.hour
7
+
8
+ resource :organizations do
9
+ route_param :org_identifier, type: String do
10
+ resource :replay do
11
+ params do
12
+ optional :before, type: Time
13
+ optional :after, type: Time
14
+ optional :hours, type: Integer
15
+ optional :service_integration_identifier
16
+ end
17
+ post do
18
+ current_customer
19
+ org = lookup_org!
20
+ cutoff_lower = params[:after]
21
+ cutoff_upper = params[:before]
22
+ interval = params.fetch(:hours, 0).positive? ? params[:hours].hours : DEFAULT_INTERVAL
23
+ if cutoff_lower.nil? && cutoff_upper.nil?
24
+ cutoff_upper = Time.now.utc
25
+ cutoff_lower = cutoff_upper - interval
26
+ elsif cutoff_lower && cutoff_upper
27
+ nil
28
+ elsif cutoff_upper
29
+ cutoff_lower = cutoff_upper - interval
30
+ elsif cutoff_lower
31
+ # Add an offset so that cutoff_upper is in the same timezone as cutoff_lower
32
+ cutoff_upper = cutoff_lower + (Time.now - cutoff_lower)
33
+ end
34
+
35
+ old_age_cutoff = Time.now - Webhookdb::LoggedWebhook.maximum_replay_history_hours.hours
36
+ merror!(400, "Webhooks older than #{old_age_cutoff.utc} cannot be replayed.") if cutoff_upper < old_age_cutoff
37
+
38
+ max_replay_hours = Webhookdb::LoggedWebhook.maximum_replay_interval_hours
39
+ if (cutoff_upper - cutoff_lower) > max_replay_hours.hours
40
+ merror!(400, "The maximum webhook replay interval is #{max_replay_hours} hours.")
41
+ end
42
+
43
+ ds = Webhookdb::LoggedWebhook.where(organization_id: org.id).
44
+ where { inserted_at >= cutoff_lower }.
45
+ where { inserted_at <= cutoff_upper }.
46
+ where(truncated_at: nil).select(:id)
47
+ if params[:service_integration_identifier].present?
48
+ sint = lookup_service_integration!(org, params[:service_integration_identifier])
49
+ ds = ds.where(service_integration_opaque_id: sint.opaque_id)
50
+ end
51
+ replayed = 0
52
+ ds.paged_each do |lw|
53
+ lw.replay_async
54
+ replayed += 1
55
+ end
56
+ s = replayed == 1 ? "" : "s"
57
+ message = "Replaying #{replayed} webhook#{s} between #{cutoff_lower.iso8601} and #{cutoff_upper.iso8601}."
58
+ status 200
59
+ present({}, with: Webhookdb::API::BaseEntity, message:)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end