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,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