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