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