active_webhook 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|