tentd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +9 -0
  5. data/Guardfile +6 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +49 -0
  8. data/Rakefile +8 -0
  9. data/bin/tent-server +3 -0
  10. data/lib/tentd.rb +31 -0
  11. data/lib/tentd/api.rb +58 -0
  12. data/lib/tentd/api/apps.rb +196 -0
  13. data/lib/tentd/api/authentication_finalize.rb +12 -0
  14. data/lib/tentd/api/authentication_lookup.rb +27 -0
  15. data/lib/tentd/api/authentication_verification.rb +50 -0
  16. data/lib/tentd/api/authorizable.rb +21 -0
  17. data/lib/tentd/api/authorization.rb +14 -0
  18. data/lib/tentd/api/core_profile_data.rb +45 -0
  19. data/lib/tentd/api/followers.rb +218 -0
  20. data/lib/tentd/api/followings.rb +241 -0
  21. data/lib/tentd/api/groups.rb +161 -0
  22. data/lib/tentd/api/middleware.rb +32 -0
  23. data/lib/tentd/api/posts.rb +373 -0
  24. data/lib/tentd/api/profile.rb +78 -0
  25. data/lib/tentd/api/router.rb +123 -0
  26. data/lib/tentd/api/router/caching_headers.rb +49 -0
  27. data/lib/tentd/api/router/extract_params.rb +88 -0
  28. data/lib/tentd/api/router/serialize_response.rb +38 -0
  29. data/lib/tentd/api/user_lookup.rb +10 -0
  30. data/lib/tentd/core_ext/hash/slice.rb +29 -0
  31. data/lib/tentd/datamapper/array_property.rb +23 -0
  32. data/lib/tentd/datamapper/query.rb +19 -0
  33. data/lib/tentd/json_patch.rb +181 -0
  34. data/lib/tentd/model.rb +30 -0
  35. data/lib/tentd/model/app.rb +68 -0
  36. data/lib/tentd/model/app_authorization.rb +113 -0
  37. data/lib/tentd/model/follower.rb +105 -0
  38. data/lib/tentd/model/following.rb +100 -0
  39. data/lib/tentd/model/group.rb +24 -0
  40. data/lib/tentd/model/mention.rb +19 -0
  41. data/lib/tentd/model/notification_subscription.rb +56 -0
  42. data/lib/tentd/model/permissible.rb +227 -0
  43. data/lib/tentd/model/permission.rb +28 -0
  44. data/lib/tentd/model/post.rb +178 -0
  45. data/lib/tentd/model/post_attachment.rb +27 -0
  46. data/lib/tentd/model/post_version.rb +64 -0
  47. data/lib/tentd/model/profile_info.rb +80 -0
  48. data/lib/tentd/model/random_public_id.rb +46 -0
  49. data/lib/tentd/model/serializable.rb +58 -0
  50. data/lib/tentd/model/type_properties.rb +36 -0
  51. data/lib/tentd/model/user.rb +39 -0
  52. data/lib/tentd/model/user_scoped.rb +14 -0
  53. data/lib/tentd/notifications.rb +13 -0
  54. data/lib/tentd/notifications/girl_friday.rb +30 -0
  55. data/lib/tentd/notifications/sidekiq.rb +50 -0
  56. data/lib/tentd/tent_type.rb +20 -0
  57. data/lib/tentd/tent_version.rb +41 -0
  58. data/lib/tentd/version.rb +3 -0
  59. data/spec/fabricators/app_authorizations_fabricator.rb +5 -0
  60. data/spec/fabricators/apps_fabricator.rb +11 -0
  61. data/spec/fabricators/followers_fabricator.rb +14 -0
  62. data/spec/fabricators/followings_fabricator.rb +17 -0
  63. data/spec/fabricators/groups_fabricator.rb +3 -0
  64. data/spec/fabricators/mentions_fabricator.rb +3 -0
  65. data/spec/fabricators/notification_subscriptions_fabricator.rb +4 -0
  66. data/spec/fabricators/permissions_fabricator.rb +1 -0
  67. data/spec/fabricators/post_attachments_fabricator.rb +8 -0
  68. data/spec/fabricators/post_versions_fabricator.rb +12 -0
  69. data/spec/fabricators/posts_fabricator.rb +12 -0
  70. data/spec/fabricators/profile_infos_fabricator.rb +30 -0
  71. data/spec/integration/api/apps_spec.rb +466 -0
  72. data/spec/integration/api/followers_spec.rb +535 -0
  73. data/spec/integration/api/followings_spec.rb +688 -0
  74. data/spec/integration/api/groups_spec.rb +207 -0
  75. data/spec/integration/api/posts_spec.rb +874 -0
  76. data/spec/integration/api/profile_spec.rb +285 -0
  77. data/spec/integration/api/router_spec.rb +102 -0
  78. data/spec/integration/model/app_authorization_spec.rb +59 -0
  79. data/spec/integration/model/app_spec.rb +63 -0
  80. data/spec/integration/model/follower_spec.rb +344 -0
  81. data/spec/integration/model/following_spec.rb +97 -0
  82. data/spec/integration/model/group_spec.rb +39 -0
  83. data/spec/integration/model/notification_subscription_spec.rb +145 -0
  84. data/spec/integration/model/post_spec.rb +658 -0
  85. data/spec/spec_helper.rb +37 -0
  86. data/spec/support/expect_server.rb +3 -0
  87. data/spec/support/json_request.rb +54 -0
  88. data/spec/support/with_constant.rb +23 -0
  89. data/spec/support/with_warnings.rb +6 -0
  90. data/spec/unit/api/authentication_finalize_spec.rb +45 -0
  91. data/spec/unit/api/authentication_lookup_spec.rb +65 -0
  92. data/spec/unit/api/authentication_verification_spec.rb +50 -0
  93. data/spec/unit/api/authorizable_spec.rb +50 -0
  94. data/spec/unit/api/authorization_spec.rb +44 -0
  95. data/spec/unit/api/caching_headers_spec.rb +121 -0
  96. data/spec/unit/core_profile_data_spec.rb +64 -0
  97. data/spec/unit/json_patch_spec.rb +407 -0
  98. data/spec/unit/tent_type_spec.rb +28 -0
  99. data/spec/unit/tent_version_spec.rb +68 -0
  100. data/tentd.gemspec +36 -0
  101. metadata +435 -0
