aboutsummaryrefslogtreecommitdiffstats
path: root/mediagoblin
diff options
context:
space:
mode:
Diffstat (limited to 'mediagoblin')
-rw-r--r--mediagoblin/db/migrations.py195
-rw-r--r--mediagoblin/db/mixin.py122
-rw-r--r--mediagoblin/db/models.py202
-rw-r--r--mediagoblin/federation/routing.py10
-rw-r--r--mediagoblin/federation/views.py62
-rw-r--r--mediagoblin/submit/lib.py10
-rw-r--r--mediagoblin/templates/mediagoblin/federation/activity.html42
-rw-r--r--mediagoblin/tools/federation.py63
-rw-r--r--mediagoblin/user_pages/views.py6
9 files changed, 670 insertions, 42 deletions
diff --git a/mediagoblin/db/migrations.py b/mediagoblin/db/migrations.py
index b5b5a026..31b8333e 100644
--- a/mediagoblin/db/migrations.py
+++ b/mediagoblin/db/migrations.py
@@ -34,7 +34,7 @@ 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)
+ Privilege, Generator)
from mediagoblin.db.extratypes import JSONEncoded, MutationDict
@@ -583,7 +583,6 @@ PRIVILEGE_FOUNDATIONS_v0 = [{'privilege_name':u'admin'},
{'privilege_name':u'commenter'},
{'privilege_name':u'active'}]
-
# vR1 stands for "version Rename 1". This only exists because we need
# to deal with dropping some booleans and it's otherwise impossible
# with sqlite.
@@ -895,3 +894,195 @@ def revert_username_index(db):
db.rollback()
db.commit()
+
+class Generator_R0(declarative_base()):
+ __tablename__ = "core__generators"
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode, nullable=False)
+ published = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ object_type = Column(Unicode, nullable=False)
+
+class ActivityIntermediator_R0(declarative_base()):
+ __tablename__ = "core__activity_intermediators"
+ id = Column(Integer, primary_key=True)
+ type = Column(Unicode, nullable=False)
+
+class Activity_R0(declarative_base()):
+ __tablename__ = "core__activities"
+ id = Column(Integer, primary_key=True)
+ actor = Column(Integer, ForeignKey(User.id), nullable=False)
+ published = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ verb = Column(Unicode, nullable=False)
+ content = Column(Unicode, nullable=True)
+ title = Column(Unicode, nullable=True)
+ generator = Column(Integer, ForeignKey(Generator_R0.id), nullable=True)
+ object = Column(Integer,
+ ForeignKey(ActivityIntermediator_R0.id),
+ nullable=False)
+ target = Column(Integer,
+ ForeignKey(ActivityIntermediator_R0.id),
+ nullable=True)
+
+@RegisterMigration(24, MIGRATIONS)
+def activity_migration(db):
+ """
+ Creates everything to create activities in GMG
+ - Adds Activity, ActivityIntermediator and Generator table
+ - Creates GMG service generator for activities produced by the server
+ - Adds the activity_as_object and activity_as_target to objects/targets
+ - Retroactively adds activities for what we can acurately work out
+ """
+ # Set constants we'll use later
+ FOREIGN_KEY = "core__activity_intermediators.id"
+ ACTIVITY_COLUMN = "activity"
+
+ # Create the new tables.
+ ActivityIntermediator_R0.__table__.create(db.bind)
+ Generator_R0.__table__.create(db.bind)
+ Activity_R0.__table__.create(db.bind)
+ db.commit()
+
+ # Initiate the tables we want to use later
+ metadata = MetaData(bind=db.bind)
+ user_table = inspect_table(metadata, "core__users")
+ activity_table = inspect_table(metadata, "core__activities")
+ generator_table = inspect_table(metadata, "core__generators")
+ collection_table = inspect_table(metadata, "core__collections")
+ media_entry_table = inspect_table(metadata, "core__media_entries")
+ media_comments_table = inspect_table(metadata, "core__media_comments")
+ ai_table = inspect_table(metadata, "core__activity_intermediators")
+
+
+ # Create the foundations for Generator
+ db.execute(generator_table.insert().values(
+ name="GNU Mediagoblin",
+ object_type="service",
+ published=datetime.datetime.now(),
+ updated=datetime.datetime.now()
+ ))
+ db.commit()
+
+ # Get the ID of that generator
+ gmg_generator = db.execute(generator_table.select(
+ generator_table.c.name==u"GNU Mediagoblin")).first()
+
+
+ # Now we want to modify the tables which MAY have an activity at some point
+ media_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
+ media_col.create(media_entry_table)
+
+ user_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
+ user_col.create(user_table)
+
+ comments_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
+ comments_col.create(media_comments_table)
+
+ collection_col = Column(ACTIVITY_COLUMN, Integer, ForeignKey(FOREIGN_KEY))
+ collection_col.create(collection_table)
+ db.commit()
+
+
+ # Now we want to retroactively add what activities we can
+ # first we'll add activities when people uploaded media.
+ # these can't have content as it's not fesible to get the
+ # correct content strings.
+ for media in db.execute(media_entry_table.select()):
+ # Now we want to create the intermedaitory
+ db_ai = db.execute(ai_table.insert().values(
+ type="media",
+ ))
+ db_ai = db.execute(ai_table.select(
+ ai_table.c.id==db_ai.inserted_primary_key[0]
+ )).first()
+
+ # Add the activity
+ activity = {
+ "verb": "create",
+ "actor": media.uploader,
+ "published": media.created,
+ "updated": media.created,
+ "generator": gmg_generator.id,
+ "object": db_ai.id
+ }
+ db.execute(activity_table.insert().values(**activity))
+
+ # Add the AI to the media.
+ db.execute(media_entry_table.update().values(
+ activity=db_ai.id
+ ).where(media_entry_table.c.id==media.id))
+
+ # Now we want to add all the comments people made
+ for comment in db.execute(media_comments_table.select()):
+ # Get the MediaEntry for the comment
+ media_entry = db.execute(
+ media_entry_table.select(
+ media_entry_table.c.id==comment.media_entry
+ )).first()
+
+ # Create an AI for target
+ db_ai_media = db.execute(ai_table.select(
+ ai_table.c.id==media_entry.activity
+ )).first().id
+
+ db.execute(
+ media_comments_table.update().values(
+ activity=db_ai_media
+ ).where(media_comments_table.c.id==media_entry.id))
+
+ # Now create the AI for the comment
+ db_ai_comment = db.execute(ai_table.insert().values(
+ type="comment"
+ )).inserted_primary_key[0]
+
+ activity = {
+ "verb": "comment",
+ "actor": comment.author,
+ "published": comment.created,
+ "updated": comment.created,
+ "generator": gmg_generator.id,
+ "object": db_ai_comment,
+ "target": db_ai_media,
+ }
+
+ # Now add the comment object
+ db.execute(activity_table.insert().values(**activity))
+
+ # Now add activity to comment
+ db.execute(media_comments_table.update().values(
+ activity=db_ai_comment
+ ).where(media_comments_table.c.id==comment.id))
+
+ # Create 'create' activities for all collections
+ for collection in db.execute(collection_table.select()):
+ # create AI
+ db_ai = db.execute(ai_table.insert().values(
+ type="collection"
+ ))
+ db_ai = db.execute(ai_table.select(
+ ai_table.c.id==db_ai.inserted_primary_key[0]
+ )).first()
+
+ # Now add link the collection to the AI
+ db.execute(collection_table.update().values(
+ activity=db_ai.id
+ ).where(collection_table.c.id==collection.id))
+
+ activity = {
+ "verb": "create",
+ "actor": collection.creator,
+ "published": collection.created,
+ "updated": collection.created,
+ "generator": gmg_generator.id,
+ "object": db_ai.id,
+ }
+
+ db.execute(activity_table.insert().values(**activity))
+
+ # Now add the activity to the collection
+ db.execute(collection_table.update().values(
+ activity=db_ai.id
+ ).where(collection_table.c.id==collection.id))
+
+ db.commit()
diff --git a/mediagoblin/db/mixin.py b/mediagoblin/db/mixin.py
index 1f2e7ec3..39690cfc 100644
--- a/mediagoblin/db/mixin.py
+++ b/mediagoblin/db/mixin.py
@@ -39,9 +39,12 @@ from mediagoblin.tools import common, licenses
from mediagoblin.tools.pluginapi import hook_handle
from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.url import slugify
+from mediagoblin.tools.translate import pass_to_ugettext as _
class UserMixin(object):
+ object_type = "person"
+
@property
def bio_html(self):
return cleaned_markdown_conversion(self.bio)
@@ -131,6 +134,11 @@ class MediaEntryMixin(GenerateSlugMixin):
return check_media_slug_used(self.uploader, slug, self.id)
@property
+ def object_type(self):
+ """ Converts media_type to pump-like type - don't use internally """
+ return self.media_type.split(".")[-1]
+
+ @property
def description_html(self):
"""
Rendered version of the description, run through
@@ -208,7 +216,7 @@ class MediaEntryMixin(GenerateSlugMixin):
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"]
)
@@ -297,6 +305,8 @@ class MediaEntryMixin(GenerateSlugMixin):
class MediaCommentMixin(object):
+ object_type = "comment"
+
@property
def content_html(self):
"""
@@ -321,6 +331,8 @@ class MediaCommentMixin(object):
class CollectionMixin(GenerateSlugMixin):
+ object_type = "collection"
+
def check_slug_used(self, slug):
# import this here due to a cyclic import issue
# (db.models -> db.mixin -> db.util -> db.models)
@@ -363,3 +375,111 @@ class CollectionItemMixin(object):
Run through Markdown and the HTML cleaner.
"""
return cleaned_markdown_conversion(self.note)
+
+class ActivityMixin(object):
+ object_type = "activity"
+
+ VALID_VERBS = ["add", "author", "create", "delete", "dislike", "favorite",
+ "follow", "like", "post", "share", "unfavorite", "unfollow",
+ "unlike", "unshare", "update", "tag"]
+
+ def get_url(self, request):
+ return request.urlgen(
+ "mediagoblin.federation.activity_view",
+ username=self.get_actor.username,
+ id=self.id,
+ qualified=True
+ )
+
+ def generate_content(self):
+ """ Produces a HTML content for object """
+ # some of these have simple and targetted. If self.target it set
+ # it will pick the targetted. If they DON'T have a targetted version
+ # the information in targetted won't be added to the content.
+ verb_to_content = {
+ "add": {
+ "simple" : _("{username} added {object}"),
+ "targetted": _("{username} added {object} to {target}"),
+ },
+ "author": {"simple": _("{username} authored {object}")},
+ "create": {"simple": _("{username} created {object}")},
+ "delete": {"simple": _("{username} deleted {object}")},
+ "dislike": {"simple": _("{username} disliked {object}")},
+ "favorite": {"simple": _("{username} favorited {object}")},
+ "follow": {"simple": _("{username} followed {object}")},
+ "like": {"simple": _("{username} liked {object}")},
+ "post": {
+ "simple": _("{username} posted {object}"),
+ "targetted": _("{username} posted {object} to {target}"),
+ },
+ "share": {"simple": _("{username} shared {object}")},
+ "unfavorite": {"simple": _("{username} unfavorited {object}")},
+ "unfollow": {"simple": _("{username} stopped following {object}")},
+ "unlike": {"simple": _("{username} unliked {object}")},
+ "unshare": {"simple": _("{username} unshared {object}")},
+ "update": {"simple": _("{username} updated {object}")},
+ "tag": {"simple": _("{username} tagged {object}")},
+ }
+
+ obj = self.get_object
+ target = self.get_target
+ actor = self.get_actor
+ content = verb_to_content.get(self.verb, None)
+
+ if content is None or obj is None:
+ return
+
+ if target is None or "targetted" not in content:
+ self.content = content["simple"].format(
+ username=actor.username,
+ object=obj.object_type
+ )
+ else:
+ self.content = content["targetted"].format(
+ username=actor.username,
+ object=obj.object_type,
+ target=target.object_type,
+ )
+
+ return self.content
+
+ def serialize(self, request):
+ obj = {
+ "id": self.id,
+ "actor": self.get_actor.serialize(request),
+ "verb": self.verb,
+ "published": self.published.isoformat(),
+ "updated": self.updated.isoformat(),
+ "content": self.content,
+ "url": self.get_url(request),
+ "object": self.get_object.serialize(request),
+ "objectType": self.object_type,
+ }
+
+ if self.generator:
+ obj["generator"] = self.get_generator.serialize(request)
+
+ if self.title:
+ obj["title"] = self.title
+
+ target = self.get_target
+ if target is not None:
+ obj["target"] = target.serialize(request)
+
+ return obj
+
+ def unseralize(self, data):
+ """
+ Takes data given and set it on this activity.
+
+ Several pieces of data are not written on because of security
+ reasons. For example changing the author or id of an activity.
+ """
+ if "verb" in data:
+ self.verb = data["verb"]
+
+ if "title" in data:
+ self.title = data["title"]
+
+ if "content" in data:
+ self.content = data["content"]
diff --git a/mediagoblin/db/models.py b/mediagoblin/db/models.py
index 1b700dce..0069c85a 100644
--- a/mediagoblin/db/models.py
+++ b/mediagoblin/db/models.py
@@ -36,7 +36,8 @@ from mediagoblin.db.extratypes import (PathTupleWithSlashes, JSONEncoded,
MutationDict)
from mediagoblin.db.base import Base, DictReadAttrProxy
from mediagoblin.db.mixin import UserMixin, MediaEntryMixin, \
- MediaCommentMixin, CollectionMixin, CollectionItemMixin
+ MediaCommentMixin, CollectionMixin, CollectionItemMixin, \
+ ActivityMixin
from mediagoblin.tools.files import delete_media_files
from mediagoblin.tools.common import import_component
@@ -71,6 +72,8 @@ class User(Base, UserMixin):
uploaded = Column(Integer, default=0)
upload_limit = Column(Integer)
+ activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
+
## TODO
# plugin data would be in a separate model
@@ -138,7 +141,7 @@ class User(Base, UserMixin):
"id": "acct:{0}@{1}".format(self.username, request.host),
"preferredUsername": self.username,
"displayName": "{0}@{1}".format(self.username, request.host),
- "objectType": "person",
+ "objectType": self.object_type,
"pump_io": {
"shared": False,
"followed": False,
@@ -309,6 +312,8 @@ class MediaEntry(Base, MediaEntryMixin):
media_metadata = Column(MutationDict.as_mutable(JSONEncoded),
default=MutationDict())
+ activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
+
## TODO
# fail_error
@@ -430,18 +435,13 @@ 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
context = {
"id": self.id,
"author": author.serialize(request),
- "objectType": self.objectType,
+ "objectType": self.object_type,
"url": self.url_for_self(request.urlgen),
"image": {
"url": request.host_url + self.thumb_url[1:],
@@ -458,7 +458,7 @@ class MediaEntry(Base, MediaEntryMixin):
"self": {
"href": request.urlgen(
"mediagoblin.federation.object",
- objectType=self.objectType,
+ object_type=self.object_type,
id=self.id,
qualified=True
),
@@ -477,14 +477,15 @@ class MediaEntry(Base, MediaEntryMixin):
context["license"] = self.license
if show_comments:
- comments = [comment.serialize(request) for comment in self.get_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,
+ object_type=self.object_type,
id=self.id,
qualified=True
),
@@ -650,13 +651,16 @@ class MediaComment(Base, MediaCommentMixin):
lazy="dynamic",
cascade="all, delete-orphan"))
+
+ activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
+
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",
+ "objectType": self.object_type,
"content": self.content,
"inReplyTo": media.serialize(request, show_comments=False),
"author": author.serialize(request)
@@ -714,6 +718,8 @@ class Collection(Base, CollectionMixin):
backref=backref("collections",
cascade="all, delete-orphan"))
+ activity = Column(Integer, ForeignKey("core__activity_intermediators.id"))
+
__table_args__ = (
UniqueConstraint('creator', 'slug'),
{})
@@ -1068,13 +1074,183 @@ class PrivilegeUserAssociation(Base):
ForeignKey(Privilege.id),
primary_key=True)
+class Generator(Base):
+ """ Information about what created an activity """
+ __tablename__ = "core__generators"
+
+ id = Column(Integer, primary_key=True)
+ name = Column(Unicode, nullable=False)
+ published = Column(DateTime, default=datetime.datetime.now)
+ updated = Column(DateTime, default=datetime.datetime.now)
+ object_type = Column(Unicode, nullable=False)
+
+ def __repr__(self):
+ return "<{klass} {name}>".format(
+ klass=self.__class__.__name__,
+ name=self.name
+ )
+
+ def serialize(self, request):
+ return {
+ "id": self.id,
+ "displayName": self.name,
+ "published": self.published.isoformat(),
+ "updated": self.updated.isoformat(),
+ "objectType": self.object_type,
+ }
+
+ def unserialize(self, data):
+ if "displayName" in data:
+ self.name = data["displayName"]
+
+
+class ActivityIntermediator(Base):
+ """
+ This is used so that objects/targets can have a foreign key back to this
+ object and activities can a foreign key to this object. This objects to be
+ used multiple times for the activity object or target and also allows for
+ different types of objects to be used as an Activity.
+ """
+ __tablename__ = "core__activity_intermediators"
+
+ id = Column(Integer, primary_key=True)
+ type = Column(Unicode, nullable=False)
+
+ TYPES = {
+ "user": User,
+ "media": MediaEntry,
+ "comment": MediaComment,
+ "collection": Collection,
+ }
+
+ def _find_model(self, obj):
+ """ Finds the model for a given object """
+ for key, model in self.TYPES.items():
+ if isinstance(obj, model):
+ return key, model
+
+ return None, None
+
+ def set(self, obj):
+ """ This sets itself as the activity """
+ key, model = self._find_model(obj)
+ if key is None:
+ raise ValueError("Invalid type of object given")
+
+ # We need to save so that self.id is populated
+ self.type = key
+ self.save()
+
+ # First set self as activity
+ obj.activity = self.id
+ obj.save()
+
+ def get(self):
+ """ Finds the object for an activity """
+ if self.type is None:
+ return None
+
+ model = self.TYPES[self.type]
+ return model.query.filter_by(activity=self.id).first()
+
+ def save(self, *args, **kwargs):
+ if self.type not in self.TYPES.keys():
+ raise ValueError("Invalid type set")
+ Base.save(self, *args, **kwargs)
+
+class Activity(Base, ActivityMixin):
+ """
+ This holds all the metadata about an activity such as uploading an image,
+ posting a comment, etc.
+ """
+ __tablename__ = "core__activities"
+
+ id = Column(Integer, primary_key=True)
+ actor = Column(Integer,
+ ForeignKey("core__users.id"),
+ nullable=False)
+ published = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ updated = Column(DateTime, nullable=False, default=datetime.datetime.now)
+ verb = Column(Unicode, nullable=False)
+ content = Column(Unicode, nullable=True)
+ title = Column(Unicode, nullable=True)
+ generator = Column(Integer,
+ ForeignKey("core__generators.id"),
+ nullable=True)
+ object = Column(Integer,
+ ForeignKey("core__activity_intermediators.id"),
+ nullable=False)
+ target = Column(Integer,
+ ForeignKey("core__activity_intermediators.id"),
+ nullable=True)
+
+ get_actor = relationship(User,
+ foreign_keys="Activity.actor", post_update=True)
+ get_generator = relationship(Generator)
+
+ def __repr__(self):
+ if self.content is None:
+ return "<{klass} verb:{verb}>".format(
+ klass=self.__class__.__name__,
+ verb=self.verb
+ )
+ else:
+ return "<{klass} {content}>".format(
+ klass=self.__class__.__name__,
+ content=self.content
+ )
+
+ @property
+ def get_object(self):
+ if self.object is None:
+ return None
+
+ ai = ActivityIntermediator.query.filter_by(id=self.object).first()
+ return ai.get()
+
+ def set_object(self, obj):
+ self.object = self._set_model(obj)
+
+ @property
+ def get_target(self):
+ if self.target is None:
+ return None
+
+ ai = ActivityIntermediator.query.filter_by(id=self.target).first()
+ return ai.get()
+
+ def set_target(self, obj):
+ self.target = self._set_model(obj)
+
+ def _set_model(self, obj):
+ # Firstly can we set obj
+ if not hasattr(obj, "activity"):
+ raise ValueError(
+ "{0!r} is unable to be set on activity".format(obj))
+
+ if obj.activity is None:
+ # We need to create a new AI
+ ai = ActivityIntermediator()
+ ai.set(obj)
+ ai.save()
+ return ai.id
+
+ # Okay we should have an existing AI
+ return ActivityIntermediator.query.filter_by(id=obj.activity).first().id
+
+ def save(self, set_updated=True, *args, **kwargs):
+ if set_updated:
+ self.updated = datetime.datetime.now()
+ super(Activity, self).save(*args, **kwargs)
+
MODELS = [
User, MediaEntry, Tag, MediaTag, MediaComment, Collection, CollectionItem,
MediaFile, FileKeynames, MediaAttachmentFile, ProcessingMetaData,
Notification, CommentNotification, ProcessingNotification, Client,
CommentSubscription, ReportBase, CommentReport, MediaReport, UserBan,
Privilege, PrivilegeUserAssociation,
- RequestToken, AccessToken, NonceTimestamp]
+ RequestToken, AccessToken, NonceTimestamp,
+ Activity, ActivityIntermediator, Generator]
"""
Foundations are the default rows that are created immediately after the tables
diff --git a/mediagoblin/federation/routing.py b/mediagoblin/federation/routing.py
index e9fa6252..2f8ed799 100644
--- a/mediagoblin/federation/routing.py
+++ b/mediagoblin/federation/routing.py
@@ -51,12 +51,12 @@ add_route(
# object endpoints
add_route(
"mediagoblin.federation.object",
- "/api/<string:objectType>/<string:id>",
+ "/api/<string:object_type>/<string:id>",
"mediagoblin.federation.views:object_endpoint"
)
add_route(
"mediagoblin.federation.object.comments",
- "/api/<string:objectType>/<string:id>/comments",
+ "/api/<string:object_type>/<string:id>/comments",
"mediagoblin.federation.views:object_comments"
)
@@ -82,4 +82,10 @@ add_route(
"mediagoblin.webfinger.whoami",
"/api/whoami",
"mediagoblin.federation.views:whoami"
+)
+
+add_route(
+ "mediagoblin.federation.activity_view",
+ "/<string:username>/activity/<string:id>",
+ "mediagoblin.federation.views:activity_view"
) \ No newline at end of file
diff --git a/mediagoblin/federation/views.py b/mediagoblin/federation/views.py
index 55d14e30..370ec8c3 100644
--- a/mediagoblin/federation/views.py
+++ b/mediagoblin/federation/views.py
@@ -20,11 +20,11 @@ import mimetypes
from werkzeug.datastructures import FileStorage
-from mediagoblin.decorators import oauth_required
+from mediagoblin.decorators import oauth_required, require_active_login
from mediagoblin.federation.decorators import user_has_privilege
-from mediagoblin.db.models import User, MediaEntry, MediaComment
+from mediagoblin.db.models import User, MediaEntry, MediaComment, Activity
from mediagoblin.tools.response import redirect, json_response, json_error, \
- render_to_response
+ render_404, render_to_response
from mediagoblin.meddleware.csrf import csrf_exempt
from mediagoblin.submit.lib import new_upload_entry, api_upload_request, \
api_add_to_feed
@@ -341,21 +341,8 @@ def feed_endpoint(request):
"items": [],
}
-
- # Look up all the media to put in the feed (this will be changed
- # when we get real feeds/inboxes/outboxes/activites)
- for media in MediaEntry.query.all():
- item = {
- "verb": "post",
- "object": media.serialize(request),
- "actor": media.get_uploader.serialize(request),
- "content": "{0} posted a picture".format(request.user.username),
- "id": media.id,
- }
- item["updated"] = item["object"]["updated"]
- item["published"] = item["object"]["published"]
- item["url"] = item["object"]["url"]
- feed["items"].append(item)
+ for activity in Activity.query.filter_by(actor=request.user.id):
+ feed["items"].append(activity.serialize(request))
feed["totalItems"] = len(feed["items"])
return json_response(feed)
@@ -363,7 +350,7 @@ def feed_endpoint(request):
@oauth_required
def object_endpoint(request):
""" Lookup for a object type """
- object_type = request.matchdict["objectType"]
+ object_type = request.matchdict["object_type"]
try:
object_id = int(request.matchdict["id"])
except ValueError:
@@ -395,17 +382,17 @@ def object_comments(request):
media = MediaEntry.query.filter_by(id=request.matchdict["id"]).first()
if media is None:
return json_error("Can't find '{0}' with ID '{1}'".format(
- request.matchdict["objectType"],
+ request.matchdict["object_type"],
request.matchdict["id"]
), 404)
- comments = response.serialize(request)
+ comments = media.serialize(request)
comments = comments.get("replies", {
"totalItems": 0,
"items": [],
"url": request.urlgen(
"mediagoblin.federation.object.comments",
- objectType=media.objectType,
+ object_type=media.object_type,
id=media.id,
qualified=True
)
@@ -555,3 +542,34 @@ def whoami(request):
)
return redirect(request, location=profile)
+
+@require_active_login
+def activity_view(request):
+ """ /<username>/activity/<id> - Display activity
+
+ This should display a HTML presentation of the activity
+ this is NOT an API endpoint.
+ """
+ # Get the user object.
+ username = request.matchdict["username"]
+ user = User.query.filter_by(username=username).first()
+
+ activity_id = request.matchdict["id"]
+
+ if request.user is None:
+ return render_404(request)
+
+ activity = Activity.query.filter_by(
+ id=activity_id,
+ author=user.id
+ ).first()
+ if activity is None:
+ return render_404(request)
+
+ return render_to_response(
+ request,
+ "mediagoblin/federation/activity.html",
+ {"activity": activity}
+ )
+
+
diff --git a/mediagoblin/submit/lib.py b/mediagoblin/submit/lib.py
index 637d5038..1813aa82 100644
--- a/mediagoblin/submit/lib.py
+++ b/mediagoblin/submit/lib.py
@@ -26,6 +26,7 @@ from werkzeug.datastructures import FileStorage
from mediagoblin import mg_globals
from mediagoblin.tools.response import json_response
from mediagoblin.tools.text import convert_to_tag_list_of_dicts
+from mediagoblin.tools.federation import create_activity
from mediagoblin.db.models import MediaEntry, ProcessingMetaData
from mediagoblin.processing import mark_entry_failed
from mediagoblin.processing.task import ProcessMedia
@@ -202,6 +203,10 @@ def submit_media(mg_app, user, submitted_file, filename,
add_comment_subscription(user, entry)
+ # Create activity
+ entry.activity = create_activity("post", entry, entry.uploader).id
+ entry.save()
+
return entry
@@ -291,4 +296,9 @@ def api_add_to_feed(request, entry):
run_process_media(entry, feed_url)
add_comment_subscription(request.user, entry)
+
+ # Create activity
+ entry.activity = create_activity("post", entry, entry.uploader).id
+ entry.save()
+
return json_response(entry.serialize(request))
diff --git a/mediagoblin/templates/mediagoblin/federation/activity.html b/mediagoblin/templates/mediagoblin/federation/activity.html
new file mode 100644
index 00000000..14377a48
--- /dev/null
+++ b/mediagoblin/templates/mediagoblin/federation/activity.html
@@ -0,0 +1,42 @@
+{#
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2014 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/>.
+#}
+{%- extends "mediagoblin/base.html" %}
+
+{% block mediagoblin_head %}
+ {% template_hook("media_head") %}
+{% endblock mediagoblin_head %}
+
+{% block mediagoblin_content %}
+<div class="media_pane eleven columns">
+ <h2 class="media_title">
+ {% if activity.title %}{{ activity.title }}{% endif %}
+ </h2>
+ {% autoescape False %}
+ <p> {{ activity.content }} </p>
+ {% endautoescape %}
+
+ <div class="media_sidebar">
+ {% block mediagoblin_after_added_sidebar %}
+ <a href="{{ activity.url(request) }}"
+ class="button_action"
+ id="button_reportmedia">
+ View {{ activity.get_object.object_type }}
+ </a>
+ {% endblock %}
+ </div>
+{% endblock %} \ No newline at end of file
diff --git a/mediagoblin/tools/federation.py b/mediagoblin/tools/federation.py
new file mode 100644
index 00000000..890e8801
--- /dev/null
+++ b/mediagoblin/tools/federation.py
@@ -0,0 +1,63 @@
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2014 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.db.models import Activity, Generator, User
+
+def create_activity(verb, obj, actor, target=None):
+ """
+ This will create an Activity object which for the obj if possible
+ and save it. The verb should be one of the following:
+ add, author, create, delete, dislike, favorite, follow
+ like, post, share, unfollow, unfavorite, unlike, unshare,
+ update, tag.
+
+ If none of those fit you might not want/need to create an activity for
+ the object. The list is in mediagoblin.db.models.Activity.VALID_VERBS
+ """
+ # exception when we try and generate an activity with an unknow verb
+ # could change later to allow arbitrary verbs but at the moment we'll play
+ # it safe.
+
+ if verb not in Activity.VALID_VERBS:
+ raise ValueError("A invalid verb type has been supplied.")
+
+ # This should exist as we're creating it by the migration for Generator
+ generator = Generator.query.filter_by(name="GNU MediaGoblin").first()
+ if generator is None:
+ generator = Generator(
+ name="GNU MediaGoblin",
+ object_type="service"
+ )
+ generator.save()
+
+ activity = Activity(verb=verb)
+ activity.set_object(obj)
+
+ if target is not None:
+ activity.set_target(target)
+
+ # If they've set it override the actor from the obj.
+ activity.actor = actor.id if isinstance(actor, User) else actor
+
+ activity.generator = generator.id
+ activity.save()
+
+ # Sigh want to do this prior to save but I can't figure a way to get
+ # around relationship() not looking up object when model isn't saved.
+ if activity.generate_content():
+ activity.save()
+
+ return activity
diff --git a/mediagoblin/user_pages/views.py b/mediagoblin/user_pages/views.py
index 1f0b9dcd..b6cbcabd 100644
--- a/mediagoblin/user_pages/views.py
+++ b/mediagoblin/user_pages/views.py
@@ -28,6 +28,7 @@ from mediagoblin.tools.response import render_to_response, render_404, \
from mediagoblin.tools.text import cleaned_markdown_conversion
from mediagoblin.tools.translate import pass_to_ugettext as _
from mediagoblin.tools.pagination import Pagination
+from mediagoblin.tools.federation import create_activity
from mediagoblin.user_pages import forms as user_forms
from mediagoblin.user_pages.lib import (send_comment_email,
add_media_to_collection, build_report_object)
@@ -201,7 +202,7 @@ def media_post_comment(request, media):
_('Your comment has been posted!'))
trigger_notification(comment, media, request)
-
+ create_activity("post", comment, comment.author, target=media)
add_comment_subscription(request.user, media)
return redirect_obj(request, media)
@@ -263,6 +264,7 @@ def media_collect(request, media):
collection.creator = request.user.id
collection.generate_slug()
collection.save()
+ create_activity("create", collection, collection.creator)
# Otherwise, use the collection selected from the drop-down
else:
@@ -289,7 +291,7 @@ def media_collect(request, media):
% (media.title, collection.title))
else: # Add item to collection
add_media_to_collection(collection, media, form.note.data)
-
+ create_activity("add", media, request.user, target=collection)
messages.add_message(request, messages.SUCCESS,
_('"%s" added to collection "%s"')
% (media.title, collection.title))