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.
- checksums.yaml +7 -0
- data/data/messages/layouts/blank.email.liquid +10 -0
- data/data/messages/layouts/minimal.email.liquid +28 -0
- data/data/messages/layouts/standard.email.liquid +28 -0
- data/data/messages/partials/button.liquid +15 -0
- data/data/messages/partials/environment_banner.liquid +9 -0
- data/data/messages/partials/footer.liquid +22 -0
- data/data/messages/partials/greeting.liquid +3 -0
- data/data/messages/partials/logo_header.liquid +18 -0
- data/data/messages/partials/signoff.liquid +1 -0
- data/data/messages/styles/v1.liquid +346 -0
- data/data/messages/templates/errors/icalendar_fetch.email.liquid +29 -0
- data/data/messages/templates/invite.email.liquid +15 -0
- data/data/messages/templates/new_customer.email.liquid +24 -0
- data/data/messages/templates/org_database_migration_finished.email.liquid +7 -0
- data/data/messages/templates/org_database_migration_started.email.liquid +9 -0
- data/data/messages/templates/specs/_field_partial.liquid +1 -0
- data/data/messages/templates/specs/basic.email.liquid +2 -0
- data/data/messages/templates/specs/basic.fake.liquid +1 -0
- data/data/messages/templates/specs/with_field.email.liquid +2 -0
- data/data/messages/templates/specs/with_field.fake.liquid +1 -0
- data/data/messages/templates/specs/with_include.email.liquid +2 -0
- data/data/messages/templates/specs/with_partial.email.liquid +1 -0
- data/data/messages/templates/verification.email.liquid +14 -0
- data/data/messages/templates/verification.sms.liquid +1 -0
- data/data/messages/web/install-customer-login.liquid +48 -0
- data/data/messages/web/install-error.liquid +17 -0
- data/data/messages/web/install-success.liquid +35 -0
- data/data/messages/web/install.liquid +20 -0
- data/data/messages/web/partials/footer.liquid +4 -0
- data/data/messages/web/partials/form_error.liquid +1 -0
- data/data/messages/web/partials/header.liquid +3 -0
- data/data/messages/web/styles.liquid +134 -0
- data/data/windows_tz.txt +461 -0
- data/db/migrations/001_testing_pixies.rb +13 -0
- data/db/migrations/002_initial.rb +132 -0
- data/db/migrations/003_ux_overhaul.rb +20 -0
- data/db/migrations/004_incremental_backfill.rb +9 -0
- data/db/migrations/005_log_webhooks.rb +24 -0
- data/db/migrations/006_generalize_roles.rb +29 -0
- data/db/migrations/007_org_dns.rb +12 -0
- data/db/migrations/008_webhook_subscriptions.rb +19 -0
- data/db/migrations/009_nonunique_stripe_subscription_customer.rb +16 -0
- data/db/migrations/010_drop_integration_soft_delete.rb +14 -0
- data/db/migrations/011_webhook_subscriptions_created_at.rb +10 -0
- data/db/migrations/012_webhook_subscriptions_created_by.rb +9 -0
- data/db/migrations/013_default_org_membership.rb +30 -0
- data/db/migrations/014_webhook_subscription_deliveries.rb +26 -0
- data/db/migrations/015_dependent_integrations.rb +9 -0
- data/db/migrations/016_encrypted_columns.rb +9 -0
- data/db/migrations/017_skip_verification.rb +9 -0
- data/db/migrations/018_sync_targets.rb +25 -0
- data/db/migrations/019_org_schema.rb +9 -0
- data/db/migrations/020_org_database_migrations.rb +25 -0
- data/db/migrations/021_no_default_org_schema.rb +14 -0
- data/db/migrations/022_database_document.rb +15 -0
- data/db/migrations/023_sync_target_schema.rb +9 -0
- data/db/migrations/024_org_semaphore_jobs.rb +9 -0
- data/db/migrations/025_integration_backfill_cursor.rb +9 -0
- data/db/migrations/026_undo_integration_backfill_cursor.rb +9 -0
- data/db/migrations/027_sync_target_http_sync.rb +12 -0
- data/db/migrations/028_logged_webhook_path.rb +24 -0
- data/db/migrations/029_encrypt_columns.rb +97 -0
- data/db/migrations/030_org_sync_target_timeout.rb +9 -0
- data/db/migrations/031_org_max_query_rows.rb +9 -0
- data/db/migrations/032_remove_db_defaults.rb +12 -0
- data/db/migrations/033_backfill_jobs.rb +26 -0
- data/db/migrations/034_backfill_job_criteria.rb +9 -0
- data/db/migrations/035_synchronous_backfill.rb +9 -0
- data/db/migrations/036_oauth.rb +26 -0
- data/db/migrations/037_oauth_used.rb +9 -0
- data/lib/amigo/durable_job.rb +416 -0
- data/lib/pry/clipboard.rb +111 -0
- data/lib/sequel/advisory_lock.rb +65 -0
- data/lib/webhookdb/admin.rb +4 -0
- data/lib/webhookdb/admin_api/auth.rb +36 -0
- data/lib/webhookdb/admin_api/customers.rb +63 -0
- data/lib/webhookdb/admin_api/database_documents.rb +20 -0
- data/lib/webhookdb/admin_api/entities.rb +66 -0
- data/lib/webhookdb/admin_api/message_deliveries.rb +61 -0
- data/lib/webhookdb/admin_api/roles.rb +15 -0
- data/lib/webhookdb/admin_api.rb +34 -0
- data/lib/webhookdb/aggregate_result.rb +63 -0
- data/lib/webhookdb/api/auth.rb +122 -0
- data/lib/webhookdb/api/connstr_auth.rb +36 -0
- data/lib/webhookdb/api/db.rb +188 -0
- data/lib/webhookdb/api/demo.rb +14 -0
- data/lib/webhookdb/api/entities.rb +198 -0
- data/lib/webhookdb/api/helpers.rb +253 -0
- data/lib/webhookdb/api/install.rb +296 -0
- data/lib/webhookdb/api/me.rb +53 -0
- data/lib/webhookdb/api/organizations.rb +254 -0
- data/lib/webhookdb/api/replay.rb +64 -0
- data/lib/webhookdb/api/service_integrations.rb +402 -0
- data/lib/webhookdb/api/services.rb +27 -0
- data/lib/webhookdb/api/stripe.rb +22 -0
- data/lib/webhookdb/api/subscriptions.rb +67 -0
- data/lib/webhookdb/api/sync_targets.rb +232 -0
- data/lib/webhookdb/api/system.rb +37 -0
- data/lib/webhookdb/api/webhook_subscriptions.rb +96 -0
- data/lib/webhookdb/api.rb +92 -0
- data/lib/webhookdb/apps.rb +93 -0
- data/lib/webhookdb/async/audit_logger.rb +38 -0
- data/lib/webhookdb/async/autoscaler.rb +84 -0
- data/lib/webhookdb/async/job.rb +18 -0
- data/lib/webhookdb/async/job_logger.rb +45 -0
- data/lib/webhookdb/async/scheduled_job.rb +18 -0
- data/lib/webhookdb/async.rb +142 -0
- data/lib/webhookdb/aws.rb +98 -0
- data/lib/webhookdb/backfill_job.rb +107 -0
- data/lib/webhookdb/backfiller.rb +107 -0
- data/lib/webhookdb/cloudflare.rb +39 -0
- data/lib/webhookdb/connection_cache.rb +177 -0
- data/lib/webhookdb/console.rb +71 -0
- data/lib/webhookdb/convertkit.rb +14 -0
- data/lib/webhookdb/crypto.rb +66 -0
- data/lib/webhookdb/customer/reset_code.rb +94 -0
- data/lib/webhookdb/customer.rb +347 -0
- data/lib/webhookdb/database_document.rb +72 -0
- data/lib/webhookdb/db_adapter/column_types.rb +37 -0
- data/lib/webhookdb/db_adapter/default_sql.rb +187 -0
- data/lib/webhookdb/db_adapter/pg.rb +96 -0
- data/lib/webhookdb/db_adapter/snowflake.rb +137 -0
- data/lib/webhookdb/db_adapter.rb +208 -0
- data/lib/webhookdb/dbutil.rb +92 -0
- data/lib/webhookdb/demo_mode.rb +100 -0
- data/lib/webhookdb/developer_alert.rb +51 -0
- data/lib/webhookdb/email_octopus.rb +21 -0
- data/lib/webhookdb/enumerable.rb +18 -0
- data/lib/webhookdb/fixtures/backfill_jobs.rb +72 -0
- data/lib/webhookdb/fixtures/customers.rb +65 -0
- data/lib/webhookdb/fixtures/database_documents.rb +27 -0
- data/lib/webhookdb/fixtures/faker.rb +41 -0
- data/lib/webhookdb/fixtures/logged_webhooks.rb +56 -0
- data/lib/webhookdb/fixtures/message_deliveries.rb +59 -0
- data/lib/webhookdb/fixtures/oauth_sessions.rb +24 -0
- data/lib/webhookdb/fixtures/organization_database_migrations.rb +37 -0
- data/lib/webhookdb/fixtures/organization_memberships.rb +54 -0
- data/lib/webhookdb/fixtures/organizations.rb +32 -0
- data/lib/webhookdb/fixtures/reset_codes.rb +23 -0
- data/lib/webhookdb/fixtures/service_integrations.rb +42 -0
- data/lib/webhookdb/fixtures/subscriptions.rb +33 -0
- data/lib/webhookdb/fixtures/sync_targets.rb +32 -0
- data/lib/webhookdb/fixtures/webhook_subscriptions.rb +35 -0
- data/lib/webhookdb/fixtures.rb +15 -0
- data/lib/webhookdb/formatting.rb +56 -0
- data/lib/webhookdb/front.rb +49 -0
- data/lib/webhookdb/github.rb +22 -0
- data/lib/webhookdb/google_calendar.rb +29 -0
- data/lib/webhookdb/heroku.rb +21 -0
- data/lib/webhookdb/http.rb +114 -0
- data/lib/webhookdb/icalendar.rb +17 -0
- data/lib/webhookdb/id.rb +17 -0
- data/lib/webhookdb/idempotency.rb +90 -0
- data/lib/webhookdb/increase.rb +42 -0
- data/lib/webhookdb/intercom.rb +23 -0
- data/lib/webhookdb/jobs/amigo_test_jobs.rb +118 -0
- data/lib/webhookdb/jobs/backfill.rb +32 -0
- data/lib/webhookdb/jobs/create_mirror_table.rb +18 -0
- data/lib/webhookdb/jobs/create_stripe_customer.rb +17 -0
- data/lib/webhookdb/jobs/customer_created_notify_internal.rb +22 -0
- data/lib/webhookdb/jobs/demo_mode_sync_data.rb +19 -0
- data/lib/webhookdb/jobs/deprecated_jobs.rb +19 -0
- data/lib/webhookdb/jobs/developer_alert_handle.rb +14 -0
- data/lib/webhookdb/jobs/durable_job_recheck_poller.rb +17 -0
- data/lib/webhookdb/jobs/emailer.rb +15 -0
- data/lib/webhookdb/jobs/icalendar_enqueue_syncs.rb +25 -0
- data/lib/webhookdb/jobs/icalendar_sync.rb +23 -0
- data/lib/webhookdb/jobs/logged_webhook_replay.rb +17 -0
- data/lib/webhookdb/jobs/logged_webhook_resilient_replay.rb +15 -0
- data/lib/webhookdb/jobs/message_dispatched.rb +16 -0
- data/lib/webhookdb/jobs/organization_database_migration_notify_finished.rb +21 -0
- data/lib/webhookdb/jobs/organization_database_migration_notify_started.rb +21 -0
- data/lib/webhookdb/jobs/organization_database_migration_run.rb +24 -0
- data/lib/webhookdb/jobs/prepare_database_connections.rb +22 -0
- data/lib/webhookdb/jobs/process_webhook.rb +47 -0
- data/lib/webhookdb/jobs/renew_watch_channel.rb +24 -0
- data/lib/webhookdb/jobs/replication_migration.rb +24 -0
- data/lib/webhookdb/jobs/reset_code_create_dispatch.rb +23 -0
- data/lib/webhookdb/jobs/scheduled_backfills.rb +77 -0
- data/lib/webhookdb/jobs/send_invite.rb +15 -0
- data/lib/webhookdb/jobs/send_test_webhook.rb +25 -0
- data/lib/webhookdb/jobs/send_webhook.rb +20 -0
- data/lib/webhookdb/jobs/sync_target_enqueue_scheduled.rb +16 -0
- data/lib/webhookdb/jobs/sync_target_run_sync.rb +38 -0
- data/lib/webhookdb/jobs/trim_logged_webhooks.rb +15 -0
- data/lib/webhookdb/jobs/webhook_resource_notify_integrations.rb +30 -0
- data/lib/webhookdb/jobs/webhook_subscription_delivery_attempt.rb +29 -0
- data/lib/webhookdb/jobs.rb +4 -0
- data/lib/webhookdb/json.rb +113 -0
- data/lib/webhookdb/liquid/expose.rb +27 -0
- data/lib/webhookdb/liquid/filters.rb +16 -0
- data/lib/webhookdb/liquid/liquification.rb +26 -0
- data/lib/webhookdb/liquid/partial.rb +12 -0
- data/lib/webhookdb/logged_webhook/resilient.rb +95 -0
- data/lib/webhookdb/logged_webhook.rb +194 -0
- data/lib/webhookdb/message/body.rb +25 -0
- data/lib/webhookdb/message/delivery.rb +127 -0
- data/lib/webhookdb/message/email_transport.rb +133 -0
- data/lib/webhookdb/message/fake_transport.rb +54 -0
- data/lib/webhookdb/message/liquid_drops.rb +29 -0
- data/lib/webhookdb/message/template.rb +89 -0
- data/lib/webhookdb/message/transport.rb +43 -0
- data/lib/webhookdb/message.rb +150 -0
- data/lib/webhookdb/messages/error_icalendar_fetch.rb +42 -0
- data/lib/webhookdb/messages/invite.rb +23 -0
- data/lib/webhookdb/messages/new_customer.rb +14 -0
- data/lib/webhookdb/messages/org_database_migration_finished.rb +23 -0
- data/lib/webhookdb/messages/org_database_migration_started.rb +24 -0
- data/lib/webhookdb/messages/specs.rb +57 -0
- data/lib/webhookdb/messages/verification.rb +23 -0
- data/lib/webhookdb/method_utilities.rb +82 -0
- data/lib/webhookdb/microsoft_calendar.rb +36 -0
- data/lib/webhookdb/nextpax.rb +14 -0
- data/lib/webhookdb/oauth/front.rb +58 -0
- data/lib/webhookdb/oauth/intercom.rb +58 -0
- data/lib/webhookdb/oauth/session.rb +24 -0
- data/lib/webhookdb/oauth.rb +80 -0
- data/lib/webhookdb/organization/alerting.rb +35 -0
- data/lib/webhookdb/organization/database_migration.rb +151 -0
- data/lib/webhookdb/organization/db_builder.rb +429 -0
- data/lib/webhookdb/organization.rb +506 -0
- data/lib/webhookdb/organization_membership.rb +58 -0
- data/lib/webhookdb/phone_number.rb +38 -0
- data/lib/webhookdb/plaid.rb +23 -0
- data/lib/webhookdb/platform.rb +27 -0
- data/lib/webhookdb/plivo.rb +52 -0
- data/lib/webhookdb/postgres/maintenance.rb +166 -0
- data/lib/webhookdb/postgres/model.rb +82 -0
- data/lib/webhookdb/postgres/model_utilities.rb +382 -0
- data/lib/webhookdb/postgres/testing_pixie.rb +16 -0
- data/lib/webhookdb/postgres/validations.rb +46 -0
- data/lib/webhookdb/postgres.rb +176 -0
- data/lib/webhookdb/postmark.rb +20 -0
- data/lib/webhookdb/redis.rb +35 -0
- data/lib/webhookdb/replicator/atom_single_feed_v1.rb +116 -0
- data/lib/webhookdb/replicator/aws_pricing_v1.rb +488 -0
- data/lib/webhookdb/replicator/base.rb +1185 -0
- data/lib/webhookdb/replicator/column.rb +482 -0
- data/lib/webhookdb/replicator/convertkit_broadcast_v1.rb +69 -0
- data/lib/webhookdb/replicator/convertkit_subscriber_v1.rb +200 -0
- data/lib/webhookdb/replicator/convertkit_tag_v1.rb +66 -0
- data/lib/webhookdb/replicator/convertkit_v1_mixin.rb +65 -0
- data/lib/webhookdb/replicator/docgen.rb +167 -0
- data/lib/webhookdb/replicator/email_octopus_campaign_v1.rb +84 -0
- data/lib/webhookdb/replicator/email_octopus_contact_v1.rb +159 -0
- data/lib/webhookdb/replicator/email_octopus_event_v1.rb +244 -0
- data/lib/webhookdb/replicator/email_octopus_list_v1.rb +101 -0
- data/lib/webhookdb/replicator/fake.rb +453 -0
- data/lib/webhookdb/replicator/front_conversation_v1.rb +45 -0
- data/lib/webhookdb/replicator/front_marketplace_root_v1.rb +55 -0
- data/lib/webhookdb/replicator/front_message_v1.rb +45 -0
- data/lib/webhookdb/replicator/front_v1_mixin.rb +22 -0
- data/lib/webhookdb/replicator/github_issue_comment_v1.rb +58 -0
- data/lib/webhookdb/replicator/github_issue_v1.rb +83 -0
- data/lib/webhookdb/replicator/github_pull_v1.rb +84 -0
- data/lib/webhookdb/replicator/github_release_v1.rb +47 -0
- data/lib/webhookdb/replicator/github_repo_v1_mixin.rb +250 -0
- data/lib/webhookdb/replicator/github_repository_event_v1.rb +45 -0
- data/lib/webhookdb/replicator/icalendar_calendar_v1.rb +465 -0
- data/lib/webhookdb/replicator/icalendar_event_v1.rb +334 -0
- data/lib/webhookdb/replicator/increase_account_number_v1.rb +77 -0
- data/lib/webhookdb/replicator/increase_account_transfer_v1.rb +61 -0
- data/lib/webhookdb/replicator/increase_account_v1.rb +63 -0
- data/lib/webhookdb/replicator/increase_ach_transfer_v1.rb +78 -0
- data/lib/webhookdb/replicator/increase_check_transfer_v1.rb +64 -0
- data/lib/webhookdb/replicator/increase_limit_v1.rb +78 -0
- data/lib/webhookdb/replicator/increase_transaction_v1.rb +74 -0
- data/lib/webhookdb/replicator/increase_v1_mixin.rb +121 -0
- data/lib/webhookdb/replicator/increase_wire_transfer_v1.rb +61 -0
- data/lib/webhookdb/replicator/intercom_contact_v1.rb +36 -0
- data/lib/webhookdb/replicator/intercom_conversation_v1.rb +38 -0
- data/lib/webhookdb/replicator/intercom_marketplace_root_v1.rb +69 -0
- data/lib/webhookdb/replicator/intercom_v1_mixin.rb +105 -0
- data/lib/webhookdb/replicator/oauth_refresh_access_token_mixin.rb +65 -0
- data/lib/webhookdb/replicator/plivo_sms_inbound_v1.rb +102 -0
- data/lib/webhookdb/replicator/postmark_inbound_message_v1.rb +94 -0
- data/lib/webhookdb/replicator/postmark_outbound_message_event_v1.rb +107 -0
- data/lib/webhookdb/replicator/schema_modification.rb +42 -0
- data/lib/webhookdb/replicator/shopify_customer_v1.rb +58 -0
- data/lib/webhookdb/replicator/shopify_order_v1.rb +64 -0
- data/lib/webhookdb/replicator/shopify_v1_mixin.rb +161 -0
- data/lib/webhookdb/replicator/signalwire_message_v1.rb +169 -0
- data/lib/webhookdb/replicator/sponsy_customer_v1.rb +54 -0
- data/lib/webhookdb/replicator/sponsy_placement_v1.rb +34 -0
- data/lib/webhookdb/replicator/sponsy_publication_v1.rb +125 -0
- data/lib/webhookdb/replicator/sponsy_slot_v1.rb +41 -0
- data/lib/webhookdb/replicator/sponsy_status_v1.rb +35 -0
- data/lib/webhookdb/replicator/sponsy_v1_mixin.rb +165 -0
- data/lib/webhookdb/replicator/state_machine_step.rb +69 -0
- data/lib/webhookdb/replicator/stripe_charge_v1.rb +77 -0
- data/lib/webhookdb/replicator/stripe_coupon_v1.rb +62 -0
- data/lib/webhookdb/replicator/stripe_customer_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_dispute_v1.rb +77 -0
- data/lib/webhookdb/replicator/stripe_invoice_item_v1.rb +82 -0
- data/lib/webhookdb/replicator/stripe_invoice_v1.rb +116 -0
- data/lib/webhookdb/replicator/stripe_payout_v1.rb +67 -0
- data/lib/webhookdb/replicator/stripe_price_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_product_v1.rb +60 -0
- data/lib/webhookdb/replicator/stripe_refund_v1.rb +101 -0
- data/lib/webhookdb/replicator/stripe_subscription_item_v1.rb +56 -0
- data/lib/webhookdb/replicator/stripe_subscription_v1.rb +75 -0
- data/lib/webhookdb/replicator/stripe_v1_mixin.rb +116 -0
- data/lib/webhookdb/replicator/transistor_episode_stats_v1.rb +141 -0
- data/lib/webhookdb/replicator/transistor_episode_v1.rb +169 -0
- data/lib/webhookdb/replicator/transistor_show_v1.rb +68 -0
- data/lib/webhookdb/replicator/transistor_v1_mixin.rb +65 -0
- data/lib/webhookdb/replicator/twilio_sms_v1.rb +156 -0
- data/lib/webhookdb/replicator/webhook_request.rb +5 -0
- data/lib/webhookdb/replicator/webhookdb_customer_v1.rb +74 -0
- data/lib/webhookdb/replicator.rb +224 -0
- data/lib/webhookdb/role.rb +42 -0
- data/lib/webhookdb/sentry.rb +35 -0
- data/lib/webhookdb/service/auth.rb +138 -0
- data/lib/webhookdb/service/collection.rb +91 -0
- data/lib/webhookdb/service/entities.rb +97 -0
- data/lib/webhookdb/service/helpers.rb +270 -0
- data/lib/webhookdb/service/middleware.rb +124 -0
- data/lib/webhookdb/service/types.rb +30 -0
- data/lib/webhookdb/service/validators.rb +32 -0
- data/lib/webhookdb/service/view_api.rb +63 -0
- data/lib/webhookdb/service.rb +219 -0
- data/lib/webhookdb/service_integration.rb +332 -0
- data/lib/webhookdb/shopify.rb +35 -0
- data/lib/webhookdb/signalwire.rb +13 -0
- data/lib/webhookdb/slack.rb +68 -0
- data/lib/webhookdb/snowflake.rb +90 -0
- data/lib/webhookdb/spec_helpers/async.rb +122 -0
- data/lib/webhookdb/spec_helpers/citest.rb +88 -0
- data/lib/webhookdb/spec_helpers/integration.rb +121 -0
- data/lib/webhookdb/spec_helpers/message.rb +41 -0
- data/lib/webhookdb/spec_helpers/postgres.rb +220 -0
- data/lib/webhookdb/spec_helpers/service.rb +432 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_columns.rb +56 -0
- data/lib/webhookdb/spec_helpers/shared_examples_for_replicators.rb +915 -0
- data/lib/webhookdb/spec_helpers/whdb.rb +139 -0
- data/lib/webhookdb/spec_helpers.rb +63 -0
- data/lib/webhookdb/sponsy.rb +14 -0
- data/lib/webhookdb/stripe.rb +37 -0
- data/lib/webhookdb/subscription.rb +203 -0
- data/lib/webhookdb/sync_target.rb +491 -0
- data/lib/webhookdb/tasks/admin.rb +49 -0
- data/lib/webhookdb/tasks/annotate.rb +36 -0
- data/lib/webhookdb/tasks/db.rb +82 -0
- data/lib/webhookdb/tasks/docs.rb +42 -0
- data/lib/webhookdb/tasks/fixture.rb +35 -0
- data/lib/webhookdb/tasks/message.rb +50 -0
- data/lib/webhookdb/tasks/regress.rb +87 -0
- data/lib/webhookdb/tasks/release.rb +27 -0
- data/lib/webhookdb/tasks/sidekiq.rb +23 -0
- data/lib/webhookdb/tasks/specs.rb +64 -0
- data/lib/webhookdb/theranest.rb +15 -0
- data/lib/webhookdb/transistor.rb +13 -0
- data/lib/webhookdb/twilio.rb +13 -0
- data/lib/webhookdb/typed_struct.rb +44 -0
- data/lib/webhookdb/version.rb +5 -0
- data/lib/webhookdb/webhook_response.rb +50 -0
- data/lib/webhookdb/webhook_subscription/delivery.rb +82 -0
- data/lib/webhookdb/webhook_subscription.rb +226 -0
- data/lib/webhookdb/windows_tz.rb +32 -0
- data/lib/webhookdb/xml.rb +92 -0
- data/lib/webhookdb.rb +224 -0
- data/lib/webterm/apps.rb +45 -0
- metadata +1129 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stripe"
|
|
4
|
+
require "webhookdb/replicator/stripe_v1_mixin"
|
|
5
|
+
|
|
6
|
+
class Webhookdb::Replicator::StripeRefundV1 < Webhookdb::Replicator::Base
|
|
7
|
+
include Appydays::Loggable
|
|
8
|
+
include Webhookdb::Replicator::StripeV1Mixin
|
|
9
|
+
|
|
10
|
+
# @return [Webhookdb::Replicator::Descriptor]
|
|
11
|
+
def self.descriptor
|
|
12
|
+
return Webhookdb::Replicator::Descriptor.new(
|
|
13
|
+
name: "stripe_refund_v1",
|
|
14
|
+
ctor: ->(sint) { Webhookdb::Replicator::StripeRefundV1.new(sint) },
|
|
15
|
+
feature_roles: [],
|
|
16
|
+
resource_name_singular: "Stripe Refund",
|
|
17
|
+
supports_webhooks: true,
|
|
18
|
+
supports_backfill: true,
|
|
19
|
+
api_docs_url: "https://stripe.com/docs/api/refunds",
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def _remote_key_column
|
|
24
|
+
return Webhookdb::Replicator::Column.new(:stripe_id, TEXT, data_key: "id")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def _denormalized_columns
|
|
28
|
+
return [
|
|
29
|
+
Webhookdb::Replicator::Column.new(:amount, INTEGER, index: true),
|
|
30
|
+
Webhookdb::Replicator::Column.new(:balance_transaction, TEXT, index: true),
|
|
31
|
+
Webhookdb::Replicator::Column.new(:charge, TEXT, index: true),
|
|
32
|
+
Webhookdb::Replicator::Column.new(:created, TIMESTAMP, index: true, converter: :tsat),
|
|
33
|
+
Webhookdb::Replicator::Column.new(:payment_intent, TEXT, index: true),
|
|
34
|
+
Webhookdb::Replicator::Column.new(:receipt_number, TEXT, index: true),
|
|
35
|
+
Webhookdb::Replicator::Column.new(:source_transfer_reversal, TEXT, index: true),
|
|
36
|
+
Webhookdb::Replicator::Column.new(:status, TEXT),
|
|
37
|
+
Webhookdb::Replicator::Column.new(:transfer_reversal, TEXT, index: true),
|
|
38
|
+
Webhookdb::Replicator::Column.new(
|
|
39
|
+
:updated,
|
|
40
|
+
TIMESTAMP,
|
|
41
|
+
index: true,
|
|
42
|
+
data_key: "created",
|
|
43
|
+
event_key: "created",
|
|
44
|
+
converter: :tsat,
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def _update_where_expr
|
|
50
|
+
return self.qualified_table_sequel_identifier[:updated] < Sequel[:excluded][:updated]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def _mixin_backfill_url
|
|
54
|
+
return "https://api.stripe.com/v1/refunds"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def _mixin_event_type_names
|
|
58
|
+
return ["charge.refund.updated"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def restricted_key_resource_name = "Charges"
|
|
62
|
+
|
|
63
|
+
def _upsert_webhook(request, upsert: true)
|
|
64
|
+
resource, _event = self._resource_and_event(request)
|
|
65
|
+
object_type = resource.fetch("object")
|
|
66
|
+
return super if object_type == "refund"
|
|
67
|
+
|
|
68
|
+
# We can hit this when processing a 'charges.updated' webhook for refunds.
|
|
69
|
+
# We have to pull a list of refunds from the "charges" webhook.
|
|
70
|
+
# There is a somewhat infuriating nested pagination
|
|
71
|
+
# mechanism here, where the refunds list in the charge takes this form:
|
|
72
|
+
#
|
|
73
|
+
# "refunds": {
|
|
74
|
+
# "object": "list",
|
|
75
|
+
# "data": [],
|
|
76
|
+
# "has_more": false,
|
|
77
|
+
# "url": "/v1/charges/ch_1JG8U9FFYxHXGyKxPaNIdc0b/refunds"
|
|
78
|
+
# }
|
|
79
|
+
#
|
|
80
|
+
# and the `has_more` and `url` fields contain the information that is rquired to kick off
|
|
81
|
+
# a paginated backfill. `has_more` is almost always going to be false, because it should be
|
|
82
|
+
# rare that a charge has more than ten refunds, so we're just going to ignore this concern
|
|
83
|
+
# for now and issue a DeveloperAlert if pagination is required.
|
|
84
|
+
refunds_obj = request.body.dig("data", "object", "refunds")
|
|
85
|
+
|
|
86
|
+
if refunds_obj.fetch("has_more") == true
|
|
87
|
+
Webhookdb::DeveloperAlert.new(
|
|
88
|
+
subsystem: "Stripe Refunds Webhook Error",
|
|
89
|
+
emoji: ":hook:",
|
|
90
|
+
fallback: "Full backfill required for integration #{self.service_integration.opaque_id}",
|
|
91
|
+
fields: [],
|
|
92
|
+
).emit
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
refunds_obj.fetch("data").each do |b|
|
|
96
|
+
new_request = request.dup
|
|
97
|
+
new_request.body = b
|
|
98
|
+
super(new_request, upsert:)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stripe"
|
|
4
|
+
require "webhookdb/replicator/stripe_v1_mixin"
|
|
5
|
+
|
|
6
|
+
class Webhookdb::Replicator::StripeSubscriptionItemV1 < Webhookdb::Replicator::Base
|
|
7
|
+
include Appydays::Loggable
|
|
8
|
+
include Webhookdb::Replicator::StripeV1Mixin
|
|
9
|
+
|
|
10
|
+
# @return [Webhookdb::Replicator::Descriptor]
|
|
11
|
+
def self.descriptor
|
|
12
|
+
return Webhookdb::Replicator::Descriptor.new(
|
|
13
|
+
name: "stripe_subscription_item_v1",
|
|
14
|
+
ctor: ->(sint) { Webhookdb::Replicator::StripeSubscriptionItemV1.new(sint) },
|
|
15
|
+
feature_roles: [],
|
|
16
|
+
resource_name_singular: "Stripe Subscription Item",
|
|
17
|
+
supports_webhooks: true,
|
|
18
|
+
supports_backfill: true,
|
|
19
|
+
api_docs_url: "https://stripe.com/docs/api/subscription_items",
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def _remote_key_column
|
|
24
|
+
return Webhookdb::Replicator::Column.new(:stripe_id, TEXT, data_key: "id")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def _denormalized_columns
|
|
28
|
+
return [
|
|
29
|
+
Webhookdb::Replicator::Column.new(:created, TIMESTAMP, index: true, converter: :tsat),
|
|
30
|
+
Webhookdb::Replicator::Column.new(:price, TEXT, index: true, data_key: ["price", "id"], optional: true),
|
|
31
|
+
Webhookdb::Replicator::Column.new(:product, TEXT, index: true, data_key: ["price", "product"], optional: true),
|
|
32
|
+
Webhookdb::Replicator::Column.new(:quantity, INTEGER),
|
|
33
|
+
Webhookdb::Replicator::Column.new(:subscription, TEXT, index: true),
|
|
34
|
+
Webhookdb::Replicator::Column.new(
|
|
35
|
+
:updated,
|
|
36
|
+
TIMESTAMP,
|
|
37
|
+
index: true,
|
|
38
|
+
data_key: "created",
|
|
39
|
+
event_key: "created",
|
|
40
|
+
converter: :tsat,
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def _update_where_expr
|
|
46
|
+
return self.qualified_table_sequel_identifier[:updated] < Sequel[:excluded][:updated]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def _mixin_backfill_url
|
|
50
|
+
return "https://api.stripe.com/v1/subscription_items"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def _mixin_event_type_names
|
|
54
|
+
return []
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "stripe"
|
|
4
|
+
require "webhookdb/replicator/stripe_v1_mixin"
|
|
5
|
+
|
|
6
|
+
class Webhookdb::Replicator::StripeSubscriptionV1 < Webhookdb::Replicator::Base
|
|
7
|
+
include Appydays::Loggable
|
|
8
|
+
include Webhookdb::Replicator::StripeV1Mixin
|
|
9
|
+
|
|
10
|
+
# @return [Webhookdb::Replicator::Descriptor]
|
|
11
|
+
def self.descriptor
|
|
12
|
+
return Webhookdb::Replicator::Descriptor.new(
|
|
13
|
+
name: "stripe_subscription_v1",
|
|
14
|
+
ctor: ->(sint) { Webhookdb::Replicator::StripeSubscriptionV1.new(sint) },
|
|
15
|
+
feature_roles: [],
|
|
16
|
+
resource_name_singular: "Stripe Subscription",
|
|
17
|
+
supports_webhooks: true,
|
|
18
|
+
supports_backfill: true,
|
|
19
|
+
api_docs_url: "https://stripe.com/docs/api/subscriptions",
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def _remote_key_column
|
|
24
|
+
return Webhookdb::Replicator::Column.new(:stripe_id, TEXT, data_key: "id")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def _denormalized_columns
|
|
28
|
+
return [
|
|
29
|
+
Webhookdb::Replicator::Column.new(:billing_cycle_anchor, TIMESTAMP, index: true, converter: :tsat),
|
|
30
|
+
Webhookdb::Replicator::Column.new(:cancel_at, TIMESTAMP, index: true, optional: true, converter: :tsat),
|
|
31
|
+
Webhookdb::Replicator::Column.new(:canceled_at, TIMESTAMP, index: true, optional: true, converter: :tsat),
|
|
32
|
+
Webhookdb::Replicator::Column.new(:created, TIMESTAMP, index: true, converter: :tsat),
|
|
33
|
+
Webhookdb::Replicator::Column.new(:current_period_end, TIMESTAMP, index: true, converter: :tsat),
|
|
34
|
+
Webhookdb::Replicator::Column.new(:current_period_start, TIMESTAMP, index: true, converter: :tsat),
|
|
35
|
+
Webhookdb::Replicator::Column.new(:customer, TEXT, index: true),
|
|
36
|
+
Webhookdb::Replicator::Column.new(:default_payment_method, TEXT),
|
|
37
|
+
Webhookdb::Replicator::Column.new(:default_source, TEXT),
|
|
38
|
+
Webhookdb::Replicator::Column.new(:discount, TEXT, index: true),
|
|
39
|
+
Webhookdb::Replicator::Column.new(:ended_at, TIMESTAMP, index: true, optional: true),
|
|
40
|
+
Webhookdb::Replicator::Column.new(:latest_invoice, TEXT, index: true),
|
|
41
|
+
Webhookdb::Replicator::Column.new(:schedule, TEXT, index: true),
|
|
42
|
+
Webhookdb::Replicator::Column.new(:start_date, TIMESTAMP, index: true, converter: :tsat),
|
|
43
|
+
Webhookdb::Replicator::Column.new(:status, TEXT),
|
|
44
|
+
Webhookdb::Replicator::Column.new(:trial_end, TIMESTAMP, optional: true, converter: :tsat),
|
|
45
|
+
Webhookdb::Replicator::Column.new(:trial_start, TIMESTAMP, optional: true, converter: :tsat),
|
|
46
|
+
Webhookdb::Replicator::Column.new(
|
|
47
|
+
:updated,
|
|
48
|
+
TIMESTAMP,
|
|
49
|
+
index: true,
|
|
50
|
+
data_key: "created",
|
|
51
|
+
event_key: "created",
|
|
52
|
+
converter: :tsat,
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def _update_where_expr
|
|
58
|
+
return self.qualified_table_sequel_identifier[:updated] < Sequel[:excluded][:updated]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def _mixin_backfill_url
|
|
62
|
+
return "https://api.stripe.com/v1/subscriptions"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def _mixin_event_type_names
|
|
66
|
+
return [
|
|
67
|
+
"customer.subscription.created",
|
|
68
|
+
"customer.subscription.deleted",
|
|
69
|
+
"customer.subscription.pending_update_applied",
|
|
70
|
+
"customer.subscription.pending_update_expired",
|
|
71
|
+
"customer.subscription.trial_will_end",
|
|
72
|
+
"customer.subscription.updated",
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webhookdb/stripe"
|
|
4
|
+
|
|
5
|
+
module Webhookdb::Replicator::StripeV1Mixin
|
|
6
|
+
def _resource_and_event(request)
|
|
7
|
+
body = request.body
|
|
8
|
+
return body.fetch("data").fetch("object"), body if body.fetch("object") == "event"
|
|
9
|
+
return body, nil
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def _mixin_backfill_url
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# this array describes which event this webhook should subscribe to
|
|
17
|
+
# https://stripe.com/docs/api/events/types
|
|
18
|
+
def _mixin_event_type_names
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def _webhook_response(request)
|
|
23
|
+
return Webhookdb::Stripe.webhook_response(request, self.service_integration.webhook_secret)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def _timestamp_column_name
|
|
27
|
+
return :updated
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def calculate_webhook_state_machine
|
|
31
|
+
step = Webhookdb::Replicator::StateMachineStep.new
|
|
32
|
+
unless self.service_integration.webhook_secret.present?
|
|
33
|
+
step.output = %{You are about to start replicating #{self.resource_name_singular} info into WebhookDB.
|
|
34
|
+
We've made an endpoint available for #{self.resource_name_singular} webhooks:
|
|
35
|
+
|
|
36
|
+
#{self._webhook_endpoint}
|
|
37
|
+
|
|
38
|
+
From your Stripe Dashboard, go to Developers -> Webhooks -> Add Endpoint.
|
|
39
|
+
Use the URL above, and choose all of the following events:
|
|
40
|
+
#{self._mixin_event_type_names.join("\n ")}
|
|
41
|
+
Then click Add Endpoint.
|
|
42
|
+
|
|
43
|
+
The page for the webhook will have a 'Signing Secret' section.
|
|
44
|
+
Reveal it, then copy the secret (it will start with `whsec_`).
|
|
45
|
+
}
|
|
46
|
+
return step.secret_prompt("secret").webhook_secret(self.service_integration)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
step.output = %(Great! WebhookDB is now listening for #{self.resource_name_singular} webhooks.
|
|
50
|
+
#{self._query_help_output}
|
|
51
|
+
In order to backfill existing #{self.resource_name_plural}, run this from a shell:
|
|
52
|
+
|
|
53
|
+
#{self._backfill_command}
|
|
54
|
+
)
|
|
55
|
+
return step.completed
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def calculate_backfill_state_machine
|
|
59
|
+
step = Webhookdb::Replicator::StateMachineStep.new
|
|
60
|
+
unless self.service_integration.backfill_key.present?
|
|
61
|
+
step.output = %(In order to backfill #{self.resource_name_plural}, we need an API key.
|
|
62
|
+
From your Stripe Dashboard, go to Developers -> API Keys -> Restricted Keys -> Create Restricted Key.
|
|
63
|
+
Create a key with Read access to #{self.restricted_key_resource_name}.
|
|
64
|
+
Submit, then copy the key when Stripe shows it to you:
|
|
65
|
+
)
|
|
66
|
+
return step.secret_prompt("Restricted Key").backfill_key(self.service_integration)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
unless (result = self.verify_backfill_credentials).verified
|
|
70
|
+
self.service_integration.replicator.clear_backfill_information
|
|
71
|
+
step.output = result.message
|
|
72
|
+
return step.secret_prompt("Restricted Key").backfill_key(self.service_integration)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
step.output = %(Great! We are going to start backfilling your #{self.resource_name_plural}.
|
|
76
|
+
#{self._query_help_output}
|
|
77
|
+
)
|
|
78
|
+
return step.completed
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def restricted_key_resource_name = self.resource_name_plural.gsub(/^Stripe /, "")
|
|
82
|
+
|
|
83
|
+
def _verify_backfill_403_err_msg
|
|
84
|
+
return "It looks like that API Key does not have permission to access #{self.resource_name_singular} Records. " \
|
|
85
|
+
"Please check the permissions by going to the list of restricted keys and " \
|
|
86
|
+
"hovering over the information icon in the entry for this key. " \
|
|
87
|
+
"Once you've verified or corrected the permissions for this key, " \
|
|
88
|
+
"please reenter the API Key you just created:"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def _verify_backfill_401_err_msg
|
|
92
|
+
return "It looks like that API Key is invalid. Please reenter the API Key you just created:"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def _verify_backfill_err_msg
|
|
96
|
+
return "An error occurred. Please reenter the API Key you just created:"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def _fetch_backfill_page(pagination_token, **_kwargs)
|
|
100
|
+
url = self._mixin_backfill_url
|
|
101
|
+
url += pagination_token if pagination_token.present?
|
|
102
|
+
response = Webhookdb::Http.get(
|
|
103
|
+
url,
|
|
104
|
+
basic_auth: {username: self.service_integration.backfill_key},
|
|
105
|
+
logger: self.logger,
|
|
106
|
+
timeout: Webhookdb::Stripe.http_timeout,
|
|
107
|
+
)
|
|
108
|
+
data = response.parsed_response
|
|
109
|
+
next_page_param = nil
|
|
110
|
+
if data["has_more"]
|
|
111
|
+
last_item_id = data["data"][-1]["id"]
|
|
112
|
+
next_page_param = "?starting_after=" + last_item_id
|
|
113
|
+
end
|
|
114
|
+
return data["data"], next_page_param
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webhookdb/replicator/transistor_v1_mixin"
|
|
4
|
+
|
|
5
|
+
class Webhookdb::Replicator::TransistorEpisodeStatsV1 < Webhookdb::Replicator::Base
|
|
6
|
+
include Appydays::Loggable
|
|
7
|
+
include Webhookdb::Replicator::TransistorV1Mixin
|
|
8
|
+
|
|
9
|
+
# @return [Webhookdb::Replicator::Descriptor]
|
|
10
|
+
def self.descriptor
|
|
11
|
+
return Webhookdb::Replicator::Descriptor.new(
|
|
12
|
+
name: "transistor_episode_stats_v1",
|
|
13
|
+
ctor: ->(sint) { Webhookdb::Replicator::TransistorEpisodeStatsV1.new(sint) },
|
|
14
|
+
feature_roles: [],
|
|
15
|
+
resource_name_singular: "Transistor Episode Stats",
|
|
16
|
+
resource_name_plural: "Transistor Episode Stats",
|
|
17
|
+
dependency_descriptor: Webhookdb::Replicator::TransistorEpisodeV1.descriptor,
|
|
18
|
+
supports_backfill: true,
|
|
19
|
+
api_docs_url: "https://developers.transistor.fm/#EpisodeAnalytics",
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
CONV_PARSE_DMY_DASH = Webhookdb::Replicator::Column::IsomorphicProc.new(
|
|
24
|
+
ruby: lambda do |s, **_|
|
|
25
|
+
return Date.strptime(s, "%d-%m-%Y")
|
|
26
|
+
rescue TypeError, Date::Error
|
|
27
|
+
return nil
|
|
28
|
+
end,
|
|
29
|
+
sql: ->(e) { Sequel.function(:to_date, e, "DD-MM-YYYY") },
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
CONV_REMOTE_KEY = Webhookdb::Replicator::Column::IsomorphicProc.new(
|
|
33
|
+
ruby: ->(_, resource:, **_) { "#{resource.fetch('episode_id')}-#{resource.fetch('date')}" },
|
|
34
|
+
# Because this is a non-nullable key, we never need this in SQL
|
|
35
|
+
sql: ->(_) { Sequel.lit("'do not use'") },
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _remote_key_column
|
|
39
|
+
return Webhookdb::Replicator::Column.new(
|
|
40
|
+
:compound_identity,
|
|
41
|
+
TEXT,
|
|
42
|
+
data_key: "<compound key, see converter>",
|
|
43
|
+
index: true,
|
|
44
|
+
optional: true,
|
|
45
|
+
converter: CONV_REMOTE_KEY,
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def _denormalized_columns
|
|
50
|
+
return [
|
|
51
|
+
Webhookdb::Replicator::Column.new(:episode_id, TEXT),
|
|
52
|
+
Webhookdb::Replicator::Column.new(:date, DATE, converter: CONV_PARSE_DMY_DASH),
|
|
53
|
+
Webhookdb::Replicator::Column.new(:downloads, INTEGER),
|
|
54
|
+
Webhookdb::Replicator::Column.new(:row_updated_at, TIMESTAMP, defaulter: :now, optional: true),
|
|
55
|
+
]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def _timestamp_column_name
|
|
59
|
+
return :row_updated_at
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def _resource_and_event(request)
|
|
63
|
+
return request.body, nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def _update_where_expr
|
|
67
|
+
return self.qualified_table_sequel_identifier[:downloads] !~ Sequel[:excluded][:downloads]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def calculate_backfill_state_machine
|
|
71
|
+
if (step = self.calculate_dependency_state_machine_step(dependency_help: ""))
|
|
72
|
+
return step
|
|
73
|
+
end
|
|
74
|
+
step = Webhookdb::Replicator::StateMachineStep.new
|
|
75
|
+
step.output = %(Great! That's all the information we need.
|
|
76
|
+
When your Transistor Episodes get added or updated, their stats will be updated in WebhookDB too.
|
|
77
|
+
|
|
78
|
+
#{self._query_help_output})
|
|
79
|
+
return step.completed
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def _backfillers
|
|
83
|
+
episode_svc = self.service_integration.depends_on.replicator
|
|
84
|
+
backfillers = episode_svc.admin_dataset(timeout: :fast) do |episode_ds|
|
|
85
|
+
episode_ds.select(:transistor_id, :created_at).map do |episode|
|
|
86
|
+
EpisodeStatsBackfiller.new(
|
|
87
|
+
episode_svc:,
|
|
88
|
+
episode_stats_svc: self,
|
|
89
|
+
episode_id: episode[:transistor_id],
|
|
90
|
+
episode_created_at: episode[:created_at],
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
return backfillers
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class EpisodeStatsBackfiller < Webhookdb::Backfiller
|
|
98
|
+
def initialize(episode_svc:, episode_stats_svc:, episode_id:, episode_created_at:)
|
|
99
|
+
@episode_svc = episode_svc
|
|
100
|
+
@episode_stats_svc = episode_stats_svc
|
|
101
|
+
@episode_id = episode_id
|
|
102
|
+
@episode_created_at = episode_created_at
|
|
103
|
+
super()
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def handle_item(item)
|
|
107
|
+
item["episode_id"] = @episode_id
|
|
108
|
+
@episode_stats_svc.upsert_webhook_body(item)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def fetch_backfill_page(_pagination_token, **_kwargs)
|
|
112
|
+
analytics_url = "https://api.transistor.fm/v1/analytics/episodes/" + @episode_id
|
|
113
|
+
# The "downloads" stat gets collected daily but will not change retroactively for a past date.
|
|
114
|
+
# If there are already rows in the enrichment table matching the episode_id, we want to check
|
|
115
|
+
# the date of the last entry so that we don't have to upsert information that we know will not
|
|
116
|
+
# be changed. We allow for a two day buffer before the date of the last entry to account for changes
|
|
117
|
+
# that may occur on the day of a new entry, while the downloads are accruing.
|
|
118
|
+
latest_update = @episode_stats_svc.admin_dataset(timeout: :fast) do |ds|
|
|
119
|
+
ds.where(episode_id: @episode_id).max(:date)
|
|
120
|
+
end
|
|
121
|
+
start_date = latest_update.nil? ? @episode_created_at : (latest_update - 2.days)
|
|
122
|
+
request_body = {
|
|
123
|
+
start_date: start_date.strftime("%d-%m-%Y"),
|
|
124
|
+
end_date: Time.now.strftime("%d-%m-%Y"),
|
|
125
|
+
}
|
|
126
|
+
response = Webhookdb::Http.get(
|
|
127
|
+
analytics_url,
|
|
128
|
+
headers: {"x-api-key" => @episode_svc.service_integration.backfill_key},
|
|
129
|
+
body: request_body,
|
|
130
|
+
logger: @episode_stats_svc.logger,
|
|
131
|
+
timeout: Webhookdb::Transistor.http_timeout,
|
|
132
|
+
)
|
|
133
|
+
data = response.parsed_response.dig("data", "attributes", "downloads") || []
|
|
134
|
+
return data, nil
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def on_dependency_webhook_upsert(_replicator, _payload, *)
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "webhookdb/transistor"
|
|
4
|
+
require "webhookdb/replicator/transistor_v1_mixin"
|
|
5
|
+
require "nokogiri"
|
|
6
|
+
|
|
7
|
+
class Webhookdb::Replicator::TransistorEpisodeV1 < Webhookdb::Replicator::Base
|
|
8
|
+
include Appydays::Loggable
|
|
9
|
+
include Webhookdb::Replicator::TransistorV1Mixin
|
|
10
|
+
|
|
11
|
+
# @return [Webhookdb::Replicator::Descriptor]
|
|
12
|
+
def self.descriptor
|
|
13
|
+
return Webhookdb::Replicator::Descriptor.new(
|
|
14
|
+
name: "transistor_episode_v1",
|
|
15
|
+
ctor: ->(sint) { Webhookdb::Replicator::TransistorEpisodeV1.new(sint) },
|
|
16
|
+
feature_roles: [],
|
|
17
|
+
resource_name_singular: "Transistor Episode",
|
|
18
|
+
supports_backfill: true,
|
|
19
|
+
api_docs_url: "https://developers.transistor.fm/#Episode",
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def _denormalized_columns
|
|
24
|
+
return [
|
|
25
|
+
Webhookdb::Replicator::Column.new(:author, TEXT, data_key: ["attributes", "author"]),
|
|
26
|
+
Webhookdb::Replicator::Column.new(
|
|
27
|
+
:created_at,
|
|
28
|
+
TIMESTAMP,
|
|
29
|
+
index: true,
|
|
30
|
+
data_key: ["attributes", "created_at"],
|
|
31
|
+
),
|
|
32
|
+
Webhookdb::Replicator::Column.new(:duration, INTEGER, data_key: ["attributes", "duration"]),
|
|
33
|
+
Webhookdb::Replicator::Column.new(:keywords, TEXT, data_key: ["attributes", "keywords"]),
|
|
34
|
+
Webhookdb::Replicator::Column.new(:number, INTEGER, index: true, data_key: ["attributes", "number"]),
|
|
35
|
+
Webhookdb::Replicator::Column.new(
|
|
36
|
+
:published_at,
|
|
37
|
+
TIMESTAMP,
|
|
38
|
+
index: true,
|
|
39
|
+
data_key: ["attributes", "published_at"],
|
|
40
|
+
),
|
|
41
|
+
Webhookdb::Replicator::Column.new(:season, INTEGER, index: true, data_key: ["attributes", "season"]),
|
|
42
|
+
Webhookdb::Replicator::Column.new(
|
|
43
|
+
:show_id,
|
|
44
|
+
TEXT,
|
|
45
|
+
index: true,
|
|
46
|
+
data_key: ["relationships", "show", "data", "id"],
|
|
47
|
+
),
|
|
48
|
+
Webhookdb::Replicator::Column.new(:status, TEXT, data_key: ["attributes", "status"]),
|
|
49
|
+
Webhookdb::Replicator::Column.new(:title, TEXT, data_key: ["attributes", "title"]),
|
|
50
|
+
Webhookdb::Replicator::Column.new(:type, TEXT, data_key: ["attributes", "type"]),
|
|
51
|
+
Webhookdb::Replicator::Column.new(
|
|
52
|
+
:updated_at,
|
|
53
|
+
TIMESTAMP,
|
|
54
|
+
index: true,
|
|
55
|
+
data_key: ["attributes", "updated_at"],
|
|
56
|
+
),
|
|
57
|
+
# Ideally these would have converters, but they'd be very confusing, and when this was built
|
|
58
|
+
# we only had one transistor user, so we truncated the table instead.
|
|
59
|
+
Webhookdb::Replicator::Column.new(:api_format, INTEGER, optional: true),
|
|
60
|
+
Webhookdb::Replicator::Column.new(:logical_summary, TEXT, optional: true),
|
|
61
|
+
Webhookdb::Replicator::Column.new(:logical_description, TEXT, optional: true),
|
|
62
|
+
]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def _prepare_for_insert(resource, event, request, enrichment)
|
|
66
|
+
h = super
|
|
67
|
+
# Transistor merged their summary and description fields so they're authored
|
|
68
|
+
# as one big 'description' HTML blob in February 2023. Previous to that,
|
|
69
|
+
# there were separate summary and description fields
|
|
70
|
+
# (we call this api_format 1).
|
|
71
|
+
#
|
|
72
|
+
# If we have a nil summary, we know this is a 'new' format (api_format 2).
|
|
73
|
+
# In that case, look for the first line of the HTML,
|
|
74
|
+
# and treat that as the summary. Anything else in the HTML is treated as
|
|
75
|
+
# the remaining description. Some care is paid to whitespace, too,
|
|
76
|
+
# since <br> tags can be used within an element.
|
|
77
|
+
summary = resource.fetch("attributes").fetch("summary", nil)
|
|
78
|
+
description = resource.fetch("attributes").fetch("description", nil)
|
|
79
|
+
if summary.nil?
|
|
80
|
+
h[:api_format] = 2
|
|
81
|
+
parsed_desc = Nokogiri::HTML5.fragment(description)
|
|
82
|
+
|
|
83
|
+
extracted_summary = self._extract_first_html_line_as_text(parsed_desc)
|
|
84
|
+
h[:logical_description] = nil
|
|
85
|
+
if extracted_summary
|
|
86
|
+
h[:logical_summary] = extracted_summary
|
|
87
|
+
h[:logical_description] = parsed_desc.to_s.strip if parsed_desc.inner_text.present?
|
|
88
|
+
else
|
|
89
|
+
h[:logical_summary] = parsed_desc.to_s.strip
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
h[:logical_summary] = summary
|
|
93
|
+
h[:logical_description] = description
|
|
94
|
+
h[:api_format] = 1
|
|
95
|
+
end
|
|
96
|
+
return h
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Usually the Transistor HTML looks like <div>foo<br><br>hello</div>.
|
|
100
|
+
# Extract 'foo' as text, remove leading <br>, and return <div>hello</div>.
|
|
101
|
+
def _extract_first_html_line_as_text(element)
|
|
102
|
+
# Grab the first div or p element, where the text is.
|
|
103
|
+
first_div = element.css("div, p").first
|
|
104
|
+
return nil unless first_div
|
|
105
|
+
# Iterate over each child element:
|
|
106
|
+
# - If it's a text element, it's part of the first line.
|
|
107
|
+
# - If it's a br/div/p element, we have reached the end of the first line.
|
|
108
|
+
# - Otherwise, it's probably some type of style element, and can be appended.
|
|
109
|
+
first_line_html = +""
|
|
110
|
+
first_div.children.to_a.each do |child|
|
|
111
|
+
if child.is_a?(Nokogiri::XML::Text)
|
|
112
|
+
first_line_html << child.inner_text
|
|
113
|
+
child.remove
|
|
114
|
+
elsif child.name == "br"
|
|
115
|
+
# Remove additional br tags, this is like
|
|
116
|
+
# removing leading whitespace of the new/remaining description.
|
|
117
|
+
while (sibling = child.next)
|
|
118
|
+
break unless sibling.name == "br"
|
|
119
|
+
sibling.remove
|
|
120
|
+
end
|
|
121
|
+
child.remove
|
|
122
|
+
break
|
|
123
|
+
elsif BLOCK_ELEMENT_TAGS.include?(child.name)
|
|
124
|
+
break
|
|
125
|
+
else
|
|
126
|
+
first_line_html << child.to_html
|
|
127
|
+
child.remove
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
first_div.remove if first_div.inner_text.blank?
|
|
131
|
+
return first_line_html.strip
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
BLOCK_ELEMENT_TAGS = ["p", "div"].freeze
|
|
135
|
+
|
|
136
|
+
def upsert_has_deps?
|
|
137
|
+
return true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_date_from_api(date_string)
|
|
141
|
+
return Time.strptime(date_string, "%d-%m-%Y")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def _fetch_backfill_page(pagination_token, last_backfilled:)
|
|
145
|
+
url = "https://api.transistor.fm/v1/episodes"
|
|
146
|
+
pagination_token = 1 if pagination_token.blank?
|
|
147
|
+
response = Webhookdb::Http.get(
|
|
148
|
+
url,
|
|
149
|
+
headers: {"x-api-key" => self.service_integration.backfill_key},
|
|
150
|
+
body: {pagination: {page: pagination_token, per: 500}},
|
|
151
|
+
logger: self.logger,
|
|
152
|
+
timeout: Webhookdb::Transistor.http_timeout,
|
|
153
|
+
)
|
|
154
|
+
data = response.parsed_response
|
|
155
|
+
episodes = data["data"]
|
|
156
|
+
current_page = data["meta"]["currentPage"]
|
|
157
|
+
total_pages = data["meta"]["totalPages"]
|
|
158
|
+
next_page = (current_page.to_i + 1 if current_page < total_pages)
|
|
159
|
+
|
|
160
|
+
if last_backfilled.present?
|
|
161
|
+
earliest_data_created = episodes.empty? ? Time.at(0) : episodes[-1].dig("attributes", "created_at")
|
|
162
|
+
paged_to_already_seen_records = earliest_data_created < last_backfilled
|
|
163
|
+
|
|
164
|
+
return episodes, nil if paged_to_already_seen_records
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
return episodes, next_page
|
|
168
|
+
end
|
|
169
|
+
end
|