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,465 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "down"
|
|
4
|
+
require "ice_cube"
|
|
5
|
+
|
|
6
|
+
require "webhookdb/icalendar"
|
|
7
|
+
require "webhookdb/jobs/icalendar_sync"
|
|
8
|
+
require "webhookdb/messages/error_icalendar_fetch"
|
|
9
|
+
|
|
10
|
+
class Webhookdb::Replicator::IcalendarCalendarV1 < Webhookdb::Replicator::Base
|
|
11
|
+
include Appydays::Loggable
|
|
12
|
+
|
|
13
|
+
RECURRENCE_PROJECTION = 5.years
|
|
14
|
+
|
|
15
|
+
def documentation_url = Webhookdb::Icalendar::DOCUMENTATION_URL
|
|
16
|
+
|
|
17
|
+
# @return [Webhookdb::Replicator::Descriptor]
|
|
18
|
+
def self.descriptor
|
|
19
|
+
return Webhookdb::Replicator::Descriptor.new(
|
|
20
|
+
name: "icalendar_calendar_v1",
|
|
21
|
+
ctor: ->(sint) { Webhookdb::Replicator::IcalendarCalendarV1.new(sint) },
|
|
22
|
+
feature_roles: [],
|
|
23
|
+
resource_name_singular: "iCalendar Calendar",
|
|
24
|
+
supports_webhooks: true,
|
|
25
|
+
description: "Fetch and convert an icalendar format file into a schematized and queryable database table.",
|
|
26
|
+
api_docs_url: "https://icalendar.org/",
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def upsert_has_deps? = true
|
|
31
|
+
|
|
32
|
+
def _webhook_response(request)
|
|
33
|
+
return Webhookdb::WebhookResponse.for_standard_secret(request, self.service_integration.webhook_secret)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def calculate_webhook_state_machine
|
|
37
|
+
step = Webhookdb::Replicator::StateMachineStep.new
|
|
38
|
+
if self.service_integration.webhook_secret.blank?
|
|
39
|
+
self.service_integration.save_changes
|
|
40
|
+
step.output = %(You are about to add support for replicating iCalendar (.ics) URLs into WebhookDB.
|
|
41
|
+
|
|
42
|
+
We have detailed instructions on this process
|
|
43
|
+
at https://docs.webhookdb.com/guides/icalendar/.
|
|
44
|
+
|
|
45
|
+
The first step is to generate a secret you will use for signing
|
|
46
|
+
API requests you send to WebhookDB. You can use '#{Webhookdb::Id.rand_enc(16)}'
|
|
47
|
+
or generate your own value.
|
|
48
|
+
Copy and paste or enter a new value, and press enter.)
|
|
49
|
+
return step.secret_prompt("secret").webhook_secret(self.service_integration)
|
|
50
|
+
end
|
|
51
|
+
step.output = %(
|
|
52
|
+
All set! Here is the endpoint to send requests to
|
|
53
|
+
from your backend. Refer to https://docs.webhookdb.com/guides/icalendar/
|
|
54
|
+
for details on the format of the request:
|
|
55
|
+
|
|
56
|
+
#{self.webhook_endpoint}
|
|
57
|
+
|
|
58
|
+
The secret to use for signing is:
|
|
59
|
+
|
|
60
|
+
#{self.service_integration.webhook_secret}
|
|
61
|
+
|
|
62
|
+
#{self._query_help_output})
|
|
63
|
+
return step.completed
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def _remote_key_column
|
|
67
|
+
return Webhookdb::Replicator::Column.new(:external_id, TEXT)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def _denormalized_columns
|
|
71
|
+
col = Webhookdb::Replicator::Column
|
|
72
|
+
return [
|
|
73
|
+
col.new(:row_created_at, TIMESTAMP, index: true, optional: true, defaulter: :now),
|
|
74
|
+
col.new(:row_updated_at, TIMESTAMP, index: true, optional: true, defaulter: :now),
|
|
75
|
+
col.new(:last_synced_at, TIMESTAMP, index: true, optional: true),
|
|
76
|
+
col.new(:ics_url, TEXT, converter: col.converter_gsub("^webcal", "https")),
|
|
77
|
+
]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def _timestamp_column_name
|
|
81
|
+
return :row_updated_at
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def _resource_and_event(request)
|
|
85
|
+
return request.body, nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def _update_where_expr
|
|
89
|
+
return self.qualified_table_sequel_identifier[:row_updated_at] < Sequel[:excluded][:row_updated_at]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def _upsert_update_expr(inserting, **_kwargs)
|
|
93
|
+
update = super
|
|
94
|
+
# Only set created_at if it's not set so the initial insert isn't modified.
|
|
95
|
+
self._coalesce_excluded_on_update(update, [:row_created_at])
|
|
96
|
+
return update
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def _resource_to_data(resource, *)
|
|
100
|
+
data = resource.dup
|
|
101
|
+
# Remove the client-provided webhook fields.
|
|
102
|
+
data.clear
|
|
103
|
+
return data
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def upsert_webhook(request)
|
|
107
|
+
request_type = request.body.fetch("type")
|
|
108
|
+
external_id = request.body.fetch("external_id")
|
|
109
|
+
case request_type
|
|
110
|
+
when "SYNC"
|
|
111
|
+
super(request)
|
|
112
|
+
Webhookdb::Jobs::IcalendarSync.perform_async(self.service_integration.id, external_id)
|
|
113
|
+
return
|
|
114
|
+
when "DELETE"
|
|
115
|
+
self.delete_data_for_external_id(external_id)
|
|
116
|
+
return
|
|
117
|
+
when "__WHDB_UNIT_TEST"
|
|
118
|
+
unless Webhookdb::RACK_ENV == "test"
|
|
119
|
+
raise "someone tried to use the special unit test google event type outside of unit tests"
|
|
120
|
+
end
|
|
121
|
+
return super(request)
|
|
122
|
+
else
|
|
123
|
+
raise ArgumentError, "Unknown request type: #{request_type}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
CLEANUP_SERVICE_NAMES = ["icalendar_event_v1"].freeze
|
|
128
|
+
SYNC_PERIOD = 4.hours
|
|
129
|
+
|
|
130
|
+
def rows_needing_sync(dataset, now: Time.now)
|
|
131
|
+
cutoff = now - SYNC_PERIOD
|
|
132
|
+
return dataset.where(Sequel[last_synced_at: nil] | Sequel.expr { last_synced_at < cutoff })
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def delete_data_for_external_id(external_id)
|
|
136
|
+
relevant_integrations = self.service_integration.recursive_dependents.
|
|
137
|
+
filter { |d| CLEANUP_SERVICE_NAMES.include?(d.service_name) }
|
|
138
|
+
self.admin_dataset do |ds|
|
|
139
|
+
ds.db.transaction do
|
|
140
|
+
ds.where(external_id:).delete
|
|
141
|
+
relevant_integrations.each do |sint|
|
|
142
|
+
ds.db[sint.replicator.qualified_table_sequel_identifier].where(calendar_external_id: external_id).delete
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
class Upserter
|
|
149
|
+
include Webhookdb::Backfiller::Bulk
|
|
150
|
+
attr_reader :upserting_replicator, :calendar_external_id
|
|
151
|
+
|
|
152
|
+
def initialize(replicator, calendar_external_id)
|
|
153
|
+
@upserting_replicator = replicator
|
|
154
|
+
@calendar_external_id = calendar_external_id
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def upsert_page_size = 500
|
|
158
|
+
def conditional_upsert? = true
|
|
159
|
+
|
|
160
|
+
def prepare_body(body)
|
|
161
|
+
body["calendar_external_id"] = @calendar_external_id
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def sync_row(row)
|
|
166
|
+
Appydays::Loggable.with_log_tags(icalendar_url: row.fetch(:ics_url)) do
|
|
167
|
+
self.with_advisory_lock(row.fetch(:pk)) do
|
|
168
|
+
if (dep = self.find_dependent("icalendar_event_v1"))
|
|
169
|
+
self._sync_row(row, dep)
|
|
170
|
+
end
|
|
171
|
+
self.admin_dataset { |ds| ds.where(pk: row.fetch(:pk)).update(last_synced_at: Time.now) }
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def _sync_row(row, dep)
|
|
177
|
+
calendar_external_id = row.fetch(:external_id)
|
|
178
|
+
request_url = row.fetch(:ics_url)
|
|
179
|
+
begin
|
|
180
|
+
io = Webhookdb::Http.chunked_download(request_url, rewindable: false)
|
|
181
|
+
rescue Down::Error => e
|
|
182
|
+
self._handle_down_error(e, request_url:, calendar_external_id:)
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
upserter = Upserter.new(dep.replicator, calendar_external_id)
|
|
187
|
+
processor = EventProcessor.new(io, upserter)
|
|
188
|
+
processor.process
|
|
189
|
+
# Delete all the extra replicator rows, and cancel all the rows that weren't upserted.
|
|
190
|
+
dep.replicator.admin_dataset do |ds|
|
|
191
|
+
ds = ds.where(calendar_external_id:)
|
|
192
|
+
if (delete_condition = processor.delete_condition)
|
|
193
|
+
ds.where(delete_condition).delete
|
|
194
|
+
end
|
|
195
|
+
# Update both the status, and set the data json to match.
|
|
196
|
+
ds.exclude(compound_identity: processor.upserted_identities).update(
|
|
197
|
+
status: "CANCELLED",
|
|
198
|
+
data: Sequel.lit('data || \'{"STATUS":{"v":"CANCELLED"}}\'::jsonb'),
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
self.admin_dataset { |ds| ds.where(pk: row.fetch(:pk)).update(last_synced_at: Time.now) }
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def _handle_down_error(e, request_url:, calendar_external_id:)
|
|
205
|
+
case e
|
|
206
|
+
when Down::TooManyRedirects
|
|
207
|
+
response_status = 301
|
|
208
|
+
response_body = "<too many redirects>"
|
|
209
|
+
when Down::TimeoutError, Down::SSLError, Down::ConnectionError, Down::InvalidUrl
|
|
210
|
+
response_status = 0
|
|
211
|
+
response_body = e.to_s
|
|
212
|
+
when Down::ClientError
|
|
213
|
+
raise e if e.response.nil?
|
|
214
|
+
response_status = e.response.code.to_i
|
|
215
|
+
expected_errors = [
|
|
216
|
+
417, # If someone uses an Outlook HTML calendar, fetch gives us a 417
|
|
217
|
+
]
|
|
218
|
+
is_problem_error = (response_status > 404 || response_status < 400) &&
|
|
219
|
+
!expected_errors.include?(response_status)
|
|
220
|
+
raise e if is_problem_error
|
|
221
|
+
response_body = e.response.body.to_s
|
|
222
|
+
when Down::ServerError
|
|
223
|
+
response_status = e.response.code.to_i
|
|
224
|
+
response_body = e.response.body.to_s
|
|
225
|
+
else
|
|
226
|
+
response_body = nil
|
|
227
|
+
response_status = nil
|
|
228
|
+
end
|
|
229
|
+
raise e if response_status.nil?
|
|
230
|
+
self.logger.warn("icalendar_fetch_error",
|
|
231
|
+
response_body:, response_status:, request_url:, calendar_external_id:,)
|
|
232
|
+
message = Webhookdb::Messages::ErrorIcalendarFetch.new(
|
|
233
|
+
self.service_integration,
|
|
234
|
+
calendar_external_id,
|
|
235
|
+
response_status:,
|
|
236
|
+
response_body:,
|
|
237
|
+
request_url:,
|
|
238
|
+
request_method: "GET",
|
|
239
|
+
)
|
|
240
|
+
self.service_integration.organization.alerting.dispatch_alert(message)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
class EventProcessor
|
|
244
|
+
attr_reader :upserted_identities
|
|
245
|
+
|
|
246
|
+
def initialize(io, upserter)
|
|
247
|
+
@io = io
|
|
248
|
+
@upserter = upserter
|
|
249
|
+
# Keep track of everything we upsert. For any rows we aren't upserting,
|
|
250
|
+
# delete them if they're recurring, or cancel them if they're not recurring.
|
|
251
|
+
# If doing it this way is slow, we could invert this (pull down all IDs and pop from the set).
|
|
252
|
+
@upserted_identities = []
|
|
253
|
+
# Keep track of all upserted recurring items.
|
|
254
|
+
# If we find a RECURRENCE-ID on a later item,
|
|
255
|
+
# we need to modify the item from the sequence by stealing its compound identity.
|
|
256
|
+
@expanded_events_by_uid = {}
|
|
257
|
+
# Delete 'extra' recurring event rows.
|
|
258
|
+
# We need to keep track of how many events each UID spawns,
|
|
259
|
+
# so we can delete any with a higher count.
|
|
260
|
+
@max_sequence_num_by_uid = {}
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def delete_condition
|
|
264
|
+
return nil if @max_sequence_num_by_uid.empty?
|
|
265
|
+
return @max_sequence_num_by_uid.map do |uid, n|
|
|
266
|
+
Sequel[recurring_event_id: uid] & (Sequel[:recurring_event_sequence] > n)
|
|
267
|
+
end.inject(&:|)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def process
|
|
271
|
+
self.each_feed_event do |feed_event|
|
|
272
|
+
self.each_projected_event(feed_event) do |ev|
|
|
273
|
+
ident, upserted = @upserter.handle_item(ev)
|
|
274
|
+
@upserted_identities << ident
|
|
275
|
+
if (recurring_uid = upserted.fetch(:recurring_event_id))
|
|
276
|
+
@expanded_events_by_uid[recurring_uid] ||= []
|
|
277
|
+
@expanded_events_by_uid[recurring_uid] << upserted
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
@upserter.flush_pending_inserts
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def each_projected_event(h)
|
|
285
|
+
raise LocalJumpError unless block_given?
|
|
286
|
+
|
|
287
|
+
uid = h.fetch("UID").fetch("v")
|
|
288
|
+
|
|
289
|
+
if (recurrence_id = h["RECURRENCE-ID"])
|
|
290
|
+
# Track down the original item in the projected sequence, so we can update it.
|
|
291
|
+
if Webhookdb::Replicator::IcalendarEventV1.value_is_date_str?(recurrence_id.fetch("v"))
|
|
292
|
+
start = Webhookdb::Replicator::IcalendarEventV1.entry_to_date(recurrence_id)
|
|
293
|
+
startfield = :start_date
|
|
294
|
+
else
|
|
295
|
+
startfield = :start_at
|
|
296
|
+
start = Webhookdb::Replicator::IcalendarEventV1.entry_to_datetime(recurrence_id).first
|
|
297
|
+
end
|
|
298
|
+
candidates = @expanded_events_by_uid[uid]
|
|
299
|
+
if candidates.nil?
|
|
300
|
+
# We can have no recurring events, even with the exclusion date.
|
|
301
|
+
# Not much we can do here- just treat it as a standalone event.
|
|
302
|
+
yield h
|
|
303
|
+
return
|
|
304
|
+
end
|
|
305
|
+
unless (match = candidates.find { |c| c[startfield] == start })
|
|
306
|
+
# There are some providers (like Apple) where an excluded event
|
|
307
|
+
# will be outside the bounds of the RRULE of its owner.
|
|
308
|
+
# Usually the RRULE has an UNTIL that is before the RECURRENCE-ID datetime.
|
|
309
|
+
#
|
|
310
|
+
# In these cases, we can use the event as-is, but we need to
|
|
311
|
+
# make sure it is treated as part of the sequence.
|
|
312
|
+
# So increment the last-seen sequence number for the UID and use that.
|
|
313
|
+
max_seq_num = @max_sequence_num_by_uid[uid] += 1
|
|
314
|
+
h["UID"] = {"v" => "#{uid}-#{max_seq_num}"}
|
|
315
|
+
h["recurring_event_id"] = uid
|
|
316
|
+
h["recurring_event_sequence"] = max_seq_num
|
|
317
|
+
yield h
|
|
318
|
+
return
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Steal the UID to overwrite the original, and record where it came from.
|
|
322
|
+
# Note that all other fields, like categories, will be overwritten with the fields in this exclusion.
|
|
323
|
+
# This seems to be correct, but we should keep an eye open in case we need to merge
|
|
324
|
+
# these exclusion events into the originals.
|
|
325
|
+
h["UID"] = {"v" => match[:uid]}
|
|
326
|
+
h["recurring_event_sequence"] = match[:recurring_event_sequence]
|
|
327
|
+
# Usually the recurrent event and exclusion have the same last-modified.
|
|
328
|
+
# But we need to set the last-modified to AFTER the original,
|
|
329
|
+
# to make sure it replaces what's in the database (the original un-excluded event
|
|
330
|
+
# may already be present in the database).
|
|
331
|
+
h["LAST-MODIFIED"] = match.fetch(:last_modified_at) + 1.second
|
|
332
|
+
yield h
|
|
333
|
+
return
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
unless h["RRULE"]
|
|
337
|
+
yield h
|
|
338
|
+
return
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# We need to convert relevant parsed ical lines back to a string for use in ice_cube.
|
|
342
|
+
# There are other ways to handle this, but this is fine for now.
|
|
343
|
+
ical_params = {}
|
|
344
|
+
if (exdates = h["RDATE"])
|
|
345
|
+
ical_params[:rtimes] = exdates.map { |d| self._time_array(d) }.flatten
|
|
346
|
+
end
|
|
347
|
+
if (exdates = h["EXDATE"])
|
|
348
|
+
ical_params[:extimes] = exdates.map { |d| self._time_array(d) }.flatten
|
|
349
|
+
end
|
|
350
|
+
ical_params[:rrules] = [self._icecube_rule_from_ical(h["RRULE"]["v"])] if h["RRULE"]
|
|
351
|
+
# DURATION is not supported
|
|
352
|
+
|
|
353
|
+
start_entry = h.fetch("DTSTART")
|
|
354
|
+
ev_replicator = Webhookdb::Replicator::IcalendarEventV1
|
|
355
|
+
is_date = ev_replicator.entry_is_date_str?(start_entry)
|
|
356
|
+
# Use actual Times for start/end since ice_cube doesn't parse them well
|
|
357
|
+
ical_params[:start_time] = ev_replicator.entry_to_date_or_datetime(start_entry).first
|
|
358
|
+
if ical_params[:start_time].year < 1000
|
|
359
|
+
# This is almost definitely a misconfiguration. Yield it as non-recurring and move on.
|
|
360
|
+
yield h
|
|
361
|
+
return
|
|
362
|
+
end
|
|
363
|
+
has_end_time = false
|
|
364
|
+
if (end_entry = h["DTEND"])
|
|
365
|
+
# the end date is optional. If we don't have one, we should never store one.
|
|
366
|
+
has_end_time = true
|
|
367
|
+
ical_params[:end_time] = ev_replicator.entry_to_date_or_datetime(end_entry).first
|
|
368
|
+
if ical_params[:end_time] < ical_params[:start_time]
|
|
369
|
+
# This is an invalid event. Not sure what it'll do to IceCube so don't send it there.
|
|
370
|
+
# Yield it as a non-recurring event and move on.
|
|
371
|
+
yield h
|
|
372
|
+
return
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
schedule = IceCube::Schedule.from_hash(ical_params)
|
|
377
|
+
dont_project_before = Webhookdb::Icalendar.oldest_recurring_event
|
|
378
|
+
dont_project_after = Time.now + RECURRENCE_PROJECTION
|
|
379
|
+
|
|
380
|
+
# Just like google, track the original event id.
|
|
381
|
+
h["recurring_event_id"] = uid
|
|
382
|
+
final_sequence = -1
|
|
383
|
+
begin
|
|
384
|
+
schedule.send(:enumerate_occurrences, schedule.start_time).each_with_index do |occ, idx|
|
|
385
|
+
next if occ.start_time < dont_project_before
|
|
386
|
+
# Given the original hash, we will modify some fields.
|
|
387
|
+
e = h.dup
|
|
388
|
+
# Keep track of how many events we're managing.
|
|
389
|
+
e["recurring_event_sequence"] = idx
|
|
390
|
+
# The new UID has the sequence number.
|
|
391
|
+
e["UID"] = {"v" => "#{uid}-#{idx}"}
|
|
392
|
+
e["DTSTART"] = self._ical_entry_from_ruby(occ.start_time, start_entry, is_date)
|
|
393
|
+
e["DTEND"] = self._ical_entry_from_ruby(occ.end_time, end_entry, is_date) if has_end_time
|
|
394
|
+
yield e
|
|
395
|
+
final_sequence = idx
|
|
396
|
+
break if occ.start_time > dont_project_after
|
|
397
|
+
end
|
|
398
|
+
rescue Date::Error
|
|
399
|
+
# It's possible we yielded some recurring events too, in that case, treat them as normal,
|
|
400
|
+
# in addition to yielding the event as non-recurring.
|
|
401
|
+
yield h
|
|
402
|
+
end
|
|
403
|
+
@max_sequence_num_by_uid[uid] = final_sequence
|
|
404
|
+
return
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# We need is_date because the recurrence/IceCube schedule may be using times, not date.
|
|
408
|
+
def _ical_entry_from_ruby(r, entry, is_date)
|
|
409
|
+
return {"v" => r.strftime("%Y%m%d")} if is_date
|
|
410
|
+
return {"v" => r.strftime("%Y%m%dT%H%M%SZ")} if r.zone == "UTC"
|
|
411
|
+
return {"v" => r.strftime("%Y%m%dT%H%M%S"), "TZID" => entry.fetch("TZID")}
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def _icecube_rule_from_ical(ical)
|
|
415
|
+
# We have seen certain ambiguous rules, like FREQ=WEEKLY with BYMONTHDAY=4.
|
|
416
|
+
# Apple interprets this as every 2 weeks; rrule.js interprets it as on the 4th of the month.
|
|
417
|
+
# IceCube errors, because `day_of_month` isn't valid on a WeeklyRule.
|
|
418
|
+
# In this case, we need to sanitize the string to remove the offending rule piece.
|
|
419
|
+
# There are probably many other offending formats, but we'll add them here as needed.
|
|
420
|
+
if ical.include?("FREQ=WEEKLY") && ical.include?("BYMONTHDAY=")
|
|
421
|
+
ical = ical.gsub(/BYMONTHDAY=[\d,]+/, "")
|
|
422
|
+
ical.delete_prefix! ";"
|
|
423
|
+
ical.delete_suffix! ";"
|
|
424
|
+
end
|
|
425
|
+
return IceCube::IcalParser.rule_from_ical(ical)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def _time_array(h)
|
|
429
|
+
expanded_entries = h["v"].split(",").map { |v| h.merge("v" => v) }
|
|
430
|
+
return expanded_entries.map do |e|
|
|
431
|
+
parsed_val, _got_tz = Webhookdb::Replicator::IcalendarEventV1.entry_to_date_or_datetime(e)
|
|
432
|
+
next parsed_val if parsed_val.is_a?(Date)
|
|
433
|
+
# Convert to UTC. We don't work with ActiveSupport timezones in the icalendar code for the most part.
|
|
434
|
+
parsed_val.utc
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def each_feed_event
|
|
439
|
+
bad_event_uids = Set.new
|
|
440
|
+
vevent_lines = []
|
|
441
|
+
in_vevent = false
|
|
442
|
+
while (line = @io.gets)
|
|
443
|
+
line.rstrip!
|
|
444
|
+
if line == "BEGIN:VEVENT"
|
|
445
|
+
in_vevent = true
|
|
446
|
+
vevent_lines << line
|
|
447
|
+
elsif line == "END:VEVENT"
|
|
448
|
+
in_vevent = false
|
|
449
|
+
vevent_lines << line
|
|
450
|
+
h = Webhookdb::Replicator::IcalendarEventV1.vevent_to_hash(vevent_lines)
|
|
451
|
+
vevent_lines.clear
|
|
452
|
+
if h.key?("DTSTART") && h.key?("UID")
|
|
453
|
+
yield h
|
|
454
|
+
else
|
|
455
|
+
bad_event_uids << h.fetch("UID", {}).fetch("v", "[missing]")
|
|
456
|
+
end
|
|
457
|
+
elsif in_vevent
|
|
458
|
+
vevent_lines << line
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
return if bad_event_uids.empty?
|
|
462
|
+
@upserter.upserting_replicator.logger.warn("invalid_vevent_hash", vevent_uids: bad_event_uids.sort)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|