firetower 0.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/.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
|
+
|