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