signaly-notify 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: