codebot 1.2.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.
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