wesabot 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +20 -0
  4. data/README.md +182 -0
  5. data/Rakefile +13 -0
  6. data/bin/wesabot +111 -0
  7. data/config/wesabot.yml.sample +4 -0
  8. data/lib/campfire/bot.rb +53 -0
  9. data/lib/campfire/configuration.rb +65 -0
  10. data/lib/campfire/message.rb +102 -0
  11. data/lib/campfire/polling_bot/plugin.rb +162 -0
  12. data/lib/campfire/polling_bot/plugins/airbrake/.gitignore +1 -0
  13. data/lib/campfire/polling_bot/plugins/airbrake/airbrake/api.rb +48 -0
  14. data/lib/campfire/polling_bot/plugins/airbrake/airbrake/error.rb +51 -0
  15. data/lib/campfire/polling_bot/plugins/airbrake/airbrake_plugin.rb +63 -0
  16. data/lib/campfire/polling_bot/plugins/airbrake/airbrake_plugin.yml.sample +3 -0
  17. data/lib/campfire/polling_bot/plugins/bookmark/bookmark.rb +15 -0
  18. data/lib/campfire/polling_bot/plugins/bookmark/bookmark_plugin.rb +89 -0
  19. data/lib/campfire/polling_bot/plugins/debug/debug_plugin.rb +23 -0
  20. data/lib/campfire/polling_bot/plugins/deploy/deploy_plugin.rb +155 -0
  21. data/lib/campfire/polling_bot/plugins/deploy/deploy_plugin.yml.sample +9 -0
  22. data/lib/campfire/polling_bot/plugins/deploy/spec/deploy_plugin_spec.rb +153 -0
  23. data/lib/campfire/polling_bot/plugins/greeting/greeting_plugin.rb +146 -0
  24. data/lib/campfire/polling_bot/plugins/greeting/greeting_setting.rb +19 -0
  25. data/lib/campfire/polling_bot/plugins/greeting/spec/greeting_plugin_spec.rb +51 -0
  26. data/lib/campfire/polling_bot/plugins/greeting/spec/greeting_setting_spec.rb +61 -0
  27. data/lib/campfire/polling_bot/plugins/help/help_plugin.rb +53 -0
  28. data/lib/campfire/polling_bot/plugins/history/history_plugin.rb +34 -0
  29. data/lib/campfire/polling_bot/plugins/image_search/displayed_image.rb +8 -0
  30. data/lib/campfire/polling_bot/plugins/image_search/image_search_plugin.rb +106 -0
  31. data/lib/campfire/polling_bot/plugins/interjector/interjector_plugin.rb +58 -0
  32. data/lib/campfire/polling_bot/plugins/kibitz/kibitz_plugin.rb +90 -0
  33. data/lib/campfire/polling_bot/plugins/kibitz/spec/kibitz_plugin_spec.rb +56 -0
  34. data/lib/campfire/polling_bot/plugins/plugin.db +0 -0
  35. data/lib/campfire/polling_bot/plugins/reload/reload_plugin.rb +24 -0
  36. data/lib/campfire/polling_bot/plugins/remind_me/remind_me_plugin.rb +149 -0
  37. data/lib/campfire/polling_bot/plugins/remind_me/reminder.rb +8 -0
  38. data/lib/campfire/polling_bot/plugins/shared/message.rb +37 -0
  39. data/lib/campfire/polling_bot/plugins/shared/user.rb +32 -0
  40. data/lib/campfire/polling_bot/plugins/sms/sms_plugin.rb +88 -0
  41. data/lib/campfire/polling_bot/plugins/sms/sms_setting.rb +7 -0
  42. data/lib/campfire/polling_bot/plugins/time/time_plugin.rb +17 -0
  43. data/lib/campfire/polling_bot/plugins/tweet/tweet.rb +52 -0
  44. data/lib/campfire/polling_bot/plugins/tweet/tweet_plugin.rb +117 -0
  45. data/lib/campfire/polling_bot/plugins/tweet/tweet_plugin.yml.sample +4 -0
  46. data/lib/campfire/polling_bot/plugins/twitter_search/twitter_search_plugin.rb +58 -0
  47. data/lib/campfire/polling_bot.rb +125 -0
  48. data/lib/campfire/sample_plugin.rb +56 -0
  49. data/lib/campfire/version.rb +3 -0
  50. data/lib/wesabot.rb +3 -0
  51. data/spec/.gitignore +1 -0
  52. data/spec/polling_bot_spec.rb +23 -0
  53. data/spec/spec_helper.rb +190 -0
  54. data/wesabot.gemspec +55 -0
  55. metadata +336 -0
