signaly-notify 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/bin/signaly-notify.rb +259 -165
  3. metadata +18 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 13a9873131db788193be40e2c480a033c8a065e0
4
- data.tar.gz: ba7fef24bbc5bce843e84aeaa323ef86a6f608e5
3
+ metadata.gz: d504ba72d7f41e9ae2d5fecf6ed2d6ac7a821525
4
+ data.tar.gz: d669974e5f5e67cc144d2ad216897284a50bc5c2
5
5
  SHA512:
6
- metadata.gz: 33176d90d86058ca014e1109fe2d8566c426e3350751161eaed98bf66a47af72b0b101c86eacfe9d1328284556e0a1426b6175c1b923a857642fb0c1121f0b25
7
- data.tar.gz: 7c864244d36b727f8fe02f846d8642db84c4deeda7f342aa34facf4dbd8d9713fc4a65115da3b91b606467b7a358677d3560380f281fa409aa4bb9711a121068
6
+ metadata.gz: f8b1aed68273983cc7c0b862829fc3c4d2960c6c71118271f398310425e97a404113fa4b18fb55415680adc16e85b0b2d8ba0f172581c7d92afc71877815db55
7
+ data.tar.gz: 024beb8fc12147e2a095839dbcd2aa68631f621674927d1456c33e83bfa90d2366f8b7b40d5fdfb41c27bffdcaa1aead313af0e152b6b8dc491940a1b66861d7
@@ -3,42 +3,33 @@
3
3
  require 'mechanize' # interaction with websites
4
4
  require 'colorize' # colorful console output
5
5
  require 'highline' # automating some tasks of the cli user interaction
6
- require 'libnotify' # visual notification
7
6
  require 'optparse'
8
7
  require 'yaml'
9
8
 
10
- SNConfig = Struct.new(:sleep_seconds,
11
- :remind_after,
9
+ # optional gems for visual notifications
10
+ begin
11
+ require 'libnotify'
12
+ rescue LoadError
13
+ end
14
+
15
+ begin
16
+ require 'ruby-growl'
17
+ rescue LoadError
18
+ end
19
+
20
+ SNConfig = Struct.new(:sleep_seconds, # between checks
21
+ :remind_after, # the first notification
12
22
  :notification_showtime,
13
23
  :debug_output,
14
24
  :login,
15
- :password)
25
+ :password,
26
+ :url, # of the checked page
27
+ :skip_login,
28
+ :config_file,
29
+ :console_only,
30
+ :notify)
16
31
 
17
- config_layers = []
18
-
19
- config = SNConfig.new
20
32
  defaults = SNConfig.new
21
-
22
- config_layers << defaults
23
- config_layers << config
24
-
25
- # merges the config structs so that the last one
26
- # in the argument list has the highest priority and value nil is considered
27
- # empty
28
- def merge_structs(*structs)
29
- merged = structs.first.dup
30
-
31
- merged.each_pair do |key, value|
32
- k = key.to_s # a sym normally; but we also want to merge a normal Hash received from Yaml
33
- structs.each do |s|
34
- if s[k] != nil then
35
- merged[k] = s[k]
36
- end
37
- end
38
- end
39
- end
40
-
41
- # set config defaults:
42
33
  # how many seconds between two checks of the site
43
34
  defaults.sleep_seconds = 60
44
35
  # if there is some pending content and I don't look at it,
@@ -48,20 +39,14 @@ defaults.remind_after = 60*5
48
39
  defaults.notification_showtime = 10
49
40
  defaults.debug_output = false
50
41
  defaults.password = nil
51
-
52
-
53
- # finds the first integer in the string and returns it
54
- # or returns 0
55
- def find_num(str)
56
- m = str.match /\d+/
57
-
58
- unless m
59
- return 0
60
- end
61
-
62
- return m[0].to_i
42
+ # use first available visual notification engine
43
+ if defined? Libnotify
44
+ defaults.notify = :libnotify
45
+ elsif defined? Growl
46
+ defaults.notify = :growl
63
47
  end
64
48
 
49
+ # how many PMs, notifications, invitations a user has at a time
65
50
  class SignalyStatus < Struct.new(:pm, :notifications, :invitations)
66
51
 
67
52
  def initialize(pm=0, notifications=0, invitations=0)
@@ -73,26 +58,40 @@ class SignalyStatus < Struct.new(:pm, :notifications, :invitations)
73
58
  return false
