def get_public_key(key_id): ''' Takes the "keyId" field of a request and uses resolve_ap_object-actor to fetch the public key of the specified actor. ''' actor = resolve_ap_object(key_id) if not actor or not actor.get('publicKey'): return error( 'An error occurred while attempting to fetch the public key of the inbound actor.', 400) public_key_wrapper = actor.get('publicKey') if public_key_wrapper.get('id') != key_id: return error('Keys don\'t match', 400) public_key_string = public_key_wrapper.get('publicKeyPem') if not public_key_string: return error('Public key not found.') public_key = RSA.importKey(bytes(public_key_string, 'utf-8')) return public_key
def route_shared_inbox(): if request.method == 'GET': return get_inbox(personalized=False) elif request.method == 'POST': activity = request.get_json() obj = resolve_ap_object(activity['object']) return new_ob_object(activity, obj)
def handle_inbound_follow(activity, obj): ''' Handles the side effects of an incoming Follow activity. Returns: Flask response if an error occurs, None otherwise ''' api_url = os.environ['API_URL'] if obj['id'].find(f'{api_url}/actors/') < 0: return error('Invalid actor ID') local_actor_name = obj['id'].replace(f'{api_url}/actors/', '').lower() leader = db.session.query(Actor).filter( db.func.lower(Actor.username) == local_actor_name).first() if leader is None: return error('Actor not found', 404) follower = resolve_ap_object(activity['actor']) follower_shared_inbox = None follower_inbox = follower['inbox'] if 'endpoints' in follower: if 'sharedInbox' in follower['endpoints']: follower_shared_inbox = follower['endpoints']['sharedInbox'] accept_activity = Accept() accept_activity.set_actor(leader) accept_activity.set_object(activity) db.session.add(accept_activity) db.session.flush() message_body = accept_activity.to_dict() message_body['object'] = activity try: signed_request(leader, message_body, url=follower_inbox) except: return error('An error occurred while processing that follow request.', 400) new_followed_by = FollowedBy(leader.id, follower['id'], follower_inbox, follower_shared_inbox) db.session.add(new_followed_by) follower_username = follower['id'] if 'preferredUsername' in follower: follower_username = follower['preferredUsername'] db.session.add( Notification(leader, f'{follower_username} has followed you', 'Follow')) return None
def handle_follow(inbound_json, actor, base_activity, base_object, is_local): ''' Handles the various oddities associated with the correspondance nature of the Follow activity. ''' leader = resolve_ap_object(inbound_json['object']) existing_follow = db.session.query(Following).filter( db.and_(Following.follower_id == actor.id, Following.leader == leader['id'])).first() if existing_follow is not None and existing_follow.approved is True: return error('You are already following this actor.') new_follow = Following(actor.id, leader['id'], leader['followers'], approved=is_local) db.session.add(new_follow) # Due to the correspondance nature of the Follow activity, it has some very unusual requirements. # We need to detect if the incoming Follow activity is targeting a local user. If it is, we don't need # To deliver server-to-server messages about this transaction. Attempting to do so would cause problems # resulting from uncomitted database transactions and be a waste of resources. if is_local: local_leader = db.session.query(Actor).filter( db.func.lower(Actor.username) == db.func.lower( inbound_json['object'].replace( f'{os.environ["API_URL"]}/actors/', ''))).first() if local_leader is None: return make_response('Actor not found', 404) actor_dict = actor.to_dict() new_followed_by = FollowedBy( local_leader.id, actor_dict['id'], actor_dict['inbox'], follower_shared_inbox=actor_dict['endpoints']['sharedInbox'], approved=True) db.session.add( Notification( local_leader, f'{actor_dict["preferredUsername"]} has followed you.', 'Follow')) db.session.add(new_followed_by) db.session.commit() else: db.session.commit( ) # This is required so when we get an Accept activity back before the end of this request, we're able to find the Follow activity try: signed_request(actor, base_activity.to_dict(), leader['inbox']) except: return error( 'Your follow request was not able to be delivered to that server.' ) return make_response('', 200)
def route_actor_inbox(actor_name): if request.method == 'POST': activity = request.get_json() recipient = db.session.query(Actor).filter_by( username=actor_name.lower()).first() if recipient is None: return error('The specified actor could not be found.', 404) obj = resolve_ap_object(request.get_json().get('object')) return new_ob_object(activity, obj, recipient) elif request.method == 'GET': return get_actor_inbox(actor_name=actor_name)
def route_undo_follow(user): from vagabond.crypto import signed_request follower = user.primary_actor leader = resolve_ap_object(request.get_json()['leader']) follow_activity = db.session.query(Follow).filter( Follow.internal_actor_id == follower.id, Follow.external_object_id == leader['id']).first() if follow_activity is None: print('@@@@@@@@@@@@@@@@@@@@@@@') return error( 'It doesn' 't appear that you follow that user :thonking:', 404) following = db.session.query(Following).filter( Following.leader == leader['id']).filter( Following.follower_id == follower.id).first() if following is None: return error( 'It doesn' 't appear that you follow that user :thonking:', 404) api_url = os.environ['API_URL'] uuid = uuid4() signed_request( follower, { '@context': 'https://www.w3.org/ns/activitystreams', 'type': 'Undo', 'id': f'{api_url}/unfollowActivity/{uuid}', 'actor': follower.to_dict()['id'], 'object': { 'type': 'Follow', 'actor': follower.to_dict()['id'], 'object': leader['id'], 'id': follow_activity.to_dict()['id'] } }, leader['inbox']) db.session.delete(follow_activity) db.session.delete(following) db.session.commit() return make_response('', 200)
def follow(actor, follow_activity): ''' actor: Actor model follow_activity: Dictionary representation of the new follow request ''' leader = resolve_ap_object(follow_activity['object']) existing_follow = db.session.query(Following).filter( db.and_(Following.follower_id == actor.id, Following.leader == leader['id'])).first() if existing_follow is not None: if existing_follow.approved is True: return error('You are already following this actor.') db.session.delete(existing_follow) new_activity = Follow() new_activity.set_actor(actor) new_activity.set_object(follow_activity['object']) db.session.add(new_activity) db.session.flush() new_follow = Following(actor.id, leader['id'], leader['followers']) db.session.add(new_follow) response = signed_request(actor, new_activity.to_dict(), leader['inbox']) db.session.commit() if response.status_code >= 400: return error('Something went wrong :(') return make_response('', 200)
def deliver(actor, message): ''' Delivers the specified message to the recipients listed in the to, bto, cc, and bcc fields. ''' api_url = os.environ['API_URL'] keys = ['to', 'bto', 'cc', 'bcc'] recipients = [] for key in keys: if key in message: if isinstance(message[key], str): message[key] = [message[key]] for value in message[key]: recipients.append(value) all_inboxes = [] for recipient in recipients: try: # possibility 1: local delivery of some kind. If delivery is to local actor, do nothing. If delivery is to local followers collection, distribute to followers. if recipient.replace(f'{api_url}/actors/', '') != recipient: splits = recipient.replace(f'{api_url}/actors/', '').split('/') if len(splits) == 2 and splits[1] == 'followers': leader = db.session.query(Actor).filter( db.func.lower(Actor.username) == db.func.lower( splits[0])).first() if leader is None: continue shared_inboxes = db.session.query( FollowedBy.follower_shared_inbox.distinct()).filter( FollowedBy.leader_id == leader.id, FollowedBy.follower_shared_inbox != None).all() for inbox in shared_inboxes: all_inboxes.append(inbox[0]) unique_inboxes = db.session.query( FollowedBy.follower_inbox.distinct()).filter( FollowedBy.leader_id == leader.id, FollowedBy.follower_shared_inbox == None).all() for inbox in unique_inboxes: all_inboxes.append(inbox[0]) # possibility 2: external delivery of some kind else: actor_types = [ 'Application', 'Group', 'Organization', 'Person', 'Service' ] recipient_object = resolve_ap_object(recipient) if recipient_object is None: continue if recipient_object['type'] in actor_types: if 'endpoints' in recipient_object and 'sharedInbox' in recipient_object[ 'endpoints'] and recipient_object['endpoints'][ 'sharedInbox'] not in all_inboxes: all_inboxes.append( recipient_object['endpoints']['sharedInbox']) elif 'inbox' in recipient_object: all_inboxes.append(recipient_object['inbox']) elif (recipient_object['type'] == 'OrderedCollection' or recipient_object['type'] == 'Collection'): # We have a page with the items all in one place if 'items' in recipient_object: for item in recipient_object['items']: if isinstance(item, str): if item not in all_inboxes: all_inboxes.append(item) elif isinstance(item, dict): if 'endpoints' in item and 'sharedInbox' in item[ 'endpoints']: shared_inbox = item['endpoints'][ 'sharedInbox'] if shared_inbox not in all_inboxes: all_inboxes.append(shared_inbox) elif 'inbox' in item: if item['inbox'] not in all_inboxes: all_inboxes.append(item['inbox']) # Traditional segmented OrderedCollection else: next_page = resolve_ap_object( recipient_object['first']) iteration = 0 while next_page is not None and iteration < 10: for item in next_page['items']: if isinstance(item, str) and item not in all_inboxes: all_inboxes.append(item) elif isinstance( item, dict ) and 'id' in item and 'type' in item and item[ 'type'] in actor_types: all_inboxes.append(item) iteration = iteration + 1 if 'next' in next_page: next_page = resolve_ap_object( next_page['next']) else: next_page = None #TODO: Delivery to external collections except Exception as e: print(e) for inbox in all_inboxes: # Don't deliver messages to ourselves! if inbox.replace(os.environ['API_URL'], '') == inbox: try: signed_request(actor, message, url=inbox) except Exception as e: print(e) print( f'Could not deliver message to the following inbox: {inbox}' ) app.logger.error( f'Could not deliver message to the following inbox: {inbox}' )
def webfinger_federated(): ''' Used for looking actors on other servers via their WebFinger interface. ''' if 'type' not in request.args: return error('Invalid request') #webfinger proxy if request.args['type'] == 'webfinger': username = request.args.get('username') hostname = request.args.get('hostname') if not username or not hostname: return error('Invalid request') try: response = requests.get( f'https://{hostname}/.well-known/webfinger?resource=acct:{username}@{hostname}' ) if response.status_code == 404: return error('User not found.', 404) return make_response(response.json(), 200) except Exception as e: return error( 'An error occurred while attempting to look up the specified user.' ) #Actor proxy elif request.args['type'] == 'actor': username = request.args.get('username') hostname = request.args.get('hostname') try: response = requests.get( f'https://{hostname}/.well-known/webfinger?resource=acct:{username}@{hostname}' ) if response.status_code >= 300: return error(response.text, response.status_code) if 'links' in response.json() and isinstance( response.json()['links'], list): for link in response.json()['links']: if 'rel' in link and link['rel'] == 'self': actor_data = resolve_ap_object(link['href']) return make_response(actor_data, 200) return error( f'Invalid WebFinger response for actor @{username}@{hostname}: no "self" link. ' ) elif 'href' in response.json(): actor_data = resolve_ap_object(response.json()['href']) return make_response(actor_data, 200) else: return error( f'Failed to look up actor @{username}@{hostname}: server sent invalid response.' ) except Exception as e: print(e) return error( 'An error occurred while attempting to look up the specified actor.' )