1 # GN MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
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.
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.
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/>.
21 from werkzeug
.datastructures
import FileStorage
23 from mediagoblin
.decorators
import oauth_required
, require_active_login
24 from mediagoblin
.federation
.decorators
import user_has_privilege
25 from mediagoblin
.db
.models
import User
, MediaEntry
, MediaComment
, Activity
26 from mediagoblin
.tools
.federation
import create_activity
, create_generator
27 from mediagoblin
.tools
.routing
import extract_url_arguments
28 from mediagoblin
.tools
.response
import redirect
, json_response
, json_error
, \
29 render_404
, render_to_response
30 from mediagoblin
.meddleware
.csrf
import csrf_exempt
31 from mediagoblin
.submit
.lib
import new_upload_entry
, api_upload_request
, \
35 from mediagoblin
.media_types
.image
import MEDIA_TYPE
as IMAGE_MEDIA_TYPE
38 def get_profile(request
):
40 Gets the user's profile for the endpoint requested.
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).
47 username
= request
.matchdict
["username"]
48 user
= User
.query
.filter_by(username
=username
).first()
53 return user
, user
.serialize(request
)
58 def profile_endpoint(request
):
59 """ This is /api/user/<username>/profile - This will give profile info """
60 user
, user_profile
= get_profile(request
)
63 username
= request
.matchdict
["username"]
65 "No such 'user' with username '{0}'".format(username
),
69 # user profiles are public so return information
70 return json_response(user_profile
)
73 def user_endpoint(request
):
74 """ This is /api/user/<username> - This will get the user """
75 user
, user_profile
= get_profile(request
)
78 username
= request
.matchdict
["username"]
80 "No such 'user' with username '{0}'".format(username
),
84 return json_response({
85 "nickname": user
.username
,
86 "updated": user
.created
.isoformat(),
87 "published": user
.created
.isoformat(),
88 "profile": user_profile
,
93 @user_has_privilege(u
'uploader')
94 def uploads_endpoint(request
):
95 """ Endpoint for file uploads """
96 username
= request
.matchdict
["username"]
97 requested_user
= User
.query
.filter_by(username
=username
).first()
99 if requested_user
is None:
100 return json_error("No such 'user' with id '{0}'".format(username
), 404)
102 if request
.method
== "POST":
103 # Ensure that the user is only able to upload to their own
105 if requested_user
.id != request
.user
.id:
107 "Not able to post to another users feed.",
111 # Wrap the data in the werkzeug file wrapper
112 if "Content-Type" not in request
.headers
:
114 "Must supply 'Content-Type' header to upload media."
117 mimetype
= request
.headers
["Content-Type"]
118 filename
= mimetypes
.guess_all_extensions(mimetype
)
119 filename
= 'unknown' + filename
[0] if filename
else filename
120 file_data
= FileStorage(
121 stream
=io
.BytesIO(request
.data
),
123 content_type
=mimetype
127 entry
= new_upload_entry(request
.user
)
128 entry
.media_type
= IMAGE_MEDIA_TYPE
129 return api_upload_request(request
, file_data
, entry
)
131 return json_error("Not yet implemented", 501)
135 def feed_endpoint(request
):
136 """ Handles the user's outbox - /api/user/<username>/feed """
137 username
= request
.matchdict
["username"]
138 requested_user
= User
.query
.filter_by(username
=username
).first()
140 # check if the user exists
141 if requested_user
is None:
142 return json_error("No such 'user' with id '{0}'".format(username
), 404)
145 data
= json
.loads(request
.data
.decode())
147 data
= {"verb": None, "object": {}}
150 if request
.method
in ["POST", "PUT"]:
151 # Validate that the activity is valid
152 if "verb" not in data
or "object" not in data
:
153 return json_error("Invalid activity provided.")
155 # Check that the verb is valid
156 if data
["verb"] not in ["post", "update"]:
157 return json_error("Verb not yet implemented", 501)
159 # We need to check that the user they're posting to is
160 # the person that they are.
161 if requested_user
.id != request
.user
.id:
163 "Not able to post to another users feed.",
168 if data
["verb"] == "post":
169 obj
= data
.get("object", None)
171 return json_error("Could not find 'object' element.")
173 if obj
.get("objectType", None) == "comment":
175 if not request
.user
.has_privilege(u
'commenter'):
177 "Privilege 'commenter' required to comment.",
181 comment
= MediaComment(author
=request
.user
.id)
182 comment
.unserialize(data
["object"], request
)
185 # Create activity for comment
186 generator
= create_generator(request
)
187 activity
= create_activity(
191 target
=comment
.get_entry
,
197 "object": comment
.serialize(request
)
199 return json_response(data
)
201 elif obj
.get("objectType", None) == "image":
202 # Posting an image to the feed
203 media_id
= int(extract_url_arguments(
204 url
=data
["object"]["id"],
205 urlmap
=request
.app
.url_map
208 media
= MediaEntry
.query
.filter_by(id=media_id
).first()
211 return json_response(
212 "No such 'image' with id '{0}'".format(media_id
),
216 if media
.uploader
!= request
.user
.id:
218 "Privilege 'commenter' required to comment.",
223 if not media
.unserialize(data
["object"]):
225 "Invalid 'image' with id '{0}'".format(media_id
)
229 # Add location if one exists
230 if "location" in data
:
231 Location
.create(data
["location"], self
)
234 api_add_to_feed(request
, media
)
236 return json_response({
238 "object": media
.serialize(request
)
241 elif obj
.get("objectType", None) is None:
242 # They need to tell us what type of object they're giving us.
243 return json_error("No objectType specified.")
245 # Oh no! We don't know about this type of object (yet)
246 object_type
= obj
.get("objectType", None)
248 "Unknown object type '{0}'.".format(object_type
)
251 # Updating existing objects
252 if data
["verb"] == "update":
253 # Check we've got a valid object
254 obj
= data
.get("object", None)
257 return json_error("Could not find 'object' element.")
259 if "objectType" not in obj
:
260 return json_error("No objectType specified.")
263 return json_error("Object ID has not been specified.")
265 obj_id
= int(extract_url_arguments(
267 urlmap
=request
.app
.url_map
270 # Now try and find object
271 if obj
["objectType"] == "comment":
272 if not request
.user
.has_privilege(u
'commenter'):
274 "Privilege 'commenter' required to comment.",
278 comment
= MediaComment
.query
.filter_by(id=obj_id
).first()
281 "No such 'comment' with id '{0}'.".format(obj_id
)
284 # Check that the person trying to update the comment is
285 # the author of the comment.
286 if comment
.author
!= request
.user
.id:
288 "Only author of comment is able to update comment.",
292 if not comment
.unserialize(data
["object"]):
294 "Invalid 'comment' with id '{0}'".format(obj_id
)
301 "object": comment
.serialize(request
),
303 return json_response(activity
)
305 elif obj
["objectType"] == "image":
306 image
= MediaEntry
.query
.filter_by(id=obj_id
).first()
309 "No such 'image' with the id '{0}'.".format(obj_id
)
312 # Check that the person trying to update the comment is
313 # the author of the comment.
314 if image
.uploader
!= request
.user
.id:
316 "Only uploader of image is able to update image.",
320 if not image
.unserialize(obj
):
322 "Invalid 'image' with id '{0}'".format(obj_id
)
324 image
.generate_slug()
329 "object": image
.serialize(request
),
331 return json_response(activity
)
332 elif obj
["objectType"] == "person":
333 # check this is the same user
334 if "id" not in obj
or obj
["id"] != requested_user
.id:
336 "Incorrect user id, unable to update"
339 requested_user
.unserialize(obj
)
340 requested_user
.save()
342 elif request
.method
!= "GET":
344 "Unsupported HTTP method {0}".format(request
.method
),
348 feed_url
= request
.urlgen(
349 "mediagoblin.federation.feed",
350 username
=request
.user
.username
,
355 "displayName": "Activities by {user}@{host}".format(
356 user
=request
.user
.username
,
359 "objectTypes": ["activity"],
375 "author": request
.user
.serialize(request
),
379 for activity
in Activity
.query
.filter_by(actor
=request
.user
.id):
381 feed
["items"].append(activity
.serialize(request
))
382 except AttributeError:
383 # This occurs because of how we hard-deletion and the object
384 # no longer existing anymore. We want to keep the Activity
385 # in case someone wishes to look it up but we shouldn't display
388 feed
["totalItems"] = len(feed
["items"])
390 return json_response(feed
)
393 def object_endpoint(request
):
394 """ Lookup for a object type """
395 object_type
= request
.matchdict
["object_type"]
397 object_id
= request
.matchdict
["id"]
399 error
= "Invalid object ID '{0}' for '{1}'".format(
400 request
.matchdict
["id"],
403 return json_error(error
)
405 if object_type
not in ["image"]:
406 # not sure why this is 404, maybe ask evan. Maybe 400?
408 "Unknown type: {0}".format(object_type
),
412 media
= MediaEntry
.query
.filter_by(id=object_id
).first()
415 "Can't find '{0}' with ID '{1}'".format(object_type
, object_id
),
419 return json_response(media
.serialize(request
))
422 def object_comments(request
):
423 """ Looks up for the comments on a object """
424 media
= MediaEntry
.query
.filter_by(id=request
.matchdict
["id"]).first()
426 return json_error("Can't find '{0}' with ID '{1}'".format(
427 request
.matchdict
["object_type"],
428 request
.matchdict
["id"]
431 comments
= media
.serialize(request
)
432 comments
= comments
.get("replies", {
435 "url": request
.urlgen(
436 "mediagoblin.federation.object.comments",
437 object_type
=media
.object_type
,
443 comments
["displayName"] = "Replies to {0}".format(comments
["url"])
444 comments
["links"] = {
445 "first": comments
["url"],
446 "self": comments
["url"],
448 return json_response(comments
)
451 # RFC6415 - Web Host Metadata
453 def host_meta(request
):
455 This provides the host-meta URL information that is outlined
456 in RFC6415. By default this should provide XRD+XML however
457 if the client accepts JSON we will provide that over XRD+XML.
458 The 'Accept' header is used to decude this.
460 A client should use this endpoint to determine what URLs to
461 use for OAuth endpoints.
467 "type": "application/json",
468 "href": request
.urlgen(
469 "mediagoblin.webfinger.well-known.webfinger",
474 "rel": "registration_endpoint",
475 "href": request
.urlgen(
476 "mediagoblin.oauth.client_register",
481 "rel": "http://apinamespace.org/oauth/request_token",
482 "href": request
.urlgen(
483 "mediagoblin.oauth.request_token",
488 "rel": "http://apinamespace.org/oauth/authorize",
489 "href": request
.urlgen(
490 "mediagoblin.oauth.authorize",
495 "rel": "http://apinamespace.org/oauth/access_token",
496 "href": request
.urlgen(
497 "mediagoblin.oauth.access_token",
502 "rel": "http://apinamespace.org/activitypub/whoami",
503 "href": request
.urlgen(
504 "mediagoblin.webfinger.whoami",
510 if "application/json" in request
.accept_mimetypes
:
511 return json_response({"links": links
})
514 return render_to_response(
516 "mediagoblin/federation/host-meta.xml",
518 mimetype
="application/xrd+xml"
521 def lrdd_lookup(request
):
523 This is the lrdd endpoint which can lookup a user (or
524 other things such as activities). This is as specified by
527 The cleint must provide a 'resource' as a GET parameter which
528 should be the query to be looked up.
531 if "resource" not in request
.args
:
532 return json_error("No resource parameter", status
=400)
534 resource
= request
.args
["resource"]
537 # Lets pull out the username
538 resource
= resource
[5:] if resource
.startswith("acct:") else resource
539 username
, host
= resource
.split("@", 1)
541 # Now lookup the user
542 user
= User
.query
.filter_by(username
=username
).first()
546 "Can't find 'user' with username '{0}'".format(username
))
548 return json_response([
550 "rel": "http://webfinger.net/rel/profile-page",
551 "href": user
.url_for_self(request
.urlgen
),
556 "href": request
.urlgen(
557 "mediagoblin.federation.user",
558 username
=user
.username
,
563 "rel": "activity-outbox",
564 "href": request
.urlgen(
565 "mediagoblin.federation.feed",
566 username
=user
.username
,
572 return json_error("Unrecognized resource parameter", status
=404)
576 """ /api/whoami - HTTP redirect to API profile """
577 if request
.user
is None:
578 return json_error("Not logged in.", status
=401)
580 profile
= request
.urlgen(
581 "mediagoblin.federation.user.profile",
582 username
=request
.user
.username
,
586 return redirect(request
, location
=profile
)
588 @require_active_login
589 def activity_view(request
):
590 """ /<username>/activity/<id> - Display activity
592 This should display a HTML presentation of the activity
593 this is NOT an API endpoint.
595 # Get the user object.
596 username
= request
.matchdict
["username"]
597 user
= User
.query
.filter_by(username
=username
).first()
599 activity_id
= request
.matchdict
["id"]
601 if request
.user
is None:
602 return render_404(request
)
604 activity
= Activity
.query
.filter_by(
609 return render_404(request
)
611 return render_to_response(
613 "mediagoblin/federation/activity.html",
614 {"activity": activity
}