Exemple #1
0
def process_submessage(request: HttpRequest,
                       user_profile: UserProfile,
                       message_id: int=REQ(validator=check_int),
                       msg_type: str=REQ(),
                       content: str=REQ(),
                       ) -> HttpResponse:
    message, user_message = access_message(user_profile, message_id)

    if not settings.ALLOW_SUB_MESSAGES:  # nocoverage
        msg = 'Feature not enabled'
        return json_error(msg)

    try:
        data = ujson.loads(content)
    except Exception:
        return json_error(_("Invalid json for submessage"))

    do_add_submessage(
        sender_id=user_profile.id,
        message_id=message.id,
        msg_type=msg_type,
        content=content,
        data=data,
    )
    return json_success()
Exemple #2
0
def add_reaction(request: HttpRequest, user_profile: UserProfile, message_id: int,
                 emoji_name: str=REQ(),
                 emoji_code: Optional[str]=REQ(default=None),
                 reaction_type: str=REQ(default="unicode_emoji")) -> HttpResponse:
    message, user_message = access_message(user_profile, message_id)

    if emoji_code is None:
        # The emoji_code argument is only required for rare corner
        # cases discussed in the long block comment below.  For simple
        # API clients, we allow specifying just the name, and just
        # look up the code using the current name->code mapping.
        emoji_code = emoji_name_to_emoji_code(message.sender.realm,
                                              emoji_name)[0]

    if Reaction.objects.filter(user_profile=user_profile,
                               message=message,
                               emoji_code=emoji_code,
                               reaction_type=reaction_type).exists():
        raise JsonableError(_("Reaction already exists."))

    query = Reaction.objects.filter(message=message,
                                    emoji_code=emoji_code,
                                    reaction_type=reaction_type)
    if query.exists():
        # If another user has already reacted to this message with
        # same emoji code, we treat the new reaction as a vote for the
        # existing reaction.  So the emoji name used by that earlier
        # reaction takes precendence over whatever was passed in this
        # request.  This is necessary to avoid a message having 2
        # "different" emoji reactions with the same emoji code (and
        # thus same image) on the same message, which looks ugly.
        #
        # In this "voting for an existing reaction" case, we shouldn't
        # check whether the emoji code and emoji name match, since
        # it's possible that the (emoji_type, emoji_name, emoji_code)
        # triple for this existing rection xmay not pass validation
        # now (e.g. because it is for a realm emoji that has been
        # since deactivated).  We still want to allow users to add a
        # vote any old reaction they see in the UI even if that is a
        # deactivated custom emoji, so we just use the emoji name from
        # the existing reaction with no further validation.
        emoji_name = query.first().emoji_name
    else:
        # Otherwise, use the name provided in this request, but verify
        # it is valid in the user's realm (e.g. not a deactivated
        # realm emoji).
        check_emoji_request(message.sender.realm, emoji_name,
                            emoji_code, reaction_type)

    if user_message is None:
        create_historical_message(user_profile, message)

    do_add_reaction(user_profile, message, emoji_name, emoji_code, reaction_type)

    return json_success()
Exemple #3
0
def get_message_edit_history(request, user_profile,
                             message_id=REQ(converter=to_non_negative_int)):
    # type: (HttpRequest, UserProfile, int) -> HttpResponse
    message, ignored_user_message = access_message(user_profile, message_id)

    # Extract the message edit history from the message
    message_edit_history = ujson.loads(message.edit_history)

    # Fill in all the extra data that will make it usable
    fill_edit_history_entries(message_edit_history, message)
    return json_success({"message_history": reversed(message_edit_history)})
Exemple #4
0
def remove_reaction_legacy(request: HttpRequest, user_profile: UserProfile,
                           message_id: int, emoji_name: Text) -> HttpResponse:

    # access_message will throw a JsonableError exception if the user
    # cannot see the message (e.g. for messages to private streams).
    message = access_message(user_profile, message_id)[0]

    # We could probably just make this check be a try/except for the
    # IntegrityError from it already existing, but this is a bit cleaner.
    if not Reaction.objects.filter(user_profile=user_profile,
                                   message=message,
                                   emoji_name=emoji_name).exists():
        raise JsonableError(_("Reaction does not exist"))

    do_remove_reaction_legacy(user_profile, message, emoji_name)

    return json_success()
def handle_remove_push_notification(user_profile_id: int, message_id: int) -> None:
    """This should be called when a message that had previously had a
    mobile push executed is read.  This triggers a mobile push notifica
    mobile app when the message is read on the server, to remove the
    message from the notification.

    """
    user_profile = get_user_profile_by_id(user_profile_id)
    message, user_message = access_message(user_profile, message_id)

    if not settings.SEND_REMOVE_PUSH_NOTIFICATIONS:
        # It's a little annoying that we duplicate this flag-clearing
        # code (also present below), but this block is scheduled to be
        # removed in a few weeks, once the app has supported the
        # feature for long enough.
        user_message.flags.active_mobile_push_notification = False
        user_message.save(update_fields=["flags"])
        return

    gcm_payload = get_common_payload(message)
    gcm_payload.update({
        'event': 'remove',
        'zulip_message_id': message_id,  # message_id is reserved for CCS
    })

    if uses_notification_bouncer():
        try:
            send_notifications_to_bouncer(user_profile_id,
                                          {},
                                          gcm_payload)
        except requests.ConnectionError:  # nocoverage
            def failure_processor(event: Dict[str, Any]) -> None:
                logging.warning(
                    "Maximum retries exceeded for trigger:%s event:push_notification" % (
                        event['user_profile_id']))
        return

    android_devices = list(PushDeviceToken.objects.filter(user=user_profile,
                                                          kind=PushDeviceToken.GCM))

    if android_devices:
        send_android_push_notification(android_devices, gcm_payload)

    user_message.flags.active_mobile_push_notification = False
    user_message.save(update_fields=["flags"])
