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,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/network_manager'
4
+
5
+ module Codebot
6
+ module Options
7
+ # A class that handles the +codebot network+ command.
8
+ class Network < Thor
9
+ check_unknown_options!
10
+
11
+ # Sets shared options for connecting to the IRC network.
12
+ def self.shared_connection_options
13
+ option :host, aliases: '-H',
14
+ desc: 'Set the server hostname or address'
15
+ option :port, type: :numeric, aliases: '-p',
16
+ desc: 'Set the port to connect to'
17
+ option :secure, type: :boolean, aliases: '-s',
18
+ desc: 'Connect securely using TLS'
19
+ option :server_password, desc: 'Set the server password'
20
+ option :nick, aliases: '-N',
21
+ desc: 'Set the nickname'
22
+ end
23
+
24
+ # Sets shared options for authenticating to the IRC network.
25
+ def self.shared_authentication_options
26
+ # Not a boolean to prevent thor from generating --no-disable-sasl flag
27
+ option :disable_sasl, type: :string, banner: '',
28
+ desc: 'Disable SASL authentication'
29
+ option :sasl_username, desc: 'Set the username for SASL authentication'
30
+ option :sasl_password, desc: 'Set the password for SASL authentication'
31
+
32
+ option :disable_nickserv, type: :string, banner: '',
33
+ desc: 'Disable NickServ authentication'
34
+ option :nickserv_username,
35
+ desc: 'Set the username for NickServ authentication'
36
+ option :nickserv_password,
37
+ desc: 'Set the password for NickServ authentication'
38
+ end
39
+
40
+ # Sets shared options for specifying properties belonging to the
41
+ # {::Codebot::Network} class.
42
+ def self.shared_propery_options
43
+ shared_connection_options
44
+ shared_authentication_options
45
+ option :bind, aliases: '-b',
46
+ desc: 'Bind to the specified IP address or host'
47
+ option :modes, aliases: '-m',
48
+ desc: 'Set user modes'
49
+ end
50
+
51
+ desc 'create NAME', 'Add a new IRC network'
52
+ shared_propery_options
53
+
54
+ # Creates a new network with the specified name.
55
+ #
56
+ # @param name [String] the name of the new network
57
+ def create(name)
58
+ Options.with_core(parent_options, true) do |core|
59
+ NetworkManager.new(core.config).create(options.merge(name: name))
60
+ end
61
+ end
62
+
63
+ desc 'update NAME', 'Edit an IRC network'
64
+ option :name, aliases: '-n',
65
+ banner: 'NEW-NAME',
66
+ desc: 'Rename this network'
67
+ shared_propery_options
68
+
69
+ # Updates the network with the specified name.
70
+ #
71
+ # @param name [String] the name of the network
72
+ def update(name)
73
+ Options.with_core(parent_options, true) do |core|
74
+ NetworkManager.new(core.config).update(name, options)
75
+ end
76
+ end
77
+
78
+ desc 'list [SEARCH]', 'List networks'
79
+
80
+ # Lists all networks, or networks with names containing the given search
81
+ # term.
82
+ #
83
+ # @param search [String, nil] an optional search term
84
+ def list(search = nil)
85
+ Options.with_core(parent_options, true) do |core|
86
+ NetworkManager.new(core.config).list(search)
87
+ end
88
+ end
89
+
90
+ desc 'destroy NAME', 'Delete an IRC network'
91
+
92
+ # Destroys the network with the specified name.
93
+ #
94
+ # @param name [String] the name of the network
95
+ def destroy(name)
96
+ Options.with_core(parent_options, true) do |core|
97
+ NetworkManager.new(core.config).destroy(name, options)
98
+ end
99
+ end
100
+
101
+ # Ensures that thor uses the correct exit code.
102
+ #
103
+ # @return true
104
+ def self.exit_on_failure?
105
+ true
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Codebot
6
+ # A JSON request payload.
7
+ class Payload
8
+ # @return [Object] the JSON object parsed from the request payload
9
+ attr_reader :json
10
+
11
+ # Constructs a new payload.
12
+ #
13
+ # @param payload [String] the request payload
14
+ def initialize(payload)
15
+ @json = JSON.parse payload
16
+ end
17
+
18
+ # Returns the JSON payload.
19
+ #
20
+ # @return [Object] the JSON object
21
+ def to_json
22
+ @json
23
+ end
24
+
25
+ # Returns the JSON string corresponding to the payload.
26
+ #
27
+ # @return [String] the JSON string
28
+ def to_s
29
+ @json.to_s
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/message'
4
+ require 'codebot/payload'
5
+
6
+ module Codebot
7
+ # A request which was received by the web server and can be delivered to the
8
+ # IRC client.
9
+ class Request
10
+ # @return [Integration] the integration to deliver this request to
11
+ attr_reader :integration
12
+
13
+ # @return [Symbol] the event that triggered the webhook delivery
14
+ attr_reader :event
15
+
16
+ # @return [Payload] the parsed request payload
17
+ attr_reader :payload
18
+
19
+ # Constructs a new request for delivery to the IRC client.
20
+ #
21
+ # @param integration [Integration] the integration for which the request
22
+ # was made
23
+ # @param event [Symbol] the event that triggered the webhook delivery
24
+ # @param payload [String] a JSON string containing the request payload
25
+ def initialize(integration, event, payload)
26
+ @integration = integration
27
+ @event = event
28
+ @payload = Payload.new payload
29
+ end
30
+
31
+ # Invokes the given block for each network this request needs to be
32
+ # delivered to.
33
+ #
34
+ # @yieldparam [Network] the network
35
+ # @yieldparam [Array<Channels>] channels that belong to the network and
36
+ # that a notification should be delivered to
37
+ def each_network
38
+ integration.channels.group_by(&:network).each do |network, channels|
39
+ yield network, channels
40
+ end
41
+ end
42
+
43
+ # Creates a message for a given channel from this request.
44
+ #
45
+ # @param channel [Channel] the channel
46
+ # @return [Message] the created message
47
+ def to_message_for(channel)
48
+ Message.new(channel, @event, @payload, @integration)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/cryptography'
4
+ require 'codebot/validation_error'
5
+
6
+ module Codebot
7
+ # This module provides data sanitization methods shared among multiple
8
+ # classes.
9
+ module Sanitizers
10
+ # Sanitizes an identifier.
11
+ #
12
+ # @param identifier [String, nil] the identifier to sanitize
13
+ # @return [String, nil] the sanitized value or +nil+ on error
14
+ def valid_identifier(identifier)
15
+ identifier.downcase if /\A[[:alnum:]_-]+\z/ =~ identifier
16
+ end
17
+
18
+ # Sanitizes an endpoint name.
19
+ #
20
+ # @param endpoint [String, nil] the endpoint name to sanitize
21
+ # @return [String, nil] the sanitized value or +nil+ on error
22
+ def valid_endpoint(endpoint)
23
+ endpoint if /\A[[:alnum:]_-]*\z/ =~ endpoint
24
+ end
25
+
26
+ # Sanitizes a webhook secret.
27
+ #
28
+ # @param secret [String, nil] the webhook secret to sanitize
29
+ # @return [String, nil] the sanitized value or +nil+ on error
30
+ def valid_secret(secret)
31
+ secret if /\A[[:print:]]*\z/ =~ secret
32
+ end
33
+
34
+ # Sanitizes a hostname.
35
+ #
36
+ # @param host [String, nil] the hostname to sanitize
37
+ # @return [String, nil] the sanitized value or +nil+ on error
38
+ def valid_host(host)
39
+ host if /\A[[:graph:]]+\z/ =~ host
40
+ end
41
+
42
+ # Sanitizes a TCP/IP port number.
43
+ #
44
+ # @param port [#to_i, #to_s] the port number to sanitize
45
+ # @return [Integer, nil] the sanitized value or +nil+ on error
46
+ def valid_port(port)
47
+ port_number = port.to_s.to_i(10) if /\A[0-9]+\z/ =~ port.to_s
48
+ port_number if (1...2**16).cover? port_number
49
+ end
50
+
51
+ # Sanitizes a boolean value.
52
+ #
53
+ # @param bool [Boolean, nil] the boolean to sanitize
54
+ # @return [Boolean, nil] the sanitized value or +nil+ on error
55
+ def valid_boolean(bool)
56
+ bool if [true, false].include? bool
57
+ end
58
+
59
+ # Sanitizes a string.
60
+ #
61
+ # @param str [String, nil] the string to sanitize
62
+ # @return [String, nil] the sanitized value or +nil+ on error
63
+ def valid_string(str)
64
+ str if str.is_a? String
65
+ end
66
+
67
+ # Sanitizes a channel name.
68
+ #
69
+ # @param channel [String, nil] the channel name to sanitize
70
+ # @return [String, nil] the sanitized value or +nil+ on error
71
+ def valid_channel_name(channel)
72
+ # Colons are currently not considered valid characters because some IRCds
73
+ # use them to delimit channel masks. This might change in the future.
74
+ channel if /\A[&#\+!][[:graph:]&&[^:,]]{,49}\z/ =~ channel
75
+ end
76
+
77
+ # Sanitizes a channel key.
78
+ #
79
+ # @param key [String, nil] the channel key to sanitize
80
+ # @return [String, nil] the sanitized value or +nil+ on error
81
+ def valid_channel_key(key)
82
+ key if /\A[[:graph:]&&[^,]]*\z/ =~ key
83
+ end
84
+
85
+ # Sanitizes a network name.
86
+ #
87
+ # @param name [String] the name of the network
88
+ # @param conf [Hash] the configuration containing all networks
89
+ # @return [Network, nil] the corresponding network or +nil+ on error
90
+ def valid_network(name, conf)
91
+ return if name.nil?
92
+
93
+ conf[:networks].find { |net| net.name_eql? name }
94
+ end
95
+
96
+ # This method requires a validation to succeed, raising an exception if it
97
+ # does not. If no original value was provided, it returns, in this order,
98
+ # the given fallback, the return value of any block passed to this method,
99
+ # or, finally, +nil+, unless the +:required+ option is set, in which case
100
+ # a +ValidationError+ is raised.
101
+ #
102
+ # @param original [Object] the original value
103
+ # @param sanitized [Object] the sanitized value
104
+ # @param fallback [Object] an optional symbol representing an instance
105
+ # variable to be returned when the original value
106
+ # is +nil+
107
+ # @raise [ValidationError] if the sanitization failed. The error message may
108
+ # be set using the +:invalid_error+ option.
109
+ # @raise [ValidationError] if the +:required+ option is set, but neither an
110
+ # original value nor a fallback value was specified
111
+ # and no block was given. The error message may be
112
+ # set using the +:required_error+ option.
113
+ # @param options [Hash] a hash optionally containing additional settings.
114
+ def valid!(original, sanitized, fallback = nil, options = {})
115
+ return sanitized unless sanitized.nil?
116
+ unless original.nil?
117
+ raise ValidationError, options[:invalid_error] % original.inspect
118
+ end
119
+ return instance_variable_get(fallback) if fallback_exist?(fallback)
120
+ return yield if block_given?
121
+ raise ValidationError, options[:required_error] if options[:required]
122
+ end
123
+
124
+ private
125
+
126
+ def fallback_exist?(fallback)
127
+ !fallback.nil? && instance_variable_defined?(fallback)
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/configuration_error'
4
+
5
+ module Codebot
6
+ # A class that can be serialized. Child classes should override the
7
+ # {#serialize} and {::deserialize} methods, and change {::serialize_as_hash?}
8
+ # to return +true+ if the {#serialize} method returns an +Array+ containing
9
+ # two elements representing key and value of a +Hash+.
10
+ class Serializable
11
+ # Serializes an array into an array or a hash.
12
+ #
13
+ # @param ary [Array] the data to serialize
14
+ # @param conf [Hash] the deserialized configuration
15
+ # @return [Array, Hash] the serialized data
16
+ def self.serialize_all(ary, conf)
17
+ data = ary.map { |entry| entry.serialize(conf) }
18
+ return data.to_h if serialize_as_hash?
19
+
20
+ data
21
+ end
22
+
23
+ # Deserializes an array or a hash into an array.
24
+ #
25
+ # @param data [Array, Hash] the data to deserialize
26
+ # @param conf [Hash] the previously deserialized configuration
27
+ # @return [Array] the deserialized data
28
+ def self.deserialize_all(data, conf)
29
+ return [] if data.nil?
30
+
31
+ if serialize_as_hash?
32
+ deserialize_all_from_hash(data, conf)
33
+ else
34
+ deserialize_all_from_array(data, conf)
35
+ end
36
+ end
37
+
38
+ # Serializes this object.
39
+ #
40
+ # @note Child classes should override this method.
41
+ # @param _conf [Hash] the deserialized configuration
42
+ # @return [Array, Hash] the serialized object
43
+ def serialize(_conf)
44
+ []
45
+ end
46
+
47
+ # Deserializes an object.
48
+ #
49
+ # @note Child classes should override this method.
50
+ # @param _key [Object] the hash key if the value was serialized into a hash
51
+ # @param _val [Hash] the serialized data
52
+ # @return [Hash] the parameters to pass to the initializer
53
+ def self.deserialize(_key = nil, _val)
54
+ {}
55
+ end
56
+
57
+ # Returns whether data is serialized into a hash rather than an array.
58
+ #
59
+ # @note Child classes might want to override this method.
60
+ # @return [Boolean] whether data is serialized into an array containing two
61
+ # elements representing key and value of a +Hash+.
62
+ def self.serialize_as_hash?
63
+ false
64
+ end
65
+
66
+ # Deserializes an array into an array.
67
+ #
68
+ # @param data [Array] the data to deserialize
69
+ # @param conf [Hash] the previously deserialized configuration
70
+ # @return [Array] the deserialized data
71
+ def self.deserialize_all_from_array(data, conf)
72
+ unless data.is_a? Array
73
+ raise ConfigurationError, "#{name}: invalid array #{data.inspect}"
74
+ end
75
+
76
+ data.map { |item| new(deserialize(item).merge(config: conf)) }
77
+ end
78
+
79
+ # Deserializes a hash into an array.
80
+ #
81
+ # @param data [Hash] the data to deserialize
82
+ # @param conf [Hash] the previously deserialized configuration
83
+ # @return [Array] the deserialized data
84
+ def self.deserialize_all_from_hash(data, conf)
85
+ unless data.is_a? Hash
86
+ raise ConfigurationError, "#{name}: invalid hash #{data.inspect}"
87
+ end
88
+
89
+ data.map do |item|
90
+ unless item.length == 2
91
+ raise ConfigurationError, "#{name}: invalid member #{item.inspect}"
92
+ end
93
+
94
+ new(deserialize(*item).merge(config: conf))
95
+ end
96
+ end
97
+
98
+ private_class_method :deserialize_all_from_array
99
+ private_class_method :deserialize_all_from_hash
100
+ end
101
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codebot
4
+ module Shortener
5
+ # Shortens URLs using Github shortener
6
+ class Github
7
+ # Shortens a URL with GitHub's git.io URL shortener. The domain
8
+ # must belong to GitHub.
9
+ #
10
+ # @param url [String] the long URL
11
+ # @return [String] the shortened URL, or the original URL if an error
12
+ # occurred.
13
+ def shorten_url(url)
14
+ return url if url.to_s.empty?
15
+
16
+ uri = URI('https://git.io')
17
+ res = Net::HTTP.post_form uri, 'url' => url.to_s
18
+ res['location'] || url.to_s
19
+ rescue StandardError
20
+ url.to_s
21
+ end
22
+ end
23
+
24
+ # Shortens URLs using a custom shortener
25
+ class Custom
26
+ def initialize(shortener_url, shortener_secret)
27
+ @shortener_url = URI(shortener_url)
28
+ @shortener_secret = shortener_secret
29
+ end
30
+
31
+ def shorten_url(url)
32
+ return url if url.to_s.empty?
33
+
34
+ res = Net::HTTP.post_form @shortener_url,
35
+ 'url' => url.to_s,
36
+ 'secret' => @shortener_secret
37
+ res.body.strip || url.to_s
38
+ rescue StandardError
39
+ url.to_s
40
+ end
41
+ end
42
+ end
43
+ end