groem 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,24 @@
1
+
2
+ require 'autotest/restart'
3
+
4
+
5
+ Autotest.add_hook :initialize do |at|
6
+ at.testlib = 'minitest/spec'
7
+
8
+ # Note only unit tests are autotest'ed here
9
+ at.add_mapping(/^spec\/unit\/.*\.rb$/) do |filename, _|
10
+ filename
11
+ end
12
+
13
+ # To include functional and integration tests, uncomment these lines
14
+ at.add_mapping(/^spec\/functional\/.*\.rb$/) do |filename, _|
15
+ filename
16
+ end
17
+ at.add_mapping(/^spec\/integration\/.*\.rb$/) do |filename, _|
18
+ filename
19
+ end
20
+
21
+ end
22
+
23
+
24
+
@@ -0,0 +1,3 @@
1
+ vendor/*
2
+ .bundle/*
3
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://gems.rubyforge.org"
2
+
3
+ gemspec
@@ -0,0 +1,9 @@
1
+ # Release history
2
+
3
+ ## 0.0.4
4
+ - Take out bundler dep except for spec_helper, set up gemspec
5
+ - Default Notification-Enabled = 'True' for registered notifications
6
+ - Update README, add LICENSE
7
+
8
+ ## 0.0.3
9
+ - Initial release
@@ -0,0 +1,185 @@
1
+ ## Groem
2
+ ### An Eventmachine-based Growl Notification Transport Protocol (GNTP) client
3
+
4
+ For documentation of the GNTP protocol, see:
5
+ [http://www.growlforwindows.com/gfw/help/gntp.aspx](http://www.growlforwindows.com/gfw/help/gntp.aspx)
6
+
7
+ and the [Growl for Windows mailing list](http://groups.google.com/group/growl-for-windows/topics?start=).
8
+
9
+ Note this is very much beta. The core functionality is fairly well tested, but the following features are not yet implemented:
10
+
11
+ - Binary resources (`x-growl-resource://`) in requests, including for icons
12
+ - Encryption
13
+ - Subscribe requests
14
+
15
+ See other limitations below.
16
+
17
+ ## Platforms supported
18
+
19
+ Groem does not rely on any OS-specific libraries per se. It's simply an implementation of a TCP protocol. It doesn't define any UI components, part of Growl's design is that's left to the user to decide. But of course: you need a Growl server running that speaks GNTP... and so far as I know, to this point it's only been implemented by Growl for Windows.
20
+
21
+ ## Motivation
22
+
23
+ I wanted to be able to send desktop notifications on my Windows box from ruby running in cygwin. Call me crazy but I seem to enjoy hacking at Windows from cygwin...! (But no fear, if you are running ruby on Windows it should work exactly the same.)
24
+
25
+ Also, I wanted the experience of implementing a protocol in Eventmachine.
26
+
27
+ Groem is a spin-off of sorts from my project for the free and wonderful [Ruby Mendicant University (RMU)](http://blog.majesticseacreature.com/).
28
+
29
+
30
+ ## Examples of usage
31
+
32
+ ### Registration
33
+
34
+ Growl needs a 'register' request to define application options and what notifications to expect from your app.
35
+
36
+ Besides what Growl needs for registration, you can also define a **default callback** for a given notification. This simplifies cases where *you don't care about anything except the user action* -- i.e. whether the user closed the box, clicked it, or ignored it (timed out). Every time you send that type of notification, the same callback context, context-type and/or target URL will be used.
37
+
38
+
39
+ app = Groem::App.new('Downloader', :host => 'localhost')
40
+
41
+ app.register do
42
+ icon 'http://www.example.com/icon.png'
43
+ header 'X-Custom-Header', 'default value'
44
+
45
+ # notification with callback expected
46
+ notification :finished, 'Your download has finished!' do |n|
47
+ n.sticky = 'True'
48
+ n.text = 'Run it!'
49
+ n.icon 'path/to/local/icon.png' #=> generate x-growl-resource (future)
50
+ n.callback 'process', :type => 'run'
51
+ end
52
+
53
+ # notification with no callback
54
+ notification :started, 'Your download is starting!',
55
+ :display_name => 'Downloader working'
56
+
57
+ end
58
+
59
+
60
+ ### Notification
61
+
62
+ Notify returns the initial response from the server (whether request was OK or error). Callbacks (the second response from the server, based on what the user did) are handled through a routing scheme, see below.
63
+
64
+
65
+ # trigger notify and callbacks
66
+ app.notify(:started, 'XYZ has started!')
67
+
68
+ # trigger notify with 'ad-hoc' callback
69
+ # == with different settings than defined in app.register
70
+ app.notify(:started, 'ABC has started!',
71
+ :callback => {:type => 'ad-hoc',
72
+ :target => 'www.my-callback-url.com'}
73
+ )
74
+
75
+ # trigger notify and handle responses
76
+ app.notify(:finished, 'ABC has finished!') do |response|
77
+ response.ok? { # handle OK response }
78
+ response.error? { # handle any ERROR response }
79
+ response.error?(400) { # handle ERROR 400 (not authorized) response }
80
+ end
81
+
82
+ # you could also do this outside of the block
83
+ response = app.notify(:finished, 'ABC has finished!')
84
+ response.ok? { # handle OK response }
85
+ response.error? { # handle any ERROR response }
86
+ response.error?(400) { # handle ERROR 400 (not authorized) response }
87
+
88
+
89
+ ### Callbacks
90
+
91
+ Callback procs allow you to capture responses from the user based on what they did (close, click, timeout), plus the two standard Growl data fields that the UI can return data in -- the context and the context-type.
92
+
93
+ A given response will be routed to *all matching callback procs*, starting with the most specific.
94
+
95
+ (Of course, if a callback target (URL) is specified in the request, you won't get back a second response -- Growl will open up your browser instead.)
96
+
97
+
98
+ app.when_close 'process' do |response|
99
+ # do something with close responses to process notifications
100
+ end
101
+
102
+ app.when_click :context => 'process', :type => 'run' do |response|
103
+ # do something with click responses that have 'process' contexts of type 'run'
104
+ end
105
+
106
+ app.when_click :type => 'integer' do |response|
107
+ # do something with click responses that have type 'integer' regardless of context
108
+ end
109
+
110
+ app.when_timedout do |response|
111
+ # do something with any timeout response regardless of context or type
112
+ end
113
+
114
+
115
+ ## Lower-level interface
116
+
117
+ If you prefer a more direct interface you can use Groem::Client, the EM connection which Groem::App is built on. It expects the request to be passed as a hash, and will throw OK, error, and callback responses back as a three-element array roughly modeled on Rack's interface (_status_, _headers_, and _body_ -- body in this case being a hash of GNTP callback headers).
118
+
119
+ For instance,
120
+
121
+
122
+ regist = {'headers' => {
123
+ 'Application-Name' => 'SurfWriter',
124
+ 'Application-Icon' => 'http://www.site.org/image.jpg'
125
+ },
126
+ 'notifications' => {
127
+ 'Download Complete' => {
128
+ 'Notification-Display-Name' => 'Download completed',
129
+ 'Notification-Enabled' => 'True',
130
+ 'X-Language' => 'English',
131
+ 'X-Timezone' => 'PST'
132
+ }
133
+ }
134
+ }
135
+
136
+ connect = Groem::Client.register(regist)
137
+ connect.when_ok { |resp| # ... }
138
+ connect.errback { |resp| # ... }
139
+
140
+ notif = {'Application-Name' => 'SurfWriter',
141
+ 'Notification-Name' => 'Download Complete',
142
+ 'Notification-ID' => some_unique_id,
143
+ 'Notification-Callback-Context' => 'myfile',
144
+ 'Notification-Callback-Context-Type' => 'confirm'
145
+ }
146
+
147
+ connect2 = Groem::Client.notify(notif)
148
+ connect2.when_ok { |resp| # ... }
149
+ connect2.errback { |resp| # ... }
150
+ connect2.when_callback { |resp| #... }
151
+
152
+
153
+ For more details see `lib/groem/marshal`, and `spec/functional/client_spec`.
154
+
155
+
156
+ ## Limitations
157
+
158
+ - No casting or uncasting of GNTP headers to or from ruby types is done -- everything is a string (or a symbol which gets converted to a string). So the interface is a little clunky.
159
+
160
+ - If a Growl server is not running, the client's EM loop will not exit and has to be interrupted. In the future it would be good to timeout, perhaps after several reconnect attempts.
161
+
162
+ - It has only been tested on Ruby 1.8.7 running on cygwin on WinXP. I am planning to test on 1.9.2.
163
+
164
+
165
+ ## License
166
+
167
+ Copyright (c) 2010 Eric Gjertsen
168
+
169
+ Permission is hereby granted, free of charge, to any person obtaining a copy
170
+ of this software and associated documentation files (the "Software"), to deal
171
+ in the Software without restriction, including without limitation the rights
172
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
173
+ copies of the Software, and to permit persons to whom the Software is
174
+ furnished to do so, subject to the following conditions:
175
+
176
+ The above copyright notice and this permission notice shall be included in
177
+ all copies or substantial portions of the Software.
178
+
179
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
180
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
181
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
182
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
183
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
184
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
185
+ THE SOFTWARE.
@@ -0,0 +1,7 @@
1
+ ## Oct 12
2
+
3
+ - Test/debug on ruby 1.9.2
4
+ - Timeout or reconnect client if no server running
5
+ - Allow casting/uncasting ruby types from/to GNTP
6
+ - Implement binary resources
7
+ - Implement encryption
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ $:.unshift File.expand_path('../lib', __FILE__)
4
+ require 'groem/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "groem"
8
+ s.version = Groem::VERSION
9
+ s.authors = ["Eric Gjertsen"]
10
+ s.email = "ericgj72@gmail.com"
11
+ s.homepage = "http://github.com/ericgj/groem"
12
+ s.summary = "Eventmachine-based Ruby Growl (GNTP) client"
13
+ s.description = ""
14
+
15
+ s.files = `git ls-files -c`.split("\n")
16
+ s.platform = Gem::Platform::RUBY
17
+ s.require_path = 'lib'
18
+ s.rubyforge_project = ''
19
+ s.required_rubygems_version = '>= 1.3.6'
20
+
21
+ s.add_runtime_dependency 'eventmachine'
22
+ s.add_runtime_dependency 'uuidtools'
23
+ s.add_development_dependency 'minitest'
24
+ end
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ require 'rubygems'
4
+ require 'lib/groem/constants'
5
+ require 'lib/groem/marshal'
6
+ require 'lib/groem/client'
7
+ require 'lib/groem/response'
8
+ require 'lib/groem/route'
9
+ require 'lib/groem/notification'
10
+ require 'lib/groem/app'
@@ -0,0 +1,197 @@
1
+ require 'eventmachine'
2
+
3
+ module Groem
4
+
5
+ class App < Struct.new(:host, :port,
6
+ :environment, :headers, :notifications)
7
+ include Groem::Marshal::Request
8
+
9
+ DEFAULT_HOST = 'localhost'
10
+ DEFAULT_PORT = 23053
11
+ DEFAULT_ENV = {'protocol' => 'GNTP', 'version' => '1.0',
12
+ 'request_method' => 'REGISTER', 'encryption_id' => 'NONE'
13
+ }
14
+
15
+ def initialize(name, opts = {})
16
+ self.environment, self.headers, self.notifications = {}, {}, {}
17
+ self.environment = DEFAULT_ENV.merge(opts.delete(:environment) || {})
18
+ self.host = opts.delete(:host) || DEFAULT_HOST
19
+ self.port = opts.delete(:port) || DEFAULT_PORT
20
+ self.headers[GNTP_APPLICATION_NAME_KEY] = name
21
+ opts.each_pair {|opt, val| self.headers[growlify_key(opt)] = val }
22
+ end
23
+
24
+ # used by Marshal::Request#dump
25
+ def [](key)
26
+ to_request[key]
27
+ end
28
+
29
+ def name; self.headers[GNTP_APPLICATION_NAME_KEY]; end
30
+
31
+ def register(&blk)
32
+ if blk.arity == 1
33
+ blk.call(self)
34
+ else
35
+ instance_eval(&blk)
36
+ end
37
+ send_register
38
+ end
39
+
40
+ def notify(name, *args, &blk)
41
+ return unless n = self.notifications[name]
42
+ opts = ((Hash === args.last) ? args.pop : {})
43
+ title = args.shift
44
+ n = n.dup # copies attributes so not overwritten
45
+ n.title = title ? title : self.name
46
+ if cb = opts.delete(:callback)
47
+ n.callback(cb)
48
+ end
49
+ opts.each_pair {|k, v| n.__send__ :"#{k}=", v}
50
+ send_notify(n, &blk)
51
+ end
52
+
53
+ def notification(name, *args)
54
+ n = Groem::Notification.new(name, *args)
55
+ yield(n) if block_given?
56
+ n.application_name = self.name
57
+ self.notifications[name] = n
58
+ end
59
+
60
+ def callbacks &blk
61
+ if blk.arity == 1
62
+ blk.call(self)
63
+ else
64
+ instance_eval(&blk)
65
+ end
66
+ end
67
+
68
+ def header(key, value)
69
+ self.headers[growlify_key(key)] = value
70
+ end
71
+
72
+ def icon(uri_or_file)
73
+ # TODO if not uri
74
+ header GNTP_APPLICATION_ICON_KEY, uri_or_file
75
+ end
76
+
77
+ def binary(key, value_or_io)
78
+ #TODO
79
+ end
80
+
81
+ #---- callback definition methods
82
+
83
+ def when_register &blk
84
+ @register_callback = blk
85
+ end
86
+
87
+ def when_register_failed &blk
88
+ @register_errback = blk
89
+ end
90
+
91
+ def when_click path=nil, &blk
92
+ when_callback GNTP_CLICK_CALLBACK_RESULT, path, &blk
93
+ end
94
+
95
+ def when_close path=nil, &blk
96
+ when_callback GNTP_CLOSE_CALLBACK_RESULT, path, &blk
97
+ end
98
+
99
+ def when_timedout path=nil, &blk
100
+ when_callback GNTP_TIMEDOUT_CALLBACK_RESULT, path, &blk
101
+ end
102
+
103
+ def when_callback action, path=nil, &blk
104
+ action = growlify_action(action)
105
+ path = \
106
+ case path
107
+ when String
108
+ [path, nil]
109
+ when Hash
110
+ [path[:context], path[:type]]
111
+ end
112
+ #puts "App defined callback: #{action} #{path.inspect}"
113
+ notify_callbacks[Groem::Route.new(action, path)] = blk
114
+ end
115
+
116
+
117
+ def to_request
118
+ {'environment' => self.environment,
119
+ 'headers' => self.headers,
120
+ 'notifications' => notifications_to_register
121
+ }
122
+ end
123
+
124
+ protected
125
+
126
+ def register_callback; @register_callback; end
127
+ def register_errback; @register_errback || register_callback; end
128
+
129
+ def notify_callbacks; @notify_callbacks ||= {}; end
130
+
131
+ def send_register
132
+ Groem::Client.response_class = Groem::Response
133
+ stop_after = !(EM.reactor_running?)
134
+ ret = nil
135
+ EM.run {
136
+ connect = Groem::Client.register(self, host, port)
137
+ connect.callback do |resp|
138
+ EM.stop if stop_after
139
+ end
140
+ connect.errback do |resp|
141
+ register_errback.call(resp) if register_errback
142
+ ret = resp
143
+ EM.stop if stop_after
144
+ end
145
+ connect.when_ok do |resp|
146
+ register_callback.call(resp) if register_callback
147
+ ret = resp
148
+ end
149
+ }
150
+ ret
151
+ end
152
+
153
+ def send_notify(notif, &blk)
154
+ Groem::Client.response_class = Groem::Response
155
+ stop_after = !(EM.reactor_running?)
156
+ ret = nil
157
+ EM.run {
158
+ connect = Groem::Client.notify(notif, host, port)
159
+ connect.callback do |resp|
160
+ EM.stop if stop_after
161
+ end
162
+ connect.errback do |resp|
163
+ yield(resp) if block_given?
164
+ ret = resp
165
+ EM.stop if stop_after
166
+ end
167
+ connect.when_ok do |resp|
168
+ yield(resp) if block_given?
169
+ ret = resp
170
+ end
171
+ connect.when_callback do |resp|
172
+ route_response(resp)
173
+ end
174
+ }
175
+ ret
176
+ end
177
+
178
+ def notifications_to_register
179
+ self.notifications.inject({}) do |memo, pair|
180
+ memo[pair[0]] = pair[1].to_register
181
+ memo
182
+ end
183
+ end
184
+
185
+ def route_response(resp)
186
+ #puts "Response callback route: #{resp.callback_route.inspect}"
187
+ notify_callbacks.sort.each do |route, blk|
188
+ #puts "Checking against pattern: #{route.pattern.inspect} => #{route.matches?(resp.callback_route).inspect}"
189
+ if route.matches?(resp.callback_route)
190
+ blk.call(resp)
191
+ end
192
+ end
193
+ end
194
+
195
+ end
196
+
197
+ end