Exemple #6
0
def remove_reaction_backend(request, user_profile, message_id, emoji_name):
    # type: (HttpRequest, UserProfile, int, text_type) -> HttpResponse

    # access_message will throw a JsonableError exception if the user
    # cannot see the message (e.g. for messages to private streams).
    message = access_message(user_profile, message_id)[0]

    existing_emojis = set(message.sender.realm.get_emoji().keys()) or set(emoji_list)
    if emoji_name not in existing_emojis:
        raise JsonableError(_("Emoji '%s' does not exist" % (emoji_name,)))

    # We could probably just make this check be a try/except for the
    # IntegrityError from it already existing, but this is a bit cleaner.
    if not Reaction.objects.filter(user_profile=user_profile, message=message, emoji_name=emoji_name).exists():
        raise JsonableError(_("Reaction does not exist"))

    do_remove_reaction(user_profile, message, emoji_name)

    return json_success()
Exemple #7
0
def process_submessage(request: HttpRequest,
                       user_profile: UserProfile,
                       message_id: int=REQ(validator=check_int),
                       msg_type: str=REQ(),
                       content: str=REQ(),
                       ) -> HttpResponse:
    message, user_message = access_message(user_profile, message_id)

    try:
        ujson.loads(content)
    except Exception:
        return json_error(_("Invalid json for submessage"))

    do_add_submessage(
        realm=user_profile.realm,
        sender_id=user_profile.id,
        message_id=message.id,
        msg_type=msg_type,
        content=content,
    )
    return json_success()
Exemple #8
0
def remove_reaction(request: HttpRequest, user_profile: UserProfile, message_id: int,
                    emoji_code: str=REQ(),
                    reaction_type: str=REQ(default="unicode_emoji")) -> HttpResponse:
    message, user_message = access_message(user_profile, message_id)

    if not Reaction.objects.filter(user_profile=user_profile,
                                   message=message,
                                   emoji_code=emoji_code,
                                   reaction_type=reaction_type).exists():
        raise JsonableError(_("Reaction doesn't exist."))

    # Unlike adding reactions, while deleting a reaction, we don't
    # check whether the provided (emoji_type, emoji_code) pair is
    # valid in this realm.  Since there's a row in the database, we
    # know it was valid when the user added their reaction in the
    # first place, so it is safe to just remove the reaction if it
    # exists.  And the (reaction_type, emoji_code) pair may no longer be
    # valid in legitimate situations (e.g. if a realm emoji was
    # deactivated by an administrator in the meantime).
    do_remove_reaction(user_profile, message, emoji_code, reaction_type)

    return json_success()
Exemple #9
0
def add_reaction_legacy(request: HttpRequest, user_profile: UserProfile,
                        message_id: int, emoji_name: Text) -> HttpResponse:

    # access_message will throw a JsonableError exception if the user
    # cannot see the message (e.g. for messages to private streams).
    message, user_message = access_message(user_profile, message_id)

    check_valid_emoji(message.sender.realm, emoji_name)

    # We could probably just make this check be a try/except for the
    # IntegrityError from it already existing, but this is a bit cleaner.
    if Reaction.objects.filter(user_profile=user_profile,
                               message=message,
                               emoji_name=emoji_name).exists():
        raise JsonableError(_("Reaction already exists"))

    if user_message is None:
        create_historical_message(user_profile, message)

    do_add_reaction_legacy(user_profile, message, emoji_name)

    return json_success()
Exemple #10
0
def remove_reaction(request: HttpRequest, user_profile: UserProfile, message_id: int,
                    emoji_name: Optional[str]=REQ(default=None),
                    emoji_code: Optional[str]=REQ(default=None),
                    reaction_type: str=REQ(default="unicode_emoji")) -> HttpResponse:
    message, user_message = access_message(user_profile, message_id)

    if emoji_code is None:
        if emoji_name is None:
            raise JsonableError(_('At least one of the following arguments '
                                  'must be present: emoji_name, emoji_code'))
        # A correct full Zulip client implementation should always
        # pass an emoji_code, because of the corner cases discussed in
        # the long block comments elsewhere in this file.  However, to
        # make it easy for simple API clients to use the reactions API
        # without needing the mapping between emoji names and codes,
        # we allow instead passing the emoji_name and looking up the
        # corresponding code using the current data.
        emoji_code = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[0]

    if not Reaction.objects.filter(user_profile=user_profile,
                                   message=message,
                                   emoji_code=emoji_code,
                                   reaction_type=reaction_type).exists():
        raise JsonableError(_("Reaction doesn't exist."))

    # Unlike adding reactions, while deleting a reaction, we don't
    # check whether the provided (emoji_type, emoji_code) pair is
    # valid in this realm.  Since there's a row in the database, we
    # know it was valid when the user added their reaction in the
    # first place, so it is safe to just remove the reaction if it
    # exists.  And the (reaction_type, emoji_code) pair may no longer be
    # valid in legitimate situations (e.g. if a realm emoji was
    # deactivated by an administrator in the meantime).
    do_remove_reaction(user_profile, message, emoji_code, reaction_type)

    return json_success()
Exemple #11
0
def handle_remove_push_notification(user_profile_id: int, message_id: int) -> None:
    """This should be called when a message that had previously had a
    mobile push executed is read.  This triggers a mobile push notifica
    mobile app when the message is read on the server, to remove the
    message from the notification.

    """
    user_profile = get_user_profile_by_id(user_profile_id)
    message, user_message = access_message(user_profile, message_id)

    gcm_payload = get_common_payload(message)
    gcm_payload.update({
        'event': 'remove',
        'zulip_message_id': message_id,  # message_id is reserved for CCS
    })
    gcm_options = {'priority': 'normal'}  # type: Dict[str, Any]

    if uses_notification_bouncer():
        try:
            send_notifications_to_bouncer(user_profile_id,
                                          {},
                                          gcm_payload,
                                          gcm_options)
        except requests.ConnectionError:  # nocoverage
            def failure_processor(event: Dict[str, Any]) -> None:
                logger.warning(
                    "Maximum retries exceeded for trigger:%s event:push_notification" % (
                        event['user_profile_id']))
    else:
        android_devices = list(PushDeviceToken.objects.filter(
            user=user_profile, kind=PushDeviceToken.GCM))
        if android_devices:
            send_android_push_notification(android_devices, gcm_payload, gcm_options)

    user_message.flags.active_mobile_push_notification = False
    user_message.save(update_fields=["flags"])
