firetower 0.0.1

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