Beispiel #1
0
    def post(self, domain):
        logging.info('Got: %s', self.request.body)

        # parse and validate AS2 activity
        try:
            activity = json.loads(self.request.body)
            assert activity
        except (TypeError, ValueError, AssertionError):
            common.error(self, "Couldn't parse body as JSON", exc_info=True)

        type = activity.get('type')
        if type not in SUPPORTED_TYPES:
            common.error(self,
                         'Sorry, %s activities are not supported yet.' % type,
                         status=501)

        # TODO: verify signature if there is one

        # fetch actor if necessary so we have name, profile photo, etc
        if activity.get('type') in ('Like', 'Announce'):
            actor = activity.get('actor')
            if actor:
                activity['actor'] = common.get_as2(actor).json()

        # send webmentions to each target
        as1 = as2.to_as1(activity)
        common.send_webmentions(self,
                                as1,
                                protocol='activitypub',
                                source_as2=json.dumps(activity))
Beispiel #2
0
    def accept_follow(self, follow, follow_unwrapped):
        """Replies to an AP Follow request with an Accept request.

        Args:
          follow: dict, AP Follow activity
          follow_unwrapped: dict, same, except with redirect URLs unwrapped
        """
        logging.info('Replying to Follow with Accept')

        followee = follow.get('object')
        followee_unwrapped = follow_unwrapped.get('object')
        follower = follow.get('actor')
        if not followee or not followee_unwrapped or not follower:
            common.error(
                self,
                'Follow activity requires object and actor. Got: %s' % follow)

        inbox = follower.get('inbox')
        follower_id = follower.get('id')
        if not inbox or not follower_id:
            common.error(self, 'Follow actor requires id and inbox. Got: %s',
                         follower)

        # store Follower
        user_domain = util.domain_from_link(followee_unwrapped)
        Follower.get_or_create(user_domain,
                               follower_id,
                               last_follow=json.dumps(follow))

        # send AP Accept
        accept = {
            '@context':
            'https://www.w3.org/ns/activitystreams',
            'id':
            util.tag_uri(appengine_config.HOST,
                         'accept/%s/%s' % ((user_domain, follow.get('id')))),
            'type':
            'Accept',
            'actor':
            followee,
            'object': {
                'type': 'Follow',
                'actor': follower_id,
                'object': followee,
            }
        }
        resp = send(accept, inbox, user_domain)
        self.response.status_int = resp.status_code
        self.response.write(resp.text)

        # send webmention
        common.send_webmentions(self,
                                as2.to_as1(follow),
                                proxy=True,
                                protocol='activitypub',
                                source_as2=json.dumps(follow_unwrapped))
Beispiel #3
0
def slap(acct):
    """Accepts POSTs to /[ACCT]/salmon and converts to outbound webmentions."""
    # TODO: unify with activitypub
    body = request.get_data(as_text=True)
    logging.info(f'Got: {body}')

    try:
        parsed = utils.parse_magic_envelope(body)
    except ParseError as e:
        error('Could not parse POST body as XML', exc_info=True)
    data = parsed['data']
    logging.info(f'Decoded: {data}')

    # check that we support this activity type
    try:
        activity = atom.atom_to_activity(data)
    except ParseError as e:
        error('Could not parse envelope data as XML', exc_info=True)

    verb = activity.get('verb')
    if verb and verb not in SUPPORTED_VERBS:
        error(f'Sorry, {verb} activities are not supported yet.', status=501)

    # verify author and signature
    author = util.get_url(activity.get('actor'))
    if ':' not in author:
        author = f'acct:{author}'
    elif not author.startswith('acct:'):
        error(f'Author URI {author} has unsupported scheme; expected acct:')

    logging.info(f'Fetching Salmon key for {author}')
    if not magicsigs.verify(data, parsed['sig'], author_uri=author):
        error('Could not verify magic signature.')
    logging.info('Verified magic signature.')

    # Verify that the timestamp is recent. Required by spec.
    # I get that this helps prevent spam, but in practice it's a bit silly,
    # and other major implementations don't (e.g. Mastodon), so forget it.
    #
    # updated = utils.parse_updated_from_atom(data)
    # if not utils.verify_timestamp(updated):
    #     error('Timestamp is more than 1h old.')

    # send webmentions to each target
    activity = atom.atom_to_activity(data)
    common.send_webmentions(activity, protocol='ostatus', source_atom=data)
    return ''
Beispiel #4
0
    def post(self, domain):
        logging.info('Got: %s', self.request.body)

        # parse and validate AS2 activity
        try:
            activity = json.loads(self.request.body)
            assert activity
        except (TypeError, ValueError, AssertionError):
            common.error(self, "Couldn't parse body as JSON", exc_info=True)

        obj = activity.get('object') or {}
        if isinstance(obj, basestring):
            obj = {'id': obj}

        type = activity.get('type')
        if type == 'Accept':  # eg in response to a Follow
            return  # noop
        if type == 'Create':
            type = obj.get('type')
        elif type not in SUPPORTED_TYPES:
            common.error(self,
                         'Sorry, %s activities are not supported yet.' % type,
                         status=501)

        # TODO: verify signature if there is one

        # fetch actor if necessary so we have name, profile photo, etc
        for elem in obj, activity:
            actor = elem.get('actor')
            if actor and isinstance(actor, basestring):
                elem['actor'] = common.get_as2(actor).json()

        activity_unwrapped = common.redirect_unwrap(activity)
        if type == 'Follow':
            self.accept_follow(activity, activity_unwrapped)
            return

        # send webmentions to each target
        as1 = as2.to_as1(activity)
        common.send_webmentions(self,
                                as1,
                                proxy=True,
                                protocol='activitypub',
                                source_as2=json.dumps(activity_unwrapped))
Beispiel #5
0
    def post(self, username, domain):
        logging.info('Got: %s', self.request.body)

        try:
            parsed = utils.parse_magic_envelope(self.request.body)
        except ParseError as e:
            common.error(self, 'Could not parse POST body as XML', exc_info=True)
        data = utils.decode(parsed['data'])
        logging.info('Decoded: %s', data)

        # check that we support this activity type
        try:
            activity = atom.atom_to_activity(data)
        except ParseError as e:
            common.error(self, 'Could not parse envelope data as XML', exc_info=True)

        verb = activity.get('verb')
        if verb and verb not in SUPPORTED_VERBS:
            common.error(self, 'Sorry, %s activities are not supported yet.' % verb,
                         status=501)

        # verify author and signature
        author = util.get_url(activity.get('actor'))
        if ':' not in author:
            author = 'acct:%s' % author
        elif not author.startswith('acct:'):
            common.error(self, 'Author URI %s has unsupported scheme; expected acct:' % author)

        logging.info('Fetching Salmon key for %s' % author)
        if not magicsigs.verify(author, data, parsed['sig']):
            common.error(self, 'Could not verify magic signature.')
        logging.info('Verified magic signature.')

        # Verify that the timestamp is recent. Required by spec.
        # I get that this helps prevent spam, but in practice it's a bit silly,
        # and other major implementations don't (e.g. Mastodon), so forget it.
        #
        # updated = utils.parse_updated_from_atom(data)
        # if not utils.verify_timestamp(updated):
        #     common.error(self, 'Timestamp is more than 1h old.')

        # send webmentions to each target
        activity = atom.atom_to_activity(data)
        common.send_webmentions(self, activity, protocol='ostatus', source_atom=data)
Beispiel #6
0
def inbox(domain):
    """Accepts POSTs to /[DOMAIN]/inbox and converts to outbound webmentions."""
    body = request.get_data(as_text=True)
    logging.info(f'Got: {body}')

    # parse and validate AS2 activity
    try:
        activity = request.json
        assert activity
    except (TypeError, ValueError, AssertionError):
        error("Couldn't parse body as JSON", exc_info=True)

    obj = activity.get('object') or {}
    if isinstance(obj, str):
        obj = {'id': obj}

    type = activity.get('type')
    if type == 'Accept':  # eg in response to a Follow
        return ''  # noop
    if type == 'Create':
        type = obj.get('type')
    elif type not in SUPPORTED_TYPES:
        error('Sorry, %s activities are not supported yet.' % type, status=501)

    # TODO: verify signature if there is one

    if type == 'Undo' and obj.get('type') == 'Follow':
        # skip actor fetch below; we don't need it to undo a follow
        undo_follow(redirect_unwrap(activity))
        return ''
    elif type == 'Delete':
        id = obj.get('id')

        # !!! temporarily disabled actually deleting Followers below because
        # mastodon.social sends Deletes for every Bridgy Fed account, all at
        # basically the same time, and we have many Follower objects, so we
        # have to do this table scan for each one, so the requests take a
        # long time and end up spawning extra App Engine instances that we
        # get billed for. and the Delete requests are almost never for
        # followers we have. TODO: revisit this and do it right.

        # if isinstance(id, str):
        #     # assume this is an actor
        #     # https://github.com/snarfed/bridgy-fed/issues/63
        #     for key in Follower.query().iter(keys_only=True):
        #         if key.id().split(' ')[-1] == id:
        #             key.delete()
        return ''

    # fetch actor if necessary so we have name, profile photo, etc
    for elem in obj, activity:
        actor = elem.get('actor')
        if actor and isinstance(actor, str):
            elem['actor'] = common.get_as2(actor).json()

    activity_unwrapped = redirect_unwrap(activity)
    if type == 'Follow':
        return accept_follow(activity, activity_unwrapped)

    # send webmentions to each target
    as1 = as2.to_as1(activity)
    common.send_webmentions(as1,
                            proxy=True,
                            protocol='activitypub',
                            source_as2=json_dumps(activity_unwrapped))

    return ''