Exemple #12
0
def json_fetch_raw_message(request, user_profile,
                           message_id=REQ(converter=to_non_negative_int)):
    # type: (HttpRequest, UserProfile, int) -> HttpResponse
    (message, user_message) = access_message(user_profile, message_id)
    return json_success({"raw_content": message.content})
Exemple #13
0
def handle_push_notification(user_profile_id: int, missed_message: Dict[str, Any]) -> None:
    """
    missed_message is the event received by the
    zerver.worker.queue_processors.PushNotificationWorker.consume function.
    """
    if not push_notifications_enabled():
        return
    user_profile = get_user_profile_by_id(user_profile_id)
    if not (receives_offline_push_notifications(user_profile) or
            receives_online_notifications(user_profile)):
        return

    user_profile = get_user_profile_by_id(user_profile_id)
    try:
        (message, user_message) = access_message(user_profile, missed_message['message_id'])
    except JsonableError:
        if ArchivedMessage.objects.filter(id=missed_message['message_id']).exists():
            # If the cause is a race with the message being deleted,
            # that's normal and we have no need to log an error.
            return
        logging.error("Unexpected message access failure handling push notifications: %s %s" % (
            user_profile.id, missed_message['message_id']))
        return

    if user_message is not None:
        # If the user has read the message already, don't push-notify.
        #
        # TODO: It feels like this is already handled when things are
        # put in the queue; maybe we should centralize this logic with
        # the `zerver/tornado/event_queue.py` logic?
        if user_message.flags.read:
            return

        # Otherwise, we mark the message as having an active mobile
        # push notification, so that we can send revocation messages
        # later.
        user_message.flags.active_mobile_push_notification = True
        user_message.save(update_fields=["flags"])
    else:
        # Users should only be getting push notifications into this
        # queue for messages they haven't received if they're
        # long-term idle; anything else is likely a bug.
        if not user_profile.long_term_idle:
            logger.error("Could not find UserMessage with message_id %s and user_id %s" % (
                missed_message['message_id'], user_profile_id))
            return

    message.trigger = missed_message['trigger']

    apns_payload = get_apns_payload(user_profile, message)
    gcm_payload = get_gcm_payload(user_profile, message)
    gcm_options = {'priority': 'high'}  # type: Dict[str, Any]
    logger.info("Sending push notifications to mobile clients for user %s" % (user_profile_id,))

    if uses_notification_bouncer():
        try:
            send_notifications_to_bouncer(user_profile_id,
                                          apns_payload,
                                          gcm_payload,
                                          gcm_options)
        except requests.ConnectionError:
            def failure_processor(event: Dict[str, Any]) -> None:
                logger.warning(
                    "Maximum retries exceeded for trigger:%s event:push_notification" % (
                        event['user_profile_id']))
            retry_event('missedmessage_mobile_notifications', missed_message,
                        failure_processor)
        return

    android_devices = list(PushDeviceToken.objects.filter(user=user_profile,
                                                          kind=PushDeviceToken.GCM))

    apple_devices = list(PushDeviceToken.objects.filter(user=user_profile,
                                                        kind=PushDeviceToken.APNS))

    if apple_devices:
        send_apple_push_notification(user_profile.id, apple_devices,
                                     apns_payload)

    if android_devices:
        send_android_push_notification(android_devices, gcm_payload, gcm_options)
Exemple #14
0
def update_message_backend(request, user_profile,
                           message_id=REQ(converter=to_non_negative_int),
                           subject=REQ(default=None),
                           propagate_mode=REQ(default="change_one"),
                           content=REQ(default=None)):
    # type: (HttpRequest, UserProfile, int, Optional[Text], Optional[str], Optional[Text]) -> HttpResponse
    if not user_profile.realm.allow_message_editing:
        return json_error(_("Your organization has turned off message editing."))

    message, ignored_user_message = access_message(user_profile, message_id)

    # You only have permission to edit a message if:
    # 1. You sent it, OR:
    # 2. This is a topic-only edit for a (no topic) message, OR:
    # 3. This is a topic-only edit and you are an admin.
    if message.sender == user_profile:
        pass
    elif (content is None) and ((message.topic_name() == "(no topic)") or
                                user_profile.is_realm_admin):
        pass
    else:
        raise JsonableError(_("You don't have permission to edit this message"))

    # If there is a change to the content, check that it hasn't been too long
    # Allow an extra 20 seconds since we potentially allow editing 15 seconds
    # past the limit, and in case there are network issues, etc. The 15 comes
    # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if
    # you change this value also change those two parameters in message_edit.js.
    edit_limit_buffer = 20
    if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0:
        deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer
        if (timezone.now() - message.pub_date) > datetime.timedelta(seconds=deadline_seconds):
            raise JsonableError(_("The time limit for editing this message has past"))

    if subject is None and content is None:
        return json_error(_("Nothing to change"))
    if subject is not None:
        subject = subject.strip()
        if subject == "":
            raise JsonableError(_("Topic can't be empty"))
    rendered_content = None
    links_for_embed = set()  # type: Set[Text]
    if content is not None:
        content = content.strip()
        if content == "":
            content = "(deleted)"
        content = truncate_body(content)

        # We exclude UserMessage.flags.historical rows since those
        # users did not receive the message originally, and thus
        # probably are not relevant for reprocessed alert_words,
        # mentions and similar rendering features.  This may be a
        # decision we change in the future.
        ums = UserMessage.objects.filter(
            message=message.id,
            flags=~UserMessage.flags.historical)

        message_users = UserProfile.objects.select_related().filter(
            id__in={um.user_profile_id for um in ums})

        # We render the message using the current user's realm; since
        # the cross-realm bots never edit messages, this should be
        # always correct.
        # Note: If rendering fails, the called code will raise a JsonableError.
        rendered_content = render_incoming_message(message,
                                                   content,
                                                   message_users,
                                                   user_profile.realm)
        links_for_embed |= message.links_for_preview

    number_changed = do_update_message(user_profile, message, subject,
                                       propagate_mode, content, rendered_content)
    # Include the number of messages changed in the logs
    request._log_data['extra'] = "[%s]" % (number_changed,)
    if links_for_embed and getattr(settings, 'INLINE_URL_EMBED_PREVIEW', None):
        event_data = {
            'message_id': message.id,
            'message_content': message.content,
            # The choice of `user_profile.realm_id` rather than
            # `sender.realm_id` must match the decision made in the
            # `render_incoming_message` call earlier in this function.
            'message_realm_id': user_profile.realm_id,
            'urls': links_for_embed}
        queue_json_publish('embed_links', event_data, lambda x: None)
    return json_success()