74
59
  end
75
60
 
76
- # utility function to handle the statuses:
61
+ # does self have anything new compared with other?
62
+ def >(other)
63
+ if other.nil?
64
+ return true
65
+ end
77
66
 
78
- def changed?(old_status, item)
79
- (old_status == nil && self[item] > 0) ||
80
- (old_status != nil && self[item] != old_status[item])
67
+ [:pm, :notifications, :invitations].each do |prop|
68
+ if send(prop) > other.send(prop) then
69
+ return true
70
+ end
71
+ end
72
+
73
+ return false
74
+ end
75
+
76
+ # is there any change between old_status and self in property prop?
77
+ def changed?(old_status, prop)
78
+ (old_status == nil && self[prop] > 0) ||
79
+ (old_status != nil && self[prop] != old_status[prop])
81
80
  end
82
81
  end
83
82
 
83
+ # interaction with signaly.cz
84
84
  class SignalyChecker
85
- # interaction with signaly.cz
86
85
 
87
- def initialize(username, password, dbg_print=false)
88
- @username = username
89
- @password = password
86
+ def initialize(config)
87
+ @username = config.login
88
+ @password = config.password
90
89
 
91
90
  @agent = Mechanize.new
92
91
 
93
- @dbg_print_pages = dbg_print # print raw html of all request results?
92
+ @dbg_print_pages = config.debug_output || false # print raw html of all request results?
94
93
 
95
- login
94
+ @checked_page = config.url || 'https://www.signaly.cz/'
96
95
  end
97
96
 
98
97
  USERMENU_XPATH = ".//div[contains(@class, 'section-usermenu')]"
@@ -100,7 +99,7 @@ class SignalyChecker
100
99
  # takes user name and password; returns a page (logged-in) or throws
101
100
  # exception
102
101
  def login
103
- page = @agent.get('https://www.signaly.cz/')
102
+ page = @agent.get(@checked_page)
104
103
  debug_page_print "front page", page
105
104
 
106
105
  login_form = page.form_with(:id => 'frm-loginForm')
@@ -130,9 +129,9 @@ class SignalyChecker
130
129
 
131
130
  def user_status
132
131
  status = SignalyStatus.new
133
- page = @agent.get('https://www.signaly.cz/')
132
+ page = @agent.get(@checked_page)
134
133
  debug_page_print "user main page", page
135
-
134
+
136
135
  menu = page.search(USERMENU_XPATH)
137
136
 
138
137
  pm = menu.search(".//a[@href='/vzkazy']")
@@ -154,14 +153,42 @@ class SignalyChecker
154
153
  def debug_page_print(title, page)
155
154
  return if ! @dbg_print_pages
156
155
 
157
- STDERR.puts
158
- STDERR.puts(("# "+title).colorize(:yellow))
156
+ STDERR.puts
157
+ STDERR.puts ("# "+title).colorize(:yellow)
159
158
  STDERR.puts
160
159
  STDERR.puts page.search(".//div[@class='navbar navbar-fixed-top section-header']")
161
160
  STDERR.puts
162
- STDERR.puts("-" * 60)
161
+ STDERR.puts "-" * 60
163
162
  STDERR.puts
164
163
  end
164
+
165
+ # finds the first integer in the string and returns it
166
+ def find_num(str, default=0)
167
+ m = str.match /\d+/
168
+ return m ? m[0].to_i : default
169
+ end
170
+ end
171
+
172
+ # dispatches events to observers - subscribed notifiers
173
+ class Notifier
174
+ def initialize
175
+ @outputters = {}
176
+ end
177
+
178
+ def add_outputter(outputter, *events)
179
+ events.each do |event|
180
+ @outputters[event] ||= []
181
+ @outputters[event] << outputter
182
+ end
183
+ self
184
+ end
185
+
186
+ def emit(event, *args)
187
+ if @outputters[event]
188
+ @outputters[event].each {|o| o.output *args }
189
+ end
190
+ self
191
+ end
165
192
  end
166
193
 
167
194
  class SignalyStatusOutputter
@@ -187,23 +214,14 @@ class ConsoleOutputter < SignalyStatusOutputter
187
214
  puts # start on a new line
188
215
  print t.strftime("%H:%M:%S")
189
216
 
