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,50 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module TentD
5
+ class API
6
+ class AuthenticationVerification < Middleware
7
+ def action(env)
8
+ if env.hmac? && !(env.hmac.verified = verify_signature(env))
9
+ env = [403, {}, ['Invalid MAC Signature']]
10
+ end
11
+ env
12
+ end
13
+
14
+ private
15
+
16
+ # constant-time comparison algorithm to prevent timing attacks
17
+ def secure_compare(a, b)
18
+ return false unless a.bytesize == b.bytesize
19
+
20
+ l = a.unpack "C#{a.bytesize}"
21
+
22
+ res = 0
23
+ b.each_byte { |byte| res |= byte ^ l.shift }
24
+ res == 0
25
+ end
26
+
27
+ def verify_signature(env)
28
+ secure_compare(env.hmac.mac, build_request_signature(env))
29
+ end
30
+
31
+ def build_request_signature(env)
32
+ time = env.hmac.ts.to_i
33
+ nonce = env.hmac.nonce
34
+ request_string = build_request_string(time, nonce, env)
35
+ signature = Base64.encode64(OpenSSL::HMAC.digest(openssl_digest(env.hmac.algorithm).new, env.hmac.secret, request_string)).sub("\n", '')
36
+ end
37
+
38
+ def build_request_string(time, nonce, env)
39
+ body = env['rack.input'].read
40
+ env['rack.input'].rewind
41
+ request_uri = env.SCRIPT_NAME + (env.QUERY_STRING != '' ? "?#{env.QUERY_STRING}" : '')
42
+ [time.to_s, nonce, env.REQUEST_METHOD.to_s.upcase, request_uri, env.HTTP_HOST.split(':').first, (env.HTTP_X_FORWARDED_PORT || env.SERVER_PORT), body, nil].join("\n")
43
+ end
44
+
45
+ def openssl_digest(mac_algorithm)
46
+ @openssl_digest ||= OpenSSL::Digest.const_get(mac_algorithm.to_s.gsub(/hmac|-/, '').upcase)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,21 @@
1
+ module TentD
2
+ class API
3
+ module Authorizable
4
+ class Error < StandardError
5
+ end
6
+
7
+ class Unauthorized < Error
8
+ end
9
+
10
+ def authorize_env!(env, scope)
11
+ unless authorize_env?(env, scope)
12
+ raise Unauthorized
13
+ end
14
+ end
15
+
16
+ def authorize_env?(env, scope)
17
+ env.authorized_scopes.to_a.include?(scope)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ module TentD
2
+ class API
3
+ class Authorization < Middleware
4
+ def action(env)
5
+ env.authorized_scopes = []
6
+ if env.current_auth.kind_of?(Model::AppAuthorization)
7
+ env.authorized_scopes = env.current_auth.scopes.map(&:to_sym)
8
+ env.authorized_scopes.delete(:read_secrets) unless env.params && env.params.secrets.to_s == 'true'
9
+ end
10
+ env
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ require 'hashie'
2
+
3
+ module TentD
4
+ class API
5
+ class CoreProfileData < Hashie::Mash
6
+ def expected_version
7
+ v = TentVersion.from_uri(Model::ProfileInfo::TENT_PROFILE_TYPE_URI)
8
+ v.parts = v.parts[0..-2] << 'x'
9
+ v
10
+ end
11
+
12
+ def versions
13
+ keys.select { |key|
14
+ key =~ %r|^#{Model::ProfileInfo::TENT_PROFILE_TYPE_URI.sub(/\/v.*?$/, '')}|
15
+ }.map { |key| TentVersion.from_uri(key) }.sort
16
+ end
17
+
18
+ def version
19
+ versions.find do |v|
20
+ v == expected_version
21
+ end
22
+ end
23
+
24
+ def version_key
25
+ Model::ProfileInfo::TENT_PROFILE_TYPE_URI.sub(%r|/v.*?$|, '/v' << version.to_s)
26
+ end
27
+
28
+ def entity?(entity)
29
+ self[version_key][:entity] == entity
30
+ end
31
+
32
+ def servers
33
+ self[version_key][:servers]
34
+ end
35
+
36
+ def entity
37
+ self[version_key][:entity]
38
+ end
39
+
40
+ def licenses
41
+ self[version_key][:licenses]
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,218 @@
1
+ module TentD
2
+ class API
3
+ class Followers
4
+ include Router
5
+
6
+ class GetActualId < Middleware
7
+ def action(env)
8
+ [:follower_id, :before_id, :since_id].each do |id_key|
9
+ if env.params[id_key] && (f = Model::Follower.first(:public_id => env.params[id_key], :fields => [:id]))
10
+ env.params[id_key] = f.id
11
+ else
12
+ env.params[id_key] = nil
13
+ end
14
+ end
15
+ env
16
+ end
17
+ end
18
+
19
+ class AuthorizeReadOne < Middleware
20
+ def action(env)
21
+ if env.params.follower_id && env.current_auth && env.current_auth.kind_of?(Model::Follower) &&
22
+ env.current_auth.id == env.params.follower_id
23
+ env.authorized_scopes << :self
24
+ end
25
+ env.full_read_authorized = authorize_env?(env, :read_followers)
26
+ env
27
+ end
28
+ end
29
+
30
+ class AuthorizeReadMany < Middleware
31
+ def action(env)
32
+ env.full_read_authorized = authorize_env?(env, :read_followers)
33
+ env
34
+ end
35
+ end
36
+
37
+ class AuthorizeWriteOne < Middleware
38
+ def action(env)
39
+ unless env.params.follower_id && env.current_auth && env.current_auth.kind_of?(Model::Follower) &&
40
+ env.current_auth.id == env.params.follower_id
41
+ authorize_env!(env, :write_followers)
42
+ end
43
+ env.authorized_scopes << :self
44
+ env
45
+ end
46
+ end
47
+
48
+ class Discover < Middleware
49
+ def action(env)
50
+ return env if env.authorized_scopes.include?(:write_followers)
51
+ return [422, {}, ['Invalid notification path']] unless env.params.data.notification_path.kind_of?(String) &&
52
+ !env.params.data.notification_path.match(%r{\Ahttps?://})
53
+ return [406, {}, ['Can not follow self']] if Model::User.current.profile_entity == env.params.data.entity
54
+ client = ::TentClient.new(nil, :faraday_adapter => TentD.faraday_adapter)
55
+ profile, profile_url = client.discover(env.params[:data]['entity']).get_profile
56
+ return [404, {}, ['Not Found']] unless profile
57
+
58
+ profile = CoreProfileData.new(profile)
59
+ return [409, {}, ['Entity Mismatch']] unless profile.entity?(env.params.data.entity)
60
+ env['profile'] = profile
61
+ env
62
+ end
63
+ end
64
+
65
+ class Confirm < Middleware
66
+ def action(env)
67
+ return env if env.authorized_scopes.include?(:write_followers)
68
+ client = TentClient.new(env.profile.servers, :faraday_adapter => TentD.faraday_adapter)
69
+ if client.follower.challenge(env.params.data.notification_path)
70
+ env
71
+ else
72
+ [403, {}, ['Unauthorized Follower']]
73
+ end
74
+ end
75
+ end
76
+
77
+ class Create < Middleware
78
+ def action(env)
79
+ return env if env.authorized_scopes.include?(:write_followers)
80
+ if follower = Model::Follower.create_follower(env.params[:data].merge('profile' => env['profile']))
81
+ env.authorized_scopes << :read_secrets
82
+ env.authorized_scopes << :self
83
+ env.notify_action = 'create'
84
+ env.notify_instance = follower
85
+ env.response = follower
86
+ end
87
+ env
88
+ end
89
+ end
90
+
91
+ class Import < Middleware
92
+ def action(env)
93
+ return env unless env.authorized_scopes.include?(:write_followers)
94
+ if env.authorized_scopes.include?(:write_secrets)
95
+ if follower = Model::Follower.create_follower(env.params.data, env.authorized_scopes)
96
+ env.response = ''
97
+ end
98
+ else
99
+ raise Unauthorized
100
+ end
101
+ env
102
+ end
103
+ end
104
+
105
+ class GetOne < Middleware
106
+ def action(env)
107
+ if env.full_read_authorized || authorize_env?(env, :self)
108
+ follower = Model::Follower.find(env.params.follower_id)
109
+ else
110
+ follower = Model::Follower.find_with_permissions(env.params.follower_id, env.current_auth)
111
+ end
112
+
113
+ if env.full_read_authorized || authorize_env?(env, :self) || (follower && follower.public?)
114
+ env.response = follower
115
+ else
116
+ raise Unauthorized
117
+ end
118
+
119
+ env
120
+ end
121
+ end
122
+
123
+ class GetCount < Middleware
124
+ def action(env)
125
+ env.params.return_count = true
126
+ env
127
+ end
128
+ end
129
+
130
+ class GetMany < Middleware
131
+ def action(env)
132
+ if env.full_read_authorized
133
+ followers = Model::Follower.fetch_all(env.params)
134
+ else
135
+ followers = Model::Follower.fetch_with_permissions(env.params, env.current_auth)
136
+ end
137
+ env.response = followers if followers
138
+ env
139
+ end
140
+ end
141
+
142
+ class Update < Middleware
143
+ def action(env)
144
+ env.response = Model::Follower.update_follower(env.params[:follower_id], env.params[:data], env.authorized_scopes)
145
+ env
146
+ end
147
+ end
148
+
149
+ class Destroy < Middleware
150
+ def action(env)
151
+ if (follower = Model::Follower.first(:id => env.params[:follower_id])) && follower.destroy
152
+ env.notify_action = 'delete'
153
+ env.notify_instance = follower
154
+ env.response = ''
155
+ end
156
+ env
157
+ end
158
+ end
159
+
160
+ class Notify < Middleware
161
+ def action(env)
162
+ return env unless follower = env.notify_instance
163
+ post = Model::Post.create(
164
+ :type => 'https://tent.io/types/post/follower/v0.1.0',
165
+ :entity => env['tent.entity'],
166
+ :content => {
167
+ :id => follower.public_id,
168
+ :entity => follower.entity,
169
+ :action => env.notify_action
170
+ }
171
+ )
172
+ Notifications.trigger(:type => post.type.uri, :post_id => post.id)
173
+ env
174
+ end
175
+ end
176
+
177
+ post '/followers' do |b|
178
+ b.use Discover
179
+ b.use Confirm
180
+ b.use Create
181
+ b.use Import
182
+ b.use Notify
183
+ end
184
+
185
+ get '/followers/count' do |b|
186
+ b.use AuthorizeReadMany
187
+ b.use GetActualId
188
+ b.use GetCount
189
+ b.use GetMany
190
+ end
191
+
192
+ get '/followers/:follower_id' do |b|
193
+ b.use GetActualId
194
+ b.use AuthorizeReadOne
195
+ b.use GetOne
196
+ end
197
+
198
+ get '/followers' do |b|
199
+ b.use AuthorizeReadMany
200
+ b.use GetActualId
201
+ b.use GetMany
202
+ end
203
+
204
+ put '/followers/:follower_id' do |b|
205
+ b.use GetActualId
206
+ b.use AuthorizeWriteOne
207
+ b.use Update
208
+ end
209
+
210
+ delete '/followers/:follower_id' do |b|
211
+ b.use GetActualId
212
+ b.use AuthorizeWriteOne
213
+ b.use Destroy
214
+ b.use Notify
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,241 @@
1
+ module TentD
2
+ class API
3
+ class Followings
4
+ include Router
5
+
6
+ class GetActualId < Middleware
7
+ def action(env)
8
+ id_mapping = [:following_id, :since_id, :before_id].select { |key| env.params.has_key?(key) }.inject({}) { |memo, key|
9
+ memo[env.params[key]] = key
10
+ env.params[key] = nil
11
+ memo
12
+ }
13
+ followings = Model::Following.all(:public_id => id_mapping.keys, :fields => [:id, :public_id])
14
+ followings.each do |following|
15
+ key = id_mapping[following.public_id]
16
+ env.params[key] = following.id
17
+ end
18
+ env
19
+ end
20
+ end
21
+
22
+ class AuthorizeWrite < Middleware
23
+ def action(env)
24
+ authorize_env!(env, :write_followings)
25
+ env
26
+ end
27
+ end
28
+
29
+ class GetOne < Middleware
30
+ def action(env)
31
+ if authorize_env?(env, :read_followings)
32
+ if following = Model::Following.first(:id => env.params.following_id, :confirmed => true)
33
+ env.following = following
34
+ env.response = following
35
+ end
36
+ else
37
+ following = Model::Following.find_with_permissions(env.params.following_id, env.current_auth) { |p,q,b| q << 'AND followings.confirmed = true' }
38
+ if following
39
+ env.response = following
40
+ else
41
+ raise Unauthorized
42
+ end
43
+ end
44
+ env
45
+ end
46
+ end
47
+
48
+ class GetCount < Middleware
49
+ def action(env)
50
+ env.params.return_count = true
51
+ env
52
+ end
53
+ end
54
+
55
+ class GetMany < Middleware
56
+ def action(env)
57
+ if authorize_env?(env, :read_followings)
58
+ env.response = Model::Following.fetch_all(env.params) { |p,q,b| q << 'followings.confirmed = true' }
59
+ else
60
+ env.response = Model::Following.fetch_with_permissions(env.params, env.current_auth) { |p,q,b| q << 'AND followings.confirmed = true' }
61
+ end
62
+ env
63
+ end
64
+ end
65
+
66
+ class Discover < Middleware
67
+ def action(env)
68
+ client = ::TentClient.new(nil, :faraday_adapter => TentD.faraday_adapter)
69
+ profile, profile_url = client.discover(env.params.data.entity).get_profile
70
+ return [404, {}, ['Not Found']] unless profile
71
+
72
+ profile = CoreProfileData.new(profile)
73
+ return [409, {}, ['Entity Mismatch']] unless profile.entity?(env.params.data.entity)
74
+ env.profile = profile
75
+ env.server_url = profile_url.sub(%r{/profile$}, '')
76
+ env
77
+ end
78
+ end
79
+
80
+ class Follow < Middleware
81
+ def action(env)
82
+ env.following = Model::Following.create(:entity => env.params.data.entity,
83
+ :groups => env.params.data.groups.to_a.map { |g| g['id'] },
84
+ :confirmed => false)
85
+ client = ::TentClient.new(env.server_url, :faraday_adapter => TentD.faraday_adapter)
86
+ res = client.follower.create(
87
+ :entity => env['tent.entity'],
88
+ :licenses => Model::ProfileInfo.tent_info.content['licenses'],
89
+ :notification_path => "notifications/#{env.following.public_id}"
90
+ )
91
+ case res.status
92
+ when 200...300
93
+ env.follow_data = res.body
94
+ else
95
+ return [res.status, res.headers, res.body]
96
+ end
97
+ env
98
+ end
99
+ end
100
+
101
+ class Create < Middleware
102
+ def action(env)
103
+ if env.following.confirm_from_params(env.follow_data.merge(env.params.data).merge(:profile => env.profile))
104
+ env.response = env.following
105
+ env.notify_action = 'create'
106
+ env.notify_instance = env.following
107
+ end
108
+ env
109
+ end
110
+ end
111
+
112
+ class Update < Middleware
113
+ def action(env)
114
+ if following = Model::Following.first(:id => env.params.following_id)
115
+ following.update_from_params(env.params.data, env.authorized_scopes)
116
+ env.response = following
117
+ end
118
+ env
119
+ end
120
+ end
121
+
122
+ class Destroy < Middleware
123
+ def action(env)
124
+ if (following = Model::Following.first(:id => env.params.following_id))
125
+ client = ::TentClient.new(following.core_profile.servers, following.auth_details.merge(:faraday_adapter => TentD.faraday_adapter))
126
+ res = client.follower.delete(following.remote_id)
127
+ following.destroy
128
+ if (200...300).to_a.include?(res.status)
129
+ env.response = ''
130
+ env.notify_action = 'delete'
131
+ env.notify_instance = following
132
+ end
133
+ end
134
+ env
135
+ end
136
+ end
137
+
138
+ class ProxyRequest < Middleware
139
+ def action(env)
140
+ following = env.following
141
+ client = TentClient.new(following.core_profile.servers.first,
142
+ following.auth_details.merge(:skip_serialization => true,
143
+ :faraday_adapter => TentD.faraday_adapter))
144
+ env.params.delete(:following_id)
145
+ path = env.params.delete(:proxy_path)
146
+ res = client.http.get(path, env.params, whitelisted_headers(env))
147
+ [res.status, res.headers, [res.body]]
148
+ end
149
+
150
+ def whitelisted_headers(env)
151
+ %w(Accept If-Modified-Since).inject({}) do |h,k|
152
+ h[k] = env['HTTP_' + k.gsub('-', '_').upcase]; h
153
+ end
154
+ end
155
+ end
156
+
157
+ class RewriteProxyCaptureParams < Middleware
158
+ def action(env)
159
+ matches = env.params.delete(:captures)
160
+ env.params.following_id = matches.first
161
+ env.params.proxy_path = '/' + matches.last
162
+ env
163
+ end
164
+ end
165
+
166
+ class Notify < Middleware
167
+ def action(env)
168
+ return env unless following = env.notify_instance
169
+ post = Model::Post.create(
170
+ :type => 'https://tent.io/types/post/following/v0.1.0',
171
+ :entity => env['tent.entity'],
172
+ :content => {
173
+ :id => following.public_id,
174
+ :entity => following.entity,
175
+ :action => env.notify_action
176
+ }
177
+ )
178
+ Notifications.trigger(:type => post.type.uri, :post_id => post.id)
179
+ env
180
+ end
181
+ end
182
+
183
+ class RedirectToFollowUI < Middleware
184
+ def action(env)
185
+ if follow_url = Model::AppAuthorization.follow_url(env.params.entity)
186
+ return [302, { "Location" => follow_url }, []]
187
+ end
188
+ env
189
+ end
190
+ end
191
+
192
+ get '/follow' do |b|
193
+ b.use RedirectToFollowUI
194
+ end
195
+
196
+ get '/followings/count' do |b|
197
+ b.use GetActualId
198
+ b.use GetCount
199
+ b.use GetMany
200
+ end
201
+
202
+ get '/followings/:following_id' do |b|
203
+ b.use GetActualId
204
+ b.use GetOne
205
+ end
206
+
207
+ get '/followings' do |b|
208
+ b.use GetActualId
209
+ b.use GetMany
210
+ end
211
+
212
+ get %r{/followings/(\w+)/(.+)} do |b|
213
+ b.use RewriteProxyCaptureParams
214
+ b.use GetActualId
215
+ b.use GetOne
216
+ b.use ProxyRequest
217
+ end
218
+
219
+ post '/followings' do |b|
220
+ b.use AuthorizeWrite
221
+ b.use Discover
222
+ b.use Follow
223
+ b.use Create
224
+ b.use Notify
225
+ end
226
+
227
+ put '/followings/:following_id' do |b|
228
+ b.use AuthorizeWrite
229
+ b.use GetActualId
230
+ b.use Update
231
+ end
232
+
233
+ delete '/followings/:following_id' do |b|
234
+ b.use AuthorizeWrite
235
+ b.use GetActualId
236
+ b.use Destroy
237
+ b.use Notify
238
+ end
239
+ end
240
+ end
241
+ end