Exemple #15
0
def add_reaction(
    request: HttpRequest,
    user_profile: UserProfile,
    message_id: int,
    emoji_name: str = REQ(),
    emoji_code: Optional[str] = REQ(default=None),
    reaction_type: Optional[str] = REQ(default=None)
) -> HttpResponse:
    message, user_message = access_message(user_profile, message_id)

    if emoji_code is None:
        # The emoji_code argument is only required for rare corner
        # cases discussed in the long block comment below.  For simple
        # API clients, we allow specifying just the name, and just
        # look up the code using the current name->code mapping.
        emoji_code = emoji_name_to_emoji_code(message.sender.realm,
                                              emoji_name)[0]

    if reaction_type is None:
        reaction_type = emoji_name_to_emoji_code(message.sender.realm,
                                                 emoji_name)[1]

    if Reaction.objects.filter(user_profile=user_profile,
                               message=message,
                               emoji_code=emoji_code,
                               reaction_type=reaction_type).exists():
        raise JsonableError(_("Reaction already exists."))

    query = Reaction.objects.filter(message=message,
                                    emoji_code=emoji_code,
                                    reaction_type=reaction_type)
    if query.exists():
        # If another user has already reacted to this message with
        # same emoji code, we treat the new reaction as a vote for the
        # existing reaction.  So the emoji name used by that earlier
        # reaction takes precedence over whatever was passed in this
        # request.  This is necessary to avoid a message having 2
        # "different" emoji reactions with the same emoji code (and
        # thus same image) on the same message, which looks ugly.
        #
        # In this "voting for an existing reaction" case, we shouldn't
        # check whether the emoji code and emoji name match, since
        # it's possible that the (emoji_type, emoji_name, emoji_code)
        # triple for this existing rection xmay not pass validation
        # now (e.g. because it is for a realm emoji that has been
        # since deactivated).  We still want to allow users to add a
        # vote any old reaction they see in the UI even if that is a
        # deactivated custom emoji, so we just use the emoji name from
        # the existing reaction with no further validation.
        emoji_name = query.first().emoji_name
    else:
        # Otherwise, use the name provided in this request, but verify
        # it is valid in the user's realm (e.g. not a deactivated
        # realm emoji).
        check_emoji_request(message.sender.realm, emoji_name, emoji_code,
                            reaction_type)

    if user_message is None:
        create_historical_message(user_profile, message)

    do_add_reaction(user_profile, message, emoji_name, emoji_code,
                    reaction_type)

    return json_success()
Exemple #16
0
def update_message_backend(
    request: HttpRequest,
    user_profile: UserMessage,
    message_id: int = REQ(converter=to_non_negative_int, path_only=True),
    stream_id: Optional[int] = REQ(converter=to_non_negative_int,
                                   default=None),
    topic_name: Optional[str] = REQ_topic(),
    propagate_mode: Optional[str] = REQ(
        default="change_one",
        str_validator=check_string_in(PROPAGATE_MODE_VALUES)),
    send_notification_to_old_thread: bool = REQ(default=True,
                                                validator=check_bool),
    send_notification_to_new_thread: bool = REQ(default=True,
                                                validator=check_bool),
    content: Optional[str] = REQ(default=None)
) -> HttpResponse:
    if not user_profile.realm.allow_message_editing:
        return json_error(
            _("Your organization has turned off message editing"))

    if propagate_mode != "change_one" and topic_name is None and stream_id is None:
        return json_error(_("Invalid propagate_mode without topic edit"))

    message, ignored_user_message = access_message(user_profile, message_id)
    is_no_topic_msg = (message.topic_name() == "(no topic)")

    # You only have permission to edit a message if:
    # you change this value also change those two parameters in message_edit.js.
    # 1. You sent it, OR:
    # 2. This is a topic-only edit for a (no topic) message, OR:
    # 3. This is a topic-only edit and you are an admin, OR:
    # 4. This is a topic-only edit and your realm allows users to edit topics.
    if message.sender == user_profile:
        pass
    elif (content is
          None) and (is_no_topic_msg or user_profile.is_realm_admin
                     or user_profile.realm.allow_community_topic_editing):
        pass
    else:
        raise JsonableError(
            _("You don't have permission to edit this message"))

    # If there is a change to the content, check that it hasn't been too long
    # Allow an extra 20 seconds since we potentially allow editing 15 seconds
    # past the limit, and in case there are network issues, etc. The 15 comes
    # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if
    # you change this value also change those two parameters in message_edit.js.
    edit_limit_buffer = 20
    if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0:
        deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer
        if (timezone_now() - message.date_sent) > datetime.timedelta(
                seconds=deadline_seconds):
            raise JsonableError(
                _("The time limit for editing this message has passed"))

    # If there is a change to the topic, check that the user is allowed to
    # edit it and that it has not been too long. If this is not the user who
    # sent the message, they are not the admin, and the time limit for editing
    # topics is passed, raise an error.
    if content is None and message.sender != user_profile and not user_profile.is_realm_admin and \
            not is_no_topic_msg:
        deadline_seconds = Realm.DEFAULT_COMMUNITY_TOPIC_EDITING_LIMIT_SECONDS + edit_limit_buffer
        if (timezone_now() - message.date_sent) > datetime.timedelta(
                seconds=deadline_seconds):
            raise JsonableError(
                _("The time limit for editing this message has passed"))

    if topic_name is None and content is None and stream_id is None:
        return json_error(_("Nothing to change"))
    if topic_name is not None:
        topic_name = topic_name.strip()
        if topic_name == "":
            raise JsonableError(_("Topic can't be empty"))
    rendered_content = None
    links_for_embed: Set[str] = set()
    prior_mention_user_ids: Set[int] = set()
    mention_user_ids: Set[int] = set()
    mention_data: Optional[MentionData] = None
    if content is not None:
        content = content.strip()
        if content == "":
            content = "(deleted)"
        content = truncate_body(content)

        mention_data = MentionData(
            realm_id=user_profile.realm.id,
            content=content,
        )
        user_info = get_user_info_for_message_updates(message.id)
        prior_mention_user_ids = user_info['mention_user_ids']

        # We render the message using the current user's realm; since
        # the cross-realm bots never edit messages, this should be
        # always correct.
        # Note: If rendering fails, the called code will raise a JsonableError.
        rendered_content = render_incoming_message(
            message,
            content,
            user_info['message_user_ids'],
            user_profile.realm,
            mention_data=mention_data)
        links_for_embed |= message.links_for_preview

        mention_user_ids = message.mentions_user_ids

    new_stream = None
    old_stream = None
    number_changed = 0

    if stream_id is not None:
        if not user_profile.is_realm_admin:
            raise JsonableError(
                _("You don't have permission to move this message"))
        if content is not None:
            raise JsonableError(
                _("Cannot change message content while changing stream"))

        old_stream = get_stream_by_id(message.recipient.type_id)
        new_stream = get_stream_by_id(stream_id)

        if not (old_stream.is_public() and new_stream.is_public()):
            # We'll likely decide to relax this condition in the
            # future; it just requires more care with details like the
            # breadcrumb messages.
            raise JsonableError(_("Streams must be public"))

    number_changed = do_update_message(
        user_profile, message, new_stream, topic_name, propagate_mode,
        send_notification_to_old_thread, send_notification_to_new_thread,
        content, rendered_content, prior_mention_user_ids, mention_user_ids,
        mention_data)

    # Include the number of messages changed in the logs
    request._log_data['extra'] = f"[{number_changed}]"
    if links_for_embed:
        event_data = {
            'message_id': message.id,
            'message_content': message.content,
            # The choice of `user_profile.realm_id` rather than
            # `sender.realm_id` must match the decision made in the
            # `render_incoming_message` call earlier in this function.
            'message_realm_id': user_profile.realm_id,
            'urls': links_for_embed
        }
        queue_json_publish('embed_links', event_data)
    return json_success()
