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