Adds the unit-tests for API and cleans up API
[mediagoblin.git] / mediagoblin / federation / views.py
1 import json
2 import io
3
4 from werkzeug.datastructures import FileStorage
5
6 from mediagoblin.media_types import sniff_media
7 from mediagoblin.decorators import oauth_required
8 from mediagoblin.db.models import User, MediaEntry, MediaComment
9 from mediagoblin.tools.response import redirect, json_response
10 from mediagoblin.meddleware.csrf import csrf_exempt
11 from mediagoblin.submit.lib import new_upload_entry
12
13 @oauth_required
14 def profile(request, raw=False):
15 """ This is /api/user/<username>/profile - This will give profile info """
16 user = request.matchdict["username"]
17 requested_user = User.query.filter_by(username=user)
18
19 # check if the user exists
20 if requested_user is None:
21 error = "No such 'user' with id '{0}'".format(user)
22 return json_response({"error": error}, status=404)
23
24 user = requested_user[0]
25
26 if raw:
27 return (user, user.serialize(request))
28
29 # user profiles are public so return information
30 return json_response(user.serialize(request))
31
32 @oauth_required
33 def user(request):
34 """ This is /api/user/<username> - This will get the user """
35 user, user_profile = profile(request, raw=True)
36 data = {
37 "nickname": user.username,
38 "updated": user.created.isoformat(),
39 "published": user.created.isoformat(),
40 "profile": user_profile
41 }
42
43 return json_response(data)
44
45 @oauth_required
46 @csrf_exempt
47 def uploads(request):
48 """ Endpoint for file uploads """
49 user = request.matchdict["username"]
50 requested_user = User.query.filter_by(username=user)
51
52 if requested_user is None:
53 error = "No such 'user' with id '{0}'".format(user)
54 return json_response({"error": error}, status=404)
55
56 request.user = requested_user[0]
57 if request.method == "POST":
58 # Wrap the data in the werkzeug file wrapper
59 file_data = FileStorage(
60 stream=io.BytesIO(request.data),
61 filename=request.args.get("qqfile", "unknown"),
62 content_type=request.headers.get("Content-Type", "application/octal-stream")
63 )
64
65 # Find media manager
66 media_type, media_manager = sniff_media(file_data)
67 entry = new_upload_entry(request.user)
68 if hasattr(media_manager, "api_upload_request"):
69 return media_manager.api_upload_request(request, file_data, entry)
70 else:
71 return json_response({"error": "Not yet implemented"}, status=400)
72
73 return json_response({"error": "Not yet implemented"}, status=400)
74
75 @oauth_required
76 @csrf_exempt
77 def feed(request):
78 """ Handles the user's outbox - /api/user/<username>/feed """
79 user = request.matchdict["username"]
80 requested_user = User.query.filter_by(username=user)
81
82 # check if the user exists
83 if requested_user is None:
84 error = "No such 'user' with id '{0}'".format(user)
85 return json_response({"error": error}, status=404)
86
87 request.user = requested_user[0]
88
89 if request.method == "POST":
90 data = json.loads(request.data)
91 obj = data.get("object", None)
92 if obj is None:
93 error = {"error": "Could not find 'object' element."}
94 return json_response(error, status=400)
95
96 if obj.get("objectType", None) == "comment":
97 # post a comment
98 media = int(data["object"]["inReplyTo"]["id"])
99 comment = MediaComment(
100 media_entry=media,
101 author=request.user.id,
102 content=data["object"]["content"]
103 )
104 comment.save()
105 data = {"verb": "post", "object": comment.serialize(request)}
106 return json_response(data)
107
108 elif obj.get("objectType", None) == "image":
109 # Posting an image to the feed
110 # NB: This is currently just handing the image back until we have an
111 # to send the image to the actual feed
112
113 media_id = int(data["object"]["id"])
114 media = MediaEntry.query.filter_by(id=media_id)
115 if media is None:
116 error = "No such 'image' with id '{0}'".format(id=media_id)
117 return json_response(error, status=404)
118 media = media[0]
119 return json_response(media.serialize(request))
120
121 elif obj.get("objectType", None) is None:
122 # They need to tell us what type of object they're giving us.
123 error = {"error": "No objectType specified."}
124 return json_response(error, status=400)
125 else:
126 # Oh no! We don't know about this type of object (yet)
127 error_message = "Unknown object type '{0}'.".format(
128 obj.get("objectType", None)
129 )
130
131 error = {"error": error_message}
132 return json_response(error, status=400)
133
134
135 feed_url = request.urlgen(
136 "mediagoblin.federation.feed",
137 username=request.user.username,
138 qualified=True
139 )
140
141 feed = {
142 "displayName": "Activities by {user}@{host}".format(
143 user=request.user.username,
144 host=request.host
145 ),
146 "objectTypes": ["activity"],
147 "url": feed_url,
148 "links": {
149 "first": {
150 "href": feed_url,
151 },
152 "self": {
153 "href": request.url,
154 },
155 "prev": {
156 "href": feed_url,
157 },
158 "next": {
159 "href": feed_url,
160 }
161 },
162 "author": request.user.serialize(request),
163 "items": [],
164 }
165
166
167 # Now lookup the user's feed.
168 for media in MediaEntry.query.all():
169 feed["items"].append({
170 "verb": "post",
171 "object": media.serialize(request),
172 "actor": request.user.serialize(request),
173 "content": "{0} posted a picture".format(request.user.username),
174 "id": 1,
175 })
176 feed["items"][-1]["updated"] = feed["items"][-1]["object"]["updated"]
177 feed["items"][-1]["published"] = feed["items"][-1]["object"]["published"]
178 feed["items"][-1]["url"] = feed["items"][-1]["object"]["url"]
179 feed["totalItems"] = len(feed["items"])
180
181 return json_response(feed)
182
183 @oauth_required
184 def object(request, raw_obj=False):
185 """ Lookup for a object type """
186 objectType = request.matchdict["objectType"]
187 uuid = request.matchdict["uuid"]
188 if objectType not in ["image"]:
189 error = "Unknown type: {0}".format(objectType)
190 # not sure why this is 404, maybe ask evan. Maybe 400?
191 return json_response({"error": error}, status=404)
192
193 media = MediaEntry.query.filter_by(slug=uuid).first()
194 if media is None:
195 # no media found with that uuid
196 error = "Can't find a {0} with ID = {1}".format(objectType, uuid)
197 return json_response({"error": error}, status=404)
198
199 if raw_obj:
200 return media
201
202 return json_response(media.serialize(request))
203
204 @oauth_required
205 def object_comments(request):
206 """ Looks up for the comments on a object """
207 media = object(request, raw_obj=True)
208 response = media
209 if isinstance(response, MediaEntry):
210 comments = response.serialize(request)
211 comments = comments.get("replies", {
212 "totalItems": 0,
213 "items": [],
214 "url": request.urlgen(
215 "mediagoblin.federation.object.comments",
216 objectType=media.objectType,
217 uuid=media.slug,
218 qualified=True)
219 })
220 comments["displayName"] = "Replies to {0}".format(comments["url"])
221 comments["links"] = {
222 "first": comments["url"],
223 "self": comments["url"],
224 }
225 response = json_response(comments)
226
227 return response
228
229
230 ##
231 # Well known
232 ##
233 def host_meta(request):
234 """ This is /.well-known/host-meta - provides URL's to resources on server """
235 links = []
236
237 # Client registration links
238 links.append({
239 "ref": "registration_endpoint",
240 "href": request.urlgen("mediagoblin.oauth.client_register", qualified=True),
241 })
242 links.append({
243 "ref": "http://apinamespace.org/oauth/request_token",
244 "href": request.urlgen("mediagoblin.oauth.request_token", qualified=True),
245 })
246 links.append({
247 "ref": "http://apinamespace.org/oauth/authorize",
248 "href": request.urlgen("mediagoblin.oauth.authorize", qualified=True),
249 })
250 links.append({
251 "ref": "http://apinamespace.org/oauth/access_token",
252 "href": request.urlgen("mediagoblin.oauth.access_token", qualified=True),
253 })
254
255 return json_response({"links": links})
256
257 def whoami(request):
258 """ This is /api/whoami - This is a HTTP redirect to api profile """
259 profile = request.urlgen(
260 "mediagoblin.federation.user.profile",
261 username=request.user.username,
262 qualified=True
263 )
264
265 return redirect(request, location=profile)