squab 1.3.2

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.
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