Commit | Line | Data |
---|---|---|
0679545f JT |
1 | # GNU MediaGoblin -- federated, autonomous media hosting |
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 | |
23 | from mediagoblin.media_types import sniff_media | |
d7b3805f | 24 | from mediagoblin.decorators import oauth_required |
967df5ef | 25 | from mediagoblin.federation.decorators import user_has_privilege |
c894b424 | 26 | from mediagoblin.db.models import User, MediaEntry, MediaComment |
5e5d4458 | 27 | from mediagoblin.tools.response import redirect, json_response, json_error |
c894b424 | 28 | from mediagoblin.meddleware.csrf import csrf_exempt |
5e5d4458 JT |
29 | from mediagoblin.submit.lib import new_upload_entry, api_upload_request, \ |
30 | api_add_to_feed | |
31 | ||
32 | # MediaTypes | |
33 | from mediagoblin.media_types.image import MEDIA_TYPE as IMAGE_MEDIA_TYPE | |
d7b3805f | 34 | |
247a3b78 | 35 | @oauth_required |
a5682e89 | 36 | def profile(request, raw=False): |
37 | """ This is /api/user/<username>/profile - This will give profile info """ | |
d7b3805f JT |
38 | user = request.matchdict["username"] |
39 | requested_user = User.query.filter_by(username=user) | |
c64fc16b | 40 | |
d7b3805f | 41 | if requested_user is None: |
5e5d4458 | 42 | return json_error("No such 'user' with id '{0}'".format(user), 404) |
d7b3805f JT |
43 | |
44 | user = requested_user[0] | |
45 | ||
a5682e89 | 46 | if raw: |
47 | return (user, user.serialize(request)) | |
48 | ||
d7b3805f JT |
49 | # user profiles are public so return information |
50 | return json_response(user.serialize(request)) | |
51 | ||
247a3b78 | 52 | @oauth_required |
a5682e89 | 53 | def user(request): |
54 | """ This is /api/user/<username> - This will get the user """ | |
55 | user, user_profile = profile(request, raw=True) | |
56 | data = { | |
57 | "nickname": user.username, | |
58 | "updated": user.created.isoformat(), | |
59 | "published": user.created.isoformat(), | |
d8f55f2b JT |
60 | "profile": user_profile, |
61 | } | |
a5682e89 | 62 | |
63 | return json_response(data) | |
64 | ||
247a3b78 | 65 | @oauth_required |
d4a21d7e | 66 | @csrf_exempt |
967df5ef | 67 | @user_has_privilege(u'uploader') |
d4a21d7e | 68 | def uploads(request): |
c64fc16b | 69 | """ Endpoint for file uploads """ |
d4a21d7e | 70 | user = request.matchdict["username"] |
71 | requested_user = User.query.filter_by(username=user) | |
72 | ||
73 | if requested_user is None: | |
5e5d4458 | 74 | return json_error("No such 'user' with id '{0}'".format(user), 404) |
d4a21d7e | 75 | |
76 | request.user = requested_user[0] | |
77 | if request.method == "POST": | |
78 | # Wrap the data in the werkzeug file wrapper | |
a14d90c2 | 79 | if "Content-Type" not in request.headers: |
5e5d4458 JT |
80 | return json_error( |
81 | "Must supply 'Content-Type' header to upload media.") | |
a14d90c2 | 82 | mimetype = request.headers["Content-Type"] |
41599bf2 JT |
83 | filename = mimetypes.guess_all_extensions(mimetype) |
84 | filename = 'unknown' + filename[0] if filename else filename | |
d4a21d7e | 85 | file_data = FileStorage( |
7810817c | 86 | stream=io.BytesIO(request.data), |
41599bf2 | 87 | filename=filename, |
6781ff3c | 88 | content_type=mimetype |
7810817c | 89 | ) |
90 | ||
91 | # Find media manager | |
41599bf2 | 92 | media_type, media_manager = sniff_media(file_data, filename) |
d4a21d7e | 93 | entry = new_upload_entry(request.user) |
5e5d4458 JT |
94 | entry.media_type = IMAGE_MEDIA_TYPE |
95 | return api_upload_request(request, file_data, entry) | |
d4a21d7e | 96 | |
5e5d4458 | 97 | return json_error("Not yet implemented", 501) |
d4a21d7e | 98 | |
247a3b78 | 99 | @oauth_required |
c894b424 | 100 | @csrf_exempt |
d7b3805f JT |
101 | def feed(request): |
102 | """ Handles the user's outbox - /api/user/<username>/feed """ | |
103 | user = request.matchdict["username"] | |
104 | requested_user = User.query.filter_by(username=user) | |
105 | ||
106 | # check if the user exists | |
107 | if requested_user is None: | |
5e5d4458 | 108 | return json_error("No such 'user' with id '{0}'".format(user), 404) |
d7b3805f | 109 | |
3c3fa5e7 | 110 | request.user = requested_user[0] |
6781ff3c | 111 | if request.data: |
c894b424 | 112 | data = json.loads(request.data) |
6781ff3c JT |
113 | else: |
114 | data = {"verb": None, "object": {}} | |
115 | ||
116 | if request.method == "POST" and data["verb"] == "post": | |
c894b424 JT |
117 | obj = data.get("object", None) |
118 | if obj is None: | |
5e5d4458 | 119 | return json_error("Could not find 'object' element.") |
c64fc16b | 120 | |
c894b424 JT |
121 | if obj.get("objectType", None) == "comment": |
122 | # post a comment | |
5e5d4458 JT |
123 | if not request.user.has_privilege(u'commenter'): |
124 | return json_error( | |
125 | "Privilege 'commenter' required to comment.", | |
126 | status=403 | |
127 | ) | |
128 | ||
d8f55f2b JT |
129 | comment = MediaComment(author=request.user.id) |
130 | comment.unserialize(data["object"]) | |
c894b424 | 131 | comment.save() |
3c3fa5e7 | 132 | data = {"verb": "post", "object": comment.serialize(request)} |
133 | return json_response(data) | |
7810817c | 134 | |
62dc7d3e | 135 | elif obj.get("objectType", None) == "image": |
136 | # Posting an image to the feed | |
62dc7d3e | 137 | media_id = int(data["object"]["id"]) |
138 | media = MediaEntry.query.filter_by(id=media_id) | |
139 | if media is None: | |
5e5d4458 JT |
140 | return json_response( |
141 | "No such 'image' with id '{0}'".format(id=media_id), | |
142 | status=404 | |
143 | ) | |
51ab5192 JT |
144 | |
145 | media = media.first() | |
d8f55f2b | 146 | if not media.unserialize(data["object"]): |
5e5d4458 JT |
147 | return json_error( |
148 | "Invalid 'image' with id '{0}'".format(media_id) | |
149 | ) | |
150 | ||
51ab5192 | 151 | media.save() |
5e5d4458 | 152 | api_add_to_feed(request, media) |
51ab5192 | 153 | |
c3b89feb JT |
154 | return json_response({ |
155 | "verb": "post", | |
156 | "object": media.serialize(request) | |
157 | }) | |
62dc7d3e | 158 | |
c894b424 | 159 | elif obj.get("objectType", None) is None: |
62dc7d3e | 160 | # They need to tell us what type of object they're giving us. |
5e5d4458 | 161 | return json_error("No objectType specified.") |
c894b424 | 162 | else: |
62dc7d3e | 163 | # Oh no! We don't know about this type of object (yet) |
5e5d4458 JT |
164 | object_type = obj.get("objectType", None) |
165 | return json_error("Unknown object type '{0}'.".format(object_type)) | |
c894b424 | 166 | |
6781ff3c JT |
167 | elif request.method in ["PUT", "POST"] and data["verb"] == "update": |
168 | # Check we've got a valid object | |
169 | obj = data.get("object", None) | |
170 | ||
171 | if obj is None: | |
5e5d4458 | 172 | return json_error("Could not find 'object' element.") |
6781ff3c JT |
173 | |
174 | if "objectType" not in obj: | |
5e5d4458 | 175 | return json_error("No objectType specified.") |
6781ff3c JT |
176 | |
177 | if "id" not in obj: | |
5e5d4458 | 178 | return json_error("Object ID has not been specified.") |
6781ff3c JT |
179 | |
180 | obj_id = obj["id"] | |
181 | ||
182 | # Now try and find object | |
183 | if obj["objectType"] == "comment": | |
5e5d4458 JT |
184 | if not request.user.has_privilege(u'commenter'): |
185 | return json_error( | |
186 | "Privilege 'commenter' required to comment.", | |
187 | status=403 | |
188 | ) | |
189 | ||
6781ff3c JT |
190 | comment = MediaComment.query.filter_by(id=obj_id) |
191 | if comment is None: | |
5e5d4458 JT |
192 | return json_error( |
193 | "No such 'comment' with id '{0}'.".format(obj_id) | |
194 | ) | |
6781ff3c | 195 | |
d8f55f2b JT |
196 | comment = comment[0] |
197 | if not comment.unserialize(data["object"]): | |
5e5d4458 JT |
198 | return json_error( |
199 | "Invalid 'comment' with id '{0}'".format(obj_id) | |
200 | ) | |
6781ff3c JT |
201 | |
202 | comment.save() | |
d8f55f2b | 203 | |
6781ff3c JT |
204 | activity = { |
205 | "verb": "update", | |
206 | "object": comment.serialize(request), | |
207 | } | |
208 | return json_response(activity) | |
209 | ||
210 | elif obj["objectType"] == "image": | |
211 | image = MediaEntry.query.filter_by(id=obj_id) | |
212 | if image is None: | |
5e5d4458 JT |
213 | return json_error( |
214 | "No such 'image' with the id '{0}'.".format(obj_id) | |
215 | ) | |
6781ff3c JT |
216 | |
217 | image = image[0] | |
d8f55f2b | 218 | if not image.unserialize(obj): |
5e5d4458 JT |
219 | return json_error( |
220 | "Invalid 'image' with id '{0}'".format(obj_id) | |
221 | ) | |
6781ff3c | 222 | image.save() |
d8f55f2b | 223 | |
6781ff3c JT |
224 | activity = { |
225 | "verb": "update", | |
226 | "object": image.serialize(request), | |
227 | } | |
228 | return json_response(activity) | |
7810817c | 229 | |
51ab5192 | 230 | elif request.method != "GET": |
5e5d4458 JT |
231 | return json_error( |
232 | "Unsupported HTTP method {0}".format(request.method), | |
233 | status=501 | |
234 | ) | |
51ab5192 | 235 | |
c894b424 | 236 | feed_url = request.urlgen( |
6781ff3c JT |
237 | "mediagoblin.federation.feed", |
238 | username=request.user.username, | |
239 | qualified=True | |
240 | ) | |
c894b424 JT |
241 | |
242 | feed = { | |
c64fc16b | 243 | "displayName": "Activities by {user}@{host}".format( |
244 | user=request.user.username, | |
245 | host=request.host | |
246 | ), | |
c894b424 JT |
247 | "objectTypes": ["activity"], |
248 | "url": feed_url, | |
249 | "links": { | |
250 | "first": { | |
251 | "href": feed_url, | |
252 | }, | |
253 | "self": { | |
254 | "href": request.url, | |
255 | }, | |
256 | "prev": { | |
257 | "href": feed_url, | |
258 | }, | |
259 | "next": { | |
260 | "href": feed_url, | |
261 | } | |
262 | }, | |
3c3fa5e7 | 263 | "author": request.user.serialize(request), |
c894b424 JT |
264 | "items": [], |
265 | } | |
c64fc16b | 266 | |
d7b3805f | 267 | |
5e5d4458 JT |
268 | # Look up all the media to put in the feed (this will be changed |
269 | # when we get real feeds/inboxes/outboxes/activites) | |
c894b424 | 270 | for media in MediaEntry.query.all(): |
a14d90c2 | 271 | item = { |
c894b424 JT |
272 | "verb": "post", |
273 | "object": media.serialize(request), | |
3c3fa5e7 | 274 | "actor": request.user.serialize(request), |
275 | "content": "{0} posted a picture".format(request.user.username), | |
c894b424 | 276 | "id": 1, |
a14d90c2 JT |
277 | } |
278 | item["updated"] = item["object"]["updated"] | |
279 | item["published"] = item["object"]["published"] | |
280 | item["url"] = item["object"]["url"] | |
281 | feed["items"].append(item) | |
c894b424 JT |
282 | feed["totalItems"] = len(feed["items"]) |
283 | ||
284 | return json_response(feed) | |
d7b3805f JT |
285 | |
286 | @oauth_required | |
98596dd0 | 287 | def object(request, raw_obj=False): |
5a2056f7 | 288 | """ Lookup for a object type """ |
6781ff3c | 289 | object_type = request.matchdict["objectType"] |
a14d90c2 JT |
290 | try: |
291 | object_id = int(request.matchdict["id"]) | |
292 | except ValueError: | |
293 | error = "Invalid object ID '{0}' for '{1}'".format( | |
294 | request.matchdict["id"], | |
295 | object_type | |
296 | ) | |
5e5d4458 | 297 | return json_error(error) |
a14d90c2 | 298 | |
6781ff3c | 299 | if object_type not in ["image"]: |
c64fc16b | 300 | # not sure why this is 404, maybe ask evan. Maybe 400? |
5e5d4458 | 301 | return json_error("Unknown type: {0}".format(object_type), status=404) |
5a2056f7 | 302 | |
a14d90c2 | 303 | media = MediaEntry.query.filter_by(id=object_id).first() |
5a2056f7 | 304 | if media is None: |
a14d90c2 JT |
305 | error = "Can't find '{0}' with ID '{1}'".format( |
306 | object_type, | |
307 | object_id | |
308 | ) | |
5e5d4458 JT |
309 | return json_error( |
310 | "Can't find '{0}' with ID '{1}'".format(object_type, object_id), | |
311 | status=404 | |
312 | ) | |
5a2056f7 | 313 | |
98596dd0 | 314 | if raw_obj: |
315 | return media | |
316 | ||
bdde87a4 | 317 | return json_response(media.serialize(request)) |
98596dd0 | 318 | |
247a3b78 | 319 | @oauth_required |
98596dd0 | 320 | def object_comments(request): |
321 | """ Looks up for the comments on a object """ | |
322 | media = object(request, raw_obj=True) | |
323 | response = media | |
324 | if isinstance(response, MediaEntry): | |
325 | comments = response.serialize(request) | |
326 | comments = comments.get("replies", { | |
6781ff3c JT |
327 | "totalItems": 0, |
328 | "items": [], | |
329 | "url": request.urlgen( | |
330 | "mediagoblin.federation.object.comments", | |
331 | objectType=media.objectType, | |
a14d90c2 | 332 | uuid=media.id, |
6781ff3c JT |
333 | qualified=True |
334 | ) | |
335 | }) | |
336 | ||
c894b424 JT |
337 | comments["displayName"] = "Replies to {0}".format(comments["url"]) |
338 | comments["links"] = { | |
339 | "first": comments["url"], | |
340 | "self": comments["url"], | |
341 | } | |
98596dd0 | 342 | response = json_response(comments) |
343 | ||
344 | return response | |
a5682e89 | 345 | |
a5682e89 | 346 | ## |
347 | # Well known | |
348 | ## | |
349 | def host_meta(request): | |
a14d90c2 | 350 | """ /.well-known/host-meta - provide URLs to resources """ |
a5682e89 | 351 | links = [] |
c64fc16b | 352 | |
a5682e89 | 353 | links.append({ |
354 | "ref": "registration_endpoint", | |
a14d90c2 JT |
355 | "href": request.urlgen( |
356 | "mediagoblin.oauth.client_register", | |
357 | qualified=True | |
358 | ), | |
0679545f | 359 | }) |
a5682e89 | 360 | links.append({ |
361 | "ref": "http://apinamespace.org/oauth/request_token", | |
a14d90c2 JT |
362 | "href": request.urlgen( |
363 | "mediagoblin.oauth.request_token", | |
364 | qualified=True | |
365 | ), | |
0679545f | 366 | }) |
a5682e89 | 367 | links.append({ |
368 | "ref": "http://apinamespace.org/oauth/authorize", | |
a14d90c2 JT |
369 | "href": request.urlgen( |
370 | "mediagoblin.oauth.authorize", | |
371 | qualified=True | |
372 | ), | |
0679545f | 373 | }) |
a5682e89 | 374 | links.append({ |
375 | "ref": "http://apinamespace.org/oauth/access_token", | |
a14d90c2 JT |
376 | "href": request.urlgen( |
377 | "mediagoblin.oauth.access_token", | |
378 | qualified=True | |
379 | ), | |
0679545f | 380 | }) |
a5682e89 | 381 | |
18297655 | 382 | return json_response({"links": links}) |
a5682e89 | 383 | |
384 | def whoami(request): | |
a14d90c2 | 385 | """ /api/whoami - HTTP redirect to API profile """ |
5e5d4458 JT |
386 | if request.user is None: |
387 | return json_error("Not logged in.", status=401) | |
388 | ||
a5682e89 | 389 | profile = request.urlgen( |
390 | "mediagoblin.federation.user.profile", | |
391 | username=request.user.username, | |
392 | qualified=True | |
0679545f | 393 | ) |
a5682e89 | 394 | |
395 | return redirect(request, location=profile) |