def get_presence_backend(request: HttpRequest, user_profile: UserProfile, email: str) -> HttpResponse: # This isn't used by the webapp; it's available for API use by # bots and other clients. We may want to add slim_presence # support for it (or just migrate its API wholesale) later. try: target = get_active_user(email, user_profile.realm) except UserProfile.DoesNotExist: return json_error(_("No such user")) if target.is_bot: return json_error(_("Presence is not supported for bot users.")) presence_dict = get_presence_for_user(target.id) if len(presence_dict) == 0: return json_error( _("No presence data for {email}").format(email=email)) # For initial version, we just include the status and timestamp keys result = dict(presence=presence_dict[target.email]) aggregated_info = result["presence"]["aggregated"] aggr_status_duration = datetime_to_timestamp( timezone_now()) - aggregated_info["timestamp"] if aggr_status_duration > settings.OFFLINE_THRESHOLD_SECS: aggregated_info["status"] = "offline" for val in result["presence"].values(): val.pop("client", None) val.pop("pushable", None) return json_success(result)
def get_presence_backend(request: HttpRequest, user_profile: UserProfile, email: str) -> HttpResponse: # This isn't used by the webapp; it's available for API use by # bots and other clients. We may want to add slim_presence # support for it (or just migrate its API wholesale) later. try: target = get_active_user_by_delivery_email(email, user_profile.realm) except UserProfile.DoesNotExist: return json_error(_('No such user')) if target.is_bot: return json_error(_('Presence is not supported for bot users.')) presence_dict = get_presence_for_user(target.id) if len(presence_dict) == 0: return json_error(_('No presence data for %s') % (target.email,)) # For initial version, we just include the status and timestamp keys result = dict(presence=presence_dict[target.email]) aggregated_info = result['presence']['aggregated'] aggr_status_duration = datetime_to_timestamp(timezone_now()) - aggregated_info['timestamp'] if aggr_status_duration > settings.OFFLINE_THRESHOLD_SECS: aggregated_info['status'] = 'offline' for val in result['presence'].values(): val.pop('client', None) val.pop('pushable', None) return json_success(result)
def apply_event(state: Dict[str, Any], event: Dict[str, Any], user_profile: UserProfile, client_gravatar: bool, slim_presence: bool, include_subscribers: bool) -> None: if event['type'] == "message": state['max_message_id'] = max(state['max_message_id'], event['message']['id']) if 'raw_unread_msgs' in state: apply_unread_message_event( user_profile, state['raw_unread_msgs'], event['message'], event['flags'], ) if event['message']['type'] != "stream": if 'raw_recent_private_conversations' in state: # Handle maintaining the recent_private_conversations data structure. conversations = state['raw_recent_private_conversations'] recipient_id = get_recent_conversations_recipient_id( user_profile, event['message']['recipient_id'], event['message']["sender_id"]) if recipient_id not in conversations: conversations[recipient_id] = dict(user_ids=sorted([ user_dict['id'] for user_dict in event['message']['display_recipient'] if user_dict['id'] != user_profile.id ]), ) conversations[recipient_id]['max_message_id'] = event[ 'message']['id'] return # Below, we handle maintaining first_message_id. for sub_dict in state.get('subscriptions', []): if event['message']['stream_id'] == sub_dict['stream_id']: if sub_dict['first_message_id'] is None: sub_dict['first_message_id'] = event['message']['id'] for stream_dict in state.get('streams', []): if event['message']['stream_id'] == stream_dict['stream_id']: if stream_dict['first_message_id'] is None: stream_dict['first_message_id'] = event['message']['id'] elif event['type'] == "hotspots": state['hotspots'] = event['hotspots'] elif event['type'] == "custom_profile_fields": state['custom_profile_fields'] = event['fields'] elif event['type'] == "realm_user": person = event['person'] person_user_id = person['user_id'] if event['op'] == "add": person = copy.deepcopy(person) if client_gravatar: if person['avatar_url'].startswith( "https://secure.gravatar.com"): person['avatar_url'] = None person['is_active'] = True if not person['is_bot']: person['profile_data'] = {} state['raw_users'][person_user_id] = person elif event['op'] == "remove": state['raw_users'][person_user_id]['is_active'] = False elif event['op'] == 'update': is_me = (person_user_id == user_profile.id) if is_me: if ('avatar_url' in person and 'avatar_url' in state): state['avatar_source'] = person['avatar_source'] state['avatar_url'] = person['avatar_url'] state['avatar_url_medium'] = person['avatar_url_medium'] if 'role' in person: state['is_admin'] = is_administrator_role(person['role']) state['is_owner'] = person[ 'role'] == UserProfile.ROLE_REALM_OWNER state['is_guest'] = person[ 'role'] == UserProfile.ROLE_GUEST # Recompute properties based on is_admin/is_guest state[ 'can_create_streams'] = user_profile.can_create_streams( ) state[ 'can_subscribe_other_users'] = user_profile.can_subscribe_other_users( ) # TODO: Probably rather than writing the perfect # live-update code for the case of racing with the # current user changing roles, we should just do a # full refetch. if 'never_subscribed' in state: subscriptions, unsubscribed, never_subscribed = gather_subscriptions_helper( user_profile, include_subscribers=include_subscribers) state['subscriptions'] = subscriptions state['unsubscribed'] = unsubscribed state['never_subscribed'] = never_subscribed if 'streams' in state: state['streams'] = do_get_streams(user_profile) for field in ['delivery_email', 'email', 'full_name']: if field in person and field in state: state[field] = person[field] # In the unlikely event that the current user # just changed to/from being an admin, we need # to add/remove the data on all bots in the # realm. This is ugly and probably better # solved by removing the all-realm-bots data # given to admin users from this flow. if ('role' in person and 'realm_bots' in state): prev_state = state['raw_users'][user_profile.id] was_admin = prev_state['is_admin'] now_admin = is_administrator_role(person['role']) if was_admin and not now_admin: state['realm_bots'] = [] if not was_admin and now_admin: state['realm_bots'] = get_owned_bot_dicts(user_profile) if client_gravatar and 'avatar_url' in person: # Respect the client_gravatar setting in the `users` data. if person['avatar_url'].startswith( "https://secure.gravatar.com"): person['avatar_url'] = None person['avatar_url_medium'] = None if person_user_id in state['raw_users']: p = state['raw_users'][person_user_id] for field in p: if field in person: p[field] = person[field] if 'role' in person: p['is_admin'] = is_administrator_role(person['role']) p['is_owner'] = person[ 'role'] == UserProfile.ROLE_REALM_OWNER p['is_guest'] = person[ 'role'] == UserProfile.ROLE_GUEST if 'custom_profile_field' in person: custom_field_id = person['custom_profile_field']['id'] custom_field_new_value = person[ 'custom_profile_field']['value'] if 'rendered_value' in person['custom_profile_field']: p['profile_data'][custom_field_id] = { 'value': custom_field_new_value, 'rendered_value': person['custom_profile_field'] ['rendered_value'], } else: p['profile_data'][custom_field_id] = { 'value': custom_field_new_value, } elif event['type'] == 'realm_bot': if event['op'] == 'add': state['realm_bots'].append(event['bot']) if event['op'] == 'remove': user_id = event['bot']['user_id'] for bot in state['realm_bots']: if bot['user_id'] == user_id: bot['is_active'] = False if event['op'] == 'delete': state['realm_bots'] = [ item for item in state['realm_bots'] if item['user_id'] != event['bot']['user_id'] ] if event['op'] == 'update': for bot in state['realm_bots']: if bot['user_id'] == event['bot']['user_id']: if 'owner_id' in event['bot']: bot_owner_id = event['bot']['owner_id'] bot['owner_id'] = bot_owner_id else: bot.update(event['bot']) elif event['type'] == 'stream': if event['op'] == 'create': for stream in event['streams']: if not stream['invite_only']: stream_data = copy.deepcopy(stream) if include_subscribers: stream_data['subscribers'] = [] # We know the stream has no traffic, and this # field is not present in the event. # # TODO: Probably this should just be added to the event. stream_data['stream_weekly_traffic'] = None # Add stream to never_subscribed (if not invite_only) state['never_subscribed'].append(stream_data) state['streams'].append(stream) state['streams'].sort(key=lambda elt: elt["name"]) if event['op'] == 'delete': deleted_stream_ids = { stream['stream_id'] for stream in event['streams'] } state['streams'] = [ s for s in state['streams'] if s['stream_id'] not in deleted_stream_ids ] state['never_subscribed'] = [ stream for stream in state['never_subscribed'] if stream['stream_id'] not in deleted_stream_ids ] if event['op'] == 'update': # For legacy reasons, we call stream data 'subscriptions' in # the state var here, for the benefit of the JS code. for obj in state['subscriptions']: if obj['name'].lower() == event['name'].lower(): obj[event['property']] = event['value'] if event['property'] == "description": obj['rendered_description'] = event[ 'rendered_description'] # Also update the pure streams data for stream in state['streams']: if stream['name'].lower() == event['name'].lower(): prop = event['property'] if prop in stream: stream[prop] = event['value'] if prop == 'description': stream['rendered_description'] = event[ 'rendered_description'] elif event['op'] == "occupy": state['streams'] += event['streams'] elif event['op'] == "vacate": stream_ids = [s["stream_id"] for s in event['streams']] state['streams'] = [ s for s in state['streams'] if s["stream_id"] not in stream_ids ] elif event['type'] == 'default_streams': state['realm_default_streams'] = event['default_streams'] elif event['type'] == 'default_stream_groups': state['realm_default_stream_groups'] = event['default_stream_groups'] elif event['type'] == 'realm': if event['op'] == "update": field = 'realm_' + event['property'] state[field] = event['value'] if event['property'] == 'plan_type': # Then there are some extra fields that also need to be set. state['zulip_plan_is_not_limited'] = event[ 'value'] != Realm.LIMITED state['realm_upload_quota'] = event['extra_data'][ 'upload_quota'] policy_permission_dict = { 'create_stream_policy': 'can_create_streams', 'invite_to_stream_policy': 'can_subscribe_other_users' } # Tricky interaction: Whether we can create streams and can subscribe other users # can get changed here. if field == 'realm_waiting_period_threshold': for policy, permission in policy_permission_dict.items(): if permission in state: state[permission] = user_profile.has_permission(policy) if event['property'] in policy_permission_dict.keys(): if policy_permission_dict[event['property']] in state: state[policy_permission_dict[ event['property']]] = user_profile.has_permission( event['property']) elif event['op'] == "update_dict": for key, value in event['data'].items(): state['realm_' + key] = value # It's a bit messy, but this is where we need to # update the state for whether password authentication # is enabled on this server. if key == 'authentication_methods': state['realm_password_auth_enabled'] = (value['Email'] or value['LDAP']) state['realm_email_auth_enabled'] = value['Email'] elif event['type'] == "subscription": if not include_subscribers and event['op'] in [ 'peer_add', 'peer_remove' ]: return if event['op'] in ["add"]: if not include_subscribers: # Avoid letting 'subscribers' entries end up in the list for i, sub in enumerate(event['subscriptions']): event['subscriptions'][i] = copy.deepcopy( event['subscriptions'][i]) del event['subscriptions'][i]['subscribers'] def name(sub: Dict[str, Any]) -> str: return sub['name'].lower() if event['op'] == "add": added_names = set(map(name, event["subscriptions"])) was_added = lambda s: name(s) in added_names # add the new subscriptions state['subscriptions'] += event['subscriptions'] # remove them from unsubscribed if they had been there state['unsubscribed'] = [ s for s in state['unsubscribed'] if not was_added(s) ] # remove them from never_subscribed if they had been there state['never_subscribed'] = [ s for s in state['never_subscribed'] if not was_added(s) ] elif event['op'] == "remove": removed_names = set(map(name, event["subscriptions"])) was_removed = lambda s: name(s) in removed_names # Find the subs we are affecting. removed_subs = list(filter(was_removed, state['subscriptions'])) # Remove our user from the subscribers of the removed subscriptions. if include_subscribers: for sub in removed_subs: sub['subscribers'].remove(user_profile.id) # We must effectively copy the removed subscriptions from subscriptions to # unsubscribe, since we only have the name in our data structure. state['unsubscribed'] += removed_subs # Now filter out the removed subscriptions from subscriptions. state['subscriptions'] = [ s for s in state['subscriptions'] if not was_removed(s) ] elif event['op'] == 'update': for sub in state['subscriptions']: if sub['name'].lower() == event['name'].lower(): sub[event['property']] = event['value'] elif event['op'] == 'peer_add': stream_id = event['stream_id'] user_id = event['user_id'] for sub in state['subscriptions']: if (sub['stream_id'] == stream_id and user_id not in sub['subscribers']): sub['subscribers'].append(user_id) for sub in state['never_subscribed']: if (sub['stream_id'] == stream_id and user_id not in sub['subscribers']): sub['subscribers'].append(user_id) elif event['op'] == 'peer_remove': stream_id = event['stream_id'] user_id = event['user_id'] for sub in state['subscriptions']: if (sub['stream_id'] == stream_id and user_id in sub['subscribers']): sub['subscribers'].remove(user_id) elif event['type'] == "presence": if slim_presence: user_key = str(event['user_id']) else: user_key = event['email'] state['presences'][user_key] = get_presence_for_user( event['user_id'], slim_presence)[user_key] elif event['type'] == "update_message": # We don't return messages in /register, so we don't need to # do anything for content updates, but we may need to update # the unread_msgs data if the topic of an unread message changed. if 'new_stream_id' in event: stream_dict = state['raw_unread_msgs']['stream_dict'] stream_id = event['new_stream_id'] for message_id in event['message_ids']: if message_id in stream_dict: stream_dict[message_id]['stream_id'] = stream_id if TOPIC_NAME in event: stream_dict = state['raw_unread_msgs']['stream_dict'] topic = event[TOPIC_NAME] for message_id in event['message_ids']: if message_id in stream_dict: stream_dict[message_id]['topic'] = topic elif event['type'] == "delete_message": if 'message_id' in event: message_ids = [event['message_id']] else: message_ids = event['message_ids'] # nocoverage max_message = Message.objects.filter( usermessage__user_profile=user_profile).order_by('-id').first() if max_message: state['max_message_id'] = max_message.id else: state['max_message_id'] = -1 if 'raw_unread_msgs' in state: for remove_id in message_ids: remove_message_id_from_unread_mgs(state['raw_unread_msgs'], remove_id) # The remainder of this block is about maintaining recent_private_conversations if 'raw_recent_private_conversations' not in state or event[ 'message_type'] != 'private': return recipient_id = get_recent_conversations_recipient_id( user_profile, event['recipient_id'], event['sender_id']) # Ideally, we'd have test coverage for these two blocks. To # do that, we'll need a test where we delete not-the-latest # messages or delete a private message not in # recent_private_conversations. if recipient_id not in state[ 'raw_recent_private_conversations']: # nocoverage return old_max_message_id = state['raw_recent_private_conversations'][ recipient_id]['max_message_id'] if old_max_message_id not in message_ids: # nocoverage return # OK, we just deleted what had been the max_message_id for # this recent conversation; we need to recompute that value # from scratch. Definitely don't need to re-query everything, # but this case is likely rare enough that it's reasonable to do so. state['raw_recent_private_conversations'] = \ get_recent_private_conversations(user_profile) elif event['type'] == "reaction": # The client will get the message with the reactions directly pass elif event['type'] == "submessage": # The client will get submessages with their messages pass elif event['type'] == 'typing': # Typing notification events are transient and thus ignored pass elif event['type'] == "attachment": # Attachment events are just for updating the "uploads" UI; # they are not sent directly. pass elif event['type'] == "update_message_flags": # We don't return messages in `/register`, so most flags we # can ignore, but we do need to update the unread_msgs data if # unread state is changed. if 'raw_unread_msgs' in state and event['flag'] == 'read' and event[ 'operation'] == 'add': for remove_id in event['messages']: remove_message_id_from_unread_mgs(state['raw_unread_msgs'], remove_id) if event['flag'] == 'starred' and 'starred_messages' in state: if event['operation'] == 'add': state['starred_messages'] += event['messages'] if event['operation'] == 'remove': state['starred_messages'] = [ message for message in state['starred_messages'] if not (message in event['messages']) ] elif event['type'] == "realm_domains": if event['op'] == 'add': state['realm_domains'].append(event['realm_domain']) elif event['op'] == 'change': for realm_domain in state['realm_domains']: if realm_domain['domain'] == event['realm_domain']['domain']: realm_domain['allow_subdomains'] = event['realm_domain'][ 'allow_subdomains'] elif event['op'] == 'remove': state['realm_domains'] = [ realm_domain for realm_domain in state['realm_domains'] if realm_domain['domain'] != event['domain'] ] elif event['type'] == "realm_emoji": state['realm_emoji'] = event['realm_emoji'] elif event['type'] == 'realm_export': # These realm export events are only available to # administrators, and aren't included in page_params. pass elif event['type'] == "alert_words": state['alert_words'] = event['alert_words'] elif event['type'] == "muted_topics": state['muted_topics'] = event["muted_topics"] elif event['type'] == "realm_filters": state['realm_filters'] = event["realm_filters"] elif event['type'] == "update_display_settings": assert event['setting_name'] in UserProfile.property_types state[event['setting_name']] = event['setting'] elif event['type'] == "update_global_notifications": assert event[ 'notification_name'] in UserProfile.notification_setting_types state[event['notification_name']] = event['setting'] elif event['type'] == "invites_changed": pass elif event['type'] == "user_group": if event['op'] == 'add': state['realm_user_groups'].append(event['group']) state['realm_user_groups'].sort(key=lambda group: group['id']) elif event['op'] == 'update': for user_group in state['realm_user_groups']: if user_group['id'] == event['group_id']: user_group.update(event['data']) elif event['op'] == 'add_members': for user_group in state['realm_user_groups']: if user_group['id'] == event['group_id']: user_group['members'].extend(event['user_ids']) user_group['members'].sort() elif event['op'] == 'remove_members': for user_group in state['realm_user_groups']: if user_group['id'] == event['group_id']: members = set(user_group['members']) user_group['members'] = list(members - set(event['user_ids'])) user_group['members'].sort() elif event['op'] == 'remove': state['realm_user_groups'] = [ ug for ug in state['realm_user_groups'] if ug['id'] != event['group_id'] ] elif event['type'] == 'user_status': user_id = event['user_id'] user_status = state['user_status'] away = event.get('away') status_text = event.get('status_text') if user_id not in user_status: user_status[user_id] = dict() if away is not None: if away: user_status[user_id]['away'] = True else: user_status[user_id].pop('away', None) if status_text is not None: if status_text == '': user_status[user_id].pop('status_text', None) else: user_status[user_id]['status_text'] = status_text if not user_status[user_id]: user_status.pop(user_id, None) state['user_status'] = user_status elif event['type'] == 'has_zoom_token': state['has_zoom_token'] = event['value'] else: raise AssertionError("Unexpected event type {}".format(event['type']))