123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873 |
- # -*- coding: utf-8 -*-
- """
- (c) 2020 - Copyright ...
-
- Authors:
- zPlus <zplus@peers.community>
- """
- import blinker
- import celery
- import flask
- import functools
- import json
- import os
- import logging
- import pagure
- import pagure.config
- import rdflib
- import requests
- import requests_http_signature
- import urllib
- from . import APP_URL
- from . import activitypub
- from . import feeds
- from . import model
- from . import settings
- from . import tasks
- log = logging.getLogger(__name__)
- log.info('Initializing forgefed plugin...')
- # Pagure uses several ways to send out notification about events that have
- # occurred within the instance. We use "blinker" and subscribe to the pagure
- # signal. See pagure.lib.notify for more info.
- blinker.signal('pagure').connect(
- lambda sender, topic, message: tasks.notification.handle_pagure_signal.delay(topic, message),
- weak=False)
- # This Flask Blueprint will be imported by Pagure
- APP = flask.Blueprint('forgefed_ns', __name__, url_prefix='/',
- template_folder='templates')
- # TODO load Blueprint configuration from file
- APP.config = {}
- def requires_login(func):
- """
- A decorator for routes to check user login.
- """
-
- @functools.wraps(func)
- def decorator(*args, **kwargs):
- if not flask.g.authenticated:
- return ("", 401) # Unauthorized
-
- return func(*args, **kwargs)
-
- return decorator
- @APP.after_request
- def add_header(response):
- """
- Automatically set Content-Type header to all the Blueprint responses.
- # TODO Untangle this!
- """
-
- # Return default headers
- if flask.request.path.startswith('/federation'):
- return response
-
- if flask.request.path.startswith('/.well-known/host-meta'):
- response.headers['Content-Type'] = 'application/xrd+xml; charset=utf-8'
- elif flask.request.path.startswith('/.well-known/webfinger'):
- response.headers['Content-Type'] = 'application/jrd+json; charset=utf-8'
- else:
- response.headers['Content-Type'] = activitypub.default_header
-
- return response
- @APP.record
- def override_pagure_routes(setup_state):
- """
- We reuse Pagure routes in order to return ActivityPub objects in response
- to "Accept: application/ld+json" request headers. The "record" decorator
- registers a callback function that is called during initialization of the
- Blueprint by Flask. While Flask offers the app context during requests
- handling, the same context is not available during initialization.
- Therefore we need this callback which is called during Flask initialization
- in order to get the app context that we need to replace the Pagure views.
-
- See https://flask.palletsprojects.com/en/1.1.x/api/#flask.Blueprint.record
- for more info about the record() function/decorator.
-
- NOTE - If Flask accepted route dispatching based on headers value, we could
- just use something like @APP.route(accept='application/activity+json').
- But it does not, so we need to override the Pagure views.
- - ActivityPub requires some routes to exist for Actors, for example
- "inbox" and "followers". However, Pagure does not have suitable
- routes that we can reuse for this purpose. For this reason, those
- routes are defined directly on the Blueprint.
- """
-
- # Reference to the main Flask app created by Pagure and to which Blueprints
- # will be attached.
- pagure_app = setup_state.app
-
- # We add our templates folder to the app's jinja path, such that we can
- # override pagure templates.
- pagure_app.jinja_loader \
- .searchpath \
- .insert(0, os.path.join(os.path.abspath(os.path.dirname(__file__)), 'templates'))
-
- # Create a symlink to pagure templates folder. This is only used when
- # "extending" templates using {% extends "master.html" %}, because extending
- # a template with the same name will trigger an infinite recursion.
- pagure_path_symlink = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'templates/__pagure__')
- if not os.path.islink(pagure_path_symlink):
- os.symlink(os.path.join(settings.PAGURE_PATH, 'pagure/templates/'), pagure_path_symlink)
-
- """
- DEPRECATED using Pagure own database instead
-
- @pagure_app.before_request
- def start_database_session():
- # At the beginning of every request we get a new connection to the
- # forgefed graph store. Please note that this function is executed
- # before *every* request, for every route, including the ones
- # defined in the Blueprint.
-
- flask.g.forgefed = database.start_database_session()
-
- @pagure_app.after_request
- def do_something(response):
- return response
-
- @pagure_app.teardown_request
- def free_database_session(exception=None):
- # Close and remove the database session that was initiated in
- # @before_requrest. This instruction should be optional since the object
- # should be automatically garbage-collected when the request is destroyed.
-
- flask.g.forgefed.commit()
- flask.g.forgefed.remove()
- """
-
- def pagure_route(endpoint):
- """
- This function returns a decorator whose job is to replace a Pagure
- view with another one that will check the HTTP "Accept" header. If the
- HTTP request is a ActivityPub one the ForgeFed plugin will take care of it,
- otherwise it will just pass through the control to the Pagure view.
-
- Additional documentation useful for this decorator: https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.view_functions
- """
-
- def decorator(forgefed_view):
- # The Flask object "pagure_app.view_functions" contains all the
- # views defined by Pagure.
- pagure_view = pagure_app.view_functions[endpoint]
-
- # https://docs.python.org/3.9/library/functools.html#functools.wraps
- @functools.wraps(forgefed_view)
- def wrapper(*args, **kwargs):
- if 'Accept' in flask.request.headers:
- # HTTP headers can contain multiple values separated by a comma
- request_headers = [
- value.strip(' ')
- for value
- in flask.request.headers.get('Accept').split(',') ]
-
- if any(header in activitypub.headers for header in request_headers):
- response = flask.make_response(forgefed_view(*args, **kwargs))
- response.headers['Content-Type'] = activitypub.default_header
- return response
-
- # If it's not an ActivityPub request, just fall through to the
- # Pagure default view.
- return pagure_view(*args, **kwargs)
- # Replace the pagure view with our own
- pagure_app.view_functions[endpoint] = wrapper
- return wrapper
-
- return decorator
-
- ###########################################################################
- # Person
- ###########################################################################
-
- @pagure_route('ui_ns.view_user')
- def person(*args, **kwargs):
- """
- Return a Person object from the Pagure user page.
- """
-
- # Retrieve path arguments
- username = kwargs.get('username')
-
- actor = flask.g.session \
- .query(model.Person) \
- .filter(model.Person.user == username) \
- .one_or_none()
-
- if not actor:
- return ({}, 404)
-
- return actor.jsonld
-
- ###########################################################################
- # Project
- ###########################################################################
-
- @pagure_route('ui_ns.view_repo')
- def project(*args, **kwargs):
- """
- Return a Project object from the project page.
- """
-
- # Retrieve path arguments
- username = kwargs.get('username')
- repo = kwargs.get('repo')
- namespace = kwargs.get('namespace')
-
- repository = pagure.lib.query.get_authorized_project(
- flask.g.session, repo, user=username, namespace=namespace)
-
- actor = flask.g.session.query(model.Project) \
- .filter(model.Project.id == repository.id) \
- .one_or_none()
-
- if not repository:
- return ({}, 404)
-
- return actor.jsonld
- ###########################################################################
- # Repository
- #
- # - Pagure uses 4 kind of URLs for repositories.
- # - The Pagure app defines a Flask before_request() function that does a
- # lot of things, among which to check if there is a "repo" variable in
- # the URL and in turn setup some context for the request. This happens
- # for *every* request, regardless if it's about a repository or not. The
- # rationale is that they rather do it this way than using a separate
- # decorator for all the repositories views, because almost all requests
- # are about repositories anyway. before_request() will then automatically
- # return 404 if a repository does not exist, so we don't need to check
- # that here, unlike what we did in person(*args, **kwargs), because these
- # views will never be executed.
- # - The Pagure before_request() retrieves the repository using the function
- # pagure.lib.query.get_authorized_project() which checks for
- # authorization and, if the repo is private, it returns 404. So there
- # should be no need to check for authorization here.
- ###########################################################################
-
- @pagure_route('ui_ns.view_repo_git')
- def repository(repo, username=None, namespace=None, *args, **kwargs):
- """
- Return a Repository actor object.
- """
-
- project = pagure.lib.query.get_authorized_project(
- flask.g.session, repo, user=username, namespace=namespace)
-
- actor = flask.g.session.query(model.Repository) \
- .filter(model.Repository.id == project.id) \
- .one_or_none()
-
- if not actor:
- return ({}, 404)
-
- return actor.jsonld
-
- ###########################################################################
- # Bug tracker
- ###########################################################################
-
- @pagure_route('ui_ns.view_issues')
- def bugtracker(*args, **kwargs):
- username = kwargs.get('username')
- namespace = kwargs.get('namespace')
- repo = kwargs.get('repo')
-
- project = pagure.lib.query.get_authorized_project(
- flask.g.session, repo, user=username, namespace=namespace)
-
- if not project:
- return ({}, 404)
-
- actor = flask.g.session.query(model.BugTracker) \
- .filter(model.BugTracker.id == project.id) \
- .one_or_none()
-
- if not actor:
- return ({}, 404)
-
- return actor.jsonld
-
- ###########################################################################
- # Tickets
- ###########################################################################
-
- @pagure_route('ui_ns.view_issue')
- def ticket(*args, **kwargs):
- username = kwargs.get('username')
- namespace = kwargs.get('namespace')
- repo = kwargs.get('repo')
- issue_id = kwargs.get('issueid')
-
- repository = pagure.lib.query.get_authorized_project(
- flask.g.session, repo, user=username, namespace=namespace)
-
- if not repository:
- return ({}, 404)
-
- ticket = flask.g.session.query(model.Ticket) \
- .filter(model.Ticket.id == issue_id) \
- .filter(model.Ticket.project_id == repository.id) \
- .one_or_none()
-
- if not ticket:
- return ({}, 404)
-
- return ticket.jsonld
-
- log.info('forgefed plugin registered by Flask.')
- ###############################################################################
- # WebFinger
- #
- # This is primarily used to support other ActivityPub software such as
- # Mastodon that relies on webfinger as a discovery protocol because users
- # use @username@domain when mentioning other users.
- # https://docs.joinmastodon.org/spec/webfinger/
- ###############################################################################
- @APP.route('/.well-known/host-meta', methods=['GET'])
- def host_meta():
- return flask.render_template('host-meta.xml', APP_URL=APP_URL)
- @APP.route('/.well-known/webfinger/<path:uri>', methods=['GET'])
- def webfinger_resource(uri):
- """
- Return the webfinger info for account "uri".
-
- :param uri: The "acct:userpard@host" to search.
- """
-
- # Only support acct: resources
- # Do we need to support other schemes? WebFinger is neutral regarding the
- # scheme of URI: it could be "acct", "http", "https", "mailto", or some
- # other scheme, but other AcitivityPub instances such as Mastodon only
- # use "acct".
- if not uri.startswith('acct:'):
- return ({}, 404)
-
- # The "acct" scheme is defined in the spec as
- # "acct" ":" userpart "@" host
- # "host" is the domain where the account is hosted
- # "userpart" contains the localinfo used by the host to retrieve the account
- userpart, host = uri[5:].rsplit('@', 1)
-
- # Now we find the actual actor's URI.
- # The "userpart" is basically the "preferredUsername" property defined in
- # model.py
- if userpart.startswith('project/'):
- actor_uri = '{}/{}'.format(APP_URL, userpart[8:])
- else:
- actor_uri = '{}/user/{}'.format(APP_URL, userpart)
-
- return flask.render_template('webfinger.json', subject=uri, actor_uri=actor_uri)
- ###############################################################################
- # Routes used to interact with remote objects of the federation, when we
- # cannot reuse another pagure route.
- ###############################################################################
- @APP.route('/federation', methods=['GET'])
- @requires_login
- def federation():
- person = flask.g.session \
- .query(model.Person) \
- .filter(model.Person.id == flask.g.fas_user.id) \
- .one_or_none()
-
- if not person:
- return flask.redirect('/')
-
- # Retrieve feeds of the current user
- items = flask.g.session \
- .query(model.Feed) \
- .filter(model.Feed.actor_uri == person.local_uri) \
- .order_by(model.Feed.created.desc()) \
- .all()
-
- # A feed "content" property contains a serialized Python dictionary.
- # We convert strings back to dictionary before rendering the template.
- feed_items = [
- { 'created': feed.created, 'content': json.loads(feed.content) }
- for feed in items
- ]
-
- return flask.render_template('federation/feeds.html', feeds=feed_items)
- @APP.route('/federation/activity/<id>', methods=['GET'])
- def federation_activity(id):
- """
- Return the JSON representation of Activities created by this instance.
- """
-
- resource = flask.g.session \
- .query(model.Resource) \
- .filter(model.Resource.uri == '{}/federation/activity/{}'.format(APP_URL, id)) \
- .one_or_none()
-
- if not resource:
- abort(404, 'Activity not found.')
- return
-
- activity = json.loads(resource.document)
-
- return activity
- @APP.route('/federation/followers', methods=['GET'])
- @requires_login
- def federation_followers():
- person = flask.g.session \
- .query(model.Person) \
- .filter(model.Person.id == flask.g.fas_user.id) \
- .one_or_none()
-
- if not person:
- return flask.redirect(url_for('forgefed_ns.federation'))
-
- # Retrieve "following" for the current user
- items = flask.g.session \
- .query(model.Collection, model.Resource) \
- .join(model.Resource, model.Collection.item == model.Resource.uri) \
- .filter(model.Collection.uri == person.followers_uri) \
- .all()
-
- items = [ json.loads(cache.document) for collection, cache in items ]
-
- return flask.render_template('federation/followers.html', followers=items)
- @APP.route('/federation/following', methods=['GET'])
- @requires_login
- def federation_following():
- person = flask.g.session \
- .query(model.Person) \
- .filter(model.Person.id == flask.g.fas_user.id) \
- .one_or_none()
-
- if not person:
- return flask.redirect(url_for('forgefed_ns.federation'))
-
- # Retrieve "following" for the current user
- items = flask.g.session \
- .query(model.Collection, model.Resource) \
- .join(model.Resource, model.Collection.item == model.Resource.uri) \
- .filter(model.Collection.uri == person.following_uri) \
- .all()
-
- items = [ json.loads(cache.document) for collection, cache in items ]
-
- return flask.render_template('federation/following.html', following=items)
- @APP.route('/federation/search', methods=['GET', 'POST'])
- @requires_login
- def federation_search():
- uri = flask.request.args.get('uri')
- search_result = None
-
- # Search for an object
- if uri:
- search_result = activitypub.fetch(uri)
-
- return flask.render_template(
- 'federation/search.html',
- uri=uri,
- search_result=search_result)
- @APP.route('/federation/follow', methods=['GET'])
- @requires_login
- def federation_follow():
- remote_actor_uri = flask.request.args.get('actor_uri')
-
- # The user that clicked the "Follow" button
- person = flask.g.session \
- .query(model.Person) \
- .filter(model.Person.id == flask.g.fas_user.id) \
- .one_or_none()
-
- if not person:
- return flask.redirect(url_for('forgefed_ns.federation'))
-
- remote_actor = activitypub.fetch(remote_actor_uri)
-
- if not remote_actor:
- raise Exception('Could not fetch remote actor.')
-
- log.debug('Remote Actor {} fetched.'.format(remote_actor_uri))
-
- # We cache a copy of the remote actor for quick lookups. This is used for
- # example when listing "following/followers" collections. If we only
- # stored the actors' URI we would need to GET the JSON data every time.
- flask.g.session.merge(model.Resource(
- uri = remote_actor_uri,
- document = json.dumps(remote_actor)))
-
- # Add the remote actor to the "following" collection
- flask.g.session.merge(model.Collection(
- uri = person.following_uri,
- item = remote_actor_uri))
-
- # Add a feed
- flask.g.session.add(model.Feed(
- actor_uri = person.local_uri,
- content = json.dumps(feeds.follow(person.jsonld, remote_actor))
- ))
-
- # Now we can create an Activity to notify remote actors
- activity = activitypub.Follow(
- actor = person.uri,
- object = remote_actor_uri,
- to = remote_actor_uri)
-
- activity.distribute()
-
- flask.g.session.commit()
-
- return flask.redirect(flask.url_for('forgefed_ns.federation_following'))
- @APP.route('/federation/submit_ticket', methods=['GET'])
- @requires_login
- def federation_submit_ticket():
- # The URL of the remote tracker (actor)
- actor_uri = flask.request.args.get('actor_uri')
-
- if not actor_uri:
- return flask.redirect(flask.url_for('ui_ns.index'))
-
- # Retrieve the remote actor
- actor = activitypub.fetch(actor_uri)
-
- # Create a local user for the project because in Pagure a project must
- # have a user ID
- user = model.test_or_set_remote_actor(
- pagure_db=flask.g.session,
- uri=actor_uri)
-
- project = flask.g.session \
- .query(model.Project) \
- .filter(model.Project.name == user.username) \
- .one_or_none()
-
- # Check if we already have a local tracker for contributing to the remote
- # tracker. If not, created it.
- if not project:
- # Create the project in the Pagure database.
- # This function will create the item in the database, and then will
- # start a new async task to create the actual .git folder.
- task = pagure.lib.query.new_project(
- flask.g.session,
- user=user.username,
- name=user.username,
- blacklist=[],
- allowed_prefix=[],
- repospanner_region=None,
- description="Remote",
- url=actor_uri,
- avatar_email=None,
- parent_id=None,
- add_readme=False,
- mirrored_from=None,
- userobj=user,
- prevent_40_chars=False,
- namespace=None,
- user_ns=False,
- ignore_existing_repo=False,
- private=False,
- )
-
- # The tracker that we've just created is only used to interact with a
- # remote one. It's not a "real" local tracker by a local user, so we
- # create a owl:sameAs relation in the graph.
- #tracker_url = '{}/{}/issues'.format(APP_URL, user.username)
- project = flask.g.session \
- .query(model.Project) \
- .filter(model.Project.name == user.username) \
- .one_or_none()
-
- flask.g.session.add(model.SameAs(
- local_uri = project.local_uri,
- remote_uri = actor_uri))
-
- #return pagure.utils.wait_for_task(task)
-
- # Redirect to the "new issue" page of the local tracker, where the user can
- # create a new issue for the remote tracker.
- return flask.redirect(flask.url_for('ui_ns.new_issue', repo=user.username))
- @APP.route('/federation/ticket/<issue_uid>/comments', methods=['GET'])
- def federation_ticket_comments(issue_uid):
- """
- Return the Collection containing a ticket's comments.
- This route exists because pagure does not have any route defined for
- comments. The path of a comment in pagure is "/<repo>/issue/<issueid>#comment-<commend_id>"
- but the trailing part of the URL after the # symbol is not sent to the server.
-
- :param issue_uid: The unique ID defined by pagure during ticket creation. This
- is used instead of the default key which is (project_id, issue_id).
-
- :param page: The page of the collection to show.
- """
-
- collection_uri = APP_URL + flask.request.path
-
- return model.OrderedCollection(collection_uri).jsonld
- @APP.route('/federation/ticket/<issue_uid>/comments/<int:page>', methods=['GET'])
- def federation_ticket_comments_page(issue_uid):
- """
- Return the CollectionPage containing a ticket's comments.
-
- :param issue_uid: The unique ID defined by pagure during ticket creation. This
- is used instead of the default key which is (project_id, issue_id).
-
- :param page: The page of the collection to show.
- """
-
- page_uri = APP_URL + flask.request.path
- collection_uri = page_uri.rsplit('/', 1)[0]
-
- # Retrieve items
- items = flask.g.session \
- .query(model.TicketComment) \
- .filter(model.TicketComment.issue_uid == issue_uid) \
- .offset(settings.COLLECTION_SIZE * page) \
- .limit(settings.COLLECTION_SIZE) \
- .all()
-
- items_ids = [ result.local_uri for result in items ]
-
- return {
- '@context': activitypub.jsonld_context,
- 'type': 'OrderedCollectionPage',
- 'id': page_uri,
- 'partOf': collection_uri,
- 'orderedItems': items_ids }
- @APP.route('/federation/ticket_comment/<comment_id>', methods=['GET'])
- def federation_ticket_comment(comment_id):
- """
- Return the JSONLD of a ticket comment.
-
- :param comment_id: The comment ID defined by pagure during comment creation.
- This is the default primary key of the comment and is unique across all
- local issues.
- """
-
- return flask.g.session \
- .query(model.TicketComment) \
- .filter(model.TicketComment.id == comment_id) \
- .one_or_none() \
- .jsonld
- ###############################################################################
- # Actor
- ###############################################################################
- @APP.route('/<path:actor>/key.pub', methods=['GET'])
- def actor_key(actor):
- """
- This object represents the public GPG key used to sign HTTP requests.
- """
-
- actor_path = '/' + actor
- actor_uri = APP_URL + '/' + actor
- actor = model.from_path(flask.g.session, actor_path)
- key_uri = APP_URL + flask.request.path
-
- if not actor:
- return ({}, 404)
-
- # Create key if it doesn't exist
- model.GpgKey.test_or_set(flask.g.session, actor_uri, key_uri)
-
- # Get the key
- key = flask.g.session.query(model.GpgKey) \
- .filter(model.GpgKey.uri == key_uri) \
- .one_or_none()
-
- return key.jsonld
- @APP.route('/<path:actor>/inbox', methods=['GET'])
- def actor_inbox(actor):
- """
- Returns an Actor's INBOX.
- This should only be called by the Actors who want to read their own INBOX.
- """
-
- return ({}, 501) # 501 Not Implemented. TODO implement C2S.
- @APP.route('/<path:actor>/outbox', methods=['GET'])
- def actor_outbox(actor, page=None):
- """
- Returns an Actor's OUTBOX.
- This should only be called by the Actors who want to read their own OUTBOX.
- """
-
- # TODO Show only "Public" OUTBOX
- # https://www.w3.org/TR/activitypub/#public-addressing
-
- return ({}, 501) # 501 Not Implemented. TODO implement C2S.
- @APP.route('/<path:actor>/inbox', methods=['POST'])
- def actor_receive(actor):
- """
- Somebody is sending a message to an actor's INBOX.
- """
-
- # TODO
- # Verify incoming request signature. Check if the HTTP POST request is
- # signed correctly.
- """
- def key_resolver(key_id, algorithm):
- return remote actor public key
-
- try:
- requests_http_signature.HTTPSignatureAuth.verify(
- flask.request,
- key_resolver=key_resolver)
- except Exception:
- return ({}, 401)
- """
-
- actor_path = '/' + actor
- actor_uri = APP_URL + '/' + actor
- actor = model.from_path(flask.g.session, actor_path)
-
- if not actor:
- return ({}, 404)
-
- # Retrieve the ActivityPub Activity from the HTTP request body. The
- # Activity is expected to be a JSON object.
- # https://flask.palletsprojects.com/en/1.1.x/api/#flask.Request.get_json
- activity = flask.request.get_json()
-
- # Schedule a task to process the incoming activity asynchronously
- tasks.delivery.validate_incoming_activity.delay(actor_uri, activity)
-
- return ({}, 202) # 202 Accepted
- @APP.route('/<path:actor>/outbox', methods=['POST'])
- def actor_send(actor):
- """
- An Actor is trying to POST an Activity to its OUTBOX.
- This should only be called by an Actor's client that want to send out a new
- Activity.
- """
-
- return ({}, 501) # 501 Not Implemented. TODO implement C2S.
- @APP.route('/<path:actor>/followers', methods=['GET'])
- def actor_followers(actor):
- """
- Show the followers of an actor.
- """
-
- actor_path = '/' + actor
- actor_uri = APP_URL + '/' + actor
- collection_uri = APP_URL + flask.request.path
- actor = model.from_path(flask.g.session, actor_path)
-
- if not actor:
- return ({}, 404)
-
- return {
- '@context': activitypub.jsonld_context,
- 'id': collection_uri,
- 'type': 'OrderedCollection',
- 'current': '{}/{}'.format(collection_uri, 0),
- 'first': '{}/{}'.format(collection_uri, 0),
- 'last': '{}/{}'.format(collection_uri, 0),
- 'totalItems': 0
- }
- @APP.route('/<path:actor>/followers/<int:page>', methods=['GET'])
- def actor_followers_page(actor, page):
- """
- Show the followers of an actor.
- """
-
- page_uri = APP_URL + flask.request.path
- collection_uri = page_uri.rsplit('/', 1)[0]
-
- # Retrieve items
- items = flask.g.session \
- .query(model.Collection) \
- .filter(model.Collection.uri == collection_uri) \
- .order_by(model.Collection.added.desc()) \
- .offset(settings.COLLECTION_SIZE * page) \
- .limit(settings.COLLECTION_SIZE) \
- .all()
-
- items_ids = [ result.item for result in items ]
-
- return {
- '@context': activitypub.jsonld_context,
- 'type': 'OrderedCollectionPage',
- 'id': page_uri,
- 'partOf': collection_uri,
- 'orderedItems': items_ids }
- @APP.route('/<path:actor>/following', methods=['GET'])
- def actor_following(actor):
- """
- Show the actors that an actor is following.
- """
-
- actor_path = '/' + actor
- actor_uri = APP_URL + '/' + actor
- collection_uri = APP_URL + flask.request.path
- actor = model.from_path(flask.g.session, actor_path)
-
- if not actor:
- return ({}, 404)
-
- return {
- '@context': activitypub.jsonld_context,
- 'id': collection_uri,
- 'type': 'OrderedCollection',
- 'current': '{}/{}'.format(collection_uri, 0),
- 'first': '{}/{}'.format(collection_uri, 0),
- 'last': '{}/{}'.format(collection_uri, 0),
- 'totalItems': 0
- }
- @APP.route('/<path:actor>/following/<int:page>', methods=['GET'])
- def actor_following_page(actor, page):
- """
- Show the actors that an actor is following.
- """
-
- page_uri = APP_URL + flask.request.path
- collection_uri = page_uri.rsplit('/', 1)[0]
-
- # Retrieve items
- items = flask.g.session \
- .query(model.Collection) \
- .filter(model.Collection.uri == collection_uri) \
- .order_by(model.Collection.added.desc()) \
- .offset(settings.COLLECTION_SIZE * page) \
- .limit(settings.COLLECTION_SIZE) \
- .all()
-
- items_ids = [ result.item for result in items ]
-
- return {
- '@context': activitypub.jsonld_context,
- 'type': 'OrderedCollectionPage',
- 'id': page_uri,
- 'partOf': collection_uri,
- 'orderedItems': items_ids }
- log.info('forgefed plugin initialized.')
|