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,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/command_error'
4
+
5
+ module Codebot
6
+ # This class manages the networks associated with a configuration.
7
+ class NetworkManager
8
+ # @return [Config] the configuration managed by this class
9
+ attr_reader :config
10
+
11
+ # Constructs a new network manager for a specified configuration.
12
+ #
13
+ # @param config [Config] the configuration to manage
14
+ def initialize(config)
15
+ @config = config
16
+ end
17
+
18
+ # Creates a new network from the given parameters.
19
+ #
20
+ # @param params [Hash] the parameters to initialize the network with
21
+ def create(params)
22
+ network = Network.new(params.merge(config: {}))
23
+ @config.transaction do
24
+ check_name_available!(network.name)
25
+ @config.networks << network
26
+ network_feedback(network, :created) unless params[:quiet]
27
+ end
28
+ end
29
+
30
+ # Updates a network with the given parameters.
31
+ #
32
+ # @param name [String] the current name of the network to update
33
+ # @param params [Hash] the parameters to update the network with
34
+ def update(name, params)
35
+ @config.transaction do
36
+ network = find_network!(name)
37
+ unless params[:name].nil?
38
+ check_name_available_except!(params[:name], network)
39
+ end
40
+ network.update!(params)
41
+ network_feedback(network, :updated) unless params[:quiet]
42
+ end
43
+ end
44
+
45
+ # Destroys a network.
46
+ #
47
+ # @param name [String] the name of the network to destroy
48
+ # @param params [Hash] the command-line options
49
+ def destroy(name, params)
50
+ @config.transaction do
51
+ network = find_network!(name)
52
+ @config.networks.delete network
53
+ network_feedback(network, :destroyed) unless params[:quiet]
54
+ end
55
+ end
56
+
57
+ # Lists all networks, or networks with names containing the given search
58
+ # term.
59
+ #
60
+ # @param search [String, nil] an optional search term
61
+ def list(search)
62
+ @config.transaction do
63
+ networks = @config.networks.dup
64
+ unless search.nil?
65
+ networks.select! { |net| net.name.downcase.include? search.downcase }
66
+ end
67
+ puts 'No networks found' if networks.empty?
68
+ networks.each { |net| show_network net }
69
+ end
70
+ end
71
+
72
+ # Finds a network given its name.
73
+ #
74
+ # @param name [String] the name to search for
75
+ # @return [Network, nil] the network, or +nil+ if none was found
76
+ def find_network(name)
77
+ @config.networks.find { |net| net.name_eql? name }
78
+ end
79
+
80
+ # Finds a network given its name.
81
+ #
82
+ # @param name [String] the name to search for
83
+ # @raise [CommandError] if no network with the given name exists
84
+ # @return [Network] the network
85
+ def find_network!(name)
86
+ network = find_network(name)
87
+ return network unless network.nil?
88
+
89
+ raise CommandError, "a network with the name #{name.inspect} " \
90
+ 'does not exist'
91
+ end
92
+
93
+ # Checks that all channels associated with an integration belong to a valid
94
+ # network.
95
+ #
96
+ # @param integration [Integration] the integration to check
97
+ def check_channels!(integration)
98
+ integration.channels.map(&:network).map(&:name).each do |network|
99
+ find_network!(network)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ # Checks that the specified name is available for use.
106
+ #
107
+ # @param name [String] the name to check for
108
+ # @raise [CommandError] if the name is already taken
109
+ def check_name_available!(name)
110
+ return if name.nil? || !find_network(name)
111
+
112
+ raise CommandError, "a network with the name #{name.inspect} " \
113
+ 'already exists'
114
+ end
115
+
116
+ # Checks that the specified name is available for use by the specified
117
+ # network.
118
+ #
119
+ # @param name [String] the name to check for
120
+ # @param network [Network] the network to ignore
121
+ # @raise [CommandError] if the name is already taken
122
+ def check_name_available_except!(name, network)
123
+ return if name.nil? || network.name_eql?(name) || !find_network(name)
124
+
125
+ raise CommandError, "a network with the name #{name.inspect} " \
126
+ 'already exists'
127
+ end
128
+
129
+ # Displays feedback about a change made to a network.
130
+ #
131
+ # @param network [Network] the network
132
+ # @param action [#to_s] the action (+:created+, +:updated+ or +:destroyed+)
133
+ def network_feedback(network, action)
134
+ puts "Network was successfully #{action}"
135
+ show_network(network)
136
+ end
137
+
138
+ # Prints information about a network.
139
+ #
140
+ # @param network [Network] the network
141
+ def show_network(network) # rubocop:disable Metrics/AbcSize
142
+ puts "Network: #{network.name}"
143
+ security = "#{network.secure ? 'secure' : 'insecure'} connection"
144
+ password = network.server_password
145
+ puts "\tServer: #{network.host}:#{network.real_port} (#{security})"
146
+ puts "\tPassword: #{'*' * password.length}" unless password.to_s.empty?
147
+ puts "\tNickname: #{network.nick}"
148
+ puts "\tBind to: #{network.bind}" unless network.bind.to_s.empty?
149
+ puts "\tUser modes: #{network.modes}" unless network.modes.to_s.empty?
150
+ show_network_sasl(network)
151
+ show_network_nickserv(network)
152
+ end
153
+
154
+ # Prints information about the SASL authentication settings for a network.
155
+ #
156
+ # @param network [Network] the network
157
+ def show_network_sasl(network)
158
+ puts "\tSASL authentication #{network.sasl? ? 'enabled' : 'disabled'}"
159
+ return unless network.sasl?
160
+
161
+ puts "\t\tUsername: #{network.sasl_username}"
162
+ puts "\t\tPassword: #{'*' * network.sasl_password.to_s.length}"
163
+ end
164
+
165
+ def nickserv_status(network)
166
+ network.nickserv? ? 'enabled' : 'disabled'
167
+ end
168
+
169
+ # Prints information about the NickServ authentication
170
+ # settings for a network.
171
+ #
172
+ # @param network [Network] the network
173
+ def show_network_nickserv(network)
174
+ puts "\tNickServ authentication #{nickserv_status(network)}"
175
+ return unless network.nickserv?
176
+
177
+ puts "\t\tUsername: #{network.nickserv_username}"
178
+ puts "\t\tPassword: #{'*' * network.nickserv_password.to_s.length}"
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ require 'codebot/options/base'
6
+
7
+ module Codebot
8
+ # This module provides functionality for parsing command-line options.
9
+ module Options
10
+ # Creates a new {Core} from the specified command-line options. Errors of
11
+ # type {UserError} are handled if they occur in the given block.
12
+ #
13
+ # @param opts [Hash] the options to initialize the core with
14
+ # @param rehash [Boolean] whether to ask a running instance to rehash its
15
+ # configuration after invoking the block
16
+ # @yield [Core] the newly created {Core}
17
+ def self.with_core(opts, rehash = false)
18
+ core = ::Codebot::Core.new(
19
+ config_file: opts[:config],
20
+ ipc_pipe: opts[:pipe]
21
+ )
22
+ with_errors { yield core }
23
+ return unless rehash
24
+
25
+ with_ipc_client(opts) do |ipc|
26
+ break unless ipc.send_rehash(!opts[:pipe].nil?)
27
+
28
+ puts 'Rehashing the running instance...' unless opts[:quiet]
29
+ end
30
+ end
31
+
32
+ # Invokes the given block, handling {UserError} errors.
33
+ def self.with_errors
34
+ yield
35
+ rescue UserError => e
36
+ STDERR.puts "Error: #{e.message}"
37
+ exit 1
38
+ end
39
+
40
+ # Creates a new {IPCClient} from the specified command-line options.
41
+ # Errors of type {UserError} are handled if they occur in the given block.
42
+ #
43
+ # @param opts [Hash] the options to initialize the client with
44
+ # @yield [IPCClient] the newly created {IPCClient}
45
+ def self.with_ipc_client(opts)
46
+ with_errors { yield IPCClient.new(opts[:pipe]) }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+
5
+ require 'codebot/options/core'
6
+ require 'codebot/options/network'
7
+ require 'codebot/options/integration'
8
+ require 'codebot/user_error'
9
+
10
+ module Codebot
11
+ module Options
12
+ # A class that handles the +codebot+ command. This class delegates handling
13
+ # of any commands to the respective class for the appropriate subcommand.
14
+ class Base < Thor
15
+ check_unknown_options!
16
+
17
+ class_option :config,
18
+ banner: 'FILE',
19
+ aliases: '-C',
20
+ desc: 'Use the specified alternate configuration file'
21
+ class_option :pipe,
22
+ banner: 'FILE',
23
+ aliases: '-P',
24
+ desc: 'Use the specified alternate named pipe'
25
+ class_option :quiet,
26
+ type: :boolean,
27
+ default: false,
28
+ aliases: '-q',
29
+ desc: 'Hide status information'
30
+
31
+ desc 'core [OPTIONS]', 'Manage a Codebot core'
32
+ subcommand 'core', Core
33
+
34
+ desc 'network [OPTIONS]', 'Manage IRC networks'
35
+ subcommand 'network', Network
36
+
37
+ desc 'integration [OPTIONS]', 'Manage integrations'
38
+ subcommand 'integration', Integration
39
+
40
+ # Ensures that thor uses the correct exit code.
41
+ #
42
+ # @return true
43
+ def self.exit_on_failure?
44
+ true
45
+ end
46
+
47
+ if Process.uid.zero? || Process.euid.zero?
48
+ STDERR.puts 'Running Codebot as root is extremely dangerous; ' \
49
+ "if you're trying to listen on a privileged port " \
50
+ 'please use a gateway server instead'
51
+ exit 1
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/ipc_client'
4
+
5
+ module Codebot
6
+ module Options
7
+ # A class that handles the +codebot core+ command.
8
+ class Core < Thor
9
+ check_unknown_options!
10
+
11
+ desc 'interactive', 'Run Codebot interactively'
12
+
13
+ # Runs Codebot interactively.
14
+ def interactive
15
+ run_core(true)
16
+ end
17
+
18
+ desc 'start', 'Start a new Codebot instance in the background'
19
+
20
+ # Starts a new Codebot instance in the background.
21
+ def start
22
+ Options.with_errors { check_fork_supported! }
23
+ check_not_running!(options)
24
+ fork { run_core(false) }
25
+ end
26
+
27
+ desc 'stop', 'Stop a running Codebot instance'
28
+
29
+ # Stops a running Codebot instance.
30
+ def stop
31
+ Options.with_ipc_client(options, &:send_stop)
32
+ end
33
+
34
+ desc 'rehash', 'Reload the configuration of a running Codebot instance'
35
+
36
+ # Reloads the configuration of a running Codebot instance.
37
+ def rehash
38
+ Options.with_ipc_client(options, &:send_rehash)
39
+ end
40
+
41
+ # Ensures that thor uses the correct exit code.
42
+ #
43
+ # @return true
44
+ def self.exit_on_failure?
45
+ true
46
+ end
47
+
48
+ private
49
+
50
+ # Ensures that a Codebot instance using the same pipe is not already
51
+ # running.
52
+ #
53
+ # @param opts [Hash] a hash containing the options that would be used for
54
+ # initializing a new core; specifically, a hash
55
+ # containing the +:pipe+ key to indicate the path to
56
+ # the named pipe used by the IPC server.
57
+ # @raise [CommandError] if the named pipe already exists
58
+ def check_not_running!(opts)
59
+ Options.with_ipc_client(opts) do |ipc|
60
+ break unless ipc.pipe_exist?
61
+
62
+ raise CommandError, 'named pipe already exists; if you are sure a ' \
63
+ 'Codebot instance is not already running, you ' \
64
+ "can delete #{ipc.pipe.inspect}"
65
+ end
66
+ end
67
+
68
+ # Ensures that the current platform supports the Process::fork method,
69
+ # raising an error if it does not.
70
+ #
71
+ # @raise [CommandError] if forking is not supported
72
+ def check_fork_supported!
73
+ return if Process.respond_to?(:fork)
74
+
75
+ raise CommandError, 'this feature is not available on ' \
76
+ "#{RUBY_PLATFORM}; please use the " \
77
+ "'interactive' command instead"
78
+ end
79
+
80
+ # Reopens the standard file descriptors to prevent the forked process
81
+ # from inheriting the file descriptors of the parent process. This method
82
+ # reopens streams using a method similar to the Unix dup2 function.
83
+ #
84
+ # @param sin [File] the file to redirect into the standard input stream,
85
+ # or +nil+ to detach and immediately close the stream.
86
+ # @param sout [File] the file to redirect the standard output stream to,
87
+ # or +nil+ to discard any data written to the stream.
88
+ # @param serr [File] the file to redirect the standard error stream to,
89
+ # or +nil+ to discard any data written to the stream.
90
+ def dup2_fds(sin: nil, sout: nil, serr: nil)
91
+ $stdin.reopen(sin || null_file('r'))
92
+ $stdout.reopen(sout || null_file('w'))
93
+ $stderr.reopen(serr || null_file('w'))
94
+ end
95
+
96
+ # Creates a new null file.
97
+ #
98
+ # @param mode [String] the mode to open the file in
99
+ # @return [File] the created file
100
+ def null_file(mode)
101
+ File.new(File::NULL, mode)
102
+ end
103
+
104
+ # Initializes any missing environment variables to their default values.
105
+ def initialize_environment
106
+ ENV['CODEBOT_PORT'] ||= 4567.to_s
107
+ ENV['RACK_ENV'] ||= 'production'
108
+ end
109
+
110
+ # Starts the bot. Unless started in interactive mode, file descriptors
111
+ # are reopened from a null file.
112
+ #
113
+ # @param interactive [Boolean] whether to start the bot in the foreground
114
+ def run_core(interactive)
115
+ initialize_environment
116
+ check_not_running!(options)
117
+ dup2_fds unless interactive
118
+ Options.with_core(options) do |core|
119
+ core.trap_signals
120
+ core.start
121
+ core.join
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/integration_manager'
4
+
5
+ module Codebot
6
+ module Options
7
+ # A class that handles the +codebot integration+ command.
8
+ class Integration < Thor
9
+ check_unknown_options!
10
+
11
+ # Sets shared options for specifying properties belonging to the
12
+ # {::Codebot::Integration} class.
13
+ def self.shared_propery_options
14
+ option :endpoint, aliases: '-e',
15
+ desc: 'Set the endpoint for incoming webhooks'
16
+ option :secret, aliases: '-s',
17
+ desc: 'Set the secret for verifying webhook payloads'
18
+ end
19
+
20
+ desc 'create NAME', 'Add a new integration'
21
+ shared_propery_options
22
+ option :channels, aliases: '-c', type: :array,
23
+ desc: 'Set the channels to deliver notifications to'
24
+
25
+ # Creates a new integration with the specified name.
26
+ #
27
+ # @param name [String] the name of the new integration
28
+ def create(name)
29
+ Options.with_core(parent_options, true) do |core|
30
+ map_channels!(options, :channels)
31
+ IntegrationManager.new(core.config).create(options.merge(name: name))
32
+ end
33
+ end
34
+
35
+ desc 'update NAME', 'Edit an integration'
36
+ option :name, aliases: '-n',
37
+ banner: 'NEW-NAME',
38
+ desc: 'Rename this integration'
39
+ shared_propery_options
40
+ option :add_channel, aliases: '-a', type: :array,
41
+ desc: 'Add a channel to this integration'
42
+ option :clear_channels, aliases: '-c', type: :boolean,
43
+ desc: 'Clear the channel list ' \
44
+ '(default: false)'
45
+ option :delete_channel, aliases: '-d', type: :array,
46
+ desc: 'Delete a channel from this integration'
47
+
48
+ # Updates the integration with the specified name.
49
+ #
50
+ # @param name [String] the name of the integration
51
+ def update(name)
52
+ Options.with_core(parent_options, true) do |core|
53
+ map_channels!(options, :add_channel)
54
+ IntegrationManager.new(core.config).update(name, options)
55
+ end
56
+ end
57
+
58
+ desc 'destroy NAME', 'Delete an integration'
59
+
60
+ # Destroys the integration with the specified name.
61
+ #
62
+ # @param name [String] the name of the integration
63
+ def destroy(name)
64
+ Options.with_core(parent_options, true) do |core|
65
+ IntegrationManager.new(core.config).destroy(name, options)
66
+ end
67
+ end
68
+
69
+ desc 'list [SEARCH]', 'List integrations'
70
+
71
+ # Lists all integrations, or integrations with names containing the given
72
+ # search term.
73
+ #
74
+ # @param search [String, nil] an optional search term
75
+ def list(search = nil)
76
+ Options.with_core(parent_options, true) do |core|
77
+ IntegrationManager.new(core.config).list(search)
78
+ end
79
+ end
80
+
81
+ # Ensures that thor uses the correct exit code.
82
+ #
83
+ # @return true
84
+ def self.exit_on_failure?
85
+ true
86
+ end
87
+
88
+ private
89
+
90
+ # Destructively converts an array of channel identifiers contained in a
91
+ # hash into the serialized form of the channels contained in the array.
92
+ # If the value +hash[key]+ is +nil+, no action is taken.
93
+ #
94
+ # @param hash [Hash] the hash containing the array of identifiers
95
+ # @param key [Object] the key corresponding to the array of identifiers
96
+ def map_channels!(hash, key)
97
+ hash[key] = hash[key].map { |id| [id, {}] }.to_h unless hash[key].nil?
98
+ end
99
+ end
100
+ end
101
+ end