wesabot 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|