Commit | Line | Data |
---|---|---|
f2698759 | 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
0679545f JT |
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 | 17 | import json |
d4a21d7e | 18 | import io |
41599bf2 | 19 | import mimetypes |
c894b424 | 20 | |
c64fc16b | 21 | from werkzeug.datastructures import FileStorage |
d4a21d7e | 22 | |
b9492011 | 23 | from mediagoblin.decorators import oauth_required, require_active_login |
4fd52036 | 24 | from mediagoblin.api.decorators import user_has_privilege |
64a456a4 | 25 | from mediagoblin.db.models import User, LocalUser, MediaEntry, Comment, TextComment, Activity |
5436d980 | 26 | from mediagoblin.tools.federation import create_activity, create_generator |
9c602458 | 27 | from mediagoblin.tools.routing import extract_url_arguments |
0af1b859 | 28 | from mediagoblin.tools.response import redirect, json_response, json_error, \ |
b9492011 | 29 | render_404, render_to_response |
c894b424 | 30 | from mediagoblin.meddleware.csrf import csrf_exempt |
5e5d4458 JT |
31 | from mediagoblin.submit.lib import new_upload_entry, api_upload_request, \ |
32 | api_add_to_feed | |
33 | ||
34 | # MediaTypes | |
35 | from mediagoblin.media_types.image import MEDIA_TYPE as IMAGE_MEDIA_TYPE | |
d7b3805f | 36 | |
9246a6ba JT |
37 | # Getters |
38 | def get_profile(request): | |
39 | """ | |
40 | Gets the user's profile for the endpoint requested. | |
41 | ||
42 | For example an endpoint which is /api/{username}/feed | |
43 | as /api/cwebber/feed would get cwebber's profile. This | |
44 | will return a tuple (username, user_profile). If no user | |
45 | can be found then this function returns a (None, None). | |
46 | """ | |
47 | username = request.matchdict["username"] | |
b4997540 | 48 | user = LocalUser.query.filter(LocalUser.username==username).first() |
9246a6ba JT |
49 | |
50 | if user is None: | |
51 | return None, None | |
52 | ||
53 | return user, user.serialize(request) | |
54 | ||
55 | ||
56 | # Endpoints | |
247a3b78 | 57 | @oauth_required |
9246a6ba | 58 | def profile_endpoint(request): |
a5682e89 | 59 | """ This is /api/user/<username>/profile - This will give profile info """ |
9246a6ba | 60 | user, user_profile = get_profile(request) |
c64fc16b | 61 | |
8d75091d | 62 | if user is None: |
9246a6ba | 63 | username = request.matchdict["username"] |
8d75091d | 64 | return json_error( |
9246a6ba | 65 | "No such 'user' with username '{0}'".format(username), |
8d75091d JT |
66 | status=404 |
67 | ) | |
d7b3805f JT |
68 | |
69 | # user profiles are public so return information | |
9246a6ba | 70 | return json_response(user_profile) |
d7b3805f | 71 | |
247a3b78 | 72 | @oauth_required |
9246a6ba | 73 | def user_endpoint(request): |
a5682e89 | 74 | """ This is /api/user/<username> - This will get the user """ |
9246a6ba | 75 | user, user_profile = get_profile(request) |
0af1b859 | 76 | |
9246a6ba JT |
77 | if user is None: |
78 | username = request.matchdict["username"] | |
79 | return json_error( | |
80 | "No such 'user' with username '{0}'".format(username), | |
81 | status=404 | |
82 | ) | |
0af1b859 | 83 | |
9246a6ba | 84 | return json_response({ |
a5682e89 | 85 | "nickname": user.username, |
86 | "updated": user.created.isoformat(), | |
87 | "published": user.created.isoformat(), | |
d8f55f2b | 88 | "profile": user_profile, |
9246a6ba | 89 | }) |
a5682e89 | 90 | |
247a3b78 | 91 | @oauth_required |
d4a21d7e | 92 | @csrf_exempt |
967df5ef | 93 | @user_has_privilege(u'uploader') |
9246a6ba | 94 | def uploads_endpoint(request): |
c64fc16b | 95 | """ Endpoint for file uploads """ |
9246a6ba | 96 | username = request.matchdict["username"] |
b4997540 | 97 | requested_user = LocalUser.query.filter(LocalUser.username==username).first() |
d4a21d7e | 98 | |
99 | if requested_user is None: | |
9246a6ba | 100 | return json_error("No such 'user' with id '{0}'".format(username), 404) |
d4a21d7e | 101 | |
d4a21d7e | 102 | if request.method == "POST": |
8917ffb1 JT |
103 | # Ensure that the user is only able to upload to their own |
104 | # upload endpoint. | |
105 | if requested_user.id != request.user.id: | |
106 | return json_error( | |
107 | "Not able to post to another users feed.", | |
108 | status=403 | |
109 | ) | |
8d75091d | 110 | |
d4a21d7e | 111 | # Wrap the data in the werkzeug file wrapper |
a14d90c2 | 112 | if "Content-Type" not in request.headers: |
5e5d4458 | 113 | return json_error( |
9246a6ba JT |
114 | "Must supply 'Content-Type' header to upload media." |
115 | ) | |
116 | ||
a14d90c2 | 117 | mimetype = request.headers["Content-Type"] |
f2b4760b RP |
118 | |
119 | if "X-File-Name" in request.headers: | |
120 | filename = request.headers["X-File-Name"] | |
121 | else: | |
122 | filename = mimetypes.guess_all_extensions(mimetype) | |
123 | filename = 'unknown' + filename[0] if filename else filename | |
124 | ||
d4a21d7e | 125 | file_data = FileStorage( |
7810817c | 126 | stream=io.BytesIO(request.data), |
41599bf2 | 127 | filename=filename, |
6781ff3c | 128 | content_type=mimetype |
7810817c | 129 | ) |
130 | ||
131 | # Find media manager | |
d4a21d7e | 132 | entry = new_upload_entry(request.user) |
5e5d4458 JT |
133 | entry.media_type = IMAGE_MEDIA_TYPE |
134 | return api_upload_request(request, file_data, entry) | |
d4a21d7e | 135 | |
5e5d4458 | 136 | return json_error("Not yet implemented", 501) |
d4a21d7e | 137 | |
f2698759 JT |
138 | @oauth_required |
139 | @csrf_exempt | |
140 | def inbox_endpoint(request, inbox=None): | |
141 | """ This is the user's inbox | |
142 | ||
143 | Currently because we don't have the ability to represent the inbox in the | |
144 | database this is not a "real" inbox in the pump.io/Activity streams 1.0 | |
145 | sense but instead just gives back all the data on the website | |
146 | ||
147 | inbox: allows you to pass a query in to limit inbox scope | |
148 | """ | |
149 | username = request.matchdict["username"] | |
b4997540 | 150 | user = LocalUser.query.filter(LocalUser.username==username).first() |
f2698759 JT |
151 | |
152 | if user is None: | |
153 | return json_error("No such 'user' with id '{0}'".format(username), 404) | |
154 | ||
155 | ||
156 | # Only the user who's authorized should be able to read their inbox | |
157 | if user.id != request.user.id: | |
158 | return json_error( | |
159 | "Only '{0}' can read this inbox.".format(user.username), | |
160 | 403 | |
161 | ) | |
162 | ||
163 | if inbox is None: | |
95dbed2d | 164 | inbox = Activity.query |
f2698759 | 165 | |
26633946 JT |
166 | # Count how many items for the "totalItems" field |
167 | total_items = inbox.count() | |
168 | ||
f2698759 JT |
169 | # We want to make a query for all media on the site and then apply GET |
170 | # limits where we can. | |
171 | inbox = inbox.order_by(Activity.published.desc()) | |
172 | ||
173 | # Limit by the "count" (default: 20) | |
26633946 JT |
174 | try: |
175 | limit = int(request.args.get("count", 20)) | |
176 | except ValueError: | |
177 | limit = 20 | |
178 | ||
179 | # Prevent the count being too big (pump uses 200 so we shall) | |
180 | limit = limit if limit <= 200 else 200 | |
181 | ||
182 | # Apply the limit | |
183 | inbox = inbox.limit(limit) | |
f2698759 JT |
184 | |
185 | # Offset (default: no offset - first <count> results) | |
186 | inbox = inbox.offset(request.args.get("offset", 0)) | |
187 | ||
188 | # build the inbox feed | |
189 | feed = { | |
190 | "displayName": "Activities for {0}".format(user.username), | |
191 | "author": user.serialize(request), | |
192 | "objectTypes": ["activity"], | |
193 | "url": request.base_url, | |
194 | "links": {"self": {"href": request.url}}, | |
195 | "items": [], | |
26633946 | 196 | "totalItems": total_items, |
f2698759 JT |
197 | } |
198 | ||
199 | for activity in inbox: | |
200 | try: | |
201 | feed["items"].append(activity.serialize(request)) | |
202 | except AttributeError: | |
203 | # As with the feed endpint this occurs because of how we our | |
204 | # hard-deletion method. Some activites might exist where the | |
205 | # Activity object and/or target no longer exist, for this case we | |
206 | # should just skip them. | |
207 | pass | |
208 | ||
f2698759 JT |
209 | return json_response(feed) |
210 | ||
211 | @oauth_required | |
212 | @csrf_exempt | |
213 | def inbox_minor_endpoint(request): | |
214 | """ Inbox subset for less important Activities """ | |
215 | inbox = Activity.query.filter( | |
216 | (Activity.verb == "update") | (Activity.verb == "delete") | |
217 | ) | |
218 | ||
219 | return inbox_endpoint(request=request, inbox=inbox) | |
220 | ||
221 | @oauth_required | |
222 | @csrf_exempt | |
223 | def inbox_major_endpoint(request): | |
224 | """ Inbox subset for most important Activities """ | |
225 | inbox = Activity.query.filter_by(verb="post") | |
226 | return inbox_endpoint(request=request, inbox=inbox) | |
227 | ||
247a3b78 | 228 | @oauth_required |
c894b424 | 229 | @csrf_exempt |
9a51bf1e | 230 | def feed_endpoint(request, outbox=None): |
d7b3805f | 231 | """ Handles the user's outbox - /api/user/<username>/feed """ |
9246a6ba | 232 | username = request.matchdict["username"] |
b4997540 | 233 | requested_user = LocalUser.query.filter(LocalUser.username==username).first() |
d7b3805f JT |
234 | |
235 | # check if the user exists | |
236 | if requested_user is None: | |
9246a6ba | 237 | return json_error("No such 'user' with id '{0}'".format(username), 404) |
d7b3805f | 238 | |
6781ff3c | 239 | if request.data: |
37865d02 | 240 | data = json.loads(request.data.decode()) |
6781ff3c JT |
241 | else: |
242 | data = {"verb": None, "object": {}} | |
243 | ||
c64fc16b | 244 | |
9246a6ba JT |
245 | if request.method in ["POST", "PUT"]: |
246 | # Validate that the activity is valid | |
247 | if "verb" not in data or "object" not in data: | |
248 | return json_error("Invalid activity provided.") | |
62dc7d3e | 249 | |
9246a6ba | 250 | # Check that the verb is valid |
4dec1cd6 | 251 | if data["verb"] not in ["post", "update", "delete"]: |
9246a6ba | 252 | return json_error("Verb not yet implemented", 501) |
5e5d4458 | 253 | |
9246a6ba JT |
254 | # We need to check that the user they're posting to is |
255 | # the person that they are. | |
256 | if requested_user.id != request.user.id: | |
257 | return json_error( | |
258 | "Not able to post to another users feed.", | |
259 | status=403 | |
260 | ) | |
8d75091d | 261 | |
9246a6ba JT |
262 | # Handle new posts |
263 | if data["verb"] == "post": | |
264 | obj = data.get("object", None) | |
265 | if obj is None: | |
266 | return json_error("Could not find 'object' element.") | |
267 | ||
268 | if obj.get("objectType", None) == "comment": | |
269 | # post a comment | |
270 | if not request.user.has_privilege(u'commenter'): | |
271 | return json_error( | |
272 | "Privilege 'commenter' required to comment.", | |
273 | status=403 | |
274 | ) | |
275 | ||
64a456a4 | 276 | comment = TextComment(actor=request.user.id) |
9c602458 | 277 | comment.unserialize(data["object"], request) |
9246a6ba | 278 | comment.save() |
35885226 | 279 | |
5436d980 JT |
280 | # Create activity for comment |
281 | generator = create_generator(request) | |
282 | activity = create_activity( | |
283 | verb="post", | |
284 | actor=request.user, | |
285 | obj=comment, | |
64a456a4 | 286 | target=comment.get_reply_to(), |
5436d980 JT |
287 | generator=generator |
288 | ) | |
289 | ||
35885226 | 290 | return json_response(activity.serialize(request)) |
9246a6ba JT |
291 | |
292 | elif obj.get("objectType", None) == "image": | |
293 | # Posting an image to the feed | |
64a456a4 | 294 | media_id = extract_url_arguments( |
9c602458 JT |
295 | url=data["object"]["id"], |
296 | urlmap=request.app.url_map | |
64a456a4 JT |
297 | )["id"] |
298 | ||
299 | # Build public_id | |
300 | public_id = request.urlgen( | |
301 | "mediagoblin.api.object", | |
302 | object_type=obj["objectType"], | |
303 | id=media_id, | |
304 | qualified=True | |
305 | ) | |
9c602458 | 306 | |
64a456a4 JT |
307 | media = MediaEntry.query.filter_by( |
308 | public_id=public_id | |
309 | ).first() | |
9246a6ba JT |
310 | |
311 | if media is None: | |
312 | return json_response( | |
313 | "No such 'image' with id '{0}'".format(media_id), | |
314 | status=404 | |
315 | ) | |
316 | ||
0f3bf8d4 | 317 | if media.actor != request.user.id: |
9246a6ba JT |
318 | return json_error( |
319 | "Privilege 'commenter' required to comment.", | |
320 | status=403 | |
321 | ) | |
322 | ||
323 | ||
324 | if not media.unserialize(data["object"]): | |
325 | return json_error( | |
326 | "Invalid 'image' with id '{0}'".format(media_id) | |
327 | ) | |
328 | ||
c0434db4 JT |
329 | |
330 | # Add location if one exists | |
331 | if "location" in data: | |
332 | Location.create(data["location"], self) | |
333 | ||
9246a6ba | 334 | media.save() |
35885226 | 335 | activity = api_add_to_feed(request, media) |
9246a6ba | 336 | |
35885226 | 337 | return json_response(activity.serialize(request)) |
9246a6ba JT |
338 | |
339 | elif obj.get("objectType", None) is None: | |
340 | # They need to tell us what type of object they're giving us. | |
341 | return json_error("No objectType specified.") | |
342 | else: | |
343 | # Oh no! We don't know about this type of object (yet) | |
344 | object_type = obj.get("objectType", None) | |
5e5d4458 | 345 | return json_error( |
9246a6ba | 346 | "Unknown object type '{0}'.".format(object_type) |
5e5d4458 | 347 | ) |
d8f55f2b | 348 | |
9246a6ba JT |
349 | # Updating existing objects |
350 | if data["verb"] == "update": | |
351 | # Check we've got a valid object | |
352 | obj = data.get("object", None) | |
353 | ||
354 | if obj is None: | |
355 | return json_error("Could not find 'object' element.") | |
356 | ||
357 | if "objectType" not in obj: | |
358 | return json_error("No objectType specified.") | |
359 | ||
360 | if "id" not in obj: | |
361 | return json_error("Object ID has not been specified.") | |
362 | ||
64a456a4 | 363 | obj_id = extract_url_arguments( |
9c602458 JT |
364 | url=obj["id"], |
365 | urlmap=request.app.url_map | |
64a456a4 JT |
366 | )["id"] |
367 | ||
368 | public_id = request.urlgen( | |
369 | "mediagoblin.api.object", | |
370 | object_type=obj["objectType"], | |
371 | id=obj_id, | |
372 | qualified=True | |
373 | ) | |
9246a6ba JT |
374 | |
375 | # Now try and find object | |
376 | if obj["objectType"] == "comment": | |
377 | if not request.user.has_privilege(u'commenter'): | |
378 | return json_error( | |
379 | "Privilege 'commenter' required to comment.", | |
380 | status=403 | |
381 | ) | |
382 | ||
64a456a4 JT |
383 | comment = TextComment.query.filter_by( |
384 | public_id=public_id | |
385 | ).first() | |
9246a6ba JT |
386 | if comment is None: |
387 | return json_error( | |
388 | "No such 'comment' with id '{0}'.".format(obj_id) | |
389 | ) | |
390 | ||
391 | # Check that the person trying to update the comment is | |
392 | # the author of the comment. | |
0f3bf8d4 | 393 | if comment.actor != request.user.id: |
9246a6ba JT |
394 | return json_error( |
395 | "Only author of comment is able to update comment.", | |
396 | status=403 | |
397 | ) | |
398 | ||
9e715bb0 | 399 | if not comment.unserialize(data["object"], request): |
9246a6ba | 400 | return json_error( |
9e715bb0 | 401 | "Invalid 'comment' with id '{0}'".format(obj["id"]) |
9246a6ba JT |
402 | ) |
403 | ||
404 | comment.save() | |
405 | ||
35885226 JT |
406 | # Create an update activity |
407 | generator = create_generator(request) | |
408 | activity = create_activity( | |
409 | verb="update", | |
410 | actor=request.user, | |
411 | obj=comment, | |
412 | generator=generator | |
413 | ) | |
414 | ||
415 | return json_response(activity.serialize(request)) | |
9246a6ba JT |
416 | |
417 | elif obj["objectType"] == "image": | |
64a456a4 JT |
418 | image = MediaEntry.query.filter_by( |
419 | public_id=public_id | |
420 | ).first() | |
9246a6ba JT |
421 | if image is None: |
422 | return json_error( | |
9e715bb0 | 423 | "No such 'image' with the id '{0}'.".format(obj["id"]) |
9246a6ba JT |
424 | ) |
425 | ||
426 | # Check that the person trying to update the comment is | |
427 | # the author of the comment. | |
0f3bf8d4 | 428 | if image.actor != request.user.id: |
9246a6ba JT |
429 | return json_error( |
430 | "Only uploader of image is able to update image.", | |
431 | status=403 | |
432 | ) | |
433 | ||
434 | if not image.unserialize(obj): | |
435 | return json_error( | |
436 | "Invalid 'image' with id '{0}'".format(obj_id) | |
437 | ) | |
63c86579 | 438 | image.generate_slug() |
9246a6ba JT |
439 | image.save() |
440 | ||
35885226 JT |
441 | # Create an update activity |
442 | generator = create_generator(request) | |
443 | activity = create_activity( | |
444 | verb="update", | |
445 | actor=request.user, | |
446 | obj=image, | |
447 | generator=generator | |
448 | ) | |
449 | ||
450 | return json_response(activity.serialize(request)) | |
c0434db4 JT |
451 | elif obj["objectType"] == "person": |
452 | # check this is the same user | |
453 | if "id" not in obj or obj["id"] != requested_user.id: | |
454 | return json_error( | |
455 | "Incorrect user id, unable to update" | |
456 | ) | |
457 | ||
458 | requested_user.unserialize(obj) | |
459 | requested_user.save() | |
7810817c | 460 | |
35885226 JT |
461 | generator = create_generator(request) |
462 | activity = create_activity( | |
463 | verb="update", | |
464 | actor=request.user, | |
465 | obj=requested_user, | |
466 | generator=generator | |
467 | ) | |
468 | ||
469 | return json_response(activity.serialize(request)) | |
470 | ||
4dec1cd6 JT |
471 | elif data["verb"] == "delete": |
472 | obj = data.get("object", None) | |
473 | if obj is None: | |
474 | return json_error("Could not find 'object' element.") | |
475 | ||
476 | if "objectType" not in obj: | |
477 | return json_error("No objectType specified.") | |
478 | ||
479 | if "id" not in obj: | |
480 | return json_error("Object ID has not been specified.") | |
481 | ||
482 | # Parse out the object ID | |
64a456a4 | 483 | obj_id = extract_url_arguments( |
4dec1cd6 JT |
484 | url=obj["id"], |
485 | urlmap=request.app.url_map | |
64a456a4 JT |
486 | )["id"] |
487 | ||
488 | public_id = request.urlgen( | |
489 | "mediagoblin.api.object", | |
490 | object_type=obj["objectType"], | |
491 | id=obj_id, | |
492 | qualified=True | |
493 | ) | |
4dec1cd6 JT |
494 | |
495 | if obj.get("objectType", None) == "comment": | |
496 | # Find the comment asked for | |
64a456a4 JT |
497 | comment = TextComment.query.filter_by( |
498 | public_id=public_id, | |
0f3bf8d4 | 499 | actor=request.user.id |
4dec1cd6 JT |
500 | ).first() |
501 | ||
502 | if comment is None: | |
503 | return json_error( | |
504 | "No such 'comment' with id '{0}'.".format(obj_id) | |
505 | ) | |
506 | ||
507 | # Make a delete activity | |
508 | generator = create_generator(request) | |
509 | activity = create_activity( | |
510 | verb="delete", | |
511 | actor=request.user, | |
512 | obj=comment, | |
513 | generator=generator | |
514 | ) | |
515 | ||
516 | # Unfortunately this has to be done while hard deletion exists | |
517 | context = activity.serialize(request) | |
518 | ||
519 | # now we can delete the comment | |
520 | comment.delete() | |
521 | ||
522 | return json_response(context) | |
523 | ||
524 | if obj.get("objectType", None) == "image": | |
525 | # Find the image | |
526 | entry = MediaEntry.query.filter_by( | |
64a456a4 | 527 | public_id=public_id, |
0f3bf8d4 | 528 | actor=request.user.id |
4dec1cd6 JT |
529 | ).first() |
530 | ||
531 | if entry is None: | |
532 | return json_error( | |
533 | "No such 'image' with id '{0}'.".format(obj_id) | |
534 | ) | |
535 | ||
536 | # Make the delete activity | |
537 | generator = create_generator(request) | |
538 | activity = create_activity( | |
539 | verb="delete", | |
540 | actor=request.user, | |
541 | obj=entry, | |
542 | generator=generator | |
543 | ) | |
544 | ||
545 | # This is because we have hard deletion | |
546 | context = activity.serialize(request) | |
547 | ||
548 | # Now we can delete the image | |
549 | entry.delete() | |
550 | ||
551 | return json_response(context) | |
552 | ||
51ab5192 | 553 | elif request.method != "GET": |
5e5d4458 JT |
554 | return json_error( |
555 | "Unsupported HTTP method {0}".format(request.method), | |
556 | status=501 | |
557 | ) | |
51ab5192 | 558 | |
c894b424 | 559 | feed = { |
c64fc16b | 560 | "displayName": "Activities by {user}@{host}".format( |
561 | user=request.user.username, | |
562 | host=request.host | |
563 | ), | |
c894b424 | 564 | "objectTypes": ["activity"], |
058964bc JT |
565 | "url": request.base_url, |
566 | "links": {"self": {"href": request.url}}, | |
3c3fa5e7 | 567 | "author": request.user.serialize(request), |
c894b424 JT |
568 | "items": [], |
569 | } | |
c64fc16b | 570 | |
058964bc | 571 | # Create outbox |
9a51bf1e | 572 | if outbox is None: |
34f1d8a2 | 573 | outbox = Activity.query.filter_by(actor=requested_user.id) |
9a51bf1e | 574 | else: |
34f1d8a2 | 575 | outbox = outbox.filter_by(actor=requested_user.id) |
058964bc JT |
576 | |
577 | # We want the newest things at the top (issue: #1055) | |
578 | outbox = outbox.order_by(Activity.published.desc()) | |
579 | ||
580 | # Limit by the "count" (default: 20) | |
26633946 JT |
581 | limit = request.args.get("count", 20) |
582 | ||
583 | try: | |
584 | limit = int(limit) | |
585 | except ValueError: | |
586 | limit = 20 | |
587 | ||
588 | # The upper most limit should be 200 | |
589 | limit = limit if limit < 200 else 200 | |
590 | ||
591 | # apply the limit | |
592 | outbox = outbox.limit(limit) | |
058964bc JT |
593 | |
594 | # Offset (default: no offset - first <count> result) | |
7c9af02a LD |
595 | offset = request.args.get("offset", 0) |
596 | try: | |
597 | offset = int(offset) | |
598 | except ValueError: | |
599 | offset = 0 | |
600 | outbox = outbox.offset(offset) | |
058964bc JT |
601 | |
602 | # Build feed. | |
603 | for activity in outbox: | |
dd733916 JT |
604 | try: |
605 | feed["items"].append(activity.serialize(request)) | |
606 | except AttributeError: | |
607 | # This occurs because of how we hard-deletion and the object | |
608 | # no longer existing anymore. We want to keep the Activity | |
609 | # in case someone wishes to look it up but we shouldn't display | |
610 | # it in the feed. | |
611 | pass | |
c894b424 JT |
612 | feed["totalItems"] = len(feed["items"]) |
613 | ||
614 | return json_response(feed) | |
d7b3805f | 615 | |
9a51bf1e JT |
616 | @oauth_required |
617 | def feed_minor_endpoint(request): | |
618 | """ Outbox for minor activities such as updates """ | |
619 | # If it's anything but GET pass it along | |
620 | if request.method != "GET": | |
621 | return feed_endpoint(request) | |
622 | ||
623 | outbox = Activity.query.filter( | |
624 | (Activity.verb == "update") | (Activity.verb == "delete") | |
625 | ) | |
626 | return feed_endpoint(request, outbox=outbox) | |
627 | ||
628 | @oauth_required | |
629 | def feed_major_endpoint(request): | |
630 | """ Outbox for all major activities """ | |
631 | # If it's anything but a GET pass it along | |
632 | if request.method != "GET": | |
633 | return feed_endpoint(request) | |
634 | ||
635 | outbox = Activity.query.filter_by(verb="post") | |
636 | return feed_endpoint(request, outbox=outbox) | |
637 | ||
d7b3805f | 638 | @oauth_required |
9246a6ba | 639 | def object_endpoint(request): |
5a2056f7 | 640 | """ Lookup for a object type """ |
0421fc5e | 641 | object_type = request.matchdict["object_type"] |
a14d90c2 | 642 | try: |
9c602458 | 643 | object_id = request.matchdict["id"] |
a14d90c2 JT |
644 | except ValueError: |
645 | error = "Invalid object ID '{0}' for '{1}'".format( | |
646 | request.matchdict["id"], | |
647 | object_type | |
648 | ) | |
5e5d4458 | 649 | return json_error(error) |
a14d90c2 | 650 | |
6781ff3c | 651 | if object_type not in ["image"]: |
c64fc16b | 652 | # not sure why this is 404, maybe ask evan. Maybe 400? |
8d75091d JT |
653 | return json_error( |
654 | "Unknown type: {0}".format(object_type), | |
655 | status=404 | |
656 | ) | |
5a2056f7 | 657 | |
64a456a4 JT |
658 | public_id = request.urlgen( |
659 | "mediagoblin.api.object", | |
660 | object_type=object_type, | |
661 | id=object_id, | |
662 | qualified=True | |
663 | ) | |
664 | ||
665 | media = MediaEntry.query.filter_by(public_id=public_id).first() | |
5a2056f7 | 666 | if media is None: |
5e5d4458 JT |
667 | return json_error( |
668 | "Can't find '{0}' with ID '{1}'".format(object_type, object_id), | |
669 | status=404 | |
670 | ) | |
5a2056f7 | 671 | |
bdde87a4 | 672 | return json_response(media.serialize(request)) |
98596dd0 | 673 | |
247a3b78 | 674 | @oauth_required |
98596dd0 | 675 | def object_comments(request): |
676 | """ Looks up for the comments on a object """ | |
64a456a4 JT |
677 | public_id = request.urlgen( |
678 | "mediagoblin.api.object", | |
679 | object_type=request.matchdict["object_type"], | |
680 | id=request.matchdict["id"], | |
681 | qualified=True | |
682 | ) | |
683 | media = MediaEntry.query.filter_by(public_id=public_id).first() | |
9246a6ba JT |
684 | if media is None: |
685 | return json_error("Can't find '{0}' with ID '{1}'".format( | |
0421fc5e | 686 | request.matchdict["object_type"], |
9246a6ba JT |
687 | request.matchdict["id"] |
688 | ), 404) | |
689 | ||
0421fc5e | 690 | comments = media.serialize(request) |
9246a6ba JT |
691 | comments = comments.get("replies", { |
692 | "totalItems": 0, | |
693 | "items": [], | |
694 | "url": request.urlgen( | |
4fd52036 | 695 | "mediagoblin.api.object.comments", |
0421fc5e | 696 | object_type=media.object_type, |
9246a6ba JT |
697 | id=media.id, |
698 | qualified=True | |
699 | ) | |
700 | }) | |
98596dd0 | 701 | |
9246a6ba JT |
702 | comments["displayName"] = "Replies to {0}".format(comments["url"]) |
703 | comments["links"] = { | |
704 | "first": comments["url"], | |
705 | "self": comments["url"], | |
706 | } | |
707 | return json_response(comments) | |
a5682e89 | 708 | |
a5682e89 | 709 | ## |
0af1b859 | 710 | # RFC6415 - Web Host Metadata |
a5682e89 | 711 | ## |
712 | def host_meta(request): | |
0af1b859 JT |
713 | """ |
714 | This provides the host-meta URL information that is outlined | |
715 | in RFC6415. By default this should provide XRD+XML however | |
716 | if the client accepts JSON we will provide that over XRD+XML. | |
717 | The 'Accept' header is used to decude this. | |
c64fc16b | 718 | |
0af1b859 JT |
719 | A client should use this endpoint to determine what URLs to |
720 | use for OAuth endpoints. | |
721 | """ | |
722 | ||
723 | links = [ | |
1bce9961 JT |
724 | { |
725 | "rel": "lrdd", | |
726 | "type": "application/json", | |
727 | "href": request.urlgen( | |
728 | "mediagoblin.webfinger.well-known.webfinger", | |
729 | qualified=True | |
730 | ) | |
731 | }, | |
0af1b859 JT |
732 | { |
733 | "rel": "registration_endpoint", | |
734 | "href": request.urlgen( | |
735 | "mediagoblin.oauth.client_register", | |
736 | qualified=True | |
737 | ), | |
738 | }, | |
739 | { | |
740 | "rel": "http://apinamespace.org/oauth/request_token", | |
741 | "href": request.urlgen( | |
742 | "mediagoblin.oauth.request_token", | |
743 | qualified=True | |
744 | ), | |
745 | }, | |
746 | { | |
747 | "rel": "http://apinamespace.org/oauth/authorize", | |
748 | "href": request.urlgen( | |
749 | "mediagoblin.oauth.authorize", | |
750 | qualified=True | |
751 | ), | |
752 | }, | |
753 | { | |
754 | "rel": "http://apinamespace.org/oauth/access_token", | |
755 | "href": request.urlgen( | |
756 | "mediagoblin.oauth.access_token", | |
757 | qualified=True | |
758 | ), | |
759 | }, | |
760 | { | |
761 | "rel": "http://apinamespace.org/activitypub/whoami", | |
762 | "href": request.urlgen( | |
763 | "mediagoblin.webfinger.whoami", | |
764 | qualified=True | |
765 | ), | |
1bce9961 | 766 | }, |
0af1b859 JT |
767 | ] |
768 | ||
769 | if "application/json" in request.accept_mimetypes: | |
770 | return json_response({"links": links}) | |
771 | ||
772 | # provide XML+XRD | |
773 | return render_to_response( | |
774 | request, | |
4fd52036 | 775 | "mediagoblin/api/host-meta.xml", |
0af1b859 JT |
776 | {"links": links}, |
777 | mimetype="application/xrd+xml" | |
778 | ) | |
a5682e89 | 779 | |
1bce9961 JT |
780 | def lrdd_lookup(request): |
781 | """ | |
782 | This is the lrdd endpoint which can lookup a user (or | |
783 | other things such as activities). This is as specified by | |
784 | RFC6415. | |
785 | ||
786 | The cleint must provide a 'resource' as a GET parameter which | |
787 | should be the query to be looked up. | |
788 | """ | |
789 | ||
790 | if "resource" not in request.args: | |
791 | return json_error("No resource parameter", status=400) | |
792 | ||
793 | resource = request.args["resource"] | |
794 | ||
795 | if "@" in resource: | |
796 | # Lets pull out the username | |
797 | resource = resource[5:] if resource.startswith("acct:") else resource | |
798 | username, host = resource.split("@", 1) | |
799 | ||
800 | # Now lookup the user | |
b4997540 | 801 | user = LocalUser.query.filter(LocalUser.username==username).first() |
1bce9961 JT |
802 | |
803 | if user is None: | |
804 | return json_error( | |
805 | "Can't find 'user' with username '{0}'".format(username)) | |
806 | ||
807 | return json_response([ | |
808 | { | |
809 | "rel": "http://webfinger.net/rel/profile-page", | |
810 | "href": user.url_for_self(request.urlgen), | |
811 | "type": "text/html" | |
812 | }, | |
813 | { | |
814 | "rel": "self", | |
815 | "href": request.urlgen( | |
4fd52036 | 816 | "mediagoblin.api.user", |
1bce9961 JT |
817 | username=user.username, |
818 | qualified=True | |
819 | ) | |
820 | }, | |
821 | { | |
822 | "rel": "activity-outbox", | |
823 | "href": request.urlgen( | |
4fd52036 | 824 | "mediagoblin.api.feed", |
1bce9961 JT |
825 | username=user.username, |
826 | qualified=True | |
827 | ) | |
828 | } | |
829 | ]) | |
830 | else: | |
831 | return json_error("Unrecognized resource parameter", status=404) | |
832 | ||
a5682e89 | 833 | |
834 | def whoami(request): | |
a14d90c2 | 835 | """ /api/whoami - HTTP redirect to API profile """ |
5e5d4458 JT |
836 | if request.user is None: |
837 | return json_error("Not logged in.", status=401) | |
838 | ||
a5682e89 | 839 | profile = request.urlgen( |
4fd52036 | 840 | "mediagoblin.api.user.profile", |
a5682e89 | 841 | username=request.user.username, |
842 | qualified=True | |
0679545f | 843 | ) |
a5682e89 | 844 | |
845 | return redirect(request, location=profile) |