# side_effects.py # # Part of kepi, an ActivityPub daemon. # Copyright (c) 2018-2019 Marnanel Thurman. # Licensed under the GNU Public License v2. """ The functions here are run as side-effects when you create an object. For example, if you create an object of type "Delete", then delete() will be run with the new activity as a parameter. """ import logging from django.conf import settings from kepi.bowler_pub.find import find, is_local from kepi.bowler_pub.delivery import deliver logger = logging.getLogger(name='kepi') def accept(activity): from kepi.bowler_pub.models.following import Following obj = activity['object__obj'] if obj['type']!='Follow': logger.warn('Object %s was Accepted, but it isn\'t a Follow', obj) return False logger.debug(' -- follow accepted') Following.accept_request( follower = obj['actor'], following = activity['actor'], ) return True def follow(activity): from kepi.bowler_pub.models.following import Following from kepi.bowler_pub.create import create if not is_local(activity['object']): logger.info('Following a remote user has no local side-effects.') return True local_user = find(activity['object'], local_only=True) remote_user = find(activity['actor']) Following.make_request( follower = activity['actor'], following = activity['object'], ) if local_user is not None and local_user.auto_follow: logger.info('Local user %s has auto_follow set; must Accept', local_user) accept_the_request = create( f_to = remote_user.url, f_type = 'Accept', f_actor = local_user.url, f_object = activity.id, run_side_effects = True, run_delivery = True, ) return True def reject(activity): from kepi.bowler_pub.models.following import Following obj = activity['object__obj'] if obj['type']!='Follow': logger.warn('Object %s was Rejected, but it isn\'t a Follow', obj) return False logger.debug(' -- follow rejected') Following.reject_request( follower = obj['actor'], following = activity['actor'], ) return True def create(activity): import kepi.bowler_pub.models as bowler_pub_models from kepi.bowler_pub.create import create as bowler_pub_create from kepi.bowler_pub.models.audience import AUDIENCE_FIELD_KEYS raw_material = activity['object'].copy() f_type = raw_material['type'].title() logger.debug('Raw material before adjustments is %s.', raw_material) for field in AUDIENCE_FIELD_KEYS.union(['attributedTo']): # copy audiences, per # https://www.w3.org/TR/activitypub/ 6.2 if field in activity: value = activity[field] if value: raw_material[field] = value try: if not issubclass(getattr(bowler_pub_models, 'Ac'+f_type), bowler_pub_models.AcItem): logger.warn('Attempt to use Create to create '+\ 'an object of type %s. '+\ 'Create can\'t create actors or activities. '+\ 'Deleting original Create.', f_type) return False except AttributeError: logger.warn('Attempt to use Create to create '+\ 'an object of type %s. '+\ 'I have no idea what that is!', f_type) return False if 'actor' in activity: attributedTo = raw_material.get('attributedTo', None) if attributedTo!=activity['actor']: logger.warn('Attribution on object is %s, but '+\ 'actor on Create is %s; '+\ 'fixing this and continuing', attributedTo, activity['actor'], ) raw_material['attributedTo'] = activity['actor'] logger.debug('Raw material after adjustments is %s.', raw_material) def it_is_relevant(something, activity): from kepi.bowler_pub.models import Audience logger.debug('Checking whether the new object is relevant to us.') for f,v in something.items(): if (f not in AUDIENCE_FIELD_KEYS) and \ f!='attributedTo': continue if type(v) in [str, bytes]: v=[v] logger.debug(' -- checking %s, currently %s', f, v) for a in v: if isinstance(a, Audience): a = a.recipient logger.debug(' -- is %s local?', a) if is_local(a): logger.debug(' -- yes!') return True try: tags = something['tag'] logger.debug(' -- checking mentions; tags are %s', tags) for tag in tags: if tag['type'].title()!='Mention': continue logger.debug(' -- is %s local?', tag['href']) if is_local(tag['href']): logger.debug(' -- yes!') return True except KeyError: logger.debug(' -- failed to find anything out '+\ 'from the mentions.') try: inReplyTo = something['inReplyTo'] logger.debug(' -- checking inReplyTo, currently %s', inReplyTo) if find(inReplyTo, local_only=True): logger.debug(' -- which is one of ours, so yes') return True except KeyError: logger.debug(' -- failed to find anything out '+\ 'from inReplyTo.') logger.debug(' -- does actor %s have local followers?', activity['actor']) try: if bowler_pub_models.Following.objects.filter( following = activity['actor'], ): logger.debug(' -- yes!') return True except KeyError: logger.debug(' -- failed to find anything out '+\ 'about local followers of actor.') logger.debug(' -- no, so it\'s irrelevant.') return False if not it_is_relevant(raw_material, activity): logger.warn('Attempt to use Create to create '+\ 'an object which isn\'t addressed to us. '+\ 'To be honest, that\'s none of our business.') return False creation = bowler_pub_create( value = raw_material, is_local_user = activity.is_local, run_side_effects = False, run_delivery = False, ) activity['object'] = creation activity.save() return True def update(activity): new_object = activity['object'] if 'id' not in new_object: logger.warn('Update did not include an id.') return False existing = find(new_object['id'], local_only = True) if existing is None: logger.warn('Update to non-existent object, %s.', new_object['id']) return False if existing['attributedTo']!=activity['actor']: logger.warn('Update by %s to object owned by %s. '+\ 'Deleting update.', activity['actor'], existing['attributedTo'], ) return False logger.debug('Updating object %s', new_object['id']) for f, v in sorted(new_object.items()): if f=='id': continue existing[f] = v # FIXME if not activity.is_local we have to remove # all properties of "existing" which aren't in # "new_object" existing.save() logger.debug(' -- done') return True def delete(activity): victim = find(activity['object'], local_only = True) if victim is None: logger.info(' -- attempt to Delete non-existent object.') return False if settings.KEPI['TOMBSTONES']: # I have a lovely cask of amontillado to show you victim.entomb() else: victim.delete() logger.info(' -- %s deleted', victim) return True