def send_response_message(bot_id: int, message_info: Dict[str, Any], response_data: Dict[str, Any]) -> None: """ bot_id is the user_id of the bot sending the response message_info is used to address the message and should have these fields: type - "stream" or "private" display_recipient - like we have in other message events topic - see get_topic_from_message_info response_data is what the bot wants to send back and has these fields: content - raw Markdown content for Zulip to render WARNING: This function sends messages bypassing the stream access check for the bot - so use with caution to not call this in codepaths that might let someone send arbitrary messages to any stream through this. """ message_type = message_info["type"] display_recipient = message_info["display_recipient"] try: topic_name: Optional[str] = get_topic_from_message_info(message_info) except KeyError: topic_name = None bot_user = get_user_profile_by_id(bot_id) realm = bot_user.realm client = get_client("OutgoingWebhookResponse") content = response_data.get("content") assert content widget_content = response_data.get("widget_content") if message_type == "stream": message_to = [display_recipient] elif message_type == "private": message_to = [recipient["email"] for recipient in display_recipient] else: raise JsonableError(_("Invalid message type")) check_send_message( sender=bot_user, client=client, message_type_name=message_type, message_to=message_to, topic_name=topic_name, message_content=content, widget_content=widget_content, realm=realm, skip_stream_access_check=True, )
def send_mm_reply_to_stream(user_profile: UserProfile, stream: Stream, topic: str, body: str) -> None: try: check_send_message( sender=user_profile, client=get_client("Internal"), message_type_name="stream", message_to=[stream.id], topic_name=topic, message_content=body, ) except JsonableError as error: error_message = "Error sending message to stream {stream} via message notification email reply:\n{error}".format( stream=stream.name, error=error.msg) internal_send_private_message( get_system_bot(settings.NOTIFICATION_BOT, user_profile.realm_id), user_profile, error_message, )
def test_get_events(self) -> None: user_profile = self.example_user("hamlet") email = user_profile.email recipient_user_profile = self.example_user("othello") recipient_email = recipient_user_profile.email self.login_user(user_profile) result = self.tornado_call( get_events, user_profile, { "apply_markdown": orjson.dumps(True).decode(), "client_gravatar": orjson.dumps(True).decode(), "event_types": orjson.dumps(["message"]).decode(), "user_client": "website", "dont_block": orjson.dumps(True).decode(), }, ) self.assert_json_success(result) queue_id = orjson.loads(result.content)["queue_id"] recipient_result = self.tornado_call( get_events, recipient_user_profile, { "apply_markdown": orjson.dumps(True).decode(), "client_gravatar": orjson.dumps(True).decode(), "event_types": orjson.dumps(["message"]).decode(), "user_client": "website", "dont_block": orjson.dumps(True).decode(), }, ) self.assert_json_success(recipient_result) recipient_queue_id = orjson.loads(recipient_result.content)["queue_id"] result = self.tornado_call( get_events, user_profile, { "queue_id": queue_id, "user_client": "website", "last_event_id": -1, "dont_block": orjson.dumps(True).decode(), }, ) events = orjson.loads(result.content)["events"] self.assert_json_success(result) self.assert_length(events, 0) local_id = "10.01" check_send_message( sender=user_profile, client=get_client("whatever"), message_type_name="private", message_to=[recipient_email], topic_name=None, message_content="hello", local_id=local_id, sender_queue_id=queue_id, ) result = self.tornado_call( get_events, user_profile, { "queue_id": queue_id, "user_client": "website", "last_event_id": -1, "dont_block": orjson.dumps(True).decode(), }, ) events = orjson.loads(result.content)["events"] self.assert_json_success(result) self.assert_length(events, 1) self.assertEqual(events[0]["type"], "message") self.assertEqual(events[0]["message"]["sender_email"], email) self.assertEqual(events[0]["local_message_id"], local_id) self.assertEqual( events[0]["message"]["display_recipient"][0]["is_mirror_dummy"], False) self.assertEqual( events[0]["message"]["display_recipient"][1]["is_mirror_dummy"], False) last_event_id = events[0]["id"] local_id = "10.02" check_send_message( sender=user_profile, client=get_client("whatever"), message_type_name="private", message_to=[recipient_email], topic_name=None, message_content="hello", local_id=local_id, sender_queue_id=queue_id, ) result = self.tornado_call( get_events, user_profile, { "queue_id": queue_id, "user_client": "website", "last_event_id": last_event_id, "dont_block": orjson.dumps(True).decode(), }, ) events = orjson.loads(result.content)["events"] self.assert_json_success(result) self.assert_length(events, 1) self.assertEqual(events[0]["type"], "message") self.assertEqual(events[0]["message"]["sender_email"], email) self.assertEqual(events[0]["local_message_id"], local_id) # Test that the received message in the receiver's event queue # exists and does not contain a local id recipient_result = self.tornado_call( get_events, recipient_user_profile, { "queue_id": recipient_queue_id, "user_client": "website", "last_event_id": -1, "dont_block": orjson.dumps(True).decode(), }, ) recipient_events = orjson.loads(recipient_result.content)["events"] self.assert_json_success(recipient_result) self.assert_length(recipient_events, 2) self.assertEqual(recipient_events[0]["type"], "message") self.assertEqual(recipient_events[0]["message"]["sender_email"], email) self.assertTrue("local_message_id" not in recipient_events[0]) self.assertEqual(recipient_events[1]["type"], "message") self.assertEqual(recipient_events[1]["message"]["sender_email"], email) self.assertTrue("local_message_id" not in recipient_events[1])
def send_message_backend( request: HttpRequest, user_profile: UserProfile, message_type_name: str = REQ("type"), req_to: Optional[str] = REQ("to", default=None), req_sender: Optional[str] = REQ("sender", default=None, documentation_pending=True), forged_str: Optional[str] = REQ("forged", default=None, documentation_pending=True), topic_name: Optional[str] = REQ_topic(), message_content: str = REQ("content"), widget_content: Optional[str] = REQ(default=None, documentation_pending=True), realm_str: Optional[str] = REQ("realm_str", default=None, documentation_pending=True), local_id: Optional[str] = REQ(default=None), queue_id: Optional[str] = REQ(default=None), delivery_type: str = REQ("delivery_type", default="send_now", documentation_pending=True), defer_until: Optional[str] = REQ("deliver_at", default=None, documentation_pending=True), tz_guess: Optional[str] = REQ("tz_guess", default=None, documentation_pending=True), time: Optional[float] = REQ(default=None, converter=to_float, documentation_pending=True), ) -> HttpResponse: # If req_to is None, then we default to an # empty list of recipients. message_to: Union[Sequence[int], Sequence[str]] = [] if req_to is not None: if message_type_name == "stream": stream_indicator = extract_stream_indicator(req_to) # For legacy reasons check_send_message expects # a list of streams, instead of a single stream. # # Also, mypy can't detect that a single-item # list populated from a Union[int, str] is actually # a Union[Sequence[int], Sequence[str]]. if isinstance(stream_indicator, int): message_to = [stream_indicator] else: message_to = [stream_indicator] else: message_to = extract_private_recipients(req_to) # Temporary hack: We're transitioning `forged` from accepting # `yes` to accepting `true` like all of our normal booleans. forged = forged_str is not None and forged_str in ["yes", "true"] client = RequestNotes.get_notes(request).client assert client is not None can_forge_sender = user_profile.can_forge_sender if forged and not can_forge_sender: raise JsonableError(_("User not authorized for this query")) realm = None if realm_str and realm_str != user_profile.realm.string_id: # The realm_str parameter does nothing, because it has to match # the user's realm - but we keep it around for backward compatibility. raise JsonableError(_("User not authorized for this query")) if client.name in [ "zephyr_mirror", "irc_mirror", "jabber_mirror", "JabberMirror" ]: # Here's how security works for mirroring: # # For private messages, the message must be (1) both sent and # received exclusively by users in your realm, and (2) # received by the forwarding user. # # For stream messages, the message must be (1) being forwarded # by an API superuser for your realm and (2) being sent to a # mirrored stream. # # The most important security checks are in # `create_mirrored_message_users` below, which checks the # same-realm constraint. if req_sender is None: raise JsonableError(_("Missing sender")) if message_type_name != "private" and not can_forge_sender: raise JsonableError(_("User not authorized for this query")) # For now, mirroring only works with recipient emails, not for # recipient user IDs. if not all(isinstance(to_item, str) for to_item in message_to): raise JsonableError( _("Mirroring not allowed with recipient user IDs")) # We need this manual cast so that mypy doesn't complain about # create_mirrored_message_users not being able to accept a Sequence[int] # type parameter. message_to = cast(Sequence[str], message_to) try: mirror_sender = create_mirrored_message_users( client, user_profile, message_to, req_sender, message_type_name) except InvalidMirrorInput: raise JsonableError(_("Invalid mirrored message")) if client.name == "zephyr_mirror" and not user_profile.realm.is_zephyr_mirror_realm: raise JsonableError( _("Zephyr mirroring is not allowed in this organization")) sender = mirror_sender else: if req_sender is not None: raise JsonableError(_("Invalid mirrored message")) sender = user_profile if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is None: raise JsonableError( _("Missing deliver_at in a request for delayed message delivery")) if (delivery_type == "send_later" or delivery_type == "remind") and defer_until is not None: deliver_at = handle_deferred_message( sender, client, message_type_name, message_to, topic_name, message_content, delivery_type, defer_until, tz_guess, forwarder_user_profile=user_profile, realm=realm, ) return json_success(request, data={"deliver_at": deliver_at}) ret = check_send_message( sender, client, message_type_name, message_to, topic_name, message_content, forged=forged, forged_timestamp=time, forwarder_user_profile=user_profile, realm=realm, local_id=local_id, sender_queue_id=queue_id, widget_content=widget_content, ) return json_success(request, data={"id": ret})