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,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/message'
4
+ require 'codebot/thread_controller'
5
+ require 'codebot/ext/cinch/ssl_extensions'
6
+ require 'cinch'
7
+ require 'cinch/plugins/identify'
8
+
9
+ module Codebot
10
+ # This class manages an IRC connection running in a separate thread.
11
+ class IRCConnection < ThreadController
12
+ # @return [Core] the bot this connection belongs to
13
+ attr_reader :core
14
+
15
+ # @return [Network] the connected network
16
+ attr_reader :network
17
+
18
+ # Constructs a new IRC connection.
19
+ #
20
+ # @param core [Core] the bot this connection belongs to
21
+ # @param network [Network] the network to connect to
22
+ def initialize(core, network)
23
+ @core = core
24
+ @network = network
25
+ @messages = Queue.new
26
+ @ready = Queue.new
27
+ end
28
+
29
+ # Schedules a message for delivery.
30
+ #
31
+ # @param message [Message] the message
32
+ def enqueue(message)
33
+ @messages << message
34
+ end
35
+
36
+ # Sets this connection to be available for delivering messages.
37
+ def set_ready!
38
+ @ready << true if @ready.empty?
39
+ end
40
+
41
+ # Starts a new managed thread if no thread is currently running.
42
+ # The thread invokes the +run+ method of the class that manages it.
43
+ #
44
+ # @return [Thread, nil] the newly created thread, or +nil+ if
45
+ # there was already a running thread
46
+ def start(*)
47
+ super(self)
48
+ end
49
+
50
+ def configure_nickserv_identification(net, conn)
51
+ return unless net.nickserv?
52
+
53
+ conn.plugins.plugins = [Cinch::Plugins::Identify]
54
+ conn.plugins.options[Cinch::Plugins::Identify] = {
55
+ username: nil_or_empty_string(net.nickserv_username),
56
+ password: net.nickserv_password,
57
+ type: :nickserv
58
+ }
59
+ end
60
+
61
+ private
62
+
63
+ # Starts this IRC thread.
64
+ #
65
+ # @param connection [IRCConnection] the connection the thread controls
66
+ def run(connection)
67
+ @connection = connection
68
+ bot = create_bot(connection)
69
+ thread = Thread.new { bot.start }
70
+ @ready.pop
71
+ loop { deliver bot, dequeue }
72
+ ensure
73
+ thread.exit unless thread.nil?
74
+ end
75
+
76
+ # Dequeue the next message.
77
+ #
78
+ # @return the message
79
+ def dequeue
80
+ @messages.pop
81
+ end
82
+
83
+ # Delivers a message to an IRC channel.
84
+ #
85
+ # @param bot [Cinch::Bot] the IRC bot
86
+ # @param message [Message] the message to deliver
87
+ def deliver(bot, message)
88
+ channel = bot.Channel(message.channel.name)
89
+ message.format.to_a.each do |msg|
90
+ channel.send msg
91
+ end
92
+ end
93
+
94
+ # Gets the list of channels associated with this network.
95
+ #
96
+ # @param config [Config] the configuration to search
97
+ # @param network [Network] the network to search for
98
+ # @return [Array<Channel>] the list of channels
99
+ def channels(config, network)
100
+ config.integrations.map(&:channels).flatten.select do |channel|
101
+ network == channel.network
102
+ end
103
+ end
104
+
105
+ # Gets the list of channel names and keys associated with this network.
106
+ # Each array element is a string containing either the channel name if no
107
+ # key is needed, or the channel name and key, separated by a space.
108
+ #
109
+ # @param config [Config] the configuration to search
110
+ # @param network [Network] the network to search for
111
+ # @return [Array<String>] the list of channel names and keys
112
+ def channel_array(config, network)
113
+ channels(config, network).map do |channel|
114
+ "#{channel.name} #{channel.key}".strip
115
+ end
116
+ end
117
+
118
+ def nil_or_empty_string(val)
119
+ if val.to_s.empty?
120
+ nil
121
+ else
122
+ val
123
+ end
124
+ end
125
+
126
+ # Constructs a new bot for the given IRC network.
127
+ #
128
+ # @param con [IRCConnection] the connection the thread controls
129
+ def create_bot(con) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
130
+ net = con.network
131
+ chan_ary = channel_array(con.core.config, network)
132
+ cc = self
133
+ Cinch::Bot.new do
134
+ configure do |c|
135
+ c.channels = chan_ary
136
+ c.local_host = net.bind
137
+ c.modes = net.modes.to_s.gsub(/\A\+/, '').chars.uniq
138
+ c.nick = net.nick
139
+ c.password = net.server_password
140
+ c.port = net.real_port
141
+ c.realname = Codebot::WEBSITE
142
+ if net.sasl?
143
+ c.sasl.username = net.sasl_username
144
+ c.sasl.password = net.sasl_password
145
+ end
146
+ c.server = net.host
147
+ c.ssl.use = net.secure
148
+ c.ssl.verify = net.secure
149
+ c.user = Codebot::PROJECT.downcase
150
+ cc.configure_nickserv_identification(net, c)
151
+ end
152
+ on(:join) { con.set_ready! }
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/payload'
4
+ require 'codebot/formatters'
5
+
6
+ module Codebot
7
+ # An IRC message generated by a {Request} and sent to a {Channel}.
8
+ class Message
9
+ # @return [Channel] the channel to send this message to
10
+ attr_reader :channel
11
+
12
+ # @return [Symbol] the event that caused this message to be sent
13
+ attr_reader :event
14
+
15
+ # @return [Payload] the parsed request payload
16
+ attr_reader :payload
17
+
18
+ # Constructs a new message for delivery to an IRC channel.
19
+ #
20
+ # @param channel [Channel] the channel to send this message to
21
+ # @param event [Symbol] the event that caused this message to be sent
22
+ # @param payload [Payload] the parsed request payload
23
+ def initialize(channel, event, payload, integration)
24
+ @channel = channel
25
+ @event = event
26
+ @payload = payload
27
+ @integration = integration
28
+ end
29
+
30
+ # Formats this message for delivery to an IRC channel.
31
+ #
32
+ # @return [Array<String>] the formatted IRC messages
33
+ def format
34
+ Formatters.format(@event, @payload.to_json, @integration)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codebot
4
+ # The project name.
5
+ PROJECT = 'Codebot'.freeze
6
+
7
+ # The current project version.
8
+ VERSION = '1.2.0'.freeze
9
+
10
+ # The project website.
11
+ WEBSITE = 'https://github.com/olabini/codebot'.freeze
12
+
13
+ # The URL to report issues with a message formatter to.
14
+ FORMATTER_ISSUE_URL = 'https://github.com/olabini/codebot/issues/new?template=formatter_issue.md'.freeze
15
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'codebot/sanitizers'
4
+ require 'codebot/serializable'
5
+
6
+ module Codebot
7
+ # This class represents an IRC network notifications can be delivered to.
8
+ class Network < Serializable # rubocop:disable Metrics/ClassLength
9
+ include Sanitizers
10
+
11
+ # @return [String] the name of this network
12
+ attr_reader :name
13
+
14
+ # @return [String] the hostname or IP address used for connecting to this
15
+ # network
16
+ attr_reader :host
17
+
18
+ # @return [Integer] the port used for connecting to this network
19
+ attr_reader :port
20
+
21
+ # @return [Boolean] whether TLS should be used when connecting to this
22
+ # network
23
+ attr_reader :secure
24
+
25
+ # @return [String] the server password
26
+ attr_reader :server_password
27
+
28
+ # @return [String] the primary nickname for this network
29
+ attr_reader :nick
30
+
31
+ # @return [String] the username for SASL authentication
32
+ attr_reader :sasl_username
33
+
34
+ # @return [String] the password for SASL authentication
35
+ attr_reader :sasl_password
36
+
37
+ # @return [String] the username for NickServ authentication
38
+ attr_reader :nickserv_username
39
+
40
+ # @return [String] the password for NickServ authentication
41
+ attr_reader :nickserv_password
42
+
43
+ # @return [String] the address to bind to
44
+ attr_reader :bind
45
+
46
+ # @return [String] user modes to set
47
+ attr_reader :modes
48
+
49
+ # Creates a new network from the supplied hash.
50
+ #
51
+ # @param params [Hash] A hash with symbolic keys representing the instance
52
+ # attributes of this network. The keys +:name+ and
53
+ # +:host+ are required.
54
+ def initialize(params)
55
+ update!(params)
56
+ end
57
+
58
+ # Updates the network from the supplied hash.
59
+ #
60
+ # @param params [Hash] A hash with symbolic keys representing the instance
61
+ # attributes of this network.
62
+ def update!(params)
63
+ self.name = params[:name]
64
+ self.server_password = params[:server_password]
65
+ self.nick = params[:nick]
66
+ self.bind = params[:bind]
67
+ self.modes = params[:modes]
68
+ update_complicated!(params)
69
+ end
70
+
71
+ def update_complicated!(params)
72
+ update_connection(params[:host], params[:port], params[:secure])
73
+ update_sasl(params[:disable_sasl],
74
+ params[:sasl_username], params[:sasl_password])
75
+ update_nickserv(params[:disable_nickserv],
76
+ params[:nickserv_username], params[:nickserv_password])
77
+ end
78
+
79
+ def name=(name)
80
+ @name = valid! name, valid_identifier(name), :@name,
81
+ required: true,
82
+ required_error: 'networks must have a name',
83
+ invalid_error: 'invalid network name %s'
84
+ end
85
+
86
+ # Updates the connection details of this network.
87
+ #
88
+ # @param host [String] the new hostname, or +nil+ to keep the current value
89
+ # @param port [Integer] the new port, or +nil+ to keep the current value
90
+ # @param secure [Boolean] whether to connect over TLS, or +nil+ to keep the
91
+ # current value
92
+ def update_connection(host, port, secure)
93
+ @host = valid! host, valid_host(host), :@host,
94
+ required: true,
95
+ required_error: 'networks must have a hostname',
96
+ invalid_error: 'invalid hostname %s'
97
+ @port = valid! port, valid_port(port), :@port,
98
+ invalid_error: 'invalid port number %s'
99
+ @secure = valid!(secure, valid_boolean(secure), :@secure,
100
+ invalid_error: 'secure must be a boolean') { false }
101
+ end
102
+
103
+ def server_password=(pass)
104
+ @server_password = valid! pass, valid_string(pass), :@server_password,
105
+ invalid_error: 'invalid server password %s'
106
+ end
107
+
108
+ def nick=(nick)
109
+ @nick = valid! nick, valid_string(nick), :@nick,
110
+ required: true,
111
+ required_error: "no nickname for #{@name.inspect} given",
112
+ invalid_error: 'invalid nickname %s'
113
+ end
114
+
115
+ # Updates the SASL authentication details of this network.
116
+ #
117
+ # @param disable [Boolean] whether to disable SASL, or +nil+ to keep the
118
+ # current value.
119
+ # @param user [String] the SASL username, or +nil+ to keep the current value
120
+ # @param pass [String] the SASL password, or +nil+ to keep the current value
121
+ def update_sasl(disable, user, pass)
122
+ @sasl_username = valid! user, valid_string(user), :@sasl_username,
123
+ invalid_error: 'invalid SASL username %s'
124
+ @sasl_password = valid! pass, valid_string(pass), :@sasl_password,
125
+ invalid_error: 'invalid SASL password %s'
126
+ return unless disable
127
+
128
+ @sasl_username = nil
129
+ @sasl_password = nil
130
+ end
131
+
132
+ # Updates the NickServ authentication details of this network.
133
+ #
134
+ # @param disable [Boolean] whether to disable NickServ, or +nil+ to keep the
135
+ # current value.
136
+ # @param user [String] the NickServ username, or +nil+ to keep the
137
+ # current value
138
+ # @param pass [String] the NickServ password, or +nil+ to keep the
139
+ # current value
140
+ def update_nickserv(disable, user, pass)
141
+ @nickserv_username = valid! user, valid_string(user), :@nickserv_username,
142
+ invalid_error: 'invalid NickServ username %s'
143
+ @nickserv_password = valid! pass, valid_string(pass), :@nickserv_password,
144
+ invalid_error: 'invalid NickServ password %s'
145
+ return unless disable
146
+
147
+ @nickserv_username = nil
148
+ @nickserv_password = nil
149
+ end
150
+
151
+ def bind=(bind)
152
+ @bind = valid! bind, valid_string(bind), :@bind,
153
+ invalid_error: 'invalid bind host %s'
154
+ end
155
+
156
+ def modes=(modes)
157
+ @modes = valid! modes, valid_string(modes), :@modes,
158
+ invalid_error: 'invalid user modes %s'
159
+ end
160
+
161
+ # Checks whether the name of this network is equal to another name.
162
+ #
163
+ # @param name [String] the other name
164
+ # @return [Boolean] +true+ if the names are equal, +false+ otherwise
165
+ def name_eql?(name)
166
+ @name.casecmp(name).zero?
167
+ end
168
+
169
+ # Returns the port used for connecting to this network, or the default port
170
+ # if no port is set.
171
+ #
172
+ # @return [Integer] the port
173
+ def real_port
174
+ port || (secure ? 6697 : 6667)
175
+ end
176
+
177
+ # Checks whether SASL is enabled for this network.
178
+ #
179
+ # @return [Boolean] whether SASL is enabled
180
+ def sasl?
181
+ !sasl_username.to_s.empty? && !sasl_password.to_s.empty?
182
+ end
183
+
184
+ # Checks whether NickServ is enabled for this network.
185
+ #
186
+ # @return [Boolean] whether NickServ is enabled
187
+ def nickserv?
188
+ !nickserv_username.to_s.empty? || !nickserv_password.to_s.empty?
189
+ end
190
+
191
+ # Checks whether this network is equal to another network.
192
+ #
193
+ # @param other [Object] the other network
194
+ # @return [Boolean] +true+ if the networks are equal, +false+ otherwise
195
+ def ==(other)
196
+ other.is_a?(Network) &&
197
+ name_eql?(other.name) &&
198
+ host.eql?(other.host) &&
199
+ port.eql?(other.port) &&
200
+ secure.eql?(other.secure)
201
+ end
202
+
203
+ # Generates a hash for this network.
204
+ #
205
+ # @return [Integer] the hash
206
+ def hash
207
+ [name, host, port, secure].hash
208
+ end
209
+
210
+ alias eql? ==
211
+
212
+ # Serializes this network.
213
+ #
214
+ # @param _conf [Hash] the deserialized configuration
215
+ # @return [Array, Hash] the serialized object
216
+ def serialize(_conf)
217
+ [name, Network.fields.map { |sym| [sym.to_s, send(sym)] }.to_h]
218
+ end
219
+
220
+ # Deserializes a network.
221
+ #
222
+ # @param name [String] the name of the network
223
+ # @param data [Hash] the serialized data
224
+ # @return [Hash] the parameters to pass to the initializer
225
+ def self.deserialize(name, data)
226
+ fields.map { |sym| [sym, data[sym.to_s]] }.to_h.merge(name: name)
227
+ end
228
+
229
+ # @return [true] to indicate that data is serialized into a hash
230
+ def self.serialize_as_hash?
231
+ true
232
+ end
233
+
234
+ # @return [Array<Symbol>] the fields used for serializing this network
235
+ def self.fields
236
+ %i[host port secure server_password nick sasl_username sasl_password
237
+ nickserv_username nickserv_password bind modes]
238
+ end
239
+ end
240
+ end