# GNU MediaGoblin -- federated, autonomous media hosting # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . import json import io import mimetypes from werkzeug.datastructures import FileStorage from mediagoblin.decorators import oauth_required from mediagoblin.federation.decorators import user_has_privilege from mediagoblin.db.models import User, MediaEntry, MediaComment from mediagoblin.tools.response import redirect, json_response, json_error from mediagoblin.meddleware.csrf import csrf_exempt from mediagoblin.submit.lib import new_upload_entry, api_upload_request, \ api_add_to_feed # MediaTypes from mediagoblin.media_types.image import MEDIA_TYPE as IMAGE_MEDIA_TYPE @oauth_required def profile(request, raw=False): """ This is /api/user//profile - This will give profile info """ user = request.matchdict["username"] requested_user = User.query.filter_by(username=user).first() if user is None: return json_error( "No such 'user' with id '{0}'".format(user), status=404 ) if raw: return (requested_user.username, requested_user.serialize(request)) # user profiles are public so return information return json_response(requested_user.serialize(request)) @oauth_required def user(request): """ This is /api/user/ - This will get the user """ user, user_profile = profile(request, raw=True) data = { "nickname": user.username, "updated": user.created.isoformat(), "published": user.created.isoformat(), "profile": user_profile, } return json_response(data) @oauth_required @csrf_exempt @user_has_privilege(u'uploader') def uploads(request): """ Endpoint for file uploads """ user = request.matchdict["username"] requested_user = User.query.filter_by(username=user).first() if requested_user is None: return json_error("No such 'user' with id '{0}'".format(user), 404) if request.method == "POST": # Ensure that the user is only able to upload to their own # upload endpoint. if requested_user.id != request.user.id: return json_error( "Not able to post to another users feed.", status=403 ) # Wrap the data in the werkzeug file wrapper if "Content-Type" not in request.headers: return json_error( "Must supply 'Content-Type' header to upload media.") mimetype = request.headers["Content-Type"] filename = mimetypes.guess_all_extensions(mimetype) filename = 'unknown' + filename[0] if filename else filename file_data = FileStorage( stream=io.BytesIO(request.data), filename=filename, content_type=mimetype ) # Find media manager entry = new_upload_entry(request.user) entry.media_type = IMAGE_MEDIA_TYPE return api_upload_request(request, file_data, entry) return json_error("Not yet implemented", 501) @oauth_required @csrf_exempt def feed(request): """ Handles the user's outbox - /api/user//feed """ user = request.matchdict["username"] requested_user = User.query.filter_by(username=user).first() # check if the user exists if requested_user is None: return json_error("No such 'user' with id '{0}'".format(user), 404) if request.data: data = json.loads(request.data) else: data = {"verb": None, "object": {}} # We need to check that the user they're posting to is # the person that they are. if request.method in ["POST", "PUT"] and \ requested_user.id != request.user.id: return json_error( "Not able to post to another users feed.", status=403 ) if request.method == "POST" and data["verb"] == "post": obj = data.get("object", None) if obj is None: return json_error("Could not find 'object' element.") if obj.get("objectType", None) == "comment": # post a comment if not request.user.has_privilege(u'commenter'): return json_error( "Privilege 'commenter' required to comment.", status=403 ) comment = MediaComment(author=request.user.id) comment.unserialize(data["object"]) comment.save() data = {"verb": "post", "object": comment.serialize(request)} return json_response(data) elif obj.get("objectType", None) == "image": # Posting an image to the feed media_id = int(data["object"]["id"]) media = MediaEntry.query.filter_by(id=media_id).first() if media is None: return json_response( "No such 'image' with id '{0}'".format(id=media_id), status=404 ) if not media.unserialize(data["object"]): return json_error( "Invalid 'image' with id '{0}'".format(media_id) ) media.save() api_add_to_feed(request, media) return json_response({ "verb": "post", "object": media.serialize(request) }) elif obj.get("objectType", None) is None: # They need to tell us what type of object they're giving us. return json_error("No objectType specified.") else: # Oh no! We don't know about this type of object (yet) object_type = obj.get("objectType", None) return json_error("Unknown object type '{0}'.".format(object_type)) elif request.method in ["PUT", "POST"] and data["verb"] == "update": # Check we've got a valid object obj = data.get("object", None) if obj is None: return json_error("Could not find 'object' element.") if "objectType" not in obj: return json_error("No objectType specified.") if "id" not in obj: return json_error("Object ID has not been specified.") obj_id = obj["id"] # Now try and find object if obj["objectType"] == "comment": if not request.user.has_privilege(u'commenter'): return json_error( "Privilege 'commenter' required to comment.", status=403 ) comment = MediaComment.query.filter_by(id=obj_id).first() if comment is None: return json_error( "No such 'comment' with id '{0}'.".format(obj_id) ) # Check that the person trying to update the comment is # the author of the comment. if comment.author != request.user.id: return json_error( "Only author of comment is able to update comment.", status=403 ) if not comment.unserialize(data["object"]): return json_error( "Invalid 'comment' with id '{0}'".format(obj_id) ) comment.save() activity = { "verb": "update", "object": comment.serialize(request), } return json_response(activity) elif obj["objectType"] == "image": image = MediaEntry.query.filter_by(id=obj_id).first() if image is None: return json_error( "No such 'image' with the id '{0}'.".format(obj_id) ) # Check that the person trying to update the comment is # the author of the comment. if image.uploader != request.user.id: return json_error( "Only uploader of image is able to update image.", status=403 ) if not image.unserialize(obj): return json_error( "Invalid 'image' with id '{0}'".format(obj_id) ) image.save() activity = { "verb": "update", "object": image.serialize(request), } return json_response(activity) elif request.method != "GET": return json_error( "Unsupported HTTP method {0}".format(request.method), status=501 ) feed_url = request.urlgen( "mediagoblin.federation.feed", username=request.user.username, qualified=True ) feed = { "displayName": "Activities by {user}@{host}".format( user=request.user.username, host=request.host ), "objectTypes": ["activity"], "url": feed_url, "links": { "first": { "href": feed_url, }, "self": { "href": request.url, }, "prev": { "href": feed_url, }, "next": { "href": feed_url, } }, "author": request.user.serialize(request), "items": [], } # Look up all the media to put in the feed (this will be changed # when we get real feeds/inboxes/outboxes/activites) for media in MediaEntry.query.all(): item = { "verb": "post", "object": media.serialize(request), "actor": request.user.serialize(request), "content": "{0} posted a picture".format(request.user.username), "id": 1, } item["updated"] = item["object"]["updated"] item["published"] = item["object"]["published"] item["url"] = item["object"]["url"] feed["items"].append(item) feed["totalItems"] = len(feed["items"]) return json_response(feed) @oauth_required def object(request, raw_obj=False): """ Lookup for a object type """ object_type = request.matchdict["objectType"] try: object_id = int(request.matchdict["id"]) except ValueError: error = "Invalid object ID '{0}' for '{1}'".format( request.matchdict["id"], object_type ) return json_error(error) if object_type not in ["image"]: # not sure why this is 404, maybe ask evan. Maybe 400? return json_error( "Unknown type: {0}".format(object_type), status=404 ) media = MediaEntry.query.filter_by(id=object_id).first() if media is None: error = "Can't find '{0}' with ID '{1}'".format( object_type, object_id ) return json_error( "Can't find '{0}' with ID '{1}'".format(object_type, object_id), status=404 ) if raw_obj: return media return json_response(media.serialize(request)) @oauth_required def object_comments(request): """ Looks up for the comments on a object """ media = object(request, raw_obj=True) response = media if isinstance(response, MediaEntry): comments = response.serialize(request) comments = comments.get("replies", { "totalItems": 0, "items": [], "url": request.urlgen( "mediagoblin.federation.object.comments", objectType=media.objectType, uuid=media.id, qualified=True ) }) comments["displayName"] = "Replies to {0}".format(comments["url"]) comments["links"] = { "first": comments["url"], "self": comments["url"], } response = json_response(comments) return response ## # Well known ## def host_meta(request): """ /.well-known/host-meta - provide URLs to resources """ links = [] links.append({ "ref": "registration_endpoint", "href": request.urlgen( "mediagoblin.oauth.client_register", qualified=True ), }) links.append({ "ref": "http://apinamespace.org/oauth/request_token", "href": request.urlgen( "mediagoblin.oauth.request_token", qualified=True ), }) links.append({ "ref": "http://apinamespace.org/oauth/authorize", "href": request.urlgen( "mediagoblin.oauth.authorize", qualified=True ), }) links.append({ "ref": "http://apinamespace.org/oauth/access_token", "href": request.urlgen( "mediagoblin.oauth.access_token", qualified=True ), }) return json_response({"links": links}) def whoami(request): """ /api/whoami - HTTP redirect to API profile """ if request.user is None: return json_error("Not logged in.", status=401) profile = request.urlgen( "mediagoblin.federation.user.profile", username=request.user.username, qualified=True ) return redirect(request, location=profile)