def send_change_stream_description_notification( stream: Stream, *, old_description: str, new_description: str, acting_user: UserProfile ) -> None: sender = get_system_bot(settings.NOTIFICATION_BOT, acting_user.realm_id) user_mention = silent_mention_syntax_for_user(acting_user) with override_language(stream.realm.default_language): if new_description == "": new_description = "*" + _("No description.") + "*" if old_description == "": old_description = "*" + _("No description.") + "*" notification_string = ( _("{user} changed the description for this stream.").format(user=user_mention) + "\n\n* **" + _("Old description") + ":**" + f"\n```` quote\n{old_description}\n````\n" + "* **" + _("New description") + ":**" + f"\n```` quote\n{new_description}\n````" ) internal_send_stream_message( sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, notification_string )
def send_zulip(sender: UserProfile, stream: Stream, topic: str, content: str) -> None: internal_send_stream_message( sender, stream, truncate_topic(topic), normalize_body(content), email_gateway=True, )
def send_message_to_signup_notification_stream( sender: UserProfile, realm: Realm, message: str, topic_name: str = _("signups")) -> None: signup_notifications_stream = realm.get_signup_notifications_stream() if signup_notifications_stream is None: return with override_language(realm.default_language): internal_send_stream_message(sender, signup_notifications_stream, topic_name, message)
def send_message_moved_breadcrumbs( user_profile: UserProfile, old_stream: Stream, old_topic: str, old_thread_notification_string: Optional[str], new_stream: Stream, new_topic: Optional[str], new_thread_notification_string: Optional[str], changed_messages_count: int, ) -> None: # Since moving content between streams is highly disruptive, # it's worth adding a couple tombstone messages showing what # happened. sender = get_system_bot(settings.NOTIFICATION_BOT, old_stream.realm_id) if new_topic is None: new_topic = old_topic user_mention = silent_mention_syntax_for_user(user_profile) old_topic_link = f"#**{old_stream.name}>{old_topic}**" new_topic_link = f"#**{new_stream.name}>{new_topic}**" if new_thread_notification_string is not None: with override_language(new_stream.realm.default_language): internal_send_stream_message( sender, new_stream, new_topic, new_thread_notification_string.format( old_location=old_topic_link, user=user_mention, changed_messages_count=changed_messages_count, ), ) if old_thread_notification_string is not None: with override_language(old_stream.realm.default_language): # Send a notification to the old stream that the topic was moved. internal_send_stream_message( sender, old_stream, old_topic, old_thread_notification_string.format( user=user_mention, new_location=new_topic_link, changed_messages_count=changed_messages_count, ), )
def zulip_server_error(report: Dict[str, Any]) -> None: email_subject = "{node}: {message}".format(**report) logger_str = logger_repr(report) user_info = user_info_str(report) deployment = deployment_repr(report) if report["has_request"]: request_repr = """\ Request info: ~~~~ - path: {path} - {method}: {data} """.format( **report ) for field in ["REMOTE_ADDR", "QUERY_STRING", "SERVER_NAME"]: val = report.get(field.lower()) if field == "QUERY_STRING": val = clean_data_from_query_parameters(str(val)) request_repr += f'- {field}: "{val}"\n' request_repr += "~~~~" else: request_repr = "Request info: none" message = f"""{logger_str} Error generated by {user_info} ~~~~ pytb {report['stack_trace']} ~~~~ {deployment} {request_repr}""" error_bot_realm = get_realm(settings.STAFF_SUBDOMAIN) error_bot = get_system_bot(settings.ERROR_BOT, error_bot_realm.id) errors_stream = get_stream("errors", error_bot_realm) internal_send_stream_message( error_bot, errors_stream, format_email_subject(email_subject), message, )
def zulip_browser_error(report: Dict[str, Any]) -> None: email_subject = "JS error: {user_email}".format(**report) user_info = user_info_str(report) body = f"User: {user_info}\n" body += "Message: {message}\n".format(**report) error_bot_realm = get_realm(settings.STAFF_SUBDOMAIN) error_bot = get_system_bot(settings.ERROR_BOT, error_bot_realm.id) errors_stream = get_stream("errors", error_bot_realm) internal_send_stream_message( error_bot, errors_stream, format_email_subject(email_subject), body, )
def notify_new_user(user_profile: UserProfile) -> None: user_count = realm_user_count(user_profile.realm) sender_email = settings.NOTIFICATION_BOT sender = get_system_bot(sender_email, user_profile.realm_id) is_first_user = user_count == 1 if not is_first_user: message = _( "{user} just signed up for Zulip. (total: {user_count})").format( user=silent_mention_syntax_for_user(user_profile), user_count=user_count) if settings.BILLING_ENABLED: from corporate.lib.registration import generate_licenses_low_warning_message_if_required licenses_low_warning_message = generate_licenses_low_warning_message_if_required( user_profile.realm) if licenses_low_warning_message is not None: message += "\n" message += licenses_low_warning_message send_message_to_signup_notification_stream(sender, user_profile.realm, message) # We also send a notification to the Zulip administrative realm admin_realm = get_realm(settings.SYSTEM_BOT_REALM) admin_realm_sender = get_system_bot(sender_email, admin_realm.id) try: # Check whether the stream exists signups_stream = get_signups_stream(admin_realm) # We intentionally use the same strings as above to avoid translation burden. message = _( "{user} just signed up for Zulip. (total: {user_count})").format( user=f"{user_profile.full_name} <`{user_profile.email}`>", user_count=user_count) internal_send_stream_message(admin_realm_sender, signups_stream, user_profile.realm.display_subdomain, message) except Stream.DoesNotExist: # If the signups stream hasn't been created in the admin # realm, don't auto-create it to send to it; just do nothing. pass
def send_change_stream_message_retention_days_notification( user_profile: UserProfile, stream: Stream, old_value: Optional[int], new_value: Optional[int]) -> None: sender = get_system_bot(settings.NOTIFICATION_BOT, user_profile.realm_id) user_mention = silent_mention_syntax_for_user(user_profile) # If switching from or to the organization's default retention policy, # we want to take the realm's default into account. if old_value is None: old_value = stream.realm.message_retention_days if new_value is None: new_value = stream.realm.message_retention_days with override_language(stream.realm.default_language): if old_value == Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP[ "unlimited"]: old_retention_period = _("Forever") new_retention_period = f"{new_value} days" summary_line = f"Messages in this stream will now be automatically deleted {new_value} days after they are sent." elif new_value == Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP[ "unlimited"]: old_retention_period = f"{old_value} days" new_retention_period = _("Forever") summary_line = _( "Messages in this stream will now be retained forever.") else: old_retention_period = f"{old_value} days" new_retention_period = f"{new_value} days" summary_line = f"Messages in this stream will now be automatically deleted {new_value} days after they are sent." notification_string = _( "{user} has changed the [message retention period](/help/message-retention-policy) for this stream:\n" "* **Old retention period**: {old_retention_period}\n" "* **New retention period**: {new_retention_period}\n\n" "{summary_line}") notification_string = notification_string.format( user=user_mention, old_retention_period=old_retention_period, new_retention_period=new_retention_period, summary_line=summary_line, ) internal_send_stream_message(sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, notification_string)
def send_change_stream_post_policy_notification( stream: Stream, *, old_post_policy: int, new_post_policy: int, acting_user: UserProfile) -> None: sender = get_system_bot(settings.NOTIFICATION_BOT, acting_user.realm_id) user_mention = silent_mention_syntax_for_user(acting_user) with override_language(stream.realm.default_language): notification_string = _( "{user} changed the [posting permissions](/help/stream-sending-policy) " "for this stream:\n\n" "* **Old permissions**: {old_policy}.\n" "* **New permissions**: {new_policy}.\n") notification_string = notification_string.format( user=user_mention, old_policy=Stream.POST_POLICIES[old_post_policy], new_policy=Stream.POST_POLICIES[new_post_policy], ) internal_send_stream_message(sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, notification_string)
def send_change_stream_permission_notification( stream: Stream, *, old_policy_name: str, new_policy_name: str, acting_user: UserProfile, ) -> None: sender = get_system_bot(settings.NOTIFICATION_BOT, acting_user.realm_id) user_mention = silent_mention_syntax_for_user(acting_user) with override_language(stream.realm.default_language): notification_string = _( "{user} changed the [access permissions](/help/stream-permissions) " "for this stream from **{old_policy}** to **{new_policy}**.") notification_string = notification_string.format( user=user_mention, old_policy=old_policy_name, new_policy=new_policy_name, ) internal_send_stream_message(sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, notification_string)
def do_rename_stream(stream: Stream, new_name: str, user_profile: UserProfile) -> Dict[str, str]: old_name = stream.name stream.name = new_name stream.save(update_fields=["name"]) RealmAuditLog.objects.create( realm=stream.realm, acting_user=user_profile, modified_stream=stream, event_type=RealmAuditLog.STREAM_NAME_CHANGED, event_time=timezone_now(), extra_data=orjson.dumps( { RealmAuditLog.OLD_VALUE: old_name, RealmAuditLog.NEW_VALUE: new_name, } ).decode(), ) recipient_id = stream.recipient_id messages = Message.objects.filter(recipient_id=recipient_id).only("id") # Update the display recipient and stream, which are easy single # items to set. old_cache_key = get_stream_cache_key(old_name, stream.realm_id) new_cache_key = get_stream_cache_key(stream.name, stream.realm_id) if old_cache_key != new_cache_key: cache_delete(old_cache_key) cache_set(new_cache_key, stream) cache_set(display_recipient_cache_key(recipient_id), stream.name) # Delete cache entries for everything else, which is cheaper and # clearer than trying to set them. display_recipient is the out of # date field in all cases. cache_delete_many(to_dict_cache_key_id(message.id) for message in messages) new_email = encode_email_address(stream, show_sender=True) # We will tell our users to essentially # update stream.name = new_name where name = old_name # and update stream.email = new_email where name = old_name. # We could optimize this by trying to send one message, but the # client code really wants one property update at a time, and # updating stream names is a pretty infrequent operation. # More importantly, we want to key these updates by id, not name, # since id is the immutable primary key, and obviously name is not. data_updates = [ ["email_address", new_email], ["name", new_name], ] for property, value in data_updates: event = dict( op="update", type="stream", property=property, value=value, stream_id=stream.id, name=old_name, ) send_event(stream.realm, event, can_access_stream_user_ids(stream)) sender = get_system_bot(settings.NOTIFICATION_BOT, stream.realm_id) with override_language(stream.realm.default_language): internal_send_stream_message( sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, _("{user_name} renamed stream {old_stream_name} to {new_stream_name}.").format( user_name=silent_mention_syntax_for_user(user_profile), old_stream_name=f"**{old_name}**", new_stream_name=f"**{new_name}**", ), ) # Even though the token doesn't change, the web client needs to update the # email forwarding address to display the correctly-escaped new name. return {"email_address": new_email}
def maybe_send_resolve_topic_notifications( *, user_profile: UserProfile, stream: Stream, old_topic: str, new_topic: str, changed_messages: List[Message], ) -> None: # Note that topics will have already been stripped in check_update_message. # # This logic is designed to treat removing a weird "✔ ✔✔ " # prefix as unresolving the topic. if old_topic.lstrip(RESOLVED_TOPIC_PREFIX) != new_topic.lstrip( RESOLVED_TOPIC_PREFIX): return topic_resolved: bool = new_topic.startswith( RESOLVED_TOPIC_PREFIX ) and not old_topic.startswith(RESOLVED_TOPIC_PREFIX) topic_unresolved: bool = old_topic.startswith( RESOLVED_TOPIC_PREFIX ) and not new_topic.startswith(RESOLVED_TOPIC_PREFIX) if not topic_resolved and not topic_unresolved: # If there's some other weird topic that does not toggle the # state of "topic starts with RESOLVED_TOPIC_PREFIX", we do # nothing. Any other logic could result in cases where we send # these notifications in a non-alternating fashion. # # Note that it is still possible for an individual topic to # have multiple "This topic was marked as resolved" # notifications in a row: one can send new messages to the # pre-resolve topic and then resolve the topic created that # way to get multiple in the resolved topic. And then an # administrator can the messages in between. We consider this # to be a fundamental risk of irresponsible message deletion, # not a bug with the "resolve topics" feature. return # Compute the users who either sent or reacted to messages that # were moved via the "resolve topic' action. Only those users # should be eligible for this message being managed as unread. affected_participant_ids = { message.sender_id for message in changed_messages } | set( Reaction.objects.filter(message__in=changed_messages).values_list( "user_profile_id", flat=True)) sender = get_system_bot(settings.NOTIFICATION_BOT, user_profile.realm_id) user_mention = silent_mention_syntax_for_user(user_profile) with override_language(stream.realm.default_language): if topic_resolved: notification_string = _( "{user} has marked this topic as resolved.") elif topic_unresolved: notification_string = _( "{user} has marked this topic as unresolved.") internal_send_stream_message( sender, stream, new_topic, notification_string.format(user=user_mention, ), limit_unread_user_ids=affected_participant_ids, )
def do_create_realm( string_id: str, name: str, *, emails_restricted_to_domains: Optional[bool] = None, email_address_visibility: Optional[int] = None, description: Optional[str] = None, invite_required: Optional[bool] = None, plan_type: Optional[int] = None, org_type: Optional[int] = None, date_created: Optional[datetime.datetime] = None, is_demo_organization: bool = False, enable_spectator_access: Optional[bool] = None, ) -> Realm: if string_id == settings.SOCIAL_AUTH_SUBDOMAIN: raise AssertionError("Creating a realm on SOCIAL_AUTH_SUBDOMAIN is not allowed!") if Realm.objects.filter(string_id=string_id).exists(): raise AssertionError(f"Realm {string_id} already exists!") if not server_initialized(): logging.info("Server not yet initialized. Creating the internal realm first.") create_internal_realm() kwargs: Dict[str, Any] = {} if emails_restricted_to_domains is not None: kwargs["emails_restricted_to_domains"] = emails_restricted_to_domains if email_address_visibility is not None: kwargs["email_address_visibility"] = email_address_visibility if description is not None: kwargs["description"] = description if invite_required is not None: kwargs["invite_required"] = invite_required if plan_type is not None: kwargs["plan_type"] = plan_type if org_type is not None: kwargs["org_type"] = org_type if enable_spectator_access is not None: kwargs["enable_spectator_access"] = enable_spectator_access if date_created is not None: # The date_created parameter is intended only for use by test # suites that want to backdate the date of a realm's creation. assert not settings.PRODUCTION kwargs["date_created"] = date_created with transaction.atomic(): realm = Realm(string_id=string_id, name=name, **kwargs) if is_demo_organization: realm.demo_organization_scheduled_deletion_date = ( realm.date_created + datetime.timedelta(days=settings.DEMO_ORG_DEADLINE_DAYS) ) set_realm_permissions_based_on_org_type(realm) realm.save() RealmAuditLog.objects.create( # acting_user will be set as the initial realm owner inside # do_create_user(..., realm_creation=True). acting_user=None, realm=realm, event_type=RealmAuditLog.REALM_CREATED, event_time=realm.date_created, ) RealmUserDefault.objects.create(realm=realm) create_system_user_groups_for_realm(realm) # Create stream once Realm object has been saved notifications_stream = ensure_stream( realm, Realm.DEFAULT_NOTIFICATION_STREAM_NAME, stream_description="Everyone is added to this stream by default. Welcome! :octopus:", acting_user=None, ) realm.notifications_stream = notifications_stream # With the current initial streams situation, the only public # stream is the notifications_stream. DefaultStream.objects.create(stream=notifications_stream, realm=realm) signup_notifications_stream = ensure_stream( realm, Realm.INITIAL_PRIVATE_STREAM_NAME, invite_only=True, stream_description="A private stream for core team members.", acting_user=None, ) realm.signup_notifications_stream = signup_notifications_stream realm.save(update_fields=["notifications_stream", "signup_notifications_stream"]) if plan_type is None and settings.BILLING_ENABLED: # We use acting_user=None for setting the initial plan type. do_change_realm_plan_type(realm, Realm.PLAN_TYPE_LIMITED, acting_user=None) admin_realm = get_realm(settings.SYSTEM_BOT_REALM) sender = get_system_bot(settings.NOTIFICATION_BOT, admin_realm.id) # Send a notification to the admin realm signup_message = _("Signups enabled") try: signups_stream = get_signups_stream(admin_realm) topic = realm.display_subdomain internal_send_stream_message( sender, signups_stream, topic, signup_message, ) except Stream.DoesNotExist: # nocoverage # If the signups stream hasn't been created in the admin # realm, don't auto-create it to send to it; just do nothing. pass setup_realm_internal_bots(realm) return realm