codebot 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE.md +32 -0
- data/.github/ISSUE_TEMPLATE/formatter_issue.md +20 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +26 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +75 -0
- data/LICENSE +21 -0
- data/README.md +230 -0
- data/Rakefile +29 -0
- data/bin/console +8 -0
- data/codebot.gemspec +49 -0
- data/exe/codebot +7 -0
- data/lib/codebot.rb +8 -0
- data/lib/codebot/channel.rb +134 -0
- data/lib/codebot/command_error.rb +17 -0
- data/lib/codebot/config.rb +125 -0
- data/lib/codebot/configuration_error.rb +17 -0
- data/lib/codebot/core.rb +76 -0
- data/lib/codebot/cryptography.rb +38 -0
- data/lib/codebot/event.rb +62 -0
- data/lib/codebot/ext/cinch/ssl_extensions.rb +37 -0
- data/lib/codebot/formatter.rb +242 -0
- data/lib/codebot/formatters.rb +109 -0
- data/lib/codebot/formatters/.rubocop.yml +2 -0
- data/lib/codebot/formatters/commit_comment.rb +43 -0
- data/lib/codebot/formatters/fork.rb +40 -0
- data/lib/codebot/formatters/gitlab_issue_hook.rb +56 -0
- data/lib/codebot/formatters/gitlab_job_hook.rb +77 -0
- data/lib/codebot/formatters/gitlab_merge_request_hook.rb +57 -0
- data/lib/codebot/formatters/gitlab_note_hook.rb +119 -0
- data/lib/codebot/formatters/gitlab_pipeline_hook.rb +51 -0
- data/lib/codebot/formatters/gitlab_push_hook.rb +83 -0
- data/lib/codebot/formatters/gitlab_wiki_page_hook.rb +56 -0
- data/lib/codebot/formatters/gollum.rb +67 -0
- data/lib/codebot/formatters/issue_comment.rb +41 -0
- data/lib/codebot/formatters/issues.rb +41 -0
- data/lib/codebot/formatters/ping.rb +79 -0
- data/lib/codebot/formatters/public.rb +30 -0
- data/lib/codebot/formatters/pull_request.rb +71 -0
- data/lib/codebot/formatters/pull_request_review_comment.rb +49 -0
- data/lib/codebot/formatters/push.rb +172 -0
- data/lib/codebot/formatters/watch.rb +38 -0
- data/lib/codebot/integration.rb +195 -0
- data/lib/codebot/integration_manager.rb +225 -0
- data/lib/codebot/ipc_client.rb +83 -0
- data/lib/codebot/ipc_server.rb +79 -0
- data/lib/codebot/irc_client.rb +102 -0
- data/lib/codebot/irc_connection.rb +156 -0
- data/lib/codebot/message.rb +37 -0
- data/lib/codebot/metadata.rb +15 -0
- data/lib/codebot/network.rb +240 -0
- data/lib/codebot/network_manager.rb +181 -0
- data/lib/codebot/options.rb +49 -0
- data/lib/codebot/options/base.rb +55 -0
- data/lib/codebot/options/core.rb +126 -0
- data/lib/codebot/options/integration.rb +101 -0
- data/lib/codebot/options/network.rb +109 -0
- data/lib/codebot/payload.rb +32 -0
- data/lib/codebot/request.rb +51 -0
- data/lib/codebot/sanitizers.rb +130 -0
- data/lib/codebot/serializable.rb +101 -0
- data/lib/codebot/shortener.rb +43 -0
- data/lib/codebot/thread_controller.rb +70 -0
- data/lib/codebot/user_error.rb +13 -0
- data/lib/codebot/validation_error.rb +17 -0
- data/lib/codebot/web_listener.rb +107 -0
- data/lib/codebot/web_server.rb +58 -0
- data/webhook.png +0 -0
- 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
|