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
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
require 'rake'
|
6
|
+
require 'rspec/core'
|
7
|
+
require 'rspec/core/rake_task'
|
8
|
+
require 'rubocop/rake_task'
|
9
|
+
|
10
|
+
task default: :check
|
11
|
+
|
12
|
+
begin
|
13
|
+
Bundler.setup :development
|
14
|
+
rescue Bundler::BundlerError => e
|
15
|
+
warn e.message
|
16
|
+
warn 'Run `bundle install` to install missing gems.'
|
17
|
+
exit e.status_code
|
18
|
+
end
|
19
|
+
|
20
|
+
task :check do
|
21
|
+
Rake::Task[:spec].execute
|
22
|
+
Rake::Task[:lint].execute
|
23
|
+
end
|
24
|
+
|
25
|
+
RSpec::Core::RakeTask.new :spec do |task|
|
26
|
+
task.ruby_opts = '-E UTF-8'
|
27
|
+
end
|
28
|
+
|
29
|
+
RuboCop::RakeTask.new :lint
|
data/bin/console
ADDED
data/codebot.gemspec
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'codebot/metadata'
|
7
|
+
|
8
|
+
def description
|
9
|
+
text = <<-DESCRIPTION
|
10
|
+
Codebot is an IRC bot that receives GitHub webhooks and forwards them to
|
11
|
+
IRC channels. It is designed to send messages in a format similar to that
|
12
|
+
of the official GitHub IRC Service. Codebot is able to stay connected after
|
13
|
+
sending messages. This eliminates the delays and visual clutter caused by
|
14
|
+
reconnecting each time a new message has to be delivered.
|
15
|
+
DESCRIPTION
|
16
|
+
text.gsub(/\s+/, ' ').strip
|
17
|
+
end
|
18
|
+
|
19
|
+
Gem::Specification.new do |spec|
|
20
|
+
spec.name = 'codebot'
|
21
|
+
spec.version = Codebot::VERSION
|
22
|
+
spec.authors = ['Janik Rabe', 'Ola Bini']
|
23
|
+
spec.email = ['codebot@olabini.se']
|
24
|
+
spec.summary = 'Forward GitHub webhooks to IRC channels'
|
25
|
+
spec.description = description
|
26
|
+
spec.homepage = 'https://github.com/olabini/codebot'
|
27
|
+
|
28
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
29
|
+
f.match(%r{^(test|spec|features)/})
|
30
|
+
end
|
31
|
+
|
32
|
+
spec.bindir = 'exe'
|
33
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
34
|
+
spec.license = 'MIT'
|
35
|
+
spec.require_paths = ['lib']
|
36
|
+
|
37
|
+
spec.required_ruby_version = '>= 2.2.0'
|
38
|
+
|
39
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
40
|
+
spec.add_development_dependency 'rake', '~> 12.3'
|
41
|
+
spec.add_development_dependency 'rspec', '~> 3.8'
|
42
|
+
spec.add_development_dependency 'rubocop', '~> 0.65.0'
|
43
|
+
|
44
|
+
spec.add_runtime_dependency 'cinch', '~> 2.3'
|
45
|
+
spec.add_runtime_dependency 'cinch-identify', '~> 1.7'
|
46
|
+
spec.add_runtime_dependency 'irb', '~> 1.0'
|
47
|
+
spec.add_runtime_dependency 'sinatra', '~> 2.0'
|
48
|
+
spec.add_runtime_dependency 'thor', '~> 0.20.0'
|
49
|
+
end
|
data/exe/codebot
ADDED
data/lib/codebot.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'codebot/sanitizers'
|
4
|
+
require 'codebot/serializable'
|
5
|
+
|
6
|
+
module Codebot
|
7
|
+
# This class represents an IRC channel notifications can be delivered to.
|
8
|
+
class Channel < Serializable
|
9
|
+
include Sanitizers
|
10
|
+
|
11
|
+
# @return [String] the name of this channel
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# @return [Network] the network this channel belongs to
|
15
|
+
attr_reader :network
|
16
|
+
|
17
|
+
# @return [String, nil] the key required for joining this channel
|
18
|
+
attr_reader :key
|
19
|
+
|
20
|
+
# @return [Boolean] whether to send messages without joining this channel
|
21
|
+
attr_reader :send_external
|
22
|
+
|
23
|
+
# Creates a new channel from the supplied hash.
|
24
|
+
#
|
25
|
+
# @param params [Hash] A hash with symbolic keys representing the instance
|
26
|
+
# attributes of this channel. The keys +:name+ and
|
27
|
+
# +:network+ are required. Alternatively, the key
|
28
|
+
# +:identifier+, which should use the format
|
29
|
+
# +network/name+, can be specified.
|
30
|
+
def initialize(params)
|
31
|
+
update!(params)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Updates the channel from the supplied hash.
|
35
|
+
#
|
36
|
+
# @param params [Hash] A hash with symbolic keys representing the instance
|
37
|
+
# attributes of this channel.
|
38
|
+
def update!(params)
|
39
|
+
set_identifier params[:identifier], params[:config] if params[:identifier]
|
40
|
+
self.name = params[:name]
|
41
|
+
self.key = params[:key]
|
42
|
+
self.send_external = params[:send_external]
|
43
|
+
set_network params[:network], params[:config]
|
44
|
+
end
|
45
|
+
|
46
|
+
def name=(name)
|
47
|
+
@name = valid! name, valid_channel_key(name), :@name,
|
48
|
+
required: true,
|
49
|
+
required_error: 'channels must have a name',
|
50
|
+
invalid_error: 'invalid channel name %s'
|
51
|
+
end
|
52
|
+
|
53
|
+
# Sets the network for this channel.
|
54
|
+
#
|
55
|
+
# @param network [String] the name of the network
|
56
|
+
# @param conf [Hash] the configuration containing all networks
|
57
|
+
def set_network(network, conf)
|
58
|
+
@network = valid! network, valid_network(network, conf), :@network,
|
59
|
+
required: true,
|
60
|
+
required_error: 'channels must have a network',
|
61
|
+
invalid_error: 'invalid channel network %s'
|
62
|
+
end
|
63
|
+
|
64
|
+
def key=(key)
|
65
|
+
@key = valid! key, valid_channel_key(key), :@key,
|
66
|
+
invalid_error: 'invalid channel key %s'
|
67
|
+
end
|
68
|
+
|
69
|
+
def key?
|
70
|
+
!key.to_s.strip.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
def send_external=(send_external)
|
74
|
+
@send_external = valid!(send_external, valid_boolean(send_external),
|
75
|
+
:@send_external,
|
76
|
+
invalid_error: 'send_external must be a ' \
|
77
|
+
'boolean') { false }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Checks whether the identifier of this channel is equal to another
|
81
|
+
# identifier.
|
82
|
+
#
|
83
|
+
# @param identifier [String] the other identifier
|
84
|
+
# @return [Boolean] +true+ if the names are equal, +false+ otherwise
|
85
|
+
def identifier_eql?(identifier)
|
86
|
+
self.identifier.casecmp(identifier).zero?
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the string used to identify this channel in configuration files.
|
90
|
+
#
|
91
|
+
# @return [String] the identifier
|
92
|
+
def identifier
|
93
|
+
"#{@network.name}/#{@name}"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Sets network and channel name based on the given identifier.
|
97
|
+
#
|
98
|
+
# @param identifier [String] the identifier
|
99
|
+
# @param conf [Hash] the configuration containing all networks
|
100
|
+
def set_identifier(identifier, conf)
|
101
|
+
network_name, self.name = identifier.split('/', 2) if identifier
|
102
|
+
set_network(network_name, conf)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Serializes this channel.
|
106
|
+
#
|
107
|
+
# @param _conf [Hash] the deserialized configuration
|
108
|
+
# @return [Array, Hash] the serialized object
|
109
|
+
def serialize(_conf)
|
110
|
+
[identifier, {
|
111
|
+
'key' => key,
|
112
|
+
'send_external' => send_external
|
113
|
+
}]
|
114
|
+
end
|
115
|
+
|
116
|
+
# Deserializes a channel.
|
117
|
+
#
|
118
|
+
# @param identifier [String] the channel identifier
|
119
|
+
# @param data [Hash] the serialized data
|
120
|
+
# @return [Hash] the parameters to pass to the initializer
|
121
|
+
def self.deserialize(identifier, data)
|
122
|
+
{
|
123
|
+
identifier: identifier,
|
124
|
+
key: data['key'],
|
125
|
+
send_external: data['send_external']
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return [true] to indicate that data is serialized into a hash
|
130
|
+
def self.serialize_as_hash?
|
131
|
+
true
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'codebot/user_error'
|
4
|
+
|
5
|
+
module Codebot
|
6
|
+
# This exception stores information about an error that occurred due to the
|
7
|
+
# user entering an invalid command, for example when two mutually exclusive
|
8
|
+
# command-line options are specified.
|
9
|
+
class CommandError < UserError
|
10
|
+
# Constructs a new command error.
|
11
|
+
#
|
12
|
+
# @param message [String] the error message
|
13
|
+
def initialize(message)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'psych'
|
4
|
+
|
5
|
+
require 'codebot/channel'
|
6
|
+
require 'codebot/integration'
|
7
|
+
require 'codebot/network'
|
8
|
+
|
9
|
+
module Codebot
|
10
|
+
# This class manages a Codebot configuration file.
|
11
|
+
class Config
|
12
|
+
# @return [Core] the bot this configuration belongs to
|
13
|
+
attr_reader :core
|
14
|
+
|
15
|
+
# @return [String] the path to the managed configuration file
|
16
|
+
attr_reader :file
|
17
|
+
|
18
|
+
# Creates a new instance of the class and loads the configuration file.
|
19
|
+
#
|
20
|
+
# @param core [Core] the bot this configuration belongs to
|
21
|
+
# @param file [String] the path to the configuration file, or +nil+ to
|
22
|
+
# use the default configuration file for this user
|
23
|
+
def initialize(core, file = nil)
|
24
|
+
@core = core
|
25
|
+
@file = file || self.class.default_file
|
26
|
+
@semaphore = Mutex.new
|
27
|
+
unsafe_load
|
28
|
+
end
|
29
|
+
|
30
|
+
# Loads the configuration from the associated file. If the file does not
|
31
|
+
# exist, it is created.
|
32
|
+
def load!
|
33
|
+
transaction { unsafe_load }
|
34
|
+
end
|
35
|
+
|
36
|
+
# A thread-safe method for making changes to the configuration. If another
|
37
|
+
# transaction is active, the calling thread waits for it to complete.
|
38
|
+
# If a +StandardError+ occurs during the transaction, the configuration
|
39
|
+
# is rolled back to the previous state.
|
40
|
+
#
|
41
|
+
# @yield invokes the given block during the transaction
|
42
|
+
# @raise [StandardError] the error that occurred during modification
|
43
|
+
# @return [true] if the transaction completes successfully
|
44
|
+
def transaction
|
45
|
+
@semaphore.synchronize do
|
46
|
+
state = @conf
|
47
|
+
begin
|
48
|
+
run_transaction(&Proc.new)
|
49
|
+
rescue StandardError
|
50
|
+
@conf = state
|
51
|
+
raise
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Array] the integrations contained in this configuration
|
57
|
+
def integrations
|
58
|
+
@conf[:integrations]
|
59
|
+
end
|
60
|
+
|
61
|
+
# @return [Array] the networks contained in this configuration
|
62
|
+
def networks
|
63
|
+
@conf[:networks]
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the path to the default configuration file for the current user.
|
67
|
+
#
|
68
|
+
# @return [String] the path to the configuration file
|
69
|
+
def self.default_file
|
70
|
+
File.join Dir.home, '.codebot.yml'
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Saves the current configuration to the configuration file.
|
76
|
+
def save!
|
77
|
+
save_to_file! @file
|
78
|
+
end
|
79
|
+
|
80
|
+
# Loads the configuration file without starting a transaction.
|
81
|
+
def unsafe_load
|
82
|
+
@conf = load_from_file! @file
|
83
|
+
save! unless File.file? @file
|
84
|
+
end
|
85
|
+
|
86
|
+
# Makes changes to the configuration, saves the file and requests that the
|
87
|
+
# bot migrate to the new version.
|
88
|
+
#
|
89
|
+
# @note This method should only be called by the {#transaction} method.
|
90
|
+
# @return [Boolean] +true+ if the transaction succeeded, +false+ otherwise
|
91
|
+
def run_transaction
|
92
|
+
yield
|
93
|
+
return false unless save!
|
94
|
+
|
95
|
+
@core.migrate!
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
# Loads the configuration from the specified file.
|
100
|
+
#
|
101
|
+
# @param file [String] the path to the configuration file
|
102
|
+
# @return [Hash] the loaded configuration, or the default configuration if
|
103
|
+
# the file did not exist
|
104
|
+
def load_from_file!(file)
|
105
|
+
data = Psych.safe_load(File.read(file)) if File.file? file
|
106
|
+
data = {} unless data.is_a? Hash
|
107
|
+
conf = {}
|
108
|
+
conf[:networks] = Network.deserialize_all data['networks'], conf
|
109
|
+
conf[:integrations] = Integration.deserialize_all data['integrations'],
|
110
|
+
conf
|
111
|
+
conf
|
112
|
+
end
|
113
|
+
|
114
|
+
# Saves the configuration to the specified file.
|
115
|
+
#
|
116
|
+
# @param file [String] the path to the configuration file
|
117
|
+
def save_to_file!(file)
|
118
|
+
data = {}
|
119
|
+
data['networks'] = Network.serialize_all @conf[:networks], @conf
|
120
|
+
data['integrations'] = Integration.serialize_all @conf[:integrations],
|
121
|
+
@conf
|
122
|
+
File.write file, Psych.dump(data)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'codebot/user_error'
|
4
|
+
|
5
|
+
module Codebot
|
6
|
+
# This exception stores information about an error that occurred due to an
|
7
|
+
# invalid configuration file, for example when an entry has the wrong data
|
8
|
+
# type.
|
9
|
+
class ConfigurationError < UserError
|
10
|
+
# Constructs a new configuration error.
|
11
|
+
#
|
12
|
+
# @param message [String] the error message
|
13
|
+
def initialize(message)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/codebot/core.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'codebot/config'
|
4
|
+
require 'codebot/irc_client'
|
5
|
+
require 'codebot/web_server'
|
6
|
+
require 'codebot/ipc_server'
|
7
|
+
|
8
|
+
module Codebot
|
9
|
+
# This class represents a {Codebot} bot.
|
10
|
+
class Core
|
11
|
+
# @return [Config] the configuration used by this bot
|
12
|
+
attr_reader :config
|
13
|
+
|
14
|
+
# @return [IRCClient] the IRC client
|
15
|
+
attr_reader :irc_client
|
16
|
+
|
17
|
+
# @return [IRCClient] the web server
|
18
|
+
attr_reader :web_server
|
19
|
+
|
20
|
+
# @return [IRCClient] the IPC server
|
21
|
+
attr_reader :ipc_server
|
22
|
+
|
23
|
+
# Creates a new bot from the supplied hash.
|
24
|
+
#
|
25
|
+
# @param params [Hash] A hash with symbolic keys for initializing this
|
26
|
+
# instance. The only accepted keys are +:config_file+
|
27
|
+
# and +:ipc_pipe+. Any other keys are ignored.
|
28
|
+
def initialize(params = {})
|
29
|
+
@config = Config.new(self, params[:config_file])
|
30
|
+
@irc_client = IRCClient.new(self)
|
31
|
+
@web_server = WebServer.new(self)
|
32
|
+
@ipc_server = IPCServer.new(self, params[:ipc_pipe])
|
33
|
+
end
|
34
|
+
|
35
|
+
# Starts this bot.
|
36
|
+
def start
|
37
|
+
@irc_client.start
|
38
|
+
@web_server.start
|
39
|
+
@ipc_server.start
|
40
|
+
end
|
41
|
+
|
42
|
+
# Stops this bot.
|
43
|
+
def stop
|
44
|
+
@ipc_server.stop
|
45
|
+
@web_server.stop
|
46
|
+
@irc_client.stop
|
47
|
+
end
|
48
|
+
|
49
|
+
# Waits for this bot to stop. If any of the managed threads finish early,
|
50
|
+
# the bot is shut down immediately.
|
51
|
+
def join
|
52
|
+
ipc = Thread.new { @ipc_server.join && stop }
|
53
|
+
web = Thread.new { @web_server.join && stop }
|
54
|
+
ipc.join
|
55
|
+
web.join
|
56
|
+
@irc_client.join
|
57
|
+
end
|
58
|
+
|
59
|
+
# Requests that the running threads migrate to an updated configuration.
|
60
|
+
def migrate!
|
61
|
+
@irc_client.migrate! unless @irc_client.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
# Sets traps for SIGINT and SIGTERM so Codebot can shut down gracefully.
|
65
|
+
def trap_signals
|
66
|
+
shutdown = proc do |signal|
|
67
|
+
STDERR.puts "\nReceived #{signal}, shutting down..."
|
68
|
+
stop
|
69
|
+
join
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
trap('INT') { shutdown.call 'SIGINT' }
|
73
|
+
trap('TERM') { shutdown.call 'SIGTERM' }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|