from typing import Dict, List from django.conf import settings from django.db import transaction from django.db.models import Count from django.utils.translation import gettext as _ from django.utils.translation import override as override_language from zerver.actions.create_realm import setup_realm_internal_bots from zerver.actions.message_send import ( do_send_messages, internal_prep_stream_message_by_name, internal_send_private_message, ) from zerver.actions.reactions import do_add_reaction from zerver.lib.emoji import get_emoji_data from zerver.lib.message import SendMessageRequest from zerver.models import Message, Realm, UserProfile, get_system_bot def missing_any_realm_internal_bots() -> bool: bot_emails = [ bot["email_template"] % (settings.INTERNAL_BOT_DOMAIN,) for bot in settings.REALM_INTERNAL_BOTS ] realm_count = Realm.objects.count() return UserProfile.objects.filter(email__in=bot_emails).values("email").annotate( count=Count("id") ).filter(count=realm_count).count() != len(bot_emails) def create_if_missing_realm_internal_bots() -> None: """This checks if there is any realm internal bot missing. If that is the case, it creates the missing realm internal bots. """ if missing_any_realm_internal_bots(): for realm in Realm.objects.all(): setup_realm_internal_bots(realm) def send_initial_direct_message(user: UserProfile) -> None: # We adjust the initial Welcome Bot direct message for education organizations. education_organization = False if ( user.realm.org_type == Realm.ORG_TYPES["education_nonprofit"]["id"] or user.realm.org_type == Realm.ORG_TYPES["education"]["id"] ): education_organization = True # We need to override the language in this code path, because it's # called from account registration, which is a pre-account API # request and thus may not have the user's language context yet. with override_language(user.default_language): if education_organization: getting_started_help = user.realm.uri + "/help/using-zulip-for-a-class" getting_started_string = ( _( "If you are new to Zulip, check out our [Using Zulip for a class guide]({getting_started_url})!" ) ).format(getting_started_url=getting_started_help) else: getting_started_help = user.realm.uri + "/help/getting-started-with-zulip" getting_started_string = ( _( "If you are new to Zulip, check out our [Getting started guide]({getting_started_url})!" ) ).format(getting_started_url=getting_started_help) organization_setup_string = "" # Add extra content on setting up a new organization for administrators. if user.is_realm_admin: if education_organization: organization_setup_help = user.realm.uri + "/help/setting-up-zulip-for-a-class" organization_setup_string = ( " " + _( "We also have a guide for [Setting up Zulip for a class]({organization_setup_url})." ) ).format(organization_setup_url=organization_setup_help) else: organization_setup_help = ( user.realm.uri + "/help/getting-your-organization-started-with-zulip" ) organization_setup_string = ( " " + _( "We also have a guide for [Setting up your organization]({organization_setup_url})." ) ).format(organization_setup_url=organization_setup_help) demo_organization_warning_string = "" # Add extra content about automatic deletion for demo organization owners. if user.is_realm_owner and user.realm.demo_organization_scheduled_deletion_date is not None: demo_organization_help = user.realm.uri + "/help/demo-organizations" demo_organization_warning_string = ( _( "Note that this is a [demo organization]({demo_organization_help_url}) and will be " "**automatically deleted** in 30 days." ) + "\n\n" ).format(demo_organization_help_url=demo_organization_help) content = "".join( [ _("Hello, and welcome to Zulip!") + "šŸ‘‹" + " ", _("This is a direct message from me, Welcome Bot.") + "\n\n", "{getting_started_text}", "{organization_setup_text}\n\n", "{demo_organization_text}", _( "I can also help you get set up! Just click anywhere on this message or press `r` to reply." ) + "\n\n", _("Here are a few messages I understand:") + " ", bot_commands(), ] ) content = content.format( getting_started_text=getting_started_string, organization_setup_text=organization_setup_string, demo_organization_text=demo_organization_warning_string, ) internal_send_private_message( get_system_bot(settings.WELCOME_BOT, user.realm_id), user, content, # Note: Welcome bot doesn't trigger email/push notifications, # as this is intended to be seen contextually in the application. disable_external_notifications=True, ) def bot_commands(no_help_command: bool = False) -> str: commands = [ "apps", "profile", "theme", "streams", "topics", "message formatting", "keyboard shortcuts", ] if not no_help_command: commands.append("help") return ", ".join(["`" + command + "`" for command in commands]) + "." def select_welcome_bot_response(human_response_lower: str) -> str: # Given the raw (pre-markdown-rendering) content for a private # message from the user to Welcome Bot, select the appropriate reply. if human_response_lower in ["app", "apps"]: return _( "You can [download](/apps/) the [mobile and desktop apps](/apps/). " "Zulip also works great in a browser." ) elif human_response_lower == "profile": return _( "Go to [Profile settings](#settings/profile) " "to add a [profile picture](/help/change-your-profile-picture) " "and edit your [profile information](/help/edit-your-profile)." ) elif human_response_lower == "theme": return _( "Go to [Preferences](#settings/preferences) " "to [switch between the light and dark themes](/help/dark-theme), " "[pick your favorite emoji theme](/help/emoji-and-emoticons#change-your-emoji-set), " "[change your language](/help/change-your-language), " "and make other tweaks to your Zulip experience." ) elif human_response_lower in ["stream", "streams", "channel", "channels"]: return "".join( [ _( "In Zulip, streams [determine who gets a message](/help/streams-and-topics). " "They are similar to channels in other chat apps." ) + "\n\n", _("[Browse and subscribe to streams](#streams/all)."), ] ) elif human_response_lower in ["topic", "topics"]: return "".join( [ _( "In Zulip, topics [tell you what a message is about](/help/streams-and-topics). " "They are light-weight subjects, very similar to the subject line of an email." ) + "\n\n", _( "Check out [Recent conversations](#recent) to see what's happening! " 'You can return to this conversation by clicking "Direct messages" in the upper left.' ), ] ) elif human_response_lower in ["keyboard", "shortcuts", "keyboard shortcuts"]: return "".join( [ _( "Zulip's [keyboard shortcuts](#keyboard-shortcuts) " "let you navigate the app quickly and efficiently." ) + "\n\n", _("Press `?` any time to see a [cheat sheet](#keyboard-shortcuts)."), ] ) elif human_response_lower in ["formatting", "message formatting"]: return "".join( [ _( "Zulip uses [Markdown](/help/format-your-message-using-markdown), " "an intuitive format for **bold**, *italics*, bulleted lists, and more. " "Click [here](#message-formatting) for a cheat sheet." ) + "\n\n", _( "Check out our [messaging tips](/help/messaging-tips) " "to learn about emoji reactions, code blocks and much more!" ), ] ) elif human_response_lower in ["help", "?"]: return "".join( [ _("Here are a few messages I understand:") + " ", bot_commands(no_help_command=True) + "\n\n", _( "Check out our [Getting started guide](/help/getting-started-with-zulip), " "or browse the [Help center](/help/) to learn more!" ), ] ) else: return "".join( [ _( "Iā€™m sorry, I did not understand your message. Please try one of the following commands:" ) + " ", bot_commands(), ] ) def send_welcome_bot_response(send_request: SendMessageRequest) -> None: """Given the send_request object for a direct message from the user to welcome-bot, trigger the welcome-bot reply.""" welcome_bot = get_system_bot(settings.WELCOME_BOT, send_request.message.sender.realm_id) human_response_lower = send_request.message.content.lower() content = select_welcome_bot_response(human_response_lower) internal_send_private_message( welcome_bot, send_request.message.sender, content, # Note: Welcome bot doesn't trigger email/push notifications, # as this is intended to be seen contextually in the application. disable_external_notifications=True, ) @transaction.atomic def send_initial_realm_messages(realm: Realm) -> None: welcome_bot = get_system_bot(settings.WELCOME_BOT, realm.id) # Make sure each stream created in the realm creation process has at least one message below # Order corresponds to the ordering of the streams on the left sidebar, to make the initial Home # view slightly less overwhelming content_of_private_streams_topic = ( _("This is a private stream, as indicated by the lock icon next to the stream name.") + " " + _("Private streams are only visible to stream members.") + "\n" "\n" + _( "To manage this stream, go to [Stream settings]({stream_settings_url}) " "and click on `{initial_private_stream_name}`." ) ).format( stream_settings_url="#streams/subscribed", initial_private_stream_name=Realm.INITIAL_PRIVATE_STREAM_NAME, ) content1_of_topic_demonstration_topic = ( _( "This is a message on stream #**{default_notification_stream_name}** with the " "topic `topic demonstration`." ) ).format(default_notification_stream_name=Realm.DEFAULT_NOTIFICATION_STREAM_NAME) content2_of_topic_demonstration_topic = ( _("Topics are a lightweight tool to keep conversations organized.") + " " + _("You can learn more about topics at [Streams and topics]({about_topics_help_url}).") ).format(about_topics_help_url="/help/streams-and-topics") content_of_swimming_turtles_topic = ( _( "This is a message on stream #**{default_notification_stream_name}** with the " "topic `swimming turtles`." ) + "\n" "\n" "[](/static/images/cute/turtle.png)" "\n" "\n" + _( "[Start a new topic]({start_topic_help_url}) any time you're not replying to a \ previous message." ) ).format( default_notification_stream_name=Realm.DEFAULT_NOTIFICATION_STREAM_NAME, start_topic_help_url="/help/start-a-new-topic", ) welcome_messages: List[Dict[str, str]] = [ { "stream": Realm.INITIAL_PRIVATE_STREAM_NAME, "topic": "private streams", "content": content_of_private_streams_topic, }, { "stream": Realm.DEFAULT_NOTIFICATION_STREAM_NAME, "topic": "topic demonstration", "content": content1_of_topic_demonstration_topic, }, { "stream": Realm.DEFAULT_NOTIFICATION_STREAM_NAME, "topic": "topic demonstration", "content": content2_of_topic_demonstration_topic, }, { "stream": realm.DEFAULT_NOTIFICATION_STREAM_NAME, "topic": "swimming turtles", "content": content_of_swimming_turtles_topic, }, ] messages = [ internal_prep_stream_message_by_name( realm, welcome_bot, message["stream"], message["topic"], message["content"], ) for message in welcome_messages ] message_ids = do_send_messages(messages) # We find the one of our just-sent messages with turtle.png in it, # and react to it. This is a bit hacky, but works and is kinda a # 1-off thing. turtle_message = Message.objects.select_for_update().get( id__in=message_ids, content__icontains="cute/turtle.png" ) emoji_data = get_emoji_data(realm.id, "turtle") do_add_reaction( welcome_bot, turtle_message, "turtle", emoji_data.emoji_code, emoji_data.reaction_type )