@@ -0,0 +1,58 @@
1
+ require 'httparty'
2
+
3
+ # plugin to search twitter
4
+ class TwitterSearchPlugin < Campfire::PollingBot::Plugin
5
+ priority 1
6
+ accepts :text_message, :addressed_to_me => true
7
+
8
+ def process(message)
9
+ searched = false
10
+
11
+ case message.command
12
+ when /(?:(?:what(?:'s)? (?:(?:are )?people|(?:is |does )?every(?:one|body)) (?:saying|tweeting|think) about)|what's the word on)\s+(?:the )?(.*?)[.?!]?\s*$/i
13
+ subject = $1
14
+ tweets = search_twitter(subject)
15
+ if tweets.any?
16
+ tweets.each {|t| bot.tweet(t) }
17
+ else
18
+ bot.say("Couldn't find anything for \"#{subject}\"")
19
+ end
20
+ return HALT
21
+ end
22
+ end
23
+
24
+ # return array of available commands and descriptions
25
+ def help
26
+ [
27
+ ["what are (people|is everyone) saying about <subject>", "search twitter for tweets on <subject>"],
28
+ ["what's the word on <subject>", "search twitter for tweets on <subject>"],
29
+ ]
30
+ end
31
+
32
+ private
33
+
34
+ # construct a twitter url from the given response json
35
+ def twitter_url(json)
36
+ "http://twitter.com/#{json['from_user']}/status/#{json['id']}"
37
+ end
38
+
39
+ def search_twitter(subject)
40
+ tweets = []
41
+ res = HTTParty.get(
42
+ "http://search.twitter.com/search.json",
43
+ :query => { :q => subject, :result_type => "mixed" },
44
+ # other possible result types include "popular" and "recent"
45
+ :headers => {'User-Agent' => 'wesabot/1.0 github-hackarts-wesabot'})
46
+ case res.code
47
+ when 200
48
+ tweets = res["results"].first(5).map {|r| twitter_url(r) }
49
+ when 400, 420
50
+ bot.say("Sorry, we've hit the Twitter rate limit for searches.")
51
+ else
52
+ bot.say("Hmm...didn't work. Got this response:")
53
+ bot.paste("#{res.code} (#{res.message})\n#{res.body}")
54
+ end
55
+
56
+ return tweets
57
+ end
58
+ end
@@ -0,0 +1,125 @@
1
+ # PollingBot - a bot that polls the room for messages
2
+ require 'campfire/bot'
3
+ require 'campfire/message'
4
+
5
+ require 'firering'
6
+
7
+ module Campfire
8
+ class PollingBot < Bot
9
+ require 'campfire/polling_bot/plugin'
10
+ attr_accessor :plugins
11
+ HEARTBEAT_INTERVAL = 3 # seconds
12
+
13
+ def initialize(config)
14
+ # load plugin queue, sorting by priority
15
+ super
16
+ self.plugins = Plugin.load_all(self)
17
+ end
18
+
19
+ # main event loop
20
+ def run
21
+ # set up a heartbeat thread for plugins that want them
22
+ Thread.new do
23
+ loop do
24
+ plugins.each {|p| p.heartbeat if p.respond_to?(:heartbeat)}
25
+ sleep HEARTBEAT_INTERVAL
26
+ end
27
+ end
28
+
29
+ host = "https://#{config.subdomain}.campfirenow.com"
30
+ conn = Firering::Connection.new(host) do |c|
31
+ c.token = config.api_token
32
+ c.logger = logger
33
+ c.retry_delay = 2
34
+ end
35
+
36
+ EM.run do
37
+ conn.room(room.id) do |room|
38
+ room.stream do |data|
39
+
40
+ begin
41
+ klass = Campfire.const_get(data.type)
42
+ message = klass.new(data)
43
+
44
+ if data.from_user?
45
+ data.user do |user|
46
+ dbuser = User.first(:campfire_id => user.id)
47
+
48
+ if dbuser.nil?
49
+ dbuser = User.create(
50
+ :campfire_id => user.id,
51
+ :name => user.name
52
+ )
53
+ else
54
+ dbuser.update(:name => user.name)
55
+ end
56
+
57
+ message.user = dbuser
58
+ process(message)
59
+ end
60
+ else
61
+ process(message)
62
+ end
63
+
64
+ rescue => e
65
+ log_error(e)
66
+ end
67
+
68
+ end
69
+ end
70
+
71
+ trap("INT") { EM.stop; raise SystemExit }
72
+ end
73
+
74
+ rescue Exception => e # leave the room if we crash
75
+ if e.kind_of?(SystemExit)
76
+ room.leave
77
+ exit 0
78
+ else
79
+ log_error(e)
80
+ room.leave
81
+ exit 1
82
+ end
83
+ end
84
+
85
+ def process(message)
86
+ logger.debug "processing #{message} (#{message.person} - #{message.body})"
87
+
88
+ # ignore messages from ourself
89
+ return if [message.person, message.person_full_name].include? self.name
90
+
91
+ plugins.each do |plugin|
92
+ begin
93
+ if plugin.accepts?(message)
94
+ logger.debug "sending to plugin #{plugin} (priority #{plugin.priority})"
95
+ status = plugin.process(message)
96
+ if status == Plugin::HALT
97
+ logger.debug "plugin chain halted"
98
+ break
99
+ end
100
+ end
101
+ rescue Exception => e
102
+ say("Oops, #{plugin.class} threw an exception. Enable debugging to see it.") unless debug
103
+ log_error(e)
104
+ break
105
+ end
106
+ end
107
+
108
+ logger.debug "done processing #{message}"
109
+ end
110
+
111
+ # determine if a message is addressed to the bot. if so, store the command in the message
112
+ def addressed_to_me?(message)
113
+ m = message.body.match(/^#{name}(?:[,:]\s*|\s+)(.*)/i)
114
+ m ||= message.body.match(/^\s*(.*?)(?:,\s+)?\b#{name}[.!?\s]*$/i)
115
+ message.command = m[1] if m
116
+ end
117
+
118
+ def log_error(e)
119
+ msg = "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
120
+ logger.error(msg)
121
+ paste(msg) if debug
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,56 @@
1
+ # Sample Campfire plugin. Plugins must be placed in the plugins directory in order to be loaded.
2
+ class SamplePlugin < Campfire::PollingBot::Plugin
3
+ # accept - specify which kinds of messages this plugin accepts. Put each type on its own line.
4
+ # You may optionally set :addressed_to_me => true to get only messages addressed to the bot.
5
+ # For example,
6
+ # accept :text_message, :addressed_to_me => true
7
+ # Will only accept text messages that are in the form "<bot name>, ..." or "... <bot name>".
8
+ # The body of the message minus the bot name will be returned by the 'command' method of the
9
+ # message.
10
+ #
11
+ # Message types are:
12
+ # - :all - all messages
13
+ # - :text_message - normal user text message
14
+ # - :paste_message - a paste
15
+ # - :enter_message - sent when a user enters the room
16
+ # - :leave_message - sent when a user leaves the room
17
+ # - :kick_message - sent when a user times out from inactivity
18
+ # - :lock_message - sent when the room is locked
19
+ # - :unlock_message - sent when the room is unlocked
20
+ # - :allow_guests_message - sent when guest access is turned on
21
+ # - :disallow_guests_message - sent when guest access is turned off
22
+ # - :topic_change_message - sent when the room's topic is changed
23
+
24
+ accepts :text_message, :addressed_to_me => true
25
+ accepts :paste_message
26
+
27
+ # priority is used to determine the plugin's order in the plugin queue. A higher number represents
28
+ # a higher priority. There are no upper or lower bounds. If you don't specify a priority, it defaults
29
+ # to 0.
30
+
31
+ priority 10
32
+
33
+ # If you need to do any one-time setup when the plugin is initially loaded, do it here. Optional.
34
+ def initialize
35
+ end
36
+
37
+ # If your plugin implements the heartbeat method, it will be called every time the bot polls the room
38
+ # for activity (currently every 3 seconds), whether or not there are any new messages. The heartbeat
39
+ # method is optional. It does not take any parameters.
40
+ def heartbeat
41
+ end
42
+
43
+ # process is the only method your plugin needs to implement. This is called by the bot whenever it
44
+ # has a new message that matches one of the message types accepted by the plugin. See Campfire::Message
45
+ # for message documentation.
46
+ # If no other plugins should receive the message after this plugin, return HALT.
47
+ def process(message)
48
+ end
49
+
50
+ # help is actually functionality provided by another plugin, HelpPlugin. Just return an array of
51
+ # ['command', 'description'] tuples
52
+ def help
53
+ [['some command', 'description of some command'],
54
+ ['some other command', 'description of some other command']]
55
+ end
56
+ end
@@ -0,0 +1,3 @@
1
+ module Campfire
2
+ VERSION = "1.0.1"
3
+ end
data/lib/wesabot.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "campfire/version"
2
+ require "campfire/configuration"
3
+ require "campfire/polling_bot"
data/spec/.gitignore ADDED
@@ -0,0 +1 @@
1
+ test.sqlite
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe Campfire::PollingBot do
4
+ before do
5
+ @bot = FakeBot.new
6
+ end
7
+
8
+ it 'recognizes commands addressed to itself' do
9
+ messages = ["wes", "wes?", "wes hi", "wes, hi", "hi, wes", "hi wes",
10
+ "wes: hi"]
11
+ messages.each do |m|
12
+ @bot.addressed_to_me?(Campfire::TextMessage.new(:body => m)).should be_true
13
+ end
14
+ end
15
+
16
+ it 'does not recognize commands not addressed to itself' do
17
+ messages = ["hi", "western", "weswes?"]
18
+ messages.each do |m|
19
+ @bot.addressed_to_me?(Campfire::TextMessage.new(:body => m)).should be_false
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,190 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup
4
+
5
+ $:.push File.expand_path("../lib", __FILE__)
6
+ require 'wesabot'
7
+ require 'rspec'
8
+
9
+ Campfire::PollingBot::Plugin.load_plugin_classes
10
+
11
+ class FakeBot < Campfire::PollingBot
12
+ def initialize
13
+ self.name = 'Wes'
14
+ self.config = Campfire::Configuration.new(:datauri => "sqlite3://#{File.expand_path('../test.sqlite', __FILE__)}")
15
+ Campfire::PollingBot::Plugin.load_all(self)
16
+ end
17
+
18
+ def say(message)
19
+ transcript << [:say, message]
20
+ end
21
+
22
+ def paste(message)
23
+ transcript << [:paste, message]
24
+ end
25
+
26
+ def say_random(messages)
27
+ transcript << [:say, messages.first]
28
+ end
29
+
30
+ def transcript
31
+ @transcript ||= []
32
+ end
33
+ end
34
+
35
+ RSpec.configure do |config|
36
+ def saying(what)
37
+ message Campfire::TextMessage, :body => what
38
+ end
39
+
40
+ alias :asking :saying
41
+ alias :say :saying
42
+
43
+ def entering
44
+ message Campfire::EnterMessage
45
+ end
46
+
47
+ alias :enter :entering
48
+
49
+ def leaving
50
+ message Campfire::LeaveMessage
51
+ end
52
+
53
+ alias :leave :leaving
54
+
55
+ def message(type, params={})
56
+ bot = FakeBot.new
57
+ @plugin.class.bot = bot
58
+ message = type.new(params)
59
+ @user ||= User.create(:name => "John Tester")
60
+ message.user = @user
61
+ @plugin.process(message) if @plugin.accepts?(message)
62
+ return bot.transcript
63
+ end
64
+
65
+ def make_wes_say(what)
66
+ MakeWesSend.new.and_say(what)
67
+ end
68
+
69
+ def make_wes_paste(what)
70
+ MakeWesSend.new.and_paste(what)
71
+ end
72
+
73
+ def make_wes_say_something
74
+ MakeWesSaySomething.new
75
+ end
76
+
77
+ alias :make_wes_say_anything :make_wes_say_something
78
+
79
+ module MessagePrinter
80
+ def print_messages(messages)
81
+ messages.map {|e| " #{e.first} #{e.last.inspect}"}.join("\n")
82
+ end
83
+ end
84
+
85
+ class MakeWesSend
86
+ include MessagePrinter
87
+
88
+ def matches?(actual)
89
+ @actual = actual
90
+
91
+ if actual.size != expected.size
92
+ @failure = [:size, actual.size, expected.size]
93
+ return false
94
+ end
95
+
96
+ actual.zip(expected).each do |a, e|
97
+ if a.first != e.first
98
+ @failure = [:type, a, e]
99
+ return false
100
+ end
101
+
102
+ case e.last
103
+ when Regexp
104
+ if a.last !~ e.last
105
+ @failure = [:match, a, e]
106
+ return false
107
+ end
108
+ else
109
+ if e.last != a.last
110
+ @failure = [:equal, a, e]
111
+ return false
112
+ end
113
+ end
114
+ end
115
+
116
+ return true
117
+ end
118
+
119
+ def failure_message
120
+ failure_type, actual, expected = *@failure
121
+
122
+ case failure_type
123
+ when :size
124
+ "expected #{expected} message(s):\n\n" +
125
+ print_messages(@expected) +
126
+ "\n\ngot #{actual} message(s):\n\n" +
127
+ print_messages(@actual)
128
+ when :type
129
+ "expected Wes to #{expected.first} #{expected.last.inspect}, " +
130
+ "but got #{actual.first} #{actual.last.inspect}"
131
+ when :match
132
+ "expected Wes to #{expected.first} something matching #{expected.last.inspect}, " +
133
+ "but got #{actual.last.inspect}"
134
+ when :equal
135
+ "expected Wes to #{expected.first} #{expected.last.inspect}, " +
136
+ "but got #{actual.last.inspect}"
137
+ else
138
+ @failure.inspect
139
+ end
140
+ end
141
+
142
+ def negative_failure_message
143
+ "expected not to get the following message(s), but did:\n\n" +
144
+ print_messages(@expected)
145
+ end
146
+
147
+ def and(expected)
148
+ expect nil, expected
149
+ end
150
+
151
+ def and_say(expected)
152
+ expect :say, expected
153
+ end
154
+
155
+ def and_paste(expected)
156
+ expect :paste, expected
157
+ end
158
+
159
+ private
160
+
161
+ def expect(type, message)
162
+ type ||= @last_type
163
+ expected << [type, message]
164
+ @last_type = type
165
+ return self
166
+ end
167
+
168
+ def expected
169
+ @expected ||= []
170
+ end
171
+ end
172
+
173
+ class MakeWesSaySomething
174
+ include MessagePrinter
175
+
176
+ def matches?(actual)
177
+ @actual = actual
178
+ actual.any?
179
+ end
180
+
181
+ def failure_message
182
+ "expected Wes to say something, but he didn't"
183
+ end
184
+
185
+ def negative_failure_message
186
+ "expected Wes not to say anything, but he did:\n\n" +
187
+ print_messages(@actual)
188
+ end
189
+ end
190
+ end
data/wesabot.gemspec ADDED
@@ -0,0 +1,55 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "campfire/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "wesabot"
7
+ s.version = Campfire::VERSION
8
+ s.authors = ["Brad Greenlee", "André Arko", "Brian Donovan"]
9
+ s.email = ["brad@footle.org", "andre@arko.net", "me@brian-donovan.com"]
10
+ s.homepage = "https://github.com/hackarts/wesabot"
11
+ s.summary = %q{Wesabe's Campfire bot framework}
12
+ s.description = %q{Wesabot is a Campfire bot framework we've been using and
13
+ developing at Wesabe since not long after our inception. It started as a
14
+ way to avoid parking tickets near our office ("Wes, remind me in 2 hours
15
+ to move my car"), and has evolved into an essential work aid. When you
16
+ enter the room, Wes greets you with a link to the point in the transcript
17
+ where you last left. You can also ask him to bookmark points in the
18
+ transcript, send an sms message (well, an email) to someone, or even post
19
+ a tweet, among other things. His functionality is easily extendable via
20
+ plugins.}
21
+
22
+ s.rubyforge_project = "wesabot"
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
27
+ s.require_paths = ["lib"]
28
+
29
+ ### core dependencies
30
+ s.add_dependency "tinder", "~> 1.7.0"
31
+ s.add_dependency "data_mapper", "~> 1.1"
32
+ s.add_dependency "dm-sqlite-adapter", "~> 1.1"
33
+ s.add_dependency "daemons"
34
+ s.add_dependency "i18n"
35
+ s.add_dependency "firering", "~> 1.2.0"
36
+
37
+ ### plugin dependencies
38
+
39
+ # airbrake
40
+ s.add_dependency "nokogiri"
41
+ s.add_dependency "rest-client"
42
+
43
+ # image_search
44
+ s.add_dependency "httparty" # also used by twitter_search
45
+ s.add_dependency "google-search"
46
+
47
+ # remind_me
48
+ s.add_dependency "chronic"
49
+
50
+ ### development dependencies
51
+ s.add_development_dependency "rspec", "~> 2.6.0"
52
+ s.add_development_dependency "rake"
53
+ s.add_development_dependency "ruby-debug"
54
+
55
+ end