Merge branch 'Federation'
This commit is contained in:
commit
aab6239477
155
docs/source/api/media.rst
Normal file
155
docs/source/api/media.rst
Normal file
@ -0,0 +1,155 @@
|
||||
.. MediaGoblin Documentation
|
||||
|
||||
Written in 2011, 2012 by MediaGoblin contributors
|
||||
|
||||
To the extent possible under law, the author(s) have dedicated all
|
||||
copyright and related and neighboring rights to this software to
|
||||
the public domain worldwide. This software is distributed without
|
||||
any warranty.
|
||||
|
||||
You should have received a copy of the CC0 Public Domain
|
||||
Dedication along with this software. If not, see
|
||||
<http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
|
||||
.. info:: Currently only image uploading is supported.
|
||||
|
||||
===============
|
||||
Uploading Media
|
||||
===============
|
||||
|
||||
To use any the APIs mentioned in this document you will required :doc:`oauth`
|
||||
|
||||
Uploading and posting an media requiest you to make two to three requests:
|
||||
|
||||
1) Uploads the data to the server
|
||||
2) Post media to feed
|
||||
3) Update media to have title, description, license, etc. (optional)
|
||||
|
||||
These steps could be condenced in the future however currently this is how the
|
||||
pump.io API works. There is currently an issue open, if you would like to change
|
||||
how this works please contribute upstream: https://github.com/e14n/pump.io/issues/657
|
||||
|
||||
----------------------
|
||||
Upload Media to Server
|
||||
----------------------
|
||||
|
||||
To upload media you should use the URI `/api/user/<username>/uploads`.
|
||||
|
||||
A POST request should be made to the media upload URI submitting at least two header:
|
||||
|
||||
* `Content-Type` - This being a valid mimetype for the media.
|
||||
* `Content-Length` - size in bytes of the media.
|
||||
|
||||
The media data should be submitted as POST data to the image upload URI.
|
||||
You will get back a JSON encoded response which will look similiar to::
|
||||
|
||||
{
|
||||
"updated": "2014-01-11T09:45:48Z",
|
||||
"links": {
|
||||
"self": {
|
||||
"href": "https://<server>/image/4wiBUV1HT8GRqseyvX8m-w"
|
||||
}
|
||||
},
|
||||
"fullImage": {
|
||||
"url": "https://<server>//uploads/<username>/2014/1/11/V3cBMw.jpg",
|
||||
"width": 505,
|
||||
"height": 600
|
||||
},
|
||||
"replies": {
|
||||
"url": "https://<server>//api/image/4wiBUV1HT8GRqseyvX8m-w/replies"
|
||||
},
|
||||
"image": {
|
||||
"url": "https://<server>/uploads/<username>/2014/1/11/V3cBMw_thumb.jpg",
|
||||
"width": 269,
|
||||
"height": 320
|
||||
},
|
||||
"author": {
|
||||
"preferredUsername": "<username>",
|
||||
"displayName": "<username>",
|
||||
"links": {
|
||||
"activity-outbox": {
|
||||
"href": "https://<server>/api/user/<username>/feed"
|
||||
},
|
||||
"self": {
|
||||
"href": "https://<server>/api/user/<username>/profile"
|
||||
},
|
||||
"activity-inbox": {
|
||||
"href": "https://<server>/api/user/<username>/inbox"
|
||||
}
|
||||
},
|
||||
"url": "https://<server>/<username>",
|
||||
"updated": "2013-08-14T10:01:21Z",
|
||||
"id": "acct:<username>@<server>",
|
||||
"objectType": "person"
|
||||
},
|
||||
"url": "https://<server>/<username>/image/4wiBUV1HT8GRqseyvX8m-w",
|
||||
"published": "2014-01-11T09:45:48Z",
|
||||
"id": "https://<server>/api/image/4wiBUV1HT8GRqseyvX8m-w",
|
||||
"objectType": "image"
|
||||
}
|
||||
|
||||
The main things in this response is `fullImage` which contains `url` (the URL
|
||||
of the original image - i.e. fullsize) and `image` which contains `url` (the URL
|
||||
of a thumbnail version).
|
||||
|
||||
.. warning:: Media which have been uploaded but not submitted to a feed will
|
||||
periodically be deleted.
|
||||
|
||||
--------------
|
||||
Submit to feed
|
||||
--------------
|
||||
|
||||
This is submitting the media to appear on the website. This will create an
|
||||
object in your feed which will then appear on the GNU MediaGoblin website so the
|
||||
user and others can view and interact with the media.
|
||||
|
||||
The URL you need to POST to is `/api/user/<username>/feed`
|
||||
|
||||
You first should do a post to the feed URI with some of the information you got
|
||||
back from the above request (which uploaded the media). The request should look
|
||||
something like::
|
||||
|
||||
{
|
||||
"verb": "post",
|
||||
"object": {
|
||||
"id": "https://<server>/api/image/6_K9m-2NQFi37je845c83w",
|
||||
"objectType": "image"
|
||||
}
|
||||
}
|
||||
|
||||
.. warning:: Any other data submitted **will** be ignored
|
||||
|
||||
-------------------
|
||||
Submitting Metadata
|
||||
-------------------
|
||||
|
||||
Finally if you wish to set a title, description and license you will need to do
|
||||
and update request to the endpoint, the following attributes can be submitted:
|
||||
|
||||
+--------------+---------------------------------------+-------------------+
|
||||
| Name | Description | Required/Optional |
|
||||
+==============+=======================================+===================+
|
||||
| displayName | This is the title for the media | Optional |
|
||||
+--------------+---------------------------------------+-------------------+
|
||||
| content | This is the description for the media | Optional |
|
||||
+--------------+---------------------------------------+-------------------+
|
||||
| license | This is the license to be used | Optional |
|
||||
+--------------+---------------------------------------+-------------------+
|
||||
|
||||
.. note:: license attribute is mediagoblin specific, pump.io does not support this attribute
|
||||
|
||||
|
||||
The update request should look something similiar to::
|
||||
|
||||
{
|
||||
"verb": "update",
|
||||
"object": {
|
||||
"displayName": "My super awesome image!",
|
||||
"content": "The awesome image I took while backpacking to modor",
|
||||
"license": "creativecommons.org/licenses/by-sa/3.0/",
|
||||
"id": "https://<server>/api/image/6_K9m-2NQFi37je845c83w",
|
||||
"objectType": "image"
|
||||
}
|
||||
}
|
||||
|
||||
.. warning:: Any other data submitted **will** be ignored.
|
65
docs/source/api/media_interaction.rst
Normal file
65
docs/source/api/media_interaction.rst
Normal file
@ -0,0 +1,65 @@
|
||||
.. MediaGoblin Documentation
|
||||
|
||||
Written in 2011, 2012 by MediaGoblin contributors
|
||||
|
||||
To the extent possible under law, the author(s) have dedicated all
|
||||
copyright and related and neighboring rights to this software to
|
||||
the public domain worldwide. This software is distributed without
|
||||
any warranty.
|
||||
|
||||
You should have received a copy of the CC0 Public Domain
|
||||
Dedication along with this software. If not, see
|
||||
<http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||
|
||||
Pump.io supports a number of different interactions that can happen against
|
||||
media. Theser are commenting, liking/favoriting and (re-)sharing. Currently
|
||||
MediaGoblin supports just commenting although other interactions will come at
|
||||
a later date.
|
||||
|
||||
--------------
|
||||
How to comment
|
||||
--------------
|
||||
|
||||
.. warning:: Commenting on a comment currently is NOT supported.
|
||||
|
||||
Commenting is done by posting a comment activity to the users feed. The
|
||||
activity should look similiar to::
|
||||
|
||||
{
|
||||
"verb": "post",
|
||||
"object": {
|
||||
"objectType": "comment",
|
||||
"inReplyTo": <media>
|
||||
}
|
||||
}
|
||||
|
||||
This is where `<media>` is the media object you have got with from the server.
|
||||
|
||||
----------------
|
||||
Getting comments
|
||||
----------------
|
||||
|
||||
The media object you get back should have a `replies` section. This should
|
||||
be an object which contains the number of replies and if there are any (i.e.
|
||||
number of replies > 0) then `items` will include an array of every item::
|
||||
|
||||
{
|
||||
"totalItems": 2,
|
||||
"items: [
|
||||
{
|
||||
"id": 1,
|
||||
"objectType": "comment",
|
||||
"content": "I'm a comment ^_^",
|
||||
"author": <author user object>
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"objectType": "comment",
|
||||
"content": "Another comment! Blimey!",
|
||||
"author": <author user object>
|
||||
}
|
||||
],
|
||||
"url": "http://some.server/api/images/1/comments/"
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ case "$selfname" in
|
||||
lazycelery.sh)
|
||||
MEDIAGOBLIN_CONFIG="${ini_file}" \
|
||||
CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery \
|
||||
$starter "$@"
|
||||
$starter -B "$@"
|
||||
;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
|
@ -23,6 +23,10 @@ allow_registration = true
|
||||
# Set to false to disable the ability for users to report offensive content
|
||||
allow_reporting = true
|
||||
|
||||
# Frequency garbage collection will run (setting to 0 or false to disable)
|
||||
# Setting units are minutes.
|
||||
garbage_collection = 60
|
||||
|
||||
## Uncomment this to put some user-overriding templates here
|
||||
# local_templates = %(here)s/user_dev/templates/
|
||||
|
||||
|
@ -25,14 +25,14 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.sql import and_
|
||||
from migrate.changeset.constraint import UniqueConstraint
|
||||
|
||||
|
||||
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
|
||||
from mediagoblin.db.migration_tools import (
|
||||
RegisterMigration, inspect_table, replace_table_hack)
|
||||
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
|
||||
Privilege)
|
||||
from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
|
||||
Privilege)
|
||||
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
|
||||
|
||||
|
||||
MIGRATIONS = {}
|
||||
|
||||
|
||||
@ -466,7 +466,6 @@ def create_oauth1_tables(db):
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
@RegisterMigration(15, MIGRATIONS)
|
||||
def wants_notifications(db):
|
||||
"""Add a wants_notifications field to User model"""
|
||||
@ -660,8 +659,8 @@ def create_moderation_tables(db):
|
||||
# admin, an active user or an inactive user ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
for admin_user in admin_users_ids:
|
||||
admin_user_id = admin_user['id']
|
||||
for privilege_id in [admin_privilege_id, uploader_privilege_id,
|
||||
reporter_privilege_id, commenter_privilege_id,
|
||||
for privilege_id in [admin_privilege_id, uploader_privilege_id,
|
||||
reporter_privilege_id, commenter_privilege_id,
|
||||
active_privilege_id]:
|
||||
db.execute(user_privilege_assoc.insert().values(
|
||||
core__privilege_id=admin_user_id,
|
||||
@ -669,7 +668,7 @@ def create_moderation_tables(db):
|
||||
|
||||
for active_user in active_users_ids:
|
||||
active_user_id = active_user['id']
|
||||
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
|
||||
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
|
||||
commenter_privilege_id, active_privilege_id]:
|
||||
db.execute(user_privilege_assoc.insert().values(
|
||||
core__privilege_id=active_user_id,
|
||||
@ -677,7 +676,7 @@ def create_moderation_tables(db):
|
||||
|
||||
for inactive_user in inactive_users_ids:
|
||||
inactive_user_id = inactive_user['id']
|
||||
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
|
||||
for privilege_id in [uploader_privilege_id, reporter_privilege_id,
|
||||
commenter_privilege_id]:
|
||||
db.execute(user_privilege_assoc.insert().values(
|
||||
core__privilege_id=inactive_user_id,
|
||||
|
@ -202,6 +202,17 @@ class MediaEntryMixin(GenerateSlugMixin):
|
||||
thumb_url = mg_globals.app.staticdirector(manager[u'default_thumb'])
|
||||
return thumb_url
|
||||
|
||||
@property
|
||||
def original_url(self):
|
||||
""" Returns the URL for the original image
|
||||
will return self.thumb_url if original url doesn't exist"""
|
||||
if u"original" not in self.media_files:
|
||||
return self.thumb_url
|
||||
|
||||
return mg_globals.app.public_store.file_url(
|
||||
self.media_files[u"original"]
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def media_manager(self):
|
||||
"""Returns the MEDIA_MANAGER of the media's media_type
|
||||
|
@ -20,6 +20,7 @@ TODO: indexes on foreignkeys, where useful.
|
||||
|
||||
import logging
|
||||
import datetime
|
||||
import base64
|
||||
|
||||
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
|
||||
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
|
||||
@ -136,6 +137,48 @@ class User(Base, UserMixin):
|
||||
return UserBan.query.get(self.id) is not None
|
||||
|
||||
|
||||
def serialize(self, request):
|
||||
user = {
|
||||
"id": "acct:{0}@{1}".format(self.username, request.host),
|
||||
"preferredUsername": self.username,
|
||||
"displayName": "{0}@{1}".format(self.username, request.host),
|
||||
"objectType": "person",
|
||||
"pump_io": {
|
||||
"shared": False,
|
||||
"followed": False,
|
||||
},
|
||||
"links": {
|
||||
"self": {
|
||||
"href": request.urlgen(
|
||||
"mediagoblin.federation.user.profile",
|
||||
username=self.username,
|
||||
qualified=True
|
||||
),
|
||||
},
|
||||
"activity-inbox": {
|
||||
"href": request.urlgen(
|
||||
"mediagoblin.federation.inbox",
|
||||
username=self.username,
|
||||
qualified=True
|
||||
)
|
||||
},
|
||||
"activity-outbox": {
|
||||
"href": request.urlgen(
|
||||
"mediagoblin.federation.feed",
|
||||
username=self.username,
|
||||
qualified=True
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if self.bio:
|
||||
user.update({"summary": self.bio})
|
||||
if self.url:
|
||||
user.update({"url": self.url})
|
||||
|
||||
return user
|
||||
|
||||
class Client(Base):
|
||||
"""
|
||||
Model representing a client - Used for API Auth
|
||||
@ -201,7 +244,6 @@ class NonceTimestamp(Base):
|
||||
nonce = Column(Unicode, nullable=False, primary_key=True)
|
||||
timestamp = Column(DateTime, nullable=False, primary_key=True)
|
||||
|
||||
|
||||
class MediaEntry(Base, MediaEntryMixin):
|
||||
"""
|
||||
TODO: Consider fetching the media_files using join
|
||||
@ -388,6 +430,87 @@ class MediaEntry(Base, MediaEntryMixin):
|
||||
# pass through commit=False/True in kwargs
|
||||
super(MediaEntry, self).delete(**kwargs)
|
||||
|
||||
@property
|
||||
def objectType(self):
|
||||
""" Converts media_type to pump-like type - don't use internally """
|
||||
return self.media_type.split(".")[-1]
|
||||
|
||||
def serialize(self, request, show_comments=True):
|
||||
""" Unserialize MediaEntry to object """
|
||||
author = self.get_uploader
|
||||
url = request.urlgen(
|
||||
"mediagoblin.user_pages.media_home",
|
||||
user=author.username,
|
||||
media=self.slug,
|
||||
qualified=True
|
||||
)
|
||||
|
||||
context = {
|
||||
"id": self.id,
|
||||
"author": author.serialize(request),
|
||||
"objectType": self.objectType,
|
||||
"url": url,
|
||||
"image": {
|
||||
"url": request.host_url + self.thumb_url[1:],
|
||||
},
|
||||
"fullImage":{
|
||||
"url": request.host_url + self.original_url[1:],
|
||||
},
|
||||
"published": self.created.isoformat(),
|
||||
"updated": self.created.isoformat(),
|
||||
"pump_io": {
|
||||
"shared": False,
|
||||
},
|
||||
"links": {
|
||||
"self": {
|
||||
"href": request.urlgen(
|
||||
"mediagoblin.federation.object",
|
||||
objectType=self.objectType,
|
||||
id=self.id,
|
||||
qualified=True
|
||||
),
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if self.title:
|
||||
context["displayName"] = self.title
|
||||
|
||||
if self.description:
|
||||
context["content"] = self.description
|
||||
|
||||
if self.license:
|
||||
context["license"] = self.license
|
||||
|
||||
if show_comments:
|
||||
comments = [comment.serialize(request) for comment in self.get_comments()]
|
||||
total = len(comments)
|
||||
context["replies"] = {
|
||||
"totalItems": total,
|
||||
"items": comments,
|
||||
"url": request.urlgen(
|
||||
"mediagoblin.federation.object.comments",
|
||||
objectType=self.objectType,
|
||||
id=self.id,
|
||||
qualified=True
|
||||
),
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def unserialize(self, data):
|
||||
""" Takes API objects and unserializes on existing MediaEntry """
|
||||
if "displayName" in data:
|
||||
self.title = data["displayName"]
|
||||
|
||||
if "content" in data:
|
||||
self.description = data["content"]
|
||||
|
||||
if "license" in data:
|
||||
self.license = data["license"]
|
||||
|
||||
return True
|
||||
|
||||
class FileKeynames(Base):
|
||||
"""
|
||||
@ -534,6 +657,37 @@ class MediaComment(Base, MediaCommentMixin):
|
||||
lazy="dynamic",
|
||||
cascade="all, delete-orphan"))
|
||||
|
||||
def serialize(self, request):
|
||||
""" Unserialize to python dictionary for API """
|
||||
media = MediaEntry.query.filter_by(id=self.media_entry).first()
|
||||
author = self.get_author
|
||||
context = {
|
||||
"id": self.id,
|
||||
"objectType": "comment",
|
||||
"content": self.content,
|
||||
"inReplyTo": media.serialize(request, show_comments=False),
|
||||
"author": author.serialize(request)
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
def unserialize(self, data):
|
||||
""" Takes API objects and unserializes on existing comment """
|
||||
# Do initial checks to verify the object is correct
|
||||
required_attributes = ["content", "inReplyTo"]
|
||||
for attr in required_attributes:
|
||||
if attr not in data:
|
||||
return False
|
||||
|
||||
# Validate inReplyTo has ID
|
||||
if "id" not in data["inReplyTo"]:
|
||||
return False
|
||||
|
||||
self.media_entry = data["inReplyTo"]["id"]
|
||||
self.content = data["content"]
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class Collection(Base, CollectionMixin):
|
||||
"""An 'album' or 'set' of media by a user.
|
||||
|
@ -22,7 +22,7 @@ from oauthlib.oauth1 import ResourceEndpoint
|
||||
|
||||
from mediagoblin import mg_globals as mgg
|
||||
from mediagoblin import messages
|
||||
from mediagoblin.db.models import MediaEntry, User, MediaComment
|
||||
from mediagoblin.db.models import MediaEntry, User, MediaComment, AccessToken
|
||||
from mediagoblin.tools.response import (
|
||||
redirect, render_404,
|
||||
render_user_banned, json_response)
|
||||
@ -401,10 +401,10 @@ def oauth_required(controller):
|
||||
|
||||
request_validator = GMGRequestValidator()
|
||||
resource_endpoint = ResourceEndpoint(request_validator)
|
||||
valid, request = resource_endpoint.validate_protected_resource_request(
|
||||
valid, r = resource_endpoint.validate_protected_resource_request(
|
||||
uri=request.url,
|
||||
http_method=request.method,
|
||||
body=request.get_data(),
|
||||
body=request.data,
|
||||
headers=dict(request.headers),
|
||||
)
|
||||
|
||||
@ -412,6 +412,13 @@ def oauth_required(controller):
|
||||
error = "Invalid oauth prarameter."
|
||||
return json_response({"error": error}, status=400)
|
||||
|
||||
# Fill user if not already
|
||||
token = authorization[u"oauth_token"]
|
||||
access_token = AccessToken.query.filter_by(token=token).first()
|
||||
if access_token is not None and request.user is None:
|
||||
user_id = access_token.user
|
||||
request.user = User.query.filter_by(id=user_id).first()
|
||||
|
||||
return controller(request, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
@ -13,11 +13,3 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.tools.routing import add_route
|
||||
|
||||
add_route('mediagoblin.webfinger.host_meta', '/.well-known/host-meta',
|
||||
'mediagoblin.webfinger.views:host_meta')
|
||||
|
||||
add_route('mediagoblin.webfinger.xrd', '/webfinger/xrd',
|
||||
'mediagoblin.webfinger.views:xrd')
|
51
mediagoblin/federation/decorators.py
Normal file
51
mediagoblin/federation/decorators.py
Normal file
@ -0,0 +1,51 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
from functools import wraps
|
||||
|
||||
from mediagoblin.db.models import User
|
||||
from mediagoblin.decorators import require_active_login
|
||||
from mediagoblin.tools.response import json_response
|
||||
|
||||
def user_has_privilege(privilege_name):
|
||||
"""
|
||||
Requires that a user have a particular privilege in order to access a page.
|
||||
In order to require that a user have multiple privileges, use this
|
||||
decorator twice on the same view. This decorator also makes sure that the
|
||||
user is not banned, or else it redirects them to the "You are Banned" page.
|
||||
|
||||
:param privilege_name A unicode object that is that represents
|
||||
the privilege object. This object is
|
||||
the name of the privilege, as assigned
|
||||
in the Privilege.privilege_name column
|
||||
"""
|
||||
|
||||
def user_has_privilege_decorator(controller):
|
||||
@wraps(controller)
|
||||
@require_active_login
|
||||
def wrapper(request, *args, **kwargs):
|
||||
user_id = request.user.id
|
||||
if not request.user.has_privilege(privilege_name):
|
||||
error = "User '{0}' needs '{1}' privilege".format(
|
||||
request.user.username,
|
||||
privilege_name
|
||||
)
|
||||
return json_response({"error": error}, status=403)
|
||||
|
||||
return controller(request, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return user_has_privilege_decorator
|
||||
|
79
mediagoblin/federation/routing.py
Normal file
79
mediagoblin/federation/routing.py
Normal file
@ -0,0 +1,79 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
from mediagoblin.tools.routing import add_route
|
||||
|
||||
# Add user profile
|
||||
add_route(
|
||||
"mediagoblin.federation.user",
|
||||
"/api/user/<string:username>/",
|
||||
"mediagoblin.federation.views:user"
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.federation.user.profile",
|
||||
"/api/user/<string:username>/profile",
|
||||
"mediagoblin.federation.views:profile"
|
||||
)
|
||||
|
||||
# Inbox and Outbox (feed)
|
||||
add_route(
|
||||
"mediagoblin.federation.feed",
|
||||
"/api/user/<string:username>/feed",
|
||||
"mediagoblin.federation.views:feed"
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.federation.user.uploads",
|
||||
"/api/user/<string:username>/uploads",
|
||||
"mediagoblin.federation.views:uploads"
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.federation.inbox",
|
||||
"/api/user/<string:username>/inbox",
|
||||
"mediagoblin.federation.views:feed"
|
||||
)
|
||||
|
||||
# object endpoints
|
||||
add_route(
|
||||
"mediagoblin.federation.object",
|
||||
"/api/<string:objectType>/<string:id>",
|
||||
"mediagoblin.federation.views:object"
|
||||
)
|
||||
add_route(
|
||||
"mediagoblin.federation.object.comments",
|
||||
"/api/<string:objectType>/<string:id>/comments",
|
||||
"mediagoblin.federation.views:object_comments"
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.webfinger.well-known.host-meta",
|
||||
"/.well-known/host-meta",
|
||||
"mediagoblin.federation.views:host_meta"
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.webfinger.well-known.host-meta.json",
|
||||
"/.well-known/host-meta.json",
|
||||
"mediagoblin.federation.views:host_meta"
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.webfinger.whoami",
|
||||
"/api/whoami",
|
||||
"mediagoblin.federation.views:whoami"
|
||||
)
|
42
mediagoblin/webfinger/__init__.py → mediagoblin/federation/task.py
Normal file → Executable file
42
mediagoblin/webfinger/__init__.py → mediagoblin/federation/task.py
Normal file → Executable file
@ -13,13 +13,37 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
mediagoblin.webfinger_ provides an LRDD discovery service and
|
||||
a web host meta information file
|
||||
|
||||
Links:
|
||||
- `LRDD Discovery Draft
|
||||
<http://tools.ietf.org/html/draft-hammer-discovery-06>`_.
|
||||
- `RFC 6415 - Web Host Metadata
|
||||
<http://tools.ietf.org/html/rfc6415>`_.
|
||||
'''
|
||||
import celery
|
||||
import datetime
|
||||
import logging
|
||||
import pytz
|
||||
|
||||
from mediagoblin.db.models import MediaEntry
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
logging.basicConfig()
|
||||
_log.setLevel(logging.DEBUG)
|
||||
|
||||
@celery.task()
|
||||
def collect_garbage():
|
||||
"""
|
||||
Garbage collection to clean up media
|
||||
|
||||
This will look for all critera on models to clean
|
||||
up. This is primerally written to clean up media that's
|
||||
entered a erroneous state.
|
||||
"""
|
||||
_log.info("Garbage collection is running.")
|
||||
now = datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=1)
|
||||
|
||||
garbage = MediaEntry.query.filter(MediaEntry.created > now)
|
||||
garbage = garbage.filter(MediaEntry.state == "unprocessed")
|
||||
|
||||
for entry in garbage.all():
|
||||
_log.info("Garbage media found with ID '{0}'".format(entry.id))
|
||||
entry.delete()
|
||||
|
||||
|
||||
|
||||
|
380
mediagoblin/federation/views.py
Normal file
380
mediagoblin/federation/views.py
Normal file
@ -0,0 +1,380 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import io
|
||||
import mimetypes
|
||||
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
from mediagoblin.media_types import sniff_media
|
||||
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
|
||||
from mediagoblin.meddleware.csrf import csrf_exempt
|
||||
from mediagoblin.submit.lib import new_upload_entry
|
||||
|
||||
@oauth_required
|
||||
def profile(request, raw=False):
|
||||
""" This is /api/user/<username>/profile - This will give profile info """
|
||||
user = request.matchdict["username"]
|
||||
requested_user = User.query.filter_by(username=user)
|
||||
|
||||
# check if the user exists
|
||||
if requested_user is None:
|
||||
error = "No such 'user' with id '{0}'".format(user)
|
||||
return json_response({"error": error}, status=404)
|
||||
|
||||
user = requested_user[0]
|
||||
|
||||
if raw:
|
||||
return (user, user.serialize(request))
|
||||
|
||||
# user profiles are public so return information
|
||||
return json_response(user.serialize(request))
|
||||
|
||||
@oauth_required
|
||||
def user(request):
|
||||
""" This is /api/user/<username> - 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)
|
||||
|
||||
if requested_user is None:
|
||||
error = "No such 'user' with id '{0}'".format(user)
|
||||
return json_response({"error": error}, status=404)
|
||||
|
||||
request.user = requested_user[0]
|
||||
if request.method == "POST":
|
||||
# Wrap the data in the werkzeug file wrapper
|
||||
if "Content-Type" not in request.headers:
|
||||
error = "Must supply 'Content-Type' header to upload media."
|
||||
return json_response({"error": error}, status=400)
|
||||
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
|
||||
media_type, media_manager = sniff_media(file_data, filename)
|
||||
entry = new_upload_entry(request.user)
|
||||
if hasattr(media_manager, "api_upload_request"):
|
||||
return media_manager.api_upload_request(request, file_data, entry)
|
||||
else:
|
||||
return json_response({"error": "Not yet implemented"}, status=501)
|
||||
|
||||
return json_response({"error": "Not yet implemented"}, status=501)
|
||||
|
||||
@oauth_required
|
||||
@csrf_exempt
|
||||
def feed(request):
|
||||
""" Handles the user's outbox - /api/user/<username>/feed """
|
||||
user = request.matchdict["username"]
|
||||
requested_user = User.query.filter_by(username=user)
|
||||
|
||||
# check if the user exists
|
||||
if requested_user is None:
|
||||
error = "No such 'user' with id '{0}'".format(user)
|
||||
return json_response({"error": error}, status=404)
|
||||
|
||||
request.user = requested_user[0]
|
||||
if request.data:
|
||||
data = json.loads(request.data)
|
||||
else:
|
||||
data = {"verb": None, "object": {}}
|
||||
|
||||
if request.method == "POST" and data["verb"] == "post":
|
||||
obj = data.get("object", None)
|
||||
if obj is None:
|
||||
error = {"error": "Could not find 'object' element."}
|
||||
return json_response(error, status=400)
|
||||
|
||||
if obj.get("objectType", None) == "comment":
|
||||
# post a comment
|
||||
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)
|
||||
if media is None:
|
||||
error = "No such 'image' with id '{0}'".format(id=media_id)
|
||||
return json_response(error, status=404)
|
||||
|
||||
media = media.first()
|
||||
if not media.unserialize(data["object"]):
|
||||
error = "Invalid 'image' with id '{0}'".format(media_id)
|
||||
return json_response({"error": error}, status=400)
|
||||
media.save()
|
||||
media.media_manager.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.
|
||||
error = {"error": "No objectType specified."}
|
||||
return json_response(error, status=400)
|
||||
else:
|
||||
# Oh no! We don't know about this type of object (yet)
|
||||
error_message = "Unknown object type '{0}'.".format(
|
||||
obj.get("objectType", None)
|
||||
)
|
||||
|
||||
error = {"error": error_message}
|
||||
return json_response(error, status=400)
|
||||
|
||||
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:
|
||||
error = {"error": "Could not find 'object' element."}
|
||||
return json_response(error, status=400)
|
||||
|
||||
if "objectType" not in obj:
|
||||
error = {"error": "No objectType specified."}
|
||||
return json_response(error, status=400)
|
||||
|
||||
if "id" not in obj:
|
||||
error = {"error": "Object ID has not been specified."}
|
||||
return json_response(error, status=400)
|
||||
|
||||
obj_id = obj["id"]
|
||||
|
||||
# Now try and find object
|
||||
if obj["objectType"] == "comment":
|
||||
comment = MediaComment.query.filter_by(id=obj_id)
|
||||
if comment is None:
|
||||
error = "No such 'comment' with id '{0}'.".format(obj_id)
|
||||
return json_response({"error": error}, status=400)
|
||||
|
||||
comment = comment[0]
|
||||
if not comment.unserialize(data["object"]):
|
||||
error = "Invalid 'comment' with id '{0}'".format(obj_id)
|
||||
return json_response({"error": error}, status=400)
|
||||
|
||||
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)
|
||||
if image is None:
|
||||
error = "No such 'image' with the id '{0}'.".format(obj_id)
|
||||
return json_response({"error": error}, status=400)
|
||||
|
||||
image = image[0]
|
||||
if not image.unserialize(obj):
|
||||
"Invalid 'image' with id '{0}'".format(obj_id)
|
||||
return json_response({"error": error}, status=400)
|
||||
image.save()
|
||||
|
||||
activity = {
|
||||
"verb": "update",
|
||||
"object": image.serialize(request),
|
||||
}
|
||||
return json_response(activity)
|
||||
|
||||
elif request.method != "GET":
|
||||
# Currently unsupported
|
||||
error = "Unsupported HTTP method {0}".format(request.method)
|
||||
return json_response({"error": error}, 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": [],
|
||||
}
|
||||
|
||||
|
||||
# Now lookup the user's feed.
|
||||
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_response({"error": error}, status=400)
|
||||
|
||||
if object_type not in ["image"]:
|
||||
error = "Unknown type: {0}".format(object_type)
|
||||
# not sure why this is 404, maybe ask evan. Maybe 400?
|
||||
return json_response({"error": error}, status=404)
|
||||
|
||||
media = MediaEntry.query.filter_by(id=object_id).first()
|
||||
if media is None:
|
||||
# no media found with that uuid
|
||||
error = "Can't find '{0}' with ID '{1}'".format(
|
||||
object_type,
|
||||
object_id
|
||||
)
|
||||
return json_response({"error": error}, 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 """
|
||||
profile = request.urlgen(
|
||||
"mediagoblin.federation.user.profile",
|
||||
username=request.user.username,
|
||||
qualified=True
|
||||
)
|
||||
|
||||
return redirect(request, location=profile)
|
@ -16,6 +16,7 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from celery import Celery
|
||||
@ -58,6 +59,18 @@ def get_celery_settings_dict(app_config, global_config,
|
||||
celery_settings['CELERY_ALWAYS_EAGER'] = True
|
||||
celery_settings['CELERY_EAGER_PROPAGATES_EXCEPTIONS'] = True
|
||||
|
||||
# Garbage collection periodic task
|
||||
frequency = app_config.get('garbage_collection', 60)
|
||||
if frequency:
|
||||
frequency = int(frequency)
|
||||
celery_settings['CELERYBEAT_SCHEDULE'] = {
|
||||
'garbage-collection': {
|
||||
'task': 'mediagoblin.federation.task.garbage_collection',
|
||||
'schedule': datetime.timedelta(minutes=frequency),
|
||||
}
|
||||
}
|
||||
celery_settings['BROKER_HEARTBEAT'] = 1
|
||||
|
||||
return celery_settings
|
||||
|
||||
|
||||
|
@ -19,12 +19,14 @@ import logging
|
||||
from mediagoblin.media_types import MediaManagerBase
|
||||
from mediagoblin.media_types.image.processing import sniff_handler, \
|
||||
ImageProcessingManager
|
||||
|
||||
from mediagoblin.tools.response import json_response
|
||||
from mediagoblin.submit.lib import prepare_queue_task, run_process_media
|
||||
from mediagoblin.notifications import add_comment_subscription
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ACCEPTED_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "tiff"]
|
||||
ACCEPTED_EXTENSIONS = ["jpe", "jpg", "jpeg", "png", "gif", "tiff"]
|
||||
MEDIA_TYPE = 'mediagoblin.media_types.image'
|
||||
|
||||
|
||||
@ -56,6 +58,37 @@ class ImageMediaManager(MediaManagerBase):
|
||||
except (KeyError, ValueError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def api_upload_request(request, file_data, entry):
|
||||
""" This handles a image upload request """
|
||||
# Use the same kind of method from mediagoblin/submit/views:submit_start
|
||||
entry.media_type = unicode(MEDIA_TYPE)
|
||||
entry.title = file_data.filename
|
||||
entry.generate_slug()
|
||||
|
||||
queue_file = prepare_queue_task(request.app, entry, file_data.filename)
|
||||
with queue_file:
|
||||
queue_file.write(request.data)
|
||||
|
||||
entry.save()
|
||||
return json_response(entry.serialize(request))
|
||||
|
||||
@staticmethod
|
||||
def api_add_to_feed(request, entry):
|
||||
""" Add media to Feed """
|
||||
if entry.title:
|
||||
# Shame we have to do this here but we didn't have the data in
|
||||
# api_upload_request as no filename is usually specified.
|
||||
entry.slug = None
|
||||
entry.generate_slug()
|
||||
|
||||
feed_url = request.urlgen(
|
||||
'mediagoblin.user_pages.atom_feed',
|
||||
qualified=True, user=request.user.username)
|
||||
|
||||
run_process_media(entry, feed_url)
|
||||
add_comment_subscription(request.user, entry)
|
||||
return json_response(entry.serialize(request))
|
||||
|
||||
def get_media_type_and_manager(ext):
|
||||
if ext in ACCEPTED_EXTENSIONS:
|
||||
|
@ -15,12 +15,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from oauthlib.common import Request
|
||||
from oauthlib.oauth1 import RequestValidator
|
||||
from oauthlib.oauth1 import RequestValidator
|
||||
|
||||
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
|
||||
|
||||
|
||||
|
||||
class GMGRequestValidator(RequestValidator):
|
||||
|
||||
enforce_ssl = False
|
||||
@ -63,14 +61,14 @@ class GMGRequestValidator(RequestValidator):
|
||||
""" Currently a stub - called when making AccessTokens """
|
||||
return list()
|
||||
|
||||
def validate_timestamp_and_nonce(self, client_key, timestamp,
|
||||
nonce, request, request_token=None,
|
||||
def validate_timestamp_and_nonce(self, client_key, timestamp,
|
||||
nonce, request, request_token=None,
|
||||
access_token=None):
|
||||
nc = NonceTimestamp.query.filter_by(timestamp=timestamp, nonce=nonce)
|
||||
nc = nc.first()
|
||||
if nc is None:
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
def validate_client_key(self, client_key, request):
|
||||
@ -78,7 +76,7 @@ class GMGRequestValidator(RequestValidator):
|
||||
client = Client.query.filter_by(id=client_key).first()
|
||||
if client is None:
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def validate_access_token(self, client_key, token, request):
|
||||
@ -119,14 +117,14 @@ class GMGRequest(Request):
|
||||
"""
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
"""
|
||||
"""
|
||||
:param request: werkzeug request object
|
||||
|
||||
|
||||
any extra params are passed to oauthlib.common.Request object
|
||||
"""
|
||||
kwargs["uri"] = kwargs.get("uri", request.url)
|
||||
kwargs["http_method"] = kwargs.get("http_method", request.method)
|
||||
kwargs["body"] = kwargs.get("body", request.get_data())
|
||||
kwargs["body"] = kwargs.get("body", request.data)
|
||||
kwargs["headers"] = kwargs.get("headers", dict(request.headers))
|
||||
|
||||
super(GMGRequest, self).__init__(*args, **kwargs)
|
||||
|
@ -18,25 +18,25 @@ from mediagoblin.tools.routing import add_route
|
||||
|
||||
# client registration & oauth
|
||||
add_route(
|
||||
"mediagoblin.oauth",
|
||||
"mediagoblin.oauth.client_register",
|
||||
"/api/client/register",
|
||||
"mediagoblin.oauth.views:client_register"
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.oauth",
|
||||
"mediagoblin.oauth.request_token",
|
||||
"/oauth/request_token",
|
||||
"mediagoblin.oauth.views:request_token"
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.oauth",
|
||||
"mediagoblin.oauth.authorize",
|
||||
"/oauth/authorize",
|
||||
"mediagoblin.oauth.views:authorize",
|
||||
)
|
||||
|
||||
add_route(
|
||||
"mediagoblin.oauth",
|
||||
"mediagoblin.oauth.access_token",
|
||||
"/oauth/access_token",
|
||||
"mediagoblin.oauth.views:access_token"
|
||||
)
|
||||
|
@ -252,6 +252,7 @@ def authorize(request):
|
||||
|
||||
if oauth_request.verifier is None:
|
||||
orequest = GMGRequest(request)
|
||||
orequest.resource_owner_key = token
|
||||
request_validator = GMGRequestValidator()
|
||||
auth_endpoint = AuthorizationEndpoint(request_validator)
|
||||
verifier = auth_endpoint.create_verifier(orequest, {})
|
||||
@ -333,7 +334,7 @@ def access_token(request):
|
||||
error = "Missing required parameter."
|
||||
return json_response({"error": error}, status=400)
|
||||
|
||||
|
||||
request.resource_owner_key = parsed_tokens["oauth_consumer_key"]
|
||||
request.oauth_token = parsed_tokens["oauth_token"]
|
||||
request_validator = GMGRequestValidator(data)
|
||||
av = AccessTokenEndpoint(request_validator)
|
||||
|
@ -35,12 +35,11 @@ def get_url_map():
|
||||
import mediagoblin.submit.routing
|
||||
import mediagoblin.user_pages.routing
|
||||
import mediagoblin.edit.routing
|
||||
import mediagoblin.webfinger.routing
|
||||
import mediagoblin.listings.routing
|
||||
import mediagoblin.notifications.routing
|
||||
import mediagoblin.oauth.routing
|
||||
import mediagoblin.federation.routing
|
||||
|
||||
|
||||
for route in PluginManager().get_routes():
|
||||
add_route(*route)
|
||||
|
||||
|
@ -13,81 +13,284 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import json
|
||||
import datetime
|
||||
|
||||
|
||||
import logging
|
||||
import base64
|
||||
|
||||
import mock
|
||||
import pytz
|
||||
import pytest
|
||||
|
||||
from webtest import AppError
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
from .resources import GOOD_JPG
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.tools import template, pluginapi
|
||||
from mediagoblin.media_types import sniff_media
|
||||
from mediagoblin.db.models import User, MediaEntry
|
||||
from mediagoblin.submit.lib import new_upload_entry
|
||||
from mediagoblin.tests.tools import fixture_add_user
|
||||
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
|
||||
BIG_BLUE
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
from mediagoblin.federation.task import collect_garbage
|
||||
from mediagoblin.moderation.tools import take_away_privileges
|
||||
|
||||
class TestAPI(object):
|
||||
def setup(self):
|
||||
""" Test mediagoblin's pump.io complient APIs """
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup(self, test_app):
|
||||
self.test_app = test_app
|
||||
self.db = mg_globals.database
|
||||
|
||||
self.user_password = u'4cc355_70k3N'
|
||||
self.user = fixture_add_user(u'joapi', self.user_password,
|
||||
privileges=[u'active',u'uploader'])
|
||||
self.user = fixture_add_user(privileges=[u'active', u'uploader'])
|
||||
|
||||
def login(self, test_app):
|
||||
test_app.post(
|
||||
'/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.user_password})
|
||||
def _activity_to_feed(self, test_app, activity, headers=None):
|
||||
""" Posts an activity to the user's feed """
|
||||
if headers:
|
||||
headers.setdefault("Content-Type", "application/json")
|
||||
else:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
def get_context(self, template_name):
|
||||
return template.TEMPLATE_TEST_CONTEXT[template_name]
|
||||
with mock.patch("mediagoblin.decorators.oauth_required",
|
||||
new_callable=self.mocked_oauth_required):
|
||||
response = test_app.post(
|
||||
"/api/user/{0}/feed".format(self.user.username),
|
||||
json.dumps(activity),
|
||||
headers=headers
|
||||
)
|
||||
|
||||
def http_auth_headers(self):
|
||||
return {'Authorization': 'Basic {0}'.format(
|
||||
base64.b64encode(':'.join([
|
||||
self.user.username,
|
||||
self.user_password])))}
|
||||
return response, json.loads(response.body)
|
||||
|
||||
def do_post(self, data, test_app, **kwargs):
|
||||
url = kwargs.pop('url', '/api/submit')
|
||||
do_follow = kwargs.pop('do_follow', False)
|
||||
def _upload_image(self, test_app, image):
|
||||
""" Uploads and image to MediaGoblin via pump.io API """
|
||||
data = open(image, "rb").read()
|
||||
headers = {
|
||||
"Content-Type": "image/jpeg",
|
||||
"Content-Length": str(len(data))
|
||||
}
|
||||
|
||||
if not 'headers' in kwargs.keys():
|
||||
kwargs['headers'] = self.http_auth_headers()
|
||||
|
||||
response = test_app.post(url, data, **kwargs)
|
||||
with mock.patch("mediagoblin.decorators.oauth_required",
|
||||
new_callable=self.mocked_oauth_required):
|
||||
response = test_app.post(
|
||||
"/api/user/{0}/uploads".format(self.user.username),
|
||||
data,
|
||||
headers=headers
|
||||
)
|
||||
image = json.loads(response.body)
|
||||
|
||||
if do_follow:
|
||||
response.follow()
|
||||
return response, image
|
||||
|
||||
return response
|
||||
def _post_image_to_feed(self, test_app, image):
|
||||
""" Posts an already uploaded image to feed """
|
||||
activity = {
|
||||
"verb": "post",
|
||||
"object": image,
|
||||
}
|
||||
|
||||
def upload_data(self, filename):
|
||||
return {'upload_files': [('file', filename)]}
|
||||
return self._activity_to_feed(test_app, activity)
|
||||
|
||||
def test_1_test_test_view(self, test_app):
|
||||
self.login(test_app)
|
||||
|
||||
response = test_app.get(
|
||||
'/api/test',
|
||||
headers=self.http_auth_headers())
|
||||
def mocked_oauth_required(self, *args, **kwargs):
|
||||
""" Mocks mediagoblin.decorator.oauth_required to always validate """
|
||||
|
||||
assert response.body == \
|
||||
'{"username": "joapi", "email": "joapi@example.com"}'
|
||||
def fake_controller(controller, request, *args, **kwargs):
|
||||
request.user = User.query.filter_by(id=self.user.id).first()
|
||||
return controller(request, *args, **kwargs)
|
||||
|
||||
def test_2_test_submission(self, test_app):
|
||||
self.login(test_app)
|
||||
def oauth_required(c):
|
||||
return lambda *args, **kwargs: fake_controller(c, *args, **kwargs)
|
||||
|
||||
response = self.do_post(
|
||||
{'title': 'Great JPG!'},
|
||||
test_app,
|
||||
**self.upload_data(GOOD_JPG))
|
||||
return oauth_required
|
||||
|
||||
assert response.status_int == 200
|
||||
def test_can_post_image(self, test_app):
|
||||
""" Tests that an image can be posted to the API """
|
||||
# First request we need to do is to upload the image
|
||||
response, image = self._upload_image(test_app, GOOD_JPG)
|
||||
|
||||
assert self.db.MediaEntry.query.filter_by(title=u'Great JPG!').first()
|
||||
# I should have got certain things back
|
||||
assert response.status_code == 200
|
||||
|
||||
assert "id" in image
|
||||
assert "fullImage" in image
|
||||
assert "url" in image["fullImage"]
|
||||
assert "url" in image
|
||||
assert "author" in image
|
||||
assert "published" in image
|
||||
assert "updated" in image
|
||||
assert image["objectType"] == "image"
|
||||
|
||||
# Check that we got the response we're expecting
|
||||
response, _ = self._post_image_to_feed(test_app, image)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_upload_image_with_filename(self, test_app):
|
||||
""" Tests that you can upload an image with filename and description """
|
||||
response, data = self._upload_image(test_app, GOOD_JPG)
|
||||
response, data = self._post_image_to_feed(test_app, data)
|
||||
|
||||
image = data["object"]
|
||||
|
||||
# Now we need to add a title and description
|
||||
title = "My image ^_^"
|
||||
description = "This is my super awesome image :D"
|
||||
license = "CC-BY-SA"
|
||||
|
||||
image["displayName"] = title
|
||||
image["content"] = description
|
||||
image["license"] = license
|
||||
|
||||
activity = {"verb": "update", "object": image}
|
||||
|
||||
with mock.patch("mediagoblin.decorators.oauth_required",
|
||||
new_callable=self.mocked_oauth_required):
|
||||
response = test_app.post(
|
||||
"/api/user/{0}/feed".format(self.user.username),
|
||||
json.dumps(activity),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
image = json.loads(response.body)["object"]
|
||||
|
||||
# Check everything has been set on the media correctly
|
||||
media = MediaEntry.query.filter_by(id=image["id"]).first()
|
||||
assert media.title == title
|
||||
assert media.description == description
|
||||
assert media.license == license
|
||||
|
||||
# Check we're being given back everything we should on an update
|
||||
assert image["id"] == media.id
|
||||
assert image["displayName"] == title
|
||||
assert image["content"] == description
|
||||
assert image["license"] == license
|
||||
|
||||
|
||||
def test_only_uploaders_post_image(self, test_app):
|
||||
""" Test that only uploaders can upload images """
|
||||
# Remove uploader permissions from user
|
||||
take_away_privileges(self.user.username, u"uploader")
|
||||
|
||||
# Now try and upload a image
|
||||
data = open(GOOD_JPG, "rb").read()
|
||||
headers = {
|
||||
"Content-Type": "image/jpeg",
|
||||
"Content-Length": str(len(data)),
|
||||
}
|
||||
|
||||
with mock.patch("mediagoblin.decorators.oauth_required",
|
||||
new_callable=self.mocked_oauth_required):
|
||||
with pytest.raises(AppError) as excinfo:
|
||||
test_app.post(
|
||||
"/api/user/{0}/uploads".format(self.user.username),
|
||||
data,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
# Assert that we've got a 403
|
||||
assert "403 FORBIDDEN" in excinfo.value.message
|
||||
|
||||
def test_object_endpoint(self, test_app):
|
||||
""" Tests that object can be looked up at endpoint """
|
||||
# Post an image
|
||||
response, data = self._upload_image(test_app, GOOD_JPG)
|
||||
response, data = self._post_image_to_feed(test_app, data)
|
||||
|
||||
# Now lookup image to check that endpoint works.
|
||||
image = data["object"]
|
||||
|
||||
assert "links" in image
|
||||
assert "self" in image["links"]
|
||||
|
||||
# Get URI and strip testing host off
|
||||
object_uri = image["links"]["self"]["href"]
|
||||
object_uri = object_uri.replace("http://localhost:80", "")
|
||||
|
||||
with mock.patch("mediagoblin.decorators.oauth_required",
|
||||
new_callable=self.mocked_oauth_required):
|
||||
request = test_app.get(object_uri)
|
||||
|
||||
image = json.loads(request.body)
|
||||
entry = MediaEntry.query.filter_by(id=image["id"]).first()
|
||||
|
||||
assert request.status_code == 200
|
||||
assert entry.id == image["id"]
|
||||
|
||||
assert "image" in image
|
||||
assert "fullImage" in image
|
||||
assert "pump_io" in image
|
||||
assert "links" in image
|
||||
|
||||
def test_post_comment(self, test_app):
|
||||
""" Tests that I can post an comment media """
|
||||
# Upload some media to comment on
|
||||
response, data = self._upload_image(test_app, GOOD_JPG)
|
||||
response, data = self._post_image_to_feed(test_app, data)
|
||||
|
||||
content = "Hai this is a comment on this lovely picture ^_^"
|
||||
|
||||
activity = {
|
||||
"verb": "post",
|
||||
"object": {
|
||||
"objectType": "comment",
|
||||
"content": content,
|
||||
"inReplyTo": data["object"],
|
||||
}
|
||||
}
|
||||
|
||||
response, comment_data = self._activity_to_feed(test_app, activity)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Find the objects in the database
|
||||
media = MediaEntry.query.filter_by(id=data["object"]["id"]).first()
|
||||
comment = media.get_comments()[0]
|
||||
|
||||
# Tests that it matches in the database
|
||||
assert comment.author == self.user.id
|
||||
assert comment.content == content
|
||||
|
||||
# Test that the response is what we should be given
|
||||
assert comment.id == comment_data["object"]["id"]
|
||||
assert comment.content == comment_data["object"]["content"]
|
||||
|
||||
def test_profile(self, test_app):
|
||||
""" Tests profile endpoint """
|
||||
uri = "/api/user/{0}/profile".format(self.user.username)
|
||||
with mock.patch("mediagoblin.decorators.oauth_required",
|
||||
new_callable=self.mocked_oauth_required):
|
||||
response = test_app.get(uri)
|
||||
profile = json.loads(response.body)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert profile["preferredUsername"] == self.user.username
|
||||
assert profile["objectType"] == "person"
|
||||
|
||||
assert "links" in profile
|
||||
|
||||
def test_garbage_collection_task(self, test_app):
|
||||
""" Test old media entry are removed by GC task """
|
||||
# Create a media entry that's unprocessed and over an hour old.
|
||||
entry_id = 72
|
||||
now = datetime.datetime.now(pytz.UTC)
|
||||
file_data = FileStorage(
|
||||
stream=open(GOOD_JPG, "rb"),
|
||||
filename="mah_test.jpg",
|
||||
content_type="image/jpeg"
|
||||
)
|
||||
|
||||
# Find media manager
|
||||
media_type, media_manager = sniff_media(file_data, "mah_test.jpg")
|
||||
entry = new_upload_entry(self.user)
|
||||
entry.id = entry_id
|
||||
entry.title = "Mah Image"
|
||||
entry.slug = "slugy-slug-slug"
|
||||
entry.media_type = 'image'
|
||||
entry.uploaded = now - datetime.timedelta(days=2)
|
||||
entry.save()
|
||||
|
||||
# Validate the model exists
|
||||
assert MediaEntry.query.filter_by(id=entry_id).first() is not None
|
||||
|
||||
# Call the garbage collection task
|
||||
collect_garbage()
|
||||
|
||||
# Now validate the image has been deleted
|
||||
assert MediaEntry.query.filter_by(id=entry_id).first() is None
|
||||
|
93
mediagoblin/tests/test_legacy_api.py
Normal file
93
mediagoblin/tests/test_legacy_api.py
Normal file
@ -0,0 +1,93 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import logging
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.tools import template, pluginapi
|
||||
from mediagoblin.tests.tools import fixture_add_user
|
||||
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
|
||||
BIG_BLUE
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestAPI(object):
|
||||
def setup(self):
|
||||
self.db = mg_globals.database
|
||||
|
||||
self.user_password = u'4cc355_70k3N'
|
||||
self.user = fixture_add_user(u'joapi', self.user_password,
|
||||
privileges=[u'active',u'uploader'])
|
||||
|
||||
def login(self, test_app):
|
||||
test_app.post(
|
||||
'/auth/login/', {
|
||||
'username': self.user.username,
|
||||
'password': self.user_password})
|
||||
|
||||
def get_context(self, template_name):
|
||||
return template.TEMPLATE_TEST_CONTEXT[template_name]
|
||||
|
||||
def http_auth_headers(self):
|
||||
return {'Authorization': 'Basic {0}'.format(
|
||||
base64.b64encode(':'.join([
|
||||
self.user.username,
|
||||
self.user_password])))}
|
||||
|
||||
def do_post(self, data, test_app, **kwargs):
|
||||
url = kwargs.pop('url', '/api/submit')
|
||||
do_follow = kwargs.pop('do_follow', False)
|
||||
|
||||
if not 'headers' in kwargs.keys():
|
||||
kwargs['headers'] = self.http_auth_headers()
|
||||
|
||||
response = test_app.post(url, data, **kwargs)
|
||||
|
||||
if do_follow:
|
||||
response.follow()
|
||||
|
||||
return response
|
||||
|
||||
def upload_data(self, filename):
|
||||
return {'upload_files': [('file', filename)]}
|
||||
|
||||
def test_1_test_test_view(self, test_app):
|
||||
self.login(test_app)
|
||||
|
||||
response = test_app.get(
|
||||
'/api/test',
|
||||
headers=self.http_auth_headers())
|
||||
|
||||
assert response.body == \
|
||||
'{"username": "joapi", "email": "joapi@example.com"}'
|
||||
|
||||
def test_2_test_submission(self, test_app):
|
||||
self.login(test_app)
|
||||
|
||||
response = self.do_post(
|
||||
{'title': 'Great JPG!'},
|
||||
test_app,
|
||||
**self.upload_data(GOOD_JPG))
|
||||
|
||||
assert response.status_int == 200
|
||||
|
||||
assert self.db.MediaEntry.query.filter_by(title=u'Great JPG!').first()
|
@ -52,8 +52,8 @@ class TestOAuth(object):
|
||||
|
||||
def register_client(self, **kwargs):
|
||||
""" Regiters a client with the API """
|
||||
|
||||
kwargs["type"] = "client_associate"
|
||||
|
||||
kwargs["type"] = "client_associate"
|
||||
kwargs["application_type"] = kwargs.get("application_type", "native")
|
||||
return self.test_app.post("/api/client/register", kwargs)
|
||||
|
||||
@ -63,7 +63,7 @@ class TestOAuth(object):
|
||||
client_info = response.json
|
||||
|
||||
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
|
||||
|
||||
|
||||
assert response.status_int == 200
|
||||
assert client is not None
|
||||
|
||||
@ -81,7 +81,7 @@ class TestOAuth(object):
|
||||
client_info = response.json
|
||||
|
||||
client = self.db.Client.query.filter_by(id=client_info["client_id"]).first()
|
||||
|
||||
|
||||
assert client is not None
|
||||
assert client.secret == client_info["client_secret"]
|
||||
assert client.application_type == query["application_type"]
|
||||
@ -163,4 +163,3 @@ class TestOAuth(object):
|
||||
assert request_token.client == client.id
|
||||
assert request_token.used == False
|
||||
assert request_token.callback == request_query["oauth_callback"]
|
||||
|
||||
|
@ -25,13 +25,16 @@ from webtest import TestApp
|
||||
|
||||
from mediagoblin import mg_globals
|
||||
from mediagoblin.db.models import User, MediaEntry, Collection, MediaComment, \
|
||||
CommentSubscription, CommentNotification, Privilege, CommentReport
|
||||
CommentSubscription, CommentNotification, Privilege, CommentReport, Client, \
|
||||
RequestToken, AccessToken
|
||||
from mediagoblin.tools import testing
|
||||
from mediagoblin.init.config import read_mediagoblin_config
|
||||
from mediagoblin.db.base import Session
|
||||
from mediagoblin.meddleware import BaseMeddleware
|
||||
from mediagoblin.auth import gen_password_hash
|
||||
from mediagoblin.gmg_commands.dbupdate import run_dbupdate
|
||||
from mediagoblin.oauth.views import OAUTH_ALPHABET
|
||||
from mediagoblin.tools.crypto import random_string
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
@ -343,3 +346,4 @@ def fixture_add_comment_report(comment=None, reported_user=None,
|
||||
Session.expunge(comment_report)
|
||||
|
||||
return comment_report
|
||||
|
||||
|
@ -16,7 +16,9 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
from mediagoblin.db.models import User
|
||||
|
||||
from mediagoblin.db.models import User, AccessToken
|
||||
from mediagoblin.oauth.tools.request import decode_authorization_header
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
@ -31,6 +33,18 @@ def setup_user_in_request(request):
|
||||
Examine a request and tack on a request.user parameter if that's
|
||||
appropriate.
|
||||
"""
|
||||
# If API request the user will be associated with the access token
|
||||
authorization = decode_authorization_header(request.headers)
|
||||
|
||||
if authorization.get(u"access_token"):
|
||||
# Check authorization header.
|
||||
token = authorization[u"oauth_token"]
|
||||
token = AccessToken.query.filter_by(token=token).first()
|
||||
if token is not None:
|
||||
request.user = token.user
|
||||
return
|
||||
|
||||
|
||||
if 'user_id' not in request.session:
|
||||
request.user = None
|
||||
return
|
||||
@ -45,8 +59,8 @@ def setup_user_in_request(request):
|
||||
|
||||
def decode_request(request):
|
||||
""" Decodes a request based on MIME-Type """
|
||||
data = request.get_data()
|
||||
|
||||
data = request.data
|
||||
|
||||
if request.content_type == json_encoded:
|
||||
data = json.loads(data)
|
||||
elif request.content_type == form_encoded or request.content_type == "":
|
||||
|
@ -1,117 +0,0 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
'''
|
||||
For references, see docstring in mediagoblin/webfinger/__init__.py
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from urlparse import urlparse
|
||||
|
||||
from mediagoblin.tools.response import render_to_response, render_404
|
||||
|
||||
def host_meta(request):
|
||||
'''
|
||||
Webfinger host-meta
|
||||
'''
|
||||
|
||||
placeholder = 'MG_LRDD_PLACEHOLDER'
|
||||
|
||||
lrdd_title = 'GNU MediaGoblin - User lookup'
|
||||
|
||||
lrdd_template = request.urlgen(
|
||||
'mediagoblin.webfinger.xrd',
|
||||
uri=placeholder,
|
||||
qualified=True)
|
||||
|
||||
return render_to_response(
|
||||
request,
|
||||
'mediagoblin/webfinger/host-meta.xml',
|
||||
{'request': request,
|
||||
'lrdd_template': lrdd_template,
|
||||
'lrdd_title': lrdd_title,
|
||||
'placeholder': placeholder})
|
||||
|
||||
MATCH_SCHEME_PATTERN = re.compile(r'^acct:')
|
||||
|
||||
def xrd(request):
|
||||
'''
|
||||
Find user data based on a webfinger URI
|
||||
'''
|
||||
param_uri = request.GET.get('uri')
|
||||
|
||||
if not param_uri:
|
||||
return render_404(request)
|
||||
|
||||
'''
|
||||
:py:module:`urlparse` does not recognize usernames in URIs of the
|
||||
form ``acct:user@example.org`` or ``user@example.org``.
|
||||
'''
|
||||
if not MATCH_SCHEME_PATTERN.search(param_uri):
|
||||
# Assume the URI is in the form ``user@example.org``
|
||||
uri = 'acct://' + param_uri
|
||||
else:
|
||||
# Assumes the URI looks like ``acct:user@example.org
|
||||
uri = MATCH_SCHEME_PATTERN.sub(
|
||||
'acct://', param_uri)
|
||||
|
||||
parsed = urlparse(uri)
|
||||
|
||||
xrd_subject = param_uri
|
||||
|
||||
# TODO: Verify that the user exists
|
||||
# Q: Does webfinger support error handling in this case?
|
||||
# Returning 404 seems intuitive, need to check.
|
||||
if parsed.username:
|
||||
# The user object
|
||||
# TODO: Fetch from database instead of using the MockUser
|
||||
user = MockUser()
|
||||
user.username = parsed.username
|
||||
|
||||
xrd_links = [
|
||||
{'attrs': {
|
||||
'rel': 'http://microformats.org/profile/hcard',
|
||||
'href': request.urlgen(
|
||||
'mediagoblin.user_pages.user_home',
|
||||
user=user.username,
|
||||
qualified=True)}},
|
||||
{'attrs': {
|
||||
'rel': 'http://schemas.google.com/g/2010#updates-from',
|
||||
'href': request.urlgen(
|
||||
'mediagoblin.user_pages.atom_feed',
|
||||
user=user.username,
|
||||
qualified=True)}}]
|
||||
|
||||
xrd_alias = request.urlgen(
|
||||
'mediagoblin.user_pages.user_home',
|
||||
user=user.username,
|
||||
qualified=True)
|
||||
|
||||
return render_to_response(
|
||||
request,
|
||||
'mediagoblin/webfinger/xrd.xml',
|
||||
{'request': request,
|
||||
'subject': xrd_subject,
|
||||
'alias': xrd_alias,
|
||||
'links': xrd_links })
|
||||
else:
|
||||
return render_404(request)
|
||||
|
||||
class MockUser(object):
|
||||
'''
|
||||
TEMPORARY user object
|
||||
'''
|
||||
username = None
|
Loading…
x
Reference in New Issue
Block a user