Exemple #17
0
def json_fetch_raw_message(request,
                           user_profile,
                           message_id=REQ(converter=to_non_negative_int)):
    # type: (HttpRequest, UserProfile, int) -> HttpResponse
    (message, user_message) = access_message(user_profile, message_id)
    return json_success({"raw_content": message.content})
Exemple #18
0
def handle_push_notification(user_profile_id: int, missed_message: Dict[str, Any]) -> None:
    """
    missed_message is the event received by the
    zerver.worker.queue_processors.PushNotificationWorker.consume function.
    """
    user_profile = get_user_profile_by_id(user_profile_id)
    if not (receives_offline_push_notifications(user_profile) or
            receives_online_notifications(user_profile)):
        return

    user_profile = get_user_profile_by_id(user_profile_id)
    (message, user_message) = access_message(user_profile, missed_message['message_id'])
    if user_message is not None:
        # If ther user has read the message already, don't push-notify.
        #
        # TODO: It feels like this is already handled when things are
        # put in the queue; maybe we should centralize this logic with
        # the `zerver/tornado/event_queue.py` logic?
        if user_message.flags.read:
            return
    else:
        # Users should only be getting push notifications into this
        # queue for messages they haven't received if they're
        # long-term idle; anything else is likely a bug.
        if not user_profile.long_term_idle:
            logging.error("Could not find UserMessage with message_id %s and user_id %s" % (
                missed_message['message_id'], user_profile_id))
            return

    message.trigger = missed_message['trigger']
    message.stream_name = missed_message.get('stream_name', None)

    apns_payload = get_apns_payload(message)
    gcm_payload = get_gcm_payload(user_profile, message)
    logging.info("Sending push notification to user %s" % (user_profile_id,))

    if uses_notification_bouncer():
        try:
            send_notifications_to_bouncer(user_profile_id,
                                          apns_payload,
                                          gcm_payload)
        except requests.ConnectionError:
            def failure_processor(event: Dict[str, Any]) -> None:
                logging.warning(
                    "Maximum retries exceeded for trigger:%s event:push_notification" % (
                        event['user_profile_id']))
            retry_event('missedmessage_mobile_notifications', missed_message,
                        failure_processor)
        return

    android_devices = list(PushDeviceToken.objects.filter(user=user_profile,
                                                          kind=PushDeviceToken.GCM))

    apple_devices = list(PushDeviceToken.objects.filter(user=user_profile,
                                                        kind=PushDeviceToken.APNS))

    if apple_devices:
        send_apple_push_notification(user_profile.id, apple_devices,
                                     apns_payload)

    if android_devices:
        send_android_push_notification(android_devices, gcm_payload)
