signaly-notify 0.0.3

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 +7 -0
  2. data/bin/signaly-notify.rb +345 -0
  3. metadata +105 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 13a9873131db788193be40e2c480a033c8a065e0
4
+ data.tar.gz: ba7fef24bbc5bce843e84aeaa323ef86a6f608e5
5
+ SHA512:
6
+ metadata.gz: 33176d90d86058ca014e1109fe2d8566c426e3350751161eaed98bf66a47af72b0b101c86eacfe9d1328284556e0a1426b6175c1b923a857642fb0c1121f0b25
7
+ data.tar.gz: 7c864244d36b727f8fe02f846d8642db84c4deeda7f342aa34facf4dbd8d9713fc4a65115da3b91b606467b7a358677d3560380f281fa409aa4bb9711a121068
@@ -0,0 +1,345 @@
1
+ #!/bin/env ruby
2
+
3
+ require 'mechanize' # interaction with websites
4
+ require 'colorize' # colorful console output
5
+ require 'highline' # automating some tasks of the cli user interaction
6
+ require 'libnotify' # visual notification
7
+ require 'optparse'
8
+ require 'yaml'
9
+
10
+ SNConfig = Struct.new(:sleep_seconds,
11
+ :remind_after,
12
+ :notification_showtime,
13
+ :debug_output,
14
+ :login,
15
+ :password)
16
+
17
+ config_layers = []
18
+
19
+ config = SNConfig.new
20
+ 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
+ # how many seconds between two checks of the site
43
+ defaults.sleep_seconds = 60
44
+ # if there is some pending content and I don't look at it,
45
+ # remind me after X seconds
46
+ defaults.remind_after = 60*5
47
+ # for how long time the notification shows up
48
+ defaults.notification_showtime = 10
49
+ defaults.debug_output = false
50
+ 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
63
+ end
64
+
65
+ class SignalyStatus < Struct.new(:pm, :notifications, :invitations)
66
+
67
+ def initialize(pm=0, notifications=0, invitations=0)
68
+ super(pm, notifications, invitations)
69
+ end
70
+
71
+ def is_there_anything?
72
+ self.each_pair {|k,v| return true if v > 0 }
73
+ return false
74
+ end
75
+
76
+ # utility function to handle the statuses:
77
+
78
+ def changed?(old_status, item)
79
+ (old_status == nil && self[item] > 0) ||
80
+ (old_status != nil && self[item] != old_status[item])
81
+ end
82
+ end
83
+
84
+ class SignalyChecker
85
+ # interaction with signaly.cz
86
+
87
+ def initialize(username, password, dbg_print=false)
88
+ @username = username
89
+ @password = password
90
+
91
+ @agent = Mechanize.new
92
+
93
+ @dbg_print_pages = dbg_print # print raw html of all request results?
94
+
95
+ login
96
+ end
97
+
98
+ USERMENU_XPATH = ".//div[contains(@class, 'section-usermenu')]"
99
+
100
+ # takes user name and password; returns a page (logged-in) or throws
101
+ # exception
102
+ def login
103
+ page = @agent.get('https://www.signaly.cz/')
104
+ debug_page_print "front page", page
105
+
106
+ login_form = page.form_with(:id => 'frm-loginForm')
107
+ unless login_form
108
+ raise "Login form not found on the index page!"
109
+ end
110
+ login_form['name'] = @username
111
+ login_form['password'] = @password
112
+
113
+ page = @agent.submit(login_form)
114
+ debug_page_print "first logged in", page
115
+
116
+ errors = page.search(".//div[@class='alert alert-error']")
117
+ if errors.size > 0 then
118
+ msg = ''
119
+ errors.each {|e| msg += e.text.strip+"\n" }
120
+ raise "Login to signaly.cz failed: "+msg
121
+ end
122
+
123
+ usermenu = page.search(USERMENU_XPATH)
124
+ if usermenu.empty? then
125
+ raise "User-menu not found. Login failed or signaly.cz UI changed again."
126
+ end
127
+
128
+ return page
129
+ end
130
+
131
+ def user_status
132
+ status = SignalyStatus.new
133
+ page = @agent.get('https://www.signaly.cz/')
134
+ debug_page_print "user main page", page
135
+
136
+ menu = page.search(USERMENU_XPATH)
137
+
138
+ pm = menu.search(".//a[@href='/vzkazy']")
139
+ status[:pm] = find_num(pm.text)
140
+
141
+ notif = menu.search(".//a[@href='/ohlasky']")
142
+ status[:notifications] = find_num(notif.text)
143
+
144
+ inv = menu.search(".//a[@href='/vyzvy']")
145
+ if inv then
146
+ status[:invitations] = find_num(inv.text)
147
+ end
148
+
149
+ return status
150
+ end
151
+
152
+ private
153
+
154
+ def debug_page_print(title, page)
155
+ return if ! @dbg_print_pages
156
+
157
+ STDERR.puts
158
+ STDERR.puts(("# "+title).colorize(:yellow))
159
+ STDERR.puts
160
+ STDERR.puts page.search(".//div[@class='navbar navbar-fixed-top section-header']")
161
+ STDERR.puts
162
+ STDERR.puts("-" * 60)
163
+ STDERR.puts
164
+ end
165
+ end
166
+
167
+ class SignalyStatusOutputter
168
+ def initialize(config)
169
+ @config = config
170
+ end
171
+
172
+ def output(new_status, old_status)
173
+ end
174
+ end
175
+
176
+ class ConsoleOutputter < SignalyStatusOutputter
177
+ def output(new_status, old_status)
178
+ print_line new_status, old_status
179
+ set_console_title new_status
180
+ end
181
+
182
+ private
183
+
184
+ def print_line(new_status, old_status)
185
+ t = Time.now
186
+
187
+ puts # start on a new line
188
+ print t.strftime("%H:%M:%S")
189
+
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)
205
+ end
206
+ puts " invitations: "+is
207
+ end
208
+
209
+ # doesn't work....
210
+ def set_console_title(status)
211
+ t = "signaly-notify: #{status[:pm]}/#{status[:notifications]}"
212
+ `echo -ne "\\033]0;#{t}\\007"`
213
+ end
214
+ end
215
+
216
+ class LibNotifyOutputter < SignalyStatusOutputter
217
+ def output(new_status, old_status)
218
+ send_notification new_status
219
+ end
220
+
221
+ private
222
+
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}"
228
+
229
+ Libnotify.show(:body => text, :summary => "signaly.cz", :timeout => @config.notification_showtime)
230
+ end
231
+ end
232
+
233
+
234
+ ############################################# main
235
+
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
242
+
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
248
+
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
262
+
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
265
+ end
266
+
267
+ opts.on "-d", "--debug", "print debugging information to STDERR" do
268
+ config.debug_output = true
269
+ end
270
+
271
+ opts.on "-h", "--help", "print this help" do
272
+ puts opts
273
+ exit 0
274
+ end
275
+
276
+ opts.on "-c", "--config FILE", "configuration file" do |f|
277
+ config_file = f
278
+ end
279
+ end
280
+ optparse.parse!
281
+
282
+ if config_file then
283
+ config_layers << YAML.load(File.open(config_file))
284
+ end
285
+
286
+ config = merge_structs(*config_layers)
287
+
288
+ unless ARGV.empty?
289
+ puts "Warning: unused commandline arguments: "+ARGV.join(', ')
290
+ end
291
+
292
+ # ask the user for missing essential information
293
+ cliio = HighLine.new
294
+
295
+ if !config.login then
296
+ config.login = cliio.ask("login: ")
297
+ end
298
+
299
+ if !config.password then
300
+ config.password = cliio.ask("password: ") {|q| q.echo = '*' }
301
+ end
302
+
303
+ checker = SignalyChecker.new config.login, config.password, config.debug_output
304
+
305
+ old_status = status = nil
306
+ last_reminder = 0
307
+
308
+ co = ConsoleOutputter.new config
309
+ lno = LibNotifyOutputter.new config
310
+
311
+ loop do
312
+ old_status = status
313
+
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
322
+ end
323
+
324
+ # print each update to the console:
325
+ co.output status, old_status
326
+
327
+ # send a notification only if there is something interesting:
328
+
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
335
+
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
342
+ end
343
+
344
+ sleep config.sleep_seconds
345
+ end
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: signaly-notify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Jakub Pavlík
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mechanize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: colorize
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: highline
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
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'
69
+ description: "signaly-notify.rb is a simple script \nlogging in with your user data
70
+ to the social network https://signaly.cz \nand notifying you - by the means of printing
71
+ to the console \nas well as sending a visual notification using libnotify - \nwhen
72
+ something new happens.\nCurrently it only detects pending private messages and notifications."
73
+ email: jkb.pavlik@gmail.com
74
+ executables:
75
+ - signaly-notify.rb
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - bin/signaly-notify.rb
80
+ homepage: http://github.com/igneus/signaly-notify
81
+ licenses:
82
+ - LGPL-3.0
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.0.14
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: notification script for signaly.cz (Czech christian social network)
105
+ test_files: []