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