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