Exemple #19
0
def handle_push_notification(user_profile_id: int,
                             missed_message: Dict[str, Any]) -> None:
    """
    missed_message is the event received by the
    zerver.worker.queue_processors.PushNotificationWorker.consume function.
    """
    if not push_notifications_enabled():
        return
    user_profile = get_user_profile_by_id(user_profile_id)

    if user_profile.is_bot:
        # BUG: Investigate why it's possible to get here.
        return  # nocoverage

    if not (user_profile.enable_offline_push_notifications
            or user_profile.enable_online_push_notifications):
        # BUG: Investigate why it's possible to get here.
        return  # nocoverage

    try:
        (message, user_message) = access_message(user_profile,
                                                 missed_message["message_id"])
    except JsonableError:
        if ArchivedMessage.objects.filter(
                id=missed_message["message_id"]).exists():
            # If the cause is a race with the message being deleted,
            # that's normal and we have no need to log an error.
            return
        logging.info(
            "Unexpected message access failure handling push notifications: %s %s",
            user_profile.id,
            missed_message["message_id"],
        )
        return

    if user_message is not None:
        # If the user has read the message already, don't push-notify.
        if user_message.flags.read or user_message.flags.active_mobile_push_notification:
            return

        # Otherwise, we mark the message as having an active mobile
        # push notification, so that we can send revocation messages
        # later.
        user_message.flags.active_mobile_push_notification = True
        user_message.save(update_fields=["flags"])
    else:
        # Users should only be getting push notifications into this
        # queue for messages they haven't received if they're
        # long-term idle; anything else is likely a bug.
        if not user_profile.long_term_idle:
            logger.error(
                "Could not find UserMessage with message_id %s and user_id %s",
                missed_message["message_id"],
                user_profile_id,
                exc_info=True,
            )
            return

    trigger = missed_message["trigger"]
    mentioned_user_group_name = None
    mentioned_user_group_id = missed_message.get("mentioned_user_group_id")

    if mentioned_user_group_id is not None:
        user_group = access_user_group_by_id(mentioned_user_group_id,
                                             user_profile,
                                             for_mention=True)
        mentioned_user_group_name = user_group.name

    apns_payload = get_message_payload_apns(user_profile, message, trigger,
                                            mentioned_user_group_id,
                                            mentioned_user_group_name)
    gcm_payload, gcm_options = get_message_payload_gcm(
        user_profile, message, trigger, mentioned_user_group_id,
        mentioned_user_group_name)
    logger.info("Sending push notifications to mobile clients for user %s",
                user_profile_id)

    if uses_notification_bouncer():
        send_notifications_to_bouncer(user_profile_id, apns_payload,
                                      gcm_payload, gcm_options)
        return

    android_devices = list(
        PushDeviceToken.objects.filter(user=user_profile,
                                       kind=PushDeviceToken.GCM))

    apple_devices = list(
        PushDeviceToken.objects.filter(user=user_profile,
                                       kind=PushDeviceToken.APNS))

    send_apple_push_notification(user_profile.id, apple_devices, apns_payload)

    send_android_push_notification(android_devices, gcm_payload, gcm_options)
Exemple #20
0
def do_update_message_flags(user_profile: UserProfile, operation: str,
                            flag: str,
                            messages: List[int]) -> Tuple[int, List[int]]:
    valid_flags = [
        item for item in UserMessage.flags
        if item not in UserMessage.NON_API_FLAGS
    ]
    if flag not in valid_flags:
        raise JsonableError(_("Invalid flag: '{}'").format(flag))
    if flag in UserMessage.NON_EDITABLE_FLAGS:
        raise JsonableError(_("Flag not editable: '{}'").format(flag))
    if operation not in ("add", "remove"):
        raise JsonableError(
            _("Invalid message flag operation: '{}'").format(operation))
    flagattr = getattr(UserMessage.flags, flag)

    msgs = UserMessage.objects.filter(user_profile=user_profile,
                                      message_id__in=messages)
    um_message_ids = {um.message_id for um in msgs}
    historical_message_ids = list(set(messages) - um_message_ids)

    # Users can mutate flags for messages that don't have a UserMessage yet.
    # First, validate that the user is even allowed to access these message_ids.
    for message_id in historical_message_ids:
        access_message(user_profile, message_id)

    # And then create historical UserMessage records.  See the called function for more context.
    create_historical_user_messages(user_id=user_profile.id,
                                    message_ids=historical_message_ids)
    with transaction.atomic():
        if operation == "add":
            msgs = (msgs.select_for_update().order_by("message_id").extra(
                where=[UserMessage.where_flag_is_absent(flagattr)]))
            updated_message_ids = [um.message_id for um in msgs]
            msgs.filter(message_id__in=updated_message_ids).update(
                flags=F("flags").bitor(flagattr))
        elif operation == "remove":
            msgs = (msgs.select_for_update().order_by("message_id").extra(
                where=[UserMessage.where_flag_is_present(flagattr)]))
            updated_message_ids = [um.message_id for um in msgs]
            msgs.filter(message_id__in=updated_message_ids).update(
                flags=F("flags").bitand(~flagattr))

    count = len(updated_message_ids)
    event = {
        "type": "update_message_flags",
        "op": operation,
        "operation": operation,
        "flag": flag,
        "messages": updated_message_ids,
        "all": False,
    }

    if flag == "read" and operation == "remove":
        # When removing the read flag (i.e. marking messages as
        # unread), extend the event with an additional object with
        # details on the messages required to update the client's
        # `unread_msgs` data structure.
        raw_unread_data = get_raw_unread_data(user_profile,
                                              updated_message_ids)
        event["message_details"] = format_unread_message_details(
            user_profile.id, raw_unread_data)

    send_event(user_profile.realm, event, [user_profile.id])

    if flag == "read" and operation == "add":
        event_time = timezone_now()
        do_clear_mobile_push_notifications_for_ids([user_profile.id],
                                                   updated_message_ids)

        do_increment_logging_stat(user_profile,
                                  COUNT_STATS["messages_read::hour"],
                                  None,
                                  event_time,
                                  increment=count)
        do_increment_logging_stat(
            user_profile,
            COUNT_STATS["messages_read_interactions::hour"],
            None,
            event_time,
            increment=min(1, count),
        )

    return count, updated_message_ids
