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,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Portions (c) 2008 Logical Awesome, LLC (released under the MIT license).
4
+ # See the LICENSE file for the full MIT license text.
5
+
6
+ module Codebot
7
+ module Formatters
8
+ # This class formats pull_request_review_comment events.
9
+ class PullRequestReviewComment < Formatter
10
+ # Formats IRC messages for a pull_request_review_comment event.
11
+ #
12
+ # @return [Array<String>] the formatted messages
13
+ def format
14
+ ["#{summary}: #{format_url url}"]
15
+ end
16
+
17
+ def summary
18
+ default_format % {
19
+ repository: format_repository(repository_name),
20
+ sender: format_user(sender_name),
21
+ number: pull_number,
22
+ hash: format_hash(commit_id),
23
+ short: prettify(comment_body)
24
+ }
25
+ end
26
+
27
+ def default_format
28
+ '[%<repository>s] %<sender>s commented on pull request #%<number>s ' \
29
+ '%<hash>s: %<summary>s'
30
+ end
31
+
32
+ def summary_url
33
+ extract(:comment, :html_url).to_s
34
+ end
35
+
36
+ def comment_body
37
+ extract(:comment, :body)
38
+ end
39
+
40
+ def commit_id
41
+ extract(:comment, :commit_id)
42
+ end
43
+
44
+ def pull_number
45
+ extract(:pull_request, :number)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: false
2
+
3
+ # Portions (c) 2008 Logical Awesome, LLC (released under the MIT license).
4
+ # See the LICENSE file for the full MIT license text.
5
+
6
+ module Codebot
7
+ module Formatters
8
+ # This class formats push events.
9
+ class Push < Formatter # rubocop:disable Metrics/ClassLength
10
+ # Formats IRC messages for a push event.
11
+ #
12
+ # @return [Array<String>] the formatted messages
13
+ def format
14
+ ["#{summary}: #{format_url url}"] + distinct_commits.map do |commit|
15
+ format_commit_message(commit)
16
+ end
17
+ end
18
+
19
+ def summary # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/LineLength
20
+ msg = "[#{format_repository repository_name}]"
21
+ msg << " #{format_user(pusher_name)}"
22
+
23
+ if created?
24
+ if tag?
25
+ msg << " tagged #{format_branch tag_name} at "
26
+ msg << if base_ref
27
+ format_branch(base_ref_name)
28
+ else
29
+ format_hash(after_sha)
30
+ end
31
+ else
32
+ msg << " created #{format_branch branch_name}"
33
+ msg << if base_ref
34
+ " from #{format_branch base_ref_name}"
35
+ else
36
+ " at #{format_hash after_sha}"
37
+ end
38
+
39
+ len = distinct_commits.length
40
+ msg << " (+#{format_number len, 'new commit', 'new commits'})"
41
+ end
42
+ elsif deleted?
43
+ msg << " #{format_dangerous 'deleted'}"
44
+ msg << " #{format_branch branch_name}"
45
+ msg << " at #{format_hash before_sha}"
46
+ elsif forced?
47
+ msg << " #{format_dangerous 'force-pushed'}"
48
+ msg << " #{format_branch branch_name}"
49
+ msg << " from #{format_hash before_sha}"
50
+ msg << " to #{format_hash after_sha}"
51
+ elsif !commits.empty? && distinct_commits.empty?
52
+ if base_ref
53
+ msg << " merged #{format_branch base_ref_name}"
54
+ msg << " into #{format_branch branch_name}"
55
+ else
56
+ msg << " fast-forwarded #{format_branch branch_name}"
57
+ msg << " from #{format_hash before_sha}"
58
+ msg << " to #{format_hash after_sha}"
59
+ end
60
+ else
61
+ len = distinct_commits.length
62
+ msg << " pushed #{format_number len, 'new commit', 'new commits'}"
63
+ msg << " to #{format_branch branch_name}"
64
+ end
65
+ msg
66
+ end
67
+
68
+ def format_commit_message(commit) # rubocop:disable Metrics/AbcSize
69
+ author = commit['author']['name'] if commit['author'].is_a? Hash
70
+ default_format % {
71
+ repository: format_repository(repository_name),
72
+ branch: format_branch(branch_name),
73
+ hash: format_hash(commit['id']),
74
+ author: format_user(author),
75
+ title: prettify(full_commit_message(commit))
76
+ }
77
+ end
78
+
79
+ def default_format
80
+ '%<repository>s/%<branch>s %<hash>s %<author>s: %<title>s'
81
+ end
82
+
83
+ def full_commit_message(commit)
84
+ (commit.is_a?(Hash) ? commit['message'] : nil).to_s
85
+ end
86
+
87
+ def summary_url # rubocop:disable Metrics/AbcSize
88
+ if created? then distinct_commits.empty? ? branch_url : compare_url
89
+ elsif deleted? then before_sha_url
90
+ elsif forced? then branch_url
91
+ elsif distinct_commits.length == 1
92
+ distinct_commits.first['url'].to_s
93
+ else
94
+ compare_url
95
+ end
96
+ end
97
+
98
+ def created?
99
+ /\A0{40}\z/ =~ extract(:before)
100
+ end
101
+
102
+ def deleted?
103
+ /\A0{40}\z/ =~ extract(:after)
104
+ end
105
+
106
+ def forced?
107
+ extract(:forced)
108
+ end
109
+
110
+ def tag?
111
+ %r{\Arefs/tags/} =~ ref
112
+ end
113
+
114
+ def commits
115
+ extract(:commits)
116
+ end
117
+
118
+ def pusher_name
119
+ extract(:pusher, :name) || 'somebody'
120
+ end
121
+
122
+ def ref
123
+ extract(:ref).to_s
124
+ end
125
+
126
+ def ref_name
127
+ ref.sub(%r{\Arefs/(heads|tags)/}, '')
128
+ end
129
+
130
+ alias tag_name ref_name
131
+ alias branch_name ref_name
132
+
133
+ def base_ref
134
+ extract(:base_ref)
135
+ end
136
+
137
+ def base_ref_name
138
+ base_ref.sub(%r{\Arefs/(heads|tags)/}, '')
139
+ end
140
+
141
+ def branch_url
142
+ "#{repository_url}/commits/#{branch_name}"
143
+ end
144
+
145
+ def compare_url
146
+ extract(:compare).to_s
147
+ end
148
+
149
+ def before_sha
150
+ extract(:before).to_s
151
+ end
152
+
153
+ def before_sha_url
154
+ "#{repository_url}/commits/#{before_sha}"
155
+ end
156
+
157
+ def after_sha
158
+ extract(:after).to_s
159
+ end
160
+
161
+ def after_sha_url
162
+ "#{repository_url}/commits/#{after_sha}"
163
+ end
164
+
165
+ def distinct_commits
166
+ extract(:distinct_commits) || commits.select do |commit|
167
+ commit['distinct'] && !commit['message'].to_s.strip.empty?
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codebot
4
+ module Formatters
5
+ # This class formats watch events.
6
+ class Watch < Formatter
7
+ # Formats IRC messages for a watch event.
8
+ #
9
+ # @return [Array<String>] the formatted messages
10
+ def format
11
+ ["#{summary}: #{format_url url}"] if started?
12
+ end
13
+
14
+ def summary
15
+ default_format % {
16
+ repository: format_repository(repository_name),
17
+ sender: format_user(sender_name)
18
+ }
19
+ end
20
+
21
+ def default_format
22
+ '[%<repository>s] %<sender>s starred the repository'
23
+ end
24
+
25
+ def action
26
+ extract(:action)
27
+ end
28
+
29
+ def started?
30
+ action.eql? 'started'
31
+ end
32
+
33
+ def summary_url
34
+ "#{repository_url}/stargazers"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/channel'
4
+ require 'codebot/cryptography'
5
+ require 'codebot/sanitizers'
6
+ require 'codebot/serializable'
7
+
8
+ module Codebot
9
+ # This class represents an integration that maps an endpoint to the
10
+ # corresponding IRC channels.
11
+ class Integration < Serializable # rubocop:disable Metrics/ClassLength
12
+ include Sanitizers
13
+
14
+ # @return [String] the name of this integration
15
+ attr_reader :name
16
+
17
+ # @return [String] the endpoint mapped to this integration
18
+ attr_reader :endpoint
19
+
20
+ # @return [String] the secret for verifying the authenticity of payloads
21
+ # delivered to the endpoint
22
+ attr_reader :secret
23
+
24
+ # @return [Array<Channel>] the channels notifications will be delivered to
25
+ attr_reader :channels
26
+
27
+ attr_accessor :gitlab
28
+ attr_accessor :shortener_url
29
+ attr_accessor :shortener_secret
30
+
31
+ # Creates a new integration from the supplied hash.
32
+ #
33
+ # @param params [Hash] A hash with symbolic keys representing the instance
34
+ # attributes of this integration. The key +:name+ is
35
+ # required.
36
+ def initialize(params)
37
+ update!(params)
38
+ end
39
+
40
+ # Updates the integration from the supplied hash.
41
+ #
42
+ # @param params [Hash] A hash with symbolic keys representing the instance
43
+ # attributes of this integration.
44
+ def update!(params)
45
+ self.name = params[:name]
46
+ self.endpoint = params[:endpoint]
47
+ self.secret = params[:secret]
48
+ self.gitlab = params[:gitlab] || false
49
+ self.shortener_url = params[:shortener_url]
50
+ self.shortener_secret = params[:shortener_secret]
51
+ set_channels params[:channels], params[:config]
52
+ end
53
+
54
+ # Adds the specified channels to this integration.
55
+ #
56
+ # @note This method is not thread-safe and should only be called from an
57
+ # active transaction.
58
+ # @param channels [Hash] the channel data to add
59
+ # @param conf [Hash] the previously deserialized configuration
60
+ # @raise [CommandError] if one of the channel identifiers already exists
61
+ def add_channels!(channels, conf)
62
+ channels.each_key do |identifier|
63
+ if @channels.any? { |chan| chan.identifier_eql?(identifier) }
64
+ raise CommandError, "channel #{identifier.inspect} already exists"
65
+ end
66
+ end
67
+ new_channels = Channel.deserialize_all(channels, conf)
68
+ @channels.push(*new_channels)
69
+ end
70
+
71
+ # Deletes the specified channels from this integration.
72
+ #
73
+ # @note This method is not thread-safe and should only be called from an
74
+ # active transaction.
75
+ # @param identifiers [Array<String>] the channel identifiers to remove
76
+ # @raise [CommandError] if one of the channel identifiers does not exist
77
+ def delete_channels!(identifiers)
78
+ identifiers.each do |identifier|
79
+ channel = @channels.find { |chan| chan.identifier_eql? identifier }
80
+ if channel.nil?
81
+ raise CommandError, "channel #{identifier.inspect} does not exist"
82
+ end
83
+
84
+ @channels.delete channel
85
+ end
86
+ end
87
+
88
+ def name=(name)
89
+ @name = valid! name, valid_identifier(name), :@name,
90
+ required: true,
91
+ required_error: 'integrations must have a name',
92
+ invalid_error: 'invalid integration name %s'
93
+ end
94
+
95
+ def endpoint=(endpoint)
96
+ @endpoint = valid!(endpoint, valid_endpoint(endpoint), :@endpoint,
97
+ invalid_error: 'invalid endpoint %s') do
98
+ Cryptography.generate_endpoint
99
+ end
100
+ end
101
+
102
+ def secret=(secret)
103
+ @secret = valid!(secret, valid_secret(secret), :@secret,
104
+ invalid_error: 'invalid secret %s') do
105
+ Cryptography.generate_secret
106
+ end
107
+ end
108
+
109
+ # Checks whether payloads delivered to this integration must be verified.
110
+ #
111
+ # @return [Boolean] whether verification is required
112
+ def verify_payloads?
113
+ !secret.to_s.strip.empty?
114
+ end
115
+
116
+ # Sets the list of channels.
117
+ #
118
+ # @param channels [Array<Channel>] the list of channels
119
+ # @param conf [Hash] the previously deserialized configuration
120
+ def set_channels(channels, conf)
121
+ if channels.nil?
122
+ @channels = [] if @channels.nil?
123
+ return
124
+ end
125
+ @channels = valid!(channels, Channel.deserialize_all(channels, conf),
126
+ :@channels,
127
+ invalid_error: 'invalid channel list %s') { [] }
128
+ end
129
+
130
+ # Checks whether the name of this integration is equal to another name.
131
+ #
132
+ # @param name [String] the other name
133
+ # @return [Boolean] +true+ if the names are equal, +false+ otherwise
134
+ def name_eql?(name)
135
+ @name.casecmp(name).zero?
136
+ end
137
+
138
+ # Checks whether the endpoint associated with this integration is equal
139
+ # to another endpoint.
140
+ #
141
+ # @param endpoint [String] the other endpoint
142
+ # @return [Boolean] +true+ if the endpoints are equal, +false+ otherwise
143
+ def endpoint_eql?(endpoint)
144
+ @endpoint.eql? endpoint
145
+ end
146
+
147
+ # Serializes this integration.
148
+ #
149
+ # @param conf [Hash] the deserialized configuration
150
+ # @return [Array, Hash] the serialized object
151
+ def serialize(conf)
152
+ check_channel_networks!(conf)
153
+ [name, {
154
+ 'endpoint' => endpoint,
155
+ 'secret' => secret,
156
+ 'gitlab' => gitlab,
157
+ 'shortener_url' => shortener_url,
158
+ 'shortener_secret' => shortener_secret,
159
+ 'channels' => Channel.serialize_all(channels, conf)
160
+ }]
161
+ end
162
+
163
+ # Compares the channels against the specified configuration, dropping any
164
+ # channels belonging to networks that no longer exist.
165
+ #
166
+ # @param conf [Config] the configuration
167
+ def check_channel_networks!(conf)
168
+ @channels.select! do |channel|
169
+ conf[:networks].include? channel.network
170
+ end
171
+ end
172
+
173
+ # Deserializes an integration.
174
+ #
175
+ # @param name [String] the name of the integration
176
+ # @param data [Hash] the serialized data
177
+ # @return [Hash] the parameters to pass to the initializer
178
+ def self.deserialize(name, data)
179
+ {
180
+ name: name,
181
+ endpoint: data['endpoint'],
182
+ secret: data['secret'],
183
+ gitlab: data['gitlab'],
184
+ shortener_url: data['shortener_url'],
185
+ shortener_secret: data['shortener_secret'],
186
+ channels: data['channels']
187
+ }
188
+ end
189
+
190
+ # @return [true] to indicate that data is serialized into a hash
191
+ def self.serialize_as_hash?
192
+ true
193
+ end
194
+ end
195
+ end