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