firetower 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.bnsignore +18 -0
- data/History.txt +4 -0
- data/README.html +406 -0
- data/README.org +135 -0
- data/Rakefile +30 -0
- data/bin/firetower +336 -0
- data/example/bot.rb +8 -0
- data/images/BaldMountainLookout.jpg +0 -0
- data/images/campfire-logo-for-fluid.png +0 -0
- data/lib/firetower.rb +80 -0
- data/lib/firetower/account.rb +52 -0
- data/lib/firetower/firetower.conf.erb +22 -0
- data/lib/firetower/plugins/core/init_v1.rb +1 -0
- data/lib/firetower/plugins/core/notify_plugin.rb +42 -0
- data/lib/firetower/room.rb +15 -0
- data/lib/firetower/server.rb +62 -0
- data/lib/firetower/session.rb +104 -0
- data/spec/firetower_spec.rb +6 -0
- data/spec/spec_helper.rb +15 -0
- data/test/test_firetower.rb +0 -0
- data/version.txt +1 -0
- metadata +206 -0
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
begin
|
3
|
+
require 'bones'
|
4
|
+
rescue LoadError
|
5
|
+
abort '### Please install the "bones" gem ###'
|
6
|
+
end
|
7
|
+
|
8
|
+
task :default => 'test:run'
|
9
|
+
task 'gem:release' => 'test:run'
|
10
|
+
|
11
|
+
Bones {
|
12
|
+
name 'firetower'
|
13
|
+
authors 'Avdi Grimm'
|
14
|
+
email 'avdi@avdi.org'
|
15
|
+
url 'http://github.com/avdi/firetower'
|
16
|
+
|
17
|
+
summary "A command-line interface to Campfire chats"
|
18
|
+
|
19
|
+
readme_file 'README.org'
|
20
|
+
|
21
|
+
depend_on 'twitter-stream', '~> 0.1.6'
|
22
|
+
depend_on 'eventmachine', '~> 0.12.10'
|
23
|
+
depend_on 'json', '~> 1.4'
|
24
|
+
depend_on 'addressable', '~> 2.1'
|
25
|
+
depend_on 'main', '~> 4.2'
|
26
|
+
depend_on 'servolux', '~> 0.9.4'
|
27
|
+
depend_on 'hookr', '~> 1.0'
|
28
|
+
depend_on 'highline', '~> 1.5'
|
29
|
+
}
|
30
|
+
|
data/bin/firetower
ADDED
@@ -0,0 +1,336 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path(
|
4
|
+
File.join(File.dirname(__FILE__), %w[.. lib firetower]))
|
5
|
+
|
6
|
+
Main do
|
7
|
+
description <<-"END"
|
8
|
+
A command-line interface to Campfire chats.
|
9
|
+
|
10
|
+
#{program} provides two primary services: a command-line syntax for posting
|
11
|
+
messages and code pastes to a Campfire chat room; and a daemon which can
|
12
|
+
monitor any number of Campfire rooms on multiple accounts and take
|
13
|
+
configurable actions when chat room events occur. Both usage modes share a
|
14
|
+
common simple Ruby-based configuration file.
|
15
|
+
END
|
16
|
+
|
17
|
+
author 'Avdi Grimm <avdi@avdi.org>'
|
18
|
+
|
19
|
+
examples [
|
20
|
+
"#{program} setup",
|
21
|
+
"#{program} say 'hello, Campfire'",
|
22
|
+
"#{program} say subdomain=mycompany room='Watercooler' 'hello room'",
|
23
|
+
"echo 'hello, Campfire' | #{program} say",
|
24
|
+
"#{program} paste '2+2 => 4'",
|
25
|
+
"#{program} paste subdomain=mycompany room='Watercooler' 'foobar'",
|
26
|
+
"#{program} paste --from=sel",
|
27
|
+
"#{program} paste --from=clip",
|
28
|
+
"#{program} paste --from=file hello.rb",
|
29
|
+
"#{program} paste --from=stdin < hello.rb",
|
30
|
+
"#{program} start",
|
31
|
+
"#{program} stop"
|
32
|
+
]
|
33
|
+
|
34
|
+
usage["CONFIGURATION"] = <<-"END"
|
35
|
+
#{program} is configured by editing the file
|
36
|
+
$HOME/.firetower/firetower.conf. The file consists of Ruby code which is
|
37
|
+
evaluated in the context of a Firetower::Session object. You can generate a
|
38
|
+
starter configuration by running '#{program} setup'.
|
39
|
+
|
40
|
+
In addition to setting up accounts and rooms and enabling plugins, the
|
41
|
+
config file can also be used to attach arbitrary event handlers to Campfire
|
42
|
+
events. For instance, the following code plays a sound when someone says
|
43
|
+
something in Campfire:
|
44
|
+
|
45
|
+
receive do |session, event|
|
46
|
+
if event['type'] == 'TextMessage'
|
47
|
+
system 'paplay ding.wav'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
The event hooks are HookR events (http://hookr.rubyforge.org), so any number
|
52
|
+
of handlers can be stacked on a given event. And more advanced usage are
|
53
|
+
possible; for instance, you can attach a Listener object to receive all
|
54
|
+
types of events. In fact, this last is how plugins are implemented.
|
55
|
+
END
|
56
|
+
|
57
|
+
fattr(:config_path) { dir + 'firetower.conf' }
|
58
|
+
fattr(:log_path) { dir + 'firetower.log' }
|
59
|
+
fattr(:pid_path) { dir + 'firetower.pid' }
|
60
|
+
|
61
|
+
logger_level ::Logger::INFO
|
62
|
+
|
63
|
+
option('dir') do
|
64
|
+
description "Where to find/put config, data, and log files"
|
65
|
+
default {
|
66
|
+
File.join(
|
67
|
+
ENV.fetch('HOME'){Etc.getpwnam(Etc.getlogin).dir},
|
68
|
+
'.firetower')
|
69
|
+
}
|
70
|
+
attr{|dir| Pathname(dir.value) }
|
71
|
+
end
|
72
|
+
|
73
|
+
mixin :address do
|
74
|
+
keyword :subdomain do
|
75
|
+
optional
|
76
|
+
attr
|
77
|
+
end
|
78
|
+
keyword :room_name do
|
79
|
+
optional
|
80
|
+
attr
|
81
|
+
end
|
82
|
+
|
83
|
+
def selected_room(session)
|
84
|
+
if params[:subdomain].given? && params[:room_name].given?
|
85
|
+
session.accounts[subdomain].rooms[room_name]
|
86
|
+
else
|
87
|
+
session.default_room
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
mode :say do
|
93
|
+
description "Say something in a campfire chat room"
|
94
|
+
mixin :address
|
95
|
+
argument :text do
|
96
|
+
optional
|
97
|
+
attr{|text| text.given? ? text.value : $stdin.read }
|
98
|
+
end
|
99
|
+
|
100
|
+
def run
|
101
|
+
with_session do |session|
|
102
|
+
room = selected_room(session)
|
103
|
+
room.account.say!(room.name, text)
|
104
|
+
# account = session.accounts[subdomain]
|
105
|
+
# account.say!(room_name, text)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
mode :paste do
|
111
|
+
description "Paste a pre-formatted message from command line, STDIN,"\
|
112
|
+
" selection, clipboard, or file"
|
113
|
+
mixin :address
|
114
|
+
|
115
|
+
argument "text_or_filename" do
|
116
|
+
description "Text to paste or file to paste from"
|
117
|
+
optional
|
118
|
+
attr
|
119
|
+
end
|
120
|
+
|
121
|
+
option 'from' do
|
122
|
+
description "Source of text. One of: clip|sel|stdin|file|arg"
|
123
|
+
argument :required
|
124
|
+
validate{|from| %w[clip sel stdin file arg auto].include?(from)}
|
125
|
+
default 'auto'
|
126
|
+
attr
|
127
|
+
end
|
128
|
+
|
129
|
+
def run
|
130
|
+
with_session do |session|
|
131
|
+
room = selected_room(session)
|
132
|
+
room.account.paste!(room.name, text)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def text
|
137
|
+
source = case from
|
138
|
+
when 'auto' then
|
139
|
+
if params['text_or_filename'].given?
|
140
|
+
'arg'
|
141
|
+
elsif !$stdin.tty?
|
142
|
+
'stdin'
|
143
|
+
else
|
144
|
+
'clip'
|
145
|
+
end
|
146
|
+
else
|
147
|
+
from
|
148
|
+
end
|
149
|
+
|
150
|
+
case source
|
151
|
+
when 'arg' then params['text_or_filename'].value
|
152
|
+
when 'sel' then `xsel --primary`
|
153
|
+
when 'clip' then `xsel --clipboard`
|
154
|
+
when 'file' then File.read(params['text_or_filename'].value)
|
155
|
+
when 'stdin' then $stdin.read
|
156
|
+
else
|
157
|
+
raise "Unknown text source #{from}"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
mode :rooms do
|
163
|
+
description "List rooms"
|
164
|
+
argument :subdomain do
|
165
|
+
optional
|
166
|
+
attr
|
167
|
+
end
|
168
|
+
|
169
|
+
def run
|
170
|
+
with_session do |session|
|
171
|
+
accounts = if params[:subdomain].given?
|
172
|
+
[session.accounts[subdomain]]
|
173
|
+
else
|
174
|
+
session.accounts.values
|
175
|
+
end
|
176
|
+
accounts.each do |account|
|
177
|
+
puts "#{account.subdomain}:"
|
178
|
+
account.rooms.keys.each do |room_name|
|
179
|
+
puts "\t#{room_name}"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
mode :account do
|
187
|
+
description "Prefix for accont-related commands"
|
188
|
+
mode :list do
|
189
|
+
description "List accounts"
|
190
|
+
|
191
|
+
def run
|
192
|
+
with_session do |session|
|
193
|
+
session.accounts.values.each do |account|
|
194
|
+
puts account.subdomain
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
mode :start do
|
202
|
+
description "Start the listener daemon"
|
203
|
+
|
204
|
+
option 'detach' do
|
205
|
+
description "Controls whether the server process will daemonize"
|
206
|
+
optional
|
207
|
+
argument :required
|
208
|
+
default true
|
209
|
+
cast :bool
|
210
|
+
attr
|
211
|
+
end
|
212
|
+
|
213
|
+
def run
|
214
|
+
if detach
|
215
|
+
firetower_daemon.startup
|
216
|
+
else
|
217
|
+
logger ::Logger.new($stdout)
|
218
|
+
start_server(:logger => logger)
|
219
|
+
end
|
220
|
+
puts "Firetower is vigilantly scanning the treetops"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
mode :stop do
|
225
|
+
description "Start the listener daemon"
|
226
|
+
|
227
|
+
def run
|
228
|
+
firetower_daemon.shutdown
|
229
|
+
puts "Firetower is no longer on duty"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
mode :setup do
|
234
|
+
description "Configure Firetower for first use"
|
235
|
+
|
236
|
+
def run
|
237
|
+
require 'erb'
|
238
|
+
hl = HighLine.new
|
239
|
+
exit_failure unless !config_path.exist? || hl.ask(<<"EOS")
|
240
|
+
A configuration already exists at #{config_path}. Are you sure you want to
|
241
|
+
overwrite it?
|
242
|
+
EOS
|
243
|
+
subdomain = hl.ask <<EOS
|
244
|
+
What is your Campfire subdomain? If your Campfire address is
|
245
|
+
http://mycompany.campfirenow.com, your subdomain is "mycompany" (without the
|
246
|
+
quotes).
|
247
|
+
EOS
|
248
|
+
token = hl.ask <<"EOS"
|
249
|
+
Please enter your Campfire API token. You can find your token at
|
250
|
+
http://#{subdomain}.campfirenow.com/member/edit
|
251
|
+
EOS
|
252
|
+
use_ssl = hl.agree("Use SSL when connecting to Campfire?")
|
253
|
+
rooms_url = "http#{use_ssl ? '?' : ''}://#{subdomain}.campfirenow.com/rooms.json"
|
254
|
+
response = open(rooms_url, :http_basic_authentication => [token, 'x']).read
|
255
|
+
room_list = JSON.parse(response)['rooms']
|
256
|
+
room_names = []
|
257
|
+
catch(:done) do
|
258
|
+
loop do
|
259
|
+
hl.choose do |menu|
|
260
|
+
menu.prompt = "Choose a room to join at startup: "
|
261
|
+
room_list.each do |room|
|
262
|
+
menu.choice(room['name']){ room_names << room['name']}
|
263
|
+
end
|
264
|
+
menu.choice("Done"){ throw :done }
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
template_path = File.expand_path(
|
269
|
+
'../lib/firetower/firetower.conf.erb',
|
270
|
+
File.dirname(__FILE__))
|
271
|
+
template = ERB.new(File.read(template_path), nil, "<>")
|
272
|
+
configuration = template.result(binding)
|
273
|
+
hl.say "The following configuration will be written to #{config_path}:\n\n"
|
274
|
+
hl.say configuration.gsub(/^/, " ")
|
275
|
+
exit_failure unless hl.agree("Write new configuration?")
|
276
|
+
config_path.open('w+') do |config_file|
|
277
|
+
config_file.write(configuration)
|
278
|
+
end
|
279
|
+
hl.say "#{program} is now configured!"
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def run
|
284
|
+
help!
|
285
|
+
end
|
286
|
+
|
287
|
+
def firetower_daemon
|
288
|
+
Servolux::Daemon.new(
|
289
|
+
:name => File.basename($PROGRAM_NAME),
|
290
|
+
:logger => logger,
|
291
|
+
:log_file => log_path.to_s,
|
292
|
+
:pid_file => pid_path.to_s,
|
293
|
+
:startup_command => lambda { start_server })
|
294
|
+
end
|
295
|
+
|
296
|
+
def start_server(options={})
|
297
|
+
with_session(:server, options) do |session|
|
298
|
+
server = Firetower::Server.new(session,
|
299
|
+
{
|
300
|
+
:log_path => log_path,
|
301
|
+
:pid_path => pid_path
|
302
|
+
}.merge(options))
|
303
|
+
server.run
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def with_session(kind=:command, options={})
|
308
|
+
session = Firetower::Session.new(kind, options)
|
309
|
+
if config_path.exist?
|
310
|
+
session.instance_eval(config_path.read, config_path.to_s, 1)
|
311
|
+
else
|
312
|
+
puts "Please run '#{program} setup' to configure #{program}"
|
313
|
+
exit_failure
|
314
|
+
end
|
315
|
+
session.execute_hook(:startup, session)
|
316
|
+
yield session
|
317
|
+
session.execute_hook(:shutdown, session)
|
318
|
+
end
|
319
|
+
|
320
|
+
def before_run
|
321
|
+
self.logger.level = ::Logger::INFO
|
322
|
+
load_plugins!
|
323
|
+
dir.mkpath
|
324
|
+
end
|
325
|
+
|
326
|
+
def load_plugins!
|
327
|
+
Gem.find_files('firetower/plugins/*/init_v1').each do |path|
|
328
|
+
path =~ %r{firetower/plugins/(.*)/}
|
329
|
+
debug "Loading plugin #{$1}"
|
330
|
+
load path
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
|
336
|
+
|
data/example/bot.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
# Drop this in ~/.firetower/firetower.conf for a simple (and VERY UNSAFE!) demo
|
2
|
+
# of a Campfire bot:
|
3
|
+
|
4
|
+
receive do |session, event|
|
5
|
+
if event['type'] == 'TextMessage' && event['body'] =~ /^!eval (.*)$/
|
6
|
+
event.room.account.paste!(event.room.name, "Eval result:\n" + eval($1).to_s)
|
7
|
+
end
|
8
|
+
end
|
Binary file
|
Binary file
|
data/lib/firetower.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'twitter/json_stream'
|
2
|
+
require 'json'
|
3
|
+
require 'addressable/uri'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'main'
|
6
|
+
require 'net/http'
|
7
|
+
require 'ostruct'
|
8
|
+
require 'yaml'
|
9
|
+
require 'net/https'
|
10
|
+
require 'servolux'
|
11
|
+
require 'hookr'
|
12
|
+
require 'pathname'
|
13
|
+
require 'English'
|
14
|
+
require 'highline'
|
15
|
+
|
16
|
+
module Firetower
|
17
|
+
|
18
|
+
# :stopdoc:
|
19
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
20
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
21
|
+
# :startdoc:
|
22
|
+
|
23
|
+
# Returns the version string for the library.
|
24
|
+
#
|
25
|
+
def self.version
|
26
|
+
@version ||= File.read(path('version.txt')).strip
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the library path for the module. If any arguments are given,
|
30
|
+
# they will be joined to the end of the libray path using
|
31
|
+
# <tt>File.join</tt>.
|
32
|
+
#
|
33
|
+
def self.libpath( *args, &block )
|
34
|
+
rv = args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
35
|
+
if block
|
36
|
+
begin
|
37
|
+
$LOAD_PATH.unshift LIBPATH
|
38
|
+
rv = block.call
|
39
|
+
ensure
|
40
|
+
$LOAD_PATH.shift
|
41
|
+
end
|
42
|
+
end
|
43
|
+
return rv
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the lpath for the module. If any arguments are given,
|
47
|
+
# they will be joined to the end of the path using
|
48
|
+
# <tt>File.join</tt>.
|
49
|
+
#
|
50
|
+
def self.path( *args, &block )
|
51
|
+
rv = args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
52
|
+
if block
|
53
|
+
begin
|
54
|
+
$LOAD_PATH.unshift PATH
|
55
|
+
rv = block.call
|
56
|
+
ensure
|
57
|
+
$LOAD_PATH.shift
|
58
|
+
end
|
59
|
+
end
|
60
|
+
return rv
|
61
|
+
end
|
62
|
+
|
63
|
+
# Utility method used to require all files ending in .rb that lie in the
|
64
|
+
# directory below this file that has the same name as the filename passed
|
65
|
+
# in. Optionally, a specific _directory_ name can be passed in such that
|
66
|
+
# the _filename_ does not have to be equivalent to the directory.
|
67
|
+
#
|
68
|
+
def self.require_all_libs_relative_to( fname, dir = nil )
|
69
|
+
dir ||= ::File.basename(fname, '.*')
|
70
|
+
search_me = ::File.expand_path(
|
71
|
+
::File.join(::File.dirname(fname), dir, '**', '*.rb'))
|
72
|
+
|
73
|
+
Dir.glob(search_me).sort.each {|rb| require rb unless rb =~ /plugins/}
|
74
|
+
end
|
75
|
+
|
76
|
+
end # module Firetower
|
77
|
+
|
78
|
+
$:.unshift(Firetower::LIBPATH)
|
79
|
+
Firetower.require_all_libs_relative_to(__FILE__)
|
80
|
+
|