herald 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ .rvmrc
4
+ Gemfile.lock
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in herald.gemspec
4
+ gemspec
@@ -0,0 +1,201 @@
1
+ Herald
2
+ ====
3
+
4
+ Herald is a simple notifier for Twitter, RSS, or email.
5
+
6
+ Pass Herald some keywords and sources to watch, and Herald will notify you using Growl, email, pinging a site, or running Ruby code as soon as those keywords appear.
7
+
8
+ Installation
9
+ ------------
10
+
11
+ ### Rubygems
12
+
13
+ gem install herald
14
+
15
+ ### GitHub
16
+
17
+ git clone git://github.com/lukes/herald.git
18
+ gem build herald.gemspec
19
+ gem install herald-<version>.gem
20
+
21
+ Usage
22
+ -----
23
+
24
+ First step is `require` Herald into your Ruby project:
25
+
26
+ require 'rubygems'
27
+ require 'herald'
28
+
29
+ Then, to watch for tweets containing "soundofmusic":
30
+
31
+ Herald.watch do
32
+ check :twitter
33
+ _for "soundofmusic"
34
+ end
35
+
36
+ Or an RSS feed:
37
+
38
+ Herald.watch do
39
+ check :rss, :from => "http://example.com/.rss"
40
+ _for "soundofmusic"
41
+ end
42
+
43
+ Or an email inbox: [Not Implemented]
44
+
45
+ Herald.watch do
46
+ check :inbox, :imap => "imap.server.com", :user => "username", :pass => "supersecret"
47
+ _for "soundofmusic"
48
+ end
49
+
50
+ ### Watching multiple sources, or for multiple keywords
51
+
52
+ Watching two RSS feeds and Twitter for two keywords
53
+
54
+ Herald.watch do
55
+ check :rss, :from => ["http://earthquakes.gov/.rss", "http://livenews.com/.rss"]
56
+ check :twitter
57
+ _for "christchurch", "earthquake"
58
+ end
59
+
60
+ Or, if sources should have different keywords
61
+
62
+ Herald.watch do
63
+ check :rss, :from => "http://earthquake.usgs.gov/earthquakes/catalogs/eqs1day-M0.xml"
64
+ _for "christchurch"
65
+ end
66
+ check :twitter do
67
+ _for "#eqnz", "#chch", "#quake"
68
+ end
69
+ end
70
+
71
+ ### Actions
72
+
73
+ By default Herald will use Ruby's `$stdout` and simply prints what it finds.
74
+
75
+ Swap in another action by passing Herald one of the following `action` parameters:
76
+
77
+ #### Growl
78
+
79
+ [Growl](http://growl.info/) is a notification system for Mac OS X.
80
+
81
+ To use Growl, enable "Listen for incoming notifications" and "Allow remote application registration" on the [Network tab](http://growl.info/documentation/exploring-preferences.php) of the Growl Preference Panel, and pass Herald `action :growl`:
82
+
83
+ Herald.watch_twitter do
84
+ _for "nz", "#election"
85
+ action :growl
86
+ end
87
+
88
+ #### Ping
89
+
90
+ To ping a URI, pass Herald `action :ping, :uri => "http://address.to.ping"`:
91
+
92
+ Herald.watch_twitter do
93
+ _for "#yaks", "#in", "#space"
94
+ action :ping, :uri => "http://counter.com/+1"
95
+ end
96
+
97
+ #### Post
98
+
99
+ To post information about what Herald finds to a URI, pass Herald `action :post, :uri => "http://address.to.post.to"`:
100
+
101
+ Herald.watch_twitter do
102
+ _for "#yaks", "#in", "#space"
103
+ action :post, :uri => "http://yakdb.com/post"
104
+ end
105
+
106
+ #### Callbacks
107
+
108
+ If you'd like to do your own thing entirely each time a keyword appears, pass a callback in the form of a Ruby block:
109
+
110
+ Herald.watch_twitter do
111
+ _for "revolution"
112
+ action do
113
+ `say "Viva!"`
114
+ end
115
+ end
116
+
117
+ ### Timer
118
+
119
+ By default Herald will sleep for 1 minute after checking each of the sources independently.
120
+ To set a different sleep time:
121
+
122
+ Herald.watch_twitter do
123
+ _for "soundofmusic"
124
+ every 30 => "seconds" # or "minutes", "hours", or "days"
125
+ end
126
+
127
+ ### Look Once
128
+
129
+ Rather than watching, if you just want to get a single poll of keywords, use `once()`. All the same parameters as with `watch()` can be used (except `every`).
130
+
131
+ Herald.once do
132
+ check :twitter
133
+ _for "#herald"
134
+ end
135
+
136
+ ### Callback Scope and Herald Metaprogramming
137
+
138
+ Callbacks allow a great deal of reflection into the internals of Herald.
139
+
140
+ If the callback is passed with the scope of `Herald`, it will have access to the `Herald` methods and instance variables:
141
+
142
+ Herald.watch_twitter do
143
+ _for "#breaking", "news"
144
+ action do
145
+ puts instance_variables
146
+ end
147
+ end
148
+
149
+ If passed in within the scope of `Herald::Watcher`, it will have access to the particular `Watcher`'s methods and instance variables:
150
+
151
+ Herald.watch do
152
+ check :twitter do
153
+ _for "#breaking", "news"
154
+ action do
155
+ puts instance_variables
156
+ end
157
+ end
158
+ end
159
+
160
+ ### For inquisitive minds
161
+
162
+ Assign the herald watcher to a variable
163
+
164
+ require 'herald'
165
+ herald = Herald.watch_rss :from => "http://www.reddit.com/r/pics/new/.rss" do
166
+ _for "imgur"
167
+ end
168
+
169
+ Edit your herald instance while it's working
170
+
171
+ herald.watchers
172
+ herald.watchers.to_s # in English
173
+ herald.watchers.first.keywords << "cats"
174
+ herald.watchers.first.action { puts "callback" }
175
+
176
+ Stop and start herald
177
+
178
+ herald.stop
179
+ herald.alive? # => false
180
+ herald.start
181
+
182
+ Start a second herald
183
+
184
+ herald_the_second = Herald.watch_twitter { _for "#herald" }
185
+
186
+ Use `Herald` class methods to inspect and edit heralds as a batch
187
+
188
+ Herald.heralds # prints both running heralds
189
+ Herald.heralds.to_s # print in English
190
+ Herald.stop # both heralds stopped
191
+ Herald.alive? # => false
192
+ Herald.start # both heralds restarted
193
+
194
+ ### Herald Binary [Not Implemented]
195
+
196
+ herald -watch twitter -for #eqnz
197
+ herald -once twitter -for #herald
198
+ herald -show-heralds
199
+ herald -modify 1 -watch rss -from http://example.com/.rss
200
+ herald -pause 1
201
+ herald -kill 1
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ task :test do
5
+ $LOAD_PATH.unshift './lib'
6
+ require 'herald'
7
+ require 'minitest/autorun'
8
+ Dir.glob("test/**/*_test.rb").each { |test| require "./#{test}" }
9
+ end
@@ -0,0 +1,12 @@
1
+ - Catch and report on exceptions in @@subprocess, or thread, when exception has killed a @@subprocess, remove it from @@subprocess
2
+
3
+ - Best way to require gems? Can I vendorise crack gem?
4
+
5
+ - Suppress Growl text on test()
6
+
7
+ - Comment in RDOC
8
+
9
+ - Handle exceptions in processes, by reporting the error, then set the @subprocess to nil
10
+
11
+ - Look at Gemspec
12
+ - Show dependency on ruby-growl if installed for mac
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "herald/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "herald"
7
+ s.version = Herald::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Luke Duncalfe"]
10
+ s.email = ["lduncalfe@eml.cc"]
11
+ s.homepage = "http://github.com/lukes/herald"
12
+ s.summary = %q{A simple notifier for Twitter, RSS, or email}
13
+ s.description = %q{Pass Herald some keywords and sources to watch, and Herald will notify you using Growl, email, pinging a site, or running Ruby code as soon as those keywords appear}
14
+
15
+ s.required_rubygems_version = ">= 1.3.6" # TODO test earliest dependency
16
+ s.add_development_dependency "minitest"
17
+ s.add_dependency "crack"
18
+ s.rubyforge_project = "herald"
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- test/*`.split("\n")
22
+ # s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ["lib"]
24
+ end
@@ -0,0 +1,178 @@
1
+ require 'rubygems'
2
+ #require 'json'
3
+ gem 'crack'
4
+ require 'crack'
5
+
6
+ require 'herald/watcher'
7
+ require 'herald/notifier'
8
+ require 'herald/notifiers/stdout'
9
+ require 'herald/item'
10
+
11
+ class Herald
12
+
13
+ @@heralds = []
14
+ attr_accessor :watchers, :keep_alive, :subprocess
15
+
16
+ def self.watch(&block)
17
+ new(&block).start
18
+ end
19
+
20
+ def self.once(&block)
21
+ herald = new()
22
+ herald.keep_alive = false
23
+ herald.send(:instance_eval, &block)
24
+ herald.start
25
+ end
26
+
27
+ # shorthand methods
28
+ def self.watch_twitter(&block)
29
+ watch() { check(:twitter, &block) }
30
+ end
31
+ def self.watch_rss(options = {}, &block)
32
+ watch() { check(:rss, options, &block) }
33
+ end
34
+
35
+ # batch methods
36
+ def self.start
37
+ return false if @@heralds.empty?
38
+ @@heralds.each do |herald|
39
+ herald.start
40
+ end
41
+ true
42
+ end
43
+
44
+ def self.stop
45
+ return false unless Herald.alive?
46
+ @@heralds.each do |herald|
47
+ herald.stop
48
+ end
49
+ true
50
+ end
51
+
52
+ # stop all heralds, and remove them
53
+ # from list of herald instances. mostly
54
+ # useful for clearing @@heralds when testing
55
+ def self.clear
56
+ stop()
57
+ @@heralds.clear
58
+ true
59
+ end
60
+
61
+ # just walk away, leaving whatever strays to themselves
62
+ def self.demonize!
63
+ @@heralds.clear
64
+ true
65
+ end
66
+
67
+ def self.alive?
68
+ @@heralds.any? { |h| h.alive? }
69
+ end
70
+
71
+ def self.heralds
72
+ @@heralds
73
+ end
74
+
75
+ #
76
+ # instance methods:
77
+ #
78
+
79
+ def initialize(&block)
80
+ @watchers = []
81
+ @keep_alive = true
82
+ if block_given?
83
+ instance_eval(&block)
84
+ end
85
+ @@heralds << self
86
+ self
87
+ end
88
+
89
+ # create a new Watcher
90
+ def check(type, options = {}, &block)
91
+ @watchers << Herald::Watcher.new(type, @keep_alive, options, &block)
92
+ end
93
+
94
+ # send keywords to Watchers
95
+ def _for(*keywords)
96
+ @watchers.each do |watcher|
97
+ watcher._for(*keywords)
98
+ end
99
+ end
100
+
101
+ # send instructions to Watchers
102
+ def action(type = nil, options = {}, &block)
103
+ @watchers.each do |watcher|
104
+ watcher.action(type, options, &block)
105
+ end
106
+ end
107
+
108
+ # send sleep time to Watchers
109
+ def every(time)
110
+ @watchers.each do |watcher|
111
+ watcher.every(time)
112
+ end
113
+ end
114
+
115
+ # start Watchers
116
+ def start
117
+ if @watchers.empty?
118
+ raise "No watchers assigned"
119
+ end
120
+ # if herald is already running, first stop
121
+ stop() if alive?
122
+ # start watching as a @subprocess
123
+ @subprocess = fork {
124
+ @watchers.each do |watcher|
125
+ watcher.start
126
+ end
127
+ # all watchers do their tasks in a new thread.
128
+ # join all thread in this @subprocess
129
+ Thread.list.each do |thread|
130
+ thread.join unless thread == Thread.main
131
+ end
132
+ }
133
+ # if herald process is persistant
134
+ if @keep_alive
135
+ Process.detach(@subprocess)
136
+ else
137
+ # wait before the end of this script
138
+ # for all watchers to finish their jobs
139
+ Process.waitpid(@subprocess)
140
+ end
141
+ self # return instance object
142
+ end
143
+
144
+ # is there a gentler way of doing it?
145
+ # or have watchers do cleanup tasks on exit?
146
+ # look at GOD
147
+ def stop
148
+ Process.kill("TERM", @subprocess) if @subprocess
149
+ @subprocess = nil
150
+ self
151
+ end
152
+ alias :end :stop
153
+ alias :kill :stop
154
+
155
+ def alive?
156
+ !!@subprocess
157
+ end
158
+
159
+ def to_s
160
+ "#{alive? ? 'Alive' : 'Stopped'}Alive"
161
+ end
162
+
163
+ private
164
+
165
+ def self.lazy_load_module(module_path)
166
+ lazy_load("herald/#{module_path}")
167
+ end
168
+
169
+ def self.lazy_load(path)
170
+ require(path)
171
+ end
172
+
173
+ end
174
+
175
+ # queue a block to always stop all forked processes on exit
176
+ at_exit do
177
+ Herald.stop
178
+ end
@@ -0,0 +1,13 @@
1
+ class Herald
2
+ class Item
3
+
4
+ attr_accessor :title, :message, :data
5
+
6
+ def initialize(title, message, data)
7
+ @title = title
8
+ @message = message
9
+ @data = data
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ class Herald
2
+ class Watcher
3
+
4
+ class Notifier
5
+
6
+ # TODO, make this dynamic? to allow people to write
7
+ # their own and drop it into notifiers dir
8
+ @@notifier_types = [:callback, :growl, :ping, :post, :stdout]
9
+ DEFAULT_NOTIFIER = :stdout
10
+
11
+ def initialize(type, options, &block)
12
+ type = type.to_sym
13
+ # check notifier type
14
+ unless @@notifier_types.include?(type)
15
+ raise ArgumentError, "#{type} is not a valid Notifier type"
16
+ end
17
+ Herald::lazy_load_module("notifiers/#{type}")
18
+ # extend class with module
19
+ send(:extend, eval(type.to_s.capitalize))
20
+ # each individual Notifier will handle their options
21
+ parse_options(options)
22
+ # if callback given
23
+ if block_given?
24
+ @proc = block
25
+ end
26
+ # call test() method of Notifier module
27
+ test()
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ class Herald
2
+ class Watcher
3
+ class Notifier
4
+
5
+ module Callback
6
+
7
+ attr_reader :proc
8
+
9
+ # no options for Callback
10
+ def parse_options(options); end
11
+
12
+ # we can't sandbox the proc to test it out,
13
+ # so just return true
14
+ def test; true; end
15
+
16
+ def notify(item)
17
+ @proc.call
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ class Herald
2
+ class Watcher
3
+ class Notifier
4
+
5
+ module Growl
6
+
7
+ # no options to parse for Growl
8
+ def parse_options(options); end
9
+
10
+ # test growl on system and throw exception if fail.
11
+ def test
12
+ # TODO suppress output
13
+ unless(system("growl --version"))
14
+ # TODO throw custom exception
15
+ raise "ruby-growl gem is not installed. Run:\n\tsudo gem install ruby-growl"
16
+ end
17
+ end
18
+
19
+ # send a Growl notification
20
+ def notify(item)
21
+ # response will be false if system() call exits with an error.
22
+ # presence of ruby-growl has already been tested in test(), so a false
23
+ # return is likely to be the user's Growl settings
24
+ # TODO escape characters in title and message
25
+ item.title.gsub!("'", '')
26
+ item.message.gsub!("'", '')
27
+ if !system("growl -H localhost -t '#{item.title}' -m '#{item.message}'")
28
+ # TODO throw custom exception
29
+ raise "Growl settings not configured to allow remote application registration. See Growl website docs: http://growl.info/documentation/exploring-preferences.php"
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ class Herald
2
+ class Watcher
3
+ class Notifier
4
+
5
+ module Ping
6
+
7
+ attr_reader :uri
8
+
9
+ # lazy-load net/http when this Module is used
10
+ def self.extended(base)
11
+ Herald.lazy_load('net/http')
12
+ end
13
+
14
+ # note: dupe between ping and post
15
+ def parse_options(options)
16
+ begin
17
+ @uri = URI.parse(options.delete(:uri) || options.delete(:url))
18
+ # if URI lib can't resolve a protocol (because it was missing from string)
19
+ if @uri.class == URI::Generic
20
+ @uri = URI.parse("http://#{@uri.path}")
21
+ end
22
+ rescue URI::InvalidURIError
23
+ raise ArgumentError, ":uri for :ping action not specified or invalid"
24
+ end
25
+ end
26
+
27
+ def test
28
+ response = Net::HTTP.new(@uri.host).head('/')
29
+ return if response.kind_of?(Net::HTTPOK)
30
+ # TODO raise custom error types
31
+ if response.kind_of?(Net::HTTPFound)
32
+ raise "URI #{@uri} is being redirected to #{response.header['location']}"
33
+ else
34
+ raise "URI #{@uri} cannot be reached. Ping returned status code: #{response.code}"
35
+ end
36
+ end
37
+
38
+ def notify(item)
39
+ Net::HTTP.new(@uri.host).head('/')
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ class Herald
2
+ class Watcher
3
+ class Notifier
4
+
5
+ # note most of this code is duplicated between ping and post
6
+ module Post
7
+
8
+ attr_reader :uri
9
+
10
+ # lazy-load net/http when this Module is used
11
+ def self.extended(base)
12
+ Herald.lazy_load('net/http')
13
+ end
14
+
15
+ def parse_options(options)
16
+ begin
17
+ @uri = URI.parse(options.delete(:uri) || options.delete(:url))
18
+ # if URI lib can't resolve a protocol (because it was missing from string)
19
+ if @uri.class == URI::Generic
20
+ @uri = URI.parse("http://#{@uri.path}")
21
+ end
22
+ rescue URI::InvalidURIError
23
+ raise ArgumentError, ":uri for :ping action not specified or invalid"
24
+ end
25
+ end
26
+
27
+ # test by pinging to URI throw exception if fail
28
+ def test
29
+ response = Net::HTTP.new(@uri.host).head('/')
30
+ return if response.kind_of?(Net::HTTPOK)
31
+ # TODO raise custom error types
32
+ if response.kind_of?(Net::HTTPFound)
33
+ raise "URI #{@uri} is being redirected to #{response.header['location']}"
34
+ else
35
+ raise "URI #{@uri} cannot be reached. Ping returned status code: #{response.code}"
36
+ end
37
+ end
38
+
39
+ def notify(item)
40
+ Net::HTTP.post_form(@uri.to_s, { "title" => item.title, "message" => item.message })
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,27 @@
1
+ class Herald
2
+ class Watcher
3
+ class Notifier
4
+
5
+ module Stdout
6
+
7
+ def parse_options(options)
8
+ # option to send stdout to file
9
+ if file = options.delete(:file)
10
+ $stdout = File.new(file, 'w')
11
+ end
12
+ end
13
+
14
+ # is always working, so true
15
+ def test; true; end
16
+
17
+ # print to $stdout
18
+ def notify(item)
19
+ $stdout.puts item.data.inspect
20
+ $stdout.flush
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ class Herald
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,99 @@
1
+ class Herald
2
+
3
+ class Watcher
4
+
5
+ @@watcher_types = [:rss, :twitter] # :imap
6
+ DEFAULT_TIMER = 60
7
+
8
+ attr_reader :type, :keep_alive, :thread, :items
9
+ attr_accessor :notifiers, :keywords, :timer
10
+
11
+ def initialize(type, keep_alive, options, &block)
12
+ @type = type.to_sym
13
+ # TODO this is prepared to handle other protocols, but might not be necessary
14
+ # if @type == :inbox
15
+ # if options.key?(:imap)
16
+ # @type = :imap
17
+ # options[:host] = options[:imap]
18
+ # end
19
+ # end
20
+ # check watcher type
21
+ unless @@watcher_types.include?(@type)
22
+ raise ArgumentError, "#{@type} is not a valid Watcher type"
23
+ end
24
+ @keep_alive = keep_alive
25
+ @keywords = []
26
+ @notifiers = []
27
+ @items = []
28
+ @timer = Watcher::DEFAULT_TIMER
29
+ Herald.lazy_load_module("watchers/#{@type}")
30
+ # extend class with module
31
+ send(:extend, eval(@type.to_s.capitalize))
32
+ # each individual Watcher will handle their options
33
+ parse_options(options)
34
+ # eval the block, if given
35
+ if block_given?
36
+ instance_eval(&block)
37
+ end
38
+ # TODO implement a Watcher::test()?
39
+ end
40
+
41
+ def _for(*keywords)
42
+ @keywords += keywords.flatten
43
+ end
44
+
45
+ # assign the Notifier
46
+ def action(type = :callback, options = {}, &block)
47
+ @notifiers << Herald::Watcher::Notifier.new(type, options, &block)
48
+ end
49
+
50
+ # parse a hash like { 120 => "seconds" }
51
+ def every(time);
52
+ quantity = time.keys.first.to_i
53
+ unit = case time.values.first
54
+ when /^second/
55
+ 1
56
+ when /^minute/
57
+ 60
58
+ when /^hour/
59
+ 3600
60
+ when /^day/
61
+ 86400
62
+ else
63
+ raise ArgumentError, "Invalid time unit for every. (Use seconds, minutes, hours or days)"
64
+ end
65
+ @timer = quantity * unit
66
+ end
67
+
68
+ # call the Notifier and pass it a message
69
+ def notify(item)
70
+ @notifiers.each do |notifier|
71
+ notifier.notify(item)
72
+ end
73
+ end
74
+
75
+ def start
76
+ # set a default Notifier for this Watcher
77
+ action(Watcher::Notifier::DEFAULT_NOTIFIER) if @notifiers.empty?
78
+ # prepare() is defined in the individual Watcher modules.
79
+ # any pre-tasks are performed before
80
+ prepare()
81
+ # begin loop, which will execute at least once (like a do-while loop)
82
+ @thread = Thread.new {
83
+ begin
84
+ activities
85
+ sleep @timer if @keep_alive
86
+ end while @keep_alive
87
+ }
88
+ end
89
+
90
+ def stop
91
+ # stop looping
92
+ @keep_alive = false
93
+ # cleanup() is defined in the individual Watcher modules
94
+ cleanup()
95
+ end
96
+
97
+ end
98
+
99
+ end
@@ -0,0 +1,55 @@
1
+ class Herald
2
+ class Watcher
3
+
4
+ module Imap
5
+
6
+ attr_accessor :user, :pass, :host, :mailbox, :last_uid
7
+
8
+ # lazy-load net/imap when this Module is used
9
+ def self.extended(base)
10
+ Herald.lazy_load('net/imap')
11
+ Herald.lazy_load('date')
12
+ end
13
+
14
+ def parse_options(options)
15
+ @user = options.delete(:user)
16
+ @pass = options.delete(:pass)
17
+ @host = options.delete(:host)
18
+ @mailbox = options.delete(:mailbox) || "INBOX"
19
+ @mailbox.upcase!
20
+ # start looking a week ago unless option given
21
+ @start_date = options.delete(:start_date) || (Date.today - 7).strftime("%d-%b-%Y")
22
+ end
23
+
24
+ def prepare; end
25
+ def cleanup; end
26
+
27
+ def to_s
28
+ "Herald IMAP Watcher, Host: #{@host}, Keywords: '#{@keywords}', Timer: #{@timer}, State: #{@keep_alive ? 'Watching' : 'Stopped'}"
29
+ end
30
+
31
+ private
32
+
33
+ def activities
34
+ puts "u: #{@user}"
35
+ puts "p: #{@pass}"
36
+ puts "h: #{@host}"
37
+ imap = Net::IMAP.new(@host)
38
+ imap.login("LOGIN", @user, @pass)
39
+ imap.select(@mailbox)
40
+ @last_look = Time.now
41
+ # if we have the id of the last email looked at
42
+ if @last_uid
43
+ search_result = imap.search(["BODY", @keywords, "SINCE", @start_date])
44
+ else
45
+ search_result = imap.search(["BODY", @keywords, "SINCE", @start_date])
46
+ end
47
+ puts search_result.inspect
48
+ imap.logout
49
+ imap.disconnect
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,64 @@
1
+ class Herald
2
+ class Watcher
3
+
4
+ module Rss
5
+
6
+ attr_accessor :uris
7
+
8
+ # lazy-load open-uri when this Module is used
9
+ def self.extended(base)
10
+ Herald.lazy_load('open-uri')
11
+ end
12
+
13
+ def parse_options(options)
14
+ @uris = Array(options.delete(:from))
15
+ if @uris.empty?
16
+ raise ArgumentError, "RSS source not specified in :from Hash"
17
+ end
18
+ end
19
+
20
+ def prepare; end
21
+ def cleanup; end
22
+
23
+ def to_s
24
+ "Herald RSS Watcher, URL: #{@uris}, Keywords: '#{@keywords}', Timer: #{@timer}, State: #{@keep_alive ? 'Watching' : 'Stopped'}"
25
+ end
26
+
27
+ private
28
+
29
+ def activities
30
+ @uris.each do |uri|
31
+ # return response as string and parse to RSS
32
+ begin
33
+ rss = Crack::XML.parse(open(uri).read)
34
+ rescue
35
+ return
36
+ end
37
+ # skip if rss variable is nil, or is missing
38
+ # rss elements in the expected nested format
39
+ next unless defined?(rss["rss"]["channel"]["item"])
40
+ # ignore items that have been part of a notification round
41
+ # or that don't contain the keywords being looked for
42
+ items = []
43
+ rss["rss"]["channel"]["item"].each do |item|
44
+ # if we've already seen this item, skip to next item
45
+ if @items.include?(item)
46
+ next
47
+ end
48
+ # keep this item if it contains keywords in the title or description
49
+ if "#{item["title"]}#{item["description"]}".match(/#{@keywords.join('|')}/i)
50
+ items << item
51
+ end
52
+ end
53
+ return if items.empty?
54
+ items.each do |item|
55
+ notify(Item.new(item["title"], item["description"], item))
56
+ end
57
+ @items += items
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,54 @@
1
+ class Herald
2
+ class Watcher
3
+
4
+ # TODO, ignore retweets, if option passed
5
+ module Twitter
6
+
7
+ attr_accessor :uri, :last_tweet_id
8
+ TWITTER_API = "http://search.twitter.com/search.json"
9
+
10
+ # lazy-load open-uri when this Module is used
11
+ def self.extended(base)
12
+ Herald.lazy_load('open-uri')
13
+ end
14
+
15
+ def parse_options(options); end
16
+
17
+ # executed before Watcher starts
18
+ def prepare
19
+ # twitter doesn't allow you to query its api without
20
+ # providing a query string
21
+ if @keywords.empty?
22
+ raise ArgumentError, "Keywords are missing"
23
+ end
24
+ # initialise array, first element will be the Twitter API with search query string,
25
+ # the second element will be a "since_id" extra query parameter, added at close of
26
+ # activities() loop
27
+ @uri = ["#{TWITTER_API}?q=#{@keywords.join('+')}"]
28
+ end
29
+
30
+ def cleanup; end
31
+
32
+ def to_s
33
+ "Herald Twitter Watcher, Keywords: '#{@keywords}', Timer: #{@timer}, State: #{@keep_alive ? 'Watching' : 'Stopped'}"
34
+ end
35
+
36
+ private
37
+
38
+ def activities
39
+ # return response as string from Twitter and parse it to JSON
40
+ json = Crack::JSON.parse(open(@uri.join("&")).read)
41
+ # will be nil if there are no results
42
+ return if json["results"].nil?
43
+ @last_tweet_id = json["max_id"]
44
+ json["results"].each do |tweet|
45
+ @items << tweet
46
+ notify(Item.new("@#{tweet['from_user']}", tweet['text'], json['results']))
47
+ end
48
+ @uri = [@uri.first, "since_id=#{@last_tweet_id}"]
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,110 @@
1
+ # start tests by 'rake test' on root path
2
+
3
+ describe Herald do
4
+ before do
5
+ @herald = Herald.watch { check :twitter; _for "test" }
6
+ end
7
+
8
+ after do
9
+ Herald.clear
10
+ end
11
+
12
+ describe "initialisation with watchers" do
13
+ it "must throw an acception if no 'check' param is given" do
14
+ assert_raises(RuntimeError) do
15
+ Herald.watch {}
16
+ end
17
+ end
18
+ it "must assign a watcher" do
19
+ @herald.watchers.size.must_equal(1)
20
+ end
21
+ it "must allow assigning multiple watchers" do
22
+ herald = Herald.watch { check :rss, :from => "http://example.com"; check :twitter; _for "test" }
23
+ herald.watchers.size.must_equal(2)
24
+ end
25
+ end
26
+
27
+ describe "initialisation of twitter watcher" do
28
+ it "must throw an error if no keywords are given" do
29
+ skip("Test not working")
30
+ assert_raises(ArgumentError) do
31
+ Herald.watch { check :twitter }
32
+ end
33
+ end
34
+ it "must assign a twitter watcher" do
35
+ @herald.watchers.first.type.must_equal(:twitter)
36
+ end
37
+ end
38
+
39
+ describe "initialisation of rss watcher" do
40
+ it "must throw an error if no rss url is given" do
41
+ assert_raises(ArgumentError) do
42
+ Herald.watch { check :rss }
43
+ end
44
+ end
45
+ it "must assign an rss watcher" do
46
+ herald = Herald.watch { check :rss, :from => "http://example.com" }
47
+ herald.watchers.first.type.must_equal(:rss)
48
+ end
49
+ end
50
+
51
+ describe "initialisation with keywords" do
52
+ it "must assign keyword when passed as a string" do
53
+ @herald.watchers.first.keywords.must_be_kind_of(Array)
54
+ @herald.watchers.first.keywords.size.must_equal(1)
55
+ @herald.watchers.first.keywords.to_s.must_equal("test")
56
+ end
57
+ it "must slurp keywords when passed as a multiargument strings" do
58
+ keywords = ["test1", "test2"]
59
+ herald = Herald.watch { check :twitter; _for *keywords }
60
+ herald.watchers.first.keywords.size.must_equal(2)
61
+ herald.watchers.first.keywords.to_s.must_equal(keywords.to_s)
62
+ end
63
+ it "must assign keywords when passed as an array" do
64
+ keywords = ["test1", "test2"]
65
+ herald = Herald.watch { check :twitter; _for keywords }
66
+ herald.watchers.first.keywords.size.must_equal(2)
67
+ herald.watchers.first.keywords.to_s.must_equal(keywords.to_s)
68
+ end
69
+ end
70
+
71
+ describe "initialisation with actions" do
72
+ it "should assign stdout as the default Notifier" do
73
+ skip("@notifiers appears as empty, possible due to threading?")
74
+ @herald.watchers.first.notifiers.size.must_equal(1)
75
+ end
76
+ end
77
+
78
+ describe "Herald class methods (stop, start, alive?) must allow batch operations" do
79
+ it "must return array of running heralds" do
80
+ Herald.heralds.must_be_kind_of(Array)
81
+ Herald.heralds.size.must_equal(1)
82
+ Herald.heralds.first.must_be_kind_of(Herald)
83
+ herald_2 = Herald.watch { check :twitter; _for "test" }
84
+ Herald.heralds.size.must_equal(2)
85
+ end
86
+ it "must report on if there are any heralds alive" do
87
+ Herald.alive?.must_equal(true)
88
+ end
89
+ it "must allow heralds to be stopped" do
90
+ Herald.stop.must_equal(true)
91
+ end
92
+ it "alive? must report false if heralds exist, but none are running" do
93
+ Herald.stop
94
+ Herald.heralds.size.must_equal(1)
95
+ Herald.alive?.must_equal(false)
96
+ end
97
+ it "must report false if heralds are told to stop, and no herald are alive" do
98
+ Herald.stop
99
+ Herald.stop.must_equal(false)
100
+ end
101
+ it "must allow heralds to be restarted" do
102
+ Herald.stop
103
+ Herald.start
104
+ Herald.alive?.must_equal(true)
105
+ end
106
+
107
+ end
108
+
109
+
110
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: herald
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ version: "0.1"
10
+ platform: ruby
11
+ authors:
12
+ - Luke Duncalfe
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-04-14 00:00:00 +12:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: minitest
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: crack
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ description: Pass Herald some keywords and sources to watch, and Herald will notify you using Growl, email, pinging a site, or running Ruby code as soon as those keywords appear
49
+ email:
50
+ - lduncalfe@eml.cc
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files: []
56
+
57
+ files:
58
+ - .gitignore
59
+ - Gemfile
60
+ - README.md
61
+ - Rakefile
62
+ - TODO.txt
63
+ - herald.gemspec
64
+ - lib/herald.rb
65
+ - lib/herald/item.rb
66
+ - lib/herald/notifier.rb
67
+ - lib/herald/notifiers/callback.rb
68
+ - lib/herald/notifiers/growl.rb
69
+ - lib/herald/notifiers/ping.rb
70
+ - lib/herald/notifiers/post.rb
71
+ - lib/herald/notifiers/stdout.rb
72
+ - lib/herald/version.rb
73
+ - lib/herald/watcher.rb
74
+ - lib/herald/watchers/imap.rb
75
+ - lib/herald/watchers/rss.rb
76
+ - lib/herald/watchers/twitter.rb
77
+ - test/herald_test.rb
78
+ has_rdoc: true
79
+ homepage: http://github.com/lukes/herald
80
+ licenses: []
81
+
82
+ post_install_message:
83
+ rdoc_options: []
84
+
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ hash: 3
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ hash: 23
102
+ segments:
103
+ - 1
104
+ - 3
105
+ - 6
106
+ version: 1.3.6
107
+ requirements: []
108
+
109
+ rubyforge_project: herald
110
+ rubygems_version: 1.6.2
111
+ signing_key:
112
+ specification_version: 3
113
+ summary: A simple notifier for Twitter, RSS, or email
114
+ test_files:
115
+ - test/herald_test.rb