signaly-notify 0.0.4 → 0.0.5
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.
- checksums.yaml +4 -4
- data/bin/signaly-notify.rb +2 -437
- data/lib/signaly/client.rb +90 -0
- data/lib/signaly/notifier.rb +23 -0
- data/lib/signaly/notify_app.rb +209 -0
- data/lib/signaly/status.rb +35 -0
- data/lib/signaly/status_outputter.rb +62 -0
- data/lib/signaly.rb +25 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ccb5948a481aa78a08ebbed8ff8cda4c33e9515
|
4
|
+
data.tar.gz: e2901bff258d213961d7982a755aa5b55fee7fc4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 758f07968904bba363def2bef562ac2a450db0c34f7074a29722c6155b2ca1fdf56d743a5dab50ea0475708a7aa227a3641de0d586eb35ab9e1cb14a05a2c8f3
|
7
|
+
data.tar.gz: 78aa078ec0218bcde21dac1ea229766ba07c7da19870a740a05a0098d130705b79e66744452ea6d5cfd66e12ecbc34223e6b3364079e96bd801b884f4ed5e7f2
|
data/bin/signaly-notify.rb
CHANGED
@@ -1,439 +1,4 @@
|
|
1
1
|
#!/bin/env ruby
|
2
2
|
|
3
|
-
require '
|
4
|
-
|
5
|
-
require 'highline' # automating some tasks of the cli user interaction
|
6
|
-
require 'optparse'
|
7
|
-
require 'yaml'
|
8
|
-
|
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
|
22
|
-
:notification_showtime,
|
23
|
-
:debug_output,
|
24
|
-
:login,
|
25
|
-
:password,
|
26
|
-
:url, # of the checked page
|
27
|
-
:skip_login,
|
28
|
-
:config_file,
|
29
|
-
:console_only,
|
30
|
-
:notify)
|
31
|
-
|
32
|
-
defaults = SNConfig.new
|
33
|
-
# how many seconds between two checks of the site
|
34
|
-
defaults.sleep_seconds = 60
|
35
|
-
# if there is some pending content and I don't look at it,
|
36
|
-
# remind me after X seconds
|
37
|
-
defaults.remind_after = 60*5
|
38
|
-
# for how long time the notification shows up
|
39
|
-
defaults.notification_showtime = 10
|
40
|
-
defaults.debug_output = false
|
41
|
-
defaults.password = nil
|
42
|
-
# use first available visual notification engine
|
43
|
-
if defined? Libnotify
|
44
|
-
defaults.notify = :libnotify
|
45
|
-
elsif defined? Growl
|
46
|
-
defaults.notify = :growl
|
47
|
-
end
|
48
|
-
|
49
|
-
# how many PMs, notifications, invitations a user has at a time
|
50
|
-
class SignalyStatus < Struct.new(:pm, :notifications, :invitations)
|
51
|
-
|
52
|
-
def initialize(pm=0, notifications=0, invitations=0)
|
53
|
-
super(pm, notifications, invitations)
|
54
|
-
end
|
55
|
-
|
56
|
-
def is_there_anything?
|
57
|
-
self.each_pair {|k,v| return true if v > 0 }
|
58
|
-
return false
|
59
|
-
end
|
60
|
-
|
61
|
-
# does self have anything new compared with other?
|
62
|
-
def >(other)
|
63
|
-
if other.nil?
|
64
|
-
return true
|
65
|
-
end
|
66
|
-
|
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])
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# interaction with signaly.cz
|
84
|
-
class SignalyChecker
|
85
|
-
|
86
|
-
def initialize(config)
|
87
|
-
@username = config.login
|
88
|
-
@password = config.password
|
89
|
-
|
90
|
-
@agent = Mechanize.new
|
91
|
-
|
92
|
-
@dbg_print_pages = config.debug_output || false # print raw html of all request results?
|
93
|
-
|
94
|
-
@checked_page = config.url || 'https://www.signaly.cz/'
|
95
|
-
end
|
96
|
-
|
97
|
-
USERMENU_XPATH = ".//div[contains(@class, 'section-usermenu')]"
|
98
|
-
|
99
|
-
# takes user name and password; returns a page (logged-in) or throws
|
100
|
-
# exception
|
101
|
-
def login
|
102
|
-
page = @agent.get(@checked_page)
|
103
|
-
debug_page_print "front page", page
|
104
|
-
|
105
|
-
login_form = page.form_with(:id => 'frm-loginForm')
|
106
|
-
unless login_form
|
107
|
-
raise "Login form not found on the index page!"
|
108
|
-
end
|
109
|
-
login_form['name'] = @username
|
110
|
-
login_form['password'] = @password
|
111
|
-
|
112
|
-
page = @agent.submit(login_form)
|
113
|
-
debug_page_print "first logged in", page
|
114
|
-
|
115
|
-
errors = page.search(".//div[@class='alert alert-error']")
|
116
|
-
if errors.size > 0 then
|
117
|
-
msg = ''
|
118
|
-
errors.each {|e| msg += e.text.strip+"\n" }
|
119
|
-
raise "Login to signaly.cz failed: "+msg
|
120
|
-
end
|
121
|
-
|
122
|
-
usermenu = page.search(USERMENU_XPATH)
|
123
|
-
if usermenu.empty? then
|
124
|
-
raise "User-menu not found. Login failed or signaly.cz UI changed again."
|
125
|
-
end
|
126
|
-
|
127
|
-
return page
|
128
|
-
end
|
129
|
-
|
130
|
-
def user_status
|
131
|
-
status = SignalyStatus.new
|
132
|
-
page = @agent.get(@checked_page)
|
133
|
-
debug_page_print "user main page", page
|
134
|
-
|
135
|
-
menu = page.search(USERMENU_XPATH)
|
136
|
-
|
137
|
-
pm = menu.search(".//a[@href='/vzkazy']")
|
138
|
-
status[:pm] = find_num(pm.text)
|
139
|
-
|
140
|
-
notif = menu.search(".//a[@href='/ohlasky']")
|
141
|
-
status[:notifications] = find_num(notif.text)
|
142
|
-
|
143
|
-
inv = menu.search(".//a[@href='/vyzvy']")
|
144
|
-
if inv then
|
145
|
-
status[:invitations] = find_num(inv.text)
|
146
|
-
end
|
147
|
-
|
148
|
-
return status
|
149
|
-
end
|
150
|
-
|
151
|
-
private
|
152
|
-
|
153
|
-
def debug_page_print(title, page)
|
154
|
-
return if ! @dbg_print_pages
|
155
|
-
|
156
|
-
STDERR.puts
|
157
|
-
STDERR.puts ("# "+title).colorize(:yellow)
|
158
|
-
STDERR.puts
|
159
|
-
STDERR.puts page.search(".//div[@class='navbar navbar-fixed-top section-header']")
|
160
|
-
STDERR.puts
|
161
|
-
STDERR.puts "-" * 60
|
162
|
-
STDERR.puts
|
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
|
192
|
-
end
|
193
|
-
|
194
|
-
class SignalyStatusOutputter
|
195
|
-
def initialize(config)
|
196
|
-
@config = config
|
197
|
-
end
|
198
|
-
|
199
|
-
def output(new_status, old_status)
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
class ConsoleOutputter < SignalyStatusOutputter
|
204
|
-
def output(new_status, old_status)
|
205
|
-
print_line new_status, old_status
|
206
|
-
set_console_title new_status
|
207
|
-
end
|
208
|
-
|
209
|
-
private
|
210
|
-
|
211
|
-
def print_line(new_status, old_status)
|
212
|
-
t = Time.now
|
213
|
-
|
214
|
-
puts # start on a new line
|
215
|
-
print t.strftime("%H:%M:%S")
|
216
|
-
|
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}"
|
223
|
-
end
|
224
|
-
puts
|
225
|
-
end
|
226
|
-
|
227
|
-
# doesn't work....
|
228
|
-
def set_console_title(status)
|
229
|
-
t = "signaly-notify: #{status[:pm]}/#{status[:notifications]}"
|
230
|
-
`echo -ne "\\033]0;#{t}\\007"`
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
class LibNotifyOutputter < SignalyStatusOutputter
|
235
|
-
def output(new_status, old_status)
|
236
|
-
text = [:pm, :notifications, :invitations].collect do |what|
|
237
|
-
"#{what}: #{new_status[what]}"
|
238
|
-
end.join "\n"
|
239
|
-
|
240
|
-
Libnotify.show(:body => text, :summary => "signaly.cz", :timeout => @config.notification_showtime)
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
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"
|
249
|
-
|
250
|
-
notif = Growl.new 'localhost', 'ruby-growl', 'GNTP'
|
251
|
-
notif.notify('signaly.cz', 'signaly.cz', text)
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
class SignalyNotifyApp
|
256
|
-
|
257
|
-
DEFAULT_CONFIG_PATH = ".config/signaly-notify/config.yaml"
|
258
|
-
|
259
|
-
attr_accessor :default_config
|
260
|
-
|
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
|
269
|
-
|
270
|
-
notifier = Notifier.new
|
271
|
-
prepare_outputters config, notifier
|
272
|
-
|
273
|
-
check_loop config, notifier
|
274
|
-
end
|
275
|
-
|
276
|
-
private
|
277
|
-
|
278
|
-
# load configuration from config file
|
279
|
-
def config_file(path=nil)
|
280
|
-
path ||= File.join ENV['HOME'], DEFAULT_CONFIG_PATH
|
281
|
-
|
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
|
290
|
-
end
|
291
|
-
|
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
|
298
|
-
|
299
|
-
opts.on "-p", "--password WORD", "user's password" do |p|
|
300
|
-
config.password = p
|
301
|
-
end
|
302
|
-
|
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"
|
308
|
-
|
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
|
312
|
-
|
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
|
316
|
-
|
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
|
336
|
-
|
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
|
377
|
-
|
378
|
-
return merged
|
379
|
-
end
|
380
|
-
|
381
|
-
def prepare_outputters(config, notifier)
|
382
|
-
notifier.add_outputter ConsoleOutputter.new(config), :checked
|
383
|
-
|
384
|
-
return if config.console_only
|
385
|
-
return unless config.notify
|
386
|
-
|
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
|
393
|
-
end
|
394
|
-
|
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
|
412
|
-
|
413
|
-
notifier.emit :checked, status, old_status
|
414
|
-
|
415
|
-
if status > old_status then
|
416
|
-
# something new
|
417
|
-
notifier.emit :changed, status, old_status
|
418
|
-
last_reminder = Time.now.to_i
|
419
|
-
|
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
|
430
|
-
end
|
431
|
-
end
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
if $0 == __FILE__ then
|
436
|
-
app = SignalyNotifyApp.new
|
437
|
-
app.default_config = defaults
|
438
|
-
app.call ARGV
|
439
|
-
end
|
3
|
+
require 'signaly'
|
4
|
+
Signaly::NotifyApp.new.call ARGV
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Signaly
|
2
|
+
# interaction with signaly.cz
|
3
|
+
class Client
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
@username = config.login
|
7
|
+
@password = config.password
|
8
|
+
|
9
|
+
@agent = Mechanize.new
|
10
|
+
|
11
|
+
@dbg_print_pages = config.debug_output || false # print raw html of all request results?
|
12
|
+
|
13
|
+
@checked_page = config.url || 'https://www.signaly.cz/'
|
14
|
+
end
|
15
|
+
|
16
|
+
USERMENU_XPATH = ".//div[contains(@class, 'section-usermenu')]"
|
17
|
+
|
18
|
+
# takes user name and password; returns a page (logged-in) or throws
|
19
|
+
# exception
|
20
|
+
def login
|
21
|
+
page = @agent.get(@checked_page)
|
22
|
+
debug_page_print "front page", page
|
23
|
+
|
24
|
+
login_form = page.form_with(:id => 'frm-loginForm')
|
25
|
+
unless login_form
|
26
|
+
raise "Login form not found on the index page!"
|
27
|
+
end
|
28
|
+
login_form['name'] = @username
|
29
|
+
login_form['password'] = @password
|
30
|
+
|
31
|
+
page = @agent.submit(login_form)
|
32
|
+
debug_page_print "first logged in", page
|
33
|
+
|
34
|
+
errors = page.search(".//div[@class='alert alert-error']")
|
35
|
+
if errors.size > 0 then
|
36
|
+
msg = ''
|
37
|
+
errors.each {|e| msg += e.text.strip+"\n" }
|
38
|
+
raise "Login to signaly.cz failed: "+msg
|
39
|
+
end
|
40
|
+
|
41
|
+
usermenu = page.search(USERMENU_XPATH)
|
42
|
+
if usermenu.empty? then
|
43
|
+
raise "User-menu not found. Login failed or signaly.cz UI changed again."
|
44
|
+
end
|
45
|
+
|
46
|
+
return page
|
47
|
+
end
|
48
|
+
|
49
|
+
def user_status
|
50
|
+
status = Status.new
|
51
|
+
page = @agent.get(@checked_page)
|
52
|
+
debug_page_print "user main page", page
|
53
|
+
|
54
|
+
menu = page.search(USERMENU_XPATH)
|
55
|
+
|
56
|
+
pm = menu.search(".//a[@href='/vzkazy']")
|
57
|
+
status[:pm] = find_num(pm.text)
|
58
|
+
|
59
|
+
notif = menu.search(".//a[@href='/ohlasky']")
|
60
|
+
status[:notifications] = find_num(notif.text)
|
61
|
+
|
62
|
+
inv = menu.search(".//a[@href='/vyzvy']")
|
63
|
+
if inv then
|
64
|
+
status[:invitations] = find_num(inv.text)
|
65
|
+
end
|
66
|
+
|
67
|
+
return status
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def debug_page_print(title, page)
|
73
|
+
return if ! @dbg_print_pages
|
74
|
+
|
75
|
+
STDERR.puts
|
76
|
+
STDERR.puts ("# "+title).colorize(:yellow)
|
77
|
+
STDERR.puts
|
78
|
+
STDERR.puts page.search(".//div[@class='navbar navbar-fixed-top section-header']")
|
79
|
+
STDERR.puts
|
80
|
+
STDERR.puts "-" * 60
|
81
|
+
STDERR.puts
|
82
|
+
end
|
83
|
+
|
84
|
+
# finds the first integer in the string and returns it
|
85
|
+
def find_num(str, default=0)
|
86
|
+
m = str.match /\d+/
|
87
|
+
return m ? m[0].to_i : default
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Signaly
|
2
|
+
# dispatches events to observers - subscribed notifiers
|
3
|
+
class Notifier
|
4
|
+
def initialize
|
5
|
+
@outputters = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_outputter(outputter, *events)
|
9
|
+
events.each do |event|
|
10
|
+
@outputters[event] ||= []
|
11
|
+
@outputters[event] << outputter
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def emit(event, *args)
|
17
|
+
if @outputters[event]
|
18
|
+
@outputters[event].each {|o| o.output *args }
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module Signaly
|
2
|
+
class NotifyApp
|
3
|
+
|
4
|
+
SNConfig = Struct.new(:sleep_seconds, # between checks
|
5
|
+
:remind_after, # the first notification
|
6
|
+
:notification_showtime,
|
7
|
+
:debug_output,
|
8
|
+
:login,
|
9
|
+
:password,
|
10
|
+
:url, # of the checked page
|
11
|
+
:skip_login,
|
12
|
+
:config_file,
|
13
|
+
:console_only,
|
14
|
+
:notify)
|
15
|
+
|
16
|
+
DEFAULT_CONFIG_PATH = ".config/signaly-notify/config.yaml"
|
17
|
+
|
18
|
+
def call(argv)
|
19
|
+
options = process_options(argv)
|
20
|
+
config = merge_structs(
|
21
|
+
default_config,
|
22
|
+
config_file(options.config_file),
|
23
|
+
options
|
24
|
+
)
|
25
|
+
ask_config config
|
26
|
+
|
27
|
+
notifier = Notifier.new
|
28
|
+
prepare_outputters config, notifier
|
29
|
+
|
30
|
+
check_loop config, notifier
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# load configuration from config file
|
36
|
+
def config_file(path=nil)
|
37
|
+
path ||= File.join ENV['HOME'], DEFAULT_CONFIG_PATH
|
38
|
+
|
39
|
+
if File.exist? path then
|
40
|
+
cfg = YAML.load(File.open(path))
|
41
|
+
# symbolize keys
|
42
|
+
cfg = cfg.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
|
43
|
+
return cfg
|
44
|
+
end
|
45
|
+
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def process_options(argv)
|
50
|
+
config = SNConfig.new
|
51
|
+
optparse = OptionParser.new do |opts|
|
52
|
+
opts.on "-u", "--user NAME", "user name used to log in" do |n|
|
53
|
+
config.login = n
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on "-p", "--password WORD", "user's password" do |p|
|
57
|
+
config.password = p
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.separator "If you don't provide any of the options above, "\
|
61
|
+
"the program will ask you to type the name and/or password on its start. "\
|
62
|
+
"(And especially "\
|
63
|
+
"for the password it's probably a bit safer to type it this way than "\
|
64
|
+
"to type it on the commandline.)\n\n"
|
65
|
+
|
66
|
+
opts.on "-s", "--sleep SECS", Integer, "how many seconds to sleep between two checks (default is #{config.sleep_seconds})" do |s|
|
67
|
+
config.sleep_seconds = s
|
68
|
+
end
|
69
|
+
|
70
|
+
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|
|
71
|
+
config.remind_after = s
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on "--notify NOTIFIER", "choose visual notification engine (possible values are 'libnotify' and 'growl')" do |s|
|
75
|
+
config.notify = s.to_sym
|
76
|
+
end
|
77
|
+
|
78
|
+
opts.on "--console-only", "don't display any visual notifications" do
|
79
|
+
config.console_only = true
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on "-d", "--debug", "print debugging information to STDERR" do
|
83
|
+
config.debug_output = true
|
84
|
+
end
|
85
|
+
|
86
|
+
opts.on "--url URL", "check URL different from the default (for developmeng)" do |s|
|
87
|
+
config.url = s
|
88
|
+
end
|
89
|
+
|
90
|
+
opts.on "--skip-login", "don't login (for development)" do
|
91
|
+
config.skip_login = true
|
92
|
+
end
|
93
|
+
|
94
|
+
opts.on "-h", "--help", "print this help" do
|
95
|
+
puts opts
|
96
|
+
exit 0
|
97
|
+
end
|
98
|
+
|
99
|
+
opts.on "-c", "--config FILE", "configuration file" do |f|
|
100
|
+
config.config_file = f
|
101
|
+
end
|
102
|
+
end
|
103
|
+
optparse.parse! argv
|
104
|
+
|
105
|
+
|
106
|
+
unless argv.empty?
|
107
|
+
STDERR.puts "Warning: unused commandline arguments: "+ARGV.join(', ')
|
108
|
+
end
|
109
|
+
|
110
|
+
return config
|
111
|
+
end
|
112
|
+
|
113
|
+
# ask the user for missing essential information
|
114
|
+
def ask_config(config)
|
115
|
+
cliio = HighLine.new
|
116
|
+
config.login ||= cliio.ask("login: ")
|
117
|
+
config.password ||= cliio.ask("password: ") {|q| q.echo = '*' }
|
118
|
+
end
|
119
|
+
|
120
|
+
# merges the config structs so that the last one
|
121
|
+
# in the argument list has the highest priority and value nil is considered
|
122
|
+
# empty
|
123
|
+
def merge_structs(*structs)
|
124
|
+
merged = structs.shift.dup
|
125
|
+
|
126
|
+
merged.each_pair do |key, value|
|
127
|
+
structs.each do |s|
|
128
|
+
next if s.nil?
|
129
|
+
if s[key] != nil then
|
130
|
+
merged[key] = s[key]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
return merged
|
136
|
+
end
|
137
|
+
|
138
|
+
def prepare_outputters(config, notifier)
|
139
|
+
notifier.add_outputter ConsoleOutputter.new(config), :checked
|
140
|
+
|
141
|
+
return if config.console_only
|
142
|
+
return unless config.notify
|
143
|
+
|
144
|
+
case config.notify.to_sym
|
145
|
+
when :libnotify
|
146
|
+
notifier.add_outputter LibNotifyOutputter.new(config), :changed, :remind
|
147
|
+
when :growl
|
148
|
+
notifier.add_outputter GrowlOutputter.new(config), :changed, :remind
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def check_loop(config, notifier)
|
153
|
+
checker = Client.new config
|
154
|
+
checker.login unless config.skip_login
|
155
|
+
|
156
|
+
old_status = status = nil
|
157
|
+
last_reminder = 0
|
158
|
+
|
159
|
+
loop do
|
160
|
+
old_status = status
|
161
|
+
|
162
|
+
begin
|
163
|
+
status = checker.user_status
|
164
|
+
rescue Exception, SocketError => e
|
165
|
+
STDERR.puts "#{e.class}: #{e.message}"
|
166
|
+
sleep config.sleep_seconds
|
167
|
+
retry
|
168
|
+
end
|
169
|
+
|
170
|
+
notifier.emit :checked, status, old_status
|
171
|
+
|
172
|
+
if status > old_status then
|
173
|
+
# something new
|
174
|
+
notifier.emit :changed, status, old_status
|
175
|
+
last_reminder = Time.now.to_i
|
176
|
+
|
177
|
+
elsif config.remind_after != 0 &&
|
178
|
+
Time.now.to_i >= last_reminder + config.remind_after &&
|
179
|
+
status.is_there_anything? then
|
180
|
+
# nothing new, but pending content should be reminded
|
181
|
+
notifier.emit :remind, status, old_status
|
182
|
+
last_reminder = Time.now.to_i
|
183
|
+
end
|
184
|
+
|
185
|
+
sleep config.sleep_seconds
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def default_config
|
190
|
+
defaults = SNConfig.new
|
191
|
+
# how many seconds between two checks of the site
|
192
|
+
defaults.sleep_seconds = 60
|
193
|
+
# if there is some pending content and I don't look at it,
|
194
|
+
# remind me after X seconds
|
195
|
+
defaults.remind_after = 60*5
|
196
|
+
# for how long time the notification shows up
|
197
|
+
defaults.notification_showtime = 10
|
198
|
+
defaults.debug_output = false
|
199
|
+
defaults.password = nil
|
200
|
+
# use first available visual notification engine
|
201
|
+
if defined? Libnotify
|
202
|
+
defaults.notify = :libnotify
|
203
|
+
elsif defined? Growl
|
204
|
+
defaults.notify = :growl
|
205
|
+
end
|
206
|
+
return defaults
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Signaly
|
2
|
+
# how many PMs, notifications, invitations a user has at a time
|
3
|
+
class Status < Struct.new(:pm, :notifications, :invitations)
|
4
|
+
|
5
|
+
def initialize(pm=0, notifications=0, invitations=0)
|
6
|
+
super(pm, notifications, invitations)
|
7
|
+
end
|
8
|
+
|
9
|
+
def is_there_anything?
|
10
|
+
self.each_pair {|k,v| return true if v > 0 }
|
11
|
+
return false
|
12
|
+
end
|
13
|
+
|
14
|
+
# does self have anything new compared with other?
|
15
|
+
def >(other)
|
16
|
+
if other.nil?
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
|
20
|
+
[:pm, :notifications, :invitations].each do |prop|
|
21
|
+
if send(prop) > other.send(prop) then
|
22
|
+
return true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
return false
|
27
|
+
end
|
28
|
+
|
29
|
+
# is there any change between old_status and self in property prop?
|
30
|
+
def changed?(old_status, prop)
|
31
|
+
(old_status == nil && self[prop] > 0) ||
|
32
|
+
(old_status != nil && self[prop] != old_status[prop])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Signaly
|
2
|
+
class StatusOutputter
|
3
|
+
def initialize(config)
|
4
|
+
@config = config
|
5
|
+
end
|
6
|
+
|
7
|
+
def output(new_status, old_status)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ConsoleOutputter < StatusOutputter
|
12
|
+
def output(new_status, old_status)
|
13
|
+
print_line new_status, old_status
|
14
|
+
set_console_title new_status
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def print_line(new_status, old_status)
|
20
|
+
t = Time.now
|
21
|
+
|
22
|
+
puts # start on a new line
|
23
|
+
print t.strftime("%H:%M:%S")
|
24
|
+
|
25
|
+
[:pm, :notifications, :invitations].each do |what|
|
26
|
+
num = new_status[what].to_s
|
27
|
+
if new_status.changed?(old_status, what) then
|
28
|
+
num = num.colorize(:red)
|
29
|
+
end
|
30
|
+
print " #{what}: #{num}"
|
31
|
+
end
|
32
|
+
puts
|
33
|
+
end
|
34
|
+
|
35
|
+
# doesn't work....
|
36
|
+
def set_console_title(status)
|
37
|
+
t = "signaly-notify: #{status[:pm]}/#{status[:notifications]}"
|
38
|
+
`echo -ne "\\033]0;#{t}\\007"`
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class LibNotifyOutputter < StatusOutputter
|
43
|
+
def output(new_status, old_status)
|
44
|
+
text = [:pm, :notifications, :invitations].collect do |what|
|
45
|
+
"#{what}: #{new_status[what]}"
|
46
|
+
end.join "\n"
|
47
|
+
|
48
|
+
Libnotify.show(:body => text, :summary => "signaly.cz", :timeout => @config.notification_showtime)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class GrowlOutputter < StatusOutputter
|
53
|
+
def output(new_status, old_status)
|
54
|
+
text = [:pm, :notifications, :invitations].collect do |what|
|
55
|
+
"#{what}: #{new_status[what]}"
|
56
|
+
end.join "\n"
|
57
|
+
|
58
|
+
notif = Growl.new 'localhost', 'ruby-growl', 'GNTP'
|
59
|
+
notif.notify('signaly.cz', 'signaly.cz', text)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/signaly.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'colorize'
|
3
|
+
require 'highline'
|
4
|
+
require 'optparse'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
# optional gems for visual notifications
|
8
|
+
begin
|
9
|
+
require 'libnotify'
|
10
|
+
rescue LoadError
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
require 'ruby-growl'
|
15
|
+
rescue LoadError
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
%w(
|
20
|
+
client
|
21
|
+
notifier
|
22
|
+
notify_app
|
23
|
+
status_outputter
|
24
|
+
status
|
25
|
+
).each {|l| require_relative "signaly/#{l}" }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: signaly-notify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
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: 2015-
|
11
|
+
date: 2015-12-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mechanize
|
@@ -63,6 +63,12 @@ extensions: []
|
|
63
63
|
extra_rdoc_files: []
|
64
64
|
files:
|
65
65
|
- bin/signaly-notify.rb
|
66
|
+
- lib/signaly.rb
|
67
|
+
- lib/signaly/client.rb
|
68
|
+
- lib/signaly/notifier.rb
|
69
|
+
- lib/signaly/notify_app.rb
|
70
|
+
- lib/signaly/status.rb
|
71
|
+
- lib/signaly/status_outputter.rb
|
66
72
|
homepage: http://github.com/igneus/signaly-notify
|
67
73
|
licenses:
|
68
74
|
- LGPL-3.0
|