# 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 # Getters def get_profile(request): """ Gets the user's profile for the endpoint requested. For example an endpoint which is /api/{username}/feed as /api/cwebber/feed would get cwebber's profile. This will return a tuple (username, user_profile). If no user can be found then this function returns a (None, None). """ username = request.matchdict["username"] user = User.query.filter_by(username=username).first() if user is None: return None, None return user, user.serialize(request) # Endpoints @oauth_required def profile_endpoint(request): """ This is /api/user//profile - This will give profile info """ user, user_profile = get_profile(request) if user is None: username = request.matchdict["username"] return json_error( "No such 'user' with username '{0}'".format(username), status=404 ) # user profiles are public so return information return json_response(user_profile) @oauth_required def user_endpoint(request): """ This is /api/user/ - This will get the user """ user, user_profile = get_profile(request) if user is None: username = request.matchdict["username"] return json_error( "No such 'user' with username '{0}'".format(username), status=404 ) return json_response({ "nickname": user.username, "updated": user.created.isoformat(), "published": user.created.isoformat(), "profile": user_profile, }) @oauth_required @csrf_exempt @user_has_privilege(u'uploader') def uploads_endpoint(request): """ Endpoint for file uploads """ username = request.matchdict["username"] requested_user = User.query.filter_by(username=username).first() if requested_user is None: return json_error("No such 'user' with id '{0}'".format(username), 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_endpoint(request): """ Handles the user's outbox - /api/user//feed """ username = request.matchdict["username"] requested_user = User.query.filter_by(username=username).first() # check if the user exists if requested_user is None: return json_error("No such 'user' with id '{0}'".format(username), 404) if request.data: data = json.loads(request.data) else: data = {"verb": None, "object": {}} if request.method in ["POST", "PUT"]: # Validate that the activity is valid if "verb" not in data or "object" not in data: return json_error("Invalid activity provided.") # Check that the verb is valid if data["verb"] not in ["post", "update"]: return json_error("Verb not yet implemented", 501) # We need to check that the user they're posting to is # the person that they are. if requested_user.id != request.user.id: return json_error( "Not able to post to another users feed.", status=403 ) # Handle new posts if 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(media_id), status=404 ) if media.uploader != request.user.id: return json_error( "Privilege 'commenter' required to comment.", status=403 ) 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) ) # Updating existing objects if 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": media.get_uploader.serialize(request), "content": "{0} posted a picture".format(request.user.username), "id": media.id, } 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_endpoint(request): """ 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: return json_error( "Can't find '{0}' with ID '{1}'".format(object_type, object_id), status=404 ) return json_response(media.serialize(request)) @oauth_required def object_comments(request): """ Looks up for the comments on a object """ media = MediaEntry.query.filter_by(id=request.matchdict["id"]).first() if media is None: return json_error("Can't find '{0}' with ID '{1}'".format( request.matchdict["objectType"], request.matchdict["id"] ), 404) comments = response.serialize(request) comments = comments.get("replies", { "totalItems": 0, "items": [], "url": request.urlgen( "mediagoblin.federation.object.comments", objectType=media.objectType, id=media.id, qualified=True ) }) comments["displayName"] = "Replies to {0}".format(comments["url"]) comments["links"] = { "first": comments["url"], "self": comments["url"], } return json_response(comments) ## # 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)