squab 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/LICENSE.md +13 -0
  2. data/README.md +4 -0
  3. data/bin/squab +4 -0
  4. data/defaults.yaml +5 -0
  5. data/lib/squab.rb +2 -0
  6. data/lib/squab/db.rb +37 -0
  7. data/lib/squab/events.rb +301 -0
  8. data/lib/squab/web.rb +231 -0
  9. data/public/api.html +66 -0
  10. data/public/css/anytime.css +777 -0
  11. data/public/css/bootstrap-responsive.css +1058 -0
  12. data/public/css/bootstrap-responsive.min.css +9 -0
  13. data/public/css/bootstrap.css +5774 -0
  14. data/public/css/bootstrap.min.css +9 -0
  15. data/public/css/docs.css +1001 -0
  16. data/public/css/normalize.css +406 -0
  17. data/public/css/pickadate/default.css +240 -0
  18. data/public/css/pickadate/default.date.css +332 -0
  19. data/public/css/prettify.css +30 -0
  20. data/public/css/squab.css +307 -0
  21. data/public/events.html +85 -0
  22. data/public/img/glyphicons-halflings-white.png +0 -0
  23. data/public/img/glyphicons-halflings.png +0 -0
  24. data/public/js/collection/events.js +74 -0
  25. data/public/js/lib/backbone-min.js +4 -0
  26. data/public/js/lib/datejs/core.js +48 -0
  27. data/public/js/lib/datejs/date-en-US.js +145 -0
  28. data/public/js/lib/jquery-latest.js +9440 -0
  29. data/public/js/lib/lodash.min.js +48 -0
  30. data/public/js/lib/pickadate/legacy.js +140 -0
  31. data/public/js/lib/pickadate/picker.date.js +957 -0
  32. data/public/js/lib/pickadate/picker.js +791 -0
  33. data/public/js/lib/typeahead.min.js +7 -0
  34. data/public/js/model/event.js +38 -0
  35. data/public/js/router.js +38 -0
  36. data/public/js/squab.js +6 -0
  37. data/public/js/view/day_view.js +22 -0
  38. data/public/js/view/event_view.js +14 -0
  39. data/public/js/view/events_view.js +46 -0
  40. data/public/js/view/search_view.js +130 -0
  41. metadata +220 -0
