codebot 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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