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/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
+