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,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/network_manager'
4
+ require 'codebot/command_error'
5
+
6
+ module Codebot
7
+ # This class manages the integrations associated with a configuration.
8
+ class IntegrationManager # rubocop:disable Metrics/ClassLength
9
+ # @return [Config] the configuration managed by this class
10
+ attr_reader :config
11
+
12
+ # Constructs a new integration manager for a specified configuration.
13
+ #
14
+ # @param config [Config] the configuration to manage
15
+ def initialize(config)
16
+ @config = config
17
+ end
18
+
19
+ # Creates a new integration from the given parameters.
20
+ #
21
+ # @param params [Hash] the parameters to initialize the integration with
22
+ def create(params)
23
+ integration = Integration.new(
24
+ params.merge(config: { networks: @config.networks })
25
+ )
26
+ @config.transaction do
27
+ check_available!(integration.name, integration.endpoint)
28
+ NetworkManager.new(@config).check_channels!(integration)
29
+ @config.integrations << integration
30
+ integration_feedback(integration, :created) unless params[:quiet]
31
+ end
32
+ end
33
+
34
+ # Updates an integration with the given parameters.
35
+ #
36
+ # @param name [String] the current name of the integration to update
37
+ # @param params [Hash] the parameters to update the integration with
38
+ def update(name, params)
39
+ @config.transaction do
40
+ integration = find_integration!(name)
41
+ check_available_except!(params[:name], params[:endpoint], integration)
42
+ update_channels!(integration, params)
43
+ NetworkManager.new(@config).check_channels!(integration)
44
+ integration.update!(params)
45
+ integration_feedback(integration, :updated) unless params[:quiet]
46
+ end
47
+ end
48
+
49
+ # Destroys an integration.
50
+ #
51
+ # @param name [String] the name of the integration to destroy
52
+ # @param params [Hash] the command-line options
53
+ def destroy(name, params)
54
+ @config.transaction do
55
+ integration = find_integration!(name)
56
+ @config.integrations.delete integration
57
+ integration_feedback(integration, :destroyed) unless params[:quiet]
58
+ end
59
+ end
60
+
61
+ # Lists all integrations, or integrations with names containing the given
62
+ # search term.
63
+ #
64
+ # @param search [String, nil] an optional search term
65
+ def list(search)
66
+ @config.transaction do
67
+ integrations = @config.integrations.dup
68
+ unless search.nil?
69
+ integrations.select! do |intg|
70
+ intg.name.downcase.include? search.downcase
71
+ end
72
+ end
73
+ puts 'No integrations found' if integrations.empty?
74
+ integrations.each { |intg| show_integration intg }
75
+ end
76
+ end
77
+
78
+ # Finds an integration given its name.
79
+ #
80
+ # @param name [String] the name to search for
81
+ # @return [Integration, nil] the integration, or +nil+ if none was found
82
+ def find_integration(name)
83
+ @config.integrations.find { |intg| intg.name_eql? name }
84
+ end
85
+
86
+ # Finds an integration given its endpoint.
87
+ #
88
+ # @param endpoint [String] the endpoint to search for
89
+ # @return [Integration, nil] the integration, or +nil+ if none was found
90
+ def find_integration_by_endpoint(endpoint)
91
+ @config.integrations.find { |intg| intg.endpoint_eql? endpoint }
92
+ end
93
+
94
+ # Finds an integration given its name.
95
+ #
96
+ # @param name [String] the name to search for
97
+ # @raise [CommandError] if no integration with the given name exists
98
+ # @return [Integration] the integration
99
+ def find_integration!(name)
100
+ integration = find_integration(name)
101
+ return integration unless integration.nil?
102
+
103
+ raise CommandError, "an integration with the name #{name.inspect} " \
104
+ 'does not exist'
105
+ end
106
+
107
+ private
108
+
109
+ # Checks that the specified name is available for use.
110
+ #
111
+ # @param name [String] the name to check for
112
+ # @raise [CommandError] if the name is already taken
113
+ def check_name_available!(name)
114
+ return unless find_integration(name)
115
+
116
+ raise CommandError, "an integration with the name #{name.inspect} " \
117
+ 'already exists'
118
+ end
119
+
120
+ # Checks that the specified endpoint is available for use.
121
+ #
122
+ # @param endpoint [String] the endpoint to check for
123
+ # @raise [CommandError] if the endpoint is already taken
124
+ def check_endpoint_available!(endpoint)
125
+ return unless find_integration_by_endpoint(endpoint)
126
+
127
+ raise CommandError, 'an integration with the endpoint ' \
128
+ "#{endpoint.inspect} already exists"
129
+ end
130
+
131
+ # Checks that the specified name and endpoint are available for use.
132
+ #
133
+ # @param name [String] the name to check for
134
+ # @param endpoint [String] the endpoint to check for
135
+ # @raise [CommandError] if name or endpoint are already taken
136
+ def check_available!(name, endpoint)
137
+ check_name_available!(name) unless name.nil?
138
+ check_endpoint_available!(endpoint) unless endpoint.nil?
139
+ end
140
+
141
+ # Checks that the specified name and endpoint are available for use by the
142
+ # specified integration.
143
+ #
144
+ # @param name [String] the name to check for
145
+ # @param endpoint [String] the endpoint to check for
146
+ # @param intg [Integration] the integration to ignore
147
+ # @raise [CommandError] if name or endpoint are already taken
148
+ def check_available_except!(name, endpoint, intg)
149
+ check_name_available!(name) unless name.nil? || intg.name_eql?(name)
150
+ return if endpoint.nil? || intg.endpoint_eql?(endpoint)
151
+
152
+ check_endpoint_available!(endpoint)
153
+ end
154
+
155
+ # Updates the channels associated with an integration from the specified
156
+ # parameters.
157
+ #
158
+ # @param integration [Integration] the integration
159
+ # @param params [Hash] the parameters to update the integration with. Valid
160
+ # keys are +:clear_channels+ to clear the channel list
161
+ # before proceeding, +:add_channel+ to add the given
162
+ # channels, and +:delete_channel+ to delete the given
163
+ # channels from the integration. All keys are optional.
164
+ # The value of +:clear_channels+ should be a boolean.
165
+ # The value of +:add_channel+ should be a hash of the
166
+ # form +identifier => params+, and +:remove_channel+
167
+ # should be an array of channel identifiers to remove.
168
+ def update_channels!(integration, params)
169
+ integration.channels.clear if params[:clear_channels]
170
+ if params[:delete_channel]
171
+ integration.delete_channels!(params[:delete_channel])
172
+ end
173
+ return unless params[:add_channel]
174
+
175
+ integration.add_channels!(params[:add_channel],
176
+ networks: @config.networks)
177
+ end
178
+
179
+ # Displays feedback about a change made to an integration.
180
+ #
181
+ # @param integration [Integration] the integration
182
+ # @param action [#to_s] the action (+:created+, +:updated+ or +:destroyed+)
183
+ def integration_feedback(integration, action)
184
+ puts "Integration was successfully #{action}"
185
+ show_integration(integration)
186
+ end
187
+
188
+ # Prints information about an integration.
189
+ #
190
+ # @param integration [Integration] the integration
191
+ def show_integration(integration)
192
+ puts "Integration: #{integration.name}"
193
+ puts "\tEndpoint: #{integration.endpoint}"
194
+ puts "\tSecret: #{show_integration_secret(integration)}"
195
+ if integration.channels.empty?
196
+ puts "\tChannels: (none)"
197
+ else
198
+ puts "\tChannels:"
199
+ show_integration_channels(integration)
200
+ end
201
+ end
202
+
203
+ # Returns an integration secret, or "(none required)" if payload integrity
204
+ # verification is disabled.
205
+ #
206
+ # @param integration [Integration] the integration
207
+ # @return [String] the secret or placeholder
208
+ def show_integration_secret(integration)
209
+ return '(none required)' unless integration.verify_payloads?
210
+
211
+ integration.secret.to_s
212
+ end
213
+
214
+ # Prints information about the channels associated with an integration.
215
+ #
216
+ # @param integration [Integration] the integration
217
+ def show_integration_channels(integration)
218
+ integration.channels.each do |channel|
219
+ puts "\t\t- #{channel.name} on #{channel.network.name}"
220
+ puts "\t\t\tKey: #{channel.key}" if channel.key?
221
+ puts "\t\t\tMessages are sent without joining" if channel.send_external
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+
5
+ module Codebot
6
+ # A pipe-based IPC client used for communicating with a running Codebot
7
+ # instance.
8
+ class IPCClient
9
+ # @return [String] the path to the named pipe
10
+ attr_reader :pipe
11
+
12
+ # Creates a new IPC client.
13
+ #
14
+ # @param pipe [String] the path to the named pipe, or +nil+ to use the
15
+ # default pipe for this user
16
+ def initialize(pipe = nil)
17
+ @pipe = pipe || IPCServer.default_pipe
18
+ end
19
+
20
+ # Sends a REHASH command to the named pipe.
21
+ #
22
+ # @param explicit [Boolean] whether this command was invoked explicitly
23
+ def send_rehash(explicit = true)
24
+ command('REHASH', explicit)
25
+ end
26
+
27
+ # Sends a STOP command to the named pipe.
28
+ #
29
+ # @param explicit [Boolean] whether this command was invoked explicitly
30
+ def send_stop(explicit = true)
31
+ command('STOP', explicit)
32
+ end
33
+
34
+ # Checks whether the named pipe exists.
35
+ #
36
+ # @return [Boolean] +true+ if the pipe exists, +false+ otherwise
37
+ def pipe_exist?
38
+ File.pipe? @pipe
39
+ end
40
+
41
+ private
42
+
43
+ # Sends a command to the named pipe.
44
+ #
45
+ # @param cmd [String] the command
46
+ # @param explicit [Boolean] whether this command was invoked explicitly
47
+ # @return [Boolean] whether the command was sent successfully
48
+ def command(cmd, explicit)
49
+ return false unless check_pipe_exist(explicit)
50
+
51
+ Timeout.timeout 5 do
52
+ File.open @pipe, 'w' do |p|
53
+ p.puts cmd
54
+ end
55
+ end
56
+ true
57
+ rescue Timeout::Error
58
+ communication_error! 'no response'
59
+ end
60
+
61
+ # Checks whether the named pipe exists.
62
+ #
63
+ # @param should_raise [Boolean] whether to raise an exception if the pipe
64
+ # does not exist
65
+ # @return [Boolean] +true+ if the pipe exists, +false+ otherwise
66
+ def check_pipe_exist(should_raise)
67
+ return true if pipe_exist?
68
+
69
+ communication_error! "missing pipe #{@pipe.inspect}" if should_raise
70
+ false
71
+ end
72
+
73
+ # Raise a {CommandError} with a message stating that communication with
74
+ # an active instance failed.
75
+ #
76
+ # @param msg [String] the error message
77
+ # @raise [CommandError] the requested error
78
+ def communication_error!(msg)
79
+ raise CommandError, "unable to communicate with the bot: #{msg} " \
80
+ '(is the bot running?)'
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codebot
4
+ # A pipe-based IPC server used for communicating with a running Codebot
5
+ # instance.
6
+ class IPCServer < ThreadController
7
+ # @return [Core] the bot this server belongs to
8
+ attr_reader :core
9
+
10
+ # @return [String] the path to the named pipe
11
+ attr_reader :pipe
12
+
13
+ # Creates a new IPC server.
14
+ #
15
+ # @param core [Core] the bot this server belongs to
16
+ # @param pipe [String] the path to the named pipe, or +nil+ to use the
17
+ # default pipe for this user
18
+ def initialize(core, pipe = nil)
19
+ super()
20
+ @core = core
21
+ @pipe = pipe || self.class.default_pipe
22
+ end
23
+
24
+ # Stops the managed thread if a thread is currently running, then deletes
25
+ # the named pipe.
26
+ #
27
+ # @return [Thread, nil] the stopped thread, or +nil+ if
28
+ # no thread was running
29
+ def stop
30
+ thr = super
31
+ delete_pipe
32
+ thr
33
+ end
34
+
35
+ # Returns the path to the default pipe for the current user.
36
+ #
37
+ # @return [String] the path to the named pipe
38
+ def self.default_pipe
39
+ File.join Dir.home, '.codebot.ipc'
40
+ end
41
+
42
+ private
43
+
44
+ # Creates the named pipe.
45
+ def create_pipe
46
+ return if File.pipe? @pipe
47
+
48
+ delete_pipe
49
+ File.mkfifo @pipe
50
+ end
51
+
52
+ # Deletes the named pipe.
53
+ def delete_pipe
54
+ File.delete @pipe if File.exist? @pipe
55
+ end
56
+
57
+ # Starts this IPC server.
58
+ def run(*)
59
+ create_pipe
60
+ file = File.open @pipe, 'r+'
61
+ while (line = file.gets.strip)
62
+ handle_command line
63
+ end
64
+ ensure
65
+ delete_pipe
66
+ end
67
+
68
+ # Handles an incoming IPC command.
69
+ #
70
+ # @param command [String] the command
71
+ def handle_command(command)
72
+ case command
73
+ when 'REHASH' then @core.config.load!
74
+ when 'STOP' then Thread.new { @core.stop }
75
+ else STDERR.puts "Unknown IPC command: #{command.inspect}"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/irc_connection'
4
+
5
+ module Codebot
6
+ # This class manages an IRC client.
7
+ class IRCClient
8
+ # Creates a new IRC client.
9
+ #
10
+ # @param core [Core] the bot this client belongs to
11
+ def initialize(core)
12
+ @active = false
13
+ @checkpoint = []
14
+ @connections = []
15
+ @core = core
16
+ @semaphore = Mutex.new
17
+ end
18
+
19
+ # Dispatches a new request.
20
+ #
21
+ # @param request [Request] the request to dispatch
22
+ def dispatch(request)
23
+ request.each_network do |network, channels|
24
+ connection = connection_to(network)
25
+ next if connection.nil?
26
+
27
+ channels.each do |channel|
28
+ message = request.to_message_for channel
29
+ connection.enqueue message
30
+ end
31
+ end
32
+ end
33
+
34
+ # Starts the IRC client.
35
+ def start
36
+ @active = true
37
+ migrate!
38
+ end
39
+
40
+ # Stops the IRC client.
41
+ def stop
42
+ @active = false
43
+ @checkpoint.clear
44
+ @connections.each(&:stop)
45
+ join
46
+ @connections.clear
47
+ end
48
+
49
+ # Waits for active connections to finish.
50
+ def join
51
+ @connections.each(&:join)
52
+ end
53
+
54
+ # Connects to and disconnects from networks as necessary in order for the
55
+ # list of connections to reflect changes to the configuration.
56
+ def migrate!
57
+ @semaphore.synchronize do
58
+ return unless @active
59
+
60
+ networks = @core.config.networks
61
+ (@checkpoint - networks).each { |network| disconnect_from network }
62
+ (networks - @checkpoint).each { |network| connect_to network }
63
+ @checkpoint = networks
64
+ end
65
+ end
66
+
67
+ private
68
+
69
+ # Finds the connection to a given network.
70
+ #
71
+ # @param network [Network] the network
72
+ # @return [IRCConnection, nil] the connection, or +nil+ if none was found
73
+ def connection_to(network)
74
+ @connections.find { |con| con.network.eql? network }
75
+ end
76
+
77
+ # Checks whether the client is connected to a given network.
78
+ #
79
+ # @param network [Network] the network
80
+ # @return [Boolean] +true+ if the client is connected, +false+ otherwise
81
+ def connected_to?(network)
82
+ !connection_to(network).nil?
83
+ end
84
+
85
+ # Connects to a given network if the same network is not already connected.
86
+ #
87
+ # @param network [Network] the network to connect to
88
+ def connect_to(network)
89
+ return if connected_to? network
90
+
91
+ @connections << IRCConnection.new(@core, network).tap(&:start)
92
+ end
93
+
94
+ # Disconnects from a given network if the network is currently connected.
95
+ #
96
+ # @param network [Network] the network to disconnected from
97
+ def disconnect_from(network)
98
+ connection = @connections.delete connection_to(network)
99
+ connection.tap(&:stop).tap(&:join) unless connection.nil?
100
+ end
101
+ end
102
+ end