collins_notify 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'collins_notify'
6
+ require 'pp'
7
+
8
+ CollinsNotify::CommandRunner.new.run ARGV
@@ -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