codebot 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE.md +32 -0
  3. data/.github/ISSUE_TEMPLATE/formatter_issue.md +20 -0
  4. data/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  5. data/.gitignore +10 -0
  6. data/.rspec +1 -0
  7. data/.rubocop.yml +11 -0
  8. data/.travis.yml +26 -0
  9. data/CODE_OF_CONDUCT.md +46 -0
  10. data/CONTRIBUTING.md +15 -0
  11. data/Gemfile +4 -0
  12. data/Gemfile.lock +75 -0
  13. data/LICENSE +21 -0
  14. data/README.md +230 -0
  15. data/Rakefile +29 -0
  16. data/bin/console +8 -0
  17. data/codebot.gemspec +49 -0
  18. data/exe/codebot +7 -0
  19. data/lib/codebot.rb +8 -0
  20. data/lib/codebot/channel.rb +134 -0
  21. data/lib/codebot/command_error.rb +17 -0
  22. data/lib/codebot/config.rb +125 -0
  23. data/lib/codebot/configuration_error.rb +17 -0
  24. data/lib/codebot/core.rb +76 -0
  25. data/lib/codebot/cryptography.rb +38 -0
  26. data/lib/codebot/event.rb +62 -0
  27. data/lib/codebot/ext/cinch/ssl_extensions.rb +37 -0
  28. data/lib/codebot/formatter.rb +242 -0
  29. data/lib/codebot/formatters.rb +109 -0
  30. data/lib/codebot/formatters/.rubocop.yml +2 -0
  31. data/lib/codebot/formatters/commit_comment.rb +43 -0
  32. data/lib/codebot/formatters/fork.rb +40 -0
  33. data/lib/codebot/formatters/gitlab_issue_hook.rb +56 -0
  34. data/lib/codebot/formatters/gitlab_job_hook.rb +77 -0
  35. data/lib/codebot/formatters/gitlab_merge_request_hook.rb +57 -0
  36. data/lib/codebot/formatters/gitlab_note_hook.rb +119 -0
  37. data/lib/codebot/formatters/gitlab_pipeline_hook.rb +51 -0
  38. data/lib/codebot/formatters/gitlab_push_hook.rb +83 -0
  39. data/lib/codebot/formatters/gitlab_wiki_page_hook.rb +56 -0
  40. data/lib/codebot/formatters/gollum.rb +67 -0
  41. data/lib/codebot/formatters/issue_comment.rb +41 -0
  42. data/lib/codebot/formatters/issues.rb +41 -0
  43. data/lib/codebot/formatters/ping.rb +79 -0
  44. data/lib/codebot/formatters/public.rb +30 -0
  45. data/lib/codebot/formatters/pull_request.rb +71 -0
  46. data/lib/codebot/formatters/pull_request_review_comment.rb +49 -0
  47. data/lib/codebot/formatters/push.rb +172 -0
  48. data/lib/codebot/formatters/watch.rb +38 -0
  49. data/lib/codebot/integration.rb +195 -0
  50. data/lib/codebot/integration_manager.rb +225 -0
  51. data/lib/codebot/ipc_client.rb +83 -0
  52. data/lib/codebot/ipc_server.rb +79 -0
  53. data/lib/codebot/irc_client.rb +102 -0
  54. data/lib/codebot/irc_connection.rb +156 -0
  55. data/lib/codebot/message.rb +37 -0
  56. data/lib/codebot/metadata.rb +15 -0
  57. data/lib/codebot/network.rb +240 -0
  58. data/lib/codebot/network_manager.rb +181 -0
  59. data/lib/codebot/options.rb +49 -0
  60. data/lib/codebot/options/base.rb +55 -0
  61. data/lib/codebot/options/core.rb +126 -0
  62. data/lib/codebot/options/integration.rb +101 -0
  63. data/lib/codebot/options/network.rb +109 -0
  64. data/lib/codebot/payload.rb +32 -0
  65. data/lib/codebot/request.rb +51 -0
  66. data/lib/codebot/sanitizers.rb +130 -0
  67. data/lib/codebot/serializable.rb +101 -0
  68. data/lib/codebot/shortener.rb +43 -0
  69. data/lib/codebot/thread_controller.rb +70 -0
  70. data/lib/codebot/user_error.rb +13 -0
  71. data/lib/codebot/validation_error.rb +17 -0
  72. data/lib/codebot/web_listener.rb +107 -0
  73. data/lib/codebot/web_server.rb +58 -0
  74. data/webhook.png +0 -0
  75. metadata +249 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'securerandom'
