groem 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +24 -0
- data/.gitignore +3 -0
- data/Gemfile +3 -0
- data/HISTORY.markdown +9 -0
- data/README.markdown +185 -0
- data/TODO.markdown +7 -0
- data/groem.gemspec +24 -0
- data/lib/groem.rb +10 -0
- data/lib/groem/app.rb +197 -0
- data/lib/groem/client.rb +169 -0
- data/lib/groem/constants.rb +74 -0
- data/lib/groem/marshal.rb +349 -0
- data/lib/groem/notification.rb +140 -0
- data/lib/groem/response.rb +86 -0
- data/lib/groem/route.rb +37 -0
- data/lib/groem/version.rb +3 -0
- data/spec/functional/app_notify_adhoc_spec.rb +73 -0
- data/spec/functional/app_notify_spec.rb +390 -0
- data/spec/functional/app_register_spec.rb +113 -0
- data/spec/functional/client_spec.rb +361 -0
- data/spec/integration/notify.rb +318 -0
- data/spec/integration/register.rb +133 -0
- data/spec/shared/dummy_server.rb +198 -0
- data/spec/shared/dummy_server_helper.rb +31 -0
- data/spec/shared/marshal_helper.rb +40 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/unit/app_spec.rb +77 -0
- data/spec/unit/marshal_request_spec.rb +380 -0
- data/spec/unit/marshal_response_spec.rb +162 -0
- data/spec/unit/notification_spec.rb +205 -0
- data/spec/unit/response_spec.rb +7 -0
- data/spec/unit/route_spec.rb +93 -0
- metadata +141 -0
data/.autotest
ADDED
@@ -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
|
+
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/HISTORY.markdown
ADDED
data/README.markdown
ADDED
@@ -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.
|
data/TODO.markdown
ADDED
data/groem.gemspec
ADDED
@@ -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
|
data/lib/groem.rb
ADDED
@@ -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'
|
data/lib/groem/app.rb
ADDED
@@ -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
|