def handle_push_notification(user_profile_id: int,
                             missed_message: Dict[str, Any]) -> None:
    """
    missed_message is the event received by the
    zerver.worker.queue_processors.PushNotificationWorker.consume function.
    """
    if not push_notifications_enabled():
        return
    user_profile = get_user_profile_by_id(user_profile_id)
    if not (receives_offline_push_notifications(user_profile)
            or receives_online_notifications(user_profile)):
        return

    user_profile = get_user_profile_by_id(user_profile_id)
    try:
        (message, user_message) = access_message(user_profile,
                                                 missed_message['message_id'])
    except JsonableError:
        if ArchivedMessage.objects.filter(
                id=missed_message['message_id']).exists():
            # If the cause is a race with the message being deleted,
            # that's normal and we have no need to log an error.
            return
        logging.error(
            "Unexpected message access failure handling push notifications: %s %s"
            % (user_profile.id, missed_message['message_id']))
        return

    if user_message is not None:
        # If the user has read the message already, don't push-notify.
        #
        # TODO: It feels like this is already handled when things are
        # put in the queue; maybe we should centralize this logic with
        # the `zerver/tornado/event_queue.py` logic?
        if user_message.flags.read:
            return

        # Otherwise, we mark the message as having an active mobile
        # push notification, so that we can send revocation messages
        # later.
        user_message.flags.active_mobile_push_notification = True
        user_message.save(update_fields=["flags"])
    else:
        # Users should only be getting push notifications into this
        # queue for messages they haven't received if they're
        # long-term idle; anything else is likely a bug.
        if not user_profile.long_term_idle:
            logger.error(
                "Could not find UserMessage with message_id %s and user_id %s"
                % (missed_message['message_id'], user_profile_id))
            return

    message.trigger = missed_message['trigger']

    apns_payload = get_message_payload_apns(user_profile, message)
    gcm_payload, gcm_options = get_message_payload_gcm(user_profile, message)
    logger.info("Sending push notifications to mobile clients for user %s" %
                (user_profile_id, ))

    if uses_notification_bouncer():
        try:
            send_notifications_to_bouncer(user_profile_id, apns_payload,
                                          gcm_payload, gcm_options)
        except requests.ConnectionError:

            def failure_processor(event: Dict[str, Any]) -> None:
                logger.warning(
                    "Maximum retries exceeded for trigger:%s event:push_notification"
                    % (event['user_profile_id']))

            retry_event('missedmessage_mobile_notifications', missed_message,
                        failure_processor)
        return

    android_devices = list(
        PushDeviceToken.objects.filter(user=user_profile,
                                       kind=PushDeviceToken.GCM))

    apple_devices = list(
        PushDeviceToken.objects.filter(user=user_profile,
                                       kind=PushDeviceToken.APNS))

    if apple_devices:
        send_apple_push_notification(user_profile.id, apple_devices,
                                     apns_payload)

    if android_devices:
        send_android_push_notification(android_devices, gcm_payload,
                                       gcm_options)
def handle_push_notification(user_profile_id: int,
                             missed_message: Dict[str, Any]) -> None:
    """
    missed_message is the event received by the
    zerver.worker.queue_processors.PushNotificationWorker.consume function.
    """
    user_profile = get_user_profile_by_id(user_profile_id)
    if not (receives_offline_push_notifications(user_profile)
            or receives_online_notifications(user_profile)):
        return

    user_profile = get_user_profile_by_id(user_profile_id)
    (message, user_message) = access_message(user_profile,
                                             missed_message['message_id'])
    if user_message is not None:
        # If ther user has read the message already, don't push-notify.
        #
        # TODO: It feels like this is already handled when things are
        # put in the queue; maybe we should centralize this logic with
        # the `zerver/tornado/event_queue.py` logic?
        if user_message.flags.read:
            return
    else:
        # Users should only be getting push notifications into this
        # queue for messages they haven't received if they're
        # long-term idle; anything else is likely a bug.
        if not user_profile.long_term_idle:
            logging.error(
                "Could not find UserMessage with message_id %s and user_id %s"
                % (missed_message['message_id'], user_profile_id))
            return

    message.trigger = missed_message['trigger']
    message.stream_name = missed_message.get('stream_name', None)

    apns_payload = get_apns_payload(message)
    gcm_payload = get_gcm_payload(user_profile, message)
    logging.info("Sending push notification to user %s" % (user_profile_id, ))

    if uses_notification_bouncer():
        try:
            send_notifications_to_bouncer(user_profile_id, apns_payload,
                                          gcm_payload)
        except requests.ConnectionError:

            def failure_processor(event: Dict[str, Any]) -> None:
                logging.warning(
                    "Maximum retries exceeded for trigger:%s event:push_notification"
                    % (event['user_profile_id']))

            retry_event('missedmessage_mobile_notifications', missed_message,
                        failure_processor)
        return

    android_devices = list(
        PushDeviceToken.objects.filter(user=user_profile,
                                       kind=PushDeviceToken.GCM))

    apple_devices = list(
        PushDeviceToken.objects.filter(user=user_profile,
                                       kind=PushDeviceToken.APNS))

    if apple_devices:
        send_apple_push_notification(user_profile.id, apple_devices,
                                     apns_payload)

    if android_devices:
        send_android_push_notification(android_devices, gcm_payload)
