active_webhook 1.0.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/.env.sample +1 -0
- data/.gitignore +63 -0
- data/.rspec +4 -0
- data/.rubocop.yml +86 -0
- data/.todo +48 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +5 -0
- data/DEV_README.md +199 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +483 -0
- data/Rakefile +23 -0
- data/active_webhook.gemspec +75 -0
- data/config/environment.rb +33 -0
- data/lib/active_webhook.rb +125 -0
- data/lib/active_webhook/adapter.rb +117 -0
- data/lib/active_webhook/callbacks.rb +53 -0
- data/lib/active_webhook/configuration.rb +105 -0
- data/lib/active_webhook/delivery/base_adapter.rb +96 -0
- data/lib/active_webhook/delivery/configuration.rb +16 -0
- data/lib/active_webhook/delivery/faraday_adapter.rb +19 -0
- data/lib/active_webhook/delivery/net_http_adapter.rb +28 -0
- data/lib/active_webhook/error_log.rb +7 -0
- data/lib/active_webhook/formatting/base_adapter.rb +109 -0
- data/lib/active_webhook/formatting/configuration.rb +18 -0
- data/lib/active_webhook/formatting/json_adapter.rb +19 -0
- data/lib/active_webhook/formatting/url_encoded_adapter.rb +28 -0
- data/lib/active_webhook/hook.rb +9 -0
- data/lib/active_webhook/logger.rb +21 -0
- data/lib/active_webhook/models/configuration.rb +18 -0
- data/lib/active_webhook/models/error_log_additions.rb +15 -0
- data/lib/active_webhook/models/subscription_additions.rb +72 -0
- data/lib/active_webhook/models/topic_additions.rb +70 -0
- data/lib/active_webhook/queueing/active_job_adapter.rb +43 -0
- data/lib/active_webhook/queueing/base_adapter.rb +67 -0
- data/lib/active_webhook/queueing/configuration.rb +15 -0
- data/lib/active_webhook/queueing/delayed_job_adapter.rb +28 -0
- data/lib/active_webhook/queueing/sidekiq_adapter.rb +43 -0
- data/lib/active_webhook/queueing/syncronous_adapter.rb +14 -0
- data/lib/active_webhook/subscription.rb +7 -0
- data/lib/active_webhook/topic.rb +7 -0
- data/lib/active_webhook/verification/base_adapter.rb +31 -0
- data/lib/active_webhook/verification/configuration.rb +13 -0
- data/lib/active_webhook/verification/hmac_sha256_adapter.rb +20 -0
- data/lib/active_webhook/verification/unsigned_adapter.rb +11 -0
- data/lib/active_webhook/version.rb +5 -0
- data/lib/generators/install_generator.rb +20 -0
- data/lib/generators/migrations_generator.rb +24 -0
- data/lib/generators/templates/20210618023338_create_active_webhook_tables.rb +31 -0
- data/lib/generators/templates/active_webhook_config.rb +87 -0
- 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,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,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,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
|