@@ -0,0 +1,78 @@
1
+ module TentD
2
+ class API
3
+ class Profile
4
+ include Router
5
+
6
+ class AuthorizeWrite < Middleware
7
+ def action(env)
8
+ authorize_env!(env, :write_profile)
9
+ env
10
+ end
11
+ end
12
+
13
+ class Get < Middleware
14
+ def action(env)
15
+ env.response = Model::ProfileInfo.get_profile(env.authorized_scopes, env.current_auth)
16
+ env
17
+ end
18
+ end
19
+
20
+ class Update < Middleware
21
+ def action(env)
22
+ data = env.params.data
23
+ type = URI.unescape(env.params.type_url)
24
+ raise Unauthorized unless ['all', type].find { |t| env.current_auth.profile_info_types.include?(t) }
25
+ env.updated_info = Model::ProfileInfo.update_profile(type, data)
26
+ env
27
+ end
28
+ end
29
+
30
+ class Patch < Middleware
31
+ def action(env)
32
+ diff_array = env.params[:data]
33
+ profile_hash = env.delete(:response)
34
+ new_profile_hash = Marshal.load(Marshal.dump(profile_hash)).to_hash # equivalent of recursive dup
35
+ JsonPatch.merge(new_profile_hash, diff_array)
36
+ if new_profile_hash != profile_hash
37
+ env.updated_info = new_profile_hash.map do |type, data|
38
+ Model::ProfileInfo.update_profile(type, data)
39
+ end
40
+ end
41
+ env
42
+ rescue JsonPatch::ObjectNotFound, JsonPatch::ObjectExists => e
43
+ env['response.status'] = 422
44
+ env
45
+ end
46
+ end
47
+
48
+ class Notify < Middleware
49
+ def action(env)
50
+ return env unless env.updated_info
51
+ Array(env.updated_info).each do |info|
52
+ Notifications.profile_info_update(:profile_info_id => info.id)
53
+ end
54
+ env
55
+ end
56
+ end
57
+
58
+ get '/profile' do |b|
59
+ b.use Get
60
+ end
61
+
62
+ put '/profile/:type_url' do |b|
63
+ b.use AuthorizeWrite
64
+ b.use Update
65
+ b.use Get
66
+ b.use Notify
67
+ end
68
+
69
+ patch '/profile' do |b|
70
+ b.use AuthorizeWrite
71
+ b.use Get
72
+ b.use Patch
73
+ b.use Get
74
+ b.use Notify
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,123 @@
1
+ require 'rack/mount'
2
+ require 'rack/head'
3
+
4
+ class Rack::Mount::RouteSet
5
+ def merge_routes(routes)
6
+ routes.each { |r| merge_route(r) }
7
+ rehash
8
+ end
9
+
10
+ def merge_route(route)
11
+ @routes << route
12
+
13
+ @recognition_key_analyzer << route.conditions
14
+
15
+ @named_routes[route.name] = route if route.name
16
+ @generation_route_keys << route.generation_keys
17
+
18
+ expire!
19
+ route
20
+ end
21
+ end
22
+
23
+ module TentD
24
+ class API
25
+ module Router
26
+ autoload :ExtractParams, 'tentd/api/router/extract_params'
27
+ autoload :SerializeResponse, 'tentd/api/router/serialize_response'
28
+ autoload :CachingHeaders, 'tentd/api/router/caching_headers'
29
+
30
+ def self.included(base)
31
+ base.extend(ClassMethods)
32
+ end
33
+
34
+ def call(env)
35
+ self.class.routes.call(env)
36
+ end
37
+
38
+ module ClassMethods
39
+ def mount(klass)
40
+ routes.merge_routes klass.routes.instance_variable_get("@routes")
41
+ end
42
+
43
+ def routes
44
+ @routes ||= Rack::Mount::RouteSet.new
45
+ end
46
+
47
+ #### This section heavily "inspired" by sinatra
48
+
49
+ # Defining a `GET` handler also automatically defines
50
+ # a `HEAD` handler.
51
+ def get(path, opts={}, &block)
52
+ route('GET', path, opts, &block)
53
+ route('HEAD', path, opts, &block)
54
+ end
55
+
56
+ def put(path, opts={}, &bk) route 'PUT', path, opts, &bk end
57
+ def post(path, opts={}, &bk) route 'POST', path, opts, &bk end
58
+ def delete(path, opts={}, &bk) route 'DELETE', path, opts, &bk end
59
+ def head(path, opts={}, &bk) route 'HEAD', path, opts, &bk end
60
+ def options(path, opts={}, &bk) route 'OPTIONS', path, opts, &bk end
61
+ def patch(path, opts={}, &bk) route 'PATCH', path, opts, &bk end
62
+
63
+ private
64
+
65
+ def route(verb, path, options={}, &block)
66
+ path, params = compile_path(path)
67
+
68
+ return if route_exists?(verb, path)
69
+
70
+ builder = Rack::Builder.new(SerializeResponse.new)
71
+ builder.use(Rack::Head)
72
+ builder.use(UserLookup)
73
+ builder.use(AuthenticationLookup)
74
+ builder.use(AuthenticationVerification)
75
+ builder.use(AuthenticationFinalize)
76
+ builder.use(ExtractParams, path, params)
77
+ builder.use(Authorization)
78
+ block.call(builder)
79
+ builder.use(CachingHeaders)
80
+
81
+ routes.add_route(builder.to_app, :request_method => verb, :path_info => path)
82
+ routes.rehash
83
+ end
84
+
85
+ def route_exists?(verb, path)
86
+ @added_routes ||= []
87
+ return true if @added_routes.include?("#{verb}#{path}")
88
+ @added_routes << "#{verb}#{path}"
89
+ false
90
+ end
91
+
92
+ def compile_path(path)
93
+ keys = []
94
+ if path.respond_to? :to_str
95
+ ignore = ""
96
+ pattern = path.to_str.gsub(/[^\?\%\\\/\:\*\w]/) do |c|
97
+ ignore << escaped(c).join if c.match(/[\.@]/)
98
+ encoded(c)
99
+ end
100
+ pattern.gsub!(/((:\w+)|\*)/) do |match|
101
+ if match == "*"
102
+ keys << 'splat'
103
+ "(.*?)"
104
+ else
105
+ keys << $2[1..-1]
106
+ "([^#{ignore}/?#]+)"
107
+ end
108
+ end
109
+ [/\A#{pattern}\z/, keys]
110
+ elsif path.respond_to?(:keys) && path.respond_to?(:match)
111
+ [path, path.keys]
112
+ elsif path.respond_to?(:names) && path.respond_to?(:match)
113
+ [path, path.names]
114
+ elsif path.respond_to? :match
115
+ [path, keys]
116
+ else
117
+ raise TypeError, path
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,49 @@
1
+ require 'time'
2
+
3
+ module TentD
4
+ class API
5
+ module Router
6
+ class CachingHeaders
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ return @app.call(env) unless %w(GET HEAD).include?(env['REQUEST_METHOD'])
13
+ last_modified_at = last_modified(env.response)
14
+ if_modified_since = env['HTTP_IF_MODIFIED_SINCE']
15
+ if if_modified_since && Time.httpdate(if_modified_since) >= last_modified_at
16
+ return [304, {}, nil]
17
+ end
18
+ status, headers, body = @app.call(env)
19
+ headers['Last-Modified'] ||= last_modified_at.httpdate if last_modified_at
20
+ headers['Cache-Control'] ||= cache_control(env.response) if cache_control(env.response)
21
+ [status, headers, body]
22
+ end
23
+
24
+ private
25
+
26
+ def last_modified(object)
27
+ if object.respond_to?(:updated_at)
28
+ object.updated_at
29
+ elsif object.kind_of?(Enumerable) && object.first.respond_to?(:updated_at)
30
+ object.map { |o| o.updated_at }.sort.last
31
+ end
32
+ end
33
+
34
+ def cache_control(object)
35
+ if object.respond_to?(:public) || object.respond_to?(:permissions)
36
+ public?(object) ? 'public' : 'private'
37
+ elsif object.kind_of?(Enumerable) && (object.first.respond_to?(:public) || object.first.kind_of?(Hash) && object.first['permissions'])
38
+ object.map { |o| public?(o) }.uniq == [true] ? 'public' : 'private'
39
+ end
40
+ end
41
+
42
+ def public?(object)
43
+ object.respond_to?(:public) && object.public ||
44
+ object.kind_of?(Hash) && (object['public'] || object['permissions'] && object['permissions']['public'])
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,88 @@
1
+ module TentD
2
+ class API
3
+ module Router
4
+ class ExtractParams
5
+ attr_accessor :pattern, :keys
6
+
7
+ def initialize(app, pattern, keys)
8
+ @app, @pattern, @keys = app, pattern, keys
9
+ end
10
+
11
+ def call(env)
12
+ add_request(env)
13
+ extract_params(env)
14
+ env['tent.entity'] ||= ENV['TENT_ENTITY']
15
+ @app.call(env)
16
+ end
17
+
18
+ private
19
+
20
+ def add_request(env)
21
+ env['request'] = Rack::Request.new(env)
22
+ end
23
+
24
+ def extract_params(env)
25
+ route = env[Rack::Mount::Prefix::KEY]
26
+ route = '/' if route.empty?
27
+ return unless match = pattern.match(route)
28
+ values = match.captures.to_a.map { |v| URI.decode_www_form_component(v) if v }
29
+
30
+ params = env['request'].params.dup
31
+
32
+ if values.any?
33
+ params.merge!('captures' => values)
34
+ keys.zip(values) { |k,v| Array === params[k] ? params[k] << v : params[k] = v if v }
35
+ end
36
+
37
+ begin
38
+ if env['CONTENT_TYPE'] =~ /\bjson\Z/
39
+ params['data'] = env['data'] || JSON.parse(env['rack.input'].read)
40
+ elsif env['CONTENT_TYPE'] =~ /\Amultipart/
41
+ key, data = params.find { |k,p| p[:type] == MEDIA_TYPE }
42
+ params.delete(key)
43
+ params['data'] = JSON.parse(data[:tempfile].read)
44
+ params['attachments'] = get_attachments(params)
45
+ end
46
+ rescue JSON::ParserError
47
+ end
48
+
49
+ env['params'] = indifferent_params(params)
50
+ end
51
+
52
+ def get_attachments(params)
53
+ params.inject([]) { |a,(key,value)|
54
+ if attachment?(value)
55
+ a << value.merge(:name => key)
56
+ elsif value.kind_of?(Hash)
57
+ a += value.select { |k,v| attachment?(v) }.map { |k,v| v.merge(:name => key) }
58
+ end
59
+ a
60
+ }
61
+ end
62
+
63
+ def attachment?(v)
64
+ v.kind_of?(Hash) && v[:tempfile].kind_of?(Tempfile)
65
+ end
66
+
67
+ # Enable string or symbol key access to the nested params hash.
68
+ def indifferent_params(object)
69
+ case object
70
+ when Hash
71
+ new_hash = indifferent_hash
72
+ object.each { |key, value| new_hash[key] = indifferent_params(value) }
73
+ new_hash
74
+ when Array
75
+ object.map { |item| indifferent_params(item) }
76
+ else
77
+ object
78
+ end
79
+ end
80
+
81
+ # Creates a Hash with indifferent access.
82
+ def indifferent_hash
83
+ Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,38 @@
1
+ require 'json'
2
+
3
+ module TentD
4
+ class API
5
+ module Router
6
+ class SerializeResponse
7
+ def call(env)
8
+ response = if env.response
9
+ env.response.kind_of?(String) ? env.response : env.response.to_json(serialization_options(env))
10
+ end
11
+ status = env['response.status'] || (response ? 200 : 404)
12
+ headers = if env['response.type'] || status == 200 && response && !response.empty?
13
+ { 'Content-Type' => env['response.type'] || MEDIA_TYPE }
14
+ else
15
+ {}
16
+ end
17
+ headers.merge!('Access-Control-Allow-Origin' => '*') if env['HTTP_ORIGIN']
18
+ [status, headers, [response.to_s]]
19
+ end
20
+
21
+ private
22
+
23
+ def serialization_options(env)
24
+ {
25
+ :app => env.current_auth.kind_of?(Model::AppAuthorization),
26
+ :authorization_token => env.authorized_scopes.include?(:read_apps),
27
+ :permissions => env.authorized_scopes.include?(:read_permissions),
28
+ :groups => env.authorized_scopes.include?(:read_groups),
29
+ :mac => env.authorized_scopes.include?(:read_secrets),
30
+ :self => env.authorized_scopes.include?(:self),
31
+ :auth_token => env.authorized_scopes.include?(:authorization_token),
32
+ :view => env.params.view
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,10 @@
1
+ module TentD
2
+ class API
3
+ class UserLookup < Middleware
4
+ def action(env)
5
+ Model::User.current ||= Model::User.first_or_create
6
+ env
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,29 @@
1
+ # Taken from rails
2
+ class Hash
3
+ # Slice a hash to include only the given keys. This is useful for
4
+ # limiting an options hash to valid keys before passing to a method:
5
+ #
6
+ # def search(criteria = {})
7
+ # assert_valid_keys(:mass, :velocity, :time)
8
+ # end
9
+ #
10
+ # search(options.slice(:mass, :velocity, :time))
11
+ #
12
+ # If you have an array of keys you want to limit to, you should splat them:
13
+ #
14
+ # valid_keys = [:mass, :velocity, :time]
15
+ # search(options.slice(*valid_keys))
16
+ def slice(*keys)
17
+ keys.each_with_object(self.class.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
18
+ end
19
+
20
+ # Replaces the hash with only the given keys.
21
+ # Returns a hash containing the removed key/value pairs.
22
+ # {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d => 4}
23
+ def slice!(*keys)
24
+ omit = slice(*self.keys - keys)
25
+ hash = slice(*keys)
26
+ replace(hash)
27
+ omit
28
+ end
29
+ end
@@ -0,0 +1,23 @@
1
+ module DataMapper
2
+ class Property
3
+ # Implements flat postgres string arrays
4
+ class Array < DataMapper::Property::Text
5
+ def custom?
6
+ true
7
+ end
8
+
9
+ def load(value)
10
+ return value if value.kind_of? ::Array
11
+ value[1..-2].split(',').map { |v| v[0,1] == '"' ? v[1..-2] : v } unless value.nil?
12
+ end
13
+
14
+ def dump(value)
15
+ "{#{value.map(&:to_s).map(&:inspect).join(',')}}" unless value.nil?
16
+ end
17
+
18
+ def typecast(value)
19
+ load(value)
20
+ end
21
+ end
22
+ end
23
+ end