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,178 @@
1
+ require 'securerandom'
2
+
3
+ module TentD
4
+ module Model
5
+ class Post
6
+ include DataMapper::Resource
7
+ include Permissible
8
+ include RandomPublicId
9
+ include Serializable
10
+ include TypeProperties
11
+ include UserScoped
12
+
13
+ storage_names[:default] = "posts"
14
+
15
+ property :id, Serial
16
+ property :entity, Text, :lazy => false, :unique_index => :upublic_id
17
+ property :public, Boolean, :default => false
18
+ property :licenses, Array, :default => []
19
+ property :content, Json, :default => {}
20
+ property :views, Json, :default => {}
21
+ property :published_at, DateTime, :default => lambda { |*args| Time.now }
22
+ property :received_at, DateTime, :default => lambda { |*args| Time.now }
23
+ property :updated_at, DateTime
24
+ property :deleted_at, ParanoidDateTime
25
+ property :app_name, Text, :lazy => false
26
+ property :app_url, Text, :lazy => false
27
+ property :original, Boolean, :default => false
28
+
29
+ has n, :permissions, 'TentD::Model::Permission', :constraint => :destroy
30
+ has n, :attachments, 'TentD::Model::PostAttachment', :constraint => :destroy
31
+ has n, :mentions, 'TentD::Model::Mention', :constraint => :destroy
32
+ belongs_to :app, 'TentD::Model::App', :required => false
33
+ belongs_to :following, 'TentD::Model::Following', :required => false
34
+
35
+ has n, :versions, 'TentD::Model::PostVersion', :constraint => :destroy
36
+
37
+ after :create, :create_version!
38
+
39
+ def create_version!(post = self)
40
+ attrs = post.attributes
41
+ attrs.delete(:id)
42
+ latest = post.versions.all(:order => :version.desc, :fields => [:version]).first
43
+ attrs[:version] = latest ? latest.version + 1 : 1
44
+ version = post.versions.create(attrs)
45
+ end
46
+
47
+ def latest_version(options = {})
48
+ versions.all({ :order => :version.desc }.merge(options)).first
49
+ end
50
+
51
+ def update(data)
52
+ mentions = data.delete(:mentions)
53
+ last_version = latest_version(:fields => [:id])
54
+ res = super(data)
55
+
56
+ create_version! # after update hook doe not fire
57
+
58
+ current_version = latest_version(:fields => [:id])
59
+
60
+ if mentions.to_a.any?
61
+ Mention.all(:post_id => self.id).update(:post_id => nil, :post_version_id => last_version.id)
62
+ mentions.each do |mention|
63
+ next unless mention[:entity]
64
+ self.mentions.create(:entity => mention[:entity], :mentioned_post_id => mention[:post], :post_version_id => current_version.id)
65
+ end
66
+ end
67
+
68
+ res
69
+ end
70
+
71
+ def self.create(data)
72
+ mentions = data.delete(:mentions)
73
+ post = super(data)
74
+
75
+ mentions.to_a.each do |mention|
76
+ next unless mention[:entity]
77
+ post.mentions.create(:entity => mention[:entity], :mentioned_post_id => mention[:post], :post_version_id => post.latest_version(:fields => [:id]).id)
78
+ end
79
+
80
+ if post.mentions.to_a.any? && post.original
81
+ post.mentions.each do |mention|
82
+ follower = Follower.first(:entity => mention.entity)
83
+ next if follower && NotificationSubscription.first(:follower => follower, :type_base => post.type.base)
84
+
85
+ Notifications.notify_entity(:entity => mention.entity, :post_id => post.id)
86
+ end
87
+ end
88
+
89
+ post
90
+ end
91
+
92
+ def self.fetch_with_permissions(params, current_auth)
93
+ super do |params, query, query_bindings|
94
+ if params.since_time
95
+ query << "AND posts.published_at > ?"
96
+ query_bindings << Time.at(params.since_time.to_i)
97
+ end
98
+
99
+ if params.before_time
100
+ query << "AND posts.published_at < ?"
101
+ query_bindings << Time.at(params.before_time.to_i)
102
+ end
103
+
104
+ if params.post_types
105
+ params.post_types = params.post_types.split(',').map { |url| URI.unescape(url) }
106
+ if params.post_types.any?
107
+ query << "AND posts.type_base IN ?"
108
+ query_bindings << params.post_types.map { |t| TentType.new(t).base }
109
+ end
110
+ end
111
+
112
+ if params.mentioned_post && params.mentioned_entity
113
+ select = query.shift
114
+ query.unshift "INNER JOIN mentions ON mentions.post_id = posts.id"
115
+ query.unshift select
116
+
117
+ query << "AND mentions.entity = ? AND mentions.mentioned_post_id = ?"
118
+ query_bindings << params.mentioned_entity
119
+ query_bindings << params.mentioned_post
120
+ end
121
+
122
+ unless params.return_count
123
+ query << "ORDER BY posts.published_at DESC"
124
+ end
125
+ end
126
+ end
127
+
128
+ def self.public_attributes
129
+ [:app_name, :app_url, :entity, :type, :licenses, :content, :published_at]
130
+ end
131
+
132
+ def self.write_attributes
133
+ public_attributes + [:following_id, :original, :public, :mentions, :views]
134
+ end
135
+
136
+ def can_notify?(app_or_follow)
137
+ return true if public && original
138
+ case app_or_follow
139
+ when AppAuthorization
140
+ app_or_follow.scopes && app_or_follow.scopes.map(&:to_sym).include?(:read_posts) ||
141
+ app_or_follow.post_types && app_or_follow.post_types.include?(type.base)
142
+ when Follower
143
+ return false unless original
144
+ q = permissions.all(:follower_access_id => app_or_follow.id)
145
+ q += permissions.all(:group_public_id => app_or_follow.groups) if app_or_follow.groups.any?
146
+ q.any?
147
+ when Following
148
+ return false unless original
149
+ q = permissions.all(:following => app_or_follow)
150
+ q += permissions.all(:group_public_id => app_or_follow.groups) if app_or_follow.groups.any?
151
+ q.any?
152
+ else
153
+ false
154
+ end
155
+ end
156
+
157
+ def as_json(options = {})
158
+ attributes = super
159
+ attributes[:type] = type.uri
160
+ attributes[:version] = latest_version(:fields => [:version]).version
161
+ attributes[:app] = { :url => attributes.delete(:app_url), :name => attributes.delete(:app_name) }
162
+
163
+ attributes[:mentions] = mentions.map do |mention|
164
+ h = { :entity => mention.entity }
165
+ h[:post] = mention.mentioned_post_id if mention.mentioned_post_id
166
+ h
167
+ end
168
+
169
+ if options[:app]
170
+ attributes[:following_id] = following.public_id if following
171
+ end
172
+
173
+ Array(options[:exclude]).each { |k| attributes.delete(k) if k }
174
+ attributes
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,27 @@
1
+ module TentD
2
+ module Model
3
+ class PostAttachment
4
+ include DataMapper::Resource
5
+
6
+ storage_names[:default] = "post_attachments"
7
+
8
+ property :id, Serial
9
+ property :type, Text, :required => true, :lazy => false
10
+ property :category, Text, :required => true, :lazy => false
11
+ property :name, Text, :required => true, :lazy => false
12
+ property :data, Text, :required => true
13
+ property :size, Integer, :required => true
14
+ timestamps :at
15
+
16
+ belongs_to :post, 'TentD::Model::Post', :required => false
17
+ belongs_to :post_version, 'TentD::Model::PostVersion', :required => false
18
+
19
+ validates_presence_of :post_id, :if => lambda { |m| m.post_version_id.nil? }
20
+ validates_presence_of :post_version_id, :if => lambda { |m| m.post_id.nil? }
21
+
22
+ def as_json(options = {})
23
+ super({ :exclude => [:id, :data, :post_id, :created_at, :updated_at] }.merge(options))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,64 @@
1
+ module TentD
2
+ module Model
3
+ class PostVersion
4
+ include DataMapper::Resource
5
+ include Permissible
6
+ include Serializable
7
+ include TypeProperties
8
+ include UserScoped
9
+
10
+ storage_names[:default] = "post_versions"
11
+
12
+ belongs_to :post, 'TentD::Model::Post', :required => true
13
+ property :version, Integer, :required => true
14
+
15
+ property :id, Serial
16
+ property :entity, Text, :lazy => false
17
+ property :public_id, String, :required => true
18
+ property :public, Boolean, :default => false
19
+ property :licenses, Array, :default => []
20
+ property :content, Json, :default => {}
21
+ property :views, Json, :default => {}
22
+ property :published_at, DateTime, :default => lambda { |*args| Time.now }
23
+ property :received_at, DateTime, :default => lambda { |*args| Time.now }
24
+ property :updated_at, DateTime
25
+ property :deleted_at, ParanoidDateTime
26
+ property :app_name, Text, :lazy => false
27
+ property :app_url, Text, :lazy => false
28
+ property :original, Boolean, :default => false
29
+
30
+ has n, :attachments, 'TentD::Model::PostAttachment', :constraint => :destroy
31
+ has n, :mentions, 'TentD::Model::Mention', :constraint => :destroy
32
+ belongs_to :app, 'TentD::Model::App', :required => false
33
+ belongs_to :following, 'TentD::Model::Following', :required => false
34
+
35
+ def self.public_attributes
36
+ Post.public_attributes
37
+ end
38
+
39
+ def as_json(options = {})
40
+ attributes = super
41
+ post_attrs = post.as_json(options)
42
+
43
+ attributes[:type] = type.uri
44
+ attributes[:version] = version
45
+ attributes[:app] = { :url => attributes.delete(:app_url), :name => attributes.delete(:app_name) }
46
+
47
+ attributes[:mentions] = mentions.map do |mention|
48
+ h = { :entity => mention.entity }
49
+ h[:post] = mention.mentioned_post_id if mention.mentioned_post_id
50
+ h
51
+ end
52
+
53
+ if options[:app]
54
+ attributes[:following_id] = following.public_id if following
55
+ end
56
+
57
+ attributes[:permissions] = post_attrs[:permissions]
58
+
59
+ Array(options[:exclude]).each { |k| attributes.delete(k) if k }
60
+ attributes
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,80 @@
1
+ require 'hashie'
2
+
3
+ module TentD
4
+ module Model
5
+ class ProfileInfo
6
+ include DataMapper::Resource
7
+ include TypeProperties
8
+ include UserScoped
9
+ include Permissible
10
+
11
+ TENT_PROFILE_TYPE_URI = 'https://tent.io/types/info/core/v0.1.0'
12
+ TENT_PROFILE_TYPE = TentType.new(TENT_PROFILE_TYPE_URI)
13
+
14
+ self.raise_on_save_failure = true
15
+
16
+ storage_names[:default] = 'profile_info'
17
+
18
+ property :id, Serial
19
+ property :public, Boolean, :default => false
20
+ property :content, Json, :default => {}, :lazy => false
21
+ property :created_at, DateTime
22
+ property :updated_at, DateTime
23
+ property :deleted_at, ParanoidDateTime
24
+
25
+ has n, :permissions, 'TentD::Model::Permission'
26
+
27
+
28
+ def self.tent_info
29
+ first(:type_base => TENT_PROFILE_TYPE.base, :order => :type_version.desc) || Hashie::Mash.new
30
+ end
31
+
32
+ def self.get_profile(authorized_scopes = [], current_auth = nil)
33
+ h = if (authorized_scopes.include?(:read_profile) || authorized_scopes.include?(:write_profile)) && current_auth.respond_to?(:profile_info_types)
34
+ current_auth.profile_info_types.include?('all') ? all : all(:type_base => current_auth.profile_info_types.map { |t| TentType.new(t).base }) + all(:public => true)
35
+ else
36
+ fetch_with_permissions({}, current_auth)
37
+ end.inject({}) do |memo, info|
38
+ memo[info.type.uri] = info.content.merge(:permissions => info.permissions_json(authorized_scopes.include?(:read_permissions)))
39
+ memo
40
+ end
41
+ h
42
+ end
43
+
44
+ def self.update_profile(type, data)
45
+ data = Hashie::Mash.new(data) unless data.kind_of?(Hashie::Mash)
46
+ type = TentType.new(type)
47
+ perms = data.delete(:permissions)
48
+ if (infos = all(:type_base => type.base)) && (info = infos.pop)
49
+ infos.to_a.each(&:destroy)
50
+ info.type = type
51
+ info.public = data.delete(:public)
52
+ info.content = data
53
+ info.save
54
+ else
55
+ info = create(:type => type, :public => data.delete(:public), :content => data)
56
+ end
57
+ info.assign_permissions(perms)
58
+ info
59
+ end
60
+
61
+ def self.create_update_post(id)
62
+ first(:id => id).create_update_post
63
+ end
64
+
65
+ def create_update_post
66
+ post = user.posts.create(
67
+ :type => 'https://tent.io/types/post/profile/v0.1.0',
68
+ :entity => user.profile_entity,
69
+ :original => true,
70
+ :content => {
71
+ :action => 'update',
72
+ :types => [self.type.uri],
73
+ }
74
+ )
75
+ Permission.copy(self, post)
76
+ Notifications.trigger(:type => post.type.uri, :post_id => post.id)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,46 @@
1
+ module TentD
2
+ module Model
3
+ module RandomPublicId
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.class_eval do
7
+ property :public_id, String, :required => true, :unique_index => :upublic_id, :default => lambda { |*args| random_id }
8
+ self.raise_on_save_failure = true
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def random_id
14
+ rand(36 ** 6).to_s(36)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # TODO: Debug DataMapper state issue
21
+ # # catch unique public_id validation and generate a new one
22
+ # def assert_save_successful(*args)
23
+ # super
24
+ # rescue DataMapper::SaveFailureError
25
+ # if self.class.all(:public_id => self.public_id).any?
26
+ # self.public_id = self.class.random_id
27
+ # save
28
+ # else
29
+ # raise
30
+ # end
31
+ # end
32
+ #
33
+ # # catch db unique constraint on public_id and generate a new one
34
+ # def _persist
35
+ # super
36
+ # rescue DataObjects::IntegrityError
37
+ # if self.class.all(:public_id => self.public_id).any?
38
+ # self.public_id = self.class.random_id
39
+ # save
40
+ # else
41
+ # raise
42
+ # end
43
+ # end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,58 @@
1
+ module TentD
2
+ module Model
3
+ module Serializable
4
+ def as_json(options = {})
5
+ attributes = super(:only => self.class.public_attributes)
6
+ attributes.merge!(:permissions => permissions_json(options[:permissions])) if respond_to?(:permissions_json)
7
+ attributes[:id] = respond_to?(:public_id) ? public_id : id
8
+
9
+ if options[:app]
10
+ [:created_at, :updated_at, :published_at, :received_at].each { |key|
11
+ attributes[key] = send(key) if respond_to?(key)
12
+ }
13
+ end
14
+
15
+ [:published_at, :updated_at, :created_at, :received_at].each do |key|
16
+ attributes[key] = attributes[key].to_time.to_i if attributes[key].respond_to?(:to_time)
17
+ end
18
+
19
+ mac_fields = [:mac_key_id, :mac_key, :mac_algorithm]
20
+ if options[:mac] && mac_fields.select { |k| respond_to?(k) }.size == mac_fields.size
21
+ mac_fields.each { |k| attributes[k] = send(k) }
22
+ end
23
+
24
+ if options[:groups] && respond_to?(:groups)
25
+ attributes[:groups] = groups.to_a.uniq
26
+ end
27
+
28
+ if relationships.map(&:name).include?(:attachments)
29
+ if options[:view] && respond_to?(:views) && (conditions = (views[options[:view]] || {})['attachments'])
30
+ conditions.map! { |c| c.slice('category', 'name', 'type') }.reject! { |c| c.empty? }
31
+ attributes[:attachments] = conditions.inject(nil) { |memo, c|
32
+ q = attachments.all(c)
33
+ memo ? memo += q : q
34
+ }
35
+ else
36
+ attributes[:attachments] = attachments.all.map { |a| a.as_json } unless options[:view] == 'meta'
37
+ end
38
+ end
39
+
40
+ if options[:view] && respond_to?(:views) && respond_to?(:content)
41
+ if keypaths = (views[options[:view]] || {})['content']
42
+ attributes[:content] = keypaths.inject({}) do |memo, keypath|
43
+ pointer = JsonPatch::HashPointer.new(content, keypath)
44
+ memo[pointer.keys.last] = pointer.exists? ? pointer.value : nil
45
+ memo
46
+ end
47
+ elsif options[:view] == 'meta'
48
+ attributes.delete(:content)
49
+ elsif options[:view] != 'full'
50
+ attributes[:content] = {}
51
+ end
52
+ end
53
+
54
+ attributes
55
+ end
56
+ end
57
+ end
58
+ end