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)
|
lazycelery.sh)
|
||||||
MEDIAGOBLIN_CONFIG="${ini_file}" \
|
MEDIAGOBLIN_CONFIG="${ini_file}" \
|
||||||
CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery \
|
CELERY_CONFIG_MODULE=mediagoblin.init.celery.from_celery \
|
||||||
$starter "$@"
|
$starter -B "$@"
|
||||||
;;
|
;;
|
||||||
*) exit 1 ;;
|
*) exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
@ -23,6 +23,10 @@ allow_registration = true
|
|||||||
# Set to false to disable the ability for users to report offensive content
|
# Set to false to disable the ability for users to report offensive content
|
||||||
allow_reporting = true
|
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
|
## Uncomment this to put some user-overriding templates here
|
||||||
# local_templates = %(here)s/user_dev/templates/
|
# local_templates = %(here)s/user_dev/templates/
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ from sqlalchemy.ext.declarative import declarative_base
|
|||||||
from sqlalchemy.sql import and_
|
from sqlalchemy.sql import and_
|
||||||
from migrate.changeset.constraint import UniqueConstraint
|
from migrate.changeset.constraint import UniqueConstraint
|
||||||
|
|
||||||
|
|
||||||
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
|
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
|
||||||
from mediagoblin.db.migration_tools import (
|
from mediagoblin.db.migration_tools import (
|
||||||
RegisterMigration, inspect_table, replace_table_hack)
|
RegisterMigration, inspect_table, replace_table_hack)
|
||||||
@ -33,6 +32,7 @@ from mediagoblin.db.models import (MediaEntry, Collection, MediaComment, User,
|
|||||||
Privilege)
|
Privilege)
|
||||||
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
|
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
|
||||||
|
|
||||||
|
|
||||||
MIGRATIONS = {}
|
MIGRATIONS = {}
|
||||||
|
|
||||||
|
|
||||||
@ -466,7 +466,6 @@ def create_oauth1_tables(db):
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@RegisterMigration(15, MIGRATIONS)
|
@RegisterMigration(15, MIGRATIONS)
|
||||||
def wants_notifications(db):
|
def wants_notifications(db):
|
||||||
"""Add a wants_notifications field to User model"""
|
"""Add a wants_notifications field to User model"""
|
||||||
|
@ -202,6 +202,17 @@ class MediaEntryMixin(GenerateSlugMixin):
|
|||||||
thumb_url = mg_globals.app.staticdirector(manager[u'default_thumb'])
|
thumb_url = mg_globals.app.staticdirector(manager[u'default_thumb'])
|
||||||
return thumb_url
|
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
|
@cached_property
|
||||||
def media_manager(self):
|
def media_manager(self):
|
||||||
"""Returns the MEDIA_MANAGER of the media's media_type
|
"""Returns the MEDIA_MANAGER of the media's media_type
|
||||||
|
@ -20,6 +20,7 @@ TODO: indexes on foreignkeys, where useful.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
import datetime
|
||||||
|
import base64
|
||||||
|
|
||||||
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
|
from sqlalchemy import Column, Integer, Unicode, UnicodeText, DateTime, \
|
||||||
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
|
Boolean, ForeignKey, UniqueConstraint, PrimaryKeyConstraint, \
|
||||||
@ -136,6 +137,48 @@ class User(Base, UserMixin):
|
|||||||
return UserBan.query.get(self.id) is not None
|
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):
|
class Client(Base):
|
||||||
"""
|
"""
|
||||||
Model representing a client - Used for API Auth
|
Model representing a client - Used for API Auth
|
||||||
@ -201,7 +244,6 @@ class NonceTimestamp(Base):
|
|||||||
nonce = Column(Unicode, nullable=False, primary_key=True)
|
nonce = Column(Unicode, nullable=False, primary_key=True)
|
||||||
timestamp = Column(DateTime, nullable=False, primary_key=True)
|
timestamp = Column(DateTime, nullable=False, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
class MediaEntry(Base, MediaEntryMixin):
|
class MediaEntry(Base, MediaEntryMixin):
|
||||||
"""
|
"""
|
||||||
TODO: Consider fetching the media_files using join
|
TODO: Consider fetching the media_files using join
|
||||||
@ -388,6 +430,87 @@ class MediaEntry(Base, MediaEntryMixin):
|
|||||||
# pass through commit=False/True in kwargs
|
# pass through commit=False/True in kwargs
|
||||||
super(MediaEntry, self).delete(**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):
|
class FileKeynames(Base):
|
||||||
"""
|
"""
|
||||||
@ -534,6 +657,37 @@ class MediaComment(Base, MediaCommentMixin):
|
|||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
cascade="all, delete-orphan"))
|
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):
|
class Collection(Base, CollectionMixin):
|
||||||
"""An 'album' or 'set' of media by a user.
|
"""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 mg_globals as mgg
|
||||||
from mediagoblin import messages
|
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 (
|
from mediagoblin.tools.response import (
|
||||||
redirect, render_404,
|
redirect, render_404,
|
||||||
render_user_banned, json_response)
|
render_user_banned, json_response)
|
||||||
@ -401,10 +401,10 @@ def oauth_required(controller):
|
|||||||
|
|
||||||
request_validator = GMGRequestValidator()
|
request_validator = GMGRequestValidator()
|
||||||
resource_endpoint = ResourceEndpoint(request_validator)
|
resource_endpoint = ResourceEndpoint(request_validator)
|
||||||
valid, request = resource_endpoint.validate_protected_resource_request(
|
valid, r = resource_endpoint.validate_protected_resource_request(
|
||||||
uri=request.url,
|
uri=request.url,
|
||||||
http_method=request.method,
|
http_method=request.method,
|
||||||
body=request.get_data(),
|
body=request.data,
|
||||||
headers=dict(request.headers),
|
headers=dict(request.headers),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -412,6 +412,13 @@ def oauth_required(controller):
|
|||||||
error = "Invalid oauth prarameter."
|
error = "Invalid oauth prarameter."
|
||||||
return json_response({"error": error}, status=400)
|
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 controller(request, *args, **kwargs)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -13,11 +13,3 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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
|
# 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/>.
|
# 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:
|
import celery
|
||||||
- `LRDD Discovery Draft
|
import datetime
|
||||||
<http://tools.ietf.org/html/draft-hammer-discovery-06>`_.
|
import logging
|
||||||
- `RFC 6415 - Web Host Metadata
|
import pytz
|
||||||
<http://tools.ietf.org/html/rfc6415>`_.
|
|
||||||
'''
|
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 os
|
||||||
import sys
|
import sys
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from celery import Celery
|
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_ALWAYS_EAGER'] = True
|
||||||
celery_settings['CELERY_EAGER_PROPAGATES_EXCEPTIONS'] = 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
|
return celery_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,12 +19,14 @@ import logging
|
|||||||
from mediagoblin.media_types import MediaManagerBase
|
from mediagoblin.media_types import MediaManagerBase
|
||||||
from mediagoblin.media_types.image.processing import sniff_handler, \
|
from mediagoblin.media_types.image.processing import sniff_handler, \
|
||||||
ImageProcessingManager
|
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__)
|
_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'
|
MEDIA_TYPE = 'mediagoblin.media_types.image'
|
||||||
|
|
||||||
|
|
||||||
@ -56,6 +58,37 @@ class ImageMediaManager(MediaManagerBase):
|
|||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
return None
|
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):
|
def get_media_type_and_manager(ext):
|
||||||
if ext in ACCEPTED_EXTENSIONS:
|
if ext in ACCEPTED_EXTENSIONS:
|
||||||
|
@ -19,8 +19,6 @@ from oauthlib.oauth1 import RequestValidator
|
|||||||
|
|
||||||
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
|
from mediagoblin.db.models import NonceTimestamp, Client, RequestToken, AccessToken
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GMGRequestValidator(RequestValidator):
|
class GMGRequestValidator(RequestValidator):
|
||||||
|
|
||||||
enforce_ssl = False
|
enforce_ssl = False
|
||||||
@ -126,7 +124,7 @@ class GMGRequest(Request):
|
|||||||
"""
|
"""
|
||||||
kwargs["uri"] = kwargs.get("uri", request.url)
|
kwargs["uri"] = kwargs.get("uri", request.url)
|
||||||
kwargs["http_method"] = kwargs.get("http_method", request.method)
|
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))
|
kwargs["headers"] = kwargs.get("headers", dict(request.headers))
|
||||||
|
|
||||||
super(GMGRequest, self).__init__(*args, **kwargs)
|
super(GMGRequest, self).__init__(*args, **kwargs)
|
||||||
|
@ -18,25 +18,25 @@ from mediagoblin.tools.routing import add_route
|
|||||||
|
|
||||||
# client registration & oauth
|
# client registration & oauth
|
||||||
add_route(
|
add_route(
|
||||||
"mediagoblin.oauth",
|
"mediagoblin.oauth.client_register",
|
||||||
"/api/client/register",
|
"/api/client/register",
|
||||||
"mediagoblin.oauth.views:client_register"
|
"mediagoblin.oauth.views:client_register"
|
||||||
)
|
)
|
||||||
|
|
||||||
add_route(
|
add_route(
|
||||||
"mediagoblin.oauth",
|
"mediagoblin.oauth.request_token",
|
||||||
"/oauth/request_token",
|
"/oauth/request_token",
|
||||||
"mediagoblin.oauth.views:request_token"
|
"mediagoblin.oauth.views:request_token"
|
||||||
)
|
)
|
||||||
|
|
||||||
add_route(
|
add_route(
|
||||||
"mediagoblin.oauth",
|
"mediagoblin.oauth.authorize",
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
"mediagoblin.oauth.views:authorize",
|
"mediagoblin.oauth.views:authorize",
|
||||||
)
|
)
|
||||||
|
|
||||||
add_route(
|
add_route(
|
||||||
"mediagoblin.oauth",
|
"mediagoblin.oauth.access_token",
|
||||||
"/oauth/access_token",
|
"/oauth/access_token",
|
||||||
"mediagoblin.oauth.views:access_token"
|
"mediagoblin.oauth.views:access_token"
|
||||||
)
|
)
|
||||||
|
@ -252,6 +252,7 @@ def authorize(request):
|
|||||||
|
|
||||||
if oauth_request.verifier is None:
|
if oauth_request.verifier is None:
|
||||||
orequest = GMGRequest(request)
|
orequest = GMGRequest(request)
|
||||||
|
orequest.resource_owner_key = token
|
||||||
request_validator = GMGRequestValidator()
|
request_validator = GMGRequestValidator()
|
||||||
auth_endpoint = AuthorizationEndpoint(request_validator)
|
auth_endpoint = AuthorizationEndpoint(request_validator)
|
||||||
verifier = auth_endpoint.create_verifier(orequest, {})
|
verifier = auth_endpoint.create_verifier(orequest, {})
|
||||||
@ -333,7 +334,7 @@ def access_token(request):
|
|||||||
error = "Missing required parameter."
|
error = "Missing required parameter."
|
||||||
return json_response({"error": error}, status=400)
|
return json_response({"error": error}, status=400)
|
||||||
|
|
||||||
|
request.resource_owner_key = parsed_tokens["oauth_consumer_key"]
|
||||||
request.oauth_token = parsed_tokens["oauth_token"]
|
request.oauth_token = parsed_tokens["oauth_token"]
|
||||||
request_validator = GMGRequestValidator(data)
|
request_validator = GMGRequestValidator(data)
|
||||||
av = AccessTokenEndpoint(request_validator)
|
av = AccessTokenEndpoint(request_validator)
|
||||||
|
@ -35,11 +35,10 @@ def get_url_map():
|
|||||||
import mediagoblin.submit.routing
|
import mediagoblin.submit.routing
|
||||||
import mediagoblin.user_pages.routing
|
import mediagoblin.user_pages.routing
|
||||||
import mediagoblin.edit.routing
|
import mediagoblin.edit.routing
|
||||||
import mediagoblin.webfinger.routing
|
|
||||||
import mediagoblin.listings.routing
|
import mediagoblin.listings.routing
|
||||||
import mediagoblin.notifications.routing
|
import mediagoblin.notifications.routing
|
||||||
import mediagoblin.oauth.routing
|
import mediagoblin.oauth.routing
|
||||||
|
import mediagoblin.federation.routing
|
||||||
|
|
||||||
for route in PluginManager().get_routes():
|
for route in PluginManager().get_routes():
|
||||||
add_route(*route)
|
add_route(*route)
|
||||||
|
@ -13,81 +13,284 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import mock
|
||||||
import logging
|
import pytz
|
||||||
import base64
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from webtest import AppError
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
|
from .resources import GOOD_JPG
|
||||||
from mediagoblin import mg_globals
|
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 mediagoblin.tests.tools import fixture_add_user
|
||||||
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
|
from mediagoblin.federation.task import collect_garbage
|
||||||
BIG_BLUE
|
from mediagoblin.moderation.tools import take_away_privileges
|
||||||
|
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class TestAPI(object):
|
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.db = mg_globals.database
|
||||||
|
|
||||||
self.user_password = u'4cc355_70k3N'
|
self.user = fixture_add_user(privileges=[u'active', u'uploader'])
|
||||||
self.user = fixture_add_user(u'joapi', self.user_password,
|
|
||||||
privileges=[u'active',u'uploader'])
|
|
||||||
|
|
||||||
def login(self, test_app):
|
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"}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
return response, json.loads(response.body)
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return response, image
|
||||||
|
|
||||||
|
def _post_image_to_feed(self, test_app, image):
|
||||||
|
""" Posts an already uploaded image to feed """
|
||||||
|
activity = {
|
||||||
|
"verb": "post",
|
||||||
|
"object": image,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._activity_to_feed(test_app, activity)
|
||||||
|
|
||||||
|
|
||||||
|
def mocked_oauth_required(self, *args, **kwargs):
|
||||||
|
""" Mocks mediagoblin.decorator.oauth_required to always validate """
|
||||||
|
|
||||||
|
def fake_controller(controller, request, *args, **kwargs):
|
||||||
|
request.user = User.query.filter_by(id=self.user.id).first()
|
||||||
|
return controller(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def oauth_required(c):
|
||||||
|
return lambda *args, **kwargs: fake_controller(c, *args, **kwargs)
|
||||||
|
|
||||||
|
return oauth_required
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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(
|
test_app.post(
|
||||||
'/auth/login/', {
|
"/api/user/{0}/uploads".format(self.user.username),
|
||||||
'username': self.user.username,
|
data,
|
||||||
'password': self.user_password})
|
headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
def get_context(self, template_name):
|
# Assert that we've got a 403
|
||||||
return template.TEMPLATE_TEST_CONTEXT[template_name]
|
assert "403 FORBIDDEN" in excinfo.value.message
|
||||||
|
|
||||||
def http_auth_headers(self):
|
def test_object_endpoint(self, test_app):
|
||||||
return {'Authorization': 'Basic {0}'.format(
|
""" Tests that object can be looked up at endpoint """
|
||||||
base64.b64encode(':'.join([
|
# Post an image
|
||||||
self.user.username,
|
response, data = self._upload_image(test_app, GOOD_JPG)
|
||||||
self.user_password])))}
|
response, data = self._post_image_to_feed(test_app, data)
|
||||||
|
|
||||||
def do_post(self, data, test_app, **kwargs):
|
# Now lookup image to check that endpoint works.
|
||||||
url = kwargs.pop('url', '/api/submit')
|
image = data["object"]
|
||||||
do_follow = kwargs.pop('do_follow', False)
|
|
||||||
|
|
||||||
if not 'headers' in kwargs.keys():
|
assert "links" in image
|
||||||
kwargs['headers'] = self.http_auth_headers()
|
assert "self" in image["links"]
|
||||||
|
|
||||||
response = test_app.post(url, data, **kwargs)
|
# Get URI and strip testing host off
|
||||||
|
object_uri = image["links"]["self"]["href"]
|
||||||
|
object_uri = object_uri.replace("http://localhost:80", "")
|
||||||
|
|
||||||
if do_follow:
|
with mock.patch("mediagoblin.decorators.oauth_required",
|
||||||
response.follow()
|
new_callable=self.mocked_oauth_required):
|
||||||
|
request = test_app.get(object_uri)
|
||||||
|
|
||||||
return response
|
image = json.loads(request.body)
|
||||||
|
entry = MediaEntry.query.filter_by(id=image["id"]).first()
|
||||||
|
|
||||||
def upload_data(self, filename):
|
assert request.status_code == 200
|
||||||
return {'upload_files': [('file', filename)]}
|
assert entry.id == image["id"]
|
||||||
|
|
||||||
def test_1_test_test_view(self, test_app):
|
assert "image" in image
|
||||||
self.login(test_app)
|
assert "fullImage" in image
|
||||||
|
assert "pump_io" in image
|
||||||
|
assert "links" in image
|
||||||
|
|
||||||
response = test_app.get(
|
def test_post_comment(self, test_app):
|
||||||
'/api/test',
|
""" Tests that I can post an comment media """
|
||||||
headers=self.http_auth_headers())
|
# 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)
|
||||||
|
|
||||||
assert response.body == \
|
content = "Hai this is a comment on this lovely picture ^_^"
|
||||||
'{"username": "joapi", "email": "joapi@example.com"}'
|
|
||||||
|
|
||||||
def test_2_test_submission(self, test_app):
|
activity = {
|
||||||
self.login(test_app)
|
"verb": "post",
|
||||||
|
"object": {
|
||||||
|
"objectType": "comment",
|
||||||
|
"content": content,
|
||||||
|
"inReplyTo": data["object"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response = self.do_post(
|
response, comment_data = self._activity_to_feed(test_app, activity)
|
||||||
{'title': 'Great JPG!'},
|
assert response.status_code == 200
|
||||||
test_app,
|
|
||||||
**self.upload_data(GOOD_JPG))
|
|
||||||
|
|
||||||
assert response.status_int == 200
|
# Find the objects in the database
|
||||||
|
media = MediaEntry.query.filter_by(id=data["object"]["id"]).first()
|
||||||
|
comment = media.get_comments()[0]
|
||||||
|
|
||||||
assert self.db.MediaEntry.query.filter_by(title=u'Great JPG!').first()
|
# 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()
|
@ -163,4 +163,3 @@ class TestOAuth(object):
|
|||||||
assert request_token.client == client.id
|
assert request_token.client == client.id
|
||||||
assert request_token.used == False
|
assert request_token.used == False
|
||||||
assert request_token.callback == request_query["oauth_callback"]
|
assert request_token.callback == request_query["oauth_callback"]
|
||||||
|
|
||||||
|
@ -25,13 +25,16 @@ from webtest import TestApp
|
|||||||
|
|
||||||
from mediagoblin import mg_globals
|
from mediagoblin import mg_globals
|
||||||
from mediagoblin.db.models import User, MediaEntry, Collection, MediaComment, \
|
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.tools import testing
|
||||||
from mediagoblin.init.config import read_mediagoblin_config
|
from mediagoblin.init.config import read_mediagoblin_config
|
||||||
from mediagoblin.db.base import Session
|
from mediagoblin.db.base import Session
|
||||||
from mediagoblin.meddleware import BaseMeddleware
|
from mediagoblin.meddleware import BaseMeddleware
|
||||||
from mediagoblin.auth import gen_password_hash
|
from mediagoblin.auth import gen_password_hash
|
||||||
from mediagoblin.gmg_commands.dbupdate import run_dbupdate
|
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
|
from datetime import datetime
|
||||||
|
|
||||||
@ -343,3 +346,4 @@ def fixture_add_comment_report(comment=None, reported_user=None,
|
|||||||
Session.expunge(comment_report)
|
Session.expunge(comment_report)
|
||||||
|
|
||||||
return comment_report
|
return comment_report
|
||||||
|
|
||||||
|
@ -16,7 +16,9 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
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__)
|
_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
|
Examine a request and tack on a request.user parameter if that's
|
||||||
appropriate.
|
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:
|
if 'user_id' not in request.session:
|
||||||
request.user = None
|
request.user = None
|
||||||
return
|
return
|
||||||
@ -45,7 +59,7 @@ def setup_user_in_request(request):
|
|||||||
|
|
||||||
def decode_request(request):
|
def decode_request(request):
|
||||||
""" Decodes a request based on MIME-Type """
|
""" Decodes a request based on MIME-Type """
|
||||||
data = request.get_data()
|
data = request.data
|
||||||
|
|
||||||
if request.content_type == json_encoded:
|
if request.content_type == json_encoded:
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
|
@ -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