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,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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'codebot'
6
+ require 'irb'
7
+
8
+ IRB.start(__FILE__)
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'codebot'
5
+ require 'codebot/user_error'
6
+
7
+ Codebot::Options::Base.start(ARGV)
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is the main module for Codebot.
4
+ module Codebot end
5
+
6
+ require 'codebot/metadata'
7
+ require 'codebot/options'
8
+ require 'codebot/core'
@@ -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
@@ -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