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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE.md +32 -0
- data/.github/ISSUE_TEMPLATE/formatter_issue.md +20 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +26 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +75 -0
- data/LICENSE +21 -0
- data/README.md +230 -0
- data/Rakefile +29 -0
- data/bin/console +8 -0
- data/codebot.gemspec +49 -0
- data/exe/codebot +7 -0
- data/lib/codebot.rb +8 -0
- data/lib/codebot/channel.rb +134 -0
- data/lib/codebot/command_error.rb +17 -0
- data/lib/codebot/config.rb +125 -0
- data/lib/codebot/configuration_error.rb +17 -0
- data/lib/codebot/core.rb +76 -0
- data/lib/codebot/cryptography.rb +38 -0
- data/lib/codebot/event.rb +62 -0
- data/lib/codebot/ext/cinch/ssl_extensions.rb +37 -0
- data/lib/codebot/formatter.rb +242 -0
- data/lib/codebot/formatters.rb +109 -0
- data/lib/codebot/formatters/.rubocop.yml +2 -0
- data/lib/codebot/formatters/commit_comment.rb +43 -0
- data/lib/codebot/formatters/fork.rb +40 -0
- data/lib/codebot/formatters/gitlab_issue_hook.rb +56 -0
- data/lib/codebot/formatters/gitlab_job_hook.rb +77 -0
- data/lib/codebot/formatters/gitlab_merge_request_hook.rb +57 -0
- data/lib/codebot/formatters/gitlab_note_hook.rb +119 -0
- data/lib/codebot/formatters/gitlab_pipeline_hook.rb +51 -0
- data/lib/codebot/formatters/gitlab_push_hook.rb +83 -0
- data/lib/codebot/formatters/gitlab_wiki_page_hook.rb +56 -0
- data/lib/codebot/formatters/gollum.rb +67 -0
- data/lib/codebot/formatters/issue_comment.rb +41 -0
- data/lib/codebot/formatters/issues.rb +41 -0
- data/lib/codebot/formatters/ping.rb +79 -0
- data/lib/codebot/formatters/public.rb +30 -0
- data/lib/codebot/formatters/pull_request.rb +71 -0
- data/lib/codebot/formatters/pull_request_review_comment.rb +49 -0
- data/lib/codebot/formatters/push.rb +172 -0
- data/lib/codebot/formatters/watch.rb +38 -0
- data/lib/codebot/integration.rb +195 -0
- data/lib/codebot/integration_manager.rb +225 -0
- data/lib/codebot/ipc_client.rb +83 -0
- data/lib/codebot/ipc_server.rb +79 -0
- data/lib/codebot/irc_client.rb +102 -0
- data/lib/codebot/irc_connection.rb +156 -0
- data/lib/codebot/message.rb +37 -0
- data/lib/codebot/metadata.rb +15 -0
- data/lib/codebot/network.rb +240 -0
- data/lib/codebot/network_manager.rb +181 -0
- data/lib/codebot/options.rb +49 -0
- data/lib/codebot/options/base.rb +55 -0
- data/lib/codebot/options/core.rb +126 -0
- data/lib/codebot/options/integration.rb +101 -0
- data/lib/codebot/options/network.rb +109 -0
- data/lib/codebot/payload.rb +32 -0
- data/lib/codebot/request.rb +51 -0
- data/lib/codebot/sanitizers.rb +130 -0
- data/lib/codebot/serializable.rb +101 -0
- data/lib/codebot/shortener.rb +43 -0
- data/lib/codebot/thread_controller.rb +70 -0
- data/lib/codebot/user_error.rb +13 -0
- data/lib/codebot/validation_error.rb +17 -0
- data/lib/codebot/web_listener.rb +107 -0
- data/lib/codebot/web_server.rb +58 -0
- data/webhook.png +0 -0
- 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
|