190
- ms = new_status[:pm].to_s
191
- if new_status.changed?(old_status, :pm) then
192
- ms = ms.colorize(:red)
193
- end
194
- print " messages: "+ms
195
-
196
- ns = new_status[:notifications].to_s
197
- if new_status.changed?(old_status, :notifications) then
198
- ns = ns.colorize(:red)
199
- end
200
- print " notifications: "+ns
201
-
202
- is = new_status[:invitations].to_s
203
- if new_status.changed?(old_status, :invitations) then
204
- is = is.colorize(:red)
217
+ [:pm, :notifications, :invitations].each do |what|
218
+ num = new_status[what].to_s
219
+ if new_status.changed?(old_status, what) then
220
+ num = num.colorize(:red)
221
+ end
222
+ print " #{what}: #{num}"
205
223
  end
206
- puts " invitations: "+is
224
+ puts
207
225
  end
208
226
 
209
227
  # doesn't work....
@@ -215,131 +233,207 @@ end
215
233
 
216
234
  class LibNotifyOutputter < SignalyStatusOutputter
217
235
  def output(new_status, old_status)
218
- send_notification new_status
219
- end
236
+ text = [:pm, :notifications, :invitations].collect do |what|
237
+ "#{what}: #{new_status[what]}"
238
+ end.join "\n"
220
239
 
221
- private
240
+ Libnotify.show(:body => text, :summary => "signaly.cz", :timeout => @config.notification_showtime)
241
+ end
242
+ end
222
243
 
223
- def send_notification(status)
224
- ms = status[:pm].to_s
225
- ns = status[:notifications].to_s
226
- is = status[:invitations].to_s
227
- text = "pm: #{ms}\nnotifications: #{ns}\ninvitations: #{is}"
244
+ class GrowlOutputter < SignalyStatusOutputter
245
+ def output(new_status, old_status)
246
+ text = [:pm, :notifications, :invitations].collect do |what|
247
+ "#{what}: #{new_status[what]}"
248
+ end.join "\n"
228
249
 
229
- Libnotify.show(:body => text, :summary => "signaly.cz", :timeout => @config.notification_showtime)
250
+ notif = Growl.new 'localhost', 'ruby-growl', 'GNTP'
251
+ notif.notify('signaly.cz', 'signaly.cz', text)
230
252
  end
231
253
  end
232
254
 
255
+ class SignalyNotifyApp
233
256
 
234
- ############################################# main
257
+ DEFAULT_CONFIG_PATH = ".config/signaly-notify/config.yaml"
235
258
 
236
- # find default config
237
- config_file = nil
238
- default_config_path = "#{ENV['HOME']}/.config/signaly-notify/config.yaml"
239
- if default_config_path then
240
- config_file = default_config_path
241
- end
259
+ attr_accessor :default_config
242
260
 
243
- # process options
244
- optparse = OptionParser.new do |opts|
245
- opts.on "-u", "--user NAME", "user name used to log in" do |n|
246
- config.login = n
247
- end
261
+ def call(argv)
262
+ options = process_options(argv)
263
+ config = merge_structs(
264
+ @default_config,
265
+ config_file(options.config_file),
266
+ options
267
+ )
268
+ ask_config config
248
269
 
249
- opts.on "-p", "--password WORD", "user's password" do |p|
250
- config.password = p
251
- end
252
-
253
- opts.separator "If you don't provide any of the options above, "\
254
- "the program will ask you to type the name and/or password on its start. "\
255
- "(And especially "\
256
- "for the password it's probably a bit safer to type it this way than "\
257
- "to type it on the commandline.)\n\n"
258
-
259
- opts.on "-s", "--sleep SECS", Integer, "how many seconds to sleep between two checks (default is #{config.sleep_seconds})" do |s|
260
- config.sleep_seconds = s
261
- end
270
+ notifier = Notifier.new
271
+ prepare_outputters config, notifier
262
272
 
263
- opts.on "-r", "--remind SECS", Integer, "if I don't bother about the contents I recieved a notification about, remind me after X seconds (default is #{config.remind_after}; set to 0 to disable)" do |s|
264
- config.remind_after = s
273
+ check_loop config, notifier
265
274
  end
266
275
 
267
- opts.on "-d", "--debug", "print debugging information to STDERR" do
268
- config.debug_output = true
269
- end
276
+ private
270
277
 
271
- opts.on "-h", "--help", "print this help" do
272
- puts opts
273
- exit 0
274
- end
278
+ # load configuration from config file
279
+ def config_file(path=nil)
280
+ path ||= File.join ENV['HOME'], DEFAULT_CONFIG_PATH
275
281
 
