campfire-bot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. data/.autotest +11 -0
  2. data/.gitignore +6 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +57 -0
  5. data/README.textile +52 -0
  6. data/TODO +72 -0
  7. data/bin/bot +13 -0
  8. data/campfire-bot.gemspec +27 -0
  9. data/cfbot-stop.sh +8 -0
  10. data/config.example.yml +31 -0
  11. data/lib/bot.rb +194 -0
  12. data/lib/event.rb +114 -0
  13. data/lib/message.rb +30 -0
  14. data/lib/plugin.rb +77 -0
  15. data/lib/version.rb +3 -0
  16. data/plugins/accountability.rb +45 -0
  17. data/plugins/austin.rb +29 -0
  18. data/plugins/basecamp.rb +48 -0
  19. data/plugins/beer.rb +214 -0
  20. data/plugins/beijing_tally.rb +30 -0
  21. data/plugins/boop.rb +127 -0
  22. data/plugins/bruce.rb +15 -0
  23. data/plugins/bugzilla.rb +198 -0
  24. data/plugins/calvin.rb +43 -0
  25. data/plugins/chuck.rb +23 -0
  26. data/plugins/dilbert.rb +51 -0
  27. data/plugins/excuse.rb +478 -0
  28. data/plugins/fail.rb +16 -0
  29. data/plugins/figlet.rb +10 -0
  30. data/plugins/fun.rb +95 -0
  31. data/plugins/garfield.rb +43 -0
  32. data/plugins/generic_search.rb +66 -0
  33. data/plugins/help.rb +13 -0
  34. data/plugins/infobot.rb +58 -0
  35. data/plugins/insult.rb +87 -0
  36. data/plugins/jira.rb +197 -0
  37. data/plugins/lolcats.rb +17 -0
  38. data/plugins/our_quotes.rb +195 -0
  39. data/plugins/quote.rb +31 -0
  40. data/plugins/schneier.rb +28 -0
  41. data/plugins/seen.rb +88 -0
  42. data/plugins/signal_filter.rb +9 -0
  43. data/plugins/svn.rb +167 -0
  44. data/plugins/twitter_echo.rb +54 -0
  45. data/plugins/unfuddle.rb +69 -0
  46. data/plugins/weather.rb +25 -0
  47. data/plugins/xkcd.rb +43 -0
  48. data/spec/beer_spec.rb +224 -0
  49. data/spec/bugzilla_spec.rb +90 -0
  50. data/spec/command_spec.rb +96 -0
  51. data/spec/jira_spec.rb +264 -0
  52. data/spec/our_quotes_spec.rb +186 -0
  53. data/spec/plugin_spec.rb +43 -0
  54. data/spec/spec.opts +1 -0
  55. data/vendor/escape/ChangeLog +30 -0
  56. data/vendor/escape/Makefile +5 -0
  57. data/vendor/escape/README +81 -0
  58. data/vendor/escape/VERSION +1 -0
  59. data/vendor/escape/escape.rb +302 -0
  60. data/vendor/escape/install.rb +109 -0
  61. data/vendor/escape/misc/README.erb +85 -0
  62. data/vendor/escape/rdoc/classes/Escape.html +427 -0
  63. data/vendor/escape/rdoc/classes/Escape.src/M000022.html +19 -0
  64. data/vendor/escape/rdoc/classes/Escape.src/M000023.html +32 -0
  65. data/vendor/escape/rdoc/classes/Escape.src/M000024.html +24 -0
  66. data/vendor/escape/rdoc/classes/Escape.src/M000025.html +19 -0
  67. data/vendor/escape/rdoc/classes/Escape.src/M000026.html +48 -0
  68. data/vendor/escape/rdoc/classes/Escape.src/M000027.html +19 -0
  69. data/vendor/escape/rdoc/classes/Escape.src/M000028.html +19 -0
  70. data/vendor/escape/rdoc/classes/Escape/HTMLAttrValue.html +113 -0
  71. data/vendor/escape/rdoc/classes/Escape/HTMLEscaped.html +113 -0
  72. data/vendor/escape/rdoc/classes/Escape/PercentEncoded.html +113 -0
  73. data/vendor/escape/rdoc/classes/Escape/ShellEscaped.html +113 -0
  74. data/vendor/escape/rdoc/classes/Escape/StringWrapper.html +242 -0
  75. data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000029.html +18 -0
  76. data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000030.html +18 -0
  77. data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000031.html +18 -0
  78. data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000032.html +18 -0
  79. data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000033.html +18 -0
  80. data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000035.html +18 -0
  81. data/vendor/escape/rdoc/classes/TestEscapeHTML.html +182 -0
  82. data/vendor/escape/rdoc/classes/TestEscapeHTML.src/M000008.html +18 -0
  83. data/vendor/escape/rdoc/classes/TestEscapeHTML.src/M000009.html +18 -0
  84. data/vendor/escape/rdoc/classes/TestEscapeHTML.src/M000010.html +18 -0
  85. data/vendor/escape/rdoc/classes/TestEscapeHTML.src/M000011.html +18 -0
  86. data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.html +182 -0
  87. data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.src/M000012.html +18 -0
  88. data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.src/M000013.html +19 -0
  89. data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.src/M000014.html +20 -0
  90. data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.src/M000015.html +22 -0
  91. data/vendor/escape/rdoc/classes/TestEscapeShellEscaped.html +167 -0
  92. data/vendor/escape/rdoc/classes/TestEscapeShellEscaped.src/M000016.html +18 -0
  93. data/vendor/escape/rdoc/classes/TestEscapeShellEscaped.src/M000017.html +20 -0
  94. data/vendor/escape/rdoc/classes/TestEscapeShellEscaped.src/M000018.html +20 -0
  95. data/vendor/escape/rdoc/classes/TestEscapeStringWrapper.html +167 -0
  96. data/vendor/escape/rdoc/classes/TestEscapeStringWrapper.src/M000019.html +20 -0
  97. data/vendor/escape/rdoc/classes/TestEscapeStringWrapper.src/M000020.html +24 -0
  98. data/vendor/escape/rdoc/classes/TestEscapeStringWrapper.src/M000021.html +22 -0
  99. data/vendor/escape/rdoc/files/escape_rb.html +136 -0
  100. data/vendor/escape/rdoc/files/install_rb.html +250 -0
  101. data/vendor/escape/rdoc/files/install_rb.src/M000001.html +23 -0
  102. data/vendor/escape/rdoc/files/install_rb.src/M000002.html +31 -0
  103. data/vendor/escape/rdoc/files/install_rb.src/M000003.html +27 -0
  104. data/vendor/escape/rdoc/files/install_rb.src/M000004.html +27 -0
  105. data/vendor/escape/rdoc/files/install_rb.src/M000005.html +21 -0
  106. data/vendor/escape/rdoc/files/install_rb.src/M000006.html +23 -0
  107. data/vendor/escape/rdoc/files/install_rb.src/M000007.html +21 -0
  108. data/vendor/escape/rdoc/files/test/test-escape_rb.html +109 -0
  109. data/vendor/escape/rdoc/fr_class_index.html +36 -0
  110. data/vendor/escape/rdoc/fr_file_index.html +29 -0
  111. data/vendor/escape/rdoc/fr_method_index.html +61 -0
  112. data/vendor/escape/rdoc/index.html +24 -0
  113. data/vendor/escape/rdoc/rdoc-style.css +208 -0
  114. data/vendor/escape/test/test-escape.rb +90 -0
  115. metadata +259 -0
