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