Fix #927 - Clean up federation code after Elrond's review
[mediagoblin.git] / mediagoblin / federation / views.py
CommitLineData
0679545f
JT
1# GNU MediaGoblin -- federated, autonomous media hosting
2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU Affero General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
c894b424 17import json
d4a21d7e 18import io
41599bf2 19import mimetypes
c894b424 20
c64fc16b 21from werkzeug.datastructures import FileStorage
d4a21d7e 22
23from mediagoblin.media_types import sniff_media
d7b3805f 24from mediagoblin.decorators import oauth_required
967df5ef 25from mediagoblin.federation.decorators import user_has_privilege
c894b424 26from mediagoblin.db.models import User, MediaEntry, MediaComment
5e5d4458 27from mediagoblin.tools.response import redirect, json_response, json_error
c894b424 28from mediagoblin.meddleware.csrf import csrf_exempt
5e5d4458
JT
29from mediagoblin.submit.lib import new_upload_entry, api_upload_request, \
30 api_add_to_feed
31
32# MediaTypes
33from mediagoblin.media_types.image import MEDIA_TYPE as IMAGE_MEDIA_TYPE
d7b3805f 34
247a3b78 35@oauth_required
a5682e89 36def profile(request, raw=False):
37 """ This is /api/user/<username>/profile - This will give profile info """
d7b3805f
JT
38 user = request.matchdict["username"]
39 requested_user = User.query.filter_by(username=user)
c64fc16b 40
d7b3805f 41 if requested_user is None:
5e5d4458 42 return json_error("No such 'user' with id '{0}'".format(user), 404)
d7b3805f
JT
43
44 user = requested_user[0]
45
a5682e89 46 if raw:
47 return (user, user.serialize(request))
48
d7b3805f
JT
49 # user profiles are public so return information
50 return json_response(user.serialize(request))
51
247a3b78 52@oauth_required
a5682e89 53def user(request):
54 """ This is /api/user/<username> - This will get the user """
55 user, user_profile = profile(request, raw=True)
56 data = {
57 "nickname": user.username,
58 "updated": user.created.isoformat(),
59 "published": user.created.isoformat(),
d8f55f2b
JT
60 "profile": user_profile,
61 }
a5682e89 62
63 return json_response(data)
64
247a3b78 65@oauth_required
d4a21d7e 66@csrf_exempt
967df5ef 67@user_has_privilege(u'uploader')
d4a21d7e 68def uploads(request):
c64fc16b 69 """ Endpoint for file uploads """
d4a21d7e 70 user = request.matchdict["username"]
71 requested_user = User.query.filter_by(username=user)
72
73 if requested_user is None:
5e5d4458 74 return json_error("No such 'user' with id '{0}'".format(user), 404)
d4a21d7e 75
76 request.user = requested_user[0]
77 if request.method == "POST":
78 # Wrap the data in the werkzeug file wrapper
a14d90c2 79 if "Content-Type" not in request.headers:
5e5d4458
JT
80 return json_error(
81 "Must supply 'Content-Type' header to upload media.")
a14d90c2 82 mimetype = request.headers["Content-Type"]
41599bf2
JT
83 filename = mimetypes.guess_all_extensions(mimetype)
84 filename = 'unknown' + filename[0] if filename else filename
d4a21d7e 85 file_data = FileStorage(
7810817c 86 stream=io.BytesIO(request.data),
41599bf2 87 filename=filename,
6781ff3c 88 content_type=mimetype
7810817c 89 )
90
91 # Find media manager
41599bf2 92 media_type, media_manager = sniff_media(file_data, filename)
d4a21d7e 93 entry = new_upload_entry(request.user)
5e5d4458
JT
94 entry.media_type = IMAGE_MEDIA_TYPE
95 return api_upload_request(request, file_data, entry)
d4a21d7e 96
5e5d4458 97 return json_error("Not yet implemented", 501)
d4a21d7e 98
247a3b78 99@oauth_required
c894b424 100@csrf_exempt
d7b3805f
JT
101def feed(request):
102 """ Handles the user's outbox - /api/user/<username>/feed """
103 user = request.matchdict["username"]
104 requested_user = User.query.filter_by(username=user)
105
106 # check if the user exists
107 if requested_user is None:
5e5d4458 108 return json_error("No such 'user' with id '{0}'".format(user), 404)
d7b3805f 109
3c3fa5e7 110 request.user = requested_user[0]
6781ff3c 111 if request.data:
c894b424 112 data = json.loads(request.data)
6781ff3c
JT
113 else:
114 data = {"verb": None, "object": {}}
115
116 if request.method == "POST" and data["verb"] == "post":
c894b424
JT
117 obj = data.get("object", None)
118 if obj is None:
5e5d4458 119 return json_error("Could not find 'object' element.")
c64fc16b 120
c894b424
JT
121 if obj.get("objectType", None) == "comment":
122 # post a comment
5e5d4458
JT
123 if not request.user.has_privilege(u'commenter'):
124 return json_error(
125 "Privilege 'commenter' required to comment.",
126 status=403
127 )
128
d8f55f2b
JT
129 comment = MediaComment(author=request.user.id)
130 comment.unserialize(data["object"])
c894b424 131 comment.save()
3c3fa5e7 132 data = {"verb": "post", "object": comment.serialize(request)}
133 return json_response(data)
7810817c 134
62dc7d3e 135 elif obj.get("objectType", None) == "image":
136 # Posting an image to the feed
62dc7d3e 137 media_id = int(data["object"]["id"])
138 media = MediaEntry.query.filter_by(id=media_id)
139 if media is None:
5e5d4458
JT
140 return json_response(
141 "No such 'image' with id '{0}'".format(id=media_id),
142 status=404
143 )
51ab5192
JT
144
145 media = media.first()
d8f55f2b 146 if not media.unserialize(data["object"]):
5e5d4458
JT
147 return json_error(
148 "Invalid 'image' with id '{0}'".format(media_id)
149 )
150
51ab5192 151 media.save()
5e5d4458 152 api_add_to_feed(request, media)
51ab5192 153
c3b89feb
JT
154 return json_response({
155 "verb": "post",
156 "object": media.serialize(request)
157 })
62dc7d3e 158
c894b424 159 elif obj.get("objectType", None) is None:
62dc7d3e 160 # They need to tell us what type of object they're giving us.
5e5d4458 161 return json_error("No objectType specified.")
c894b424 162 else:
62dc7d3e 163 # Oh no! We don't know about this type of object (yet)
5e5d4458
JT
164 object_type = obj.get("objectType", None)
165 return json_error("Unknown object type '{0}'.".format(object_type))
c894b424 166
6781ff3c
JT
167 elif request.method in ["PUT", "POST"] and data["verb"] == "update":
168 # Check we've got a valid object
169 obj = data.get("object", None)
170
171 if obj is None:
5e5d4458 172 return json_error("Could not find 'object' element.")
6781ff3c
JT
173
174 if "objectType" not in obj:
5e5d4458 175 return json_error("No objectType specified.")
6781ff3c
JT
176
177 if "id" not in obj:
5e5d4458 178 return json_error("Object ID has not been specified.")
6781ff3c
JT
179
180 obj_id = obj["id"]
181
182 # Now try and find object
183 if obj["objectType"] == "comment":
5e5d4458
JT
184 if not request.user.has_privilege(u'commenter'):
185 return json_error(
186 "Privilege 'commenter' required to comment.",
187 status=403
188 )
189
6781ff3c
JT
190 comment = MediaComment.query.filter_by(id=obj_id)
191 if comment is None:
5e5d4458
JT
192 return json_error(
193 "No such 'comment' with id '{0}'.".format(obj_id)
194 )
6781ff3c 195
d8f55f2b
JT
196 comment = comment[0]
197 if not comment.unserialize(data["object"]):
5e5d4458
JT
198 return json_error(
199 "Invalid 'comment' with id '{0}'".format(obj_id)
200 )
6781ff3c
JT
201
202 comment.save()
d8f55f2b 203
6781ff3c
JT
204 activity = {
205 "verb": "update",
206 "object": comment.serialize(request),
207 }
208 return json_response(activity)
209
210 elif obj["objectType"] == "image":
211 image = MediaEntry.query.filter_by(id=obj_id)
212 if image is None:
5e5d4458
JT
213 return json_error(
214 "No such 'image' with the id '{0}'.".format(obj_id)
215 )
6781ff3c
JT
216
217 image = image[0]
d8f55f2b 218 if not image.unserialize(obj):
5e5d4458
JT
219 return json_error(
220 "Invalid 'image' with id '{0}'".format(obj_id)
221 )
6781ff3c 222 image.save()
d8f55f2b 223
6781ff3c
JT
224 activity = {
225 "verb": "update",
226 "object": image.serialize(request),
227 }
228 return json_response(activity)
7810817c 229
51ab5192 230 elif request.method != "GET":
5e5d4458
JT
231 return json_error(
232 "Unsupported HTTP method {0}".format(request.method),
233 status=501
234 )
51ab5192 235
c894b424 236 feed_url = request.urlgen(
6781ff3c
JT
237 "mediagoblin.federation.feed",
238 username=request.user.username,
239 qualified=True
240 )
c894b424
JT
241
242 feed = {
c64fc16b 243 "displayName": "Activities by {user}@{host}".format(
244 user=request.user.username,
245 host=request.host
246 ),
c894b424
JT
247 "objectTypes": ["activity"],
248 "url": feed_url,
249 "links": {
250 "first": {
251 "href": feed_url,
252 },
253 "self": {
254 "href": request.url,
255 },
256 "prev": {
257 "href": feed_url,
258 },
259 "next": {
260 "href": feed_url,
261 }
262 },
3c3fa5e7 263 "author": request.user.serialize(request),
c894b424
JT
264 "items": [],
265 }
c64fc16b 266
d7b3805f 267
5e5d4458
JT
268 # Look up all the media to put in the feed (this will be changed
269 # when we get real feeds/inboxes/outboxes/activites)
c894b424 270 for media in MediaEntry.query.all():
a14d90c2 271 item = {
c894b424
JT
272 "verb": "post",
273 "object": media.serialize(request),
3c3fa5e7 274 "actor": request.user.serialize(request),
275 "content": "{0} posted a picture".format(request.user.username),
c894b424 276 "id": 1,
a14d90c2
JT
277 }
278 item["updated"] = item["object"]["updated"]
279 item["published"] = item["object"]["published"]
280 item["url"] = item["object"]["url"]
281 feed["items"].append(item)
c894b424
JT
282 feed["totalItems"] = len(feed["items"])
283
284 return json_response(feed)
d7b3805f
JT
285
286@oauth_required
98596dd0 287def object(request, raw_obj=False):
5a2056f7 288 """ Lookup for a object type """
6781ff3c 289 object_type = request.matchdict["objectType"]
a14d90c2
JT
290 try:
291 object_id = int(request.matchdict["id"])
292 except ValueError:
293 error = "Invalid object ID '{0}' for '{1}'".format(
294 request.matchdict["id"],
295 object_type
296 )
5e5d4458 297 return json_error(error)
a14d90c2 298
6781ff3c 299 if object_type not in ["image"]:
c64fc16b 300 # not sure why this is 404, maybe ask evan. Maybe 400?
5e5d4458 301 return json_error("Unknown type: {0}".format(object_type), status=404)
5a2056f7 302
a14d90c2 303 media = MediaEntry.query.filter_by(id=object_id).first()
5a2056f7 304 if media is None:
a14d90c2
JT
305 error = "Can't find '{0}' with ID '{1}'".format(
306 object_type,
307 object_id
308 )
5e5d4458
JT
309 return json_error(
310 "Can't find '{0}' with ID '{1}'".format(object_type, object_id),
311 status=404
312 )
5a2056f7 313
98596dd0 314 if raw_obj:
315 return media
316
bdde87a4 317 return json_response(media.serialize(request))
98596dd0 318
247a3b78 319@oauth_required
98596dd0 320def object_comments(request):
321 """ Looks up for the comments on a object """
322 media = object(request, raw_obj=True)
323 response = media
324 if isinstance(response, MediaEntry):
325 comments = response.serialize(request)
326 comments = comments.get("replies", {
6781ff3c
JT
327 "totalItems": 0,
328 "items": [],
329 "url": request.urlgen(
330 "mediagoblin.federation.object.comments",
331 objectType=media.objectType,
a14d90c2 332 uuid=media.id,
6781ff3c
JT
333 qualified=True
334 )
335 })
336
c894b424
JT
337 comments["displayName"] = "Replies to {0}".format(comments["url"])
338 comments["links"] = {
339 "first": comments["url"],
340 "self": comments["url"],
341 }
98596dd0 342 response = json_response(comments)
343
344 return response
a5682e89 345
a5682e89 346##
347# Well known
348##
349def host_meta(request):
a14d90c2 350 """ /.well-known/host-meta - provide URLs to resources """
a5682e89 351 links = []
c64fc16b 352
a5682e89 353 links.append({
354 "ref": "registration_endpoint",
a14d90c2
JT
355 "href": request.urlgen(
356 "mediagoblin.oauth.client_register",
357 qualified=True
358 ),
0679545f 359 })
a5682e89 360 links.append({
361 "ref": "http://apinamespace.org/oauth/request_token",
a14d90c2
JT
362 "href": request.urlgen(
363 "mediagoblin.oauth.request_token",
364 qualified=True
365 ),
0679545f 366 })
a5682e89 367 links.append({
368 "ref": "http://apinamespace.org/oauth/authorize",
a14d90c2
JT
369 "href": request.urlgen(
370 "mediagoblin.oauth.authorize",
371 qualified=True
372 ),
0679545f 373 })
a5682e89 374 links.append({
375 "ref": "http://apinamespace.org/oauth/access_token",
a14d90c2
JT
376 "href": request.urlgen(
377 "mediagoblin.oauth.access_token",
378 qualified=True
379 ),
0679545f 380 })
a5682e89 381
18297655 382 return json_response({"links": links})
a5682e89 383
384def whoami(request):
a14d90c2 385 """ /api/whoami - HTTP redirect to API profile """
5e5d4458
JT
386 if request.user is None:
387 return json_error("Not logged in.", status=401)
388
a5682e89 389 profile = request.urlgen(
390 "mediagoblin.federation.user.profile",
391 username=request.user.username,
392 qualified=True
0679545f 393 )
a5682e89 394
395 return redirect(request, location=profile)