wesabot 1.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.
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