snarl-snp 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,138 @@
1
+ class Snarl
2
+ class SNP
3
+ module Action
4
+
5
+ NOTIFICATION_PARAM_ORDER = [:title, :text, :icon, :timeout, :class, :action, :app]
6
+
7
+ # Sends a SNP command "register" to Snarl. For registering +app+ to Snarl setting window.
8
+ # snp.register('Ruby-Snarl')
9
+ # +app+ :: an application name. Snarl uses it as an application ID.
10
+ # Snarl::SNP keeps +app+ for add_class method and notification method.
11
+ # +app+ default is SNP::DEFAULT_APP, 'Ruby-Snarl'.
12
+ # Returns SNP::Response object.
13
+ #
14
+ # Snarl sends back a casual error when +app+ is already registered.
15
+ # It is treated as SNP::SNPError::Casual::SNP_ERROR_ALREADY_REGISTERED.
16
+ def register(app=nil)
17
+ @app = app
18
+ cmds = {:action => 'register', :app => app}
19
+ request(Request.new(cmds))
20
+ end
21
+ alias :app= :register
22
+
23
+ # Sends a SNP command "add_class" to Snarl. For adding +classid+ class and its +classtitle+ nickname.
24
+ # snp.add_class('green')
25
+ # snp.add_class('red', 'failure popup')
26
+ # +classid+ :: classname ID on the registered application
27
+ # +classtitle+ :: display alias for +classname+, optional
28
+ # Returns SNP::Response object.
29
+ # Before adding a class, you should register the application.
30
+ #
31
+ # Snarl sends back a casual error when +classid+ is already added.
32
+ # It is treated as SNP::SNPError::Casual::SNP_ERROR_CLASS_ALREADY_EXISTS.
33
+ def add_class(classid, classtitle=nil)
34
+ # TODO: add_class(app=nil, classid, classtitle=nil)
35
+ # type=SNP#?version=1.0#?action=add_class#?class=t returns (107) Bad Packet
36
+ raise "#{self}#register(appname) required before add_class" unless @app
37
+ cmds = {:action => 'add_class', :app => @app, :class => classid.to_s, :title => classtitle}
38
+ request(Request.new(cmds))
39
+ end
40
+
41
+ # Sends SNP command "notification" to Snarl. For making a popup message itself.
42
+ # snp.notification('title', 'text', 'icon.jpg', 10, 'classA') # 10 is timeout Integer
43
+ # snp.notification(:title => 't', :text => 't', :icon => 'i.jpg', :timeout => 10, :class => 'claA')
44
+ # +title+ :: title of popup. String.
45
+ # +text+ :: text body of popup. String. linebreaks should be only "\n". "\r" confuses Snarl.
46
+ # +icon+ :: icon image path of popup. String/#to_s. path on Snarl machine or http URL. bmp/jpg/png/gif.
47
+ # +timeout+ :: display seconds of popup. Integer. if nil, DEFAULT_TIMEOUT 10. if 0, popup never closes automatically.
48
+ # +class+ :: classid of popup. String. It should have been added by "add_class" method when you use.
49
+ # notification(title, text, icon=nil, timeout=nil, classid=nil) or notification(keyword-hash).
50
+ # snp.notification('title', 'text', 10)
51
+ # snp.notification('title', 'text')
52
+ # snp.notification('text') # title == DEFAULT_APP == 'Ruby-Snarl'
53
+ def notification(*keyhash)
54
+ # TODO: priority
55
+ cmds = normalize_notification_params(keyhash)
56
+ request(Request.new(cmds))
57
+ end
58
+ alias :notify :notification
59
+
60
+ # Sends SNP command "unregister" to Snarl. For removing +app+ from Snarl setting window.
61
+ # snp.unregister('Ruby-Snarl')
62
+ # After this, Snarl users can not edit the settings for +app+ 's popup.
63
+ # If you allow users to edit settings, do not send unregister.
64
+ # Without sending unregister, the applications are always reseted when Snarl restarts.
65
+ #
66
+ # Snarl sends back a casual error when +app+ is not registered.
67
+ # It is treated as SNP::SNPError::Casual::SNP_ERROR_NOT_REGISTERED.
68
+ def unregister(app=nil)
69
+ app = app || @app
70
+ raise "#{self}#unregister requires appname." unless app
71
+ cmds = {:action => 'unregister', :app => app}
72
+ request(Request.new(cmds))
73
+ end
74
+
75
+ # Sends SNP command "hello".
76
+ # irb> Snarl::SNP.new('127.0.0.1').hello
77
+ # SNP/1.1/0/OK/Snarl R2.21
78
+ def hello
79
+ request(Request.new({:action => 'hello'}))
80
+ end
81
+
82
+ # Sends SNP command "version".
83
+ # irb> Snarl::SNP.new('127.0.0.1').version
84
+ # SNP/1.1/0/OK/40.15
85
+ def version
86
+ request(Request.new({:action => 'version'}))
87
+ end
88
+
89
+ # UTILS -----------------------------
90
+
91
+ private
92
+
93
+ def paramarray_to_hash(param_array)
94
+ title, text, icon, timeout, classid, action, app = param_array
95
+ pattern = {
96
+ :only_text => (param_array.size == 1),
97
+ :text_timeout => (param_array.size == 2) && title && text.kind_of?(Integer),
98
+ :title_text_timeout => (param_array.size == 3) && title && text && icon.kind_of?(Integer)
99
+ }
100
+ case
101
+ when pattern[:only_text] then # notify("msg")
102
+ {:title => nil, :text => title}
103
+ when pattern[:text_timeout] then # notify("msg", 10)
104
+ {:title => nil, :text => title, :timeout => text}
105
+ when pattern[:title_text_timeout] then # notify("tit", "msg", 10)
106
+ {:title => title, :text => text, :timeout => icon}
107
+ else
108
+ Hash[*NOTIFICATION_PARAM_ORDER.zip(param_array).flatten].delete_if{|k, v| v.nil?}
109
+ end
110
+ end
111
+
112
+ # we support for:
113
+ # notify( 'text')
114
+ # notify('title', 'text')
115
+ # notify( 'text', 10)
116
+ # notify('title', 'text', 10)
117
+ def normalize_notification_params(param)
118
+ res = Hash.new
119
+ if param[0].kind_of?(Hash) then
120
+ keyhash = param[0]
121
+ else
122
+ keyhash = paramarray_to_hash(param)
123
+ end
124
+ NOTIFICATION_PARAM_ORDER.each do |command|
125
+ keyhash_value = (keyhash[command.to_s] || keyhash[command])
126
+ res[command] = keyhash_value if keyhash_value
127
+ end
128
+ res[:action] = 'notification'
129
+ res[:app] = @app if (res[:app].nil? && @app) # default notification
130
+ res[:app] = nil if res[:app] == :anonymous # from snp.show_message
131
+ res[:title] = (@title || DEFAULT_TITLE) unless res[:title]
132
+ res[:timeout] = (@timeout || DEFAULT_TIMEOUT) unless res[:timeout]
133
+ res[:icon] = icon(res[:icon]) if res[:icon]
134
+ return res
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,32 @@
1
+
2
+ # when config file exists, require this config
3
+ class Snarl
4
+ class SNP
5
+ class Config
6
+
7
+ DEFAULT_HOST = '127.0.0.1'
8
+ DEFAULT_PORT = 9887
9
+ @host = nil
10
+ @port = nil
11
+
12
+ def self.host
13
+ @host || ENV['SNARL_HOST'] || DEFAULT_HOST
14
+ end
15
+ def self.host=(v)
16
+ @host = if v then v.to_s else nil end
17
+ end
18
+
19
+ def self.port
20
+ @port || ENV['SNARL_PORT'] || DEFAULT_PORT
21
+ end
22
+ def self.port=(v)
23
+ @port = if v then v.to_i else nil end
24
+ end
25
+
26
+ def self.reset
27
+ self.host = nil
28
+ self.port = nil
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,76 @@
1
+ class Snarl
2
+ class SNP
3
+ module Error
4
+ class SNPError < StandardError
5
+ def initialize(response, request=nil) # request is String or Request
6
+ @response, @request = response, request
7
+ end
8
+ attr_accessor :request, :response
9
+ def code ; @response.code ; end
10
+ def message ; "(#{code}) #{@response.message}" ; end
11
+ def ok? ; @response.ok? ; end
12
+ end
13
+
14
+ class Casual < SNPError ; end
15
+
16
+ class SNP_OK < Casual ; end
17
+ # (0) OK
18
+
19
+ class SNP_ERROR_NOT_REGISTERED < Casual ; end
20
+ # (202) The application hasn't been registered.
21
+
22
+ class SNP_ERROR_ALREADY_REGISTERED < Casual ; end
23
+ # (203) The application is already registered.
24
+
25
+ class SNP_ERROR_CLASS_ALREADY_EXISTS < Casual ; end
26
+ # (204) Class is already registered.
27
+
28
+ class Fatal < SNPError ; end
29
+
30
+ class SNP_ERROR_FAILED < Fatal ; end
31
+ # (101) An internal error occurred - usually this represents a fault within Snarl itself.
32
+
33
+ class SNP_ERROR_UNKNOWN_COMMAND < Fatal ; end
34
+ # (102) An unknown action was specified.
35
+
36
+ class SNP_ERROR_TIMED_OUT < Fatal ; end
37
+ # (103) The command sending (or subsequent reply) timed out.
38
+
39
+ class SNP_ERROR_BAD_PACKET < Fatal ; end
40
+ # (107) The command packet is wrongly formed.
41
+
42
+ class SNP_ERROR_NOT_RUNNING < Fatal ; end
43
+ # (201) Incoming network notification handling has been disabled by the user.
44
+
45
+ class RUBYSNARL_UNKNOWN_RESPONSE < Fatal ; end
46
+ # (???) Snarl returns unknown return code.
47
+
48
+ CODE_TO_OBJ = {
49
+ '0' => SNP_OK,
50
+ '101' => SNP_ERROR_FAILED,
51
+ '102' => SNP_ERROR_UNKNOWN_COMMAND,
52
+ '103' => SNP_ERROR_TIMED_OUT,
53
+ '107' => SNP_ERROR_BAD_PACKET,
54
+ '201' => SNP_ERROR_NOT_RUNNING,
55
+ '202' => SNP_ERROR_NOT_REGISTERED,
56
+ '203' => SNP_ERROR_ALREADY_REGISTERED,
57
+ '204' => SNP_ERROR_CLASS_ALREADY_EXISTS
58
+ }
59
+
60
+ def self.klass(response, request=nil)
61
+ if klass = Error::CODE_TO_OBJ[response.to_s] then
62
+ klass
63
+ elsif response.kind_of?(Error::SNPError)
64
+ response
65
+ else
66
+ Error::RUBYSNARL_UNKNOWN_RESPONSE
67
+ end
68
+ end
69
+
70
+ def self.raise(response, request)
71
+ Error.klass.new(response, request)
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,83 @@
1
+ class Snarl
2
+ class SNP
3
+ class Request
4
+ PROTOCOL_NAME = "SNP"
5
+ PROTOCOL_VERSION ="1.0"
6
+
7
+ SNP_ACTIONS = {
8
+ 'register' => %w(type version action app),
9
+ 'add_class' => %w(type version action app class title),
10
+ 'notification' => %w(type version action app class title text timeout icon priority),
11
+ 'unregister' => %w(type version action app),
12
+ 'hello' => %w(type version action),
13
+ 'version' => %w(type version action),
14
+ }
15
+
16
+ # You have to send non-ascii messages by "Windows" encoding.
17
+ ENCODING = 'cp932'
18
+
19
+ SNP_SEPARATOR = '#?'
20
+ SNP_TERMINAL_STRING = "\r\n"
21
+ SNP_HEADER = {'type' => PROTOCOL_NAME, 'version' => PROTOCOL_VERSION}
22
+
23
+ # make SNP request string from command hash and is Request object.
24
+ def initialize(cmd_hash={})
25
+ @commands = {}.update(cmd_hash)
26
+ end
27
+
28
+ attr_reader :commands
29
+
30
+ # Adds command key and value to Request
31
+ def []=(cmdkey, value) ; @commands[cmdkey] = value ; end # TODO: normalize
32
+ # Returns command value for command key
33
+ def [](cmdkey) ; @commands[cmdkey] ; end
34
+
35
+ # Returns Request query string with SNP_TERMINAL_STRING "\r\n"
36
+ def to_str ; query + SNP_TERMINAL_STRING ; end # FIXME: include "\r\n"?
37
+ alias :to_s :to_str
38
+ # Returns Request query string. has no SNP_TERMINAL_STRING "\r\n".
39
+ def inspect ; query ; end
40
+ def action ; @commands['action'] ; end
41
+
42
+ private
43
+
44
+ def order_command_pair
45
+ action = @commands['action']
46
+ @commands.to_a.sort_by{|pair| SNP_ACTIONS[action].index(pair[0])}
47
+ end
48
+
49
+ def query
50
+ normalize
51
+ order_command_pair.map{|pair| pair.join('=')}.join(SNP_SEPARATOR)
52
+ end
53
+
54
+ # normalize item pairs
55
+ # - symbol keys and upcase KEYS are normalized into downcased string keys.
56
+ # - query should not have any "\r". use "\n"
57
+ # - when value is nil, delete the item pair.
58
+ def normalize
59
+ norm_commands = Hash[*@commands.map{|k, v| [crlf2lf(k).downcase, crlf2lf(v)]}.flatten]
60
+ action = normalize_action(norm_commands['action'])
61
+ norm_commands['action'] = action
62
+ @commands = SNP_HEADER.dup
63
+ available_commands_in(action).each do |cmd|
64
+ @commands[cmd] = norm_commands[cmd] if norm_commands[cmd]
65
+ end
66
+ end
67
+
68
+ def normalize_action(action_type)
69
+ action_type.downcase.gsub(/-/){'_'}.sub(/\Aaddclass\Z/){'add_class'}
70
+ end
71
+
72
+ def available_commands_in(action)
73
+ SNP_ACTIONS[action] || {}
74
+ end
75
+
76
+ def crlf2lf(s)
77
+ s ? s.to_s.gsub(/\r\n/){"\n"}.gsub(/\r/){"\n"} : s
78
+ end
79
+
80
+ end
81
+ end
82
+ end
83
+
@@ -0,0 +1,36 @@
1
+ class Snarl
2
+ class SNP
3
+ class Response
4
+ ResponseParseRegexp = /\ASNP\/[\d\.]+\/(\d+)\/(.+?)\Z/
5
+ def initialize(s)
6
+ if s.respond_to?(:get) then
7
+ @response = s.get
8
+ else
9
+ @response = s
10
+ end
11
+ parse_response
12
+ end
13
+
14
+ attr_reader :code, :message, :infomation, :response
15
+ attr_accessor :request
16
+ alias :status :code
17
+
18
+ def parse_response
19
+ if ResponseParseRegexp =~ @response.chomp then
20
+ @code = $1
21
+ @message, @infomation= $2.split(/\//)
22
+ else
23
+ raise Snarl::SNP::Error::RUBYSNARL_UNKNOWN_RESPONSE.new(self, nil)
24
+ end
25
+ raise error.new(self, nil) unless ok?
26
+ end
27
+
28
+ def error ; Error.klass(self.code) ; end
29
+
30
+ def to_s ; code ; end # puts response #=> "0"
31
+ def inspect ; @response.chomp ; end # p response #=> "SNP/1.1/0/OK/456"
32
+
33
+ def ok? ; code.to_i.zero? ; end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,142 @@
1
+ class Snarl # conpat for ruby-snarl
2
+
3
+ class SNP
4
+
5
+ include Action
6
+
7
+ # default "timeout command" value. popup disappers in 10 seconds.
8
+ DEFAULT_TIMEOUT = 10
9
+
10
+ # default title for "non-title" notification
11
+ DEFAULT_TITLE = 'Ruby-Snarl'
12
+
13
+ # Snarl::SNP.new('127.0.0.1', 9887)
14
+ def initialize(host=nil, port=nil, verbose=false)
15
+ @host = host
16
+ @port = port
17
+ @verbose = verbose
18
+ @logger = nil
19
+ @app = nil
20
+ @timeout = nil
21
+ @iconset = {}
22
+ @title = nil
23
+ end
24
+
25
+ # When you set it true, all unimportant SNP errors raise.
26
+ # Default is false, Snarl::SNP::Error::Casual are disabled.
27
+ attr_accessor :verbose
28
+
29
+ # a value of SNP command "app".
30
+ attr_reader :app
31
+
32
+ attr_accessor :title
33
+
34
+ # a value of SNP command "timeout".
35
+ attr_accessor :timeout
36
+
37
+ # set Logger object. It is used when sending request and getting response.
38
+ def logger=(logger)
39
+ @logger = (logger.kind_of?(Class) ? logger.new($stdout) : logger)
40
+ end
41
+
42
+ # send Snarl::SNP::Request/Hash/String and get Snarl::SNP::Response fron Snarl.
43
+ # When the response "fatal" response, raise errors.
44
+ # When method +verbose+ returns true, "casual" errors also raises.
45
+ def request(req)
46
+ req = Request.new(req) if req.kind_of?(Hash)
47
+ debug(req)
48
+ begin
49
+ action = req.kind_of?(Request) ? req.action : '(string)'
50
+ res = get_response(req)
51
+ info("#{action}: #{res.inspect}")
52
+ rescue Error::Casual => ex
53
+ info("#{action}: (ignored) #{ex.message}")
54
+ raise if verbose
55
+ rescue Error::Fatal => ex
56
+ info("#{action}: #{ex.message}")
57
+ raise
58
+ end
59
+ return res
60
+ end
61
+
62
+ # SHORTCUT METHODS ----------------
63
+
64
+ # add_classes('type1', 'type2', 'type3')
65
+ # add_classes(['type1', desc1], ['type2', desc2], ['type3', desc3])
66
+ def add_classes(*classes)
67
+ classes.each do |classpair|
68
+ classpair = [classpair, nil] if classpair.kind_of?(String)
69
+ add_class(*classpair)
70
+ end
71
+ end
72
+
73
+ # returns icon path if SNP knows. optional.
74
+ def icon(s)
75
+ if @iconset.has_key?(s) then @iconset[s] else s end
76
+ end
77
+
78
+ # set icons pair. quite optional.
79
+ # snp.iconset(:red => 'red.jpg')
80
+ # snp.notification('title', 'text', :red) #=> sends "icon=red.jpg"
81
+ # snp.notification('title', 'text', 'blue') #=> sends "icon=blue"
82
+ def iconset(icons)
83
+ @iconset = icons
84
+ end
85
+ alias :icons :iconset
86
+
87
+ def ping
88
+ notification(DEFAULT_TITLE, 'Ruby Snarl-SNP Ping Message', 3, nil)
89
+ end
90
+
91
+ alias :message :notification
92
+
93
+ def snarl_hello
94
+ hello.infomation
95
+ end
96
+
97
+ def snarl_version
98
+ version.infomation
99
+ end
100
+
101
+ # Snarl::SNP.open(host, port){|snp| snp.register ... }
102
+ # "ensure block" is empty. TCPSocket is closed per access.
103
+ def self.open(host=nil, port=nil, verbose=false, &block)
104
+ client = new(host, port, verbose)
105
+ yield(client) # socket always closed in TCPSocket#open{...}
106
+ client
107
+ end
108
+
109
+ # send message only. app is "anonymous".
110
+ # Snarl::SNP.show_message(host, 9887, title, text, tomeout, icon)
111
+ # Snarl::SNP.show_message(host, title, text, tomeout, icon)
112
+ def self.show_message(host, port, title=nil, text=nil, timeout=nil, icon=nil)
113
+ # TODO: (host, title, text, 10)
114
+ if port.kind_of?(String) && icon.nil? then
115
+ port, title, text, timeout, icon = nil, port, title, text, timeout
116
+ end
117
+ new(host, port).notification(:title => title, :text => text, :timeout => timeout, :icon => icon)
118
+ end
119
+
120
+ def self.ping(host=nil, port=nil)
121
+ new(host, port).ping
122
+ end
123
+ # ----------------- SHORTCUT METHODS
124
+
125
+ private
126
+
127
+ def get_response(req)
128
+ host = @host || Config.host
129
+ port = @port || Config.port
130
+ TCPSocket.open(host, port) do |s|
131
+ s.write(req)
132
+ res = Response.new(s.gets)
133
+ res.request = req
134
+ res
135
+ end
136
+ end
137
+
138
+ def info(m); @logger.info(m) if @logger; end
139
+ def debug(m); @logger.debug(m) if @logger; end
140
+
141
+ end
142
+ end