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))
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))
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 ''
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))
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)
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 ''