herald 0.1

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.
@@ -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