github_bot 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/.github/ISSUE_TEMPLATE/bug_report.md +32 -0
- data/.github/ISSUE_TEMPLATE/config.yml +6 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +30 -0
- data/.github/pull_request_template.md +32 -0
- data/.github/workflows/cd.yml +53 -0
- data/.github/workflows/ci.yml +39 -0
- data/.gitignore +23 -0
- data/.rspec +4 -0
- data/.rubocop.yml +927 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +2 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +40 -0
- data/CONTRIBUTORS.md +3 -0
- data/Gemfile +22 -0
- data/LICENSE +205 -0
- data/NOTICE +13 -0
- data/README.md +66 -0
- data/Rakefile +8 -0
- data/app/controllers/github_bot/application_controller.rb +40 -0
- data/app/controllers/github_bot/concerns/response.rb +25 -0
- data/app/controllers/github_bot/webhooks/github_controller.rb +36 -0
- data/app/helpers/github_bot/github_request_helper.rb +123 -0
- data/bin/bundle-audit +29 -0
- data/bin/bundler-audit +29 -0
- data/bin/rails +26 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/config/routes.rb +13 -0
- data/github_bot.gemspec +36 -0
- data/lib/github_bot.rb +10 -0
- data/lib/github_bot/engine.rb +9 -0
- data/lib/github_bot/github/check_run.rb +56 -0
- data/lib/github_bot/github/client.rb +203 -0
- data/lib/github_bot/github/payload.rb +225 -0
- data/lib/github_bot/rails/railtie.rb +15 -0
- data/lib/github_bot/tasks/base.rb +42 -0
- data/lib/github_bot/validator/base.rb +232 -0
- data/lib/github_bot/version.rb +11 -0
- metadata +154 -0
data/lib/github_bot.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'github_bot', '**/*.rb'), &method(:require))
|
4
|
+
|
5
|
+
# Public: The github bot is utilized for assistance with webhook interactions provided by
|
6
|
+
# the incoming github events
|
7
|
+
module GithubBot
|
8
|
+
mattr_accessor :draw_routes_in_host_app
|
9
|
+
self.draw_routes_in_host_app = true
|
10
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GithubBot
|
4
|
+
module Github
|
5
|
+
# Public: Class to keep track of the check run that has been created for execution
|
6
|
+
class CheckRun
|
7
|
+
# The name/identifier of the check run
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# Public: Create a new instance of the CheckRun
|
11
|
+
#
|
12
|
+
# @params opts [Hash] A hash of options to utilized within the check run
|
13
|
+
# @option opts [:symbol] :name The name of the check run
|
14
|
+
# @option opts [:symbol] :repo The repository the checked run will be associated
|
15
|
+
# @option opts [:symbol] :sha The SHA commit for the check run to execute
|
16
|
+
# @option opts [:symbol] :client_api The GitHub API
|
17
|
+
def initialize(name:, repo:, sha:, client_api:, **opts)
|
18
|
+
@client_api = client_api
|
19
|
+
@repo = repo
|
20
|
+
@sha = sha
|
21
|
+
@name = name
|
22
|
+
@run = @client_api.create_check_run(
|
23
|
+
repo,
|
24
|
+
name,
|
25
|
+
sha,
|
26
|
+
opts.merge(
|
27
|
+
status: 'queued'
|
28
|
+
)
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Updates the check run to be in progress
|
33
|
+
def in_progress!(**options)
|
34
|
+
update(status: 'in_progress', started_at: Time.now, **options)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Public: Updates the check run to be complete
|
38
|
+
def complete!(**options)
|
39
|
+
update(status: 'completed', conclusion: 'success', completed_at: Time.now, **options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Updates the check run to require action
|
43
|
+
def action_required!(**options)
|
44
|
+
update(status: 'completed', conclusion: 'action_required', completed_at: Time.now, **options)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def update(**options)
|
50
|
+
options[:accept] = Octokit::Preview::PREVIEW_TYPES[:checks]
|
51
|
+
|
52
|
+
@client_api.update_check_run(@repo, @run.id, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'octokit'
|
4
|
+
require 'uri'
|
5
|
+
require_relative 'payload'
|
6
|
+
|
7
|
+
module GithubBot
|
8
|
+
module Github
|
9
|
+
# Public: The Client class manages the client interactions with GitHub
|
10
|
+
# such as file retrieval, pull request comments, and pull request checks
|
11
|
+
class Client
|
12
|
+
# include the payload attributes
|
13
|
+
include GithubBot::Github::Payload
|
14
|
+
FILE_REMOVED_STATUS = 'removed'
|
15
|
+
RAW_TYPE = 'application/vnd.github.v3.raw'
|
16
|
+
|
17
|
+
class << self
|
18
|
+
# Public: Initialize the singleton with the incoming request information
|
19
|
+
#
|
20
|
+
# @param request [Object] The incoming request payload
|
21
|
+
def initialize(request)
|
22
|
+
@instance = new(request)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Public: Returns the current instance of the Client
|
26
|
+
#
|
27
|
+
# @raise [StandardError] Raises error if instance has not been initialized before usage
|
28
|
+
def instance
|
29
|
+
raise StandardError, 'client not initialize' unless @instance
|
30
|
+
|
31
|
+
@instance
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Creates a new instance of the Client to manage the GitHub api transactions
|
36
|
+
#
|
37
|
+
# @param request [Object] The incoming request payload
|
38
|
+
def initialize(request)
|
39
|
+
@request = request
|
40
|
+
end
|
41
|
+
|
42
|
+
# Public: Retrieve the contents of a file
|
43
|
+
#
|
44
|
+
# @param file [Sawyer::Resource] The file for retrieving contents
|
45
|
+
def file_content(file)
|
46
|
+
raw_contents file
|
47
|
+
rescue Octokit::NotFound
|
48
|
+
''
|
49
|
+
rescue Octokit::Forbidden => e
|
50
|
+
if e.errors.any? && e.errors.first[:code] == 'too_large'
|
51
|
+
# revert to using the raw_url for getting the content
|
52
|
+
return URI.parse(file.raw_url).open.read
|
53
|
+
end
|
54
|
+
|
55
|
+
raise e
|
56
|
+
end
|
57
|
+
|
58
|
+
# Public: Return the modified files, excluding those that have been removed, from the pull request
|
59
|
+
#
|
60
|
+
# @return [Array<Sawyer::Resource>] A list of modified files impacted with the current pull request
|
61
|
+
def modified_files
|
62
|
+
files.reject do |github_file|
|
63
|
+
github_file.status == FILE_REMOVED_STATUS
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Public: Returns an array of all the files impacted with the current pull request
|
68
|
+
#
|
69
|
+
# @return [Array<Sawyer::Resource>] A list of all files impacted with the current pull request
|
70
|
+
def files
|
71
|
+
return [] if pull_request.nil?
|
72
|
+
|
73
|
+
@files ||= client.pull_request_files(repository_full_name, pull_request_number)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Public: Added a comment to the existing pull request
|
77
|
+
#
|
78
|
+
# @param opts [Hash] The parameter options for adding a comment
|
79
|
+
# @option opts [:symbol] :message The message to add to the pull request
|
80
|
+
def comment(message:, **opts)
|
81
|
+
client.add_comment(repository_full_name, pull_request_number, message, **opts)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Public: Creates a GitHub check run for execution
|
85
|
+
#
|
86
|
+
# @return [GithubBot::Github::CheckRun] The created check run
|
87
|
+
def create_check_run(name:, **opts)
|
88
|
+
CheckRun.new(
|
89
|
+
name: name,
|
90
|
+
repo: repository_full_name,
|
91
|
+
sha: head_sha,
|
92
|
+
client_api: client,
|
93
|
+
**opts
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Public: Returns a GitHub pull request object with the details of the current request
|
98
|
+
#
|
99
|
+
# @return [Sawyer::Resource] The pull request details associated to current request
|
100
|
+
def pull_request_details
|
101
|
+
@pull_request_details ||= client.pull_request(repository[:full_name], pull_request[:number])
|
102
|
+
end
|
103
|
+
|
104
|
+
# Public: Returns the current list of pull request reviewers
|
105
|
+
#
|
106
|
+
# @return [Array<Sawyer::Resource>] The list of current pull request reviewers
|
107
|
+
def pull_request_reviewers
|
108
|
+
@pull_request_reviewers ||= client.pull_request_reviews(
|
109
|
+
repository[:full_name],
|
110
|
+
pull_request[:number]
|
111
|
+
).sort_by(&:submitted_at)
|
112
|
+
end
|
113
|
+
|
114
|
+
# Public: Returns the current list of approving pull request reviewers
|
115
|
+
#
|
116
|
+
# @return [Array<Sawyer::Resource>] The list of approving pull request reviewers
|
117
|
+
def approving_reviewers
|
118
|
+
pull_request_reviewers.select { |r| r.state == 'APPROVED' }
|
119
|
+
end
|
120
|
+
|
121
|
+
# Public: Returns the current list of request comments
|
122
|
+
#
|
123
|
+
# @return [Array<Sawyer::Resource>] The list of pull request comments
|
124
|
+
def pull_request_comments
|
125
|
+
@pull_request_comments ||= client.issue_comments(
|
126
|
+
repository[:full_name],
|
127
|
+
pull_request[:number]
|
128
|
+
).sort_by(&:created_at)
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
# Decode the content retrieved from GitHub
|
134
|
+
#
|
135
|
+
# @param content [Sawyer::Resource] The content returned from GitHub to decode
|
136
|
+
# @return [String] Returns the Base64-decoded version of the content
|
137
|
+
def decode(content)
|
138
|
+
content&.content ? Base64.decode64(content.content).force_encoding('UTF-8') : ''
|
139
|
+
end
|
140
|
+
|
141
|
+
# Decode the contents of the file
|
142
|
+
def decode_contents(file)
|
143
|
+
decode(client.contents(repository_full_name, path: file.filename, ref: head_sha))
|
144
|
+
end
|
145
|
+
|
146
|
+
# Get raw contents from passed file
|
147
|
+
def raw_contents(file)
|
148
|
+
client.contents(repository_full_name, path: file.filename, ref: head_sha, headers: { 'Accept': RAW_TYPE })
|
149
|
+
end
|
150
|
+
|
151
|
+
# Returns an instance of the GitHub client API to utilize
|
152
|
+
def client
|
153
|
+
@client ||= Octokit::Client.new(access_token: installation_access_token, auto_paginate: true)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Gets an access token associated to the request
|
157
|
+
def installation_access_token
|
158
|
+
Octokit::Client.new(bearer_token: private_key).create_installation_access_token(installation_id)[:token]
|
159
|
+
end
|
160
|
+
|
161
|
+
# Gets the private key associated to the GitHub application
|
162
|
+
def private_key
|
163
|
+
private_pem =
|
164
|
+
if ENV['GITHUB_APP_PEM_PATH']
|
165
|
+
File.read(Rails.root.join(ENV['GITHUB_APP_PEM_PATH']))
|
166
|
+
elsif ENV['GITHUB_APP_PEM_V2']
|
167
|
+
ENV['GITHUB_APP_PEM_V2'].gsub('\\\n', "\n")
|
168
|
+
elsif ENV['GITHUB_APP_PEM']
|
169
|
+
ENV['GITHUB_APP_PEM'].gsub('\n', "\n")
|
170
|
+
else
|
171
|
+
raise StandardError, "'GITHUB_APP_PEM_PATH' or 'GITHUB_APP_PEM' needs to be set"
|
172
|
+
end
|
173
|
+
|
174
|
+
private_key = OpenSSL::PKey::RSA.new(private_pem)
|
175
|
+
|
176
|
+
current = Time.current.to_i
|
177
|
+
|
178
|
+
payload = {
|
179
|
+
iat: current,
|
180
|
+
exp: current + (10 * 60),
|
181
|
+
iss: ENV['GITHUB_APP_IDENTIFIER']
|
182
|
+
}
|
183
|
+
JWT.encode(payload, private_key, 'RS256')
|
184
|
+
end
|
185
|
+
|
186
|
+
# relay messages to Octokit::Client if responds to allow extension
|
187
|
+
# of the client and extend/overwrite those concerned with
|
188
|
+
def method_missing(method, *args, &block)
|
189
|
+
return super unless respond_to_missing?(method)
|
190
|
+
|
191
|
+
return payload[method] if payload.key?(method)
|
192
|
+
|
193
|
+
client.send(method, *args, &block)
|
194
|
+
end
|
195
|
+
|
196
|
+
# because tasks are executed by a validator, some methods are relayed across
|
197
|
+
# from the task back to the validator. this override checks for that existence
|
198
|
+
def respond_to_missing?(method, *args)
|
199
|
+
payload.key?(method) || client.respond_to?(method, *args)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open-uri'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
module GithubBot
|
8
|
+
module Github
|
9
|
+
# The GitHub webhook payload information
|
10
|
+
# @see https://developer.github.com/webhooks/event-payloads/#webhook-payload-object-common-properties
|
11
|
+
module Payload
|
12
|
+
# Public: Returns the installation identifier associated to the event
|
13
|
+
#
|
14
|
+
# @return [Integer] identifier of the GitHub App installation
|
15
|
+
def installation_id
|
16
|
+
payload[:installation][:id]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Public: Return <true> if the payload event type is of type 'pull_request'; otherwise, <false>
|
20
|
+
def pull_request?
|
21
|
+
payload_type == 'pull_request'
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Return <true> if the payload event type is of type 'check_run'; otherwise, <false>
|
25
|
+
def check_run?
|
26
|
+
payload_type == 'check_run'
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Return <true> if the payload event type is of type 'review_requested'; otherwise, <false>
|
30
|
+
def review_requested?
|
31
|
+
payload_type == 'review_requested'
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Return <true> if the payload event type is of type 'review_request_removed'; otherwise, <false>
|
35
|
+
def review_request_removed?
|
36
|
+
payload_type == 'review_request_removed'
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Return <true> if the action type is of type 'issue_comment'; otherwise, <false>
|
40
|
+
# This is used for all other comment triggered issue_comment events
|
41
|
+
def issue_comment?
|
42
|
+
payload_type == 'issue_comment'
|
43
|
+
end
|
44
|
+
|
45
|
+
# Public: Return <true> if the payload event type is of type 'labeled'; otherwise, <false>
|
46
|
+
def labeled?
|
47
|
+
payload_type == 'labeled'
|
48
|
+
end
|
49
|
+
|
50
|
+
# Public: Return <true> if the payload event type is of type 'unlabeled'; otherwise, <false>
|
51
|
+
def unlabeled?
|
52
|
+
payload_type == 'unlabeled'
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Return the activity related to the pull request
|
56
|
+
#
|
57
|
+
# @return [Hash] The activity related to the pull request
|
58
|
+
def pull_request
|
59
|
+
if pull_request?
|
60
|
+
payload[:pull_request]
|
61
|
+
elsif check_run?
|
62
|
+
payload[:check_run][:pull_requests].first
|
63
|
+
elsif issue_comment?
|
64
|
+
payload[:issue]
|
65
|
+
else
|
66
|
+
payload.key?(:pull_request) ? payload[:pull_request] : {}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Public: Returns <true> if the sender is of type 'Bot'; otherwise, <false>
|
71
|
+
def sender_type_bot?
|
72
|
+
payload[:sender][:type].downcase == 'bot' # rubocop:disable Performance/Casecmp
|
73
|
+
end
|
74
|
+
|
75
|
+
# Public: Returns the pull request number from the payload
|
76
|
+
#
|
77
|
+
# @return [Integer] The pull request number from the payload
|
78
|
+
def pull_request_number
|
79
|
+
pull_request[:number]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Public: Returns the SHA of the most recent commit for this pull request
|
83
|
+
#
|
84
|
+
# @return [String] The SHA of the most recent commit for this pull request
|
85
|
+
def head_sha
|
86
|
+
if issue_comment?
|
87
|
+
'HEAD'
|
88
|
+
else
|
89
|
+
pull_request[:head][:sha]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Public: Returns the head branch that the changes are on
|
94
|
+
#
|
95
|
+
# @return [String] The head branch that the changes are on
|
96
|
+
def head_branch
|
97
|
+
pull_request[:head][:ref]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Public: Returns the base branch that the head was based on
|
101
|
+
#
|
102
|
+
# @return [String] The base branch that the head was based on
|
103
|
+
def base_branch
|
104
|
+
pull_request[:base][:ref]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Public: Returns the pull request body content
|
108
|
+
#
|
109
|
+
# @return [String] The pull request body content
|
110
|
+
def pull_request_body
|
111
|
+
pull_request[:body]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Public: Returns the name of the repository where the event was triggered
|
115
|
+
#
|
116
|
+
# @return [String] The name of the repository where the event was triggered
|
117
|
+
def repository_name
|
118
|
+
repository[:name]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Public: Returns the organization and repository name
|
122
|
+
#
|
123
|
+
# @return [String] The organization and repository name
|
124
|
+
def repository_full_name
|
125
|
+
repository[:full_name]
|
126
|
+
end
|
127
|
+
|
128
|
+
# Public: Returns the repository URL utilized for performing a 'git clone'
|
129
|
+
#
|
130
|
+
# @return [String] The repository URL utilized for performing a 'git clone'
|
131
|
+
def repository_clone_url
|
132
|
+
repository[:clone_url]
|
133
|
+
end
|
134
|
+
|
135
|
+
# Public: Returns the repository fork URL from the original project with the most
|
136
|
+
# recent updated forked instance first
|
137
|
+
#
|
138
|
+
# @return [Array] The array of [String] URLs associated to the forked repositories
|
139
|
+
def repository_fork_urls
|
140
|
+
return @repository_fork_urls if @repository_fork_urls
|
141
|
+
|
142
|
+
@repository_fork_urls =
|
143
|
+
[].tap do |ar|
|
144
|
+
json = URI.parse(repository[:forks_url]).open.read
|
145
|
+
JSON.parse(json).sort_by { |i| Date.parse i['updated_at'] }.reverse_each do |fork|
|
146
|
+
ar << fork['clone_url']
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Public: Returns the repository default branch
|
152
|
+
#
|
153
|
+
# @return [String] The repository default branch
|
154
|
+
def repository_default_branch
|
155
|
+
repository[:default_branch]
|
156
|
+
end
|
157
|
+
|
158
|
+
# Public: Returns a list of class object names to create for validation
|
159
|
+
#
|
160
|
+
# @return [Array<String>] A list of strings associated to the class object validators
|
161
|
+
def repository_pull_request_bots
|
162
|
+
return @repository_pull_request_bots if @repository_pull_request_bots
|
163
|
+
|
164
|
+
file = raw_file_url('.github-bots')
|
165
|
+
@repository_pull_request_bots = [].tap do |ar|
|
166
|
+
if file
|
167
|
+
resp = YAML.safe_load(URI.parse(file).open.read)
|
168
|
+
ar << resp['pull_request'] if resp['pull_request']
|
169
|
+
end
|
170
|
+
end.flatten
|
171
|
+
rescue SyntaxError => e
|
172
|
+
Rails.logger.error message: "Error parsing file '#{file}'", exception: e
|
173
|
+
|
174
|
+
# Allow continuation of process just won't utilize any bot validations until
|
175
|
+
# parsing error of file is corrected
|
176
|
+
{}
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def method_missing(method_name, *args)
|
182
|
+
payload[method_name] || super
|
183
|
+
end
|
184
|
+
|
185
|
+
def respond_to_missing?(method, *args)
|
186
|
+
payload.key?(method) || super
|
187
|
+
end
|
188
|
+
|
189
|
+
def raw_file_url(path)
|
190
|
+
# https://github.com/api/v3/repos/poloka/foo-config/contents/.github-bot?ref=
|
191
|
+
content_file = File.join(repository_contents_url, "#{path}?ref=#{head_sha}")
|
192
|
+
JSON.parse(URI.parse(content_file).open(&:read))['download_url']
|
193
|
+
rescue ::OpenURI::HTTPError => e
|
194
|
+
raise StandardError, "file '#{content_file}' not found" unless /404 Not Found/i.match?(e.message)
|
195
|
+
end
|
196
|
+
|
197
|
+
def repository_contents_url
|
198
|
+
# drop the last 'path' portion of the API endpoint
|
199
|
+
# https://github.com/api/v3/repos/poloka/foo-config/contents/{+path}
|
200
|
+
repository[:contents_url].split('/')[0...-1].join('/')
|
201
|
+
end
|
202
|
+
|
203
|
+
def payload
|
204
|
+
@payload ||= verify_webhook_signature(payload_body, @request.env['HTTP_X_HUB_SIGNATURE'])
|
205
|
+
end
|
206
|
+
|
207
|
+
def payload_body
|
208
|
+
@payload_body ||= @request.body.read
|
209
|
+
end
|
210
|
+
|
211
|
+
def payload_type
|
212
|
+
@request.env['HTTP_X_GITHUB_EVENT'] if @request
|
213
|
+
end
|
214
|
+
|
215
|
+
def verify_webhook_signature(payload, payload_signature)
|
216
|
+
signature = 'sha1=' + OpenSSL::HMAC.hexdigest(
|
217
|
+
OpenSSL::Digest.new('sha1'),
|
218
|
+
ENV['GITHUB_WEBHOOK_SECRET'],
|
219
|
+
payload
|
220
|
+
)
|
221
|
+
Rack::Utils.secure_compare(signature, payload_signature) ? JSON.parse(payload).with_indifferent_access : false
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|