webwatchr 0.0.2
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 +7 -0
- data/Gemfile +13 -0
- data/LICENSE +674 -0
- data/README.md +122 -0
- data/Rakefile +11 -0
- data/lib/sites/bandcamp.rb +60 -0
- data/lib/sites/bsky.rb +176 -0
- data/lib/sites/postch.rb +112 -0
- data/lib/sites/songkick.rb +28 -0
- data/lib/webwatchr/alerting.rb +129 -0
- data/lib/webwatchr/base.rb +119 -0
- data/lib/webwatchr/logger.rb +31 -0
- data/lib/webwatchr/main.rb +51 -0
- data/lib/webwatchr/site.rb +575 -0
- data/lib/webwatchr.rb +1 -0
- data/tests/helpers.rb +32 -0
- data/tests/infra_test.rb +271 -0
- data/webwatchr.gemspec +12 -0
- metadata +56 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Webwatchr
|
|
4
|
+
require_relative "alerting"
|
|
5
|
+
class Main
|
|
6
|
+
include Loggable
|
|
7
|
+
|
|
8
|
+
def initialize(&block)
|
|
9
|
+
@alerts = []
|
|
10
|
+
super()
|
|
11
|
+
setup_logs
|
|
12
|
+
run!
|
|
13
|
+
instance_eval(&block)
|
|
14
|
+
logger.info("Webwatcher finished working")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def running?
|
|
18
|
+
return (File.exist?(PARAMS[:pid_file]) and not PARAMS[:site])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set(key, val)
|
|
22
|
+
PARAMS[key] = val
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def setup_logs
|
|
27
|
+
FileUtils.mkdir_p(PARAMS[:log_dir])
|
|
28
|
+
log_out_file = if PARAMS[:verbose] || PARAMS[:test]
|
|
29
|
+
$stdout
|
|
30
|
+
else
|
|
31
|
+
File.join(PARAMS[:log_dir], 'webwatchr.log')
|
|
32
|
+
end
|
|
33
|
+
log_out_file_rotation = 'weekly'
|
|
34
|
+
log_level = $VERBOSE ? Logger::DEBUG : Logger::INFO
|
|
35
|
+
|
|
36
|
+
MyLog.instance.configure(log_out_file, log_out_file_rotation, log_level)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def init()
|
|
40
|
+
logger.debug("Starting WebWatchr")
|
|
41
|
+
|
|
42
|
+
FileUtils.mkdir_p(PARAMS[:last_dir])
|
|
43
|
+
FileUtils.mkdir_p(PARAMS[:cache_dir])
|
|
44
|
+
|
|
45
|
+
Dir[File.join(__dir__, '..', 'sites', '*.rb')].sort.each do |site_path|
|
|
46
|
+
logger.debug("Loading #{site_path}")
|
|
47
|
+
require site_path
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def update(site_class, &block)
|
|
52
|
+
if (PARAMS[:mode] == :single) && site_class.to_s != PARAMS[:site]
|
|
53
|
+
logger.info("Running in single site mode, skipping #{site_class} (!= #{PARAMS[:site]})")
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
site = site_class.create(&block)
|
|
58
|
+
|
|
59
|
+
site.alerters = @alerts
|
|
60
|
+
|
|
61
|
+
logger.info "Running #{site.name}"
|
|
62
|
+
if block
|
|
63
|
+
site.instance_eval(&block)
|
|
64
|
+
end
|
|
65
|
+
Timeout.timeout(PARAMS[:site_timeout]) {
|
|
66
|
+
site.update(test: PARAMS[:test], cache_dir: PARAMS[:cache_dir], last_dir: PARAMS[:last_dir])
|
|
67
|
+
}
|
|
68
|
+
rescue Net::OpenTimeout, Errno::ENETUNREACH, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Zlib::BufError, Errno::ECONNREFUSED, SocketError, Net::ReadTimeout => e
|
|
69
|
+
logger.warn "Failed pulling #{site}: #{e.message}"
|
|
70
|
+
# Do nothing, try later
|
|
71
|
+
rescue SystemExit => e
|
|
72
|
+
msg = "User requested we quit while updating #{site}\n"
|
|
73
|
+
logger.error msg
|
|
74
|
+
warn msg
|
|
75
|
+
raise e
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
msg = "Issue with #{site_class} : #{e}\n"
|
|
78
|
+
msg += "#{e.message}\n"
|
|
79
|
+
logger.error msg
|
|
80
|
+
msg += e.backtrace.join("\n")
|
|
81
|
+
logger.debug e.backtrace.join("\n")
|
|
82
|
+
warn msg
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def run!
|
|
86
|
+
if running?
|
|
87
|
+
logger.info "Already running. Quitting"
|
|
88
|
+
exit
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
File.open(PARAMS[:pid_file], 'w+') { |f|
|
|
93
|
+
f.puts($$)
|
|
94
|
+
init()
|
|
95
|
+
}
|
|
96
|
+
ensure
|
|
97
|
+
if File.exist?(PARAMS[:pid_file])
|
|
98
|
+
FileUtils.rm PARAMS[:pid_file]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def add_default_alert(type, &block)
|
|
104
|
+
case type
|
|
105
|
+
when :email
|
|
106
|
+
alert = Alerting::EmailAlert.create(&block)
|
|
107
|
+
@alerts.append(alert)
|
|
108
|
+
when :telegram
|
|
109
|
+
alert = Alerting::TelegramAlert.create(&block)
|
|
110
|
+
@alerts.append(alert)
|
|
111
|
+
when :stdout
|
|
112
|
+
alert = Alerting::StdoutAlert.create()
|
|
113
|
+
@alerts.append(alert)
|
|
114
|
+
else
|
|
115
|
+
raise StandardError, "Unknown alert type: #{type}."
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
require 'singleton'
|
|
3
|
+
|
|
4
|
+
class MyLog
|
|
5
|
+
include Singleton
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@many_loggers = {}
|
|
9
|
+
@default_level = Logger::DEBUG
|
|
10
|
+
@default_out = $stdout
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def logger(class_name)
|
|
14
|
+
unless @many_loggers[class_name]
|
|
15
|
+
@many_loggers[class_name] = Logger.new(@default_out, @default_rotation, level: @default_level, progname: class_name)
|
|
16
|
+
end
|
|
17
|
+
return @many_loggers[class_name]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def configure(out, rotation, level)
|
|
21
|
+
@default_out = out
|
|
22
|
+
@default_rotation = rotation
|
|
23
|
+
@default_level = level
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
module Loggable
|
|
28
|
+
def logger
|
|
29
|
+
MyLog.instance.logger(self.class.name)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/ruby
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "logger"
|
|
6
|
+
|
|
7
|
+
$: << "./lib/" # for telegram to load
|
|
8
|
+
|
|
9
|
+
trap("INT") do
|
|
10
|
+
warn "User interrupted"
|
|
11
|
+
exit
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Webwatchr
|
|
15
|
+
include Loggable
|
|
16
|
+
|
|
17
|
+
PARAMS = { mode: :normal, test: false } # rubocop:disable Style/MutableConstant
|
|
18
|
+
|
|
19
|
+
if ARGV.any?
|
|
20
|
+
OptionParser.new { |o|
|
|
21
|
+
o.banner = "WebWatchr is a script to poll websites and alert on changes.
|
|
22
|
+
Exemple uses:
|
|
23
|
+
* Updates all webpages according to their 'wait' value, and compare against internal state, and update it.
|
|
24
|
+
ruby #{__FILE__}
|
|
25
|
+
* Updates sites-available/site.rb, ignoring 'wait' value, and compare against internal state, and update it.
|
|
26
|
+
ruby #{__FILE__} -s site.rb
|
|
27
|
+
|
|
28
|
+
Usage: ruby #{__FILE__} "
|
|
29
|
+
o.on("-sSITE", "--site=SITE", "Run WebWatcher on one site only. It has to be the name of the class for that site.") do |val|
|
|
30
|
+
PARAMS[:site] = val
|
|
31
|
+
PARAMS[:mode] = :single
|
|
32
|
+
end
|
|
33
|
+
o.on("-v", "--verbose", "Be verbose (output to STDOUT instead of logfile") do
|
|
34
|
+
PARAMS[:verbose] = true
|
|
35
|
+
end
|
|
36
|
+
o.on("-t", "--test", "Check website and return what we've parsed") do
|
|
37
|
+
PARAMS[:test] = true
|
|
38
|
+
end
|
|
39
|
+
o.on("-h", "--help", "Prints this help") {
|
|
40
|
+
puts o
|
|
41
|
+
exit
|
|
42
|
+
}
|
|
43
|
+
}.parse!()
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
PARAMS[:cache_dir] = File.join(__dir__, "..", "..", ".cache")
|
|
47
|
+
PARAMS[:last_dir] = File.join(__dir__, "..", "..", ".lasts")
|
|
48
|
+
PARAMS[:log_dir] = File.join(__dir__, "..", "..", "logs")
|
|
49
|
+
PARAMS[:pid_file] = File.join(__dir__, "..", "..", "webwatchr.pid")
|
|
50
|
+
require "webwatchr/base"
|
|
51
|
+
end
|