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.
- data/.gitignore +5 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +182 -0
- data/Rakefile +13 -0
- data/bin/wesabot +111 -0
- data/config/wesabot.yml.sample +4 -0
- data/lib/campfire/bot.rb +53 -0
- data/lib/campfire/configuration.rb +65 -0
- data/lib/campfire/message.rb +102 -0
- data/lib/campfire/polling_bot/plugin.rb +162 -0
- data/lib/campfire/polling_bot/plugins/airbrake/.gitignore +1 -0
- data/lib/campfire/polling_bot/plugins/airbrake/airbrake/api.rb +48 -0
- data/lib/campfire/polling_bot/plugins/airbrake/airbrake/error.rb +51 -0
- data/lib/campfire/polling_bot/plugins/airbrake/airbrake_plugin.rb +63 -0
- data/lib/campfire/polling_bot/plugins/airbrake/airbrake_plugin.yml.sample +3 -0
- data/lib/campfire/polling_bot/plugins/bookmark/bookmark.rb +15 -0
- data/lib/campfire/polling_bot/plugins/bookmark/bookmark_plugin.rb +89 -0
- data/lib/campfire/polling_bot/plugins/debug/debug_plugin.rb +23 -0
- data/lib/campfire/polling_bot/plugins/deploy/deploy_plugin.rb +155 -0
- data/lib/campfire/polling_bot/plugins/deploy/deploy_plugin.yml.sample +9 -0
- data/lib/campfire/polling_bot/plugins/deploy/spec/deploy_plugin_spec.rb +153 -0
- data/lib/campfire/polling_bot/plugins/greeting/greeting_plugin.rb +146 -0
- data/lib/campfire/polling_bot/plugins/greeting/greeting_setting.rb +19 -0
- data/lib/campfire/polling_bot/plugins/greeting/spec/greeting_plugin_spec.rb +51 -0
- data/lib/campfire/polling_bot/plugins/greeting/spec/greeting_setting_spec.rb +61 -0
- data/lib/campfire/polling_bot/plugins/help/help_plugin.rb +53 -0
- data/lib/campfire/polling_bot/plugins/history/history_plugin.rb +34 -0
- data/lib/campfire/polling_bot/plugins/image_search/displayed_image.rb +8 -0
- data/lib/campfire/polling_bot/plugins/image_search/image_search_plugin.rb +106 -0
- data/lib/campfire/polling_bot/plugins/interjector/interjector_plugin.rb +58 -0
- data/lib/campfire/polling_bot/plugins/kibitz/kibitz_plugin.rb +90 -0
- data/lib/campfire/polling_bot/plugins/kibitz/spec/kibitz_plugin_spec.rb +56 -0
- data/lib/campfire/polling_bot/plugins/plugin.db +0 -0
- data/lib/campfire/polling_bot/plugins/reload/reload_plugin.rb +24 -0
- data/lib/campfire/polling_bot/plugins/remind_me/remind_me_plugin.rb +149 -0
- data/lib/campfire/polling_bot/plugins/remind_me/reminder.rb +8 -0
- data/lib/campfire/polling_bot/plugins/shared/message.rb +37 -0
- data/lib/campfire/polling_bot/plugins/shared/user.rb +32 -0
- data/lib/campfire/polling_bot/plugins/sms/sms_plugin.rb +88 -0
- data/lib/campfire/polling_bot/plugins/sms/sms_setting.rb +7 -0
- data/lib/campfire/polling_bot/plugins/time/time_plugin.rb +17 -0
- data/lib/campfire/polling_bot/plugins/tweet/tweet.rb +52 -0
- data/lib/campfire/polling_bot/plugins/tweet/tweet_plugin.rb +117 -0
- data/lib/campfire/polling_bot/plugins/tweet/tweet_plugin.yml.sample +4 -0
- data/lib/campfire/polling_bot/plugins/twitter_search/twitter_search_plugin.rb +58 -0
- data/lib/campfire/polling_bot.rb +125 -0
- data/lib/campfire/sample_plugin.rb +56 -0
- data/lib/campfire/version.rb +3 -0
- data/lib/wesabot.rb +3 -0
- data/spec/.gitignore +1 -0
- data/spec/polling_bot_spec.rb +23 -0
- data/spec/spec_helper.rb +190 -0
- data/wesabot.gemspec +55 -0
- metadata +336 -0
@@ -0,0 +1,162 @@
|
|
1
|
+
# Campfire AbstractPollingBot Plugin base class
|
2
|
+
#
|
3
|
+
# To create a plugin, extend from this class, and just drop it into the plugins directory.
|
4
|
+
# See sample_plugin.rb for more information.
|
5
|
+
#
|
6
|
+
require 'dm-core'
|
7
|
+
require 'dm-migrations'
|
8
|
+
|
9
|
+
module Campfire
|
10
|
+
class PollingBot
|
11
|
+
class Plugin
|
12
|
+
attr_accessor :config
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
# load the config file if we have one
|
16
|
+
name = self.to_s.gsub(/([[:upper:]]+)([[:upper:]][[:lower:]])/,'\1_\2').
|
17
|
+
gsub(/([[:lower:]\d])([[:upper:]])/,'\1_\2').
|
18
|
+
tr("-", "_").
|
19
|
+
downcase
|
20
|
+
filepath = File.join(self.class.directory, "#{name}.yml")
|
21
|
+
if File.exists?(filepath)
|
22
|
+
self.config = YAML.load_file(filepath)
|
23
|
+
else
|
24
|
+
self.config = {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# keep track of subclasses
|
29
|
+
def self.inherited(klass)
|
30
|
+
# save the plugin's directory
|
31
|
+
filepath = caller[0].split(':')[0]
|
32
|
+
klass.directory = File.dirname(caller[0].split(':')[0])
|
33
|
+
super if defined? super
|
34
|
+
ensure
|
35
|
+
( @subclasses ||= [] ).push(klass).uniq!
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.subclasses
|
39
|
+
@subclasses ||= []
|
40
|
+
@subclasses.inject( [] ) do |list, subclass|
|
41
|
+
list.push(subclass, *subclass.subclasses)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.directory=(dir)
|
46
|
+
@directory = dir
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.directory
|
50
|
+
@directory
|
51
|
+
end
|
52
|
+
|
53
|
+
# bot accessor
|
54
|
+
def self.bot
|
55
|
+
@@bot
|
56
|
+
end
|
57
|
+
def self.bot=(bot)
|
58
|
+
@@bot = bot
|
59
|
+
end
|
60
|
+
attr_writer :bot
|
61
|
+
def bot
|
62
|
+
@bot || self.class.bot
|
63
|
+
end
|
64
|
+
|
65
|
+
def logger
|
66
|
+
bot.logger
|
67
|
+
end
|
68
|
+
|
69
|
+
HALT = 1 # returned by a command when command processing should halt (continues by default)
|
70
|
+
|
71
|
+
def self.load_all(bot)
|
72
|
+
self.bot = bot
|
73
|
+
|
74
|
+
load_plugin_classes
|
75
|
+
|
76
|
+
# set up the database now that the plugins are loaded
|
77
|
+
setup_database(bot.config.datauri)
|
78
|
+
|
79
|
+
plugin_classes = self.subclasses.sort {|a,b| b.priority <=> a.priority }
|
80
|
+
# initialize plugins
|
81
|
+
plugins = plugin_classes.map { |p_class| p_class.new }
|
82
|
+
return plugins
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.load_plugin_classes
|
86
|
+
# add each plugin dir to the load path
|
87
|
+
Dir.glob(File.dirname(__FILE__) + "/plugins/*").each {|dir| $LOAD_PATH << dir }
|
88
|
+
# load core first
|
89
|
+
paths = Dir.glob(File.dirname(__FILE__) + "/plugins/shared/*.rb")
|
90
|
+
# load all models & plugins
|
91
|
+
paths += Dir.glob(File.dirname(__FILE__) + "/plugins/*/*.rb")
|
92
|
+
paths.each do |path|
|
93
|
+
begin
|
94
|
+
path.match(/(.*?)\.rb$/) && (require $1)
|
95
|
+
rescue Exception => e
|
96
|
+
$stderr.puts "Unable to load #{path}: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# set up the plugin database
|
102
|
+
def self.setup_database(datauri)
|
103
|
+
DataMapper.setup(:default, datauri)
|
104
|
+
DataMapper.auto_upgrade!
|
105
|
+
# not related to setting up the database, but for lack of a better place....
|
106
|
+
DataMapper::Model.raise_on_save_failure = true
|
107
|
+
end
|
108
|
+
|
109
|
+
# method to set or get the priority. Higher value == higher priority. Default is 0
|
110
|
+
# command subclasses set their priority like so:
|
111
|
+
# class FooPlugin << Campfire::PollingBot::Plugin
|
112
|
+
# priority 10
|
113
|
+
# ...
|
114
|
+
def self.priority(value = nil)
|
115
|
+
if value
|
116
|
+
@priority = value
|
117
|
+
end
|
118
|
+
return @priority || 0
|
119
|
+
end
|
120
|
+
|
121
|
+
# convenience method to get the priority of a plugin instance
|
122
|
+
def priority
|
123
|
+
self.class.priority
|
124
|
+
end
|
125
|
+
|
126
|
+
# called from Plugin objects to indicate what kinds of messages they accept
|
127
|
+
# if the :addressed_to_me flag is true, it will only accept messages addressed
|
128
|
+
# to the bot (e.g. "Wes, ____" or "______, Wes")
|
129
|
+
# Examples:
|
130
|
+
# accepts :text_message, :addressed_to_me => true
|
131
|
+
# accepts :enter_message
|
132
|
+
# accepts :all
|
133
|
+
def self.accepts(message_type, params = {})
|
134
|
+
@accepts ||= {}
|
135
|
+
if message_type == :all
|
136
|
+
@accepts[:all] = params[:addressed_to_me] ? :addressed_to_me : :for_anyone
|
137
|
+
else
|
138
|
+
klass = Campfire.const_get(message_type.to_s.gsub(/(?:^|_)(\S)/) {$1.upcase})
|
139
|
+
@accepts[klass] = params[:addressed_to_me] ? :addressed_to_me : :for_anyone
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# returns true if the plugin accepts the given message type
|
144
|
+
def self.accepts?(message)
|
145
|
+
if @accepts[:all]
|
146
|
+
@accepts[:all] == :addressed_to_me ? bot.addressed_to_me?(message) : true
|
147
|
+
elsif @accepts[message.class]
|
148
|
+
@accepts[message.class] == :addressed_to_me ? bot.addressed_to_me?(message) : true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# convenience method to call accepts on a plugin instance
|
153
|
+
def accepts?(message)
|
154
|
+
self.class.accepts?(message)
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_s
|
158
|
+
self.class.to_s
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
airbrake_plugin.yml
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'rest-client'
|
3
|
+
|
4
|
+
module Airbrake
|
5
|
+
class API
|
6
|
+
def initialize(domain, auth_token)
|
7
|
+
@host = "#{domain}.airbrakeapp.com"
|
8
|
+
@auth_token = auth_token
|
9
|
+
end
|
10
|
+
|
11
|
+
def resolve_error(error_id, resolved=true)
|
12
|
+
api_put("/errors/#{error_id}", {}, :group => {:resolved => resolved})
|
13
|
+
end
|
14
|
+
|
15
|
+
def errors(show_resolved=false)
|
16
|
+
doc = Nokogiri::XML(api_get("/errors.xml", :show_resolved => show_resolved))
|
17
|
+
doc.xpath("/groups/group").map {|node| Airbrake::Error.from_xml(node) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def error_url(error)
|
21
|
+
error_id = error.is_a?(Airbrake::Error) ? error.error_id : error
|
22
|
+
"https://#{@host}/errors/#{error_id}"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def api_uri(endpoint, query = {})
|
28
|
+
query = {:auth_token => @auth_token}.update(query)
|
29
|
+
uri = URI::HTTPS.build(:host => @host, :path => endpoint, :query => to_query(query))
|
30
|
+
return uri.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
# convert a hash of param to a query string
|
34
|
+
def to_query(params)
|
35
|
+
params.map{|k,v| "#{CGI::escape(k.to_s)}=#{CGI::escape(v.to_s)}"}.join('&')
|
36
|
+
end
|
37
|
+
|
38
|
+
def api_get(endpoint, query = {})
|
39
|
+
RestClient.get(api_uri(endpoint, query))
|
40
|
+
end
|
41
|
+
|
42
|
+
def api_put(endpoint, query = {}, params = {})
|
43
|
+
return RestClient.put(api_uri(endpoint, query), params)
|
44
|
+
rescue RestClient::Exception => e
|
45
|
+
return e.response
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Airbrake
|
2
|
+
class Error
|
3
|
+
include DataMapper::Resource
|
4
|
+
property :id, Serial
|
5
|
+
property :error_id, Integer, :required => true
|
6
|
+
|
7
|
+
attr_accessor :field
|
8
|
+
|
9
|
+
# Given a block of XML returned from the Airbrake API, return an
|
10
|
+
# Airbrake::Error object
|
11
|
+
def self.from_xml(xml)
|
12
|
+
field = {}
|
13
|
+
xml.xpath('./*').each do |node|
|
14
|
+
content = node.content
|
15
|
+
# convert to proper ruby types
|
16
|
+
type = node.attributes['type'] && node.attributes['type'].value
|
17
|
+
case type
|
18
|
+
when "boolean"
|
19
|
+
content = content == "true"
|
20
|
+
when "datetime"
|
21
|
+
content = Time.parse(content)
|
22
|
+
when "integer"
|
23
|
+
content = content.to_i
|
24
|
+
end
|
25
|
+
key = node.name.tr('-','_')
|
26
|
+
field[key.to_sym] = content
|
27
|
+
end
|
28
|
+
error = new()
|
29
|
+
error.field = field
|
30
|
+
error.error_id = field[:id]
|
31
|
+
return error
|
32
|
+
end
|
33
|
+
|
34
|
+
def summary
|
35
|
+
"#{@field[:error_message]} at #{@field[:file]}:#{@field[:line_number]}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](key)
|
39
|
+
key = key.to_s.tr('-','_').to_sym
|
40
|
+
@field[key]
|
41
|
+
end
|
42
|
+
|
43
|
+
def eql?(other)
|
44
|
+
self.error_id.eql?(other.error_id)
|
45
|
+
end
|
46
|
+
|
47
|
+
def hash
|
48
|
+
self.error_id.hash
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'airbrake/api'
|
2
|
+
require 'airbrake/error'
|
3
|
+
|
4
|
+
class AirbrakePlugin < Campfire::PollingBot::Plugin
|
5
|
+
accepts :text_message, :addressed_to_me => true
|
6
|
+
priority 10
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@api = Airbrake::API.new(config['domain'], config['auth_token'])
|
11
|
+
end
|
12
|
+
|
13
|
+
def process(message)
|
14
|
+
case message.command
|
15
|
+
when /((?:un(?:-)?)?resolve) error (?:#|number)?\s*(\d+)/
|
16
|
+
action, error_num = $1, $2
|
17
|
+
res = @api.resolve_error(error_num, action == "resolve")
|
18
|
+
case res.code
|
19
|
+
when 200
|
20
|
+
bot.say("Ok, #{action}d error ##{error_num}")
|
21
|
+
when 404
|
22
|
+
bot.say("Hmm. Airbrake couldn't find error ##{error_num}")
|
23
|
+
else
|
24
|
+
bot.say("Huh. Airbrake gave me this response:")
|
25
|
+
bot.paste(res.to_s)
|
26
|
+
end
|
27
|
+
return HALT
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def heartbeat
|
32
|
+
# check every <check_interval> seconds (heartbeat is called every 3 sec)
|
33
|
+
num_heartbeats = config['check_interval'] / Campfire::PollingBot::HEARTBEAT_INTERVAL
|
34
|
+
@heartbeat_counter ||= 0
|
35
|
+
@heartbeat_counter += 1
|
36
|
+
return unless (@heartbeat_counter % num_heartbeats) == 1
|
37
|
+
handle_errors
|
38
|
+
end
|
39
|
+
|
40
|
+
def handle_errors
|
41
|
+
# fetch errors we know about, announce new ones, and remove resolved ones
|
42
|
+
unresolved_errors = @api.errors
|
43
|
+
known_errors = Airbrake::Error.all
|
44
|
+
new_errors = unresolved_errors - known_errors
|
45
|
+
resolved_errors = known_errors - unresolved_errors
|
46
|
+
resolved_errors.each {|e| e.destroy }
|
47
|
+
new_errors.each {|e| e.save }
|
48
|
+
announce(new_errors) if new_errors.any?
|
49
|
+
end
|
50
|
+
|
51
|
+
def announce(errors)
|
52
|
+
msg = "Got #{errors.length} new error#{errors.length > 1 ? 's' : ''} from Airbrake" +
|
53
|
+
(errors.length > 5 ? ". Here are the first 5:" : ":")
|
54
|
+
bot.say(msg)
|
55
|
+
errors.first(5).each { |e| bot.say("#{e.summary} (#{@api.error_url(e)})") }
|
56
|
+
end
|
57
|
+
|
58
|
+
# return array of available commands and descriptions
|
59
|
+
def help
|
60
|
+
[["resolve <error number>", "mark an error as resolved"],
|
61
|
+
["unresolve <error number>", "mark an error as unresolved"]]
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# used by BookmarksPlugin
|
2
|
+
class Bookmark
|
3
|
+
include DataMapper::Resource
|
4
|
+
property :id, Serial
|
5
|
+
property :room, Integer, :required => true, :index => true
|
6
|
+
property :message_id, Integer, :required => true
|
7
|
+
property :person, String, :index => true
|
8
|
+
property :name, String, :index => true
|
9
|
+
property :timestamp, Time, :required => true, :index => true
|
10
|
+
|
11
|
+
# return link to bookmark
|
12
|
+
def link
|
13
|
+
"/room/#{self.room}/transcript/message/#{self.message_id}"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# Plugin to allow saving of transcript bookmarks
|
2
|
+
class BookmarkPlugin < Campfire::PollingBot::Plugin
|
3
|
+
accepts :text_message, :addressed_to_me => true
|
4
|
+
|
5
|
+
def process(message)
|
6
|
+
case message.command
|
7
|
+
when /^(?:add |create )?bookmark:?\s*(?:this as:?)?\s*("?)(.*?)\1$/i
|
8
|
+
save_bookmark(message, $2)
|
9
|
+
bot.say("Ok, saved bookmark: #{$2}")
|
10
|
+
return HALT
|
11
|
+
when /(?:list|show) (\S+) bookmarks/i, /(?:list|show) bookmarks for (\S+)/i
|
12
|
+
list_bookmarks(message.person, $1)
|
13
|
+
return HALT
|
14
|
+
when /(list|show) bookmarks$/
|
15
|
+
list_bookmarks(message.person, message.person)
|
16
|
+
return HALT
|
17
|
+
when /delete bookmark (?:#\s*)?(\d+)/
|
18
|
+
delete_bookmark(message.person, $1.to_i)
|
19
|
+
return HALT
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# return array of available commands and descriptions
|
25
|
+
def help
|
26
|
+
[["bookmark: <name>", "bookmark the current location"]]
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def save_bookmark(message, name)
|
32
|
+
Bookmark.create(:person => message.person,
|
33
|
+
:name => name,
|
34
|
+
:room => bot.room.id,
|
35
|
+
:message_id => message.message_id,
|
36
|
+
:timestamp => Time.now)
|
37
|
+
end
|
38
|
+
|
39
|
+
def list_bookmarks(current_person, request_person)
|
40
|
+
case request_person.downcase
|
41
|
+
when 'my', 'me'
|
42
|
+
request_person = current_person
|
43
|
+
else
|
44
|
+
request_person.gsub!(/'s$/i,'')
|
45
|
+
end
|
46
|
+
request_person.capitalize!
|
47
|
+
|
48
|
+
case request_person
|
49
|
+
when /everyone/i, /all/i
|
50
|
+
if (bookmarks = Bookmark.all(:order => [:name])).any?
|
51
|
+
bot.say("Here are all the bookmarks I have:")
|
52
|
+
bookmarks.each do |bookmark|
|
53
|
+
bot.say("#{bookmark.id} - #{bookmark.name} (#{bookmark_link(bookmark)}) by #{bookmark.person}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
return
|
57
|
+
else
|
58
|
+
bookmarks = Bookmark.all(:conditions => {:person => request_person}, :order => [:name])
|
59
|
+
if bookmarks.any?
|
60
|
+
if request_person == current_person
|
61
|
+
bot.say("Here are the bookmarks I have for you, #{current_person}:")
|
62
|
+
else
|
63
|
+
bot.say("Here are the bookmarks for #{request_person}:")
|
64
|
+
end
|
65
|
+
bookmarks.each do |bookmark|
|
66
|
+
bot.say("#{bookmark.id} - #{bookmark.name} (#{bookmark_link(bookmark)})")
|
67
|
+
end
|
68
|
+
return
|
69
|
+
end
|
70
|
+
end
|
71
|
+
bot.say("I couldn't find any bookmarks for #{request_person}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def delete_bookmark(current_person, id)
|
75
|
+
bookmark = Bookmark.get(id)
|
76
|
+
if bookmark.person == current_person
|
77
|
+
bookmark.destroy
|
78
|
+
bot.say("Ok, I've deleted bookmark ##{id}.")
|
79
|
+
else
|
80
|
+
bot.say("Sorry, #{current_person}, but I couldn't find a bookmark with that id that belongs to you.")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# return link to bookmark
|
85
|
+
def bookmark_link(bookmark)
|
86
|
+
bot.base_uri + bookmark.link
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Toggle debug mode
|
2
|
+
class DebugPlugin < Campfire::PollingBot::Plugin
|
3
|
+
accepts :text_message, :addressed_to_me => true
|
4
|
+
priority 100
|
5
|
+
|
6
|
+
def process(message)
|
7
|
+
case message.command
|
8
|
+
when /(enable|disable) debug/i
|
9
|
+
if $1 == 'enable'
|
10
|
+
bot.debug = true
|
11
|
+
else
|
12
|
+
bot.debug = false
|
13
|
+
end
|
14
|
+
bot.say("ok, debugging is #{bot.debug ? 'enabled' : 'disabled'}")
|
15
|
+
return HALT
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# return array of available commands and descriptions
|
20
|
+
def help
|
21
|
+
[['<enable|disable> debugging', "enable or disable debug mode"]]
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
# Plugin to get a list of commits that are on deck to be deployed
|
4
|
+
class DeployPlugin < Campfire::PollingBot::Plugin
|
5
|
+
accepts :text_message, :addressed_to_me => true
|
6
|
+
|
7
|
+
def process(message)
|
8
|
+
case message.command
|
9
|
+
when /deploy\s([^\s\!]+)(?:(?: to)? (staging|nine|production))?( with migrations)?/
|
10
|
+
project, env, migrate = $1, $2, $3
|
11
|
+
name = env ? "#{project} #{env}" : project
|
12
|
+
env ||= "production"
|
13
|
+
|
14
|
+
if not projects.any?
|
15
|
+
bot.say("Sorry #{message.person}, I don't know about any projects. Please configure the deploy plugin.")
|
16
|
+
return HALT
|
17
|
+
end
|
18
|
+
|
19
|
+
project ||= default_project
|
20
|
+
if project.nil?
|
21
|
+
bot.say("Sorry #{message.person}, I don't have a default project. Here are the projects I do know about:")
|
22
|
+
bot.paste(projects.keys.sort.join("\n"))
|
23
|
+
return HALT
|
24
|
+
end
|
25
|
+
project.downcase!
|
26
|
+
|
27
|
+
info = project_info(project)
|
28
|
+
if info.nil?
|
29
|
+
bot.say("Sorry #{message.person}, I don't know anything about #{name}. Here are the projects I do know about:")
|
30
|
+
bot.paste(projects.keys.sort.join("\n"))
|
31
|
+
return HALT
|
32
|
+
end
|
33
|
+
|
34
|
+
bot.say("Okay, trying to deploy #{name}...")
|
35
|
+
|
36
|
+
begin
|
37
|
+
deploy = migrate ? "deploy:migrations" : "deploy"
|
38
|
+
git(project, "bundle exec cap #{env} #{deploy}")
|
39
|
+
rescue => e
|
40
|
+
bot.log_error(e)
|
41
|
+
bot.say("Sorry #{message.person}, I couldn't deploy #{name}.")
|
42
|
+
return HALT
|
43
|
+
end
|
44
|
+
|
45
|
+
bot.say("Done.")
|
46
|
+
return HALT
|
47
|
+
|
48
|
+
when /on deck(?: for ([^\s\?]+)( staging)?)?/
|
49
|
+
project, staging = $1, $2
|
50
|
+
project ||= default_project
|
51
|
+
name = staging ? "#{project} staging" : project
|
52
|
+
|
53
|
+
if not projects.any?
|
54
|
+
bot.say("Sorry #{message.person}, I don't know about any projects. Please configure the deploy plugin.")
|
55
|
+
return HALT
|
56
|
+
end
|
57
|
+
|
58
|
+
if project.nil?
|
59
|
+
bot.say("Sorry #{message.person}, I don't have a default project. Here are the projects I do know about:")
|
60
|
+
bot.paste(projects.keys.sort.join("\n"))
|
61
|
+
return HALT
|
62
|
+
end
|
63
|
+
project.downcase!
|
64
|
+
|
65
|
+
info = project_info(project)
|
66
|
+
if info.nil?
|
67
|
+
bot.say("Sorry #{message.person}, I don't know anything about #{name}. Here are the projects I do know about:")
|
68
|
+
bot.paste(projects.keys.sort.join("\n"))
|
69
|
+
return HALT
|
70
|
+
end
|
71
|
+
|
72
|
+
range = nil
|
73
|
+
begin
|
74
|
+
range = "#{deployed_revision(project, staging)}..HEAD"
|
75
|
+
shortlog = project_shortlog(project, range)
|
76
|
+
rescue => e
|
77
|
+
bot.log_error(e)
|
78
|
+
bot.say("Sorry #{message.person}, I couldn't get what's on deck for #{name}.")
|
79
|
+
return HALT
|
80
|
+
end
|
81
|
+
|
82
|
+
if shortlog.nil? || shortlog =~ /\A\s*\Z/
|
83
|
+
bot.say("There's nothing on deck for #{name} right now.")
|
84
|
+
return HALT
|
85
|
+
end
|
86
|
+
|
87
|
+
bot.say("Here's what's on deck for #{name}:")
|
88
|
+
bot.paste("$ git shortlog #{range}\n\n#{shortlog}")
|
89
|
+
|
90
|
+
return HALT
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def help
|
95
|
+
help_lines = [
|
96
|
+
["what's on deck for <project>?", "shortlog of changes not yet deployed to production"],
|
97
|
+
["what's on deck for <project> staging?", "shortlog of changes not yet deployed to staging"],
|
98
|
+
]
|
99
|
+
if default_project
|
100
|
+
help_lines << ["what's on deck?", "shortlog of changes not yet deployed to #{default_project}"]
|
101
|
+
end
|
102
|
+
return help_lines
|
103
|
+
end
|
104
|
+
|
105
|
+
def projects
|
106
|
+
(config && config['projects']) || {}
|
107
|
+
end
|
108
|
+
|
109
|
+
def default_project
|
110
|
+
(config && config['default_project']) ||
|
111
|
+
(projects.size == 1 ? projects.keys.first : nil)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def project_info(project)
|
117
|
+
projects[project]
|
118
|
+
end
|
119
|
+
|
120
|
+
def project_shortlog(project, treeish)
|
121
|
+
info = project_info(project)
|
122
|
+
return nil if info.nil?
|
123
|
+
|
124
|
+
return git(project, "git shortlog #{treeish}")
|
125
|
+
end
|
126
|
+
|
127
|
+
def deployed_revision(project, staging = false)
|
128
|
+
info = project_info(project)
|
129
|
+
return nil if info.nil?
|
130
|
+
|
131
|
+
host = staging ? info['staging'] : info['url']
|
132
|
+
return nil if host.nil?
|
133
|
+
|
134
|
+
return open("http://#{host}/REVISION").read.chomp
|
135
|
+
end
|
136
|
+
|
137
|
+
def repository_path(project)
|
138
|
+
File.expand_path File.join(config["repository_base_path"], "#{project}")
|
139
|
+
end
|
140
|
+
|
141
|
+
def git(project, cmd)
|
142
|
+
dir = repository_path(project)
|
143
|
+
out = Dir.chdir(dir) do
|
144
|
+
# don't want output from the pull
|
145
|
+
system("git pull")
|
146
|
+
`#{cmd}`
|
147
|
+
end
|
148
|
+
|
149
|
+
unless $?.exitstatus.zero?
|
150
|
+
raise "attempt to run `#{cmd}` in #{dir} failed with status #{$?.exitstatus}\n#{out}"
|
151
|
+
end
|
152
|
+
|
153
|
+
return out
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# config file for DeployPlugin
|
2
|
+
# sites are expected to have a file at /REVISION
|
3
|
+
# containing the git sha of the deployed revision
|
4
|
+
repository_base_path: src
|
5
|
+
default_project: my_super_site
|
6
|
+
projects:
|
7
|
+
my_super_site:
|
8
|
+
url: www.example.com
|
9
|
+
staging: staging.example.com
|