active_webhook 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.env.sample +1 -0
  3. data/.gitignore +63 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +86 -0
  6. data/.todo +48 -0
  7. data/.travis.yml +14 -0
  8. data/CHANGELOG.md +5 -0
  9. data/DEV_README.md +199 -0
  10. data/Gemfile +8 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +483 -0
  13. data/Rakefile +23 -0
  14. data/active_webhook.gemspec +75 -0
  15. data/config/environment.rb +33 -0
  16. data/lib/active_webhook.rb +125 -0
  17. data/lib/active_webhook/adapter.rb +117 -0
  18. data/lib/active_webhook/callbacks.rb +53 -0
  19. data/lib/active_webhook/configuration.rb +105 -0
  20. data/lib/active_webhook/delivery/base_adapter.rb +96 -0
  21. data/lib/active_webhook/delivery/configuration.rb +16 -0
  22. data/lib/active_webhook/delivery/faraday_adapter.rb +19 -0
  23. data/lib/active_webhook/delivery/net_http_adapter.rb +28 -0
  24. data/lib/active_webhook/error_log.rb +7 -0
  25. data/lib/active_webhook/formatting/base_adapter.rb +109 -0
  26. data/lib/active_webhook/formatting/configuration.rb +18 -0
  27. data/lib/active_webhook/formatting/json_adapter.rb +19 -0
  28. data/lib/active_webhook/formatting/url_encoded_adapter.rb +28 -0
  29. data/lib/active_webhook/hook.rb +9 -0
  30. data/lib/active_webhook/logger.rb +21 -0
  31. data/lib/active_webhook/models/configuration.rb +18 -0
  32. data/lib/active_webhook/models/error_log_additions.rb +15 -0
  33. data/lib/active_webhook/models/subscription_additions.rb +72 -0
  34. data/lib/active_webhook/models/topic_additions.rb +70 -0
  35. data/lib/active_webhook/queueing/active_job_adapter.rb +43 -0
  36. data/lib/active_webhook/queueing/base_adapter.rb +67 -0
  37. data/lib/active_webhook/queueing/configuration.rb +15 -0
  38. data/lib/active_webhook/queueing/delayed_job_adapter.rb +28 -0
  39. data/lib/active_webhook/queueing/sidekiq_adapter.rb +43 -0
  40. data/lib/active_webhook/queueing/syncronous_adapter.rb +14 -0
  41. data/lib/active_webhook/subscription.rb +7 -0
  42. data/lib/active_webhook/topic.rb +7 -0
  43. data/lib/active_webhook/verification/base_adapter.rb +31 -0
  44. data/lib/active_webhook/verification/configuration.rb +13 -0
  45. data/lib/active_webhook/verification/hmac_sha256_adapter.rb +20 -0
  46. data/lib/active_webhook/verification/unsigned_adapter.rb +11 -0
  47. data/lib/active_webhook/version.rb +5 -0
  48. data/lib/generators/install_generator.rb +20 -0
  49. data/lib/generators/migrations_generator.rb +24 -0
  50. data/lib/generators/templates/20210618023338_create_active_webhook_tables.rb +31 -0
  51. data/lib/generators/templates/active_webhook_config.rb +87 -0
  52. metadata +447 -0
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Models
5
+ module TopicAdditions
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ self.table_name = "active_webhook_topics"
10
+
11
+ scope :enabled, -> { where(disabled_at: nil) }
12
+ scope :with_key, lambda { |key:, version: nil|
13
+ scope = where(key: key)
14
+ scope = scope.where(version: version) if version.present?
15
+ scope
16
+ }
17
+
18
+ def self.last_with_key(key)
19
+ where(key: key).order(id: :desc).first
20
+ end
21
+
22
+ before_validation :set_valid_version
23
+ validates :key, presence: true
24
+ validates :version, presence: true, uniqueness: { scope: :key }
25
+ end
26
+
27
+ def disable(reason = nil)
28
+ self.disabled_at = Time.current
29
+ self.disabled_reason = reason
30
+ end
31
+
32
+ def disable!(reason = nil)
33
+ disable reason
34
+ save!
35
+ end
36
+
37
+ def enable
38
+ self.disabled_at = nil
39
+ self.disabled_reason = nil
40
+ end
41
+
42
+ def enable!
43
+ enable
44
+ save!
45
+ end
46
+
47
+ def disabled?
48
+ !enabled?
49
+ end
50
+
51
+ def enabled?
52
+ disabled_at.nil?
53
+ end
54
+
55
+ protected
56
+
57
+ def set_valid_version
58
+ return if version.present?
59
+
60
+ last_with_key = self.class.last_with_key key
61
+ versions = last_with_key&.version.to_s.split(".")
62
+ versions = [0] if versions.empty?
63
+ version = versions.pop
64
+ versions << version.to_i + 1
65
+
66
+ self.version = versions.join(".")
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Queueing
5
+ class ActiveJobAdapter < BaseAdapter
6
+ class SubscriptionJob < ApplicationJob
7
+ queue_as :low_priority
8
+
9
+ def perform(subscription, hook, context)
10
+ context.symbolize_keys!
11
+ hook = Hook.from_h(hook.symbolize_keys) unless hook.nil?
12
+
13
+ ActiveWebhook.queueing_adapter.fulfill_subscription(
14
+ subscription: subscription,
15
+ hook: hook,
16
+ job_id: job_id,
17
+ **context
18
+ )
19
+ end
20
+ end
21
+
22
+ class TopicJob < ApplicationJob
23
+ queue_as :low_priority
24
+
25
+ def perform(key, version, context)
26
+ context.symbolize_keys!
27
+
28
+ ActiveWebhook.queueing_adapter.new(key: key, version: version, **context).fulfill_topic
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ def promise_subscription(subscription:, hook:)
35
+ SubscriptionJob.perform_later subscription, hook&.to_h, context
36
+ end
37
+
38
+ def promise_topic
39
+ TopicJob.perform_later key, version, context
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Queueing
5
+ class BaseAdapter < Adapter
6
+ attribute :key, :version, :format_first
7
+
8
+ def format_first
9
+ @format_first.nil? ? component_configuration.format_first : @format_first
10
+ end
11
+
12
+ # returns count of jobs enqueued
13
+ def call
14
+ return fulfill_topic if format_first
15
+
16
+ promise_topic
17
+ 1
18
+ end
19
+
20
+ def self.build_hook(subscription, **context)
21
+ ActiveWebhook.formatting_adapter.call(subscription: subscription, **context)
22
+ end
23
+
24
+ def self.fulfill_subscription(subscription:, hook: nil, **context)
25
+ ActiveWebhook.delivery_adapter.call(
26
+ subscription: subscription,
27
+ hook: hook || build_hook(subscription, **context),
28
+ **context
29
+ ) if ActiveWebhook.enabled?
30
+ true
31
+ end
32
+
33
+ def fulfill_topic
34
+ subscriptions.each do |subscription|
35
+ hook = format_first ? self.class.build_hook(subscription, **context) : nil
36
+ byebug if context.key?("key")
37
+ promise_subscription subscription: subscription, hook: hook
38
+ end
39
+ subscriptions.count
40
+ end
41
+
42
+ protected
43
+
44
+ def self.component_name
45
+ "queueing"
46
+ end
47
+
48
+ def promise_subscription(_subscription:, _hook:)
49
+ raise NotImplementedError, "#promise_subscription must be implemented."
50
+ end
51
+
52
+ def promise_topic
53
+ raise NotImplementedError, "#promise_topic must be implemented."
54
+ end
55
+
56
+ def subscriptions
57
+ subscriptions_scope.all
58
+ end
59
+
60
+ def subscriptions_scope
61
+ ActiveWebhook.subscription_model.enabled.joins(:topic).includes(:topic).merge(
62
+ ActiveWebhook.topic_model.enabled.with_key(key: key, version: version)
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Queueing
5
+ class Configuration
6
+ include ActiveWebhook::Configuration::Base
7
+
8
+ define_option :adapter,
9
+ values: %i[syncronous sidekiq delayed_job active_job],
10
+ allow_proc: true
11
+
12
+ define_option :format_first, values: [true, false], default: false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delayed_job"
4
+
5
+ module ActiveWebhook
6
+ module Queueing
7
+ class DelayedJobAdapter < BaseAdapter
8
+ protected
9
+
10
+ def promise_subscription(subscription:, hook:)
11
+ ActiveWebhook.queueing_adapter.fulfill_subscription(
12
+ subscription: subscription,
13
+ hook: hook,
14
+ # NOTE: not implemented yet;
15
+ # SEE: https://stackoverflow.com/questions/21590798/referencing-delayed-job-job-id-from-within-the-job-task
16
+ # job_id: ???
17
+ **context
18
+ )
19
+ end
20
+ handle_asynchronously :promise_subscription
21
+
22
+ def promise_topic
23
+ fulfill_topic
24
+ end
25
+ handle_asynchronously :promise_topic
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sidekiq"
4
+
5
+ module ActiveWebhook
6
+ module Queueing
7
+ class SidekiqAdapter < BaseAdapter
8
+ class SubscriptionWorker
9
+ include Sidekiq::Worker
10
+
11
+ def perform(subscription, hook, context)
12
+ subscription = ActiveWebhook.subscription_model.find_by(id: subscription)
13
+ hook = Hook.from_h(hook.symbolize_keys) unless hook.nil?
14
+
15
+ ActiveWebhook.queueing_adapter.fulfill_subscription(
16
+ subscription: subscription,
17
+ hook: hook,
18
+ job_id: jid,
19
+ **context.symbolize_keys
20
+ )
21
+ end
22
+ end
23
+
24
+ class TopicWorker
25
+ include Sidekiq::Worker
26
+
27
+ def perform(key, version, context)
28
+ ActiveWebhook.queueing_adapter.new(key: key, version: version, **context.symbolize_keys).fulfill_topic
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ def promise_subscription(subscription:, hook:)
35
+ SubscriptionWorker.perform_async subscription.id, hook&.to_h, context
36
+ end
37
+
38
+ def promise_topic
39
+ TopicWorker.perform_async key, version, context
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Queueing
5
+ class SyncronousAdapter < BaseAdapter
6
+ def call
7
+ subscriptions.each do |subscription|
8
+ self.class.fulfill_subscription subscription: subscription, **context
9
+ end
10
+ subscriptions.count
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ class Subscription < ActiveRecord::Base
5
+ include ActiveWebhook::Models::SubscriptionAdditions
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ class Topic < ActiveRecord::Base
5
+ include ActiveWebhook::Models::TopicAdditions
6
+ end
7
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Verification
5
+ class BaseAdapter < Adapter
6
+ attribute :secret, :data
7
+
8
+ def call
9
+ return {} unless secret.present?
10
+
11
+ {
12
+ strategy => signature
13
+ }
14
+ end
15
+
16
+ protected
17
+
18
+ def self.component_name
19
+ "verification"
20
+ end
21
+
22
+ def signature
23
+ raise NotImplementedError, "#signature must be implemented."
24
+ end
25
+
26
+ def strategy
27
+ self.class.name.delete_suffix("Adapter")
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Verification
5
+ class Configuration
6
+ include ActiveWebhook::Configuration::Base
7
+
8
+ define_option :adapter,
9
+ values: %i[unsigned hmac_sha256],
10
+ allow_proc: true
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "base64"
5
+ require "openssl"
6
+ require "active_support/security_utils"
7
+
8
+ module ActiveWebhook
9
+ module Verification
10
+ class HMACSHA256Adapter < BaseAdapter
11
+ def signature
12
+ Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", secret, data))
13
+ end
14
+
15
+ def strategy
16
+ "Hmac-SHA256"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ module Verification
5
+ class UnsignedAdapter < BaseAdapter
6
+ def call
7
+ {}
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveWebhook
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module ActiveWebhook
6
+ module Generators
7
+ class InstallGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+ desc "Creates all the files needed to use Active Webhook"
10
+
11
+ def copy_config
12
+ template "active_webhook_config.rb", Rails.root.join('config','active_webhook.rb')
13
+ end
14
+
15
+ def run_other_generators
16
+ invoke "active_webhook:migrations"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module ActiveWebhook
6
+ module Generators
7
+ class MigrationsGenerator < ::Rails::Generators::Base
8
+ include ::Rails::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+ desc "Creates the migrations needed to use Active Webhook"
12
+
13
+ def self.next_migration_number(path)
14
+ next_migration_number = current_migration_number(path) + 1
15
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
16
+ end
17
+
18
+ def copy_migrations
19
+ migration_template "20210618023338_create_active_webhook_tables.rb",
20
+ Rails.root.join('db','migrate','create_active_webhook_tables.rb')
21
+ end
22
+ end
23
+ end
24
+ end