276
- opts.on "-c", "--config FILE", "configuration file" do |f|
277
- config_file = f
282
+ if File.exist? path then
283
+ cfg = YAML.load(File.open(path))
284
+ # symbolize keys
285
+ cfg = cfg.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
286
+ return cfg
287
+ end
288
+
289
+ return nil
278
290
  end
279
- end
280
- optparse.parse!
281
291
 
282
- if config_file then
283
- config_layers << YAML.load(File.open(config_file))
284
- end
292
+ def process_options(argv)
293
+ config = SNConfig.new
294
+ optparse = OptionParser.new do |opts|
295
+ opts.on "-u", "--user NAME", "user name used to log in" do |n|
296
+ config.login = n
297
+ end
285
298
 
286
- config = merge_structs(*config_layers)
299
+ opts.on "-p", "--password WORD", "user's password" do |p|
300
+ config.password = p
301
+ end
287
302
 
288
- unless ARGV.empty?
289
- puts "Warning: unused commandline arguments: "+ARGV.join(', ')
290
- end
303
+ opts.separator "If you don't provide any of the options above, "\
304
+ "the program will ask you to type the name and/or password on its start. "\
305
+ "(And especially "\
306
+ "for the password it's probably a bit safer to type it this way than "\
307
+ "to type it on the commandline.)\n\n"
291
308
 
292
- # ask the user for missing essential information
293
- cliio = HighLine.new
309
+ opts.on "-s", "--sleep SECS", Integer, "how many seconds to sleep between two checks (default is #{config.sleep_seconds})" do |s|
310
+ config.sleep_seconds = s
311
+ end
294
312
 
295
- if !config.login then
296
- config.login = cliio.ask("login: ")
297
- end
313
+ opts.on "-r", "--remind SECS", Integer, "if I don't bother about the contents I recieved a notification about, remind me after X seconds (default is #{config.remind_after}; set to 0 to disable)" do |s|
314
+ config.remind_after = s
315
+ end
298
316
 
299
- if !config.password then
300
- config.password = cliio.ask("password: ") {|q| q.echo = '*' }
301
- end
317
+ opts.on "--notify NOTIFIER", "choose visual notification engine (possible values are 'libnotify' and 'growl')" do |s|
318
+ config.notify = s.to_sym
319
+ end
320
+
321
+ opts.on "--console-only", "don't display any visual notifications" do
322
+ config.console_only = true
323
+ end
324
+
325
+ opts.on "-d", "--debug", "print debugging information to STDERR" do
326
+ config.debug_output = true
327
+ end
328
+
329
+ opts.on "--url URL", "check URL different from the default (for developmeng)" do |s|
330
+ config.url = s
331
+ end
332
+
333
+ opts.on "--skip-login", "don't login (for development)" do
334
+ config.skip_login = true
335
+ end
302
336
 
303
- checker = SignalyChecker.new config.login, config.password, config.debug_output
337
+ opts.on "-h", "--help", "print this help" do
338
+ puts opts
339
+ exit 0
340
+ end
341
+
342
+ opts.on "-c", "--config FILE", "configuration file" do |f|
343
+ config.config_file = f
344
+ end
345
+ end
346
+ optparse.parse! argv
347
+
348
+
349
+ unless argv.empty?
350
+ STDERR.puts "Warning: unused commandline arguments: "+ARGV.join(', ')
351
+ end
352
+
353
+ return config
354
+ end
355
+
356
+ # ask the user for missing essential information
357
+ def ask_config(config)
358
+ cliio = HighLine.new
359
+ config.login ||= cliio.ask("login: ")
360
+ config.password ||= cliio.ask("password: ") {|q| q.echo = '*' }
361
+ end
362
+
363
+ # merges the config structs so that the last one
364
+ # in the argument list has the highest priority and value nil is considered
365
+ # empty
366
+ def merge_structs(*structs)
367
+ merged = structs.shift.dup
368
+
369
+ merged.each_pair do |key, value|
370
+ structs.each do |s|
371
+ next if s.nil?
372
+ if s[key] != nil then
373
+ merged[key] = s[key]
374
+ end
375
+ end
376
+ end
304
377
 
305
- old_status = status = nil
306
- last_reminder = 0
378
+ return merged
379
+ end
307
380
 