@@ -0,0 +1,17 @@
1
+ require 'open-uri'
2
+ require 'hpricot'
3
+ require 'tempfile'
4
+
5
+ class LolCats < CampfireBot::Plugin
6
+ on_command 'lolcat', :lolcats
7
+
8
+ def initialize
9
+ @log = Logging.logger["CampfireBot::Plugin::Lolcat"]
10
+ end
11
+
12
+ def lolcats(msg)
13
+ # Scrape random lolcat
14
+ lolcat = (Hpricot(open('http://icanhascheezburger.com/?random#top'))/'div.entry img').first['src']
15
+ msg.speak(lolcat)
16
+ end
17
+ end
@@ -0,0 +1,195 @@
1
+ require 'yaml'
2
+
3
+ # Remember quotes and quips added upon request. Rather than pulling
4
+ # public quoes form the internet, the idea is that you can maintain
5
+ # your own list of quotes said by people at your campfire.
6
+ #
7
+ # Since this plugin maintains long-term persistent data, not just
8
+ # temporary state, its data file lives under var/ instead of tmp/.
9
+ #
10
+ # You can import from the eggdrop QuoteBot.log with this, which will
11
+ # overwrite any existing campfire-bot quotes:
12
+ #
13
+ # ruby -e '
14
+ # require "yaml"
15
+ # log = File.readlines("QuoteBot.log").collect { |l|
16
+ # action, scope, quoter, room, id, quote = l.split(" ", 6)
17
+ # quote.gsub!(/\x03\d\d/, "") # remove IRC color codes
18
+ # [action, quoter, room, id.sub(/^[(]#/, "").sub(/[)]$/, "").to_i,
19
+ # nil, quote]
20
+ # }
21
+ # File.open("var/quote-log.yml", "w") do |out|
22
+ # YAML.dump(log, out)
23
+ # end
24
+ # puts "wrote #{log.size} log entries"
25
+ # '
26
+ #
27
+ # Issues:
28
+ #
29
+ # * The entire log is rewritten after each add/del, which takes 1.2
30
+ # sec for my 3k quotes. It's the YAML.dump call that's slow. An
31
+ # alternative would be to store each log entry as a document and only
32
+ # append to the file. However, an incomplete write would corrupt the
33
+ # file. Maybe there's a way to discard the last, corrupt yaml
34
+ # document? For now, it's fast enough.
35
+ #
36
+ # * Would be nice when there are multiple matches to iterate through
37
+ # them when the same query is repeated.
38
+ #
39
+ # * There is another quote plugin in quote.rb whose model is to pull
40
+ # quotes from the internet. The upside is you have a list of quotes
41
+ # to start with. The downside is that it doesn't foster much sense
42
+ # of community nor develop common culture. To avoid a command name
43
+ # collision, the quote recall command of this plugin may be
44
+ # configured. The default commant is "!ourquote", but "!quote" is
45
+ # more natural.
46
+ #
47
+ class OurQuotes < CampfireBot::Plugin
48
+ on_command 'addquote', :addquote
49
+ on_command 'rmquote', :rmquote
50
+ # Configure with "our_quote_recall_command: quote"
51
+ #on_command 'quote', :quote
52
+
53
+ config_var :data_file, File.join(BOT_ROOT, 'var', 'quote-log.yml')
54
+ config_var :recall_command, "ourquote"
55
+ config_var :debug_enabled, false
56
+
57
+ attr_reader :use_htmlentities
58
+
59
+ def initialize()
60
+ super
61
+
62
+ self.class.on_command recall_command.to_s, :quote
63
+
64
+ # Put this in the constructor so we don't fail to find htmlentities
65
+ # every time we fetch a bug title.
66
+ begin
67
+ require 'htmlentities'
68
+ @use_htmlentities = true
69
+ rescue LoadError
70
+ debug "Falling back to 'cgi', install 'htmlentities' better unescaping"
71
+ require 'cgi'
72
+ end
73
+
74
+ @log = begin
75
+ YAML::load(File.read(@data_file))
76
+ rescue Errno::ENOENT => e
77
+ debug e
78
+ []
79
+ end
80
+ end
81
+
82
+ def debug(spew)
83
+ $stderr.puts "#{self.class.name}: #{spew}" if debug_enabled
84
+ end
85
+
86
+ def addquote(msg)
87
+ debug "Addquote requested: #{msg}"
88
+ if msg[:message].empty?
89
+ msg.speak "Please include the text of the quote and attribution."
90
+ return
91
+ end
92
+ append_add(msg[:person], msg[:room].name, msg[:message])
93
+ # Show the users quote numberes that start with 1 not 0
94
+ msg.speak "Added quote ##{quotes.length}."
95
+ end
96
+
97
+ def append_add(quoter, room, quote)
98
+ debug "ADD: #{quotes.length + 1} #{quote}"
99
+ @log.push ["ADD", quoter, room, quotes.length + 1, Time.now, decode(quote)]
100
+ write_log
101
+ end
102
+
103
+ def decode(str)
104
+ # Unicode decode: The "&" of "&lt;" appear as "\\u0026".
105
+ html = str.gsub(/\\u([0-9a-f]{4})/i) { eval "0x#{$1}.chr" }
106
+ html_decode(html)
107
+ end
108
+
109
+ # Returns the non-HTML version of the given string using
110
+ # htmlentities if available, or else unescapeHTML
111
+ def html_decode(html)
112
+ if use_htmlentities
113
+ HTMLEntities.new.decode(html)
114
+ else
115
+ CGI.unescapeHTML(html)
116
+ end
117
+ end
118
+
119
+ def quote(msg)
120
+ q = msg[:message]
121
+ id = q.to_i
122
+ # If numeric
123
+ if id.to_s == q
124
+ msg.speak quotes[id - 1] ? format_quote(id) : "No quote ##{id}."
125
+ else
126
+ matches = select_quote_ids {|quote| quote.include?(q) }
127
+ msg.speak matches.empty? ?
128
+ "No matching quotes." :
129
+ format_quote(matches[rand(matches.length)]) +
130
+ (q == "" ? "" :
131
+ " (#{matches.size} match#{"es" if matches.size != 1})")
132
+ end
133
+ end
134
+
135
+ def format_quote(id)
136
+ "##{id} #{quotes[id - 1]}"
137
+ end
138
+
139
+ def select_quote_ids
140
+ returning([]) {|ids|
141
+ quotes.each_with_index {|quote,idx|
142
+ if !quote.nil? && yield(quote)
143
+ ids.push(idx + 1)
144
+ end
145
+ }
146
+ }
147
+ end
148
+
149
+ def rmquote(msg)
150
+ q = msg[:message]
151
+ id = q.to_i
152
+ # If numeric
153
+ if id.to_s != q || !quotes[id - 1]
154
+ msg.speak "No quote ##{q}."
155
+ return
156
+ end
157
+ append_del(msg[:person], msg[:room].name, id)
158
+ msg.speak "Deleted quote ##{id}."
159
+ end
160
+
161
+ def append_del(rmer, room, id)
162
+ debug "DEL: #{quotes.length} #{id}"
163
+ @log.push ["DEL", rmer, room, id, Time.now, nil]
164
+ write_log
165
+ end
166
+
167
+ def quotes
168
+ return @quotes if @quotes
169
+ debug "Rebuilding @quotes from @log of length #{@log.size}"
170
+ @quotes = []
171
+ @log.each { |action, quoter, room, id, ts, quote|
172
+ case action
173
+ when "ADD" then
174
+ raise "Bad ID: #{id}" if id - 1 != @quotes.length
175
+ @quotes.push quote
176
+ when "DEL" then
177
+ @quotes[id - 1] = nil
178
+ # Allow any trailing IDs with nil quotes to be reused
179
+ @quotes.pop while @quotes.last.nil? && !@quotes.empty?
180
+ else raise "Unknown log action #{action}"
181
+ end
182
+ }
183
+ @quotes
184
+ end
185
+
186
+ def write_log
187
+ debug "Writing #{@log.length} log entries"
188
+ File.open("#{data_file}.tmp", 'w') do |out|
189
+ YAML.dump(@log, out)
190
+ end
191
+ debug "Renaming .tmp file to #{data_file}"
192
+ File.rename("#{data_file}.tmp", data_file)
193
+ @quotes = nil
194
+ end
195
+ end
@@ -0,0 +1,31 @@
1
+ require 'open-uri'
2
+ require 'hpricot'
3
+
4
+ class Quote < CampfireBot::Plugin
5
+ on_command 'quote', :quote
6
+
7
+ def quote(msg)
8
+ # Get our quotes from the web
9
+ url = "http://quotes4all.net/rss/000010110/quotes.xml"
10
+ response = ''
11
+
12
+ begin
13
+ # open-uri RDoc: http://stdlib.rubyonrails.org/libdoc/open-uri/rdoc/index.html
14
+ open(url, "User-Agent" => "Ruby/#{RUBY_VERSION}",
15
+ "From" => "Campfire") { |f|
16
+
17
+ # Save the response body
18
+ response = f.read
19
+ }
20
+
21
+ # HPricot RDoc: http://code.whytheluckystiff.net/hpricot/
22
+ doc = Hpricot(response)
23
+
24
+ msg.speak((doc/"*/item/description").inner_html.gsub(/<\/?[^>]*>/,"").gsub(/\s+/," ").gsub(/\&quote;/,"'").gsub(/\&[\#|\w]\w+\;/,"").gsub(/\#39\;/,"'"))
25
+ msg.speak((doc/"*/item/title").inner_html)
26
+
27
+ rescue Exception => e
28
+ msg.speak(e, "\n")
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ require 'open-uri'
2
+ require 'hpricot'
3
+
4
+ class Schneier < CampfireBot::Plugin
5
+ BASE_URL = 'http://geekz.co.uk/schneierfacts/'
6
+
7
+ on_command 'schneier', :schneier
8
+
9
+ def schneier(msg)
10
+ quote = case msg[:message].split(/\s+/)[0]
11
+ when 'latest'
12
+ fetch_quote(true)
13
+ when 'random'
14
+ fetch_quote
15
+ else
16
+ fetch_quote
17
+ end
18
+ msg.speak quote
19
+ rescue => e
20
+ msg.speak e
21
+ end
22
+
23
+ private
24
+
25
+ def fetch_quote(latest = false)
26
+ CGI::unescapeHTML((Hpricot(open("#{BASE_URL}#{'fact/latest' if latest}"))).search('p .fact').html)
27
+ end
28
+ end
@@ -0,0 +1,88 @@
1
+ # Courtesy of joshwand (http://github.com/joshwand)
2
+ class Seen < CampfireBot::Plugin
3
+ ACTIVITY_REGEXP = /^(.*)$/
4
+ SEEN_REGEXP = /([^\?]+)(?=\?)*/
5
+
6
+ on_message Regexp.new("#{ACTIVITY_REGEXP.source}", Regexp::IGNORECASE), :update
7
+ on_command 'seen', :seen
8
+ on_command 'reload_seen', :reload
9
+
10
+ def initialize
11
+ @data_file = File.join(BOT_ROOT, 'tmp', "seen-#{BOT_ENVIRONMENT}.yml")
12
+ @seen = YAML::load(File.read(@data_file)) rescue {}
13
+ end
14
+
15
+ def update(msg)
16
+ left_room = (msg[:message] == "has left the room " ? true : false)
17
+ @seen[msg[:person]] = {:time => Time.now, :left => left_room}
18
+
19
+ File.open(@data_file, 'w') do |out|
20
+ YAML.dump(@seen, out)
21
+ end
22
+ end
23
+
24
+ def seen(msg)
25
+ found = false
26
+ puts msg[:message]
27
+ puts msg[:message] =~ SEEN_REGEXP
28
+
29
+ if !$1.nil?
30
+ first_name = $1.match("[A-Za-z]+")[0]
31
+
32
+ @seen.each do |person, seenat|
33
+ if person.downcase.include?(first_name.downcase)
34
+ time_ago = time_ago_in_words(seenat[:time])
35
+ left = seenat[:left] ? "leaving the room " : ""
36
+ msg.speak("#{person} was last seen #{left}#{time_ago} ago")
37
+ found = true
38
+ end
39
+ end
40
+
41
+ if !found
42
+ msg.speak("Sorry, I haven't seen #{first_name}.")
43
+ end
44
+
45
+ end
46
+ end
47
+
48
+ def reload(msg)
49
+ @seen = {}
50
+ msg.speak("ok, reloaded seen db")
51
+ end
52
+
53
+ protected
54
+
55
+ def time_ago_in_words(from_time, include_seconds = false)
56
+ distance_of_time_in_words(from_time, Time.now, include_seconds)
57
+ end
58
+
59
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
60
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
61
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
62
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
63
+ distance_in_seconds = ((to_time - from_time).abs).round
64
+
65
+ case distance_in_minutes
66
+ when 0..1
67
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
68
+ case distance_in_seconds
69
+ when 0..4 then 'less than 5 seconds'
70
+ when 5..9 then 'less than 10 seconds'
71
+ when 10..19 then 'less than 20 seconds'
72
+ when 20..39 then 'half a minute'
73
+ when 40..59 then 'less than a minute'
74
+ else '1 minute'
75
+ end
76
+
77
+ when 2..44 then "#{distance_in_minutes} minutes"
78
+ when 45..89 then 'about 1 hour'
79
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
80
+ when 1440..2879 then '1 day'
81
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
82
+ when 43200..86399 then 'about 1 month'
83
+ when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
84
+ when 525600..1051199 then 'about 1 year'
85
+ else "over #{(distance_in_minutes / 525600).round} years"
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,9 @@
1
+ class SignalFilter < CampfireBot::Plugin
2
+ on_message /^\[.*?\]/i, :echo_signal
3
+
4
+ def echo_signal(msg)
5
+ unless msg[:room] == bot.config['signal_target_room']
6
+ bot.rooms[bot.config['signal_target_room']].speak("#{msg[:person]} #{msg[:message]}")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,167 @@
1
+ require 'open-uri'
2
+ require 'hpricot'
3
+ require 'tempfile'
4
+
5
+ class Svn < CampfireBot::Plugin
6
+
7
+ at_interval 20.minutes, :check_svn
8
+ on_command 'svn', :checksvn_command
9
+
10
+
11
+ def initialize
12
+ # log "initializing... "
13
+ @data_file = File.join(BOT_ROOT, 'tmp', "svn-#{BOT_ENVIRONMENT}-#{bot.config['room']}.yml")
14
+ @cached_revisions = YAML::load(File.read(@data_file)) rescue {}
15
+ @last_checked ||= 10.minutes.ago
16
+ @urls = bot.config['svn_urls']
17
+ @log = Logging.logger["CampfireBot::Plugin::Svn"]
18
+ end
19
+
20
+ # respond to checkjira command-- same as interval except we answer with 'no issues found' if
21
+ def checksvn_command(msg)
22
+ msg.speak "no new commits since I last checked #{@lastlast} ago" if !check_svn(msg)
23
+ end
24
+
25
+ def check_svn(msg)
26
+
27
+ saw_a_commit = false
28
+ old_cache = Marshal::load(Marshal.dump(@cached_revisions)) # since ruby doesn't have deep copy
29
+
30
+ @lastlast = time_ago_in_words(@last_checked)
31
+ commits = fetch_svn_urls
32
+
33
+ commits.each do |commit|
34
+ # p commit
35
+ if new?(commit, old_cache)
36
+ saw_an_issue = true
37
+
38
+ @cached_revisions = update_cache(commit, @cached_revisions)
39
+ flush_cache(@cached_revisions)
40
+
41
+ messagetext = "#{commit[:author]} committed revision #{commit[:revision]} #{time_ago_in_words(commit[:date])} ago on #{commit[:url]}:\n"
42
+
43
+ messagetext += "\n#{commit[:message]}\n"
44
+ messagetext += "----\n"
45
+ commit[:paths].each do |path|
46
+ messagetext += path[:action] + " " + path[:path] + "\n"
47
+ end
48
+
49
+ msg.paste(messagetext)
50
+ @log.info messagetext
51
+
52
+ end
53
+ end
54
+
55
+ @last_checked = Time.now
56
+ @log.info "no new commits." if !saw_a_commit
57
+
58
+ saw_a_commit
59
+ end
60
+
61
+ protected
62
+
63
+ # fetch jira url and return a list of commit Hashes
64
+ def fetch_svn_urls()
65
+ urls = bot.config['svn_urls']
66
+ commits = []
67
+ urls.each do |url|
68
+ begin
69
+ @log.info "checking #{url} for new commits..."
70
+ xmldata = `svn log --xml -v --limit 15 #{url}`
71
+ doc = REXML::Document.new(xmldata)
72
+
73
+ doc.elements.inject('log/logentry', commits) do |commits, element|
74
+ commits.push({:url => url}.merge(parse_entry_info(element)))
75
+ end
76
+
77
+ rescue Exception => e
78
+ @log.error "error connecting to svn: #{e.message}"
79
+ end
80
+ end
81
+ return commits
82
+ end
83
+
84
+ # extract commit hash from indivrevisionual xml element
85
+ def parse_entry_info(xml_element)
86
+
87
+ revision = xml_element.attributes['revision']
88
+ author = xml_element.elements['author'].text
89
+ date = DateTime.parse(xml_element.elements['date'].text)
90
+ message = xml_element.elements['msg'].text
91
+
92
+ paths = xml_element.elements.collect('paths/path') do |e|
93
+ {
94
+ :action => e.attributes['action'],
95
+ :path => e.text
96
+ }
97
+ end
98
+
99
+ return {
100
+ :revision => revision,
101
+ :author => author,
102
+ :message => message,
103
+ :date => date,
104
+ :paths => paths
105
+ }
106
+ end
107
+
108
+ # has this commit been seen before this run?
109
+ def new?(commit, old_cache)
110
+ !old_cache.key?(commit[:url]) or old_cache[commit[:url]] < commit[:revision].to_i
111
+ end
112
+
113
+ # only update the cached highest revision if it is in fact the highest revision
114
+ def update_cache(commit, cache)
115
+ cache[commit[:url]] = commit[:revision].to_i if new?(commit, cache)
116
+ cache
117
+ end
118
+
119
+ # write the cache to disk
120
+ def flush_cache(cache)
121
+ File.open(@data_file, 'w') do |out|
122
+ YAML.dump(cache, out)
123
+ end
124
+ end
125
+
126
+
127
+
128
+ #
129
+ # time/utility functions
130
+ #
131
+
132
+
133
+ def time_ago_in_words(from_time, include_seconds = false)
134
+ distance_of_time_in_words(from_time, Time.now, include_seconds)
135
+ end
136
+
137
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
138
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
139
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
140
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
141
+ distance_in_seconds = ((to_time - from_time).abs).round
142
+
143
+ case distance_in_minutes
144
+ when 0..1
145
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
146
+ case distance_in_seconds
147
+ when 0..4 then 'less than 5 seconds'
148
+ when 5..9 then 'less than 10 seconds'
149
+ when 10..19 then 'less than 20 seconds'
150
+ when 20..39 then 'half a minute'
151
+ when 40..59 then 'less than a minute'
152
+ else '1 minute'
153
+ end
154
+
155
+ when 2..44 then "#{distance_in_minutes} minutes"
156
+ when 45..89 then 'about 1 hour'
157
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
158
+ when 1440..2879 then '1 day'
159
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
160
+ when 43200..86399 then 'about 1 month'
161
+ when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
162
+ when 525600..1051199 then 'about 1 year'
163
+ else "over #{(distance_in_minutes / 525600).round} years"
164
+ end
165
+ end
166
+
167
+ end