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