@@ -0,0 +1,13 @@
1
+ Copyright 2013 Square Inc.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,4 @@
1
+ Squab Human Oriented Event Bus
2
+ =====
3
+
4
+ This is the Squab Server directory. Please see the root level directory for information on squab, using squab, and developing squab.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'squab/web'
4
+ Squab::Web.run!
@@ -0,0 +1,5 @@
1
+ port: 8082
2
+ environment: development
3
+ # Without a leading slash, this is relative to your lib/squab directory
4
+ root: '../../'
5
+ dbconn: "sqlite:///tmp/squab.sqlite"
@@ -0,0 +1,2 @@
1
+ require 'squab/db'
2
+ require 'squab/events'
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'rubygems'
4
+ require 'sequel'
5
+
6
+ module Squab
7
+ class DB
8
+ attr_accessor :events, :threadsafe
9
+
10
+ def initialize(conn_string)
11
+ @db = Sequel.connect(conn_string)
12
+ if not @db.table_exists?('events')
13
+ bootstrap
14
+ end
15
+ # SQLite is not threadsafe
16
+ @threadsafe = @db.database_type != :sqlite
17
+ @events = @db.from(:events)
18
+ end
19
+
20
+ def bootstrap
21
+ @db.create_table :events do
22
+ primary_key :id
23
+ String :uid
24
+ String :value
25
+ String :url
26
+ String :source
27
+ Float :date
28
+ Boolean :deleted
29
+ index :uid
30
+ index :date
31
+ index :source
32
+ index :deleted
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,301 @@
1
+ require 'rubygems'
2
+ require 'squab/db'
3
+ require 'json'
4
+
5
+ module Squab
6
+ class Events < Squab::DB
7
+ def initialize(conn_string, opts={})
8
+ @return_json = opts.include?(:json) ? opts[:json] : true
9
+ super(conn_string)
10
+ end
11
+
12
+ def all(limit=1000)
13
+ all_events = nil
14
+ if limit == 0
15
+ all_events = @events.all.sort_by { |event| event[:id] }
16
+ all_events.reverse!
17
+ else
18
+ all_events = @events.limit(limit.to_i).reverse_order(:id)
19
+ end
20
+ format_rows(all_events)
21
+ end
22
+
23
+ # Wrap calls with this when you don't want JSON to return ever.
24
+ # TODO: Redo the entire internal-returning-json-thing
25
+ def with_no_json(&block)
26
+ # Whatever the situation is on json, store it, and set it to false for
27
+ # the period of this block call
28
+ returner_type = @return_json
29
+ @return_json = false
30
+
31
+ ret_val = block.call
32
+
33
+ # Put it back to whatever it was
34
+ @return_json = returner_type
35
+ ret_val
36
+ end
37
+
38
+ def event_slice(search_params)
39
+ # Grab all events, or just a slice of events if there's
40
+ # a time specified
41
+ # TODO: Build this off the sequel data source so it's not
42
+ # a giant if/else
43
+ with_no_json do
44
+ if search_params.include?(:from) || search_params.include?(:to)
45
+ from = search_params.delete(:from)
46
+ to = search_params.delete(:to)
47
+ if to.nil?
48
+ newer_than(from)
49
+ elsif from.nil?
50
+ between(0, to)
51
+ else
52
+ between(from, to)
53
+ end
54
+ elsif search_params.include?(:fromId) || search_params.include?(:toId)
55
+ from_id = search_params.delete(:fromId)
56
+ to_id = search_params.delete(:toId)
57
+ if to_id.nil?
58
+ starting_at(from_id)
59
+ elsif from_id.nil?
60
+ between_ids(1, to_id)
61
+ else
62
+ between_ids(from_id, to_id)
63
+ end
64
+ else
65
+ # If all the params nil or empty or 0 then return a limited set
66
+ if search_params.all? {|_, v| v.to_i == 0}
67
+ all(1000)
68
+ else
69
+ # We have some search params, so return everything
70
+ all(0)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def search_fields(search_params)
77
+ search_params = keys_to_sym(search_params)
78
+ ret_limit = search_params.delete(:limit)
79
+ all_events = event_slice(search_params)
80
+
81
+ # Filter out nils and empty strings up front
82
+ # This allows the catch-all default-all to still work
83
+ valid_search_params = {}
84
+ search_params.each do |k,v|
85
+ next if v.nil?
86
+ next if v.empty?
87
+ valid_search_params[k] = v
88
+ end
89
+
90
+ # Short circuit here and give back all events if there's no search
91
+ # parameters
92
+ if valid_search_params.empty?
93
+ if ret_limit
94
+ return all_events.limit(ret_limit)
95
+ else
96
+ return all_events
97
+ end
98
+ end
99
+
100
+ ret_events = []
101
+
102
+ all_events.each do |event|
103
+ matched = false
104
+ valid_search_params.each do |k, v|
105
+ # Silently ignore bad search fields
106
+ if event[k]
107
+ if event[k].match(/#{v}/i)
108
+ matched = true
109
+ else
110
+ matched = false
111
+ # Stop looking, this is an AND search
112
+ break
113
+ end
114
+ end
115
+ end
116
+ if matched
117
+ ret_events.push(event)
118
+ end
119
+ end
120
+
121
+ if ret_limit
122
+ ret_events[0..(ret_limit.to_i - 1)]
123
+ else
124
+ ret_events
125
+ end
126
+ end
127
+
128
+ # This now brokers between the new and old style search
129
+ def search(pattern, fields=['value'])
130
+ if pattern.kind_of?(Hash)
131
+ format_rows(search_fields(pattern))
132
+ elsif pattern.kind_of?(String)
133
+ format_rows(search_text(pattern, fields))
134
+ else
135
+ format_rows(nil)
136
+ end
137
+ end
138
+
139
+ # Old style search, one pattern, multiple fields
140
+ def search_text(pattern, fields)
141
+ all_events = with_no_json do
142
+ all(0)
143
+ end
144
+ ret_events = []
145
+
146
+ all_events.each do |event|
147
+ event.each do |k, v|
148
+ if fields.member?(k.to_s) or fields.member?('all')
149
+ if v =~ /#{pattern}/i
150
+ ret_events.push(event)
151
+ break
152
+ end
153
+ end
154
+ end
155
+ end
156
+ ret_events
157
+ end
158
+
159
+ def by_user(uid, limit=1000)
160
+ user_events = @events.where(:uid => uid).reverse_order(:id).limit(limit)
161
+ format_rows(user_events)
162
+ end
163
+
164
+ def by_source(source, limit=1000)
165
+ source_events = @events.where(:source => source).reverse_order(:id).limit(limit)
166
+ format_rows(source_events)
167
+ end
168
+
169
+ def by_id(id)
170
+ single_event = @events.where(:id => id)
171
+ format_rows(single_event)
172
+ end
173
+
174
+ def recent()
175
+ all(50)
176
+ end
177
+
178
+ def delete(event_id)
179
+ end
180
+
181
+ def starting_at(event_id)
182
+ new_events = @events.where{id >= "#{event_id}"}.reverse_order(:id)
183
+ format_rows(new_events)
184
+ end
185
+
186
+ def between_ids(start_id, end_id)
187
+ events = @events.where(:id => start_id..end_id)
188
+ format_rows(events)
189
+ end
190
+
191
+ def list_distinct(column)
192
+ column = column.to_sym
193
+ things = @events.select(column).distinct
194
+ thing_list = things.map{|t| t[column]}
195
+ # don't return nil, that's just silly
196
+ thing_list.delete(nil)
197
+ if @return_json
198
+ "#{thing_list.to_json}\n"
199
+ else
200
+ thing_list
201
+ end
202
+ end
203
+
204
+ def url_list()
205
+ list_distinct(:url)
206
+ end
207
+
208
+ def source_list()
209
+ list_distinct(:source)
210
+ end
211
+
212
+ def user_list()
213
+ list_distinct(:uid)
214
+ end
215
+
216
+ def format_rows(rows)
217
+ if @return_json
218
+ rows_to_json(rows)
219
+ else
220
+ rows
221
+ end
222
+ end
223
+
224
+ def rows_to_json(rows)
225
+ ret_events = []
226
+ unless rows.nil?
227
+ rows.each do |row|
228
+ cur_event = Squab::Event.new(row[:value],
229
+ row[:url],
230
+ row[:uid],
231
+ row[:source],
232
+ row[:date],
233
+ row[:id])
234
+ ret_events.push(cur_event)
235
+ end
236
+ end
237
+ "#{ret_events.to_json}\n"
238
+ end
239
+
240
+ def newer_than(timestamp)
241
+ new_events = @events.where{date >= "#{timestamp.to_s}"}.reverse_order(:id)
242
+ format_rows(new_events)
243
+ end
244
+
245
+ def between(from, to)
246
+ new_events = @events.where{date >= "#{from}"}.where{date <= "#{to}"}.reverse_order(:id)
247
+ format_rows(new_events)
248
+ end
249
+
250
+ def add_event(event)
251
+ if event.date.nil?
252
+ now = Time.now.to_i
253
+ event.date = now
254
+ end
255
+ @events.insert(:date => event.date,
256
+ :value => event.value,
257
+ :url => event.url,
258
+ :source => event.source,
259
+ :uid => event.uid,
260
+ :deleted => false)
261
+ event.to_json
262
+ end
263
+
264
+ def keys_to_sym(old_hash)
265
+ new_hash = {}
266
+ old_hash.each do |k,v|
267
+ new_hash[k.to_sym] = old_hash[k]
268
+ end
269
+ new_hash
270
+ end
271
+ end
272
+
273
+ class Event
274
+ attr_accessor :date, :value, :url, :uid, :id, :source
275
+
276
+ def initialize(value, url, uid, source, date=nil, id=nil)
277
+ @date = date
278
+ @value = value
279
+ @uid = uid
280
+ @url = url
281
+ @source = source
282
+ @id = id
283
+ @push = false
284
+ end
285
+
286
+ def to_h
287
+ {
288
+ date: @date,
289
+ uid: @uid,
290
+ value: @value,
291
+ url: @url,
292
+ source: @source,
293
+ id: @id,
294
+ }
295
+ end
296
+
297
+ def to_json(*a)
298
+ self.to_h.to_json(*a)
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,231 @@
1
+ require 'json'
2
+ require 'pathname'
3
+ require 'sinatra'
4
+ require 'sinatra/config_file'
5
+ require 'squab'
6
+
7
+ module Squab
8
+ class Web < Sinatra::Base
9
+ register Sinatra::ConfigFile
10
+
11
+ # Set up some defaults we include with the package
12
+ default_config = File.join(File.dirname(__FILE__), '../../defaults.yaml')
13
+ config_file default_config
14
+
15
+ # Check for user provided defaults
16
+ config = ENV['SQUAB_CONFIG'] || '/etc/squab.yaml'
17
+ if File.exists?(config)
18
+ config_file config
19
+ set :config, config
20
+ else
21
+ set :config, default_config
22
+ end
23
+
24
+ # Take whatever the connect string is from the config and feed it
25
+ # to Squab::Events and make an object
26
+ set :dbconn, Squab::Events.new(settings.dbconn)
27
+
28
+ # Some database backends are not threadsafe, use a lock if squab
29
+ # doesn't report they are threadsafe
30
+ if not settings.dbconn.threadsafe
31
+ enable :lock
32
+ end
33
+
34
+ unless settings.root.start_with?('/')
35
+ # Allow relative pathing
36
+ set :root,
37
+ File.expand_path(File.join(File.dirname(__FILE__), settings.root))
38
+ end
39
+
40
+ enable :logging
41
+ enable :dump_errors
42
+ disable :raise_errors
43
+
44
+ configure :development do
45
+ # 0 is debug level
46
+ set :logging, 0
47
+ enable :show_exceptions
48
+ end
49
+
50
+ helpers do
51
+ def bad_request
52
+ status 400
53
+ redirect "api.html"
54
+ end
55
+ def safe_db(&block)
56
+ begin
57
+ block.call
58
+ rescue Sequel::DatabaseConnectionError, Sequel::DatabaseError => e
59
+ $stderr.puts e
60
+ exit!
61
+ end
62
+ end
63
+ def get_json_body(request)
64
+ data = nil
65
+ begin
66
+ request.body.rewind
67
+ data = JSON.parse request.body.read
68
+ logger.debug(data.to_s)
69
+ rescue JSON::ParserError
70
+ request.body.rewind
71
+ logger.warn("Bad JSON Body: " + request.body.read)
72
+ logger.debug("Bad Request: " + request.inspect.to_s)
73
+ end
74
+ data
75
+ end
76
+ end
77
+
78
+ get '/' do
79
+ File.read(File.join(settings.public_folder, 'events.html'))
80
+ end
81
+
82
+ get '/api/v1/events' do
83
+ safe_db do
84
+ settings.dbconn.all
85
+ end
86
+ end
87
+
88
+ get '/api/v1/events/recent' do
89
+ safe_db do
90
+ settings.dbconn.recent
91
+ end
92
+ end
93
+
94
+ get '/api/v1/events/limit/:limit' do |limit|
95
+ safe_db do
96
+ settings.dbconn.all(limit)
97
+ end
98
+ end
99
+
100
+ get '/api/v1/events/:id' do |id|
101
+ safe_db do
102
+ settings.dbconn.by_id(id)
103
+ end
104
+ end
105
+
106
+ get '/api/v1/events/starting/:id' do |id|
107
+ safe_db do
108
+ settings.dbconn.starting_at(id)
109
+ end
110
+ end
111
+
112
+ get '/api/v1/events/starting/:start_id/to/:end_id' do |start_id, end_id|
113
+ safe_db do
114
+ settings.dbconn.between_ids(start_id, end_id)
115
+ end
116
+ end
117
+
118
+ get '/api/v1/events/since/:date' do |date|
119
+ date = date.to_i
120
+ now = Time.now.to_i
121
+ if date > now
122
+ date = now
123
+ end
124
+ safe_db do
125
+ settings.dbconn.newer_than(date)
126
+ end
127
+ end
128
+
129
+ get '/api/v1/events/since/:start_date/to/:end_date' do |start_date, end_date|
130
+ start_date = start_date.to_i
131
+ end_date = end_date.to_i
132
+ now = Time.now.to_i
133
+ if end_date > now
134
+ end_date = now
135
+ end
136
+ if start_date > end_date
137
+ []
138
+ else
139
+ safe_db do
140
+ settings.dbconn.between(start_date, end_date)
141
+ end
142
+ end
143
+ end
144
+
145
+ get '/api/v1/events/user/:user' do |user|
146
+ safe_db do
147
+ settings.dbconn.by_user(user)
148
+ end
149
+ end
150
+
151
+ get '/api/v1/events/source/:source' do |source|
152
+ safe_db do
153
+ settings.dbconn.by_source(source)
154
+ end
155
+ end
156
+
157
+ get '/api/v1/events/search/:field/:pattern' do |field, pattern|
158
+ safe_db do
159
+ settings.dbconn.search(pattern, [field])
160
+ end
161
+ end
162
+
163
+ get '/api/v1/events/search/:field/:pattern/limit/:limit' do |field, pattern, limit|
164
+ safe_db do
165
+ settings.dbconn.search({field => pattern, :limit => limit})
166
+ end
167
+ end
168
+
169
+ get '/api/v1/users' do
170
+ safe_db do
171
+ settings.dbconn.user_list
172
+ end
173
+ end
174
+
175
+ get '/api/v1/urls' do
176
+ safe_db do
177
+ settings.dbconn.url_list
178
+ end
179
+ end
180
+
181
+ get '/api/v1/sources' do
182
+ safe_db do
183
+ settings.dbconn.source_list
184
+ end
185
+ end
186
+
187
+ post '/api/v1/events' do
188
+ data = get_json_body(request)
189
+ return 400 unless data.kind_of?(Hash)
190
+ safe_db do
191
+ e = settings.dbconn
192
+
193
+ new_event = Event.new(data['value'],
194
+ data['url'],
195
+ data['uid'],
196
+ data['source'],
197
+ data['date'])
198
+
199
+ "#{e.add_event(new_event)}\n"
200
+ end
201
+ end
202
+
203
+ post '/api/v1/events/search' do
204
+ data = get_json_body(request)
205
+ return 400 unless data.kind_of?(Hash)
206
+ safe_db do
207
+ if data.has_key?('fields') && data.has_key?('pattern')
208
+ settings.dbconn.search(data["pattern"], data["fields"])
209
+ else
210
+ settings.dbconn.search(data)
211
+ end
212
+ end
213
+ end
214
+
215
+ get '/_status' do
216
+ if File.exists?(File.join(File.dirname(settings.config), ('down')))
217
+ 500
218
+ else
219
+ 200
220
+ end
221
+ end
222
+
223
+ get '/api' do
224
+ redirect "api.html"
225
+ end
226
+
227
+ get '*' do
228
+ File.read(File.join(settings.public_folder, 'events.html'))
229
+ end
230
+ end
231
+ end