class Growl::GNTP
Growl Notification Transport Protocol 1.0
In growl 1.3, GNTP replaced the UDP growl protocol from earlier versions. GNTP has some new features beyond those supported in earlier versions including:
-
Callback support
-
Notification icons
-
Encrypted notifications (not supported by growl at this time)
Notably, subscription support is not implemented.
This implementation is based on information from www.growlforwindows.com/gfw/help/gntp.aspx
Constants
Attributes
Enables encryption for request bodies.
Note that this does not appear to be supported in a released version of growl.
Sets the application icon
The icon may be any image NSImage supports
Hash of notifications registered with the server
Password for authenticating and encrypting requests. If this is set, authentication automatically takes place.
Public Class Methods
Creates a new Growl::GNTP instance that will
communicate with host
and has the given
application
name, and will send the given
notification_names
.
If you wish to set icons or display names for notifications, use #add_notification instead of
sending notification_names
.
# File lib/ruby-growl/gntp.rb, line 181 def initialize host, application, notification_names = nil @host = host @application = application @notifications = {} @uuid = UUID.new notification_names.each do |name| add_notification name end if notification_names @encrypt = 'NONE' @password = nil @icon = nil end
Public Instance Methods
Adds a notification with name
(internal) and
display_name
(shown to user). The icon
map be an
image (anything NSImage supports) or a URI (which is unsupported in growl
1.3). If the notification is enabled
it will be displayed by
default.
# File lib/ruby-growl/gntp.rb, line 202 def add_notification name, display_name = nil, icon = nil, enabled = true @notifications[name] = display_name, icon, enabled end
Creates a symmetric encryption cipher for key
based on the encrypt method.
# File lib/ruby-growl/gntp.rb, line 210 def cipher key, iv = nil algorithm = ENCRYPTION_ALGORITHMS[@encrypt] raise Error, "unknown GNTP encryption mode #{@encrypt}" unless algorithm cipher = OpenSSL::Cipher.new algorithm cipher.encrypt cipher.key = key if iv then cipher.iv = iv else iv = cipher.random_iv end return cipher, iv end
Creates a TCP connection to the chosen host
# File lib/ruby-growl/gntp.rb, line 232 def connect TCPSocket.new @host, PORT end
Returns an encryption key, authentication hash and random salt for the
given hash algorithm
.
# File lib/ruby-growl/gntp.rb, line 240 def key_hash algorithm key = @password.dup.force_encoding Encoding::BINARY salt = self.salt basis = "#{key}#{salt}" key = algorithm.digest basis hash = algorithm.hexdigest key return key, hash, salt end
Sends a notification
with the given title
and
text
. The priority
may be between -2 (lowest)
and 2 (highest). sticky
will indicate the notification must
be manually dismissed. callback_url
is supposed to open the
given URL on the server's web browser when clicked, but I haven't
seen this work.
If a block is given, it is called when the notification is clicked, times out, or is manually dismissed.
# File lib/ruby-growl/gntp.rb, line 262 def notify(notification, title, text = nil, priority = 0, sticky = false, coalesce_id = nil, callback_url = nil, &block) raise ArgumentError, 'provide either a url or a block for callbacks, ' 'not both' if block and callback_url callback = callback_url || block_given? packet = packet_notify(notification, title, text, priority, sticky, coalesce_id, callback) send packet, &block end
Creates a type
packet (such as REGISTER or NOTIFY) with the
given headers
and resources
. Handles
authentication and encryption of the packet.
# File lib/ruby-growl/gntp.rb, line 281 def packet type, headers, resources = {} packet = [] body = [] body << "Application-Name: #{@application}" body << "Origin-Software-Name: ruby-growl" body << "Origin-Software-Version: #{Growl::VERSION}" body << "Origin-Platform-Name: ruby" body << "Origin-Platform-Version: #{RUBY_VERSION}" body << "Connection: close" body.concat headers body << nil body = body.join "\r\n" if @password then digest = Digest::SHA512 key, hash, salt = key_hash digest key_info = "SHA512:#{hash}.#{Digest.hexencode salt}" end if @encrypt == 'NONE' then packet << ["GNTP/1.0", type, "NONE", key_info].compact.join(' ') packet << body.force_encoding("ASCII-8BIT") else encipher, iv = cipher key encrypt_info = "#{@encrypt}:#{Digest.hexencode iv}" packet << "GNTP/1.0 #{type} #{encrypt_info} #{key_info}" encrypted = encipher.update body encrypted << encipher.final packet << encrypted end resources.each do |id, data| if iv then encipher, = cipher key, iv encrypted = encipher.update data encrypted << encipher.final data = encrypted end packet << "Identifier: #{id}" packet << "Length: #{data.length}" packet << nil packet << data packet << nil end packet << nil packet << nil packet.join "\r\n" end
Creates a notify packet. See notify for parameter details.
# File lib/ruby-growl/gntp.rb, line 343 def packet_notify(notification, title, text, priority, sticky, coalesce_id, callback) raise ArgumentError, "invalid priority level #{priority}" unless priority >= -2 and priority <= 2 resources = {} _, icon, = @notifications[notification] if URI === icon then icon_uri = icon elsif icon then id = @uuid.generate resources[id] = icon end headers = [] headers << "Notification-ID: #{@uuid.generate}" headers << "Notification-Coalescing-ID: #{coalesce_id}" if coalesce_id headers << "Notification-Name: #{notification}" headers << "Notification-Title: #{title}" headers << "Notification-Text: #{text}" if text headers << "Notification-Priority: #{priority}" if priority.nonzero? headers << "Notification-Sticky: True" if sticky headers << "Notification-Icon: #{icon}" if icon_uri headers << "Notification-Icon: x-growl-resource://#{id}" if id if callback then headers << "Notification-Callback-Context: context" headers << "Notification-Callback-Context-Type: type" headers << "Notification-Callback-Target: #{callback}" unless callback == true end packet :NOTIFY, headers, resources end
Creates a registration packet
# File lib/ruby-growl/gntp.rb, line 383 def packet_register resources = {} headers = [] case @icon when URI then headers << "Application-Icon: #{@icon}" when NilClass then # ignore else app_icon_id = @uuid.generate headers << "Application-Icon: x-growl-resource://#{app_icon_id}" resources[app_icon_id] = @icon end headers << "Notifications-Count: #{@notifications.length}" headers << nil @notifications.each do |name, (display_name, icon, enabled)| headers << "Notification-Name: #{name}" headers << "Notification-Display-Name: #{display_name}" if display_name headers << "Notification-Enabled: true" if enabled # This does not appear to be used by growl so ruby-growl sends the # icon with every notification. if URI === icon then headers << "Notification-Icon: #{icon}" elsif icon then id = @uuid.generate headers << "Notification-Icon: x-growl-resource://#{id}" resources[id] = icon end headers << nil end headers.pop # remove trailing nil packet :REGISTER, headers, resources end
Parses the value
for header
into the correct ruby
type
# File lib/ruby-growl/gntp.rb, line 432 def parse_header header, value return [header, nil] if value == '(null)' case header when 'Notification-Enabled', 'Notification-Sticky' then if value =~ /^(true|yes)$/i then [header, true] elsif value =~ /^(false|no)$/i then [header, false] else [header, value] end when 'Notification-Callback-Timestamp' then [header, Time.parse(value)] when 'Error-Code', 'Notifications-Count', 'Notifications-Priority', 'Subscriber-Port', 'Subscription-TTL' then [header, value.to_i] when 'Application-Name', 'Error-Description', 'Notification-Callback-Context', 'Notification-Callback-Context-Type', 'Notification-Callback-Target', 'Notification-Coalescing-ID', 'Notification-Display-Name', 'Notification-ID', 'Notification-Name', 'Notification-Text', 'Notification-Title', 'Origin-Machine-Name', 'Origin-Platform-Name', 'Origin-Platform-Version', 'Origin-Software-Version', 'Origin-Sofware-Name', 'Subscriber-ID', 'Subscriber-Name' then value.force_encoding Encoding::UTF_8 [header, value] when 'Application-Icon', 'Notification-Icon' then value = URI value [header, value] else [header, value] end end
Receives and handles the response packet
from the server and
either raises an error or returns a headers Hash.
# File lib/ruby-growl/gntp.rb, line 487 def receive packet $stderr.puts "> #{packet.gsub(/\r\n/, "\n> ")}" if $DEBUG packet = packet.strip.split "\r\n" info = packet.shift info =~ %r^GNTP/([\d.]+) (\S+) (\S+)$% version = $1 message = $2 raise Error, "invalid info line #{info.inspect}" unless version headers = packet.flat_map do |header| key, value = header.split ': ', 2 parse_header key, value end headers = Hash[*headers] return headers if %w[-OK -CALLBACK].include? message error_code = headers['Error-Code'] error_class = ERROR_MAP[error_code] error_message = headers['Error-Description'] raise error_class.new(error_message, headers) end
Sends a registration packet based on the given notifications
# File lib/ruby-growl/gntp.rb, line 520 def register send packet_register end
Creates a random salt for use in authentication and encryption
# File lib/ruby-growl/gntp.rb, line 527 def salt OpenSSL::Random.random_bytes 16 end
Sends packet
to the server and yields a callback, if given
# File lib/ruby-growl/gntp.rb, line 534 def send packet socket = connect $stderr.puts "< #{packet.gsub(/\r\n/, "\n< ")}" if $DEBUG socket.write packet result = receive socket.gets "\r\n\r\n\r\n" if block_given? then callback = receive socket.gets "\r\n\r\n\r\n" yield callback end result end