Exemple #23
0
def update_message_backend(request, user_profile,
                           message_id=REQ(converter=to_non_negative_int),
                           subject=REQ(default=None),
                           propagate_mode=REQ(default="change_one"),
                           content=REQ(default=None)):
    # type: (HttpRequest, UserProfile, int, Optional[Text], Optional[str], Optional[Text]) -> HttpResponse
    if not user_profile.realm.allow_message_editing:
        return json_error(_("Your organization has turned off message editing."))

    message, ignored_user_message = access_message(user_profile, message_id)

    # You only have permission to edit a message if:
    # 1. You sent it, OR:
    # 2. This is a topic-only edit for a (no topic) message, OR:
    # 3. This is a topic-only edit and you are an admin.
    if message.sender == user_profile:
        pass
    elif (content is None) and ((message.topic_name() == "(no topic)") or
                                user_profile.is_realm_admin):
        pass
    else:
        raise JsonableError(_("You don't have permission to edit this message"))

    # If there is a change to the content, check that it hasn't been too long
    # Allow an extra 20 seconds since we potentially allow editing 15 seconds
    # past the limit, and in case there are network issues, etc. The 15 comes
    # from (min_seconds_to_edit + seconds_left_buffer) in message_edit.js; if
    # you change this value also change those two parameters in message_edit.js.
    edit_limit_buffer = 20
    if content is not None and user_profile.realm.message_content_edit_limit_seconds > 0:
        deadline_seconds = user_profile.realm.message_content_edit_limit_seconds + edit_limit_buffer
        if (timezone.now() - message.pub_date) > datetime.timedelta(seconds=deadline_seconds):
            raise JsonableError(_("The time limit for editing this message has past"))

    if subject is None and content is None:
        return json_error(_("Nothing to change"))
    if subject is not None:
        subject = subject.strip()
        if subject == "":
            raise JsonableError(_("Topic can't be empty"))
    rendered_content = None
    links_for_embed = set()  # type: Set[Text]
    if content is not None:
        content = content.strip()
        if content == "":
            content = "(deleted)"
        content = truncate_body(content)

        # We exclude UserMessage.flags.historical rows since those
        # users did not receive the message originally, and thus
        # probably are not relevant for reprocessed alert_words,
        # mentions and similar rendering features.  This may be a
        # decision we change in the future.
        ums = UserMessage.objects.filter(
            message=message.id,
            flags=~UserMessage.flags.historical)

        message_users = UserProfile.objects.select_related().filter(
            id__in={um.user_profile_id for um in ums})

        # We render the message using the current user's realm; since
        # the cross-realm bots never edit messages, this should be
        # always correct.
        # Note: If rendering fails, the called code will raise a JsonableError.
        rendered_content = render_incoming_message(message,
                                                   content,
                                                   message_users,
                                                   user_profile.realm)
        links_for_embed |= message.links_for_preview

    number_changed = do_update_message(user_profile, message, subject,
                                       propagate_mode, content, rendered_content)
    # Include the number of messages changed in the logs
    request._log_data['extra'] = "[%s]" % (number_changed,)
    if links_for_embed and getattr(settings, 'INLINE_URL_EMBED_PREVIEW', None):
        event_data = {
            'message_id': message.id,
            'message_content': message.content,
            # The choice of `user_profile.realm_id` rather than
            # `sender.realm_id` must match the decision made in the
            # `render_incoming_message` call earlier in this function.
            'message_realm_id': user_profile.realm_id,
            'urls': links_for_embed}
        queue_json_publish('embed_links', event_data, lambda x: None)
    return json_success()
Exemple #24
0
def handle_push_notification(user_profile_id: int,
                             missed_message: Dict[str, Any]) -> None:
    """
    missed_message is the event received by the
    zerver.worker.queue_processors.PushNotificationWorker.consume function.
    """
    if not push_notifications_enabled():
        return
    user_profile = get_user_profile_by_id(user_profile_id)

    if user_profile.is_bot:  # nocoverage
        # We don't expect to reach here for bot users. However, this code exists
        # to find and throw away any pre-existing events in the queue while
        # upgrading from versions before our notifiability logic was implemented.
        # TODO/compatibility: This block can be removed when one can no longer
        # upgrade from versions <= 4.0 to versions >= 5.0
        logger.warning(
            "Send-push-notification event found for bot user %s. Skipping.",
            user_profile_id)
        return

    if not (user_profile.enable_offline_push_notifications
            or user_profile.enable_online_push_notifications):
        # BUG: Investigate why it's possible to get here.
        return  # nocoverage

    try:
        (message, user_message) = access_message(user_profile,
                                                 missed_message["message_id"])
    except JsonableError:
        if ArchivedMessage.objects.filter(
                id=missed_message["message_id"]).exists():
            # If the cause is a race with the message being deleted,
            # that's normal and we have no need to log an error.
            return
        logging.info(
            "Unexpected message access failure handling push notifications: %s %s",
            user_profile.id,
            missed_message["message_id"],
        )
        return

    if user_message is not None:
        # If the user has read the message already, don't push-notify.
        if user_message.flags.read or user_message.flags.active_mobile_push_notification:
            return

        # Otherwise, we mark the message as having an active mobile
        # push notification, so that we can send revocation messages
        # later.
        user_message.flags.active_mobile_push_notification = True
        user_message.save(update_fields=["flags"])
    else:
        # Users should only be getting push notifications into this
        # queue for messages they haven't received if they're
        # long-term idle; anything else is likely a bug.
        if not user_profile.long_term_idle:
            logger.error(
                "Could not find UserMessage with message_id %s and user_id %s",
                missed_message["message_id"],
                user_profile_id,
                exc_info=True,
            )
            return

    trigger = missed_message["trigger"]
    mentioned_user_group_name = None
    # mentioned_user_group_id will be None if the user is personally mentioned
    # regardless whether they are a member of the mentioned user group in the
    # message or not.
    mentioned_user_group_id = missed_message.get("mentioned_user_group_id")

    if mentioned_user_group_id is not None:
        user_group = UserGroup.objects.get(id=mentioned_user_group_id,
                                           realm=user_profile.realm)
        mentioned_user_group_name = user_group.name

    # Soft reactivate if pushing to a long_term_idle user that is personally mentioned
    soft_reactivate_if_personal_notification(user_profile, {trigger},
                                             mentioned_user_group_name)

    apns_payload = get_message_payload_apns(user_profile, message, trigger,
                                            mentioned_user_group_id,
                                            mentioned_user_group_name)
    gcm_payload, gcm_options = get_message_payload_gcm(
        user_profile, message, trigger, mentioned_user_group_id,
        mentioned_user_group_name)
    logger.info("Sending push notifications to mobile clients for user %s",
                user_profile_id)

    if uses_notification_bouncer():
        total_android_devices, total_apple_devices = send_notifications_to_bouncer(
            user_profile_id, apns_payload, gcm_payload, gcm_options)
        logger.info(
            "Sent mobile push notifications for user %s through bouncer: %s via FCM devices, %s via APNs devices",
            user_profile_id,
            total_android_devices,
            total_apple_devices,
        )
        return

    android_devices = list(
        PushDeviceToken.objects.filter(user=user_profile,
                                       kind=PushDeviceToken.GCM))

    apple_devices = list(
        PushDeviceToken.objects.filter(user=user_profile,
                                       kind=PushDeviceToken.APNS))

    logger.info(
        "Sending mobile push notifications for local user %s: %s via FCM devices, %s via APNs devices",
        user_profile_id,
        len(android_devices),
        len(apple_devices),
    )
    user_identity = UserPushIndentityCompat(user_id=user_profile.id)
    send_apple_push_notification(user_identity, apple_devices, apns_payload)
    send_android_push_notification(user_identity, android_devices, gcm_payload,
                                   gcm_options)