308
- co = ConsoleOutputter.new config
309
- lno = LibNotifyOutputter.new config
381
+ def prepare_outputters(config, notifier)
382
+ notifier.add_outputter ConsoleOutputter.new(config), :checked
310
383
 
311
- loop do
312
- old_status = status
384
+ return if config.console_only
385
+ return unless config.notify
313
386
 
314
- begin
315
- status = checker.user_status
316
- rescue Exception => e
317
- Libnotify.show(:body => e.message,
318
- :summary => "signaly.cz: ERROR",
319
- :timeout => 20)
320
- sleep 21
321
- raise
387
+ case config.notify.to_sym
388
+ when :libnotify
389
+ notifier.add_outputter LibNotifyOutputter.new(config), :changed, :remind
390
+ when :growl
391
+ notifier.add_outputter GrowlOutputter.new(config), :changed, :remind
392
+ end
322
393
  end
323
394
 
324
- # print each update to the console:
325
- co.output status, old_status
395
+ def check_loop(config, notifier)
396
+ checker = SignalyChecker.new config
397
+ checker.login unless config.skip_login
398
+
399
+ old_status = status = nil
400
+ last_reminder = 0
401
+
402
+ loop do
403
+ old_status = status
404
+
405
+ begin
406
+ status = checker.user_status
407
+ rescue Exception, SocketError => e
408
+ STDERR.puts "#{e.class}: #{e.message}"
409
+ sleep config.sleep_seconds
410
+ retry
411
+ end
326
412
 
327
- # send a notification only if there is something interesting:
413
+ notifier.emit :checked, status, old_status
328
414
 
329
- if old_status == nil ||
330
- (status[:pm] > old_status[:pm] ||
331
- status[:notifications] > old_status[:notifications]) then
332
- # something new
333
- lno.output status, old_status
334
- last_reminder = Time.now.to_i
415
+ if status > old_status then
416
+ # something new
417
+ notifier.emit :changed, status, old_status
418
+ last_reminder = Time.now.to_i
335
419
 
336
- elsif config.remind_after != 0 &&
337
- Time.now.to_i >= last_reminder + config.remind_after &&
338
- status.is_there_anything? then
339
- # nothing new, but pending content should be reminded
340
- lno.output status, old_status
341
- last_reminder = Time.now.to_i
420
+ elsif config.remind_after != 0 &&
421
+ Time.now.to_i >= last_reminder + config.remind_after &&
422
+ status.is_there_anything? then
423
+ # nothing new, but pending content should be reminded
424
+ notifier.emit :remind, status, old_status
425
+ last_reminder = Time.now.to_i
426
+ end
427
+
428
+ sleep config.sleep_seconds
429
+ end
342
430
  end
431
+ end
432
+
433
+
343
434
 
344
- sleep config.sleep_seconds
435
+ if $0 == __FILE__ then
436
+ app = SignalyNotifyApp.new
437
+ app.default_config = defaults
438
+ app.call ARGV
345
439
  end
metadata CHANGED
@@ -1,71 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: signaly-notify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Pavlík
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-01-05 00:00:00.000000000 Z
11
+ date: 2015-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mechanize
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '2.7'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '2.7'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: colorize
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '0.7'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '0.7'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: highline
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '>='
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '1.7'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: libnotify
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - '>='
67
- - !ruby/object:Gem::Version
68
- version: '0'
54
+ version: '1.7'
69
55
  description: "signaly-notify.rb is a simple script \nlogging in with your user data
70
56
  to the social network https://signaly.cz \nand notifying you - by the means of printing
71
57
  to the console \nas well as sending a visual notification using libnotify - \nwhen
@@ -88,18 +74,19 @@ require_paths:
88
74
  - lib
89
75
  required_ruby_version: !ruby/object:Gem::Requirement
90
76
  requirements:
91
- - - '>='
77
+ - - ">="
92
78
  - !ruby/object:Gem::Version
93
79
  version: '0'
94
80
  required_rubygems_version: !ruby/object:Gem::Requirement
95
81
  requirements:
96
- - - '>='
82
+ - - ">="
97
83
  - !ruby/object:Gem::Version
98
84
  version: '0'
99
85
  requirements: []
100
86
  rubyforge_project:
101
- rubygems_version: 2.0.14
87
+ rubygems_version: 2.2.2
102
88
  signing_key:
103
89
  specification_version: 4
104
90
  summary: notification script for signaly.cz (Czech christian social network)
105
91
  test_files: []
92
+ has_rdoc: