Пример #1
0
def render():
    """Fetches a stored Response and renders it as HTML."""
    source = flask_util.get_required_param('source')
    target = flask_util.get_required_param('target')

    id = f'{source} {target}'
    resp = Response.get_by_id(id)
    if not resp:
        error(f'No stored response for {id}', status=404)

    if resp.source_mf2:
        as1 = microformats2.json_to_object(json_loads(resp.source_mf2))
    elif resp.source_as2:
        as1 = as2.to_as1(json_loads(resp.source_as2))
    elif resp.source_atom:
        as1 = atom.atom_to_activity(resp.source_atom)
    else:
        error(f'Stored response for {id} has no data', status=404)

    # add HTML meta redirect to source page. should trigger for end users in
    # browsers but not for webmention receivers (hopefully).
    html = microformats2.activities_to_html([as1])
    utf8 = '<meta charset="utf-8">'
    refresh = f'<meta http-equiv="refresh" content="0;url={source}">'
    return html.replace(utf8, utf8 + '\n' + refresh)
Пример #2
0
    def get(self):
        source = util.get_required_param(self, 'source')
        target = util.get_required_param(self, 'target')

        id = '%s %s' % (source, target)
        resp = Response.get_by_id(id)
        if not resp:
            self.abort(404, 'No stored response for %s' % id)

        if resp.source_mf2:
            as1 = microformats2.json_to_object(json.loads(resp.source_mf2))
        elif resp.source_as2:
            as1 = as2.to_as1(json.loads(resp.source_as2))
        elif resp.source_atom:
            as1 = atom.atom_to_activity(resp.source_atom)
        else:
            self.abort(404, 'Stored response for %s has no data' % id)

        # add HTML meta redirect to source page. should trigger for end users in
        # browsers but not for webmention receivers (hopefully).
        html = microformats2.activities_to_html([as1])
        utf8 = '<meta charset="utf-8">'
        refresh = '<meta http-equiv="refresh" content="0;url=%s">' % source
        html = html.replace(utf8, utf8 + '\n' + refresh)

        self.response.write(html)
Пример #3
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))
Пример #4
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))
Пример #5
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):
            self.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:
            self.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
            return self.undo_follow(self.redirect_unwrap(activity))
        elif type == 'Delete':
            id = obj.get('id')
            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 = self.redirect_unwrap(activity)
        if type == 'Follow':
            return self.accept_follow(activity, activity_unwrapped)

        # send webmentions to each target
        as1 = as2.to_as1(activity)
        self.send_webmentions(as1,
                              proxy=True,
                              protocol='activitypub',
                              source_as2=json_dumps(activity_unwrapped))
Пример #6
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))
Пример #7
0
    def get(self):
        source = util.get_required_param(self, 'source')
        target = util.get_required_param(self, 'target')

        id = '%s %s' % (source, target)
        resp = Response.get_by_id(id)
        if not resp:
            self.abort(404, 'No stored response for %s' % id)

        if resp.source_mf2:
            as1 = microformats2.json_to_object(json.loads(resp.source_mf2))
        elif resp.source_as2:
            as1 = as2.to_as1(json.loads(resp.source_as2))
        elif resp.source_atom:
            as1 = atom.atom_to_activity(resp.source_atom)
        else:
            self.abort(404, 'Stored response for %s has no data' % id)

        self.response.write(microformats2.activities_to_html([as1]))
Пример #8
0
 def test_to_as1_blank(self):
   self.assertEqual({}, as2.to_as1(None))
   self.assertEqual({}, as2.to_as1({}))
Пример #9
0
    def get(self):
        input = util.get_required_param(self, 'input')
        if input not in INPUTS:
            raise exc.HTTPBadRequest('Invalid input: %s, expected one of %r' %
                                     (input, INPUTS))
        url, body = self._fetch(util.get_required_param(self, 'url'))

        # decode data
        if input in ('activitystreams', 'as1', 'as2', 'mf2-json', 'json-mf2',
                     'jsonfeed'):
            try:
                body_json = json.loads(body)
                body_items = (body_json if isinstance(body_json, list) else
                              body_json.get('items') or [body_json])
            except (TypeError, ValueError):
                raise exc.HTTPBadRequest('Could not decode %s as JSON' % url)

        mf2 = None
        if input == 'html':
            mf2 = mf2py.parse(doc=body, url=url)
        elif input in ('mf2-json', 'json-mf2'):
            mf2 = body_json
            mf2.setdefault('rels', {})  # mf2util expects rels

        actor = None
        title = None
        if mf2:

            def fetch_mf2_func(url):
                if util.domain_or_parent_in(
                        urlparse.urlparse(url).netloc, SILO_DOMAINS):
                    return {
                        'items': [{
                            'type': ['h-card'],
                            'properties': {
                                'url': [url]
                            }
                        }]
                    }
                _, doc = self._fetch(url)
                return mf2py.parse(doc=doc, url=url)

            try:
                actor = microformats2.find_author(
                    mf2, fetch_mf2_func=fetch_mf2_func)
                title = microformats2.get_title(mf2)
            except (KeyError, ValueError) as e:
                raise exc.HTTPBadRequest('Could not parse %s as %s: %s' %
                                         (url, input, e))

        if input in ('as1', 'activitystreams'):
            activities = body_items
        elif input == 'as2':
            activities = [as2.to_as1(obj) for obj in body_items]
        elif input == 'atom':
            try:
                activities = atom.atom_to_activities(body)
            except ElementTree.ParseError as e:
                raise exc.HTTPBadRequest('Could not parse %s as XML: %s' %
                                         (url, e))
            except ValueError as e:
                raise exc.HTTPBadRequest('Could not parse %s as Atom: %s' %
                                         (url, e))
        elif input == 'html':
            activities = microformats2.html_to_activities(body, url, actor)
        elif input in ('mf2-json', 'json-mf2'):
            activities = [
                microformats2.json_to_object(item, actor=actor)
                for item in mf2.get('items', [])
            ]
        elif input == 'jsonfeed':
            try:
                activities, actor = jsonfeed.jsonfeed_to_activities(body_json)
            except ValueError as e:
                logging.exception('jsonfeed_to_activities failed')
                raise exc.HTTPBadRequest('Could not parse %s as JSON Feed' %
                                         url)

        self.write_response(
            source.Source.make_activities_base_response(activities),
            url=url,
            actor=actor,
            title=title)
Пример #10
0
    def get(self):
        input = util.get_required_param(self, 'input')
        if input not in INPUTS:
            raise exc.HTTPBadRequest('Invalid input: %s, expected one of %r' %
                                     (input, INPUTS))

        orig_url = util.get_required_param(self, 'url')
        fragment = urllib.parse.urlparse(orig_url).fragment
        if fragment and input != 'html':
            raise exc.HTTPBadRequest(
                'URL fragments only supported with input=html.')

        resp = util.requests_get(orig_url, gateway=True)
        final_url = resp.url

        # decode data
        if input in ('activitystreams', 'as1', 'as2', 'mf2-json', 'json-mf2',
                     'jsonfeed'):
            try:
                body_json = json_loads(resp.text)
                body_items = (body_json if isinstance(body_json, list) else
                              body_json.get('items') or [body_json])
            except (TypeError, ValueError):
                raise exc.HTTPBadRequest('Could not decode %s as JSON' %
                                         final_url)

        mf2 = None
        if input == 'html':
            mf2 = util.parse_mf2(resp, id=fragment)
            if id and not mf2:
                raise exc.HTTPBadRequest(
                    'Got fragment %s but no element found with that id.' %
                    fragment)
        elif input in ('mf2-json', 'json-mf2'):
            mf2 = body_json
            if not hasattr(mf2, 'get'):
                raise exc.HTTPBadRequest(
                    'Expected microformats2 JSON input to be dict, got %s' %
                    mf2.__class__.__name__)
            mf2.setdefault('rels', {})  # mf2util expects rels

        actor = None
        title = None
        hfeed = None
        if mf2:

            def fetch_mf2_func(url):
                if util.domain_or_parent_in(
                        urllib.parse.urlparse(url).netloc, SILO_DOMAINS):
                    return {
                        'items': [{
                            'type': ['h-card'],
                            'properties': {
                                'url': [url]
                            }
                        }]
                    }
                return util.fetch_mf2(url, gateway=True)

            try:
                actor = microformats2.find_author(
                    mf2, fetch_mf2_func=fetch_mf2_func)
                title = microformats2.get_title(mf2)
                hfeed = mf2util.find_first_entry(mf2, ['h-feed'])
            except (KeyError, ValueError) as e:
                raise exc.HTTPBadRequest('Could not parse %s as %s: %s' %
                                         (final_url, input, e))

        try:
            if input in ('as1', 'activitystreams'):
                activities = body_items
            elif input == 'as2':
                activities = [as2.to_as1(obj) for obj in body_items]
            elif input == 'atom':
                try:
                    activities = atom.atom_to_activities(resp.text)
                except ElementTree.ParseError as e:
                    raise exc.HTTPBadRequest('Could not parse %s as XML: %s' %
                                             (final_url, e))
                except ValueError as e:
                    raise exc.HTTPBadRequest('Could not parse %s as Atom: %s' %
                                             (final_url, e))
            elif input == 'html':
                activities = microformats2.html_to_activities(resp,
                                                              url=final_url,
                                                              id=fragment,
                                                              actor=actor)
            elif input in ('mf2-json', 'json-mf2'):
                activities = [
                    microformats2.json_to_object(item, actor=actor)
                    for item in mf2.get('items', [])
                ]
            elif input == 'jsonfeed':
                activities, actor = jsonfeed.jsonfeed_to_activities(body_json)
        except ValueError as e:
            logging.warning('parsing input failed', stack_info=True)
            self.abort(
                400,
                'Could not parse %s as %s: %s' % (final_url, input, str(e)))

        self.write_response(
            source.Source.make_activities_base_response(activities),
            url=final_url,
            actor=actor,
            title=title,
            hfeed=hfeed)
Пример #11
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 ''
Пример #12
0
 def test_to_as1_blank(self):
     self.assertEqual({}, as2.to_as1(None))
     self.assertEqual({}, as2.to_as1({}))
Пример #13
0
  def get(self):
    input = util.get_required_param(self, 'input')
    if input not in INPUTS:
      raise exc.HTTPBadRequest('Invalid input: %s, expected one of %r' %
                               (input, INPUTS))
    url, body = self._fetch(util.get_required_param(self, 'url'))

    # decode data
    if input in ('activitystreams', 'as1', 'as2', 'mf2-json', 'json-mf2', 'jsonfeed'):
      try:
        body_json = json.loads(body)
        body_items = (body_json if isinstance(body_json, list)
                      else body_json.get('items') or [body_json])
      except (TypeError, ValueError):
        raise exc.HTTPBadRequest('Could not decode %s as JSON' % url)

    mf2 = None
    if input == 'html':
      mf2 = mf2py.parse(doc=body, url=url, img_with_alt=True)
    elif input in ('mf2-json', 'json-mf2'):
      mf2 = body_json
      if not hasattr(mf2, 'get'):
        raise exc.HTTPBadRequest(
          'Expected microformats2 JSON input to be dict, got %s' %
          mf2.__class__.__name__)
      mf2.setdefault('rels', {})  # mf2util expects rels

    actor = None
    title = None
    hfeed = None
    if mf2:
      def fetch_mf2_func(url):
        if util.domain_or_parent_in(urlparse.urlparse(url).netloc, SILO_DOMAINS):
          return {'items': [{'type': ['h-card'], 'properties': {'url': [url]}}]}
        _, doc = self._fetch(url)
        return mf2py.parse(doc=doc, url=url, img_with_alt=True)

      try:
        actor = microformats2.find_author(mf2, fetch_mf2_func=fetch_mf2_func)
        title = microformats2.get_title(mf2)
        hfeed = mf2util.find_first_entry(mf2, ['h-feed'])
      except (KeyError, ValueError) as e:
        raise exc.HTTPBadRequest('Could not parse %s as %s: %s' % (url, input, e))

    try:
      if input in ('as1', 'activitystreams'):
        activities = body_items
      elif input == 'as2':
        activities = [as2.to_as1(obj) for obj in body_items]
      elif input == 'atom':
        try:
          activities = atom.atom_to_activities(body)
        except ElementTree.ParseError as e:
          raise exc.HTTPBadRequest('Could not parse %s as XML: %s' % (url, e))
        except ValueError as e:
          raise exc.HTTPBadRequest('Could not parse %s as Atom: %s' % (url, e))
      elif input == 'html':
        activities = microformats2.html_to_activities(body, url, actor)
      elif input in ('mf2-json', 'json-mf2'):
        activities = [microformats2.json_to_object(item, actor=actor)
                      for item in mf2.get('items', [])]
      elif input == 'jsonfeed':
        activities, actor = jsonfeed.jsonfeed_to_activities(body_json)
    except ValueError as e:
      logging.warning('parsing input failed', exc_info=True)
      self.abort(400, 'Could not parse %s as %s: %s' % (url, input, str(e)))

    self.write_response(source.Source.make_activities_base_response(activities),
                        url=url, actor=actor, title=title, hfeed=hfeed)