collins_notify 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +15 -0
- data/README.rdoc +27 -0
- data/Rakefile +74 -0
- data/VERSION +1 -0
- data/bin/collins-notify +8 -0
- data/lib/collins_notify.rb +16 -0
- data/lib/collins_notify/adapter/email.rb +108 -0
- data/lib/collins_notify/adapter/helper/carried-pigeon.rb +213 -0
- data/lib/collins_notify/adapter/hipchat.rb +92 -0
- data/lib/collins_notify/adapter/irc.rb +88 -0
- data/lib/collins_notify/application.rb +55 -0
- data/lib/collins_notify/command_runner.rb +73 -0
- data/lib/collins_notify/configuration.rb +198 -0
- data/lib/collins_notify/configuration_mixin.rb +98 -0
- data/lib/collins_notify/errors.rb +4 -0
- data/lib/collins_notify/notifier.rb +165 -0
- data/lib/collins_notify/options.rb +128 -0
- data/lib/collins_notify/version.rb +19 -0
- data/sample_config.yaml +32 -0
- data/templates/default_email.erb +11 -0
- data/templates/default_email.html.erb +8 -0
- data/templates/default_hipchat.erb +1 -0
- data/templates/default_irc.erb +1 -0
- metadata +136 -0
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source :rubygems
|
2
|
+
|
3
|
+
gem 'collins_client', '~> 0.2.10'
|
4
|
+
gem 'hipchat', '~> 0.7.0'
|
5
|
+
gem 'mail', '~> 2.4.4'
|
6
|
+
gem 'nokogiri', '~> 1.5.2'
|
7
|
+
|
8
|
+
group :development do
|
9
|
+
gem "rspec", "~> 2.12.0"
|
10
|
+
gem "yard", "~> 0.8.3"
|
11
|
+
gem "rdoc", "~> 3.12"
|
12
|
+
gem "bundler", "~> 1.2.0"
|
13
|
+
gem "jeweler", "~> 1.8.4"
|
14
|
+
gem "simplecov"
|
15
|
+
end
|
data/README.rdoc
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
= collins-notify
|
2
|
+
|
3
|
+
CLI and library for sending templated (or not) notifications via email, hipchat,
|
4
|
+
and IRC.
|
5
|
+
|
6
|
+
== Exit Codes
|
7
|
+
|
8
|
+
Exit codes only apply to when you use the CommandRunner (via the collins-notify
|
9
|
+
CLI interface). Otherwise, exceptions and return values will indicate the status
|
10
|
+
of an operation.
|
11
|
+
|
12
|
+
* 0 - Success
|
13
|
+
* 1 - Invalid configuration value
|
14
|
+
* 2 - Could not parse CLI options
|
15
|
+
* 3 - Unable to run with specified configuration
|
16
|
+
* 4 - Timeout talking to remote service
|
17
|
+
* 5 - General error sending notification
|
18
|
+
|
19
|
+
== Contributing to collins-notify
|
20
|
+
|
21
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
22
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
23
|
+
* Fork the project.
|
24
|
+
* Start a feature/bugfix branch.
|
25
|
+
* Commit and push until you are happy with your contribution.
|
26
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
27
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
data/Rakefile
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
require 'jeweler'
|
14
|
+
require 'yard'
|
15
|
+
|
16
|
+
jeweler = Jeweler::Tasks.new do |gem|
|
17
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
18
|
+
gem.name = "collins_notify"
|
19
|
+
gem.homepage = "https://github.com/tumblr/collins/tree/master/support/ruby/collins-notify"
|
20
|
+
gem.license = "MIT"
|
21
|
+
gem.summary = %Q{Notifications for Collins}
|
22
|
+
gem.description = %Q{Send notifications via hipchat, IRC and email}
|
23
|
+
gem.email = "bmatheny@tumblr.com"
|
24
|
+
gem.authors = ["Blake Matheny"]
|
25
|
+
%w[config.yaml spec/**/* .gitignore .rspec .rvmrc .document .rbenv-version].each do |fp|
|
26
|
+
gem.files.exclude fp
|
27
|
+
end
|
28
|
+
gem.add_runtime_dependency 'collins_client', '~> 0.2.10'
|
29
|
+
gem.add_runtime_dependency 'hipchat', '~> 0.7.0'
|
30
|
+
gem.add_runtime_dependency 'mail', '~> 2.4.4'
|
31
|
+
gem.add_runtime_dependency 'nokogiri', '~> 1.5.2'
|
32
|
+
end
|
33
|
+
|
34
|
+
task :help do
|
35
|
+
puts("rake -T # See available rake tasks")
|
36
|
+
puts("rake spec # Run tests")
|
37
|
+
puts("rake make # version:bump:patch gemspec build")
|
38
|
+
puts("rake publish # run copy_gem")
|
39
|
+
puts("rake all # make then publish")
|
40
|
+
puts("rake # this help")
|
41
|
+
end
|
42
|
+
|
43
|
+
task :publish do
|
44
|
+
package_abs = jeweler.jeweler.gemspec_helper.gem_path
|
45
|
+
package_name = File.basename(package_abs)
|
46
|
+
|
47
|
+
puts "Run 'copy_gem #{package_name}'"
|
48
|
+
end
|
49
|
+
|
50
|
+
task :make => ["version:bump:patch", "gemspec", "build"] do
|
51
|
+
puts("Done!")
|
52
|
+
end
|
53
|
+
|
54
|
+
task :all => [:make, :publish]
|
55
|
+
|
56
|
+
require 'rspec/core'
|
57
|
+
require 'rspec/core/rake_task'
|
58
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
59
|
+
spec.fail_on_error = false
|
60
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
61
|
+
end
|
62
|
+
|
63
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
64
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
65
|
+
spec.rcov = true
|
66
|
+
end
|
67
|
+
|
68
|
+
task :default => :help
|
69
|
+
task :rspec => :spec
|
70
|
+
|
71
|
+
YARD::Rake::YardocTask.new do |t|
|
72
|
+
t.files = ['lib/**/*.rb']
|
73
|
+
t.options = ['--markup', 'markdown']
|
74
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/collins-notify
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'timeout'
|
5
|
+
require 'collins_client'
|
6
|
+
require 'collins_notify/errors'
|
7
|
+
require 'collins_notify/configuration_mixin'
|
8
|
+
require 'collins_notify/configuration'
|
9
|
+
require 'collins_notify/command_runner'
|
10
|
+
require 'collins_notify/options'
|
11
|
+
require 'collins_notify/version'
|
12
|
+
require 'collins_notify/notifier'
|
13
|
+
require 'collins_notify/adapter/email'
|
14
|
+
require 'collins_notify/adapter/hipchat'
|
15
|
+
require 'collins_notify/adapter/irc'
|
16
|
+
require 'collins_notify/application'
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
module CollinsNotify
|
4
|
+
class EmailAdapter < Notifier
|
5
|
+
register_name :email
|
6
|
+
supports_mimetype :text, :html
|
7
|
+
require_config
|
8
|
+
|
9
|
+
def configure!
|
10
|
+
mail_cfg = mail_options
|
11
|
+
del_meth = mail_cfg.fetch(:delivery_method, :smtp)
|
12
|
+
logger.info "Configuring email delivery method #{del_meth}"
|
13
|
+
logger.debug "Using email config values #{mail_cfg.inspect}"
|
14
|
+
Mail.defaults do
|
15
|
+
delivery_method del_meth, mail_cfg
|
16
|
+
end
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
# Available in template binding
|
21
|
+
# sender_address - Address of sender
|
22
|
+
# recipient_address - Resolved address of recipient
|
23
|
+
# subject - Mail subject
|
24
|
+
# message_obj - Depends on call
|
25
|
+
def notify! message_obj = OpenStruct.new, to = nil
|
26
|
+
sender_address = get_sender_address message_obj
|
27
|
+
recipient_address = get_recipient_address message_obj, to
|
28
|
+
subject = get_subject message_obj
|
29
|
+
message = get_message_body(binding)
|
30
|
+
subject = message.subject # Subject may change based on message template
|
31
|
+
(raise CollinsNotify::CollinsNotifyException.new("No subject specified")) if subject.nil?
|
32
|
+
logger.info "From: #{sender_address}, To: #{recipient_address}, Subject: #{subject}"
|
33
|
+
logger.trace "Message body:\n#{message}"
|
34
|
+
begin
|
35
|
+
mail = Mail.new message
|
36
|
+
logger.debug "Attempting to deliver email message"
|
37
|
+
if config.test? then
|
38
|
+
logger.info "Not delivering email, running in test mode"
|
39
|
+
else
|
40
|
+
logger.info "Delivering email message"
|
41
|
+
mail.deliver!
|
42
|
+
end
|
43
|
+
true
|
44
|
+
rescue Exception => e
|
45
|
+
logger.fatal "Error sending email - #{e}"
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
def get_recipient_address mo, to
|
52
|
+
if config.recipient then
|
53
|
+
make_address config.recipient
|
54
|
+
elsif to then
|
55
|
+
make_address to
|
56
|
+
elsif fetch_mo_option(mo, :recipient, nil) then
|
57
|
+
make_address fetch_mo_option(mo, :recipient, nil)
|
58
|
+
else
|
59
|
+
raise CollinsNotify::ConfigurationError.new "No email.recipient or config.recipient specified"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def get_sender_address mo
|
64
|
+
default = mail_options.fetch(:sender_address, "Notifier <notifier@example.com>")
|
65
|
+
fetch_mo_option(mo, :sender_address, default)
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_subject mo
|
69
|
+
default = mail_options.fetch(:subject, nil)
|
70
|
+
fetch_mo_option(mo, :subject, default)
|
71
|
+
end
|
72
|
+
|
73
|
+
def handle_html b, html, plain_text
|
74
|
+
is_html = !html.nil?
|
75
|
+
message = Mail::Message.new plain_text
|
76
|
+
if is_html then
|
77
|
+
message.body = ""
|
78
|
+
message.html_part = Mail::Part.new do
|
79
|
+
content_type 'text/html; charset=UTF-8'
|
80
|
+
body html
|
81
|
+
end
|
82
|
+
message.text_part = Mail::Part.new do
|
83
|
+
body plain_text
|
84
|
+
end
|
85
|
+
end
|
86
|
+
message[:from] = fetch_bound_option(b, "sender_address", "NOOP") unless message.from
|
87
|
+
message[:to] = fetch_bound_option(b, "recipient_address", "NOOP") unless message.to
|
88
|
+
message[:subject] = fetch_bound_option(b, "subject", "Collins Notifier") unless message.subject
|
89
|
+
message
|
90
|
+
end
|
91
|
+
|
92
|
+
def mail_options
|
93
|
+
@mail_options ||= symbolize_hash(deep_copy_hash(config.adapters[:email]))
|
94
|
+
end
|
95
|
+
|
96
|
+
def make_address address
|
97
|
+
if address.include?("@") then
|
98
|
+
return address
|
99
|
+
end
|
100
|
+
domain = "example.com"
|
101
|
+
if mail_options[:domain] then
|
102
|
+
domain = mail_options[:domain]
|
103
|
+
end
|
104
|
+
"#{address}@#{domain}"
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require "addressable/uri"
|
2
|
+
require "socket"
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
# Code is largely ripped off from https://github.com/portertech/carrier-pigeon which carriers an MIT
|
6
|
+
# license. Carrier pigeon offers no ability to detect a 433 response (nickname already in use) and
|
7
|
+
# automatically fix it. There's also not much in the way of debugging. That's all that is added
|
8
|
+
# here.
|
9
|
+
class CarriedPigeon
|
10
|
+
|
11
|
+
def self.send(options={})
|
12
|
+
raise "You must supply a message" unless options[:message]
|
13
|
+
if options[:uri] then
|
14
|
+
uri = Addressable::URI.parse(options[:uri])
|
15
|
+
options[:host] = uri.host
|
16
|
+
options[:port] = uri.port || 6667
|
17
|
+
options[:nick] = uri.user
|
18
|
+
options[:password] = uri.password
|
19
|
+
unless options[:channel] then
|
20
|
+
options[:channel] = "#" + uri.fragment
|
21
|
+
end
|
22
|
+
end
|
23
|
+
pigeon = CarriedPigeon.new options
|
24
|
+
pigeon.message options[:message], options[:notice]
|
25
|
+
pigeon.die
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :connected, :logger, :options
|
29
|
+
def initialize(options={})
|
30
|
+
[:host, :port, :nick, :channel, :logger].each do |option|
|
31
|
+
raise "You must provide an IRC #{option}" unless options.has_key?(option)
|
32
|
+
end
|
33
|
+
@logger = options[:logger]
|
34
|
+
@options = options
|
35
|
+
connect! if (options.fetch(:connect, true)) # default to connect, same as old behavior
|
36
|
+
end
|
37
|
+
|
38
|
+
def auth
|
39
|
+
# Only auth if not connected and password is specified
|
40
|
+
if connected? or !password then
|
41
|
+
return false
|
42
|
+
end
|
43
|
+
# Must be first according to RFC 2812
|
44
|
+
sendln "PASS #{password}", :log
|
45
|
+
sendln "USER #{nick} 0 * :#{real_name}", :log
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def connect!
|
50
|
+
return false if connected?
|
51
|
+
tcp_socket = TCPSocket.new(host, port)
|
52
|
+
if ssl? then
|
53
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
54
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
55
|
+
@socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
|
56
|
+
@socket.sync = true
|
57
|
+
@socket.sync_close = true
|
58
|
+
@socket.connect
|
59
|
+
else
|
60
|
+
@socket = tcp_socket
|
61
|
+
end
|
62
|
+
if auth? and not auth then
|
63
|
+
die
|
64
|
+
raise "Could not authenticate"
|
65
|
+
end
|
66
|
+
set_nick!
|
67
|
+
@connected = true
|
68
|
+
issue_nickserv_command
|
69
|
+
register
|
70
|
+
end
|
71
|
+
|
72
|
+
def message message, notice = false, opts = {}
|
73
|
+
command = notice ? "NOTICE" : "PRIVMSG"
|
74
|
+
# Reset join/channel/channel_password options if specified
|
75
|
+
options[:join] = opts[:join] if opts.key?(:join)
|
76
|
+
options[:channel] = opts[:channel] if opts.key?(:channel)
|
77
|
+
if opts.key?(:channel) then
|
78
|
+
options[:channel_password] = opts[:channel_password]
|
79
|
+
end
|
80
|
+
join!
|
81
|
+
sendln "#{command} #{channel} :#{message}", :log
|
82
|
+
end
|
83
|
+
|
84
|
+
def die
|
85
|
+
begin
|
86
|
+
sendln "QUIT :quit", :log
|
87
|
+
socket.gets until socket.eof?
|
88
|
+
rescue Exception => e
|
89
|
+
logger.error "Error quitting IRC - #{e}"
|
90
|
+
ensure
|
91
|
+
socket.close
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
protected
|
96
|
+
attr_accessor :socket
|
97
|
+
|
98
|
+
def auth?; options.key?(:password); end
|
99
|
+
def channel_password?; options.key?(:channel_password); end
|
100
|
+
def connected?; (connected == true); end
|
101
|
+
def join?; (options[:join] == true); end
|
102
|
+
def nickserv_command?; options.key?(:nickserv_command); end
|
103
|
+
def nickserv_password?; options.key?(:nickserv_password); end
|
104
|
+
def register_first?; (options[:register_first] == true); end
|
105
|
+
def ssl?; (options[:ssl] == true); end
|
106
|
+
|
107
|
+
def channel; options[:channel]; end
|
108
|
+
def channel_password; options[:channel_password]; end
|
109
|
+
def host; options[:host]; end
|
110
|
+
def mode; options.fetch(:mode, 0); end
|
111
|
+
def nick; options[:nick]; end
|
112
|
+
def nickserv_command; options[:nickserv_command]; end
|
113
|
+
def nickserv_password; options[:nickserv_password]; end
|
114
|
+
def password; options[:password]; end
|
115
|
+
def port; options[:port]; end
|
116
|
+
def real_name; options.fetch(:real_name, nick); end
|
117
|
+
|
118
|
+
# Where a response was an error or not
|
119
|
+
def error? response
|
120
|
+
rc = response_code response
|
121
|
+
rc >= 400 && rc < 600
|
122
|
+
end
|
123
|
+
|
124
|
+
# Generate a nickname, useful if yours is taken
|
125
|
+
def gen_nick name
|
126
|
+
names = name.split('_')
|
127
|
+
if names.size == 1 then
|
128
|
+
"#{name}_1"
|
129
|
+
elsif names.last.to_i > 0 then
|
130
|
+
num = names.pop.to_i
|
131
|
+
if num > 10 then
|
132
|
+
raise "Too many nicknames generated (10)"
|
133
|
+
end
|
134
|
+
"#{names.join('_')}_#{num + 1}"
|
135
|
+
else
|
136
|
+
"#{name}_1"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def issue_nickserv_command
|
141
|
+
if nickserv_password? then
|
142
|
+
sendln "PRIVMSG NICKSERV :IDENTIFY #{nickserv_password}"
|
143
|
+
elsif nickserv_command? then
|
144
|
+
sendln nickserv_command
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def join!
|
149
|
+
return unless join?
|
150
|
+
join = "JOIN #{channel}"
|
151
|
+
join += " #{channel_password}" if channel_password?
|
152
|
+
sendln join, :log
|
153
|
+
end
|
154
|
+
|
155
|
+
def log_response
|
156
|
+
response = socket.gets
|
157
|
+
if error? response then
|
158
|
+
logger.error "IRC response: #{response.strip}"
|
159
|
+
else
|
160
|
+
logger.trace "IRC response: #{response.strip}"
|
161
|
+
end
|
162
|
+
response
|
163
|
+
end
|
164
|
+
|
165
|
+
def register
|
166
|
+
if register_first?
|
167
|
+
while line = socket.gets
|
168
|
+
case line
|
169
|
+
when /^PING :(.+)$/i
|
170
|
+
sendln "PONG :#{$1}"
|
171
|
+
break
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# 0 on no response code, otherwise integer
|
178
|
+
def response_code response
|
179
|
+
response.to_s.split[1].to_i
|
180
|
+
end
|
181
|
+
|
182
|
+
# Send an IRC command to a socket, second arg can be one of :log or :none
|
183
|
+
# :log - log the response and get it
|
184
|
+
# :none - do nothing
|
185
|
+
def sendln cmd, log_or_get = :none
|
186
|
+
logger.trace "IRC command : #{cmd}"
|
187
|
+
r = socket.puts cmd
|
188
|
+
case log_or_get
|
189
|
+
when :log
|
190
|
+
log_response
|
191
|
+
else
|
192
|
+
r
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def set_nick! name = nil
|
197
|
+
if name.nil? then
|
198
|
+
name = nick
|
199
|
+
end
|
200
|
+
response = sendln "NICK #{name}", :log
|
201
|
+
rc = response_code response
|
202
|
+
if rc == 433 then
|
203
|
+
logger.info "Generating new NICK to connect with"
|
204
|
+
set_nick! gen_nick(name)
|
205
|
+
elsif error?(response) then
|
206
|
+
raise "Unable to set nick to #{name} - #{response}"
|
207
|
+
else
|
208
|
+
# Reset nick if needed
|
209
|
+
options[:nick] = name
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|