5
+
6
+ module Codebot
7
+ # This module provides convenience methods for performing cryptographic
8
+ # operations.
9
+ module Cryptography
10
+ # Generates a random name for an integration endpoint.
11
+ #
12
+ # @return [String] the generated name
13
+ def self.generate_endpoint
14
+ SecureRandom.uuid
15
+ end
16
+
17
+ # Generates a random webhook secret.
18
+ #
19
+ # @return [String] the generated secret
20
+ def self.generate_secret(len = nil)
21
+ SecureRandom.base64(len || 32)
22
+ end
23
+
24
+ # Verifies a webhook signature.
25
+ #
26
+ # @param body [String] the webhook body
27
+ # @param secret [String] the correct secret
28
+ # @param signature [String] the signature to verify
29
+ # @return [Boolean] whether the signature is correct
30
+ def self.valid_signature?(body, secret, signature)
31
+ return false if signature.nil?
32
+
33
+ digest = OpenSSL::Digest.new 'sha1'
34
+ good_signature = 'sha1=' + OpenSSL::HMAC.hexdigest(digest, secret, body)
35
+ Rack::Utils.secure_compare good_signature, signature
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codebot
4
+ # This module provides methods for processing webhook events.
5
+ module Event
6
+ # The currently supported events.
7
+ VALID_SYMBOLS = %i[
8
+ commit_comment
9
+ fork
10
+ gollum
11
+ issue_comment
12
+ issues
13
+ ping
14
+ public
15
+ pull_request
16
+ pull_request_review_comment
17
+ push
18
+ watch
19
+
20
+ gitlab_push_hook
21
+ gitlab_tag_push_hook
22
+ gitlab_job_hook
23
+ gitlab_build_hook
24
+ gitlab_pipeline_hook
25
+ gitlab_issue_hook
26
+ gitlab_note_hook
27
+ gitlab_merge_request_hook
28
+ gitlab_wiki_page_hook
29
+ ].freeze
30
+ # TODO: Support for create
31
+ # TODO: Support for delete
32
+ # TODO: Support for deployment
33
+ # TODO: Support for deployment_status
34
+ # TODO: Support for installation
35
+ # TODO: Support for installation_repositories
36
+ # TODO: Support for label
37
+ # TODO: Support for marketplace_purchase
38
+ # TODO: Support for member
39
+ # TODO: Support for membership
40
+ # TODO: Support for milestone
41
+ # TODO: Support for organization
42
+ # TODO: Support for org_block
43
+ # TODO: Support for page_build
44
+ # TODO: Support for project_card
45
+ # TODO: Support for project_column
46
+ # TODO: Support for project
47
+ # TODO: Support for pull_request_review
48
+ # TODO: Support for repository
49
+ # TODO: Support for release
50
+ # TODO: Support for status
51
+ # TODO: Support for team
52
+ # TODO: Support for team_add
53
+
54
+ # Converts a webhook event to its corresponding symbol.
55
+ #
56
+ # @param str [String] the event string
57
+ # @return [Symbol, nil] the symbol, or +nil+ if the event is not supported
58
+ def self.symbolize(str)
59
+ VALID_SYMBOLS.find { |sym| sym.to_s.casecmp(str).zero? }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cinch'
4
+
5
+ module Codebot
6
+ # Extensions
7
+ module Ext
8
+ # Cinch extensions
9
+ module Cinch
10
+ # Cinch SSL extensions
11
+ module SSLExtensions
12
+ # Patch the OpenSSL::SSL::SSLContext#ca_path= method to set cert_store
13
+ # to the default certificate store, which Cinch does not currently do.
14
+ #
15
+ # @param path [String] the path to the CA certificate directory
16
+ def ca_path=(path)
17
+ if caller(1..1).first.include?('/lib/cinch/')
18
+ puts 'Codebot: patching Cinch to use the default certificate store'
19
+ self.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
20
+ end
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ # Patch module OpenSSL
29
+ module OpenSSL
30
+ # Patch module OpenSSL::SSL
31
+ module SSL
32
+ # Patch class OpenSSL::SSL::SSLContext
33
+ class SSLContext
34
+ prepend Codebot::Ext::Cinch::SSLExtensions
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+
6
+ module Codebot
7
+ # This class formats events.
8
+ class Formatter # rubocop:disable Metrics/ClassLength
9
+ # @return [Object] the JSON payload object
10
+ attr_reader :payload
11
+
12
+ # Initializes a new formatter.
13
+ #
14
+ # @param payload [Object] the JSON payload object
15
+ def initialize(payload, shortener)
16
+ @payload = payload
17
+ @shortener = shortener
18
+ end
19
+
20
+ # Formats IRC messages for an unknown event.
21
+ #
22
+ # @return [Array<String>] the formatted messages
23
+ def format
24
+ ['An unknown event occurred']
25
+ end
26
+
27
+ # Shortens the summary URL. If this method is used, the child class must
28
+ # implement the +#summary_url+ method.
29
+ #
30
+ # @return [String] the shortened summary URL
31
+ def url
32
+ shorten_url summary_url
33
+ end
34
+
35
+ def gitlab_url
36
+ summary_url
37
+ end
38
+
39
+ # Formats a repository name.
40
+ #
41
+ # @param repository [String] the name
42
+ # @return [String] the formatted name
43
+ def format_repository(repository)
44
+ ::Cinch::Formatting.format(:pink, repository.to_s)
45
+ end
46
+
47
+ # Formats a branch name.
48
+ #
49
+ # @param branch [String] the name
50
+ # @return [String] the formatted name
51
+ def format_branch(branch)
52
+ ::Cinch::Formatting.format(:purple, branch.to_s)
53
+ end
54
+
55
+ # Formats a commit hash.
56
+ #
57
+ # @param hash [String] the hash
58
+ # @return [String] the formatted hash
59
+ def format_hash(hash)
60
+ ::Cinch::Formatting.format(:grey, hash.to_s[0..6])
61
+ end
62
+
63
+ # Formats a user name.
64
+ #
65
+ # @param user [String] the name
66
+ # @return [String] the formatted name
67
+ def format_user(user)
68
+ ::Cinch::Formatting.format(:silver, user.to_s)
69
+ end
70
+
71
+ # Formats a URL.
72
+ #
73
+ # @param url [String] the URL
74
+ # @return [String] the formatted URL
75
+ def format_url(url)
76
+ ::Cinch::Formatting.format(:blue, :underline, url.to_s)
77
+ end
78
+
79
+ # Formats a number.
80
+ #
81
+ # @param num [Integer] the number
82
+ # @param singular [String, nil] the singular noun to append to the number
83
+ # @param plural [String, nil] the plural noun to append to the number
84
+ # @return [String] the formatted number
85
+ def format_number(num, singular = nil, plural = nil)
86
+ bold_num = ::Cinch::Formatting.format(:bold, num.to_s)
87
+ (bold_num + ' ' + (num == 1 ? singular : plural).to_s).strip
88
+ end
89
+
90
+ # Formats the name of a potentially dangerous operation, such as a deletion
91
+ # or force-push.
92
+ #
93
+ # @param text [String] the text to format
94
+ # @return [String] the formatted text
95
+ def format_dangerous(text)
96
+ ::Cinch::Formatting.format(:red, text.to_s)
97
+ end
98
+
99
+ # Formats the name of a webhook event.
100
+ #
101
+ # @param name [String] the name to format
102
+ # @return [String] the formatted name
103
+ def format_event(name)
104
+ ::Cinch::Formatting.format(:bold, name.to_s)
105
+ end
106
+
107
+ # Constructs a sentence from array elements, connecting them with commas
108
+ # and conjunctions.
109
+ #
110
+ # @param ary [Array<String>] the array
111
+ # @param empty_sentence [String, nil] the sentence to return if the array
112
+ # is empty
113
+ # @return [String] the constructed sentence
114
+ def ary_to_sentence(ary, empty_sentence = nil)
115
+ case ary.length
116
+ when 0 then empty_sentence.to_s
117
+ when 1 then ary.first
118
+ when 2 then ary.join(' and ')
119
+ else
120
+ *ary, last_element = ary
121
+ ary_to_sentence([ary.join(', '), last_element])
122
+ end
123
+ end
124
+
125
+ # Sanitize the given text for delivery to an IRC channel. Most importantly,
126
+ # this method prevents attackers from injecting arbitrary commands into the
127
+ # bot's connection by ensuring that the text does not contain any newline
128
+ # characters. Any IRC formatting codes in the text are also removed.
129
+ #
130
+ # @param text [String] the text to sanitize
131
+ # @return [String] the sanitized text
132
+ def sanitize(text)
133
+ ::Cinch::Formatting.unformat(text.to_s.gsub(/[[:space:]]+/, ' ')).strip
134
+ end
135
+
136
+ # Truncates the given text, appending a suffix if it was above the allowed
137
+ # length.
138
+ #
139
+ # @param text [String] the text to truncate
140
+ # @param suffix [String] the suffix to append if the text is truncated
141
+ # @param length [Integer] the maximum length including the suffix
142
+ # @yield [String] the truncated string before the ellipsis is appended
143
+ # @return short [String] the abbreviated text
144
+ def abbreviate(text, suffix: ' ...', length: 200)
145
+ content_length = length - suffix.length
146
+ short = text.to_s.lines.first.to_s.strip[0...content_length].strip
147
+ yield text if block_given?
148
+ short << suffix unless short.eql? text.to_s.strip
149
+ short
150
+ end
151
+
152
+ # Abbreviates the given text, removes any trailing punctuation except for
153
+ # the ellipsis appended if the text was truncated, and sanitizes the text
154
+ # for delivery to IRC.
155
+ #
156
+ # @param text [String] the text to process
157
+ # @return [String] the processed text
158
+ def prettify(text)
159
+ pretty = abbreviate(text) { |short| short.sub!(/[[:punct:]]+\z/, '') }
160
+ sanitize pretty
161
+ end
162
+
163
+ # Extracts the repository name from the payload.
164
+ #
165
+ # @return [String, nil] the repository name
166
+ def repository_name
167
+ extract(:repository, :name)
168
+ end
169
+
170
+ # Extracts the repository URL from the payload.
171
+ #
172
+ # @return [String, nil] the repository URL
173
+ def repository_url
174
+ extract(:repository, :html_url)
175
+ end
176
+
177
+ def gitlab_repository_url
178
+ extract(:repository, :homepage)
179
+ end
180
+
181
+ # Extracts the action from the payload.
182
+ #
183
+ # @return [String, nil] the action
184
+ def action
185
+ extract(:action).to_s
186
+ end
187
+
188
+ def gitlab_action
189
+ extract(:object_attributes, :action).to_s
190
+ end
191
+
192
+ # Checks whether the action is 'opened'.
193
+ #
194
+ # @return [Boolean] whether the action is 'opened'.
195
+ def opened?
196
+ action.eql? 'opened'
197
+ end
198
+
199
+ # Checks whether the action is 'closed'.
200
+ #
201
+ # @return [Boolean] whether the action is 'closed'.
202
+ def closed?
203
+ action.eql? 'closed'
204
+ end
205
+
206
+ def gitlab_opened?
207
+ gitlab_action.eql? 'open'
208
+ end
209
+
210
+ # Checks whether the action is 'closed'.
211
+ #
212
+ # @return [Boolean] whether the action is 'closed'.
213
+ def gitlab_closed?
214
+ gitlab_action.eql? 'close'
215
+ end
216
+
217
+ # Extracts the user name of the person who triggered this event.
218
+ #
219
+ # @return [String, nil] the user name
220
+ def sender_name
221
+ extract(:sender, :login)
222
+ end
223
+
224
+ # Safely extracts a value from a JSON object.
225
+ #
226
+ # @param path [Array<#to_s>] the path to traverse
227
+ # @return [Object, nil] the extracted object or +nil+ if no object was
228
+ # found at the given path
229
+ def extract(*path)
230
+ node = payload
231
+ node if path.all? do |sub|
232
+ break unless node.is_a? Hash
233
+
234
+ node = node[sub.to_s]
235
+ end
236
+ end
237
+
238
+ def shorten_url(url)
239
+ @shortener.shorten_url(url)
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cinch'
4
+ require 'codebot/formatter'
5
+ require 'codebot/formatters/commit_comment'
6
+ require 'codebot/formatters/fork'
7
+ require 'codebot/formatters/gollum'
8
+ require 'codebot/formatters/issue_comment'
9
+ require 'codebot/formatters/issues'
10
+ require 'codebot/formatters/ping'
11
+ require 'codebot/formatters/public'
12
+ require 'codebot/formatters/pull_request'
13
+ require 'codebot/formatters/pull_request_review_comment'
14
+ require 'codebot/formatters/push'
15
+ require 'codebot/formatters/watch'
16
+ require 'codebot/formatters/gitlab_push_hook'
17
+ require 'codebot/formatters/gitlab_issue_hook'
18
+ require 'codebot/formatters/gitlab_job_hook'
19
+ require 'codebot/formatters/gitlab_pipeline_hook'
20
+ require 'codebot/formatters/gitlab_note_hook'
21
+ require 'codebot/formatters/gitlab_merge_request_hook'
22
+ require 'codebot/formatters/gitlab_wiki_page_hook'
23
+ require 'codebot/shortener'
24
+
25
+ module Codebot
26
+ # This module provides methods for formatting outgoing IRC messages.
27
+ module Formatters
28
+ # Formats IRC messages for an event.
29
+ #
30
+ # @param event [Symbol] the webhook event
31
+ # @param payload [Object] the JSON payload object
32
+ # @param color [Boolean] whether to use formatting codes
33
+ # @return [Array<String>] the formatted messages
34
+ def self.format(event, payload, integration, color = true)
35
+ messages = format_color(event, payload, integration).to_a
36
+ messages.map! { |msg| "\x0F" + msg }
37
+ messages.map! { |msg| ::Cinch::Formatting.unformat(msg) } unless color
38
+ messages
39
+ rescue StandardError => e
40
+ STDERR.puts e.message
41
+ STDERR.puts e.backtrace
42
+ url = ::Cinch::Formatting.format(:blue, :underline, FORMATTER_ISSUE_URL)
43
+ ['An error occurred while formatting this message. More information ' \
44
+ "has been printed to STDERR. Please report this issue to #{url}."]
45
+ end
46
+
47
+ def self.shortener(inte)
48
+ Shortener::Custom.new(inte.shortener_url, inte.shortener_secret)
49
+ end
50
+
51
+ def self.create_formatter(event, payload, integration) # rubocop:disable Metrics/LineLength, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
52
+ case event
53
+ when :commit_comment
54
+ Formatters::CommitComment.new(payload, Shortener::Github.new)
55
+ when :fork
56
+ Formatters::Fork.new(payload, Shortener::Github.new)
57
+ when :gollum
58
+ Formatters::Gollum.new(payload, Shortener::Github.new)
59
+ when :issue_comment
60
+ Formatters::IssueComment.new(payload, Shortener::Github.new)
61
+ when :issues
62
+ Formatters::Issues.new(payload, Shortener::Github.new)
63
+ when :ping
64
+ Formatters::Ping.new(payload, Shortener::Github.new)
65
+ when :public
66
+ Formatters::Public.new(payload, Shortener::Github.new)
67
+ when :pull_request
68
+ Formatters::PullRequest.new(payload, Shortener::Github.new)
69
+ when :pull_request_review_comment
70
+ Formatters::PullRequestReviewComment.new(payload, Shortener::Github.new)
71
+ when :push
72
+ Formatters::Push.new(payload, Shortener::Github.new)
73
+ when :watch
74
+ Formatters::Watch.new(payload, Shortener::Github.new)
75
+ when :gitlab_push_hook
76
+ Formatters::Gitlab::PushHook.new(payload, shortener(integration))
77
+ when :gitlab_tag_push_hook
78
+ Formatters::Gitlab::PushHook.new(payload, shortener(integration))
79
+ when :gitlab_job_hook
80
+ Formatters::Gitlab::JobHook.new(payload, shortener(integration))
81
+ when :gitlab_build_hook
82
+ Formatters::Gitlab::JobHook.new(payload, shortener(integration))
83
+ when :gitlab_pipeline_hook
84
+ Formatters::Gitlab::PipelineHook.new(payload, shortener(integration))
85
+ when :gitlab_issue_hook
86
+ Formatters::Gitlab::IssueHook.new(payload, shortener(integration))
87
+ when :gitlab_note_hook
88
+ Formatters::Gitlab::NoteHook.new(payload, shortener(integration))
89
+ when :gitlab_merge_request_hook
90
+ Formatters::Gitlab::MergeRequestHook.new(payload,
91
+ shortener(integration))
92
+ when :gitlab_wiki_page_hook
93
+ Formatters::Gitlab::WikiPageHook.new(payload,
94
+ shortener(integration))
95
+ else "Error: missing formatter for #{event.inspect}"
96
+ end
97
+ end
98
+
99
+ # Formats colored IRC messages. This method should not be called directly
100
+ # from outside this module.
101
+ #
102
+ # @param event [Symbol] the webhook event
103
+ # @param payload [Object] the JSON payload object
104
+ # @return [Array<String>] the formatted messages
105
+ def self.format_color(event, payload, integration) # rubocop:disable Metrics/LineLength
106
+ create_formatter(event, payload, integration).format
107
+ end
108
+ end
109
+ end