signaly-notify 0.0.3

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 +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: []