| 1 | |
| 2 | import json |
| 3 | import io |
| 4 | import mimetypes |
| 5 | |
| 6 | from werkzeug.datastructures import FileStorage |
| 7 | |
| 8 | from mediagoblin.media_types import sniff_media |
| 9 | from mediagoblin.decorators import oauth_required |
| 10 | from mediagoblin.db.models import User, MediaEntry, MediaComment |
| 11 | from mediagoblin.tools.response import redirect, json_response |
| 12 | from mediagoblin.meddleware.csrf import csrf_exempt |
| 13 | from mediagoblin.submit.lib import new_upload_entry |
| 14 | |
| 15 | @oauth_required |
| 16 | def profile(request, raw=False): |
| 17 | """ This is /api/user/<username>/profile - This will give profile info """ |
| 18 | user = request.matchdict["username"] |
| 19 | requested_user = User.query.filter_by(username=user) |
| 20 | |
| 21 | # check if the user exists |
| 22 | if requested_user is None: |
| 23 | error = "No such 'user' with id '{0}'".format(user) |
| 24 | return json_response({"error": error}, status=404) |
| 25 | |
| 26 | user = requested_user[0] |
| 27 | |
| 28 | if raw: |
| 29 | return (user, user.serialize(request)) |
| 30 | |
| 31 | # user profiles are public so return information |
| 32 | return json_response(user.serialize(request)) |
| 33 | |
| 34 | @oauth_required |
| 35 | def user(request): |
| 36 | """ This is /api/user/<username> - This will get the user """ |
| 37 | user, user_profile = profile(request, raw=True) |
| 38 | data = { |
| 39 | "nickname": user.username, |
| 40 | "updated": user.created.isoformat(), |
| 41 | "published": user.created.isoformat(), |
| 42 | "profile": user_profile |
| 43 | } |
| 44 | |
| 45 | return json_response(data) |
| 46 | |
| 47 | @oauth_required |
| 48 | @csrf_exempt |
| 49 | def uploads(request): |
| 50 | """ Endpoint for file uploads """ |
| 51 | user = request.matchdict["username"] |
| 52 | requested_user = User.query.filter_by(username=user) |
| 53 | |
| 54 | if requested_user is None: |
| 55 | error = "No such 'user' with id '{0}'".format(user) |
| 56 | return json_response({"error": error}, status=404) |
| 57 | |
| 58 | request.user = requested_user[0] |
| 59 | if request.method == "POST": |
| 60 | # Wrap the data in the werkzeug file wrapper |
| 61 | mimetype = request.headers.get("Content-Type", "application/octal-stream") |
| 62 | filename = mimetypes.guess_all_extensions(mimetype) |
| 63 | filename = 'unknown' + filename[0] if filename else filename |
| 64 | file_data = FileStorage( |
| 65 | stream=io.BytesIO(request.data), |
| 66 | filename=filename, |
| 67 | content_type=request.headers.get("Content-Type", "application/octal-stream") |
| 68 | ) |
| 69 | |
| 70 | # Find media manager |
| 71 | media_type, media_manager = sniff_media(file_data, filename) |
| 72 | entry = new_upload_entry(request.user) |
| 73 | if hasattr(media_manager, "api_upload_request"): |
| 74 | return media_manager.api_upload_request(request, file_data, entry) |
| 75 | else: |
| 76 | return json_response({"error": "Not yet implemented"}, status=501) |
| 77 | |
| 78 | return json_response({"error": "Not yet implemented"}, status=501) |
| 79 | |
| 80 | @oauth_required |
| 81 | @csrf_exempt |
| 82 | def feed(request): |
| 83 | """ Handles the user's outbox - /api/user/<username>/feed """ |
| 84 | user = request.matchdict["username"] |
| 85 | requested_user = User.query.filter_by(username=user) |
| 86 | |
| 87 | # check if the user exists |
| 88 | if requested_user is None: |
| 89 | error = "No such 'user' with id '{0}'".format(user) |
| 90 | return json_response({"error": error}, status=404) |
| 91 | |
| 92 | request.user = requested_user[0] |
| 93 | |
| 94 | if request.method == "POST": |
| 95 | data = json.loads(request.data) |
| 96 | obj = data.get("object", None) |
| 97 | if obj is None: |
| 98 | error = {"error": "Could not find 'object' element."} |
| 99 | return json_response(error, status=400) |
| 100 | |
| 101 | if obj.get("objectType", None) == "comment": |
| 102 | # post a comment |
| 103 | media = int(data["object"]["inReplyTo"]["id"]) |
| 104 | comment = MediaComment( |
| 105 | media_entry=media, |
| 106 | author=request.user.id, |
| 107 | content=data["object"]["content"] |
| 108 | ) |
| 109 | comment.save() |
| 110 | data = {"verb": "post", "object": comment.serialize(request)} |
| 111 | return json_response(data) |
| 112 | |
| 113 | elif obj.get("objectType", None) == "image": |
| 114 | # Posting an image to the feed |
| 115 | # NB: This is currently just handing the image back until we have an |
| 116 | # to send the image to the actual feed |
| 117 | |
| 118 | media_id = int(data["object"]["id"]) |
| 119 | media = MediaEntry.query.filter_by(id=media_id) |
| 120 | if media is None: |
| 121 | error = "No such 'image' with id '{0}'".format(id=media_id) |
| 122 | return json_response(error, status=404) |
| 123 | media = media[0] |
| 124 | return json_response({ |
| 125 | "verb": "post", |
| 126 | "object": media.serialize(request) |
| 127 | }) |
| 128 | |
| 129 | elif obj.get("objectType", None) is None: |
| 130 | # They need to tell us what type of object they're giving us. |
| 131 | error = {"error": "No objectType specified."} |
| 132 | return json_response(error, status=400) |
| 133 | else: |
| 134 | # Oh no! We don't know about this type of object (yet) |
| 135 | error_message = "Unknown object type '{0}'.".format( |
| 136 | obj.get("objectType", None) |
| 137 | ) |
| 138 | |
| 139 | error = {"error": error_message} |
| 140 | return json_response(error, status=400) |
| 141 | |
| 142 | |
| 143 | feed_url = request.urlgen( |
| 144 | "mediagoblin.federation.feed", |
| 145 | username=request.user.username, |
| 146 | qualified=True |
| 147 | ) |
| 148 | |
| 149 | feed = { |
| 150 | "displayName": "Activities by {user}@{host}".format( |
| 151 | user=request.user.username, |
| 152 | host=request.host |
| 153 | ), |
| 154 | "objectTypes": ["activity"], |
| 155 | "url": feed_url, |
| 156 | "links": { |
| 157 | "first": { |
| 158 | "href": feed_url, |
| 159 | }, |
| 160 | "self": { |
| 161 | "href": request.url, |
| 162 | }, |
| 163 | "prev": { |
| 164 | "href": feed_url, |
| 165 | }, |
| 166 | "next": { |
| 167 | "href": feed_url, |
| 168 | } |
| 169 | }, |
| 170 | "author": request.user.serialize(request), |
| 171 | "items": [], |
| 172 | } |
| 173 | |
| 174 | |
| 175 | # Now lookup the user's feed. |
| 176 | for media in MediaEntry.query.all(): |
| 177 | feed["items"].append({ |
| 178 | "verb": "post", |
| 179 | "object": media.serialize(request), |
| 180 | "actor": request.user.serialize(request), |
| 181 | "content": "{0} posted a picture".format(request.user.username), |
| 182 | "id": 1, |
| 183 | }) |
| 184 | feed["items"][-1]["updated"] = feed["items"][-1]["object"]["updated"] |
| 185 | feed["items"][-1]["published"] = feed["items"][-1]["object"]["published"] |
| 186 | feed["items"][-1]["url"] = feed["items"][-1]["object"]["url"] |
| 187 | feed["totalItems"] = len(feed["items"]) |
| 188 | |
| 189 | return json_response(feed) |
| 190 | |
| 191 | @oauth_required |
| 192 | def object(request, raw_obj=False): |
| 193 | """ Lookup for a object type """ |
| 194 | objectType = request.matchdict["objectType"] |
| 195 | uuid = request.matchdict["uuid"] |
| 196 | if objectType not in ["image"]: |
| 197 | error = "Unknown type: {0}".format(objectType) |
| 198 | # not sure why this is 404, maybe ask evan. Maybe 400? |
| 199 | return json_response({"error": error}, status=404) |
| 200 | |
| 201 | media = MediaEntry.query.filter_by(slug=uuid).first() |
| 202 | if media is None: |
| 203 | # no media found with that uuid |
| 204 | error = "Can't find a {0} with ID = {1}".format(objectType, uuid) |
| 205 | return json_response({"error": error}, status=404) |
| 206 | |
| 207 | if raw_obj: |
| 208 | return media |
| 209 | |
| 210 | return json_response(media.serialize(request)) |
| 211 | |
| 212 | @oauth_required |
| 213 | def object_comments(request): |
| 214 | """ Looks up for the comments on a object """ |
| 215 | media = object(request, raw_obj=True) |
| 216 | response = media |
| 217 | if isinstance(response, MediaEntry): |
| 218 | comments = response.serialize(request) |
| 219 | comments = comments.get("replies", { |
| 220 | "totalItems": 0, |
| 221 | "items": [], |
| 222 | "url": request.urlgen( |
| 223 | "mediagoblin.federation.object.comments", |
| 224 | objectType=media.objectType, |
| 225 | uuid=media.slug, |
| 226 | qualified=True) |
| 227 | }) |
| 228 | comments["displayName"] = "Replies to {0}".format(comments["url"]) |
| 229 | comments["links"] = { |
| 230 | "first": comments["url"], |
| 231 | "self": comments["url"], |
| 232 | } |
| 233 | response = json_response(comments) |
| 234 | |
| 235 | return response |
| 236 | |
| 237 | |
| 238 | ## |
| 239 | # Well known |
| 240 | ## |
| 241 | def host_meta(request): |
| 242 | """ This is /.well-known/host-meta - provides URL's to resources on server """ |
| 243 | links = [] |
| 244 | |
| 245 | # Client registration links |
| 246 | links.append({ |
| 247 | "ref": "registration_endpoint", |
| 248 | "href": request.urlgen("mediagoblin.oauth.client_register", qualified=True), |
| 249 | }) |
| 250 | links.append({ |
| 251 | "ref": "http://apinamespace.org/oauth/request_token", |
| 252 | "href": request.urlgen("mediagoblin.oauth.request_token", qualified=True), |
| 253 | }) |
| 254 | links.append({ |
| 255 | "ref": "http://apinamespace.org/oauth/authorize", |
| 256 | "href": request.urlgen("mediagoblin.oauth.authorize", qualified=True), |
| 257 | }) |
| 258 | links.append({ |
| 259 | "ref": "http://apinamespace.org/oauth/access_token", |
| 260 | "href": request.urlgen("mediagoblin.oauth.access_token", qualified=True), |
| 261 | }) |
| 262 | |
| 263 | return json_response({"links": links}) |
| 264 | |
| 265 | def whoami(request): |
| 266 | """ This is /api/whoami - This is a HTTP redirect to api profile """ |
| 267 | profile = request.urlgen( |
| 268 | "mediagoblin.federation.user.profile", |
| 269 | username=request.user.username, |
| 270 | qualified=True |
| 271 | ) |
| 272 | |
| 273 | return redirect(request, location=profile) |