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